@gobing-ai/knowledge-kit 0.0.36 → 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 (159) 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 +2878 -152
  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 +180 -12
  14. package/plugins/generations/dailynews-gen/src/script-plan.ts +32 -0
  15. package/plugins/generations/episode-plan-gen/dist/index.js +158 -12
  16. package/plugins/generations/gemini-tts/README.md +4 -3
  17. package/plugins/generations/gemini-tts/dist/index.js +158 -12
  18. package/plugins/generations/image-gen/dist/index.js +158 -12
  19. package/plugins/generations/news-report-gen/dist/index.js +158 -12
  20. package/plugins/generations/omni-voice-gen/README.md +15 -4
  21. package/plugins/generations/omni-voice-gen/profiles.json +1 -2
  22. package/plugins/generations/voice-gen/dist/index.js +158 -12
  23. package/plugins/generations/voxcpm-tts/README.md +21 -1
  24. package/plugins/generations/voxcpm-tts/dist/index.js +173 -13
  25. package/plugins/generations/voxcpm-tts/scripts/audit-speaker-consistency.py +109 -0
  26. package/plugins/generations/voxcpm-tts/src/voxcpm-client.ts +28 -1
  27. package/plugins/ingestions/aihot-ingest/dist/index.js +158 -12
  28. package/plugins/ingestions/horizon-ingest/dist/index.js +158 -12
  29. package/plugins/ingestions/last30days-ingest/dist/index.js +158 -12
  30. package/plugins/ingestions/web-search/dist/index.js +158 -12
  31. package/plugins/kk/commands/workflow-run.md +56 -18
  32. package/plugins/kk/decisions/kk-itc.yaml +46 -0
  33. package/plugins/kk/decisions/kk-seo-keyword.yaml +57 -0
  34. package/plugins/kk/decisions/kk-seo-page.yaml +230 -0
  35. package/plugins/kk/decisions/kk-seo-pair.yaml +21 -0
  36. package/plugins/kk/decisions/kk-seo-site.yaml +90 -0
  37. package/plugins/kk/plugin.json +1 -1
  38. package/plugins/kk/publish-presets/chinese-article.json +14 -0
  39. package/plugins/kk/publish-presets/technical-article.json +14 -0
  40. package/plugins/kk/references/smart-seo-attribution.md +85 -0
  41. package/plugins/kk/scripts/daily-stages.ts +42 -2
  42. package/plugins/kk/scripts/daily-voice-style.zh.md +1 -1
  43. package/plugins/kk/scripts/itc-stages.ts +25 -118
  44. package/plugins/kk/scripts/kk-workflow-stages.ts +38 -1
  45. package/plugins/kk/scripts/seo-decisions.ts +477 -0
  46. package/plugins/kk/scripts/seo-enrich.ts +728 -0
  47. package/plugins/kk/scripts/seo-facts.ts +610 -0
  48. package/plugins/kk/scripts/seo-fetch.ts +766 -0
  49. package/plugins/kk/scripts/seo-report.ts +460 -0
  50. package/plugins/kk/scripts/seo-rules.ts +1882 -0
  51. package/plugins/kk/scripts/seo-stages.ts +1044 -0
  52. package/plugins/kk/skills/article-adapt/SKILL.md +2 -2
  53. package/plugins/kk/skills/smart-seo/SKILL.md +183 -0
  54. package/plugins/kk/skills/smart-seo/references/method.md +126 -0
  55. package/plugins/kk/skills/smart-seo/references/narrative.md +125 -0
  56. package/plugins/kk/skills/smart-seo/references/operations.md +202 -0
  57. package/plugins/kk/skills/smart-seo/references/seo-geo-sources.md +90 -0
  58. package/plugins/kk/workflows/feature-verification.yaml +28 -24
  59. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +86 -18
  60. package/plugins/kk/workflows/kk-itc-author.yaml +406 -0
  61. package/plugins/kk/workflows/kk-itc.yaml +208 -682
  62. package/plugins/kk/workflows/kk-smart-seo.yaml +251 -0
  63. package/plugins/publishings/bilibili-pub/dist/index.js +21782 -0
  64. package/plugins/publishings/bilibili-pub/dist/profile-check.js +20410 -0
  65. package/plugins/publishings/bilibili-pub/package.json +20 -0
  66. package/plugins/publishings/bilibili-pub/plugin.json +12 -0
  67. package/plugins/publishings/bilibili-pub/src/flow.ts +1296 -0
  68. package/plugins/publishings/bilibili-pub/src/index.ts +139 -0
  69. package/plugins/publishings/bilibili-pub/src/profile-check.ts +19 -0
  70. package/plugins/publishings/bilibili-pub/src/selectors.ts +222 -0
  71. package/plugins/publishings/bilibili-pub/tsconfig.json +4 -0
  72. package/plugins/publishings/cnblogs-pub/dist/index.js +21383 -0
  73. package/plugins/publishings/cnblogs-pub/dist/profile-check.js +20412 -0
  74. package/plugins/publishings/cnblogs-pub/package.json +19 -0
  75. package/plugins/publishings/cnblogs-pub/plugin.json +12 -0
  76. package/plugins/publishings/cnblogs-pub/src/flow.ts +708 -0
  77. package/plugins/publishings/cnblogs-pub/src/index.ts +172 -0
  78. package/plugins/publishings/cnblogs-pub/src/profile-check.ts +24 -0
  79. package/plugins/publishings/cnblogs-pub/src/selectors.ts +104 -0
  80. package/plugins/publishings/cnblogs-pub/tsconfig.json +4 -0
  81. package/plugins/publishings/csdn-pub/dist/index.js +21552 -0
  82. package/plugins/publishings/csdn-pub/dist/profile-check.js +20963 -0
  83. package/plugins/publishings/csdn-pub/package.json +19 -0
  84. package/plugins/publishings/csdn-pub/plugin.json +12 -0
  85. package/plugins/publishings/csdn-pub/src/index.ts +1178 -0
  86. package/plugins/publishings/csdn-pub/src/profile-check.ts +23 -0
  87. package/plugins/publishings/csdn-pub/tsconfig.json +4 -0
  88. package/plugins/publishings/emdash-pub/dist/index.js +158 -12
  89. package/plugins/publishings/emdash-pub/plugin.json +22 -1
  90. package/plugins/publishings/infoq-pub/dist/index.js +160 -12
  91. package/plugins/publishings/infoq-pub/dist/profile-check.js +160 -12
  92. package/plugins/publishings/infoq-pub/plugin.json +21 -0
  93. package/plugins/publishings/jike-pub/dist/index.js +21379 -0
  94. package/plugins/publishings/jike-pub/dist/profile-check.js +20951 -0
  95. package/plugins/publishings/jike-pub/package.json +19 -0
  96. package/plugins/publishings/jike-pub/plugin.json +12 -0
  97. package/plugins/publishings/jike-pub/src/index.ts +800 -0
  98. package/plugins/publishings/jike-pub/src/profile-check.ts +21 -0
  99. package/plugins/publishings/jike-pub/tsconfig.json +4 -0
  100. package/plugins/publishings/juejin-pub/dist/index.js +160 -12
  101. package/plugins/publishings/juejin-pub/dist/profile-check.js +160 -12
  102. package/plugins/publishings/juejin-pub/plugin.json +21 -0
  103. package/plugins/publishings/medium-pub/dist/index.js +158 -12
  104. package/plugins/publishings/medium-pub/plugin.json +22 -1
  105. package/plugins/publishings/podcast-pub/dist/index.js +158 -12
  106. package/plugins/publishings/qiita-pub/dist/index.js +158 -12
  107. package/plugins/publishings/qiita-pub/plugin.json +24 -1
  108. package/plugins/publishings/shipinhao-pub/dist/index.js +21715 -0
  109. package/plugins/publishings/shipinhao-pub/dist/profile-check.js +20431 -0
  110. package/plugins/publishings/shipinhao-pub/package.json +19 -0
  111. package/plugins/publishings/shipinhao-pub/plugin.json +12 -0
  112. package/plugins/publishings/shipinhao-pub/src/flow.ts +1323 -0
  113. package/plugins/publishings/shipinhao-pub/src/index.ts +173 -0
  114. package/plugins/publishings/shipinhao-pub/src/profile-check.ts +25 -0
  115. package/plugins/publishings/shipinhao-pub/src/selectors.ts +241 -0
  116. package/plugins/publishings/shipinhao-pub/tsconfig.json +4 -0
  117. package/plugins/publishings/sspai-pub/dist/index.js +21371 -0
  118. package/plugins/publishings/sspai-pub/dist/profile-check.js +20414 -0
  119. package/plugins/publishings/sspai-pub/package.json +19 -0
  120. package/plugins/publishings/sspai-pub/plugin.json +12 -0
  121. package/plugins/publishings/sspai-pub/src/flow.ts +624 -0
  122. package/plugins/publishings/sspai-pub/src/index.ts +167 -0
  123. package/plugins/publishings/sspai-pub/src/profile-check.ts +25 -0
  124. package/plugins/publishings/sspai-pub/src/selectors.ts +122 -0
  125. package/plugins/publishings/sspai-pub/tsconfig.json +4 -0
  126. package/plugins/publishings/substack-pub/dist/index.js +160 -12
  127. package/plugins/publishings/substack-pub/dist/profile-check.js +160 -12
  128. package/plugins/publishings/substack-pub/plugin.json +21 -0
  129. package/plugins/publishings/surfdash-pub/dist/index.js +158 -12
  130. package/plugins/publishings/surfdash-pub/plugin.json +22 -1
  131. package/plugins/publishings/threads-pub/dist/index.js +158 -12
  132. package/plugins/publishings/wechatmp-pub/dist/index.js +1669 -502
  133. package/plugins/publishings/wechatmp-pub/dist/profile-check.js +204 -21
  134. package/plugins/publishings/wechatmp-pub/plugin.json +32 -0
  135. package/plugins/publishings/wechatmp-pub/src/cgi-draft.ts +540 -0
  136. package/plugins/publishings/wechatmp-pub/src/cgi-media.ts +380 -0
  137. package/plugins/publishings/wechatmp-pub/src/cgi.ts +485 -0
  138. package/plugins/publishings/wechatmp-pub/src/flow.ts +104 -0
  139. package/plugins/publishings/weibo-pub/dist/index.js +160 -12
  140. package/plugins/publishings/weibo-pub/dist/profile-check.js +160 -12
  141. package/plugins/publishings/weibo-pub/plugin.json +35 -0
  142. package/plugins/publishings/x-pub/dist/index.js +160 -12
  143. package/plugins/publishings/x-pub/dist/profile-check.js +160 -12
  144. package/plugins/publishings/x-pub/plugin.json +40 -0
  145. package/plugins/publishings/xhs-pub/dist/index.js +160 -12
  146. package/plugins/publishings/xhs-pub/dist/profile-check.js +160 -12
  147. package/plugins/publishings/xhs-pub/plugin.json +37 -0
  148. package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +21491 -0
  149. package/plugins/publishings/xiaoyuzhou-pub/dist/profile-check.js +20417 -0
  150. package/plugins/publishings/xiaoyuzhou-pub/package.json +19 -0
  151. package/plugins/publishings/xiaoyuzhou-pub/plugin.json +12 -0
  152. package/plugins/publishings/xiaoyuzhou-pub/src/flow.ts +826 -0
  153. package/plugins/publishings/xiaoyuzhou-pub/src/index.ts +174 -0
  154. package/plugins/publishings/xiaoyuzhou-pub/src/profile-check.ts +26 -0
  155. package/plugins/publishings/xiaoyuzhou-pub/src/selectors.ts +125 -0
  156. package/plugins/publishings/xiaoyuzhou-pub/tsconfig.json +4 -0
  157. package/plugins/publishings/zenn-pub/dist/index.js +158 -12
  158. package/plugins/publishings/zenn-pub/plugin.json +22 -1
  159. package/plugins/generations/omni-voice-gen/voices/robin-news.wav +0 -0
@@ -0,0 +1,173 @@
1
+ /**
2
+ * shipinhao-pub — shipinhao (channels.weixin.qq.com) short-video publisher plugin surface (task 0296).
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 → video file → cover
6
+ * → tags → original → unsupported fields — all refused before any browser opens),
7
+ * `getPublishTransport`, the `--in/--out` IO contract and the CLI `main`. The selectors and the
8
+ * vendor-grounded site predicates live in `src/selectors.ts`, the browser transport and flow in
9
+ * `src/flow.ts`, and the operator session CLI in `src/profile-check.ts`.
10
+ *
11
+ * Evidence: the recovered extension adapter
12
+ * `.spur/run/yijianfa-source/platforms/shipinhao.js` (2817 lines) pins the field semantics cited
13
+ * throughout `src/selectors.ts` and `src/flow.ts` (root policy 514-545, upload 549-631, fields
14
+ * 638-674, originality 2221-2310, cover 1461-1467/1853-1871/2111-2215, publish 2312-2326).
15
+ *
16
+ * publishStatus contract (task 0296 Design freeze): absent defaults to 'draft' and saves through
17
+ * the exact-visible 保存草稿 candidate (provisional — the vendor never saves a draft, so absence
18
+ * is a typed failure, never a manufactured draft and never a publish click); 'public' publishes
19
+ * through the scoped exact-发表 button and requires a fresh id-bearing works receipt plus
20
+ * persisted state; any unknown value refuses before the browser.
21
+ */
22
+ import { dirname } from 'node:path';
23
+ import { parseArgs } from 'node:util';
24
+ import { type Content, ContentSchema, type Result, ResultSchema } from '@gobing-ai/kk-core';
25
+ import { type BrowserDriver, FakeBrowserTransport, type PublishTransport } from '@gobing-ai/publish-harness';
26
+ import { createNodeFileSystem } from '@gobing-ai/ts-runtime';
27
+ import { echoError } from '@gobing-ai/ts-utils';
28
+ import { bounded, boundedDetail, errorMessage, fail, ShipinhaoBrowserTransport } from './flow';
29
+ import { SHP_TARGET } from './selectors';
30
+
31
+ /**
32
+ * Declared operations (ADR-029 dispatch): the source registers a single short-video flow
33
+ * (`shortVideo`; the vendor's type dispatch, shipinhao.js:10,2802, declares this one kind).
34
+ */
35
+ export const SHP_OPERATIONS = ['shortVideo'] as const;
36
+ export type ShipinhaoOperation = (typeof SHP_OPERATIONS)[number];
37
+
38
+ /**
39
+ * Resolves the requested operation (`options.operation ?? content.metadata.operation ?? default`)
40
+ * BEFORE any transport — and therefore before any browser — is resolved. An undeclared value is
41
+ * refused by name (this source publishes short video only).
42
+ * @param options - Per-call publish options (`operation`).
43
+ * @param content - Content whose `metadata.operation` is the fallback source.
44
+ * @returns The resolved operation, or the refusal message.
45
+ */
46
+ export function resolveOperation(
47
+ options?: Record<string, unknown>,
48
+ content?: Content,
49
+ ): { operation: ShipinhaoOperation; error?: undefined } | { operation?: undefined; error: string } {
50
+ const raw = options?.operation ?? content?.metadata?.operation ?? 'shortVideo';
51
+ if (typeof raw !== 'string' || !(SHP_OPERATIONS as readonly string[]).includes(raw)) {
52
+ return {
53
+ error: `unsupported operation '${bounded(raw)}' for ${SHP_TARGET}: expected one of ${SHP_OPERATIONS.join(', ')}`,
54
+ };
55
+ }
56
+ return { operation: raw as ShipinhaoOperation };
57
+ }
58
+
59
+ /**
60
+ * Resolves the driver from `options.driver ?? 'script'`. This channel ships one driver and no
61
+ * `agent` path, so anything else — including a wrong type — is refused by name before a transport
62
+ * is built.
63
+ * @param options - Per-call publish options (`driver`).
64
+ * @returns The resolved driver, or the refusal message.
65
+ */
66
+ function resolveDriver(
67
+ options?: Record<string, unknown>,
68
+ ): { driver: BrowserDriver; error?: undefined } | { driver?: undefined; error: string } {
69
+ const raw = options?.driver ?? 'script';
70
+ if (raw !== 'script') {
71
+ return {
72
+ error: `unsupported driver '${bounded(raw)}' for ${SHP_TARGET}: expected one of script (the shipinhao-pub browser flow has no agent path)`,
73
+ };
74
+ }
75
+ return { driver: 'script' };
76
+ }
77
+
78
+ /**
79
+ * Resolves the channel transport: the publish-harness fake under
80
+ * `KNOWLEDGE_KIT_PUBLISH_TRANSPORT=fake` (offline, no profile, no browser), otherwise the real
81
+ * shipinhao browser transport (`src/flow.ts`). API-only paths never load Playwright eagerly:
82
+ * the harness launcher imports Playwright dynamically at launch time only.
83
+ * @param operation - Resolved operation (`shortVideo`).
84
+ * @param driver - Resolved driver; only `script` is declared.
85
+ * @returns The publish transport for this channel.
86
+ */
87
+ export function getPublishTransport(
88
+ operation: ShipinhaoOperation = 'shortVideo',
89
+ driver: BrowserDriver = 'script',
90
+ ): PublishTransport {
91
+ if (process.env.KNOWLEDGE_KIT_PUBLISH_TRANSPORT === 'fake') return new FakeBrowserTransport('script');
92
+ return new ShipinhaoBrowserTransport(operation, driver);
93
+ }
94
+
95
+ /** Writes the Result to `--out`, creating its directory when the caller named a nested path. */
96
+ async function writePublishResult(outputPath: string, result: Result): Promise<void> {
97
+ const outDir = dirname(outputPath);
98
+ if (outDir && outDir !== '.') await createNodeFileSystem().ensureDir(outDir);
99
+ await createNodeFileSystem().writeFile(outputPath, JSON.stringify(result, null, 2));
100
+ }
101
+
102
+ /**
103
+ * The plugin's `--in/--out` contract: validate the Content, resolve **and refuse** the operation
104
+ * and the driver before any transport exists, then publish through the injected or configured
105
+ * transport (whose own ladder refuses status/format/payload/video/cover/tags/original problems —
106
+ * also all pre-browser).
107
+ * @param inputPath - Path to the Content JSON.
108
+ * @param outputPath - Path the Result JSON is written to, on success and on refusal.
109
+ * @param transport - Optional transport injection for tests and offline runs.
110
+ */
111
+ export async function processPublishIO(
112
+ inputPath: string,
113
+ outputPath: string,
114
+ transport?: PublishTransport,
115
+ ): Promise<void> {
116
+ await createNodeFileSystem().deleteFile(outputPath);
117
+ let content: Content;
118
+ let options: Record<string, unknown> | undefined;
119
+ try {
120
+ const raw = JSON.parse(await createNodeFileSystem().readFile(inputPath)) as Record<string, unknown>;
121
+ content = ContentSchema.parse(raw);
122
+ if (raw.options !== undefined && typeof raw.options === 'object' && raw.options !== null) {
123
+ options = raw.options as Record<string, unknown>;
124
+ }
125
+ } catch (error: unknown) {
126
+ const message = boundedDetail(`Invalid Content input: ${errorMessage(error)}`);
127
+ await writePublishResult(outputPath, fail(SHP_TARGET, message));
128
+ throw new Error(message);
129
+ }
130
+ const mode = resolveOperation(options, content);
131
+ if (mode.error !== undefined) {
132
+ await writePublishResult(outputPath, fail(SHP_TARGET, mode.error));
133
+ throw new Error(mode.error);
134
+ }
135
+ const driverMode = resolveDriver(options);
136
+ if (driverMode.error !== undefined) {
137
+ await writePublishResult(outputPath, fail(SHP_TARGET, driverMode.error));
138
+ throw new Error(driverMode.error);
139
+ }
140
+ const resolved = transport ?? getPublishTransport(mode.operation, driverMode.driver);
141
+ const result = ResultSchema.parse(await resolved.publish({ content, options }));
142
+ await writePublishResult(outputPath, result);
143
+ if (!result.ok) throw new Error(result.error ?? 'Publish failed');
144
+ }
145
+
146
+ /**
147
+ * CLI entry point: `--in <content.json> --out <result.json>`.
148
+ * @returns Process exit code (0 published, 1 refused or invalid arguments).
149
+ */
150
+ export async function main(): Promise<number> {
151
+ let values: { in?: string; out?: string };
152
+ try {
153
+ ({ values } = parseArgs({ options: { in: { type: 'string' }, out: { type: 'string' } } }));
154
+ } catch (error: unknown) {
155
+ echoError(`Invalid arguments: ${errorMessage(error)}`);
156
+ return 1;
157
+ }
158
+ if (!values.in || !values.out) {
159
+ echoError('Missing required arguments: --in and --out');
160
+ return 1;
161
+ }
162
+ try {
163
+ await processPublishIO(values.in, values.out);
164
+ return 0;
165
+ } catch (error: unknown) {
166
+ echoError(`shipinhao-pub failed: ${boundedDetail(errorMessage(error))}`);
167
+ return 1;
168
+ }
169
+ }
170
+
171
+ if (import.meta.main) {
172
+ process.exit(await main());
173
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * shipinhao profile-session capability (task 0296, Chunk A).
3
+ *
4
+ * `login` opens the Channels publishing home (https://channels.weixin.qq.com/platform/post/video,
5
+ * see `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` > `SHP_PROFILE_DIR` >
9
+ * `KK_BROWSER_PROFILE_DIR` > `~/.config/kk/browser-profiles/shipinhao`) 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 vendor-grounded creator-form evidence (the unique video
14
+ * `input[type=file]`, shipinhao.js:580-583, and the 添加描述/标题 placeholders, shipinhao.js:516-518,
15
+ * 669-674 — see `src/selectors.ts`); an unsupported-browser page is also not-ready. The
16
+ * login-redirect regex is provisional. A selector that never attaches is a refusal
17
+ * (`bun plugins/publishings/shipinhao-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 { SHP_CONFIG } from './selectors';
22
+
23
+ if (import.meta.main) {
24
+ process.exit(await runProfileCli(SHP_CONFIG, Bun.argv.slice(2)));
25
+ }
@@ -0,0 +1,241 @@
1
+ /**
2
+ * shipinhao-pub selectors — every selector/URL/regex/timeout constant this channel resolves
3
+ * (task 0296).
4
+ *
5
+ * Grounding: the recovered WeChat Channels (视频号) extension adapter
6
+ * `.spur/run/yijianfa-source/platforms/shipinhao.js` (2817 lines, verified read-only 2026-10-15)
7
+ * pins the publishing-page mechanics cited below. The page may run inside a `<wujie-app>`
8
+ * shadowRoot; once wujie-app exists the vendor waits for its shadowRoot and never falls back to
9
+ * document (shipinhao.js:3-5, 532-545). Field receipts follow MultiSync v2: only a page-readable
10
+ * result counts, and a clicked 发表 without a works record is unknown (shipinhao.js:6-9). The
11
+ * 声明原创 flow (entry checkbox → declare-body-wrapper agreement checkbox → 声明原创 confirm
12
+ * button) is ported from the 爱贝壳 1.7.0 short-video branch, where the vendor runs it
13
+ * UNCONDITIONALLY and degrades any failure to a log (shipinhao.js:10-14, 2221-2224, 2270-2310) —
14
+ * this plugin instead runs it only on an explicit `metadata.original === true` operator assertion
15
+ * and type-fails instead of degrading. Anything the evidence does not pin is **Provisional** and
16
+ * fails closed: a missing control is a loud typed refusal, never a silent no-op or a broad
17
+ * fallback.
18
+ */
19
+ import type { ChannelBrowserConfig } from '@gobing-ai/publish-harness/browser';
20
+ import { waitUntil } from '@gobing-ai/publish-harness/browser';
21
+ import type { IsAuthenticatedPredicate } from '@gobing-ai/ts-browser-automation';
22
+
23
+ /** Site-owned target slug; also the profile channel key (`SHP_PROFILE_DIR`). */
24
+ export const SHP_TARGET = 'shipinhao';
25
+
26
+ /**
27
+ * Fixed home entry (task 0296 Design freeze): the Channels short-video publishing page. Grounded
28
+ * as the display entry only — shipinhao.js never navigates (it fills in place inside an
29
+ * already-open assistant tab).
30
+ */
31
+ export const SHP_HOME_URL = 'https://channels.weixin.qq.com/platform/post/video';
32
+
33
+ /**
34
+ * Login/passport redirect marker. **Provisional**: shipinhao.js never observes a redirect URL; it
35
+ * only gates on creator-form readiness (shipinhao.js:155-164). The readiness predicate below
36
+ * carries the real gate: an unauthenticated visit is rejected by the missing creator form, not by
37
+ * this regex alone.
38
+ */
39
+ export const SHP_LOGIN_URL_RE = /(open\.weixin\.qq\.com|channels\.weixin\.qq\.com\/platform\/(login|scan))/;
40
+
41
+ /**
42
+ * Root-readiness snapshot evaluated on the live page (vendor `rootReadinessSnapshot`,
43
+ * shipinhao.js:514-529, and `accountRoots`, shipinhao.js:61-63): whether `wujie-app` is present,
44
+ * whether its shadowRoot is attached, and the direct-evidence probe counts (`input[type="file"]`
45
+ * plus the 描述/标题 placeholder probes, shipinhao.js:516-518). The publish flow refuses instead
46
+ * of guessing a root from this snapshot — see `resolveCreatorRoot` in `src/flow.ts`.
47
+ */
48
+ const ROOT_SNAPSHOT_SCRIPT = `(() => { // kk-shp-root-snapshot
49
+ const wujieApp = document.querySelector('wujie-app');
50
+ const shadow = wujieApp === null || wujieApp === undefined ? null : (wujieApp.shadowRoot ?? null);
51
+ const count = (root, selector) => root === null ? 0 : root.querySelectorAll(selector).length;
52
+ const directFileInputs = count(document, 'input[type="file"]');
53
+ const directTextEvidence = count(document, '[data-placeholder*="添加描述"], input[placeholder*="标题"]');
54
+ const shadowFileInputs = count(shadow, 'input[type="file"]');
55
+ const shadowTextEvidence = count(shadow, '[data-placeholder*="添加描述"], input[placeholder*="标题"]');
56
+ return JSON.stringify({
57
+ wujiePresent: wujieApp !== null,
58
+ shadowRootReady: shadow !== null,
59
+ directFileInputs,
60
+ directTextEvidence,
61
+ shadowFileInputs,
62
+ shadowTextEvidence,
63
+ });
64
+ })()`;
65
+
66
+ /** Shape of {@link ROOT_SNAPSHOT_SCRIPT}'s JSON payload. */
67
+ export interface ShipinhaoRootSnapshot {
68
+ wujiePresent: boolean;
69
+ shadowRootReady: boolean;
70
+ directFileInputs: number;
71
+ directTextEvidence: number;
72
+ shadowFileInputs: number;
73
+ shadowTextEvidence: number;
74
+ }
75
+
76
+ /** Reads the root-readiness snapshot; a failed evaluate reads as "nothing ready yet". */
77
+ export async function readRootSnapshot(page: {
78
+ evaluate: (script: string) => Promise<unknown>;
79
+ }): Promise<ShipinhaoRootSnapshot> {
80
+ try {
81
+ const parsed: unknown = JSON.parse(String(await page.evaluate(ROOT_SNAPSHOT_SCRIPT)));
82
+ if (parsed !== null && typeof parsed === 'object') return parsed as ShipinhaoRootSnapshot;
83
+ } catch {
84
+ // navigation/replacement races read as not-ready; the callers poll and refuse at the deadline
85
+ }
86
+ return {
87
+ wujiePresent: false,
88
+ shadowRootReady: false,
89
+ directFileInputs: 0,
90
+ directTextEvidence: 0,
91
+ shadowFileInputs: 0,
92
+ shadowTextEvidence: 0,
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Vendor wait ceilings (shipinhao.js:38-43), imported verbatim as this plugin's step budgets:
98
+ * ROOT_WAIT_MS 30000 (root-resolve), FORM_WAIT_MS 90000 / PROCESSING_WAIT_MS 120000 (media), and
99
+ * the cover ceilings COVER_PREVIEW_WAIT_MS 30000 / COVER_SAVE_WAIT_MS 60000. CONTROL_WAIT_MS 15000
100
+ * is subsumed by the shared 30s step budget (`STEP_TIMEOUT_MS` in `src/flow.ts`).
101
+ */
102
+ export const SHP_ROOT_WAIT_MS = 30_000;
103
+ export const SHP_FORM_WAIT_MS = 90_000;
104
+ export const SHP_PROCESSING_WAIT_MS = 120_000;
105
+ export const SHP_COVER_PREVIEW_WAIT_MS = 30_000;
106
+ export const SHP_COVER_SAVE_WAIT_MS = 60_000;
107
+
108
+ /**
109
+ * Publishing-form controls. `videoInput`, `titleInput` and `descriptionEditor` are **Grounded**
110
+ * (video accept kinds shipinhao.js:551-555, semantic upload regions shipinhao.js:565-568, unique
111
+ * untyped input shipinhao.js:580-583, title candidates shipinhao.js:638-654, description editor
112
+ * candidates shipinhao.js:669-674). The cover dialog/confirm shapes are **Grounded**
113
+ * (shipinhao.js:1853-1871, 2145-2149); `saveButton` and the receipt URL shapes are
114
+ * **Provisional** (the vendor never saves a draft — no 保存草稿 anywhere in shipinhao.js — and
115
+ * never reads an id-bearing receipt: a clicked 发表 without a works record is unknown,
116
+ * shipinhao.js:2326). All fail closed when absent or ambiguous.
117
+ */
118
+ export const SHP_VIDEO_SELECTORS = {
119
+ /**
120
+ * Video file input candidates, vendor priority: accept-kind (shipinhao.js:549-555) → semantic
121
+ * upload region 上传视频/视频文件/选择视频 (shipinhao.js:565-568) → the single untyped
122
+ * `input[type="file"]` (shipinhao.js:580-583; the flow refuses when more than one matches).
123
+ */
124
+ videoInput: [
125
+ 'input[type="file"][accept*="video"]',
126
+ 'input[type="file"][accept*=".mp4"]',
127
+ 'input[type="file"][accept*=".mov"]',
128
+ 'section:has(input[type="file"]):has-text("上传视频") input[type="file"]',
129
+ 'section:has(input[type="file"]):has-text("视频文件") input[type="file"]',
130
+ 'input[type="file"]',
131
+ ],
132
+ /** Short-title input candidates (短标题 exact → 填写标题 → any 标题; shipinhao.js:638-654). */
133
+ titleInput: ['input[placeholder*="短标题"]', 'input[placeholder*="填写标题"]', 'input[placeholder*="标题"]'],
134
+ /** Description editor candidates (添加描述 placeholders and the 话题和描述 editor; shipinhao.js:669-674). */
135
+ descriptionEditor: [
136
+ '[data-placeholder^="添加描述"]',
137
+ '[placeholder^="添加描述"]',
138
+ '[role="textbox"][aria-label^="添加描述"]',
139
+ '[placeholder*="话题和描述"]',
140
+ ],
141
+ /**
142
+ * Vertical-cover edit entry inside the 个人主页卡片 3:4 slot (shipinhao.js:2145). The flow
143
+ * reads the slot's committed evidence and clicks this entry to open the cover dialog.
144
+ */
145
+ coverEditEntry: ['div.cover-preview-wrap > div'],
146
+ /**
147
+ * Cover dialog container candidates (vendor `findCoverControl`, shipinhao.js:2144-2149): the
148
+ * flow requires the resolved dialog to hold an `input[type="file"]` before use.
149
+ */
150
+ coverDialog: [
151
+ 'div.cover-control-wrap',
152
+ 'div[class*="cover"][class*="dialog"]',
153
+ 'div[class*="cover"][class*="modal"]',
154
+ '[role="dialog"]',
155
+ ],
156
+ /** File input inside the resolved cover dialog (vendor `findCoverControl` gate, shipinhao.js:2146-2148). */
157
+ coverDialogFileInputSuffix: 'input[type="file"]',
158
+ /**
159
+ * Exact footer confirm label of the cover dialog, then the vendor fallback label set
160
+ * (shipinhao.js:1853-1871): `div.finder-dialog-footer button` reading exactly 确认 first, then
161
+ * 确认|完成|保存|应用|确定 — the crop-confirm → outer-submit ladder the vendor walks
162
+ * (shipinhao.js:1883-1916) is driven by these labels only.
163
+ */
164
+ coverConfirmLabels: ['确认', '完成', '保存', '应用', '确定'],
165
+ coverConfirmFooter: 'div.finder-dialog-footer button',
166
+ /**
167
+ * Agreement checkbox inside the 声明原创 dialog (shipinhao.js:2285-2287). **Grounded.**
168
+ */
169
+ originalAgreementCheckbox: 'div.declare-body-wrapper input[type="checkbox"].ant-checkbox-input',
170
+ /** The 声明原创 confirm button label, matched on normalized exact text (shipinhao.js:2296-2299). **Grounded.** */
171
+ originalConfirmLabel: '声明原创',
172
+ /**
173
+ * The exact-visible draft-save control. **Provisional** (task 0296 Design freeze): the vendor
174
+ * never saves a draft (no 保存草稿 anywhere in shipinhao.js; without an explicit publish
175
+ * request it leaves the form for human confirmation, shipinhao.js:2318-2319). `text-is`
176
+ * exact match, no fallback; absence is a typed draft failure, never a manufactured draft and
177
+ * never a publish click.
178
+ */
179
+ saveButton: ['button:text-is("保存草稿")'],
180
+ /**
181
+ * The publish button, exact 发表 on button/role=button (vendor `findPublishButton`,
182
+ * shipinhao.js:2312-2316). **Grounded label; Provisional as this flow's rail** — reached only
183
+ * on an explicit `publishStatus: 'public'`.
184
+ */
185
+ publishButton: ['button:text-is("发表")', '[role="button"]:text-is("发表")'],
186
+ } as const;
187
+
188
+ /** Keys of the selector map, for typed probes. */
189
+ export type ShipinhaoVideoSelectorKey = keyof typeof SHP_VIDEO_SELECTORS;
190
+
191
+ /**
192
+ * Receipt-shaped URLs. **Provisional**: the vendor never reads a receipt (a clicked 发表 without
193
+ * a works record is `unknown`, shipinhao.js:2326), so these are the id-bearing works/record
194
+ * shapes the task 0296 Design freeze names. Only shapes in this list count as receipts, and only
195
+ * when they were not on screen before the click (fresh-signal rule) — a toast alone never
196
+ * confirms, and disagreeing fresh ids are an ambiguous typed failure.
197
+ */
198
+ export const SHP_RECEIPT_URL_RES = [
199
+ /channels\.weixin\.qq\.com\/platform\/record\/detail\/[0-9a-zA-Z_-]+/,
200
+ /channels\.weixin\.qq\.com\/platform\/post\/video\/[0-9a-zA-Z_-]+/,
201
+ ] as const;
202
+
203
+ /** Upload/processing ceiling for the video step (media ceiling — large local files, task 0296 freeze). */
204
+ export const SHP_UPLOAD_TIMEOUT_MS = 600_000;
205
+
206
+ /** Login deadline for the operator CLI and the session gate (shared harness default). */
207
+ export const SHP_LOGIN_TIMEOUT_MS = 180_000;
208
+
209
+ /**
210
+ * Shipinhao-owned readiness predicate: no login redirect, and the creator form observed in its
211
+ * real root — either the wujie-app shadowRoot with creator-form evidence inside it, or the
212
+ * direct-document evidence probes (vendor `accountRoots` shipinhao.js:61-63 and
213
+ * `rootReadinessSnapshot` shipinhao.js:514-529; the vendor reads a missing creator form as
214
+ * not-ready, shipinhao.js:160-164). The wujie root can be late on a cold load, so the snapshot is
215
+ * polled briefly (never a blind sleep) before the gate refuses. Shared by the publish preflight
216
+ * and the operator profile CLI, so both apply one gate. The login-redirect regex is provisional
217
+ * (see `SHP_LOGIN_URL_RE`); the creator-form probes carry the real gate.
218
+ */
219
+ export const isShipinhaoAuthenticated: IsAuthenticatedPredicate = async (page) => {
220
+ if (SHP_LOGIN_URL_RE.test(page.url())) return false;
221
+ const ready = (snapshot: ShipinhaoRootSnapshot): boolean => {
222
+ if (snapshot.wujiePresent)
223
+ return snapshot.shadowRootReady && snapshot.shadowFileInputs + snapshot.shadowTextEvidence > 0;
224
+ return snapshot.directFileInputs + snapshot.directTextEvidence > 0;
225
+ };
226
+ return waitUntil(async () => ready(await readRootSnapshot(page)), 15_000, 500);
227
+ };
228
+
229
+ /**
230
+ * Site-owned config for the shared session check and operator CLI (ADR-032). The check opens the
231
+ * publishing home (`SHP_HOME_URL`); the manifest `browserSession.homeUrl` is the same entry. The
232
+ * publish preflight never forwards `checkHeadedOnly`: it is the operator CLI's setting, and a
233
+ * refusal must never escalate to a headed window on its own.
234
+ */
235
+ export const SHP_CONFIG: ChannelBrowserConfig = {
236
+ channel: SHP_TARGET,
237
+ homeUrl: SHP_HOME_URL,
238
+ isAuthenticated: isShipinhaoAuthenticated,
239
+ loginTimeoutMs: SHP_LOGIN_TIMEOUT_MS,
240
+ checkHeadedOnly: false,
241
+ };
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "../../../tooling/typescript/base.json",
3
+ "include": ["src"]
4
+ }