@gobing-ai/knowledge-kit 0.0.31 → 0.0.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/dist/index.js +10 -10
- package/package.json +4 -4
- package/plugins/generations/content-gen/dist/index.js +9 -9
- package/plugins/generations/core-facts-gen/dist/index.js +12 -12
- package/plugins/generations/daily-article-gen/dist/index.js +12 -12
- package/plugins/generations/dailynews-gen/dist/index.js +9 -9
- package/plugins/generations/episode-plan-gen/dist/index.js +12 -12
- package/plugins/generations/gemini-tts/dist/index.js +9 -9
- package/plugins/generations/image-gen/dist/index.js +9 -9
- package/plugins/generations/news-report-gen/dist/index.js +12 -12
- package/plugins/generations/voice-gen/dist/index.js +9 -9
- package/plugins/ingestions/aihot-ingest/dist/index.js +12 -12
- package/plugins/ingestions/horizon-ingest/dist/index.js +12 -12
- package/plugins/ingestions/karakeep-local/dist/index.js +13 -13
- package/plugins/ingestions/last30days-ingest/dist/index.js +12 -12
- package/plugins/ingestions/web-search/dist/index.js +10 -10
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/scripts/kk-workflow-stages.ts +173 -15
- package/plugins/kk/skills/content-judge/SKILL.md +12 -2
- package/plugins/kk/skills/content-judge/references/rubrics.md +8 -2
- package/plugins/kk/skills/content-safety/SKILL.md +24 -13
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +65 -8
- package/plugins/publishings/emdash-pub/dist/index.js +9 -9
- package/plugins/publishings/infoq-pub/dist/index.js +8602 -211
- package/plugins/publishings/infoq-pub/package.json +3 -1
- package/plugins/publishings/infoq-pub/plugin.json +1 -1
- package/plugins/publishings/infoq-pub/src/index.ts +246 -29
- package/plugins/publishings/infoq-pub/src/profile-check.ts +19 -0
- package/plugins/publishings/juejin-pub/dist/index.js +8665 -211
- package/plugins/publishings/juejin-pub/package.json +3 -1
- package/plugins/publishings/juejin-pub/plugin.json +1 -1
- package/plugins/publishings/juejin-pub/src/index.ts +305 -29
- package/plugins/publishings/juejin-pub/src/profile-check.ts +19 -0
- package/plugins/publishings/medium-pub/dist/index.js +9 -9
- package/plugins/publishings/podcast-pub/dist/index.js +9 -9
- package/plugins/publishings/qiita-pub/dist/index.js +9 -9
- package/plugins/publishings/substack-pub/dist/index.js +8610 -211
- package/plugins/publishings/substack-pub/package.json +3 -1
- package/plugins/publishings/substack-pub/plugin.json +1 -1
- package/plugins/publishings/substack-pub/src/index.ts +271 -29
- package/plugins/publishings/substack-pub/src/profile-check.ts +74 -0
- package/plugins/publishings/surfdash-pub/dist/index.js +9 -9
- package/plugins/publishings/wechatmp-pub/dist/index.js +1227 -348
- package/plugins/publishings/wechatmp-pub/src/enrich.ts +229 -0
- package/plugins/publishings/wechatmp-pub/src/flow.ts +896 -0
- package/plugins/publishings/wechatmp-pub/src/index.ts +186 -516
- package/plugins/publishings/wechatmp-pub/src/modes.ts +403 -0
- package/plugins/publishings/wechatmp-pub/src/page.ts +260 -0
- package/plugins/publishings/wechatmp-pub/src/profile-check.ts +7 -5
- package/plugins/publishings/x-pub/dist/index.js +8626 -221
- package/plugins/publishings/x-pub/package.json +3 -1
- package/plugins/publishings/x-pub/plugin.json +1 -1
- package/plugins/publishings/x-pub/src/index.ts +258 -20
- package/plugins/publishings/x-pub/src/profile-check.ts +19 -0
- package/plugins/publishings/xhs-pub/dist/index.js +843 -346
- package/plugins/publishings/xhs-pub/plugin.json +1 -1
- package/plugins/publishings/xhs-pub/src/flow.ts +572 -0
- package/plugins/publishings/xhs-pub/src/index.ts +101 -593
- package/plugins/publishings/xhs-pub/src/modes.ts +300 -0
- package/plugins/publishings/xhs-pub/src/page.ts +267 -0
- package/plugins/publishings/xhs-pub/src/profile-check.ts +2 -3
- package/plugins/publishings/zenn-pub/dist/index.js +9 -9
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
},
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@gobing-ai/kk-core": "workspace:*",
|
|
10
|
+
"@gobing-ai/ts-browser-automation": "catalog:",
|
|
10
11
|
"@gobing-ai/publish-harness": "workspace:*",
|
|
11
12
|
"@gobing-ai/ts-runtime": "catalog:",
|
|
12
|
-
"@gobing-ai/ts-utils": "catalog:"
|
|
13
|
+
"@gobing-ai/ts-utils": "catalog:",
|
|
14
|
+
"playwright": "catalog:"
|
|
13
15
|
},
|
|
14
16
|
"devDependencies": {
|
|
15
17
|
"@types/bun": "1.3.14"
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
"kind": "publish",
|
|
4
4
|
"entry": "./dist/index.js",
|
|
5
5
|
"version": "1.0.0",
|
|
6
|
-
"description": "Publish plugin for Juejin (juejin.cn) via browser
|
|
6
|
+
"description": "Publish plugin for Juejin (juejin.cn): article drafts through the bytemd editor via script browser transport (ts-browser-automation)"
|
|
7
7
|
}
|
|
@@ -6,63 +6,339 @@ import {
|
|
|
6
6
|
FakeBrowserTransport,
|
|
7
7
|
type PublishTransport,
|
|
8
8
|
type PublishTransportPayload,
|
|
9
|
-
|
|
9
|
+
trySelectors,
|
|
10
10
|
} from '@gobing-ai/publish-harness';
|
|
11
|
+
import {
|
|
12
|
+
type ChannelBrowserConfig,
|
|
13
|
+
checkChannelSession,
|
|
14
|
+
expectOutcome,
|
|
15
|
+
fillCodeMirror,
|
|
16
|
+
readScopedSignal,
|
|
17
|
+
resolveChannelProfileDir,
|
|
18
|
+
resolveSelector,
|
|
19
|
+
waitUntil,
|
|
20
|
+
} from '@gobing-ai/publish-harness/browser';
|
|
21
|
+
import {
|
|
22
|
+
type BrowserLauncher,
|
|
23
|
+
BrowserProfileMissingError,
|
|
24
|
+
type IsAuthenticatedPredicate,
|
|
25
|
+
LoginTimeoutError,
|
|
26
|
+
withProfilePage,
|
|
27
|
+
} from '@gobing-ai/ts-browser-automation';
|
|
11
28
|
import { createNodeFileSystem } from '@gobing-ai/ts-runtime';
|
|
12
29
|
import { echoError } from '@gobing-ai/ts-utils';
|
|
30
|
+
import type { Page } from 'playwright';
|
|
13
31
|
|
|
14
|
-
|
|
32
|
+
/** Channel slug: names the Result target and resolves the `JUEJIN_PROFILE_DIR` tier. */
|
|
33
|
+
export const CHANNEL = 'juejin';
|
|
15
34
|
const TARGET = 'juejin';
|
|
35
|
+
/** Juejin's new-draft editor — the seeded URL the operator dogfood re-measures (R1). */
|
|
36
|
+
export const EDITOR_URL = 'https://juejin.cn/editor/drafts/new?v=2';
|
|
37
|
+
/** Login/SSO URLs that mean "not authenticated" — the editor never renders behind them. */
|
|
38
|
+
export const LOGIN_URL_RE = /(\/login|passport\.juejin\.cn)/;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Selectors measured against the Juejin editor (wt seed `juejin-playwright.ts:26-62`); each list is
|
|
42
|
+
* tried in order and the operator dogfood re-measures them. `publish` opens the live popup and
|
|
43
|
+
* `confirm` publishes it — both live-only (E91-D5).
|
|
44
|
+
*/
|
|
45
|
+
export const SELECTORS = {
|
|
46
|
+
title: ['input.title-input', 'input[placeholder*="标题"]'],
|
|
47
|
+
body: ['.bytemd-editor .CodeMirror', '.CodeMirror', '.cm-editor'],
|
|
48
|
+
publish: ['button:has-text("发布")'],
|
|
49
|
+
category: '.category-list .item',
|
|
50
|
+
tagInput: '.tag-input input',
|
|
51
|
+
confirm: ['button:has-text("确定并发布")'],
|
|
52
|
+
} as const;
|
|
53
|
+
|
|
54
|
+
const STEP_TIMEOUT_MS = 30_000;
|
|
55
|
+
const LOGIN_TIMEOUT_MS = 180_000;
|
|
56
|
+
/** Juejin autosaves the draft on input: the design's window before the one manual save key press. */
|
|
57
|
+
const AUTOSAVE_TIMEOUT_MS = 5_000;
|
|
58
|
+
const SAVE_KEY = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
|
|
59
|
+
/** Operator recovery command every session failure names — never a cookie, page HTML or stack. */
|
|
60
|
+
const LOGIN_INSTRUCTION = 'bun plugins/publishings/juejin-pub/src/profile-check.ts login';
|
|
61
|
+
/** A saved draft lands on `/editor/drafts/<id>`; a live publish on `/post/<id>` or `/published/<id>`. */
|
|
62
|
+
const DRAFT_URL_RE = /\/editor\/drafts\/(\d+)/;
|
|
63
|
+
const LIVE_SIGNAL_RE = /juejin\.cn\/(post|published)\//;
|
|
64
|
+
const LIVE_URL_ID_RE = /juejin\.cn\/(?:post|published)\/(\d+)/;
|
|
65
|
+
const DRAFT_TOASTS = ['保存成功'];
|
|
66
|
+
const LIVE_TOASTS = ['发布成功'];
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Juejin-owned readiness predicate: the editor is reachable (no login redirect) and the title field
|
|
70
|
+
* is attached. Shared by the publish preflight and the profile-session CLI (R1, R5).
|
|
71
|
+
*/
|
|
72
|
+
export const isAuthenticated: IsAuthenticatedPredicate = async (page) =>
|
|
73
|
+
!LOGIN_URL_RE.test(page.url()) && (await trySelectors(page, SELECTORS.title, { visible: false })).found;
|
|
74
|
+
|
|
75
|
+
/** Site-owned config for the shared session check and operator CLI (0183). */
|
|
76
|
+
export const CONFIG: ChannelBrowserConfig = {
|
|
77
|
+
channel: CHANNEL,
|
|
78
|
+
homeUrl: EDITOR_URL,
|
|
79
|
+
isAuthenticated,
|
|
80
|
+
loginTimeoutMs: LOGIN_TIMEOUT_MS,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** Validated live-run inputs: the publish popup cannot confirm without both (R4). */
|
|
84
|
+
interface LiveInputs {
|
|
85
|
+
category: string;
|
|
86
|
+
tags: string[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* One run shape: a draft run needs nothing beyond the content, a live run carries the validated
|
|
91
|
+
* popup inputs — so "live without a category or tags" is not a representable state.
|
|
92
|
+
*/
|
|
93
|
+
type EditorRun =
|
|
94
|
+
| { publishStatus: 'draft'; profileDir: string }
|
|
95
|
+
| { publishStatus: 'public'; live: LiveInputs; profileDir: string };
|
|
16
96
|
|
|
17
97
|
function fail(error: string): Result {
|
|
18
98
|
return ResultSchema.parse({ ok: false, target: TARGET, error });
|
|
19
99
|
}
|
|
20
100
|
|
|
21
|
-
function
|
|
22
|
-
|
|
23
|
-
return `${CHANNEL}-draft-${title.length}`;
|
|
101
|
+
function errorMessage(error: unknown): string {
|
|
102
|
+
return error instanceof Error ? error.message : String(error);
|
|
24
103
|
}
|
|
25
104
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
105
|
+
/** Session failures whose one-liner must name the recovery command (R5). */
|
|
106
|
+
function needsLogin(error: unknown): boolean {
|
|
107
|
+
return error instanceof LoginTimeoutError || error instanceof BrowserProfileMissingError;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* `确定并发布` needs a category and at least one tag, so a live run names the missing field instead
|
|
112
|
+
* of opening a browser it cannot finish in. Returns the validated inputs, or the failure message.
|
|
113
|
+
*/
|
|
114
|
+
function liveInputs(content: Content): LiveInputs | string {
|
|
115
|
+
const category = content.metadata?.category;
|
|
116
|
+
if (typeof category !== 'string' || category.trim().length === 0) {
|
|
117
|
+
return 'live publish requires content.metadata.category (a Juejin category name)';
|
|
118
|
+
}
|
|
119
|
+
const raw = content.metadata?.tags;
|
|
120
|
+
const tags = Array.isArray(raw)
|
|
121
|
+
? raw.filter((tag): tag is string => typeof tag === 'string' && tag.trim().length > 0)
|
|
122
|
+
: [];
|
|
123
|
+
// An absent, empty or partly unusable tag list is named as the missing field rather than
|
|
124
|
+
// silently trimmed down to the tags that happened to parse.
|
|
125
|
+
if (!Array.isArray(raw) || tags.length === 0 || tags.length !== raw.length) {
|
|
126
|
+
return 'live publish requires content.metadata.tags (at least one non-empty tag)';
|
|
127
|
+
}
|
|
128
|
+
return { category: category.trim(), tags };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Text of the first visible toast among `texts`, or null. `:text(…)` matches on a substring, so
|
|
133
|
+
* the element's own text is returned rather than the candidate literal.
|
|
134
|
+
*/
|
|
135
|
+
/**
|
|
136
|
+
* The draft/live outcome, read only from the page's notification containers (residual 0196): an
|
|
137
|
+
* unscoped `:text(...)` match also hits the article body this flow filled a few steps earlier, so
|
|
138
|
+
* a body containing `保存成功` reported a draft that was never saved.
|
|
139
|
+
*/
|
|
140
|
+
async function readToast(page: Page, texts: readonly string[]): Promise<string | null> {
|
|
141
|
+
return readScopedSignal(page, texts);
|
|
34
142
|
}
|
|
35
143
|
|
|
36
144
|
export class JuejinBrowserTransport implements PublishTransport {
|
|
37
145
|
public readonly kind = 'browser';
|
|
38
146
|
public readonly driver: BrowserDriver;
|
|
39
|
-
private readonly
|
|
40
|
-
private readonly
|
|
147
|
+
private readonly launcher?: BrowserLauncher;
|
|
148
|
+
private readonly stepTimeoutMs: number;
|
|
41
149
|
|
|
42
|
-
constructor(driver: BrowserDriver = '
|
|
150
|
+
constructor(driver: BrowserDriver = 'script', deps: { launcher?: BrowserLauncher; stepTimeoutMs?: number } = {}) {
|
|
43
151
|
this.driver = driver;
|
|
44
|
-
this.
|
|
45
|
-
this.
|
|
152
|
+
this.launcher = deps.launcher;
|
|
153
|
+
this.stepTimeoutMs = deps.stepTimeoutMs ?? STEP_TIMEOUT_MS;
|
|
46
154
|
}
|
|
47
155
|
|
|
48
156
|
public async publish(payload: PublishTransportPayload): Promise<Result> {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
157
|
+
if (this.driver !== 'script') {
|
|
158
|
+
return fail(`unsupported driver '${this.driver}' for ${TARGET}`);
|
|
159
|
+
}
|
|
160
|
+
const requested = payload.content.metadata?.publishStatus;
|
|
161
|
+
if (requested !== undefined && requested !== 'draft' && requested !== 'public') {
|
|
162
|
+
return fail(`unknown publishStatus '${String(requested)}' (expected absent, 'draft' or 'public')`);
|
|
163
|
+
}
|
|
164
|
+
// Live pre-validation, before any browser opens: the popup's two prerequisites (R4).
|
|
165
|
+
let live: LiveInputs | undefined;
|
|
166
|
+
if (requested === 'public') {
|
|
167
|
+
const inputs = liveInputs(payload.content);
|
|
168
|
+
if (typeof inputs === 'string') return fail(inputs);
|
|
169
|
+
live = inputs;
|
|
170
|
+
}
|
|
171
|
+
const override = typeof payload.options?.profileDir === 'string' ? payload.options.profileDir : undefined;
|
|
172
|
+
const profileDir = resolveChannelProfileDir(CHANNEL, override);
|
|
173
|
+
|
|
174
|
+
// ADR-025 preflight: an unauthenticated or absent profile must never produce a headed
|
|
175
|
+
// visit, so the read-only headless check runs first and a refusal stays a fast refusal.
|
|
176
|
+
const session = await checkChannelSession(CONFIG, {
|
|
177
|
+
profileDir,
|
|
178
|
+
launcher: this.launcher,
|
|
179
|
+
headedFallback: false,
|
|
180
|
+
timeoutMs: this.stepTimeoutMs,
|
|
181
|
+
});
|
|
182
|
+
if (session.status !== 'verified') {
|
|
183
|
+
return fail(`${CHANNEL} session preflight refused: ${session.detail} — run: ${LOGIN_INSTRUCTION}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const run: EditorRun =
|
|
187
|
+
live === undefined ? { publishStatus: 'draft', profileDir } : { publishStatus: 'public', live, profileDir };
|
|
188
|
+
try {
|
|
189
|
+
return await withProfilePage(
|
|
190
|
+
{
|
|
191
|
+
profileDir,
|
|
192
|
+
url: EDITOR_URL,
|
|
193
|
+
isAuthenticated,
|
|
194
|
+
// Headed by default (sites bot-wall headless loads); the headless preflight
|
|
195
|
+
// above is the unattended pass, `options.headless === true` the opt-in.
|
|
196
|
+
headless: payload.options?.headless === true,
|
|
197
|
+
launcher: this.launcher,
|
|
198
|
+
// The session is already preflight-verified, so the post-goto confirmation is
|
|
199
|
+
// a bounded step like every other one, not the 180s interactive login wait.
|
|
200
|
+
timeoutMs: this.stepTimeoutMs,
|
|
201
|
+
},
|
|
202
|
+
async ({ page }) => this.runEditorFlow(page, payload.content, run),
|
|
57
203
|
);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
const hint = needsLogin(error) ? ` — run: ${LOGIN_INSTRUCTION}` : '';
|
|
206
|
+
return fail(`${errorMessage(error)}${hint}`);
|
|
58
207
|
}
|
|
59
|
-
return draftResult(payload.content, this.driver, profileDir);
|
|
60
208
|
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* The site-owned editor flow: title → markdown body → draft signal, then — for a live run only
|
|
212
|
+
* — `发布` → category → tags → `确定并发布` → live signal. Every outcome comes from an
|
|
213
|
+
* `expectOutcome` probe, so a wait or a click without an observed signal is never a success
|
|
214
|
+
* (E91-D6), and a draft run never touches a live control (E91-D5).
|
|
215
|
+
*/
|
|
216
|
+
private async runEditorFlow(page: Page, content: Content, run: EditorRun): Promise<Result> {
|
|
217
|
+
const metadata = { driver: this.driver, publishStatus: run.publishStatus, profileDir: run.profileDir };
|
|
218
|
+
|
|
219
|
+
const titleSelector = await resolveSelector(page, SELECTORS.title, {
|
|
220
|
+
timeoutMs: this.stepTimeoutMs,
|
|
221
|
+
label: 'title input',
|
|
222
|
+
});
|
|
223
|
+
await page
|
|
224
|
+
.locator(titleSelector)
|
|
225
|
+
.first()
|
|
226
|
+
.fill(content.title ?? '');
|
|
227
|
+
|
|
228
|
+
const bodySelector = await resolveSelector(page, SELECTORS.body, {
|
|
229
|
+
timeoutMs: this.stepTimeoutMs,
|
|
230
|
+
label: 'body editor',
|
|
231
|
+
});
|
|
232
|
+
// bytemd is CodeMirror over markdown: the body goes in as markdown, never as HTML (E91-D4).
|
|
233
|
+
await fillCodeMirror(page, bodySelector, content.body, { timeoutMs: this.stepTimeoutMs });
|
|
234
|
+
|
|
235
|
+
// Draft evidence (E91-D6): the autosave gets its window, then the manual save key is pressed
|
|
236
|
+
// once when the page showed nothing. Success still needs the observed signal.
|
|
237
|
+
const autosaveMs = Math.min(AUTOSAVE_TIMEOUT_MS, this.stepTimeoutMs);
|
|
238
|
+
if (!(await waitUntil(async () => (await probeDraft(page)) !== null, autosaveMs))) {
|
|
239
|
+
await page.keyboard.press(SAVE_KEY);
|
|
240
|
+
}
|
|
241
|
+
const draft = await expectOutcome(() => probeDraft(page), {
|
|
242
|
+
timeoutMs: this.stepTimeoutMs,
|
|
243
|
+
label: `juejin draft save in '${bodySelector}'`,
|
|
244
|
+
});
|
|
245
|
+
if (!draft.ok) return fail(draft.error);
|
|
246
|
+
|
|
247
|
+
if (run.publishStatus === 'draft') {
|
|
248
|
+
const url = page.url();
|
|
249
|
+
const id = idFromUrl(url);
|
|
250
|
+
return ResultSchema.parse({
|
|
251
|
+
ok: true,
|
|
252
|
+
target: TARGET,
|
|
253
|
+
...(id === undefined ? {} : { id }),
|
|
254
|
+
url,
|
|
255
|
+
message: `${TARGET} draft saved (unpublished) in profile ${run.profileDir}`,
|
|
256
|
+
metadata,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const publishSelector = await resolveSelector(page, SELECTORS.publish, {
|
|
261
|
+
timeoutMs: this.stepTimeoutMs,
|
|
262
|
+
label: 'publish control',
|
|
263
|
+
});
|
|
264
|
+
await page.locator(publishSelector).first().click();
|
|
265
|
+
await fillPublishPopup(page, run.live, this.stepTimeoutMs);
|
|
266
|
+
|
|
267
|
+
const confirmSelector = await resolveSelector(page, SELECTORS.confirm, {
|
|
268
|
+
timeoutMs: this.stepTimeoutMs,
|
|
269
|
+
label: 'confirm control',
|
|
270
|
+
});
|
|
271
|
+
await page.locator(confirmSelector).first().click();
|
|
272
|
+
const live = await expectOutcome(() => probeLive(page), {
|
|
273
|
+
timeoutMs: this.stepTimeoutMs,
|
|
274
|
+
label: `juejin publish '${confirmSelector}'`,
|
|
275
|
+
});
|
|
276
|
+
if (!live.ok) return fail(live.error);
|
|
277
|
+
|
|
278
|
+
const url = page.url();
|
|
279
|
+
const id = idFromUrl(url);
|
|
280
|
+
return ResultSchema.parse({
|
|
281
|
+
ok: true,
|
|
282
|
+
target: TARGET,
|
|
283
|
+
...(id === undefined ? {} : { id }),
|
|
284
|
+
url,
|
|
285
|
+
message: `${TARGET} published live in profile ${run.profileDir}`,
|
|
286
|
+
metadata,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Live popup (E91-D5): the category item whose text is exactly `metadata.category`, then every tag
|
|
293
|
+
* typed into the tag input with `Enter`. Runs only on the `public` path — `发布` opens this popup.
|
|
294
|
+
*/
|
|
295
|
+
async function fillPublishPopup(page: Page, live: LiveInputs, timeoutMs: number): Promise<void> {
|
|
296
|
+
await resolveSelector(page, [SELECTORS.category], { timeoutMs, label: 'category list' });
|
|
297
|
+
const items = page.locator(SELECTORS.category);
|
|
298
|
+
const total = await items.count();
|
|
299
|
+
let picked = false;
|
|
300
|
+
for (let index = 0; index < total; index += 1) {
|
|
301
|
+
const item = items.nth(index);
|
|
302
|
+
if (((await item.textContent()) ?? '').trim() === live.category) {
|
|
303
|
+
await item.click();
|
|
304
|
+
picked = true;
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (!picked) {
|
|
309
|
+
throw new Error(`category '${live.category}': no '${SELECTORS.category}' item matched its exact text`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const tagSelector = await resolveSelector(page, [SELECTORS.tagInput], { timeoutMs, label: 'tag input' });
|
|
313
|
+
for (const tag of live.tags) {
|
|
314
|
+
const input = page.locator(tagSelector).first();
|
|
315
|
+
await input.fill(tag);
|
|
316
|
+
await input.press('Enter');
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** The article id the observed URL carries: `/editor/drafts/<id>` (draft) or `/post/<id>` (live). */
|
|
321
|
+
function idFromUrl(url: string): string | undefined {
|
|
322
|
+
return DRAFT_URL_RE.exec(url)?.[1] ?? LIVE_URL_ID_RE.exec(url)?.[1];
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Draft evidence (E91-D6): the editor URL moved onto a draft id, or a visible `保存成功` toast. */
|
|
326
|
+
async function probeDraft(page: Page): Promise<string | null> {
|
|
327
|
+
const url = page.url();
|
|
328
|
+
if (DRAFT_URL_RE.test(url)) return url;
|
|
329
|
+
return readToast(page, DRAFT_TOASTS);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Live evidence (E91-D6): a `/post/`- or `/published/`-shaped URL, or a visible `发布成功` toast. */
|
|
333
|
+
async function probeLive(page: Page): Promise<string | null> {
|
|
334
|
+
const url = page.url();
|
|
335
|
+
if (LIVE_SIGNAL_RE.test(url)) return url;
|
|
336
|
+
return readToast(page, LIVE_TOASTS);
|
|
61
337
|
}
|
|
62
338
|
|
|
63
|
-
export function getPublishTransport(driver: BrowserDriver = '
|
|
339
|
+
export function getPublishTransport(driver: BrowserDriver = 'script'): PublishTransport {
|
|
64
340
|
if (process.env.KNOWLEDGE_KIT_PUBLISH_TRANSPORT === 'fake') {
|
|
65
|
-
return new FakeBrowserTransport(
|
|
341
|
+
return new FakeBrowserTransport('script');
|
|
66
342
|
}
|
|
67
343
|
return new JuejinBrowserTransport(driver);
|
|
68
344
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Juejin profile-session capability (R5, AC3).
|
|
3
|
+
*
|
|
4
|
+
* `login` opens the editor in a headed browser and resolves only once Juejin's readiness predicate
|
|
5
|
+
* passes; `check` reopens the profile headlessly and reports `verified` or `refused`. Arg parsing,
|
|
6
|
+
* profile precedence (`--profile` > `JUEJIN_PROFILE_DIR` > `KK_BROWSER_PROFILE_DIR` >
|
|
7
|
+
* `~/.config/kk/browser-profiles/juejin`) and the exit codes (0 verified, 1 refused, 2 usage error)
|
|
8
|
+
* all live in the shared `runProfileCli` (0183), so the publish preflight and this CLI apply the
|
|
9
|
+
* same gate.
|
|
10
|
+
*
|
|
11
|
+
* Operator dogfood (plan step 6, handed to 0192): run `profile-check.ts login`, then `check`, on
|
|
12
|
+
* the same profile, and re-measure the editor URL, the selectors and the draft signal.
|
|
13
|
+
*/
|
|
14
|
+
import { runProfileCli } from '@gobing-ai/publish-harness/browser';
|
|
15
|
+
import { CONFIG } from './index';
|
|
16
|
+
|
|
17
|
+
if (import.meta.main) {
|
|
18
|
+
process.exit(await runProfileCli(CONFIG, Bun.argv.slice(2)));
|
|
19
|
+
}
|
|
@@ -14642,7 +14642,7 @@ var init_zod = __esm(() => {
|
|
|
14642
14642
|
init_external();
|
|
14643
14643
|
});
|
|
14644
14644
|
|
|
14645
|
-
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.
|
|
14645
|
+
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/errors.js
|
|
14646
14646
|
var ErrorCode;
|
|
14647
14647
|
var init_errors3 = __esm(() => {
|
|
14648
14648
|
ErrorCode = {
|
|
@@ -14653,7 +14653,7 @@ var init_errors3 = __esm(() => {
|
|
|
14653
14653
|
};
|
|
14654
14654
|
});
|
|
14655
14655
|
|
|
14656
|
-
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.
|
|
14656
|
+
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/api-response.js
|
|
14657
14657
|
var API_ERROR_CODES, ERROR_CODE_TO_HTTP, CLIENT_SAFE_CODES;
|
|
14658
14658
|
var init_api_response = __esm(() => {
|
|
14659
14659
|
init_errors3();
|
|
@@ -14676,13 +14676,13 @@ var init_api_response = __esm(() => {
|
|
|
14676
14676
|
CLIENT_SAFE_CODES = new Set([ErrorCode.NotFound, ErrorCode.Validation, ErrorCode.Conflict]);
|
|
14677
14677
|
});
|
|
14678
14678
|
|
|
14679
|
-
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.
|
|
14679
|
+
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/cursor.js
|
|
14680
14680
|
var init_cursor = () => {};
|
|
14681
14681
|
|
|
14682
|
-
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.
|
|
14682
|
+
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/event-severity.js
|
|
14683
14683
|
var init_event_severity = () => {};
|
|
14684
14684
|
|
|
14685
|
-
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.
|
|
14685
|
+
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/output.js
|
|
14686
14686
|
function processStream(name) {
|
|
14687
14687
|
const proc = globalThis.process;
|
|
14688
14688
|
const stream = proc?.[name];
|
|
@@ -14700,7 +14700,7 @@ function echoError(message, target = defaultStderrTarget ?? processStream("stder
|
|
|
14700
14700
|
}
|
|
14701
14701
|
var defaultStderrTarget;
|
|
14702
14702
|
|
|
14703
|
-
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.
|
|
14703
|
+
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/index.js
|
|
14704
14704
|
var init_dist = __esm(() => {
|
|
14705
14705
|
init_api_response();
|
|
14706
14706
|
init_cursor();
|
|
@@ -21698,7 +21698,7 @@ var init_dist2 = __esm(() => {
|
|
|
21698
21698
|
$visitAsync = visit.visitAsync;
|
|
21699
21699
|
});
|
|
21700
21700
|
|
|
21701
|
-
// ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.
|
|
21701
|
+
// ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.15/node_modules/@gobing-ai/ts-runtime/dist/config.js
|
|
21702
21702
|
var configSchema;
|
|
21703
21703
|
var init_config = __esm(() => {
|
|
21704
21704
|
init_dist2();
|
|
@@ -21722,7 +21722,7 @@ var init_config = __esm(() => {
|
|
|
21722
21722
|
});
|
|
21723
21723
|
});
|
|
21724
21724
|
|
|
21725
|
-
// ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.
|
|
21725
|
+
// ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.15/node_modules/@gobing-ai/ts-runtime/dist/file-system-node.js
|
|
21726
21726
|
import { appendFileSync, cpSync, createReadStream, createWriteStream, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
|
|
21727
21727
|
import { dirname, resolve as resolvePath } from "path";
|
|
21728
21728
|
function createNodeFileSystem(root, paths) {
|
|
@@ -21900,7 +21900,7 @@ var PluginManifestSchema = exports_external.looseObject({
|
|
|
21900
21900
|
description: exports_external.string().optional(),
|
|
21901
21901
|
operations: exports_external.record(exports_external.string(), PluginOperationSchema).optional()
|
|
21902
21902
|
});
|
|
21903
|
-
// ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.
|
|
21903
|
+
// ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.15/node_modules/@gobing-ai/ts-runtime/dist/index.js
|
|
21904
21904
|
init_file_system_node();
|
|
21905
21905
|
init_config();
|
|
21906
21906
|
|
|
@@ -14642,7 +14642,7 @@ var init_zod = __esm(() => {
|
|
|
14642
14642
|
init_external();
|
|
14643
14643
|
});
|
|
14644
14644
|
|
|
14645
|
-
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.
|
|
14645
|
+
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/errors.js
|
|
14646
14646
|
var ErrorCode;
|
|
14647
14647
|
var init_errors3 = __esm(() => {
|
|
14648
14648
|
ErrorCode = {
|
|
@@ -14653,7 +14653,7 @@ var init_errors3 = __esm(() => {
|
|
|
14653
14653
|
};
|
|
14654
14654
|
});
|
|
14655
14655
|
|
|
14656
|
-
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.
|
|
14656
|
+
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/api-response.js
|
|
14657
14657
|
var API_ERROR_CODES, ERROR_CODE_TO_HTTP, CLIENT_SAFE_CODES;
|
|
14658
14658
|
var init_api_response = __esm(() => {
|
|
14659
14659
|
init_errors3();
|
|
@@ -14676,13 +14676,13 @@ var init_api_response = __esm(() => {
|
|
|
14676
14676
|
CLIENT_SAFE_CODES = new Set([ErrorCode.NotFound, ErrorCode.Validation, ErrorCode.Conflict]);
|
|
14677
14677
|
});
|
|
14678
14678
|
|
|
14679
|
-
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.
|
|
14679
|
+
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/cursor.js
|
|
14680
14680
|
var init_cursor = () => {};
|
|
14681
14681
|
|
|
14682
|
-
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.
|
|
14682
|
+
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/event-severity.js
|
|
14683
14683
|
var init_event_severity = () => {};
|
|
14684
14684
|
|
|
14685
|
-
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.
|
|
14685
|
+
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/output.js
|
|
14686
14686
|
function processStream(name) {
|
|
14687
14687
|
const proc = globalThis.process;
|
|
14688
14688
|
const stream = proc?.[name];
|
|
@@ -14700,7 +14700,7 @@ function echoError(message, target = defaultStderrTarget ?? processStream("stder
|
|
|
14700
14700
|
}
|
|
14701
14701
|
var defaultStderrTarget;
|
|
14702
14702
|
|
|
14703
|
-
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.
|
|
14703
|
+
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/index.js
|
|
14704
14704
|
var init_dist = __esm(() => {
|
|
14705
14705
|
init_api_response();
|
|
14706
14706
|
init_cursor();
|
|
@@ -21698,7 +21698,7 @@ var init_dist2 = __esm(() => {
|
|
|
21698
21698
|
$visitAsync = visit.visitAsync;
|
|
21699
21699
|
});
|
|
21700
21700
|
|
|
21701
|
-
// ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.
|
|
21701
|
+
// ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.15/node_modules/@gobing-ai/ts-runtime/dist/config.js
|
|
21702
21702
|
var configSchema;
|
|
21703
21703
|
var init_config = __esm(() => {
|
|
21704
21704
|
init_dist2();
|
|
@@ -21722,7 +21722,7 @@ var init_config = __esm(() => {
|
|
|
21722
21722
|
});
|
|
21723
21723
|
});
|
|
21724
21724
|
|
|
21725
|
-
// ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.
|
|
21725
|
+
// ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.15/node_modules/@gobing-ai/ts-runtime/dist/file-system-node.js
|
|
21726
21726
|
import { appendFileSync, cpSync, createReadStream, createWriteStream, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
|
|
21727
21727
|
import { dirname, resolve as resolvePath } from "path";
|
|
21728
21728
|
function createNodeFileSystem(root, paths) {
|
|
@@ -21902,7 +21902,7 @@ var PluginManifestSchema = exports_external.looseObject({
|
|
|
21902
21902
|
description: exports_external.string().optional(),
|
|
21903
21903
|
operations: exports_external.record(exports_external.string(), PluginOperationSchema).optional()
|
|
21904
21904
|
});
|
|
21905
|
-
// ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.
|
|
21905
|
+
// ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.15/node_modules/@gobing-ai/ts-runtime/dist/index.js
|
|
21906
21906
|
init_file_system_node();
|
|
21907
21907
|
init_config();
|
|
21908
21908
|
|
|
@@ -14642,7 +14642,7 @@ var init_zod = __esm(() => {
|
|
|
14642
14642
|
init_external();
|
|
14643
14643
|
});
|
|
14644
14644
|
|
|
14645
|
-
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.
|
|
14645
|
+
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/errors.js
|
|
14646
14646
|
var ErrorCode;
|
|
14647
14647
|
var init_errors3 = __esm(() => {
|
|
14648
14648
|
ErrorCode = {
|
|
@@ -14653,7 +14653,7 @@ var init_errors3 = __esm(() => {
|
|
|
14653
14653
|
};
|
|
14654
14654
|
});
|
|
14655
14655
|
|
|
14656
|
-
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.
|
|
14656
|
+
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/api-response.js
|
|
14657
14657
|
var API_ERROR_CODES, ERROR_CODE_TO_HTTP, CLIENT_SAFE_CODES;
|
|
14658
14658
|
var init_api_response = __esm(() => {
|
|
14659
14659
|
init_errors3();
|
|
@@ -14676,13 +14676,13 @@ var init_api_response = __esm(() => {
|
|
|
14676
14676
|
CLIENT_SAFE_CODES = new Set([ErrorCode.NotFound, ErrorCode.Validation, ErrorCode.Conflict]);
|
|
14677
14677
|
});
|
|
14678
14678
|
|
|
14679
|
-
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.
|
|
14679
|
+
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/cursor.js
|
|
14680
14680
|
var init_cursor = () => {};
|
|
14681
14681
|
|
|
14682
|
-
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.
|
|
14682
|
+
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/event-severity.js
|
|
14683
14683
|
var init_event_severity = () => {};
|
|
14684
14684
|
|
|
14685
|
-
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.
|
|
14685
|
+
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/output.js
|
|
14686
14686
|
function processStream(name) {
|
|
14687
14687
|
const proc = globalThis.process;
|
|
14688
14688
|
const stream = proc?.[name];
|
|
@@ -14700,7 +14700,7 @@ function echoError(message, target = defaultStderrTarget ?? processStream("stder
|
|
|
14700
14700
|
}
|
|
14701
14701
|
var defaultStderrTarget;
|
|
14702
14702
|
|
|
14703
|
-
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.
|
|
14703
|
+
// ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.15/node_modules/@gobing-ai/ts-utils/dist/index.js
|
|
14704
14704
|
var init_dist = __esm(() => {
|
|
14705
14705
|
init_api_response();
|
|
14706
14706
|
init_cursor();
|
|
@@ -21698,7 +21698,7 @@ var init_dist2 = __esm(() => {
|
|
|
21698
21698
|
$visitAsync = visit.visitAsync;
|
|
21699
21699
|
});
|
|
21700
21700
|
|
|
21701
|
-
// ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.
|
|
21701
|
+
// ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.15/node_modules/@gobing-ai/ts-runtime/dist/config.js
|
|
21702
21702
|
var configSchema;
|
|
21703
21703
|
var init_config = __esm(() => {
|
|
21704
21704
|
init_dist2();
|
|
@@ -21722,7 +21722,7 @@ var init_config = __esm(() => {
|
|
|
21722
21722
|
});
|
|
21723
21723
|
});
|
|
21724
21724
|
|
|
21725
|
-
// ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.
|
|
21725
|
+
// ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.15/node_modules/@gobing-ai/ts-runtime/dist/file-system-node.js
|
|
21726
21726
|
import { appendFileSync, cpSync, createReadStream, createWriteStream, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
|
|
21727
21727
|
import { dirname, resolve as resolvePath } from "path";
|
|
21728
21728
|
function createNodeFileSystem(root, paths) {
|
|
@@ -21900,7 +21900,7 @@ var PluginManifestSchema = exports_external.looseObject({
|
|
|
21900
21900
|
description: exports_external.string().optional(),
|
|
21901
21901
|
operations: exports_external.record(exports_external.string(), PluginOperationSchema).optional()
|
|
21902
21902
|
});
|
|
21903
|
-
// ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.
|
|
21903
|
+
// ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.15/node_modules/@gobing-ai/ts-runtime/dist/index.js
|
|
21904
21904
|
init_file_system_node();
|
|
21905
21905
|
init_config();
|
|
21906
21906
|
|