@gobing-ai/knowledge-kit 0.0.19 → 0.0.22

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 (69) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +161 -142
  3. package/package.json +1 -1
  4. package/plugins/generations/content-gen/dist/index.js +20 -3
  5. package/plugins/generations/core-facts-gen/dist/index.js +20 -3
  6. package/plugins/generations/daily-article-gen/dist/index.js +20 -3
  7. package/plugins/generations/dailynews-gen/dist/index.js +29 -4
  8. package/plugins/generations/dailynews-gen/src/script-builder.ts +19 -1
  9. package/plugins/generations/episode-plan-gen/dist/index.js +20 -3
  10. package/plugins/generations/image-gen/dist/index.js +147 -69
  11. package/plugins/generations/image-gen/src/presets.ts +2 -2
  12. package/plugins/generations/image-gen/src/providers/agnes.ts +6 -2
  13. package/plugins/generations/image-gen/src/providers/azure.ts +6 -2
  14. package/plugins/generations/image-gen/src/providers/dashscope.ts +10 -3
  15. package/plugins/generations/image-gen/src/providers/google.ts +70 -29
  16. package/plugins/generations/image-gen/src/providers/huggingface.ts +3 -0
  17. package/plugins/generations/image-gen/src/providers/jimeng.ts +9 -2
  18. package/plugins/generations/image-gen/src/providers/minimax.ts +10 -2
  19. package/plugins/generations/image-gen/src/providers/openai.ts +9 -3
  20. package/plugins/generations/image-gen/src/providers/openrouter.ts +16 -4
  21. package/plugins/generations/image-gen/src/providers/replicate.ts +13 -3
  22. package/plugins/generations/image-gen/src/providers/seedream.ts +4 -1
  23. package/plugins/generations/image-gen/src/providers/timeouts.ts +14 -0
  24. package/plugins/generations/image-gen/src/providers/types.ts +10 -0
  25. package/plugins/generations/image-gen/src/providers/zai.ts +4 -1
  26. package/plugins/generations/news-report-gen/dist/index.js +20 -3
  27. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/pipeline.py +13 -5
  28. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/qc.py +23 -15
  29. package/plugins/generations/voice-gen/dist/index.js +36 -10
  30. package/plugins/generations/voice-gen/src/voicebox-client.ts +18 -4
  31. package/plugins/generations/voice-gen/src/voicebox-server.ts +2 -1
  32. package/plugins/ingestions/aihot-ingest/dist/index.js +26 -6
  33. package/plugins/ingestions/aihot-ingest/plugin.json +1 -1
  34. package/plugins/ingestions/aihot-ingest/src/client.ts +5 -0
  35. package/plugins/ingestions/aihot-ingest/src/rss.ts +12 -2
  36. package/plugins/ingestions/horizon-ingest/dist/index.js +20 -3
  37. package/plugins/ingestions/last30days-ingest/dist/index.js +20 -3
  38. package/plugins/ingestions/web-search/dist/index.js +22 -3
  39. package/plugins/ingestions/web-search/src/index.ts +4 -0
  40. package/plugins/kk/commands/workflow-run.md +20 -6
  41. package/plugins/kk/plugin.json +1 -1
  42. package/plugins/kk/scripts/itc-stages.ts +563 -0
  43. package/plugins/kk/scripts/kk-workflow-stages.ts +125 -18
  44. package/plugins/kk/scripts/render-md.ts +7 -1
  45. package/plugins/kk/skills/article-adapt/SKILL.md +85 -0
  46. package/plugins/kk/workflows/kk-daily-ai-voice.yaml +26 -16
  47. package/plugins/kk/workflows/kk-itc.yaml +461 -12
  48. package/plugins/publishings/emdash-pub/dist/index.js +76 -19
  49. package/plugins/publishings/emdash-pub/src/index.ts +4 -0
  50. package/plugins/publishings/infoq-pub/dist/index.js +74 -21
  51. package/plugins/publishings/juejin-pub/dist/index.js +74 -21
  52. package/plugins/publishings/medium-pub/dist/index.js +77 -19
  53. package/plugins/publishings/medium-pub/src/index.ts +5 -0
  54. package/plugins/publishings/podcast-pub/dist/index.js +130 -34
  55. package/plugins/publishings/podcast-pub/src/index.ts +20 -5
  56. package/plugins/publishings/podcast-pub/src/map.ts +11 -0
  57. package/plugins/publishings/podcast-pub/src/show-notes.ts +66 -13
  58. package/plugins/publishings/qiita-pub/dist/index.js +75 -18
  59. package/plugins/publishings/qiita-pub/src/index.ts +4 -0
  60. package/plugins/publishings/substack-pub/dist/index.js +74 -21
  61. package/plugins/publishings/surfdash-pub/dist/index.js +99 -62
  62. package/plugins/publishings/surfdash-pub/src/index.ts +60 -19
  63. package/plugins/publishings/wechatmp-pub/dist/index.js +79 -22
  64. package/plugins/publishings/wechatmp-pub/src/index.ts +12 -3
  65. package/plugins/publishings/x-pub/dist/index.js +56 -37
  66. package/plugins/publishings/x-pub/src/index.ts +4 -0
  67. package/plugins/publishings/xhs-pub/dist/index.js +74 -21
  68. package/plugins/publishings/zenn-pub/dist/index.js +103 -25
  69. package/plugins/publishings/zenn-pub/src/index.ts +43 -9
@@ -1,6 +1,7 @@
1
1
  import { extname } from 'node:path';
2
2
  import { readBytes } from '../bytes';
3
3
  import { extractImageFromResponse, getOpenAISize, type OpenAIImageResponse } from './openai';
4
+ import { PROVIDER_REQUEST_TIMEOUT_MS } from './timeouts';
4
5
  import type { ImageProvider, ImageRequest, ImageResult, ProviderFactory, ProviderOptions } from './types';
5
6
 
6
7
  /**
@@ -77,6 +78,7 @@ export function validateReferenceImages(referenceImages: readonly string[]): voi
77
78
 
78
79
  export const createAzure: ProviderFactory = (options?: ProviderOptions): ImageProvider => {
79
80
  const fetchFn = options?.fetch ?? fetch;
81
+ const timeoutMs = options?.timeoutMs ?? PROVIDER_REQUEST_TIMEOUT_MS;
80
82
  const env = options?.env ?? process.env;
81
83
  // Azure's own env-precedence for a default model: explicit deployment, then the base URL,
82
84
  // then the frozen 'gpt-image-2' (matches the baoyu getDefaultModel chain; spec facts unchanged).
@@ -135,17 +137,19 @@ export const createAzure: ProviderFactory = (options?: ProviderOptions): ImagePr
135
137
  method: 'POST',
136
138
  headers: { 'api-key': apiKey },
137
139
  body: form,
140
+ signal: AbortSignal.timeout(timeoutMs),
138
141
  });
139
142
  if (!res.ok) throw new Error(`Azure OpenAI edits API error: ${await res.text()}`);
140
- return extractImageFromResponse((await res.json()) as OpenAIImageResponse, fetchFn);
143
+ return extractImageFromResponse((await res.json()) as OpenAIImageResponse, fetchFn, timeoutMs);
141
144
  }
142
145
  const res = await fetchFn(buildURL(endpoint, deployment, '/images/generations', env), {
143
146
  method: 'POST',
144
147
  headers: { 'content-type': 'application/json', 'api-key': apiKey },
145
148
  body: JSON.stringify({ prompt: req.prompt, size, n: 1, quality: getAzureQuality(req.quality) }),
149
+ signal: AbortSignal.timeout(timeoutMs),
146
150
  });
147
151
  if (!res.ok) throw new Error(`Azure OpenAI API error: ${await res.text()}`);
148
- return extractImageFromResponse((await res.json()) as OpenAIImageResponse, fetchFn);
152
+ return extractImageFromResponse((await res.json()) as OpenAIImageResponse, fetchFn, timeoutMs);
149
153
  })();
150
154
  return { bytes, extension: 'png', provider: 'azure', model: req.model };
151
155
  },
@@ -1,6 +1,7 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import { echoError } from '@gobing-ai/ts-utils';
3
3
  import { refToDataUri } from './refs';
4
+ import { PROVIDER_REQUEST_TIMEOUT_MS } from './timeouts';
4
5
  import type { ImageProvider, ImageRequest, ImageResult, ProviderFactory, ProviderOptions } from './types';
5
6
 
6
7
  /**
@@ -389,7 +390,11 @@ interface DashScopeResponse {
389
390
  };
390
391
  }
391
392
 
392
- async function extractImageFromResponse(result: DashScopeResponse, fetchFn: typeof fetch): Promise<Uint8Array> {
393
+ async function extractImageFromResponse(
394
+ result: DashScopeResponse,
395
+ fetchFn: typeof fetch,
396
+ timeoutMs: number = PROVIDER_REQUEST_TIMEOUT_MS,
397
+ ): Promise<Uint8Array> {
393
398
  let imageData: string | null = null;
394
399
  if (result.output?.result_image !== undefined) {
395
400
  imageData = result.output.result_image;
@@ -403,7 +408,7 @@ async function extractImageFromResponse(result: DashScopeResponse, fetchFn: type
403
408
  }
404
409
  if (imageData === null) throw new Error('No image in response');
405
410
  if (imageData.startsWith('http://') || imageData.startsWith('https://')) {
406
- const imgRes = await fetchFn(imageData);
411
+ const imgRes = await fetchFn(imageData, { signal: AbortSignal.timeout(timeoutMs) });
407
412
  if (!imgRes.ok) throw new Error('Failed to download image');
408
413
  return new Uint8Array(await imgRes.arrayBuffer());
409
414
  }
@@ -412,6 +417,7 @@ async function extractImageFromResponse(result: DashScopeResponse, fetchFn: type
412
417
 
413
418
  export const createDashscope: ProviderFactory = (options?: ProviderOptions): ImageProvider => {
414
419
  const fetchFn = options?.fetch ?? fetch;
420
+ const timeoutMs = options?.timeoutMs ?? PROVIDER_REQUEST_TIMEOUT_MS;
415
421
  const env = options?.env ?? process.env;
416
422
  const readEnv = (key: string): string => env[key] ?? '';
417
423
  const supportsRef = (model: string): boolean => model.includes('wan2.7');
@@ -476,9 +482,10 @@ export const createDashscope: ProviderFactory = (options?: ProviderOptions): Ima
476
482
  method: 'POST',
477
483
  headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },
478
484
  body: JSON.stringify(body),
485
+ signal: AbortSignal.timeout(timeoutMs),
479
486
  });
480
487
  if (!res.ok) throw new Error(`DashScope API error (${res.status}): ${await res.text()}`);
481
- const bytes = await extractImageFromResponse((await res.json()) as DashScopeResponse, fetchFn);
488
+ const bytes = await extractImageFromResponse((await res.json()) as DashScopeResponse, fetchFn, timeoutMs);
482
489
  return { bytes, extension: 'png', provider: 'dashscope', model: req.model };
483
490
  },
484
491
  };
@@ -1,7 +1,12 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import { execFileSync } from 'node:child_process';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { createNodeFileSystem } from '@gobing-ai/ts-runtime';
3
7
  import { readBytes } from '../bytes';
4
8
  import { mimeForPath } from './refs';
9
+ import { PROVIDER_REQUEST_TIMEOUT_MS } from './timeouts';
5
10
  import type { ImageProvider, ImageRequest, ImageResult, ProviderFactory, ProviderOptions } from './types';
6
11
 
7
12
  /**
@@ -112,6 +117,25 @@ function getHttpProxy(readEnv: (key: string) => string): string | null {
112
117
  );
113
118
  }
114
119
 
120
+ /**
121
+ * curl config-file quoting: backslash-escape `\` and `"` inside a double-quoted value.
122
+ * curl's config parser is not a shell, but it does honour these two escapes.
123
+ */
124
+ const curlQuote = (s: string): string => `"${s.replace(/[\\"]/g, '\\$&')}"`;
125
+
126
+ /** Replaces every secret with `***`; empty entries are skipped so `''` cannot mangle the text. */
127
+ const redact = (s: string, secrets: string[]): string =>
128
+ secrets.filter((x) => x !== '').reduce((acc, x) => acc.replaceAll(x, '***'), s);
129
+
130
+ /** Decoded password from a proxy URL, or '' when the URL carries none or does not parse. */
131
+ function proxyPassword(proxy: string): string {
132
+ try {
133
+ return decodeURIComponent(new URL(proxy).password);
134
+ } catch {
135
+ return '';
136
+ }
137
+ }
138
+
115
139
  interface GeminiResponse {
116
140
  candidates?: Array<{ content?: { parts?: Array<{ inlineData?: { data?: string } }> } }>;
117
141
  }
@@ -123,6 +147,7 @@ interface ImagenResponse {
123
147
 
124
148
  export const createGoogle: ProviderFactory = (options?: ProviderOptions): ImageProvider => {
125
149
  const fetchFn = options?.fetch ?? fetch;
150
+ const timeoutMs = options?.timeoutMs ?? PROVIDER_REQUEST_TIMEOUT_MS;
126
151
  const env = options?.env ?? process.env;
127
152
  const readEnv = (key: string): string => env[key] ?? '';
128
153
  const refModels = [
@@ -143,35 +168,50 @@ export const createGoogle: ProviderFactory = (options?: ProviderOptions): ImageP
143
168
  const proxy = getHttpProxy(readEnv);
144
169
  // Bun's fetch drops long-lived proxied sockets; upstream works around it with curl (0115 port).
145
170
  if (proxy !== null && options?.fetch === undefined) {
146
- const result = (() => {
147
- try {
148
- return execFileSync(
149
- 'curl',
150
- [
151
- '-s',
152
- '--connect-timeout',
153
- '30',
154
- '--max-time',
155
- '300',
156
- '-x',
157
- proxy,
158
- url,
159
- '-H',
160
- 'Content-Type: application/json',
161
- '-H',
162
- `x-goog-api-key: ${apiKey}`,
163
- '-d',
164
- '@-',
165
- ],
166
- { input: JSON.stringify(body), encoding: 'utf8', timeout: 310_000 },
167
- );
168
- } catch (error: unknown) {
169
- const e = error as { message?: string; stderr?: string | Buffer };
170
- const details =
171
- (typeof e.stderr === 'string' ? e.stderr.trim() : '') || e.message || 'curl request failed';
172
- throw new Error(`Google API request failed via curl: ${details}`);
173
- }
174
- })();
171
+ // Secrets stay out of argv: curl reads the proxy and the API key from a config file
172
+ // (`-K <path>`), and the request body arrives on stdin (`-d @-`). `execFileSync` embeds the
173
+ // full argv in `error.message`, so `-x <proxy>` / `-H 'x-goog-api-key: …'` would leak both
174
+ // the key and the proxy password into logs. `-sS` (not `-s`) keeps curl's own diagnostic on
175
+ // stderr, so `details` no longer falls back to that argv-bearing message.
176
+ //
177
+ // File IO goes through the `@gobing-ai/ts-runtime` seam (`no-direct-fs-io`): the seam is
178
+ // text-only with no `mode` argument, and the file lives in `os.tmpdir()`, which is a
179
+ // per-user directory (0700 on macOS).
180
+ const configPath = join(tmpdir(), `kk-image-gen-${randomUUID()}.curlrc`);
181
+ const config = `proxy = ${curlQuote(proxy)}\nheader = ${curlQuote(`x-goog-api-key: ${apiKey}`)}\n`;
182
+ const fileSystem = createNodeFileSystem();
183
+ await fileSystem.writeFile(configPath, config);
184
+ let result: string;
185
+ try {
186
+ result = execFileSync(
187
+ 'curl',
188
+ [
189
+ '-sS',
190
+ '--connect-timeout',
191
+ '30',
192
+ '--max-time',
193
+ '300',
194
+ '-K',
195
+ configPath,
196
+ '-H',
197
+ 'Content-Type: application/json',
198
+ '-d',
199
+ '@-',
200
+ url,
201
+ ],
202
+ { input: JSON.stringify(body), encoding: 'utf8', timeout: 310_000 },
203
+ );
204
+ } catch (error: unknown) {
205
+ const e = error as { message?: string; stderr?: string | Buffer };
206
+ const details =
207
+ (typeof e.stderr === 'string' ? e.stderr.trim() : '') || e.message || 'curl request failed';
208
+ // Redact the raw `user:pass@host` form as well as the decoded password.
209
+ throw new Error(
210
+ `Google API request failed via curl: ${redact(details, [apiKey, proxyPassword(proxy), proxy])}`,
211
+ );
212
+ } finally {
213
+ await fileSystem.deleteFile(configPath);
214
+ }
175
215
  const parsed = JSON.parse(result) as T & { error?: { code?: string; message?: string } };
176
216
  if (parsed.error !== undefined) {
177
217
  throw new Error(`Google API error (${parsed.error.code}): ${parsed.error.message}`);
@@ -182,6 +222,7 @@ export const createGoogle: ProviderFactory = (options?: ProviderOptions): ImageP
182
222
  method: 'POST',
183
223
  headers: { 'content-type': 'application/json', 'x-goog-api-key': apiKey },
184
224
  body: JSON.stringify(body),
225
+ signal: AbortSignal.timeout(timeoutMs),
185
226
  });
186
227
  if (!res.ok) {
187
228
  const err = await res.text();
@@ -1,3 +1,4 @@
1
+ import { PROVIDER_REQUEST_TIMEOUT_MS } from './timeouts';
1
2
  import type { ImageProvider, ImageRequest, ImageResult, ProviderFactory, ProviderOptions } from './types';
2
3
 
3
4
  /**
@@ -25,6 +26,7 @@ export const buildPayload = (
25
26
 
26
27
  export const createHuggingface: ProviderFactory = (options?: ProviderOptions): ImageProvider => {
27
28
  const fetchFn = options?.fetch ?? fetch;
29
+ const timeoutMs = options?.timeoutMs ?? PROVIDER_REQUEST_TIMEOUT_MS;
28
30
  const env = options?.env ?? process.env;
29
31
  const readEnv = (key: string): string => env[key] ?? '';
30
32
 
@@ -44,6 +46,7 @@ export const createHuggingface: ProviderFactory = (options?: ProviderOptions): I
44
46
  method: 'POST',
45
47
  headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
46
48
  body: JSON.stringify(buildPayload(req)),
49
+ signal: AbortSignal.timeout(timeoutMs),
47
50
  });
48
51
  if (!res.ok) {
49
52
  const text = (await res.text()).slice(0, 200);
@@ -1,4 +1,5 @@
1
1
  import * as crypto from 'node:crypto';
2
+ import { PROVIDER_REQUEST_TIMEOUT_MS } from './timeouts';
2
3
  import type { ImageProvider, ImageRequest, ImageResult, ProviderFactory, ProviderOptions } from './types';
3
4
 
4
5
  /**
@@ -142,6 +143,7 @@ interface JimengTaskResponse {
142
143
 
143
144
  export const createJimeng: ProviderFactory = (options?: ProviderOptions): ImageProvider => {
144
145
  const fetchFn = options?.fetch ?? fetch;
146
+ const timeoutMs = options?.timeoutMs ?? PROVIDER_REQUEST_TIMEOUT_MS;
145
147
  const env = options?.env ?? process.env;
146
148
  const readEnv = (key: string): string => env[key] ?? '';
147
149
 
@@ -170,7 +172,12 @@ export const createJimeng: ProviderFactory = (options?: ProviderOptions): ImageP
170
172
  'cv',
171
173
  endpoint.canonicalUri,
172
174
  );
173
- return fetchFn(endpoint.url, { method: 'POST', headers: { ...headers, authorization }, body });
175
+ return fetchFn(endpoint.url, {
176
+ method: 'POST',
177
+ headers: { ...headers, authorization },
178
+ body,
179
+ signal: AbortSignal.timeout(timeoutMs),
180
+ });
174
181
  }
175
182
 
176
183
  async function submitTask(
@@ -214,7 +221,7 @@ export const createJimeng: ProviderFactory = (options?: ProviderOptions): ImageP
214
221
  }
215
222
  if (status === 'done' && image_urls !== undefined && image_urls.length > 0) {
216
223
  const url = image_urls[0] as string;
217
- const imgRes = await fetchFn(url);
224
+ const imgRes = await fetchFn(url, { signal: AbortSignal.timeout(timeoutMs) });
218
225
  if (!imgRes.ok) throw new Error(`Failed to download image from ${url}`);
219
226
  return new Uint8Array(await imgRes.arrayBuffer());
220
227
  }
@@ -1,6 +1,7 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import { readBytes } from '../bytes';
3
3
  import { mimeForPath } from './refs';
4
+ import { PROVIDER_REQUEST_TIMEOUT_MS } from './timeouts';
4
5
  import type { ImageProvider, ImageRequest, ImageResult, ProviderFactory, ProviderOptions } from './types';
5
6
 
6
7
  /**
@@ -117,6 +118,7 @@ export async function buildSubjectReference(referenceImages: string[]): Promise<
117
118
  export async function extractImageFromResponse(
118
119
  result: MinimaxResponse,
119
120
  fetchFn: typeof fetch = fetch,
121
+ timeoutMs: number = PROVIDER_REQUEST_TIMEOUT_MS,
120
122
  ): Promise<Uint8Array> {
121
123
  const baseResp = result.base_resp;
122
124
  if (baseResp?.status_code !== undefined && baseResp.status_code !== 0) {
@@ -126,7 +128,7 @@ export async function extractImageFromResponse(
126
128
  if (base64Image !== undefined) return Uint8Array.from(Buffer.from(base64Image, 'base64'));
127
129
  const url = result.data?.image_urls?.[0];
128
130
  if (url !== undefined) {
129
- const response = await fetchFn(url);
131
+ const response = await fetchFn(url, { signal: AbortSignal.timeout(timeoutMs) });
130
132
  if (!response.ok) throw new Error(`Failed to download image from MiniMax: ${response.status}`);
131
133
  return new Uint8Array(await response.arrayBuffer());
132
134
  }
@@ -135,6 +137,7 @@ export async function extractImageFromResponse(
135
137
 
136
138
  export const createMinimax: ProviderFactory = (options?: ProviderOptions): ImageProvider => {
137
139
  const fetchFn = options?.fetch ?? fetch;
140
+ const timeoutMs = options?.timeoutMs ?? PROVIDER_REQUEST_TIMEOUT_MS;
138
141
  const env = options?.env ?? process.env;
139
142
  const readEnv = (key: string): string => env[key] ?? '';
140
143
 
@@ -160,11 +163,16 @@ export const createMinimax: ProviderFactory = (options?: ProviderOptions): Image
160
163
  method: 'POST',
161
164
  headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },
162
165
  body: JSON.stringify(body),
166
+ signal: AbortSignal.timeout(timeoutMs),
163
167
  });
164
168
  if (!response.ok) {
165
169
  throw new Error(`MiniMax API error (${response.status}): ${await response.text()}`);
166
170
  }
167
- const bytes = await extractImageFromResponse((await response.json()) as MinimaxResponse, fetchFn);
171
+ const bytes = await extractImageFromResponse(
172
+ (await response.json()) as MinimaxResponse,
173
+ fetchFn,
174
+ timeoutMs,
175
+ );
168
176
  return { bytes, extension: 'jpg', provider: 'minimax', model: req.model };
169
177
  },
170
178
  };
@@ -1,6 +1,7 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import { basename } from 'node:path';
3
3
  import { readBytes } from '../bytes';
4
+ import { PROVIDER_REQUEST_TIMEOUT_MS } from './timeouts';
4
5
  import type { ImageProvider, ImageRequest, ImageResult, ProviderFactory, ProviderOptions } from './types';
5
6
 
6
7
  /**
@@ -207,13 +208,14 @@ export function getMimeType(filename: string): string {
207
208
  export async function extractImageFromResponse(
208
209
  result: OpenAIImageResponse,
209
210
  fetchFn: typeof fetch = fetch,
211
+ timeoutMs: number = PROVIDER_REQUEST_TIMEOUT_MS,
210
212
  ): Promise<Uint8Array> {
211
213
  const img = result.data[0];
212
214
  if (img?.b64_json !== undefined && img.b64_json !== '') {
213
215
  return Uint8Array.from(Buffer.from(img.b64_json, 'base64'));
214
216
  }
215
217
  if (img?.url !== undefined) {
216
- const imgRes = await fetchFn(img.url);
218
+ const imgRes = await fetchFn(img.url, { signal: AbortSignal.timeout(timeoutMs) });
217
219
  if (!imgRes.ok) throw new Error('Failed to download image');
218
220
  return new Uint8Array(await imgRes.arrayBuffer());
219
221
  }
@@ -222,6 +224,7 @@ export async function extractImageFromResponse(
222
224
 
223
225
  export const createOpenai: ProviderFactory = (options?: ProviderOptions): ImageProvider => {
224
226
  const fetchFn = options?.fetch ?? fetch;
227
+ const timeoutMs = options?.timeoutMs ?? PROVIDER_REQUEST_TIMEOUT_MS;
225
228
  const env = options?.env ?? process.env;
226
229
  const readEnv = (key: string): string => env[key] ?? '';
227
230
  const supportsRef = (model: string): boolean => model.includes('gpt-image');
@@ -255,6 +258,7 @@ export const createOpenai: ProviderFactory = (options?: ProviderOptions): ImageP
255
258
  method: 'POST',
256
259
  headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },
257
260
  body: JSON.stringify({ model: req.model, messages: [{ role: 'user', content: req.prompt }] }),
261
+ signal: AbortSignal.timeout(timeoutMs),
258
262
  });
259
263
  if (!chatRes.ok) throw new Error(`OpenAI API error: ${await chatRes.text()}`);
260
264
  const content =
@@ -301,17 +305,19 @@ export const createOpenai: ProviderFactory = (options?: ProviderOptions): ImageP
301
305
  method: 'POST',
302
306
  headers: { authorization: `Bearer ${apiKey}` },
303
307
  body: form,
308
+ signal: AbortSignal.timeout(timeoutMs),
304
309
  });
305
310
  if (!res.ok) throw new Error(`OpenAI edits API error: ${await res.text()}`);
306
- return extractImageFromResponse((await res.json()) as OpenAIImageResponse, fetchFn);
311
+ return extractImageFromResponse((await res.json()) as OpenAIImageResponse, fetchFn, timeoutMs);
307
312
  }
308
313
  const res = await fetchFn(`${baseURL}/images/generations`, {
309
314
  method: 'POST',
310
315
  headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },
311
316
  body: JSON.stringify(buildOpenAIGenerationsBody(req.prompt, req.model, args)),
317
+ signal: AbortSignal.timeout(timeoutMs),
312
318
  });
313
319
  if (!res.ok) throw new Error(`OpenAI API error: ${await res.text()}`);
314
- return extractImageFromResponse((await res.json()) as OpenAIImageResponse, fetchFn);
320
+ return extractImageFromResponse((await res.json()) as OpenAIImageResponse, fetchFn, timeoutMs);
315
321
  })();
316
322
  return { bytes, extension: 'png', provider: 'openai', model: req.model };
317
323
  },
@@ -1,5 +1,6 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import { refToDataUri } from './refs';
3
+ import { PROVIDER_REQUEST_TIMEOUT_MS } from './timeouts';
3
4
  import type { ImageProvider, ImageRequest, ImageResult, ProviderFactory, ProviderOptions } from './types';
4
5
 
5
6
  /**
@@ -137,11 +138,15 @@ function decodeDataUrl(value: string): Uint8Array | null {
137
138
  return Uint8Array.from(Buffer.from(match[1] as string, 'base64'));
138
139
  }
139
140
 
140
- async function downloadImage(value: string, fetchFn: typeof fetch): Promise<Uint8Array> {
141
+ async function downloadImage(
142
+ value: string,
143
+ fetchFn: typeof fetch,
144
+ timeoutMs: number = PROVIDER_REQUEST_TIMEOUT_MS,
145
+ ): Promise<Uint8Array> {
141
146
  const inline = decodeDataUrl(value);
142
147
  if (inline !== null) return inline;
143
148
  if (value.startsWith('http://') || value.startsWith('https://')) {
144
- const response = await fetchFn(value);
149
+ const response = await fetchFn(value, { signal: AbortSignal.timeout(timeoutMs) });
145
150
  if (!response.ok) throw new Error(`Failed to download OpenRouter image: ${response.status}`);
146
151
  return new Uint8Array(await response.arrayBuffer());
147
152
  }
@@ -151,12 +156,13 @@ async function downloadImage(value: string, fetchFn: typeof fetch): Promise<Uint
151
156
  export async function extractImageFromResponse(
152
157
  result: OpenRouterResponse,
153
158
  fetchFn: typeof fetch = fetch,
159
+ timeoutMs: number = PROVIDER_REQUEST_TIMEOUT_MS,
154
160
  ): Promise<Uint8Array> {
155
161
  const choice = result.choices?.[0];
156
162
  const message = choice?.message;
157
163
  for (const image of message?.images ?? []) {
158
164
  const imageUrl = extractImageUrl(image);
159
- if (imageUrl !== null) return downloadImage(imageUrl, fetchFn);
165
+ if (imageUrl !== null) return downloadImage(imageUrl, fetchFn, timeoutMs);
160
166
  }
161
167
  if (Array.isArray(message?.content)) {
162
168
  for (const item of message.content) {
@@ -211,6 +217,7 @@ export function buildRequestBody(
211
217
 
212
218
  export const createOpenrouter: ProviderFactory = (options?: ProviderOptions): ImageProvider => {
213
219
  const fetchFn = options?.fetch ?? fetch;
220
+ const timeoutMs = options?.timeoutMs ?? PROVIDER_REQUEST_TIMEOUT_MS;
214
221
  const env = options?.env ?? process.env;
215
222
  const readEnv = (key: string): string => env[key] ?? '';
216
223
 
@@ -246,11 +253,16 @@ export const createOpenrouter: ProviderFactory = (options?: ProviderOptions): Im
246
253
  method: 'POST',
247
254
  headers,
248
255
  body: JSON.stringify(body),
256
+ signal: AbortSignal.timeout(timeoutMs),
249
257
  });
250
258
  if (!response.ok) {
251
259
  throw new Error(`OpenRouter API error (${response.status}): ${await response.text()}`);
252
260
  }
253
- const bytes = await extractImageFromResponse((await response.json()) as OpenRouterResponse, fetchFn);
261
+ const bytes = await extractImageFromResponse(
262
+ (await response.json()) as OpenRouterResponse,
263
+ fetchFn,
264
+ timeoutMs,
265
+ );
254
266
  return { bytes, extension: 'png', provider: 'openrouter', model: req.model };
255
267
  },
256
268
  };
@@ -1,5 +1,6 @@
1
1
  import { refToDataUri } from './refs';
2
2
  import { extensionFromUrl } from './seedream';
3
+ import { PROVIDER_REQUEST_TIMEOUT_MS } from './timeouts';
3
4
  import type { ImageProvider, ImageRequest, ImageResult, ProviderFactory, ProviderOptions } from './types';
4
5
 
5
6
  /**
@@ -208,10 +209,11 @@ export function extractOutputUrl(prediction: PredictionResponse): string {
208
209
  export const createReplicate: ProviderFactory = (options?: ProviderOptions): ImageProvider => {
209
210
  const fetchFn = options?.fetch ?? fetch;
210
211
  const env = options?.env ?? process.env;
212
+ const timeoutMs = options?.timeoutMs ?? PROVIDER_REQUEST_TIMEOUT_MS;
211
213
  const readEnv = (key: string): string => env[key] ?? '';
212
214
 
213
215
  async function downloadImage(url: string): Promise<Uint8Array> {
214
- const res = await fetchFn(url);
216
+ const res = await fetchFn(url, { signal: AbortSignal.timeout(timeoutMs) });
215
217
  if (!res.ok) throw new Error(`Failed to download image from Replicate: ${res.status}`);
216
218
  return new Uint8Array(await res.arrayBuffer());
217
219
  }
@@ -219,7 +221,10 @@ export const createReplicate: ProviderFactory = (options?: ProviderOptions): Ima
219
221
  async function pollPrediction(apiToken: string, getUrl: string): Promise<PredictionResponse> {
220
222
  const start = Date.now();
221
223
  while (Date.now() - start < MAX_POLL_MS) {
222
- const res = await fetchFn(getUrl, { headers: { authorization: `Bearer ${apiToken}` } });
224
+ const res = await fetchFn(getUrl, {
225
+ headers: { authorization: `Bearer ${apiToken}` },
226
+ signal: AbortSignal.timeout(timeoutMs),
227
+ });
223
228
  if (!res.ok) throw new Error(`Replicate poll error (${res.status}): ${await res.text()}`);
224
229
  const prediction = (await res.json()) as PredictionResponse;
225
230
  if (prediction.status === 'succeeded') return prediction;
@@ -260,7 +265,12 @@ export const createReplicate: ProviderFactory = (options?: ProviderOptions): Ima
260
265
  Prefer: `wait=${SYNC_WAIT_SECONDS}`,
261
266
  };
262
267
  let prediction = (await (
263
- await fetchFn(url, { method: 'POST', headers, body: JSON.stringify(body) })
268
+ await fetchFn(url, {
269
+ method: 'POST',
270
+ headers,
271
+ body: JSON.stringify(body),
272
+ signal: AbortSignal.timeout(timeoutMs),
273
+ })
264
274
  ).json()) as PredictionResponse;
265
275
  if (prediction.status !== 'succeeded') {
266
276
  if (prediction.urls?.get === undefined)
@@ -1,5 +1,6 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import { readBytes } from '../bytes';
3
+ import { PROVIDER_REQUEST_TIMEOUT_MS } from './timeouts';
3
4
  import type { ImageProvider, ImageRequest, ImageResult, ProviderFactory, ProviderOptions } from './types';
4
5
 
5
6
  /**
@@ -36,6 +37,7 @@ export function extensionFromUrl(url: string): string {
36
37
 
37
38
  export const createSeedream: ProviderFactory = (options?: ProviderOptions): ImageProvider => {
38
39
  const fetchFn = options?.fetch ?? fetch;
40
+ const timeoutMs = options?.timeoutMs ?? PROVIDER_REQUEST_TIMEOUT_MS;
39
41
  const env = options?.env ?? process.env;
40
42
  const readEnv = (key: string): string => env[key] ?? '';
41
43
  const supportsRef = (model: string): boolean => REF_MODEL_FRAGMENTS.some((fragment) => model.includes(fragment));
@@ -80,6 +82,7 @@ export const createSeedream: ProviderFactory = (options?: ProviderOptions): Imag
80
82
  method: 'POST',
81
83
  headers: { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' },
82
84
  body: JSON.stringify(body),
85
+ signal: AbortSignal.timeout(timeoutMs),
83
86
  });
84
87
  const text = await resp.text();
85
88
  if (!resp.ok) {
@@ -107,7 +110,7 @@ export const createSeedream: ProviderFactory = (options?: ProviderOptions): Imag
107
110
  };
108
111
  }
109
112
  if (image.url !== undefined) {
110
- const r = await fetchFn(image.url);
113
+ const r = await fetchFn(image.url, { signal: AbortSignal.timeout(timeoutMs) });
111
114
  if (!r.ok) {
112
115
  throw new Error(`seedream fetch URL HTTP ${r.status}`);
113
116
  }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Per-request HTTP timeout shared by every image-gen provider.
3
+ *
4
+ * This lives in a leaf module (no imports) rather than in `types.ts` because `types.ts` builds the
5
+ * `PROVIDERS` registry from value imports of each adapter, which would make `types.ts ⇄ <adapter>`
6
+ * a runtime import cycle: a consumer that imported an adapter first (e.g. the google helper tests)
7
+ * would evaluate `types.ts` while the adapter was still initialising and hit a TDZ error on the
8
+ * registry's `create` reference. `types.ts` re-exports this constant, so importing it from there
9
+ * still works.
10
+ *
11
+ * Bun's `fetch` — like WHATWG fetch — has no overall request deadline unless a `signal` is passed,
12
+ * so a stalled peer would hang the workflow step until someone killed the run (R5).
13
+ */
14
+ export const PROVIDER_REQUEST_TIMEOUT_MS = 300_000;
@@ -70,10 +70,20 @@ export interface ImageProvider {
70
70
  }
71
71
 
72
72
  /** Injection seam for tests — mirrors `VoiceboxClientOptions.fetch` (voice-gen). */
73
+ /**
74
+ * Default per-request timeout for every image-gen provider HTTP call. Defined in `./timeouts` (a
75
+ * leaf module) and re-exported here so `types.ts` stays the public surface; see that file for why
76
+ * it cannot be declared inline (import cycle → TDZ). Overridable per call via
77
+ * `ProviderOptions.timeoutMs` (tests).
78
+ */
79
+ export { PROVIDER_REQUEST_TIMEOUT_MS } from './timeouts';
80
+
73
81
  export interface ProviderOptions {
74
82
  fetch?: typeof fetch;
75
83
  /** Environment consulted instead of process.env (tests). */
76
84
  env?: Record<string, string | undefined>;
85
+ /** Per-request timeout override (tests). Defaults to {@link PROVIDER_REQUEST_TIMEOUT_MS}. */
86
+ timeoutMs?: number;
77
87
  }
78
88
 
79
89
  export type ProviderFactory = (options?: ProviderOptions) => ImageProvider;
@@ -1,3 +1,4 @@
1
+ import { PROVIDER_REQUEST_TIMEOUT_MS } from './timeouts';
1
2
  import type { ImageProvider, ImageRequest, ImageResult, ProviderFactory, ProviderOptions } from './types';
2
3
 
3
4
  /**
@@ -180,6 +181,7 @@ interface ZaiResponse {
180
181
 
181
182
  export const createZai: ProviderFactory = (options?: ProviderOptions): ImageProvider => {
182
183
  const fetchFn = options?.fetch ?? fetch;
184
+ const timeoutMs = options?.timeoutMs ?? PROVIDER_REQUEST_TIMEOUT_MS;
183
185
  const env = options?.env ?? process.env;
184
186
  const readEnv = (key: string): string => env[key] ?? '';
185
187
 
@@ -220,11 +222,12 @@ export const createZai: ProviderFactory = (options?: ProviderOptions): ImageProv
220
222
  quality: req.quality === 'normal' ? 'standard' : 'hd',
221
223
  size,
222
224
  }),
225
+ signal: AbortSignal.timeout(timeoutMs),
223
226
  });
224
227
  if (!response.ok) throw new Error(`Z.AI API error (${response.status}): ${await response.text()}`);
225
228
  const url = ((await response.json()) as ZaiResponse).data?.[0]?.url;
226
229
  if (url === undefined) throw new Error('No image URL in Z.AI response');
227
- const imageResponse = await fetchFn(url);
230
+ const imageResponse = await fetchFn(url, { signal: AbortSignal.timeout(timeoutMs) });
228
231
  if (!imageResponse.ok) throw new Error(`Failed to download image from Z.AI: ${imageResponse.status}`);
229
232
  return {
230
233
  bytes: new Uint8Array(await imageResponse.arrayBuffer()),
@@ -21781,6 +21781,22 @@ var ResultSchema = exports_external.object({
21781
21781
  error: exports_external.string().optional(),
21782
21782
  metadata: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
21783
21783
  });
21784
+ var TargetOutcomeSchema = exports_external.object({
21785
+ target: exports_external.string(),
21786
+ ok: exports_external.boolean(),
21787
+ status: exports_external.enum(["published", "failed", "skipped"]),
21788
+ url: exports_external.string().optional(),
21789
+ id: exports_external.string().optional(),
21790
+ error: exports_external.string().optional(),
21791
+ durationMs: exports_external.number().optional(),
21792
+ metadata: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
21793
+ });
21794
+ var AggregateResultSchema = exports_external.object({
21795
+ ok: exports_external.boolean(),
21796
+ summary: exports_external.object({ total: exports_external.number(), succeeded: exports_external.number(), failed: exports_external.number(), skipped: exports_external.number() }),
21797
+ targets: exports_external.array(TargetOutcomeSchema),
21798
+ metadata: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
21799
+ });
21784
21800
  var DocListSchema = exports_external.array(DocSchema);
21785
21801
  var omniVoiceGenContentSchema = ContentSchema.extend({
21786
21802
  format: exports_external.literal("audio"),
@@ -21809,14 +21825,15 @@ var PluginKindSchema = exports_external.enum(PLUGIN_KINDS);
21809
21825
  // ../../packages/kk-core/src/manifest.ts
21810
21826
  init_zod();
21811
21827
  var OperationIOSchema = exports_external.enum(["doc-list", "markdown"]);
21812
- var PluginManifestSchema = exports_external.object({
21828
+ var PluginOperationSchema = exports_external.object({ input: OperationIOSchema, output: OperationIOSchema });
21829
+ var PluginManifestSchema = exports_external.looseObject({
21813
21830
  name: exports_external.string().min(1),
21814
21831
  kind: PluginKindSchema,
21815
21832
  entry: exports_external.string().min(1),
21816
21833
  version: exports_external.string().optional(),
21817
21834
  description: exports_external.string().optional(),
21818
- operations: exports_external.record(exports_external.string(), exports_external.object({ input: OperationIOSchema, output: OperationIOSchema })).optional()
21819
- }).passthrough();
21835
+ operations: exports_external.record(exports_external.string(), PluginOperationSchema).optional()
21836
+ });
21820
21837
  // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.4.39/node_modules/@gobing-ai/ts-runtime/dist/index.js
21821
21838
  init_file_system_node();
21822
21839
  init_config();