@gobing-ai/knowledge-kit 0.0.37 → 0.1.0

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 (157) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/board/collect/index.css +654 -16
  3. package/dist/board/collect/index.js +191 -64
  4. package/dist/board/daily-voice/index.css +679 -12
  5. package/dist/board/daily-voice/index.js +185 -66
  6. package/dist/board/itc/index.css +679 -13
  7. package/dist/board/itc/index.js +188 -66
  8. package/dist/index.js +2864 -139
  9. package/package.json +3 -2
  10. package/plugins/generations/content-gen/dist/index.js +158 -12
  11. package/plugins/generations/core-facts-gen/dist/index.js +158 -12
  12. package/plugins/generations/daily-article-gen/dist/index.js +158 -12
  13. package/plugins/generations/dailynews-gen/dist/index.js +158 -12
  14. package/plugins/generations/episode-plan-gen/dist/index.js +158 -12
  15. package/plugins/generations/gemini-tts/README.md +4 -3
  16. package/plugins/generations/gemini-tts/dist/index.js +158 -12
  17. package/plugins/generations/image-gen/dist/index.js +158 -12
  18. package/plugins/generations/news-report-gen/dist/index.js +158 -12
  19. package/plugins/generations/omni-voice-gen/README.md +15 -4
  20. package/plugins/generations/omni-voice-gen/profiles.json +1 -2
  21. package/plugins/generations/voice-gen/dist/index.js +158 -12
  22. package/plugins/generations/voxcpm-tts/README.md +21 -1
  23. package/plugins/generations/voxcpm-tts/dist/index.js +173 -13
  24. package/plugins/generations/voxcpm-tts/scripts/audit-speaker-consistency.py +109 -0
  25. package/plugins/generations/voxcpm-tts/src/voxcpm-client.ts +28 -1
  26. package/plugins/ingestions/aihot-ingest/dist/index.js +158 -12
  27. package/plugins/ingestions/horizon-ingest/dist/index.js +158 -12
  28. package/plugins/ingestions/last30days-ingest/dist/index.js +158 -12
  29. package/plugins/ingestions/web-search/dist/index.js +158 -12
  30. package/plugins/kk/commands/workflow-run.md +56 -18
  31. package/plugins/kk/decisions/kk-itc.yaml +46 -0
  32. package/plugins/kk/decisions/kk-seo-keyword.yaml +57 -0
  33. package/plugins/kk/decisions/kk-seo-page.yaml +230 -0
  34. package/plugins/kk/decisions/kk-seo-pair.yaml +21 -0
  35. package/plugins/kk/decisions/kk-seo-site.yaml +90 -0
  36. package/plugins/kk/plugin.json +1 -1
  37. package/plugins/kk/publish-presets/chinese-article.json +14 -0
  38. package/plugins/kk/publish-presets/technical-article.json +14 -0
  39. package/plugins/kk/references/smart-seo-attribution.md +85 -0
  40. package/plugins/kk/scripts/daily-stages.ts +42 -2
  41. package/plugins/kk/scripts/itc-stages.ts +25 -118
  42. package/plugins/kk/scripts/kk-workflow-stages.ts +38 -1
  43. package/plugins/kk/scripts/seo-decisions.ts +477 -0
  44. package/plugins/kk/scripts/seo-enrich.ts +728 -0
  45. package/plugins/kk/scripts/seo-facts.ts +610 -0
  46. package/plugins/kk/scripts/seo-fetch.ts +766 -0
  47. package/plugins/kk/scripts/seo-report.ts +460 -0
  48. package/plugins/kk/scripts/seo-rules.ts +1882 -0
  49. package/plugins/kk/scripts/seo-stages.ts +1044 -0
  50. package/plugins/kk/skills/article-adapt/SKILL.md +2 -2
  51. package/plugins/kk/skills/smart-seo/SKILL.md +183 -0
  52. package/plugins/kk/skills/smart-seo/references/method.md +126 -0
  53. package/plugins/kk/skills/smart-seo/references/narrative.md +125 -0
  54. package/plugins/kk/skills/smart-seo/references/operations.md +202 -0
  55. package/plugins/kk/skills/smart-seo/references/seo-geo-sources.md +90 -0
  56. package/plugins/kk/workflows/feature-verification.yaml +28 -24
  57. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +85 -17
  58. package/plugins/kk/workflows/kk-itc-author.yaml +406 -0
  59. package/plugins/kk/workflows/kk-itc.yaml +208 -682
  60. package/plugins/kk/workflows/kk-smart-seo.yaml +251 -0
  61. package/plugins/publishings/bilibili-pub/dist/index.js +21782 -0
  62. package/plugins/publishings/bilibili-pub/dist/profile-check.js +20410 -0
  63. package/plugins/publishings/bilibili-pub/package.json +20 -0
  64. package/plugins/publishings/bilibili-pub/plugin.json +12 -0
  65. package/plugins/publishings/bilibili-pub/src/flow.ts +1296 -0
  66. package/plugins/publishings/bilibili-pub/src/index.ts +139 -0
  67. package/plugins/publishings/bilibili-pub/src/profile-check.ts +19 -0
  68. package/plugins/publishings/bilibili-pub/src/selectors.ts +222 -0
  69. package/plugins/publishings/bilibili-pub/tsconfig.json +4 -0
  70. package/plugins/publishings/cnblogs-pub/dist/index.js +21383 -0
  71. package/plugins/publishings/cnblogs-pub/dist/profile-check.js +20412 -0
  72. package/plugins/publishings/cnblogs-pub/package.json +19 -0
  73. package/plugins/publishings/cnblogs-pub/plugin.json +12 -0
  74. package/plugins/publishings/cnblogs-pub/src/flow.ts +708 -0
  75. package/plugins/publishings/cnblogs-pub/src/index.ts +172 -0
  76. package/plugins/publishings/cnblogs-pub/src/profile-check.ts +24 -0
  77. package/plugins/publishings/cnblogs-pub/src/selectors.ts +104 -0
  78. package/plugins/publishings/cnblogs-pub/tsconfig.json +4 -0
  79. package/plugins/publishings/csdn-pub/dist/index.js +21552 -0
  80. package/plugins/publishings/csdn-pub/dist/profile-check.js +20963 -0
  81. package/plugins/publishings/csdn-pub/package.json +19 -0
  82. package/plugins/publishings/csdn-pub/plugin.json +12 -0
  83. package/plugins/publishings/csdn-pub/src/index.ts +1178 -0
  84. package/plugins/publishings/csdn-pub/src/profile-check.ts +23 -0
  85. package/plugins/publishings/csdn-pub/tsconfig.json +4 -0
  86. package/plugins/publishings/emdash-pub/dist/index.js +158 -12
  87. package/plugins/publishings/emdash-pub/plugin.json +22 -1
  88. package/plugins/publishings/infoq-pub/dist/index.js +160 -12
  89. package/plugins/publishings/infoq-pub/dist/profile-check.js +160 -12
  90. package/plugins/publishings/infoq-pub/plugin.json +21 -0
  91. package/plugins/publishings/jike-pub/dist/index.js +21379 -0
  92. package/plugins/publishings/jike-pub/dist/profile-check.js +20951 -0
  93. package/plugins/publishings/jike-pub/package.json +19 -0
  94. package/plugins/publishings/jike-pub/plugin.json +12 -0
  95. package/plugins/publishings/jike-pub/src/index.ts +800 -0
  96. package/plugins/publishings/jike-pub/src/profile-check.ts +21 -0
  97. package/plugins/publishings/jike-pub/tsconfig.json +4 -0
  98. package/plugins/publishings/juejin-pub/dist/index.js +160 -12
  99. package/plugins/publishings/juejin-pub/dist/profile-check.js +160 -12
  100. package/plugins/publishings/juejin-pub/plugin.json +21 -0
  101. package/plugins/publishings/medium-pub/dist/index.js +158 -12
  102. package/plugins/publishings/medium-pub/plugin.json +22 -1
  103. package/plugins/publishings/podcast-pub/dist/index.js +158 -12
  104. package/plugins/publishings/qiita-pub/dist/index.js +158 -12
  105. package/plugins/publishings/qiita-pub/plugin.json +24 -1
  106. package/plugins/publishings/shipinhao-pub/dist/index.js +21715 -0
  107. package/plugins/publishings/shipinhao-pub/dist/profile-check.js +20431 -0
  108. package/plugins/publishings/shipinhao-pub/package.json +19 -0
  109. package/plugins/publishings/shipinhao-pub/plugin.json +12 -0
  110. package/plugins/publishings/shipinhao-pub/src/flow.ts +1323 -0
  111. package/plugins/publishings/shipinhao-pub/src/index.ts +173 -0
  112. package/plugins/publishings/shipinhao-pub/src/profile-check.ts +25 -0
  113. package/plugins/publishings/shipinhao-pub/src/selectors.ts +241 -0
  114. package/plugins/publishings/shipinhao-pub/tsconfig.json +4 -0
  115. package/plugins/publishings/sspai-pub/dist/index.js +21371 -0
  116. package/plugins/publishings/sspai-pub/dist/profile-check.js +20414 -0
  117. package/plugins/publishings/sspai-pub/package.json +19 -0
  118. package/plugins/publishings/sspai-pub/plugin.json +12 -0
  119. package/plugins/publishings/sspai-pub/src/flow.ts +624 -0
  120. package/plugins/publishings/sspai-pub/src/index.ts +167 -0
  121. package/plugins/publishings/sspai-pub/src/profile-check.ts +25 -0
  122. package/plugins/publishings/sspai-pub/src/selectors.ts +122 -0
  123. package/plugins/publishings/sspai-pub/tsconfig.json +4 -0
  124. package/plugins/publishings/substack-pub/dist/index.js +160 -12
  125. package/plugins/publishings/substack-pub/dist/profile-check.js +160 -12
  126. package/plugins/publishings/substack-pub/plugin.json +21 -0
  127. package/plugins/publishings/surfdash-pub/dist/index.js +158 -12
  128. package/plugins/publishings/surfdash-pub/plugin.json +22 -1
  129. package/plugins/publishings/threads-pub/dist/index.js +158 -12
  130. package/plugins/publishings/wechatmp-pub/dist/index.js +1669 -502
  131. package/plugins/publishings/wechatmp-pub/dist/profile-check.js +204 -21
  132. package/plugins/publishings/wechatmp-pub/plugin.json +32 -0
  133. package/plugins/publishings/wechatmp-pub/src/cgi-draft.ts +540 -0
  134. package/plugins/publishings/wechatmp-pub/src/cgi-media.ts +380 -0
  135. package/plugins/publishings/wechatmp-pub/src/cgi.ts +485 -0
  136. package/plugins/publishings/wechatmp-pub/src/flow.ts +104 -0
  137. package/plugins/publishings/weibo-pub/dist/index.js +160 -12
  138. package/plugins/publishings/weibo-pub/dist/profile-check.js +160 -12
  139. package/plugins/publishings/weibo-pub/plugin.json +35 -0
  140. package/plugins/publishings/x-pub/dist/index.js +160 -12
  141. package/plugins/publishings/x-pub/dist/profile-check.js +160 -12
  142. package/plugins/publishings/x-pub/plugin.json +40 -0
  143. package/plugins/publishings/xhs-pub/dist/index.js +160 -12
  144. package/plugins/publishings/xhs-pub/dist/profile-check.js +160 -12
  145. package/plugins/publishings/xhs-pub/plugin.json +37 -0
  146. package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +21491 -0
  147. package/plugins/publishings/xiaoyuzhou-pub/dist/profile-check.js +20417 -0
  148. package/plugins/publishings/xiaoyuzhou-pub/package.json +19 -0
  149. package/plugins/publishings/xiaoyuzhou-pub/plugin.json +12 -0
  150. package/plugins/publishings/xiaoyuzhou-pub/src/flow.ts +826 -0
  151. package/plugins/publishings/xiaoyuzhou-pub/src/index.ts +174 -0
  152. package/plugins/publishings/xiaoyuzhou-pub/src/profile-check.ts +26 -0
  153. package/plugins/publishings/xiaoyuzhou-pub/src/selectors.ts +125 -0
  154. package/plugins/publishings/xiaoyuzhou-pub/tsconfig.json +4 -0
  155. package/plugins/publishings/zenn-pub/dist/index.js +158 -12
  156. package/plugins/publishings/zenn-pub/plugin.json +22 -1
  157. package/plugins/generations/omni-voice-gen/voices/robin-news.wav +0 -0
@@ -0,0 +1,174 @@
1
+ /**
2
+ * xiaoyuzhou-pub — xiaoyuzhou (podcaster.xiaoyuzhoufm.com) podcast publisher plugin surface (task 0295).
3
+ *
4
+ * This module is the manifest-facing plugin surface: the declared operation list, the pre-browser
5
+ * refusal ladder (operation → driver → publishStatus → format → title → body → audio file →
6
+ * podcastId → duration → cover → unsupported fields — all refused before any browser opens),
7
+ * `getPublishTransport`, the `--in/--out` IO contract and the CLI `main`. The selectors live in
8
+ * `src/selectors.ts`, the browser transport and flow in `src/flow.ts`, and the operator session
9
+ * CLI in `src/profile-check.ts`.
10
+ *
11
+ * Evidence: the recovered extension adapter
12
+ * `.spur/memory/evidence/publish-ready-0288-0297-20261014-01/source/platforms/xiaoyuzhou.js`
13
+ * (83 lines) pins the field semantics cited below (fill xiaoyuzhou.js:23-78, register map
14
+ * xiaoyuzhou.js:83) but is a fill-only adapter — no save protocol, no receipt, and it stops
15
+ * before the publish button (xiaoyuzhou.js:6, 18-19). That button is deliberately never clicked:
16
+ * this slice saves drafts only.
17
+ *
18
+ * publishStatus contract (task 0295 Design freeze): absent defaults to 'draft' and saves through
19
+ * the exact-visible 保存草稿|保存 candidate (provisional, bounded, fail closed); 'public' and any
20
+ * unknown value refuse before the browser — final publication on the studio is not evidenced and
21
+ * not requested by the task AC.
22
+ */
23
+ import { dirname } from 'node:path';
24
+ import { parseArgs } from 'node:util';
25
+ import { type Content, ContentSchema, type Result, ResultSchema } from '@gobing-ai/kk-core';
26
+ import { type BrowserDriver, FakeBrowserTransport, type PublishTransport } from '@gobing-ai/publish-harness';
27
+ import { createNodeFileSystem } from '@gobing-ai/ts-runtime';
28
+ import { echoError } from '@gobing-ai/ts-utils';
29
+ import { bounded, boundedDetail, errorMessage, fail, XiaoyuzhouBrowserTransport } from './flow';
30
+ import { XYZF_TARGET } from './selectors';
31
+
32
+ /**
33
+ * Declared operations (ADR-029 dispatch): the source is a podcast-audio adapter — it runs the
34
+ * same audio fill for every type and notes it is really a podcast audio upload (xiaoyuzhou.js:7).
35
+ */
36
+ export const XYZF_OPERATIONS = ['podcast'] as const;
37
+ export type XiaoyuzhouOperation = (typeof XYZF_OPERATIONS)[number];
38
+
39
+ /**
40
+ * Resolves the requested operation (`options.operation ?? content.metadata.operation ?? default`)
41
+ * BEFORE any transport — and therefore before any browser — is resolved. An undeclared value is
42
+ * refused by name (the source handles podcast audio only, xiaoyuzhou.js:3-7).
43
+ * @param options - Per-call publish options (`operation`).
44
+ * @param content - Content whose `metadata.operation` is the fallback source.
45
+ * @returns The resolved operation, or the refusal message.
46
+ */
47
+ export function resolveOperation(
48
+ options?: Record<string, unknown>,
49
+ content?: Content,
50
+ ): { operation: XiaoyuzhouOperation; error?: undefined } | { operation?: undefined; error: string } {
51
+ const raw = options?.operation ?? content?.metadata?.operation ?? 'podcast';
52
+ if (typeof raw !== 'string' || !(XYZF_OPERATIONS as readonly string[]).includes(raw)) {
53
+ return {
54
+ error: `unsupported operation '${bounded(raw)}' for ${XYZF_TARGET}: expected one of ${XYZF_OPERATIONS.join(', ')}`,
55
+ };
56
+ }
57
+ return { operation: raw as XiaoyuzhouOperation };
58
+ }
59
+
60
+ /**
61
+ * Resolves the driver from `options.driver ?? 'script'`. This channel ships one driver and no
62
+ * `agent` path, so anything else — including a wrong type — is refused by name before a transport
63
+ * is built.
64
+ * @param options - Per-call publish options (`driver`).
65
+ * @returns The resolved driver, or the refusal message.
66
+ */
67
+ function resolveDriver(
68
+ options?: Record<string, unknown>,
69
+ ): { driver: BrowserDriver; error?: undefined } | { driver?: undefined; error: string } {
70
+ const raw = options?.driver ?? 'script';
71
+ if (raw !== 'script') {
72
+ return {
73
+ error: `unsupported driver '${bounded(raw)}' for ${XYZF_TARGET}: expected one of script (the xiaoyuzhou-pub browser flow has no agent path)`,
74
+ };
75
+ }
76
+ return { driver: 'script' };
77
+ }
78
+
79
+ /**
80
+ * Resolves the channel transport: the publish-harness fake under
81
+ * `KNOWLEDGE_KIT_PUBLISH_TRANSPORT=fake` (offline, no profile, no browser), otherwise the real
82
+ * xiaoyuzhou browser transport (`src/flow.ts`). API-only paths never load Playwright eagerly:
83
+ * the harness launcher imports Playwright dynamically at launch time only.
84
+ * @param operation - Resolved operation (`podcast`).
85
+ * @param driver - Resolved driver; only `script` is declared.
86
+ * @returns The publish transport for this channel.
87
+ */
88
+ export function getPublishTransport(
89
+ operation: XiaoyuzhouOperation = 'podcast',
90
+ driver: BrowserDriver = 'script',
91
+ ): PublishTransport {
92
+ if (process.env.KNOWLEDGE_KIT_PUBLISH_TRANSPORT === 'fake') return new FakeBrowserTransport('script');
93
+ return new XiaoyuzhouBrowserTransport(operation, driver);
94
+ }
95
+
96
+ /** Writes the Result to `--out`, creating its directory when the caller named a nested path. */
97
+ async function writePublishResult(outputPath: string, result: Result): Promise<void> {
98
+ const outDir = dirname(outputPath);
99
+ if (outDir && outDir !== '.') await createNodeFileSystem().ensureDir(outDir);
100
+ await createNodeFileSystem().writeFile(outputPath, JSON.stringify(result, null, 2));
101
+ }
102
+
103
+ /**
104
+ * The plugin's `--in/--out` contract: validate the Content, resolve **and refuse** the operation
105
+ * and the driver before any transport exists, then publish through the injected or configured
106
+ * transport (whose own ladder refuses status/format/payload/audio/show/cover problems — also all
107
+ * pre-browser).
108
+ * @param inputPath - Path to the Content JSON.
109
+ * @param outputPath - Path the Result JSON is written to, on success and on refusal.
110
+ * @param transport - Optional transport injection for tests and offline runs.
111
+ */
112
+ export async function processPublishIO(
113
+ inputPath: string,
114
+ outputPath: string,
115
+ transport?: PublishTransport,
116
+ ): Promise<void> {
117
+ await createNodeFileSystem().deleteFile(outputPath);
118
+ let content: Content;
119
+ let options: Record<string, unknown> | undefined;
120
+ try {
121
+ const raw = JSON.parse(await createNodeFileSystem().readFile(inputPath)) as Record<string, unknown>;
122
+ content = ContentSchema.parse(raw);
123
+ if (raw.options !== undefined && typeof raw.options === 'object' && raw.options !== null) {
124
+ options = raw.options as Record<string, unknown>;
125
+ }
126
+ } catch (error: unknown) {
127
+ const message = boundedDetail(`Invalid Content input: ${errorMessage(error)}`);
128
+ await writePublishResult(outputPath, fail(XYZF_TARGET, message));
129
+ throw new Error(message);
130
+ }
131
+ const mode = resolveOperation(options, content);
132
+ if (mode.error !== undefined) {
133
+ await writePublishResult(outputPath, fail(XYZF_TARGET, mode.error));
134
+ throw new Error(mode.error);
135
+ }
136
+ const driverMode = resolveDriver(options);
137
+ if (driverMode.error !== undefined) {
138
+ await writePublishResult(outputPath, fail(XYZF_TARGET, driverMode.error));
139
+ throw new Error(driverMode.error);
140
+ }
141
+ const resolved = transport ?? getPublishTransport(mode.operation, driverMode.driver);
142
+ const result = ResultSchema.parse(await resolved.publish({ content, options }));
143
+ await writePublishResult(outputPath, result);
144
+ if (!result.ok) throw new Error(result.error ?? 'Publish failed');
145
+ }
146
+
147
+ /**
148
+ * CLI entry point: `--in <content.json> --out <result.json>`.
149
+ * @returns Process exit code (0 published, 1 refused or invalid arguments).
150
+ */
151
+ export async function main(): Promise<number> {
152
+ let values: { in?: string; out?: string };
153
+ try {
154
+ ({ values } = parseArgs({ options: { in: { type: 'string' }, out: { type: 'string' } } }));
155
+ } catch (error: unknown) {
156
+ echoError(`Invalid arguments: ${errorMessage(error)}`);
157
+ return 1;
158
+ }
159
+ if (!values.in || !values.out) {
160
+ echoError('Missing required arguments: --in and --out');
161
+ return 1;
162
+ }
163
+ try {
164
+ await processPublishIO(values.in, values.out);
165
+ return 0;
166
+ } catch (error: unknown) {
167
+ echoError(`xiaoyuzhou-pub failed: ${boundedDetail(errorMessage(error))}`);
168
+ return 1;
169
+ }
170
+ }
171
+
172
+ if (import.meta.main) {
173
+ process.exit(await main());
174
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * xiaoyuzhou profile-session capability (task 0295, Chunk A).
3
+ *
4
+ * `login` opens the xiaoyuzhou studio home (https://podcaster.xiaoyuzhoufm.com/, see
5
+ * `src/selectors.ts`) in a headed browser and resolves only once the site readiness predicate
6
+ * passes; `check` reopens the profile (headlessly, unless the operator passes `--headed` or the
7
+ * config sets `checkHeadedOnly`) and reports `verified` or `refused`; `open` just brings the
8
+ * window up. Arg parsing, profile precedence (`--profile` > `XYZF_PROFILE_DIR` >
9
+ * `KK_BROWSER_PROFILE_DIR` > `~/.config/kk/browser-profiles/xiaoyuzhou`) and the exit codes
10
+ * (0 verified, 1 refused, 2 usage error) all live in the shared `runProfileCli`, so the publish
11
+ * preflight and this CLI apply one gate.
12
+ *
13
+ * The readiness predicate observes the site-owned grounded controls (the audio `input#upload`
14
+ * xiaoyuzhou.js:27, whose absence the source reads as maybe-unlogged xiaoyuzhou.js:28, and the
15
+ * title `input[name="title"]` xiaoyuzhou.js:55 — see `src/selectors.ts`); an
16
+ * unsupported-browser page is also not-ready, since the studio officially requires desktop
17
+ * Chrome 147+ (rerun with KK_BROWSER_CHANNEL=chrome). The login-redirect regex is provisional. A
18
+ * selector that never attaches is a refusal (`bun plugins/publishings/xiaoyuzhou-pub/src/profile-check.ts login`
19
+ * is the recovery), never silent success.
20
+ */
21
+ import { runProfileCli } from '@gobing-ai/publish-harness/browser';
22
+ import { XYZF_CONFIG } from './selectors';
23
+
24
+ if (import.meta.main) {
25
+ process.exit(await runProfileCli(XYZF_CONFIG, Bun.argv.slice(2)));
26
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * xiaoyuzhou-pub selectors — every selector/URL/regex constant this channel resolves (task 0295).
3
+ *
4
+ * Grounding: the recovered extension adapter
5
+ * `.spur/memory/evidence/publish-ready-0288-0297-20261014-01/source/platforms/xiaoyuzhou.js` (83
6
+ * lines, verified 2026-10-15 against the snapshot at
7
+ * `~xprojects/knowledge-kit/.spur/run/yijianfa-source/platforms/xiaoyuzhou.js`) pins the studio
8
+ * form controls in its fill (xiaoyuzhou.js:27, 55-56, 65): the audio `input#upload`, the title
9
+ * `input[name="title"]`, and the Draft.js shownotes editor `div[data-contents="true"]`. Its
10
+ * mechanism is UI 填表 (xiaoyuzhou.js:3-6): fileDatas[0] → DataTransfer → `files=` +
11
+ * change/input dispatch (xiaoyuzhou.js:32, 46-50), a bare `.value` title write explicitly flagged
12
+ * uncertain (逆向存疑) in the source itself (xiaoyuzhou.js:57-58), and a text/html simulated paste into
13
+ * the Draft.js editor (xiaoyuzhou.js:72-74). The source never saves (不点发布按钮,
14
+ * xiaoyuzhou.js:6) and marks episode covers unsupported (xiaoyuzhou.js:17).
15
+ * Anything that evidence does not pin is **Provisional** and fails closed when absent — a missing
16
+ * control is a loud typed refusal, never a silent no-op or a broad fallback.
17
+ */
18
+ import { trySelectors } from '@gobing-ai/publish-harness';
19
+ import type { ChannelBrowserConfig } from '@gobing-ai/publish-harness/browser';
20
+ import type { IsAuthenticatedPredicate } from '@gobing-ai/ts-browser-automation';
21
+
22
+ /** Site-owned target slug; also the profile channel key (`XYZF_PROFILE_DIR`). */
23
+ export const XYZF_TARGET = 'xiaoyuzhou';
24
+
25
+ /**
26
+ * Fixed home entry (task 0295 Design freeze): the podcast studio at podcaster.xiaoyuzhoufm.com.
27
+ * Grounded as the display entry only — the recovered xiaoyuzhou.js never navigates (it fills in
28
+ * place inside an already-open studio tab).
29
+ */
30
+ export const XYZF_HOME_URL = 'https://podcaster.xiaoyuzhoufm.com/';
31
+
32
+ /**
33
+ * Login/passport redirect marker. **Provisional**: xiaoyuzhou.js never observes a redirect URL;
34
+ * it only infers logged-out state from the missing audio input (xiaoyuzhou.js:28
35
+ * 「可能未登录」). The readiness predicate below carries the real gate.
36
+ */
37
+ export const XYZF_LOGIN_URL_RE = /(passport\.xiaoyuzhoufm\.com|\/(signin|login))/;
38
+
39
+ /**
40
+ * Studio unsupported-browser page. **Provisional shape, official requirement**: the 小宇宙
41
+ * podcaster studio is documented to require desktop Chrome 147+ and serves an
42
+ * `unsupported-browser` page otherwise (observed bundled Chromium 155.0.8059.40 as
43
+ * observed-only, never as a support claim). Hitting this page is an environment failure BEFORE
44
+ * any upload: the operator runs the channel with `KK_BROWSER_CHANNEL=chrome` (the existing
45
+ * `kkBrowserLauncher` reads it; no Playwright pin change).
46
+ */
47
+ export const XYZF_UNSUPPORTED_URL_RE = /unsupported-browser/;
48
+
49
+ /**
50
+ * Studio form selectors. `audioInput`, `titleInput` and `shownotesEditor` are **Grounded**
51
+ * (xiaoyuzhou.js:27, 55-56, 65); `coverInput` and `saveButton` are **Provisional** bounded
52
+ * candidates from the task 0295 Design freeze — fail closed when absent, no broad fallback.
53
+ */
54
+ export const XYZF_AUDIO_SELECTORS = {
55
+ /** Audio file input. **Grounded** (xiaoyuzhou.js:27); its absence there reads as maybe-unlogged (xiaoyuzhou.js:28). */
56
+ audioInput: ['input#upload'],
57
+ /** Title input. **Grounded** (xiaoyuzhou.js:55-56). */
58
+ titleInput: ['input[name="title"]'],
59
+ /** Draft.js shownotes editor. **Grounded** (xiaoyuzhou.js:65); missing there is a typed shownotes failure (xiaoyuzhou.js:66-70). */
60
+ shownotesEditor: ['div[data-contents="true"]'],
61
+ /**
62
+ * Cover file control, scoped to the 上传封面/单集封面 section's file input. **Provisional**:
63
+ * the vendor marks episode covers unsupported (xiaoyuzhou.js:17) and pins no selector, so
64
+ * this stays a bounded semantic candidate — never a pretend-provenance claim and never a
65
+ * silent skip; a missing control is a typed cover failure.
66
+ */
67
+ coverInput: ['form:has(div[data-contents="true"]) input[type="file"]:not(#upload)'],
68
+ /**
69
+ * The exact-visible draft-save control(s). **Provisional** (task 0295 Design freeze; the
70
+ * source never saves — xiaoyuzhou.js:6, 18-19); `text-is` exact match, most specific wording
71
+ * first, no other fallback. Absence is a typed draft-save failure, never a manufactured draft.
72
+ */
73
+ saveButton: ['button:text-is("保存草稿")', 'button:text-is("保存")'],
74
+ } as const;
75
+
76
+ /** Keys of the selector map, for typed probes. */
77
+ export type XiaoyuzhouAudioSelectorKey = keyof typeof XYZF_AUDIO_SELECTORS;
78
+
79
+ /**
80
+ * Persisted draft receipt URL shapes. **Provisional**: xiaoyuzhou.js is a fill-only adapter and
81
+ * never reads a receipt (it stops before publish, xiaoyuzhou.js:6, 18-19); these are the
82
+ * episode/podcast id-bearing shapes the task 0295 Design freeze names. Only shapes in this list
83
+ * count as receipts, and only when they were not on screen before the save (fresh-signal rule) —
84
+ * a toast alone never confirms, and disagreeing fresh ids are an ambiguous typed failure.
85
+ */
86
+ export const XYZF_RECEIPT_URL_RES = [
87
+ /podcaster\.xiaoyuzhoufm\.com\/episode\/[0-9a-zA-Z]+/,
88
+ /xiaoyuzhoufm\.com\/episode\/[0-9a-zA-Z]+/,
89
+ /podcaster\.xiaoyuzhoufm\.com\/podcast\/[0-9a-zA-Z]+/,
90
+ ] as const;
91
+
92
+ /** Upload/processing ceiling for the audio step (media ceiling — large local files, task 0295 freeze). */
93
+ export const XYZF_UPLOAD_TIMEOUT_MS = 600_000;
94
+
95
+ /** Login deadline for the operator CLI and the session gate (shared harness default). */
96
+ export const XYZF_LOGIN_TIMEOUT_MS = 180_000;
97
+
98
+ /**
99
+ * Xiaoyuzhou-owned readiness predicate: no login redirect, no unsupported-browser environment
100
+ * page, and the grounded audio input (xiaoyuzhou.js:27) plus title input (xiaoyuzhou.js:55)
101
+ * attached — xiaoyuzhou.js:28 makes the missing audio input the maybe-unlogged marker. Shared by
102
+ * the publish preflight and the operator profile CLI, so both apply one gate. The login-redirect
103
+ * regex is provisional (see `XYZF_LOGIN_URL_RE`); the grounded-control probes carry the real gate.
104
+ */
105
+ export const isXiaoyuzhouAuthenticated: IsAuthenticatedPredicate = async (page) => {
106
+ if (XYZF_UNSUPPORTED_URL_RE.test(page.url())) return false;
107
+ if (XYZF_LOGIN_URL_RE.test(page.url())) return false;
108
+ const audio = await trySelectors(page, XYZF_AUDIO_SELECTORS.audioInput, { visible: false });
109
+ if (!audio.found) return false;
110
+ return (await trySelectors(page, XYZF_AUDIO_SELECTORS.titleInput, { visible: false })).found;
111
+ };
112
+
113
+ /**
114
+ * Site-owned config for the shared session check and operator CLI (ADR-032). The check opens the
115
+ * studio home (`XYZF_HOME_URL`); the manifest `browserSession.homeUrl` is the same entry. The
116
+ * publish preflight never forwards `checkHeadedOnly`: it is the operator CLI's setting, and a
117
+ * refusal must never escalate to a headed window on its own.
118
+ */
119
+ export const XYZF_CONFIG: ChannelBrowserConfig = {
120
+ channel: XYZF_TARGET,
121
+ homeUrl: XYZF_HOME_URL,
122
+ isAuthenticated: isXiaoyuzhouAuthenticated,
123
+ loginTimeoutMs: XYZF_LOGIN_TIMEOUT_MS,
124
+ checkHeadedOnly: false,
125
+ };
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "../../../tooling/typescript/base.json",
3
+ "include": ["src"]
4
+ }
@@ -11985,6 +11985,83 @@ import { homedir } from "os";
11985
11985
  import { dirname as dirname2, join } from "path";
11986
11986
  import { parseArgs } from "util";
11987
11987
 
11988
+ // ../../packages/kk-core/src/content-limits.ts
11989
+ var PLATFORM_CONTENT_LIMITS = Object.freeze({
11990
+ schemaVersion: 5,
11991
+ tagLimits: Object.freeze({
11992
+ default: 7,
11993
+ imageText: Object.freeze({
11994
+ instagram: 5,
11995
+ douyin: 5,
11996
+ kuaishou: 4,
11997
+ weixin: 1,
11998
+ xiaohongshu: 10
11999
+ }),
12000
+ shortVideo: Object.freeze({
12001
+ douyin: 5,
12002
+ kuaishou: 4,
12003
+ xiaohongshu: 10,
12004
+ bilibili: 10,
12005
+ zhihu: 5,
12006
+ tiktok: 20,
12007
+ youtube: 60
12008
+ })
12009
+ }),
12010
+ titleCapabilities: Object.freeze({
12011
+ dynamic: Object.freeze({
12012
+ bilibili: Object.freeze({ max: 20, unit: "utf16_code_units" }),
12013
+ zhihu: Object.freeze({ max: 50, unit: "utf16_code_units" }),
12014
+ douban: Object.freeze({ max: 70, unit: "utf16_code_units" }),
12015
+ x: null,
12016
+ threads: null,
12017
+ weibo: null,
12018
+ jike: null,
12019
+ zsxq: null,
12020
+ toutiao: null,
12021
+ juejin: null
12022
+ }),
12023
+ imageText: Object.freeze({
12024
+ xiaohongshu: Object.freeze({ max: 20, unit: "xiaohongshu_title_weighted" }),
12025
+ weixin: Object.freeze({ max: 20, unit: "utf16_code_units" }),
12026
+ douyin: Object.freeze({ max: 20, unit: "utf16_code_units" }),
12027
+ shipinhao: Object.freeze({ max: 22, unit: "utf16_code_units" }),
12028
+ reddit: Object.freeze({ max: 300, unit: "utf16_code_units" })
12029
+ }),
12030
+ article: Object.freeze({
12031
+ juejin: Object.freeze({ max: 100, unit: "utf16_code_units" }),
12032
+ csdn: Object.freeze({ max: 100, unit: "utf16_code_units" }),
12033
+ cnblogs: Object.freeze({ max: 128, unit: "utf16_code_units" }),
12034
+ weibo: Object.freeze({ max: 30, unit: "utf16_code_units" }),
12035
+ sspai: Object.freeze({ max: 80, unit: "utf16_code_units" })
12036
+ }),
12037
+ shortVideo: Object.freeze({
12038
+ shipinhao: Object.freeze({ max: 16, unit: "utf16_code_units" }),
12039
+ douyin: Object.freeze({ max: 30, unit: "utf16_code_units" }),
12040
+ bilibili: Object.freeze({ max: 80, unit: "utf16_code_units" })
12041
+ })
12042
+ }),
12043
+ bodyCapabilities: Object.freeze({
12044
+ dynamic: Object.freeze({
12045
+ threads: Object.freeze({ max: 500, unit: "unicode_code_points", behavior: "split_thread" }),
12046
+ x: Object.freeze({ max: 280, unit: "weighted", behavior: "split_thread" }),
12047
+ weibo: Object.freeze({ max: 2000, unit: "unicode_code_points" }),
12048
+ jike: Object.freeze({ max: 1000, unit: "unicode_code_points" })
12049
+ }),
12050
+ imageText: Object.freeze({
12051
+ xiaohongshu: Object.freeze({ max: 1000, unit: "unicode_code_points" }),
12052
+ douyin: Object.freeze({ max: 1000, unit: "unicode_code_points" })
12053
+ }),
12054
+ article: Object.freeze({
12055
+ wechatmp: Object.freeze({ max: 50000, unit: "unicode_code_points" }),
12056
+ juejin: Object.freeze({ max: 50000, unit: "unicode_code_points" })
12057
+ }),
12058
+ shortVideo: Object.freeze({
12059
+ douyin: Object.freeze({ max: 1000, unit: "unicode_code_points" }),
12060
+ kuaishou: Object.freeze({ max: 1000, unit: "unicode_code_points" }),
12061
+ shipinhao: Object.freeze({ max: 1000, unit: "unicode_code_points" })
12062
+ })
12063
+ })
12064
+ });
11988
12065
  // ../../packages/kk-core/src/kinds.ts
11989
12066
  init_zod();
11990
12067
 
@@ -12075,6 +12152,67 @@ var BrowserSessionDescriptorSchema = object({
12075
12152
  }
12076
12153
  }).optional()
12077
12154
  }).strict();
12155
+ var ContentCountUnitSchema = _enum([
12156
+ "utf16_code_units",
12157
+ "unicode_code_points",
12158
+ "weighted",
12159
+ "xiaohongshu_title_weighted"
12160
+ ]);
12161
+ var LimitEntrySchema = object({
12162
+ max: number2().int().positive(),
12163
+ unit: ContentCountUnitSchema
12164
+ }).strict();
12165
+ var PublishingProfileLimitsSchema = object({
12166
+ title: LimitEntrySchema.optional(),
12167
+ body: LimitEntrySchema.optional()
12168
+ }).strict();
12169
+ var PublishingProfileRequiresSchema = object({
12170
+ cover: boolean2().optional(),
12171
+ orderedImages: boolean2().optional()
12172
+ }).strict();
12173
+ var PublishingProfileModeBindingSchema = object({
12174
+ metadata: record(string2(), unknown())
12175
+ }).strict();
12176
+ var PublishingProfileModesSchema = object({
12177
+ draft: PublishingProfileModeBindingSchema.optional(),
12178
+ live: PublishingProfileModeBindingSchema.optional()
12179
+ }).strict().superRefine((modes, ctx) => {
12180
+ if (modes.draft === undefined && modes.live === undefined) {
12181
+ ctx.addIssue({
12182
+ code: "custom",
12183
+ message: 'modes must define at least one of "draft" or "live"'
12184
+ });
12185
+ }
12186
+ });
12187
+ var PublishingProfileSchema = object({
12188
+ format: string2().min(1),
12189
+ locale: string2().regex(/^(source|[a-z]{2,3}(-[A-Za-z0-9]+)*)$/, {
12190
+ message: 'locale must be "source" or a valid language tag'
12191
+ }),
12192
+ limits: PublishingProfileLimitsSchema.optional(),
12193
+ requires: PublishingProfileRequiresSchema.optional(),
12194
+ modes: PublishingProfileModesSchema
12195
+ }).strict();
12196
+ var PublishingCapabilitiesSchema = object({
12197
+ defaultProfile: string2().min(1),
12198
+ profiles: record(string2().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/), PublishingProfileSchema)
12199
+ }).strict().superRefine((data, ctx) => {
12200
+ const keys = Object.keys(data.profiles);
12201
+ if (keys.length === 0) {
12202
+ ctx.addIssue({
12203
+ code: "custom",
12204
+ path: ["profiles"],
12205
+ message: "profiles record must be nonempty"
12206
+ });
12207
+ }
12208
+ if (!(data.defaultProfile in data.profiles)) {
12209
+ ctx.addIssue({
12210
+ code: "custom",
12211
+ path: ["defaultProfile"],
12212
+ message: `defaultProfile "${data.defaultProfile}" must reference a declared profile`
12213
+ });
12214
+ }
12215
+ });
12078
12216
  var PluginManifestSchemaBase = looseObject({
12079
12217
  name: string2().min(1),
12080
12218
  kind: PluginKindSchema,
@@ -12083,23 +12221,31 @@ var PluginManifestSchemaBase = looseObject({
12083
12221
  version: string2().optional(),
12084
12222
  description: string2().optional(),
12085
12223
  operations: record(string2(), PluginOperationSchema).optional(),
12086
- browserSession: BrowserSessionDescriptorSchema.optional()
12224
+ browserSession: BrowserSessionDescriptorSchema.optional(),
12225
+ publishing: PublishingCapabilitiesSchema.optional()
12087
12226
  });
12088
12227
  var PluginManifestSchema = PluginManifestSchemaBase.superRefine((manifest, ctx) => {
12089
- if (manifest.browserSession === undefined)
12090
- return;
12091
- if (manifest.kind !== "publish") {
12092
- ctx.addIssue({
12093
- code: "custom",
12094
- path: ["browserSession"],
12095
- message: `browserSession requires kind "publish", not "${manifest.kind}"`
12096
- });
12228
+ if (manifest.browserSession !== undefined) {
12229
+ if (manifest.kind !== "publish") {
12230
+ ctx.addIssue({
12231
+ code: "custom",
12232
+ path: ["browserSession"],
12233
+ message: `browserSession requires kind "publish", not "${manifest.kind}"`
12234
+ });
12235
+ }
12236
+ if (manifest.check === undefined) {
12237
+ ctx.addIssue({
12238
+ code: "custom",
12239
+ path: ["browserSession"],
12240
+ message: "browserSession requires a check probe entry that owns the session routines"
12241
+ });
12242
+ }
12097
12243
  }
12098
- if (manifest.check === undefined) {
12244
+ if (manifest.publishing !== undefined && manifest.kind !== "publish") {
12099
12245
  ctx.addIssue({
12100
12246
  code: "custom",
12101
- path: ["browserSession"],
12102
- message: "browserSession requires a check probe entry that owns the session routines"
12247
+ path: ["publishing"],
12248
+ message: `publishing requires kind "publish", not "${manifest.kind}"`
12103
12249
  });
12104
12250
  }
12105
12251
  });
@@ -3,5 +3,26 @@
3
3
  "kind": "publish",
4
4
  "entry": "./dist/index.js",
5
5
  "version": "1.0.0",
6
- "description": "Publish plugin for Zenn (zenn.dev) invoking Zenn CLI via FileCli transport"
6
+ "description": "Publish plugin for Zenn (zenn.dev) invoking Zenn CLI via FileCli transport",
7
+ "publishing": {
8
+ "defaultProfile": "article",
9
+ "profiles": {
10
+ "article": {
11
+ "format": "article",
12
+ "locale": "ja",
13
+ "modes": {
14
+ "draft": {
15
+ "metadata": {
16
+ "published": false
17
+ }
18
+ },
19
+ "live": {
20
+ "metadata": {
21
+ "published": true
22
+ }
23
+ }
24
+ }
25
+ }
26
+ }
27
+ }
7
28
  }