@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.
Files changed (63) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +10 -10
  3. package/package.json +4 -4
  4. package/plugins/generations/content-gen/dist/index.js +9 -9
  5. package/plugins/generations/core-facts-gen/dist/index.js +12 -12
  6. package/plugins/generations/daily-article-gen/dist/index.js +12 -12
  7. package/plugins/generations/dailynews-gen/dist/index.js +9 -9
  8. package/plugins/generations/episode-plan-gen/dist/index.js +12 -12
  9. package/plugins/generations/gemini-tts/dist/index.js +9 -9
  10. package/plugins/generations/image-gen/dist/index.js +9 -9
  11. package/plugins/generations/news-report-gen/dist/index.js +12 -12
  12. package/plugins/generations/voice-gen/dist/index.js +9 -9
  13. package/plugins/ingestions/aihot-ingest/dist/index.js +12 -12
  14. package/plugins/ingestions/horizon-ingest/dist/index.js +12 -12
  15. package/plugins/ingestions/karakeep-local/dist/index.js +13 -13
  16. package/plugins/ingestions/last30days-ingest/dist/index.js +12 -12
  17. package/plugins/ingestions/web-search/dist/index.js +10 -10
  18. package/plugins/kk/plugin.json +1 -1
  19. package/plugins/kk/scripts/kk-workflow-stages.ts +173 -15
  20. package/plugins/kk/skills/content-judge/SKILL.md +12 -2
  21. package/plugins/kk/skills/content-judge/references/rubrics.md +8 -2
  22. package/plugins/kk/skills/content-safety/SKILL.md +24 -13
  23. package/plugins/kk/workflows/kk-daily-ai-voice.yaml +65 -8
  24. package/plugins/publishings/emdash-pub/dist/index.js +9 -9
  25. package/plugins/publishings/infoq-pub/dist/index.js +8602 -211
  26. package/plugins/publishings/infoq-pub/package.json +3 -1
  27. package/plugins/publishings/infoq-pub/plugin.json +1 -1
  28. package/plugins/publishings/infoq-pub/src/index.ts +246 -29
  29. package/plugins/publishings/infoq-pub/src/profile-check.ts +19 -0
  30. package/plugins/publishings/juejin-pub/dist/index.js +8665 -211
  31. package/plugins/publishings/juejin-pub/package.json +3 -1
  32. package/plugins/publishings/juejin-pub/plugin.json +1 -1
  33. package/plugins/publishings/juejin-pub/src/index.ts +305 -29
  34. package/plugins/publishings/juejin-pub/src/profile-check.ts +19 -0
  35. package/plugins/publishings/medium-pub/dist/index.js +9 -9
  36. package/plugins/publishings/podcast-pub/dist/index.js +9 -9
  37. package/plugins/publishings/qiita-pub/dist/index.js +9 -9
  38. package/plugins/publishings/substack-pub/dist/index.js +8610 -211
  39. package/plugins/publishings/substack-pub/package.json +3 -1
  40. package/plugins/publishings/substack-pub/plugin.json +1 -1
  41. package/plugins/publishings/substack-pub/src/index.ts +271 -29
  42. package/plugins/publishings/substack-pub/src/profile-check.ts +74 -0
  43. package/plugins/publishings/surfdash-pub/dist/index.js +9 -9
  44. package/plugins/publishings/wechatmp-pub/dist/index.js +1227 -348
  45. package/plugins/publishings/wechatmp-pub/src/enrich.ts +229 -0
  46. package/plugins/publishings/wechatmp-pub/src/flow.ts +896 -0
  47. package/plugins/publishings/wechatmp-pub/src/index.ts +186 -516
  48. package/plugins/publishings/wechatmp-pub/src/modes.ts +403 -0
  49. package/plugins/publishings/wechatmp-pub/src/page.ts +260 -0
  50. package/plugins/publishings/wechatmp-pub/src/profile-check.ts +7 -5
  51. package/plugins/publishings/x-pub/dist/index.js +8626 -221
  52. package/plugins/publishings/x-pub/package.json +3 -1
  53. package/plugins/publishings/x-pub/plugin.json +1 -1
  54. package/plugins/publishings/x-pub/src/index.ts +258 -20
  55. package/plugins/publishings/x-pub/src/profile-check.ts +19 -0
  56. package/plugins/publishings/xhs-pub/dist/index.js +843 -346
  57. package/plugins/publishings/xhs-pub/plugin.json +1 -1
  58. package/plugins/publishings/xhs-pub/src/flow.ts +572 -0
  59. package/plugins/publishings/xhs-pub/src/index.ts +101 -593
  60. package/plugins/publishings/xhs-pub/src/modes.ts +300 -0
  61. package/plugins/publishings/xhs-pub/src/page.ts +267 -0
  62. package/plugins/publishings/xhs-pub/src/profile-check.ts +2 -3
  63. 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 InfoQ CN (xie.infoq.cn) via browser/agent transport"
6
+ "description": "Publish plugin for InfoQ CN (xie.infoq.cn): article drafts through the InfoQ editor via script browser transport (ts-browser-automation)"
7
7
  }
@@ -6,63 +6,280 @@ import {
6
6
  FakeBrowserTransport,
7
7
  type PublishTransport,
8
8
  type PublishTransportPayload,
9
- resolveBrowserProfileDir,
9
+ trySelectors,
10
10
  } from '@gobing-ai/publish-harness';
11
+ import {
12
+ type ChannelBrowserConfig,
13
+ checkChannelSession,
14
+ expectOutcome,
15
+ markdownToHtml,
16
+ pasteHtml,
17
+ readScopedSignal,
18
+ resolveChannelProfileDir,
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
- const CHANNEL = 'infoq';
32
+ /** Channel slug: names the Result target and resolves the `INFOQ_PROFILE_DIR` tier. */
33
+ export const CHANNEL = 'infoq';
15
34
  const TARGET = 'infoq';
35
+ /**
36
+ * InfoQ's write page. The wt seed tries `/write`, `/write/article/new` and `/article/create`
37
+ * (`infoq-playwright.ts:545-547`); the operator dogfood re-measures this one.
38
+ */
39
+ export const EDITOR_URL = 'https://xie.infoq.cn/write';
40
+ /** Login/SSO URLs that mean "not authenticated" — the editor never renders behind them. */
41
+ export const LOGIN_URL_RE = /(account\.infoq\.cn|\/login)/;
42
+
43
+ /**
44
+ * Selectors measured against the InfoQ editor (wt seed `infoq-playwright.ts:26-62`); each list is
45
+ * tried in order and the operator dogfood re-measures them. `live` is `提交`, InfoQ's submit for
46
+ * editorial review — the site has no self-publish control.
47
+ */
48
+ export const SELECTORS = {
49
+ title: ['input[placeholder*="标题"]', 'textarea[placeholder*="标题"]'],
50
+ summary: ['input[placeholder*="摘要"]', 'textarea[placeholder*="摘要"]', 'input[name="summary"]'],
51
+ body: ['.ProseMirror[contenteditable="true"]', '[contenteditable="true"]'],
52
+ draft: ['button:has-text("保存草稿")'],
53
+ live: ['button:has-text("提交")'],
54
+ } as const;
55
+
56
+ const STEP_TIMEOUT_MS = 30_000;
57
+ const LOGIN_TIMEOUT_MS = 180_000;
58
+ /** Operator recovery command every session failure names — never a cookie, page HTML or stack. */
59
+ const LOGIN_INSTRUCTION = 'bun plugins/publishings/infoq-pub/src/profile-check.ts login';
60
+ /** A saved draft lands on a URL carrying its article id; a query-string move is not a signal. */
61
+ const DRAFT_URL_ID_RE = /\/(\d{4,}|[0-9a-f]{16,})(?:\/|$|\?)/;
62
+ const DRAFT_TOASTS = ['保存成功', '已保存'];
63
+ const LIVE_TOASTS = ['提交成功', '审核'];
64
+
65
+ /**
66
+ * InfoQ-owned readiness predicate: the editor is reachable (no SSO redirect) and the title field
67
+ * is attached. Shared by the publish preflight and the profile-session CLI (R1, R5).
68
+ */
69
+ export const isAuthenticated: IsAuthenticatedPredicate = async (page) =>
70
+ !LOGIN_URL_RE.test(page.url()) && (await trySelectors(page, SELECTORS.title, { visible: false })).found;
71
+
72
+ /** Site-owned config for the shared session check and operator CLI (0183). */
73
+ export const CONFIG: ChannelBrowserConfig = {
74
+ channel: CHANNEL,
75
+ homeUrl: EDITOR_URL,
76
+ isAuthenticated,
77
+ loginTimeoutMs: LOGIN_TIMEOUT_MS,
78
+ };
16
79
 
17
80
  function fail(error: string): Result {
18
81
  return ResultSchema.parse({ ok: false, target: TARGET, error });
19
82
  }
20
83
 
21
- function draftId(content: Content): string {
22
- const title = content.title ?? 'untitled';
23
- return `${CHANNEL}-draft-${title.length}`;
84
+ function errorMessage(error: unknown): string {
85
+ return error instanceof Error ? error.message : String(error);
86
+ }
87
+
88
+ /** Session failures whose one-liner must name the recovery command (R5). */
89
+ function needsLogin(error: unknown): boolean {
90
+ return error instanceof LoginTimeoutError || error instanceof BrowserProfileMissingError;
24
91
  }
25
92
 
26
- function draftResult(content: Content, driver: BrowserDriver, profileDir: string): Result {
27
- return ResultSchema.parse({
28
- ok: true,
29
- target: TARGET,
30
- id: draftId(content),
31
- message: `${TARGET} draft prepared (unpublished)`,
32
- metadata: { driver, publishStatus: 'draft', profileDir },
33
- });
93
+ /**
94
+ * First candidate selector with an attached match, polled to the deadline. Attachment — not
95
+ * visibility — is the readiness signal here: InfoQ keeps the editor's inputs attached behind a
96
+ * collapsing layout, and the shared `trySelectors` filters on visibility by default.
97
+ */
98
+ async function findSelector(
99
+ page: Page,
100
+ selectors: readonly string[],
101
+ what: string,
102
+ timeoutMs: number,
103
+ ): Promise<string> {
104
+ const found: { selector: string | null } = { selector: null };
105
+ const attached = await waitUntil(async () => {
106
+ const result = await trySelectors(page, selectors, { visible: false });
107
+ found.selector = result.selector;
108
+ return result.found && result.selector !== null;
109
+ }, timeoutMs);
110
+ const selector = found.selector;
111
+ if (!attached || selector === null) {
112
+ throw new Error(`${what}: ${selectors.join(' | ')} not attached within ${timeoutMs}ms`);
113
+ }
114
+ return selector;
115
+ }
116
+
117
+ /**
118
+ * Text of the first visible toast among `texts`, or null. `:text(…)` matches on a substring, so
119
+ * the element's own text is returned rather than the candidate literal.
120
+ */
121
+ /**
122
+ * The draft/live outcome, read only from the page's notification containers (residual 0195): an
123
+ * unscoped `:text(...)` match also hits the article body this flow pasted a few steps earlier, so
124
+ * a body containing `保存成功` reported a draft that was never saved.
125
+ */
126
+ async function readToast(page: Page, texts: readonly string[]): Promise<string | null> {
127
+ return readScopedSignal(page, texts);
34
128
  }
35
129
 
36
130
  export class InfoqBrowserTransport implements PublishTransport {
37
131
  public readonly kind = 'browser';
38
132
  public readonly driver: BrowserDriver;
39
- private readonly loginTimeoutMs: number;
40
- private readonly sessionReady: boolean;
133
+ private readonly launcher?: BrowserLauncher;
134
+ private readonly stepTimeoutMs: number;
41
135
 
42
- constructor(driver: BrowserDriver = 'agent', opts?: { loginTimeoutMs?: number; sessionReady?: boolean }) {
136
+ constructor(driver: BrowserDriver = 'script', deps: { launcher?: BrowserLauncher; stepTimeoutMs?: number } = {}) {
43
137
  this.driver = driver;
44
- this.loginTimeoutMs = opts?.loginTimeoutMs ?? 120_000;
45
- this.sessionReady = opts?.sessionReady ?? false;
138
+ this.launcher = deps.launcher;
139
+ this.stepTimeoutMs = deps.stepTimeoutMs ?? STEP_TIMEOUT_MS;
46
140
  }
47
141
 
48
142
  public async publish(payload: PublishTransportPayload): Promise<Result> {
49
- const override =
50
- typeof payload.options?.profileDir === 'string'
51
- ? payload.options.profileDir
52
- : (process.env.INFOQ_PROFILE_DIR ?? process.env.KK_BROWSER_PROFILE_DIR);
53
- const profileDir = resolveBrowserProfileDir(CHANNEL, override);
54
- if (!this.sessionReady) {
55
- return fail(
56
- `login timeout after ${this.loginTimeoutMs}ms: no authenticated ${CHANNEL} session in ${profileDir}`,
143
+ if (this.driver !== 'script') {
144
+ return fail(`unsupported driver '${this.driver}' for ${TARGET}`);
145
+ }
146
+ const requested = payload.content.metadata?.publishStatus;
147
+ if (requested !== undefined && requested !== 'draft' && requested !== 'public') {
148
+ return fail(`unknown publishStatus '${String(requested)}' (expected absent, 'draft' or 'public')`);
149
+ }
150
+ const publishStatus = requested === 'public' ? 'public' : 'draft';
151
+ const override = typeof payload.options?.profileDir === 'string' ? payload.options.profileDir : undefined;
152
+ const profileDir = resolveChannelProfileDir(CHANNEL, override);
153
+
154
+ // ADR-025 preflight: an unauthenticated or absent profile must never produce a headed
155
+ // visit, so the read-only headless check runs first and a refusal stays a fast refusal.
156
+ const session = await checkChannelSession(CONFIG, {
157
+ profileDir,
158
+ launcher: this.launcher,
159
+ headedFallback: false,
160
+ timeoutMs: this.stepTimeoutMs,
161
+ });
162
+ if (session.status !== 'verified') {
163
+ return fail(`${CHANNEL} session preflight refused: ${session.detail} — run: ${LOGIN_INSTRUCTION}`);
164
+ }
165
+
166
+ try {
167
+ return await withProfilePage(
168
+ {
169
+ profileDir,
170
+ url: EDITOR_URL,
171
+ isAuthenticated,
172
+ // Headed by default (sites bot-wall headless loads); the headless preflight
173
+ // above is the unattended pass, `options.headless === true` the opt-in.
174
+ headless: payload.options?.headless === true,
175
+ launcher: this.launcher,
176
+ // The session is already preflight-verified, so the post-goto confirmation is
177
+ // a bounded step like every other one, not the 180s interactive login wait.
178
+ timeoutMs: this.stepTimeoutMs,
179
+ },
180
+ async ({ page }) => this.runEditorFlow(page, payload.content, publishStatus, profileDir),
57
181
  );
182
+ } catch (error) {
183
+ const hint = needsLogin(error) ? ` — run: ${LOGIN_INSTRUCTION}` : '';
184
+ return fail(`${errorMessage(error)}${hint}`);
58
185
  }
59
- return draftResult(payload.content, this.driver, profileDir);
60
186
  }
187
+
188
+ /**
189
+ * The site-owned editor flow: title → optional summary → ProseMirror paste → draft control,
190
+ * plus the submit control only for `publishStatus === 'public'`. Every outcome comes from an
191
+ * `expectOutcome` probe, so a click without an observed signal is never a success (E91-D6).
192
+ */
193
+ private async runEditorFlow(
194
+ page: Page,
195
+ content: Content,
196
+ publishStatus: 'draft' | 'public',
197
+ profileDir: string,
198
+ ): Promise<Result> {
199
+ const metadata = { driver: this.driver, publishStatus, profileDir };
200
+
201
+ const titleSelector = await findSelector(page, SELECTORS.title, 'title input', this.stepTimeoutMs);
202
+ await page
203
+ .locator(titleSelector)
204
+ .first()
205
+ .fill(content.title ?? '');
206
+
207
+ const summary = content.metadata?.summary;
208
+ if (typeof summary === 'string' && summary.length > 0) {
209
+ const summarySelector = await findSelector(page, SELECTORS.summary, 'summary input', this.stepTimeoutMs);
210
+ await page.locator(summarySelector).first().fill(summary);
211
+ }
212
+
213
+ const bodySelector = await findSelector(page, SELECTORS.body, 'body editor', this.stepTimeoutMs);
214
+ await pasteHtml(page, bodySelector, markdownToHtml(content.body), { timeoutMs: this.stepTimeoutMs });
215
+
216
+ const urlBeforeSave = page.url();
217
+ const draftSelector = await findSelector(page, SELECTORS.draft, 'draft control', this.stepTimeoutMs);
218
+ await page.locator(draftSelector).first().click();
219
+
220
+ const draft = await expectOutcome(() => probeDraft(page, urlBeforeSave), {
221
+ timeoutMs: this.stepTimeoutMs,
222
+ label: `infoq draft save '${draftSelector}'`,
223
+ });
224
+ if (!draft.ok) return fail(draft.error);
225
+ // The probe only returns a URL that moved and carried an id, so a capture exists exactly
226
+ // for that branch; a toast text carries none.
227
+ const id = DRAFT_URL_ID_RE.exec(draft.evidence)?.[1];
228
+
229
+ if (publishStatus === 'draft') {
230
+ return ResultSchema.parse({
231
+ ok: true,
232
+ target: TARGET,
233
+ ...(id === undefined ? {} : { id }),
234
+ url: page.url(),
235
+ message: `${TARGET} draft saved (unpublished) in profile ${profileDir}`,
236
+ metadata,
237
+ });
238
+ }
239
+
240
+ const liveSelector = await findSelector(page, SELECTORS.live, 'submit control', this.stepTimeoutMs);
241
+ // Captured at the click, not at the draft: the draft run already moved the URL, and a
242
+ // pre-draft baseline would read that move as the submit's own outcome.
243
+ const urlBeforeSubmit = page.url();
244
+ await page.locator(liveSelector).first().click();
245
+ const live = await expectOutcome(() => probeLive(page, urlBeforeSubmit), {
246
+ timeoutMs: this.stepTimeoutMs,
247
+ label: `infoq submit '${liveSelector}'`,
248
+ });
249
+ if (!live.ok) return fail(live.error);
250
+
251
+ return ResultSchema.parse({
252
+ ok: true,
253
+ target: TARGET,
254
+ ...(id === undefined ? {} : { id }),
255
+ url: page.url(),
256
+ message: `${TARGET} submitted for InfoQ editorial review`,
257
+ metadata,
258
+ });
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Draft evidence (E91-D6): the editor URL moved onto an article id, or a visible save toast.
264
+ * A URL that merely gained a query parameter is not evidence of a saved draft.
265
+ */
266
+ async function probeDraft(page: Page, urlBeforeSave: string): Promise<string | null> {
267
+ const url = page.url();
268
+ if (url !== urlBeforeSave && DRAFT_URL_ID_RE.test(url)) return url;
269
+ return readToast(page, DRAFT_TOASTS);
270
+ }
271
+
272
+ /** Live evidence: a visible submit toast, or a URL that moved off the URL the draft run left. */
273
+ async function probeLive(page: Page, urlBeforeSave: string): Promise<string | null> {
274
+ const toast = await readToast(page, LIVE_TOASTS);
275
+ if (toast !== null) return toast;
276
+ const url = page.url();
277
+ return url === urlBeforeSave ? null : url;
61
278
  }
62
279
 
63
- export function getPublishTransport(driver: BrowserDriver = 'agent'): PublishTransport {
280
+ export function getPublishTransport(driver: BrowserDriver = 'script'): PublishTransport {
64
281
  if (process.env.KNOWLEDGE_KIT_PUBLISH_TRANSPORT === 'fake') {
65
- return new FakeBrowserTransport(driver);
282
+ return new FakeBrowserTransport('script');
66
283
  }
67
284
  return new InfoqBrowserTransport(driver);
68
285
  }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * InfoQ profile-session capability (R5, AC3).
3
+ *
4
+ * `login` opens the editor in a headed browser and resolves only once InfoQ's readiness
5
+ * predicate passes; `check` reopens the profile headlessly and reports `verified` or `refused`.
6
+ * Arg parsing, profile precedence (`--profile` > `INFOQ_PROFILE_DIR` > `KK_BROWSER_PROFILE_DIR` >
7
+ * `~/.config/kk/browser-profiles/infoq`) and the exit codes (0 verified, 1 refused, 2 usage
8
+ * error) all live in the shared `runProfileCli` (0183), so the publish preflight and this CLI
9
+ * apply the same gate.
10
+ *
11
+ * Manual dogfood (plan step 6): `bun plugins/publishings/infoq-pub/src/profile-check.ts login`
12
+ * then `… check` on the same profile.
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
+ }