@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/dist/board/collect/index.css +654 -16
- package/dist/board/collect/index.js +191 -64
- package/dist/board/daily-voice/index.css +679 -12
- package/dist/board/daily-voice/index.js +185 -66
- package/dist/board/itc/index.css +679 -13
- package/dist/board/itc/index.js +188 -66
- package/dist/index.js +2864 -139
- package/package.json +3 -2
- package/plugins/generations/content-gen/dist/index.js +158 -12
- package/plugins/generations/core-facts-gen/dist/index.js +158 -12
- package/plugins/generations/daily-article-gen/dist/index.js +158 -12
- package/plugins/generations/dailynews-gen/dist/index.js +158 -12
- package/plugins/generations/episode-plan-gen/dist/index.js +158 -12
- package/plugins/generations/gemini-tts/README.md +4 -3
- package/plugins/generations/gemini-tts/dist/index.js +158 -12
- package/plugins/generations/image-gen/dist/index.js +158 -12
- package/plugins/generations/news-report-gen/dist/index.js +158 -12
- package/plugins/generations/omni-voice-gen/README.md +15 -4
- package/plugins/generations/omni-voice-gen/profiles.json +1 -2
- package/plugins/generations/voice-gen/dist/index.js +158 -12
- package/plugins/generations/voxcpm-tts/README.md +21 -1
- package/plugins/generations/voxcpm-tts/dist/index.js +173 -13
- package/plugins/generations/voxcpm-tts/scripts/audit-speaker-consistency.py +109 -0
- package/plugins/generations/voxcpm-tts/src/voxcpm-client.ts +28 -1
- package/plugins/ingestions/aihot-ingest/dist/index.js +158 -12
- package/plugins/ingestions/horizon-ingest/dist/index.js +158 -12
- package/plugins/ingestions/last30days-ingest/dist/index.js +158 -12
- package/plugins/ingestions/web-search/dist/index.js +158 -12
- package/plugins/kk/commands/workflow-run.md +56 -18
- package/plugins/kk/decisions/kk-itc.yaml +46 -0
- package/plugins/kk/decisions/kk-seo-keyword.yaml +57 -0
- package/plugins/kk/decisions/kk-seo-page.yaml +230 -0
- package/plugins/kk/decisions/kk-seo-pair.yaml +21 -0
- package/plugins/kk/decisions/kk-seo-site.yaml +90 -0
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/publish-presets/chinese-article.json +14 -0
- package/plugins/kk/publish-presets/technical-article.json +14 -0
- package/plugins/kk/references/smart-seo-attribution.md +85 -0
- package/plugins/kk/scripts/daily-stages.ts +42 -2
- package/plugins/kk/scripts/itc-stages.ts +25 -118
- package/plugins/kk/scripts/kk-workflow-stages.ts +38 -1
- package/plugins/kk/scripts/seo-decisions.ts +477 -0
- package/plugins/kk/scripts/seo-enrich.ts +728 -0
- package/plugins/kk/scripts/seo-facts.ts +610 -0
- package/plugins/kk/scripts/seo-fetch.ts +766 -0
- package/plugins/kk/scripts/seo-report.ts +460 -0
- package/plugins/kk/scripts/seo-rules.ts +1882 -0
- package/plugins/kk/scripts/seo-stages.ts +1044 -0
- package/plugins/kk/skills/article-adapt/SKILL.md +2 -2
- package/plugins/kk/skills/smart-seo/SKILL.md +183 -0
- package/plugins/kk/skills/smart-seo/references/method.md +126 -0
- package/plugins/kk/skills/smart-seo/references/narrative.md +125 -0
- package/plugins/kk/skills/smart-seo/references/operations.md +202 -0
- package/plugins/kk/skills/smart-seo/references/seo-geo-sources.md +90 -0
- package/plugins/kk/workflows/feature-verification.yaml +28 -24
- package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +85 -17
- package/plugins/kk/workflows/kk-itc-author.yaml +406 -0
- package/plugins/kk/workflows/kk-itc.yaml +208 -682
- package/plugins/kk/workflows/kk-smart-seo.yaml +251 -0
- package/plugins/publishings/bilibili-pub/dist/index.js +21782 -0
- package/plugins/publishings/bilibili-pub/dist/profile-check.js +20410 -0
- package/plugins/publishings/bilibili-pub/package.json +20 -0
- package/plugins/publishings/bilibili-pub/plugin.json +12 -0
- package/plugins/publishings/bilibili-pub/src/flow.ts +1296 -0
- package/plugins/publishings/bilibili-pub/src/index.ts +139 -0
- package/plugins/publishings/bilibili-pub/src/profile-check.ts +19 -0
- package/plugins/publishings/bilibili-pub/src/selectors.ts +222 -0
- package/plugins/publishings/bilibili-pub/tsconfig.json +4 -0
- package/plugins/publishings/cnblogs-pub/dist/index.js +21383 -0
- package/plugins/publishings/cnblogs-pub/dist/profile-check.js +20412 -0
- package/plugins/publishings/cnblogs-pub/package.json +19 -0
- package/plugins/publishings/cnblogs-pub/plugin.json +12 -0
- package/plugins/publishings/cnblogs-pub/src/flow.ts +708 -0
- package/plugins/publishings/cnblogs-pub/src/index.ts +172 -0
- package/plugins/publishings/cnblogs-pub/src/profile-check.ts +24 -0
- package/plugins/publishings/cnblogs-pub/src/selectors.ts +104 -0
- package/plugins/publishings/cnblogs-pub/tsconfig.json +4 -0
- package/plugins/publishings/csdn-pub/dist/index.js +21552 -0
- package/plugins/publishings/csdn-pub/dist/profile-check.js +20963 -0
- package/plugins/publishings/csdn-pub/package.json +19 -0
- package/plugins/publishings/csdn-pub/plugin.json +12 -0
- package/plugins/publishings/csdn-pub/src/index.ts +1178 -0
- package/plugins/publishings/csdn-pub/src/profile-check.ts +23 -0
- package/plugins/publishings/csdn-pub/tsconfig.json +4 -0
- package/plugins/publishings/emdash-pub/dist/index.js +158 -12
- package/plugins/publishings/emdash-pub/plugin.json +22 -1
- package/plugins/publishings/infoq-pub/dist/index.js +160 -12
- package/plugins/publishings/infoq-pub/dist/profile-check.js +160 -12
- package/plugins/publishings/infoq-pub/plugin.json +21 -0
- package/plugins/publishings/jike-pub/dist/index.js +21379 -0
- package/plugins/publishings/jike-pub/dist/profile-check.js +20951 -0
- package/plugins/publishings/jike-pub/package.json +19 -0
- package/plugins/publishings/jike-pub/plugin.json +12 -0
- package/plugins/publishings/jike-pub/src/index.ts +800 -0
- package/plugins/publishings/jike-pub/src/profile-check.ts +21 -0
- package/plugins/publishings/jike-pub/tsconfig.json +4 -0
- package/plugins/publishings/juejin-pub/dist/index.js +160 -12
- package/plugins/publishings/juejin-pub/dist/profile-check.js +160 -12
- package/plugins/publishings/juejin-pub/plugin.json +21 -0
- package/plugins/publishings/medium-pub/dist/index.js +158 -12
- package/plugins/publishings/medium-pub/plugin.json +22 -1
- package/plugins/publishings/podcast-pub/dist/index.js +158 -12
- package/plugins/publishings/qiita-pub/dist/index.js +158 -12
- package/plugins/publishings/qiita-pub/plugin.json +24 -1
- package/plugins/publishings/shipinhao-pub/dist/index.js +21715 -0
- package/plugins/publishings/shipinhao-pub/dist/profile-check.js +20431 -0
- package/plugins/publishings/shipinhao-pub/package.json +19 -0
- package/plugins/publishings/shipinhao-pub/plugin.json +12 -0
- package/plugins/publishings/shipinhao-pub/src/flow.ts +1323 -0
- package/plugins/publishings/shipinhao-pub/src/index.ts +173 -0
- package/plugins/publishings/shipinhao-pub/src/profile-check.ts +25 -0
- package/plugins/publishings/shipinhao-pub/src/selectors.ts +241 -0
- package/plugins/publishings/shipinhao-pub/tsconfig.json +4 -0
- package/plugins/publishings/sspai-pub/dist/index.js +21371 -0
- package/plugins/publishings/sspai-pub/dist/profile-check.js +20414 -0
- package/plugins/publishings/sspai-pub/package.json +19 -0
- package/plugins/publishings/sspai-pub/plugin.json +12 -0
- package/plugins/publishings/sspai-pub/src/flow.ts +624 -0
- package/plugins/publishings/sspai-pub/src/index.ts +167 -0
- package/plugins/publishings/sspai-pub/src/profile-check.ts +25 -0
- package/plugins/publishings/sspai-pub/src/selectors.ts +122 -0
- package/plugins/publishings/sspai-pub/tsconfig.json +4 -0
- package/plugins/publishings/substack-pub/dist/index.js +160 -12
- package/plugins/publishings/substack-pub/dist/profile-check.js +160 -12
- package/plugins/publishings/substack-pub/plugin.json +21 -0
- package/plugins/publishings/surfdash-pub/dist/index.js +158 -12
- package/plugins/publishings/surfdash-pub/plugin.json +22 -1
- package/plugins/publishings/threads-pub/dist/index.js +158 -12
- package/plugins/publishings/wechatmp-pub/dist/index.js +1669 -502
- package/plugins/publishings/wechatmp-pub/dist/profile-check.js +204 -21
- package/plugins/publishings/wechatmp-pub/plugin.json +32 -0
- package/plugins/publishings/wechatmp-pub/src/cgi-draft.ts +540 -0
- package/plugins/publishings/wechatmp-pub/src/cgi-media.ts +380 -0
- package/plugins/publishings/wechatmp-pub/src/cgi.ts +485 -0
- package/plugins/publishings/wechatmp-pub/src/flow.ts +104 -0
- package/plugins/publishings/weibo-pub/dist/index.js +160 -12
- package/plugins/publishings/weibo-pub/dist/profile-check.js +160 -12
- package/plugins/publishings/weibo-pub/plugin.json +35 -0
- package/plugins/publishings/x-pub/dist/index.js +160 -12
- package/plugins/publishings/x-pub/dist/profile-check.js +160 -12
- package/plugins/publishings/x-pub/plugin.json +40 -0
- package/plugins/publishings/xhs-pub/dist/index.js +160 -12
- package/plugins/publishings/xhs-pub/dist/profile-check.js +160 -12
- package/plugins/publishings/xhs-pub/plugin.json +37 -0
- package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +21491 -0
- package/plugins/publishings/xiaoyuzhou-pub/dist/profile-check.js +20417 -0
- package/plugins/publishings/xiaoyuzhou-pub/package.json +19 -0
- package/plugins/publishings/xiaoyuzhou-pub/plugin.json +12 -0
- package/plugins/publishings/xiaoyuzhou-pub/src/flow.ts +826 -0
- package/plugins/publishings/xiaoyuzhou-pub/src/index.ts +174 -0
- package/plugins/publishings/xiaoyuzhou-pub/src/profile-check.ts +26 -0
- package/plugins/publishings/xiaoyuzhou-pub/src/selectors.ts +125 -0
- package/plugins/publishings/xiaoyuzhou-pub/tsconfig.json +4 -0
- package/plugins/publishings/zenn-pub/dist/index.js +158 -12
- package/plugins/publishings/zenn-pub/plugin.json +22 -1
- 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
|
+
};
|
|
@@ -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
|
|
19937
|
-
|
|
19938
|
-
|
|
19939
|
-
|
|
19940
|
-
|
|
19941
|
-
|
|
19942
|
-
|
|
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.
|
|
20091
|
+
if (manifest.publishing !== undefined && manifest.kind !== "publish") {
|
|
19946
20092
|
ctx.addIssue({
|
|
19947
20093
|
code: "custom",
|
|
19948
|
-
path: ["
|
|
19949
|
-
message:
|
|
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;
|