@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,167 @@
1
+ /**
2
+ * sspai-pub — sspai (sspai.com) Matrix-column article publisher plugin surface (task 0294).
3
+ *
4
+ * This module is the manifest-facing plugin surface: the declared operation list, the pre-browser
5
+ * refusal ladder (operation → driver → publishStatus → title → body → unsupported fields → cover
6
+ * file — all refused before any browser opens), `getPublishTransport`, the `--in/--out` IO
7
+ * contract and the CLI `main`. The selectors live in `src/selectors.ts`, the browser transport
8
+ * and flow in `src/flow.ts`, and the operator session CLI in `src/profile-check.ts`.
9
+ *
10
+ * Evidence: the recovered extension adapter
11
+ * `.spur/memory/evidence/publish-ready-0288-0297-20261014-01/source/platforms/sspai.js` pins the
12
+ * field semantics cited below (fill sspai.js:23-77, register map sspai.js:82) but is a fill-only
13
+ * adapter — no save protocol, no receipt, and it stops at the preview control (sspai.js:15-16,
14
+ * 68-76). That preview is deliberately never clicked: this slice saves drafts only.
15
+ *
16
+ * publishStatus contract (task 0294 Design freeze): absent defaults to 'draft' and saves through
17
+ * the exact-visible 保存草稿|保存 candidate (provisional, bounded, fail closed); 'public' and any
18
+ * unknown value refuse before the browser — final publication/submission on sspai is not
19
+ * evidenced and not requested by the task AC.
20
+ */
21
+ import { dirname } from 'node:path';
22
+ import { parseArgs } from 'node:util';
23
+ import { type Content, ContentSchema, type Result, ResultSchema } from '@gobing-ai/kk-core';
24
+ import { type BrowserDriver, FakeBrowserTransport, type PublishTransport } from '@gobing-ai/publish-harness';
25
+ import { createNodeFileSystem } from '@gobing-ai/ts-runtime';
26
+ import { echoError } from '@gobing-ai/ts-utils';
27
+ import { bounded, boundedDetail, errorMessage, fail, SspaiBrowserTransport } from './flow';
28
+ import { SSPAI_TARGET } from './selectors';
29
+
30
+ /** Declared operations (ADR-029 dispatch): the source handles article only (sspai.js:24). */
31
+ export const SSPAI_OPERATIONS = ['article'] as const;
32
+ export type SspaiOperation = (typeof SSPAI_OPERATIONS)[number];
33
+
34
+ /**
35
+ * Resolves the requested operation. An undeclared value is refused by name, before any transport —
36
+ * and therefore before any browser — is resolved (the source handles article only, sspai.js:24).
37
+ * @param options - Per-call publish options (`operation`).
38
+ * @param content - Content whose `metadata.operation` is the fallback source.
39
+ * @returns The resolved operation, or the refusal message.
40
+ */
41
+ export function resolveOperation(
42
+ options?: Record<string, unknown>,
43
+ content?: Content,
44
+ ): { operation: SspaiOperation; error?: undefined } | { operation?: undefined; error: string } {
45
+ const raw = options?.operation ?? content?.metadata?.operation ?? 'article';
46
+ if (typeof raw !== 'string' || !(SSPAI_OPERATIONS as readonly string[]).includes(raw)) {
47
+ return {
48
+ error: `unsupported operation '${bounded(raw)}' for ${SSPAI_TARGET}: expected one of ${SSPAI_OPERATIONS.join(', ')}`,
49
+ };
50
+ }
51
+ return { operation: raw as SspaiOperation };
52
+ }
53
+
54
+ /**
55
+ * Resolves the driver from `options.driver ?? 'script'`. This channel ships one driver and no
56
+ * `agent` path, so anything else — including a wrong type — is refused by name before a transport
57
+ * is built.
58
+ * @param options - Per-call publish options (`driver`).
59
+ * @returns The resolved driver, or the refusal message.
60
+ */
61
+ function resolveDriver(
62
+ options?: Record<string, unknown>,
63
+ ): { driver: BrowserDriver; error?: undefined } | { driver?: undefined; error: string } {
64
+ const raw = options?.driver ?? 'script';
65
+ if (raw !== 'script') {
66
+ return {
67
+ error: `unsupported driver '${bounded(raw)}' for ${SSPAI_TARGET}: expected one of script (the sspai-pub browser flow has no agent path)`,
68
+ };
69
+ }
70
+ return { driver: 'script' };
71
+ }
72
+
73
+ /**
74
+ * Resolves the channel transport: the publish-harness fake under
75
+ * `KNOWLEDGE_KIT_PUBLISH_TRANSPORT=fake` (offline, no profile, no browser), otherwise the real
76
+ * sspai browser transport (`src/flow.ts`).
77
+ * @param operation - Resolved operation (`article`).
78
+ * @param driver - Resolved driver; only `script` is declared.
79
+ * @returns The publish transport for this channel.
80
+ */
81
+ export function getPublishTransport(
82
+ operation: SspaiOperation = 'article',
83
+ driver: BrowserDriver = 'script',
84
+ ): PublishTransport {
85
+ if (process.env.KNOWLEDGE_KIT_PUBLISH_TRANSPORT === 'fake') return new FakeBrowserTransport('script');
86
+ return new SspaiBrowserTransport(operation, driver);
87
+ }
88
+
89
+ /** Writes the Result to `--out`, creating its directory when the caller named a nested path. */
90
+ async function writePublishResult(outputPath: string, result: Result): Promise<void> {
91
+ const outDir = dirname(outputPath);
92
+ if (outDir && outDir !== '.') await createNodeFileSystem().ensureDir(outDir);
93
+ await createNodeFileSystem().writeFile(outputPath, JSON.stringify(result, null, 2));
94
+ }
95
+
96
+ /**
97
+ * The plugin's `--in/--out` contract: validate the Content, resolve **and refuse** the operation
98
+ * and the driver before any transport exists, then publish through the injected or configured
99
+ * transport (whose own ladder refuses status/payload/unsupported-field/cover problems — also all
100
+ * pre-browser).
101
+ * @param inputPath - Path to the Content JSON.
102
+ * @param outputPath - Path the Result JSON is written to, on success and on refusal.
103
+ * @param transport - Optional transport injection for tests and offline runs.
104
+ */
105
+ export async function processPublishIO(
106
+ inputPath: string,
107
+ outputPath: string,
108
+ transport?: PublishTransport,
109
+ ): Promise<void> {
110
+ await createNodeFileSystem().deleteFile(outputPath);
111
+ let content: Content;
112
+ let options: Record<string, unknown> | undefined;
113
+ try {
114
+ const raw = JSON.parse(await createNodeFileSystem().readFile(inputPath)) as Record<string, unknown>;
115
+ content = ContentSchema.parse(raw);
116
+ if (raw.options !== undefined && typeof raw.options === 'object' && raw.options !== null) {
117
+ options = raw.options as Record<string, unknown>;
118
+ }
119
+ } catch (error: unknown) {
120
+ const message = boundedDetail(`Invalid Content input: ${errorMessage(error)}`);
121
+ await writePublishResult(outputPath, fail(SSPAI_TARGET, message));
122
+ throw new Error(message);
123
+ }
124
+ const mode = resolveOperation(options, content);
125
+ if (mode.error !== undefined) {
126
+ await writePublishResult(outputPath, fail(SSPAI_TARGET, mode.error));
127
+ throw new Error(mode.error);
128
+ }
129
+ const driverMode = resolveDriver(options);
130
+ if (driverMode.error !== undefined) {
131
+ await writePublishResult(outputPath, fail(SSPAI_TARGET, driverMode.error));
132
+ throw new Error(driverMode.error);
133
+ }
134
+ const resolved = transport ?? getPublishTransport(mode.operation, driverMode.driver);
135
+ const result = ResultSchema.parse(await resolved.publish({ content, options }));
136
+ await writePublishResult(outputPath, result);
137
+ if (!result.ok) throw new Error(result.error ?? 'Publish failed');
138
+ }
139
+
140
+ /**
141
+ * CLI entry point: `--in <content.json> --out <result.json>`.
142
+ * @returns Process exit code (0 published, 1 refused or invalid arguments).
143
+ */
144
+ export async function main(): Promise<number> {
145
+ let values: { in?: string; out?: string };
146
+ try {
147
+ ({ values } = parseArgs({ options: { in: { type: 'string' }, out: { type: 'string' } } }));
148
+ } catch (error: unknown) {
149
+ echoError(`Invalid arguments: ${errorMessage(error)}`);
150
+ return 1;
151
+ }
152
+ if (!values.in || !values.out) {
153
+ echoError('Missing required arguments: --in and --out');
154
+ return 1;
155
+ }
156
+ try {
157
+ await processPublishIO(values.in, values.out);
158
+ return 0;
159
+ } catch (error: unknown) {
160
+ echoError(`sspai-pub failed: ${boundedDetail(errorMessage(error))}`);
161
+ return 1;
162
+ }
163
+ }
164
+
165
+ if (import.meta.main) {
166
+ process.exit(await main());
167
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * sspai profile-session capability (task 0294, Chunk A).
3
+ *
4
+ * `login` opens the sspai article-editor entry (https://sspai.com/post/write, provisional — 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` > `SSPAI_PROFILE_DIR` >
9
+ * `KK_BROWSER_PROFILE_DIR` > `~/.config/kk/browser-profiles/sspai`) 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 editor controls (the grounded
14
+ * `textarea[placeholder="请输入标题..."]` sspai.js:26 and `div[contenteditable="true"]`
15
+ * sspai.js:59, whose absence sspai.js:60 reads as logged-out — see `src/selectors.ts`); the
16
+ * login-redirect regex is provisional. A selector that never attaches is a refusal
17
+ * (`bun plugins/publishings/sspai-pub/src/profile-check.ts login` is the recovery), never
18
+ * silent success.
19
+ */
20
+ import { runProfileCli } from '@gobing-ai/publish-harness/browser';
21
+ import { SSPAI_CONFIG } from './selectors';
22
+
23
+ if (import.meta.main) {
24
+ process.exit(await runProfileCli(SSPAI_CONFIG, Bun.argv.slice(2)));
25
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * sspai-pub selectors — every selector/URL/regex constant this channel resolves (task 0294).
3
+ *
4
+ * Grounding: the recovered extension adapter
5
+ * `.spur/memory/evidence/publish-ready-0288-0297-20261014-01/source/platforms/sspai.js` (82 lines)
6
+ * pins the editor controls in its fill (sspai.js:26, 32, 50, 59) and its register map
7
+ * (sspai.js:82): `textarea[placeholder="请输入标题..."]`, the first
8
+ * `div[contenteditable="true"]`, and the page's first `input[type="file"]`. Its mechanism is UI
9
+ * 填表 (sspai.js:5): title textarea value + contenteditable simulated `text/html` paste
10
+ * (sspai.js:62-65) + cover file-input change dispatch (sspai.js:38-40); `a.editor-extra-button-preview`
11
+ * (sspai.js:68) is a PREVIEW control only (sspai.js:15-16) and never satisfies a save.
12
+ * Anything that evidence does not pin is **Provisional** and fails closed when absent — a missing
13
+ * control is a loud typed refusal, never a silent no-op or a broad fallback.
14
+ */
15
+ import { trySelectors } from '@gobing-ai/publish-harness';
16
+ import type { ChannelBrowserConfig } from '@gobing-ai/publish-harness/browser';
17
+ import type { IsAuthenticatedPredicate } from '@gobing-ai/ts-browser-automation';
18
+
19
+ /** Site-owned target slug; also the profile channel key (`SSPAI_PROFILE_DIR`). */
20
+ export const SSPAI_TARGET = 'sspai';
21
+
22
+ /**
23
+ * Fixed home entry (task 0294 Design freeze). Grounded as the display entry only — the recovered
24
+ * sspai.js never navigates (it runs inside an already-open editor tab), so no URL is pinned by it.
25
+ */
26
+ export const SSPAI_HOME_URL = 'https://sspai.com/';
27
+
28
+ /**
29
+ * Article-editor entry where the flow composes. **Provisional**: sspai.js never navigates
30
+ * (sspai.js fills in place only), and the current editor route is unpinned; `/post/write` is the
31
+ * historically documented entry (archived official guide, https://sspai.com/post/37790 — draft
32
+ * saving is documented there historically only, not as current account UI). Every navigation to
33
+ * this URL fails closed behind the readiness predicate below.
34
+ */
35
+ export const SSPAI_EDIT_URL = 'https://sspai.com/post/write';
36
+
37
+ /**
38
+ * Login/passport redirect marker. **Provisional**: sspai.js never observes a redirect URL; it
39
+ * only infers logged-out state from the missing editor (sspai.js:60 「可能未登录」).
40
+ * account.sspai.com is sspai's known SSO entry. The readiness predicate below carries the real
41
+ * gate — a page body merely mentioning 登录 is never sufficient.
42
+ */
43
+ export const SSPAI_LOGIN_URL_RE = /(account\.sspai\.com|\/(signin|login))/;
44
+
45
+ /**
46
+ * Article-editor selectors. `titleInput`, `bodyEditor`, `coverInput` and `previewButton` are
47
+ * **Grounded** (sspai.js:26, 50; 59; 32; 68 and the sspai.js:82 register map); `draftSave` is a
48
+ * **Provisional** bounded candidate from the task 0294 Design freeze — exact visible
49
+ * 保存草稿|保存, ordered resolution, no broad primary-button fallback, fail closed when absent.
50
+ */
51
+ export const SSPAI_ARTICLE_SELECTORS = {
52
+ /** Title textarea. **Grounded** (sspai.js:26, 50, 82). */
53
+ titleInput: ['textarea[placeholder="请输入标题..."]'],
54
+ /** Rich-text body editor (contenteditable, first on the editor page). **Grounded** (sspai.js:59, 82); missing there means 未登录 (sspai.js:60). */
55
+ bodyEditor: ['div[contenteditable="true"]'],
56
+ /**
57
+ * Cover file input, scoped to the active article editor's form. **Grounded** that the source
58
+ * takes the page's first `input[type="file"]` (sspai.js:32), but the task 0294 refine
59
+ * correction freezes the scoping — a global first file input could target an unrelated
60
+ * control, and the readback would then "verify" the wrong input. The contenteditable node
61
+ * itself cannot host the input (the body paste replaces the editor's children), so the
62
+ * editor-scoped shape is the form that contains the article editor. No page-first fallback.
63
+ */
64
+ coverInput: ['form:has(div[contenteditable="true"]) input[type="file"]'],
65
+ /**
66
+ * The exact-visible draft-save control(s). **Provisional** (task 0294 Design freeze; the
67
+ * archived guide documents draft saving historically only — sspai.js never saves);
68
+ * `text-is` exact match, most specific wording first, no other fallback. Absence is a typed
69
+ * draft-save failure, never a manufactured draft.
70
+ */
71
+ draftSave: ['button:text-is("保存草稿")', 'button:text-is("保存")'],
72
+ /**
73
+ * The preview control. **Grounded** (sspai.js:68) — probed read-only for editor-chrome
74
+ * capability evidence only; it is NEVER clicked in the draft path (sspai.js:15-16: the source
75
+ * itself stops at preview, and preview is not a save).
76
+ */
77
+ previewButton: ['a.editor-extra-button-preview'],
78
+ } as const;
79
+
80
+ /** Keys of the selector map, for typed probes. */
81
+ export type SspaiArticleSelectorKey = keyof typeof SSPAI_ARTICLE_SELECTORS;
82
+
83
+ /**
84
+ * Persisted draft receipt URL shapes. **Provisional**: sspai.js is a fill-only adapter and never
85
+ * reads a receipt; these are the public-post and drafts-list id shapes the task 0294 Design
86
+ * freeze names. Only shapes in this list count as receipts, and only when they were not on
87
+ * screen before the save (fresh-signal rule) — a toast alone never confirms.
88
+ */
89
+ export const SSPAI_RECEIPT_URL_RES = [/sspai\.com\/post\/\d+/, /sspai\.com\/me\/drafts\/\d+/] as const;
90
+
91
+ /** Login deadline for the operator CLI and the session gate (shared harness default). */
92
+ export const SSPAI_LOGIN_TIMEOUT_MS = 180_000;
93
+
94
+ /**
95
+ * Sspai-owned readiness predicate: no login redirect and the site-owned title textarea (grounded,
96
+ * sspai.js:26) plus contenteditable editor (grounded, sspai.js:59) attached — sspai.js:60 makes
97
+ * the missing editor the logged-out marker. Shared by the publish preflight and the operator
98
+ * profile CLI, so both apply one gate. The login-redirect regex is provisional (see
99
+ * `SSPAI_LOGIN_URL_RE`); the editor-control probes carry the real gate, and body text mentioning
100
+ * 登录 is never read.
101
+ */
102
+ export const isSspaiAuthenticated: IsAuthenticatedPredicate = async (page) => {
103
+ if (SSPAI_LOGIN_URL_RE.test(page.url())) return false;
104
+ const title = await trySelectors(page, SSPAI_ARTICLE_SELECTORS.titleInput, { visible: false });
105
+ if (!title.found) return false;
106
+ return (await trySelectors(page, SSPAI_ARTICLE_SELECTORS.bodyEditor, { visible: false })).found;
107
+ };
108
+
109
+ /**
110
+ * Site-owned config for the shared session check and operator CLI (ADR-032). The check opens the
111
+ * article-editor entry (see `SSPAI_EDIT_URL`); the manifest `browserSession.homeUrl` stays the
112
+ * fixed display entry `SSPAI_HOME_URL`. The publish preflight never forwards `checkHeadedOnly`:
113
+ * it is the operator CLI's setting, and a refusal must never escalate to a headed window on its
114
+ * own.
115
+ */
116
+ export const SSPAI_CONFIG: ChannelBrowserConfig = {
117
+ channel: SSPAI_TARGET,
118
+ homeUrl: SSPAI_EDIT_URL,
119
+ isAuthenticated: isSspaiAuthenticated,
120
+ loginTimeoutMs: SSPAI_LOGIN_TIMEOUT_MS,
121
+ checkHeadedOnly: false,
122
+ };
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "../../../tooling/typescript/base.json",
3
+ "include": ["src"]
4
+ }
@@ -19832,6 +19832,83 @@ var init_runtime_node_bun = __esm(() => {
19832
19832
  import { dirname as dirname4 } from "path";
19833
19833
  import { parseArgs } from "util";
19834
19834
 
19835
+ // ../../packages/kk-core/src/content-limits.ts
19836
+ var PLATFORM_CONTENT_LIMITS = Object.freeze({
19837
+ schemaVersion: 5,
19838
+ tagLimits: Object.freeze({
19839
+ default: 7,
19840
+ imageText: Object.freeze({
19841
+ instagram: 5,
19842
+ douyin: 5,
19843
+ kuaishou: 4,
19844
+ weixin: 1,
19845
+ xiaohongshu: 10
19846
+ }),
19847
+ shortVideo: Object.freeze({
19848
+ douyin: 5,
19849
+ kuaishou: 4,
19850
+ xiaohongshu: 10,
19851
+ bilibili: 10,
19852
+ zhihu: 5,
19853
+ tiktok: 20,
19854
+ youtube: 60
19855
+ })
19856
+ }),
19857
+ titleCapabilities: Object.freeze({
19858
+ dynamic: Object.freeze({
19859
+ bilibili: Object.freeze({ max: 20, unit: "utf16_code_units" }),
19860
+ zhihu: Object.freeze({ max: 50, unit: "utf16_code_units" }),
19861
+ douban: Object.freeze({ max: 70, unit: "utf16_code_units" }),
19862
+ x: null,
19863
+ threads: null,
19864
+ weibo: null,
19865
+ jike: null,
19866
+ zsxq: null,
19867
+ toutiao: null,
19868
+ juejin: null
19869
+ }),
19870
+ imageText: Object.freeze({
19871
+ xiaohongshu: Object.freeze({ max: 20, unit: "xiaohongshu_title_weighted" }),
19872
+ weixin: Object.freeze({ max: 20, unit: "utf16_code_units" }),
19873
+ douyin: Object.freeze({ max: 20, unit: "utf16_code_units" }),
19874
+ shipinhao: Object.freeze({ max: 22, unit: "utf16_code_units" }),
19875
+ reddit: Object.freeze({ max: 300, unit: "utf16_code_units" })
19876
+ }),
19877
+ article: Object.freeze({
19878
+ juejin: Object.freeze({ max: 100, unit: "utf16_code_units" }),
19879
+ csdn: Object.freeze({ max: 100, unit: "utf16_code_units" }),
19880
+ cnblogs: Object.freeze({ max: 128, unit: "utf16_code_units" }),
19881
+ weibo: Object.freeze({ max: 30, unit: "utf16_code_units" }),
19882
+ sspai: Object.freeze({ max: 80, unit: "utf16_code_units" })
19883
+ }),
19884
+ shortVideo: Object.freeze({
19885
+ shipinhao: Object.freeze({ max: 16, unit: "utf16_code_units" }),
19886
+ douyin: Object.freeze({ max: 30, unit: "utf16_code_units" }),
19887
+ bilibili: Object.freeze({ max: 80, unit: "utf16_code_units" })
19888
+ })
19889
+ }),
19890
+ bodyCapabilities: Object.freeze({
19891
+ dynamic: Object.freeze({
19892
+ threads: Object.freeze({ max: 500, unit: "unicode_code_points", behavior: "split_thread" }),
19893
+ x: Object.freeze({ max: 280, unit: "weighted", behavior: "split_thread" }),
19894
+ weibo: Object.freeze({ max: 2000, unit: "unicode_code_points" }),
19895
+ jike: Object.freeze({ max: 1000, unit: "unicode_code_points" })
19896
+ }),
19897
+ imageText: Object.freeze({
19898
+ xiaohongshu: Object.freeze({ max: 1000, unit: "unicode_code_points" }),
19899
+ douyin: Object.freeze({ max: 1000, unit: "unicode_code_points" })
19900
+ }),
19901
+ article: Object.freeze({
19902
+ wechatmp: Object.freeze({ max: 50000, unit: "unicode_code_points" }),
19903
+ juejin: Object.freeze({ max: 50000, unit: "unicode_code_points" })
19904
+ }),
19905
+ shortVideo: Object.freeze({
19906
+ douyin: Object.freeze({ max: 1000, unit: "unicode_code_points" }),
19907
+ kuaishou: Object.freeze({ max: 1000, unit: "unicode_code_points" }),
19908
+ shipinhao: Object.freeze({ max: 1000, unit: "unicode_code_points" })
19909
+ })
19910
+ })
19911
+ });
19835
19912
  // ../../packages/kk-core/src/kinds.ts
19836
19913
  init_zod();
19837
19914
 
@@ -19922,6 +19999,67 @@ var BrowserSessionDescriptorSchema = object({
19922
19999
  }
19923
20000
  }).optional()
19924
20001
  }).strict();
20002
+ var ContentCountUnitSchema = _enum([
20003
+ "utf16_code_units",
20004
+ "unicode_code_points",
20005
+ "weighted",
20006
+ "xiaohongshu_title_weighted"
20007
+ ]);
20008
+ var LimitEntrySchema = object({
20009
+ max: number2().int().positive(),
20010
+ unit: ContentCountUnitSchema
20011
+ }).strict();
20012
+ var PublishingProfileLimitsSchema = object({
20013
+ title: LimitEntrySchema.optional(),
20014
+ body: LimitEntrySchema.optional()
20015
+ }).strict();
20016
+ var PublishingProfileRequiresSchema = object({
20017
+ cover: boolean2().optional(),
20018
+ orderedImages: boolean2().optional()
20019
+ }).strict();
20020
+ var PublishingProfileModeBindingSchema = object({
20021
+ metadata: record(string2(), unknown())
20022
+ }).strict();
20023
+ var PublishingProfileModesSchema = object({
20024
+ draft: PublishingProfileModeBindingSchema.optional(),
20025
+ live: PublishingProfileModeBindingSchema.optional()
20026
+ }).strict().superRefine((modes, ctx) => {
20027
+ if (modes.draft === undefined && modes.live === undefined) {
20028
+ ctx.addIssue({
20029
+ code: "custom",
20030
+ message: 'modes must define at least one of "draft" or "live"'
20031
+ });
20032
+ }
20033
+ });
20034
+ var PublishingProfileSchema = object({
20035
+ format: string2().min(1),
20036
+ locale: string2().regex(/^(source|[a-z]{2,3}(-[A-Za-z0-9]+)*)$/, {
20037
+ message: 'locale must be "source" or a valid language tag'
20038
+ }),
20039
+ limits: PublishingProfileLimitsSchema.optional(),
20040
+ requires: PublishingProfileRequiresSchema.optional(),
20041
+ modes: PublishingProfileModesSchema
20042
+ }).strict();
20043
+ var PublishingCapabilitiesSchema = object({
20044
+ defaultProfile: string2().min(1),
20045
+ profiles: record(string2().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/), PublishingProfileSchema)
20046
+ }).strict().superRefine((data, ctx) => {
20047
+ const keys = Object.keys(data.profiles);
20048
+ if (keys.length === 0) {
20049
+ ctx.addIssue({
20050
+ code: "custom",
20051
+ path: ["profiles"],
20052
+ message: "profiles record must be nonempty"
20053
+ });
20054
+ }
20055
+ if (!(data.defaultProfile in data.profiles)) {
20056
+ ctx.addIssue({
20057
+ code: "custom",
20058
+ path: ["defaultProfile"],
20059
+ message: `defaultProfile "${data.defaultProfile}" must reference a declared profile`
20060
+ });
20061
+ }
20062
+ });
19925
20063
  var PluginManifestSchemaBase = looseObject({
19926
20064
  name: string2().min(1),
19927
20065
  kind: PluginKindSchema,
@@ -19930,23 +20068,31 @@ var PluginManifestSchemaBase = looseObject({
19930
20068
  version: string2().optional(),
19931
20069
  description: string2().optional(),
19932
20070
  operations: record(string2(), PluginOperationSchema).optional(),
19933
- browserSession: BrowserSessionDescriptorSchema.optional()
20071
+ browserSession: BrowserSessionDescriptorSchema.optional(),
20072
+ publishing: PublishingCapabilitiesSchema.optional()
19934
20073
  });
19935
20074
  var PluginManifestSchema = PluginManifestSchemaBase.superRefine((manifest, ctx) => {
19936
- if (manifest.browserSession === undefined)
19937
- return;
19938
- if (manifest.kind !== "publish") {
19939
- ctx.addIssue({
19940
- code: "custom",
19941
- path: ["browserSession"],
19942
- message: `browserSession requires kind "publish", not "${manifest.kind}"`
19943
- });
20075
+ if (manifest.browserSession !== undefined) {
20076
+ if (manifest.kind !== "publish") {
20077
+ ctx.addIssue({
20078
+ code: "custom",
20079
+ path: ["browserSession"],
20080
+ message: `browserSession requires kind "publish", not "${manifest.kind}"`
20081
+ });
20082
+ }
20083
+ if (manifest.check === undefined) {
20084
+ ctx.addIssue({
20085
+ code: "custom",
20086
+ path: ["browserSession"],
20087
+ message: "browserSession requires a check probe entry that owns the session routines"
20088
+ });
20089
+ }
19944
20090
  }
19945
- if (manifest.check === undefined) {
20091
+ if (manifest.publishing !== undefined && manifest.kind !== "publish") {
19946
20092
  ctx.addIssue({
19947
20093
  code: "custom",
19948
- path: ["browserSession"],
19949
- message: "browserSession requires a check probe entry that owns the session routines"
20094
+ path: ["publishing"],
20095
+ message: `publishing requires kind "publish", not "${manifest.kind}"`
19950
20096
  });
19951
20097
  }
19952
20098
  });
@@ -20569,6 +20715,8 @@ async function ensureProfileDir2(profileDir, headless) {
20569
20715
  throw new BrowserProfileMissingError(profileDir);
20570
20716
  await mkdir2(profileDir, { recursive: true, mode: 448 });
20571
20717
  }
20718
+ // ../../packages/publish-harness/src/browser-anti-detection.ts
20719
+ var MAX_QUEUED_BYTES = 8 * 1024 * 1024;
20572
20720
  // ../../packages/publish-harness/src/browser.ts
20573
20721
  var DEFAULT_CHECK_TIMEOUT_MS = 30000;
20574
20722
  var DEFAULT_STEP_TIMEOUT_MS = 30000;