abelworkflow 1.0.0 → 1.1.1
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/extensions/pi-gpt-responses-compat/index.ts +0 -17
- package/lib/providers/claude.mjs +3 -27
- package/lib/providers/codex.mjs +7 -15
- package/lib/providers/pi.mjs +2 -37
- package/lib/providers/skills.mjs +10 -0
- package/package.json +1 -1
- package/skills/grok-search/scripts/groksearch_cli.py +42 -15
- package/extensions/pi-gpt-responses-compat/tls-fetch.mjs +0 -188
|
@@ -1,23 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { createNodeInsecureDispatcher, installProviderTlsFetch } from "./tls-fetch.mjs";
|
|
3
|
-
|
|
4
|
-
let nodeInsecureDispatcher: unknown;
|
|
5
|
-
|
|
6
|
-
function getNodeInsecureDispatcher() {
|
|
7
|
-
if (nodeInsecureDispatcher) return nodeInsecureDispatcher;
|
|
8
|
-
nodeInsecureDispatcher = createNodeInsecureDispatcher();
|
|
9
|
-
return nodeInsecureDispatcher;
|
|
10
|
-
}
|
|
11
2
|
|
|
12
3
|
export default function (pi: ExtensionAPI) {
|
|
13
|
-
if (typeof globalThis.fetch === "function") {
|
|
14
|
-
const runtime = typeof (globalThis as any).Bun === "undefined" ? "node" : "bun";
|
|
15
|
-
installProviderTlsFetch({
|
|
16
|
-
runtime,
|
|
17
|
-
insecureDispatcher: runtime === "node" ? getNodeInsecureDispatcher : undefined
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
|
|
21
4
|
pi.on("before_provider_request", (event, ctx) => {
|
|
22
5
|
const payload = event.payload as any;
|
|
23
6
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return;
|
package/lib/providers/claude.mjs
CHANGED
|
@@ -62,23 +62,12 @@ function mergeClaudeSettingsWithDefaults(settings) {
|
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
function applyClaudeInsecureTlsSetting(env = {}, enabled = false) {
|
|
66
|
-
const nextEnv = { ...env };
|
|
67
|
-
if (enabled) {
|
|
68
|
-
nextEnv.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
69
|
-
} else if (nextEnv.NODE_TLS_REJECT_UNAUTHORIZED === "0") {
|
|
70
|
-
delete nextEnv.NODE_TLS_REJECT_UNAUTHORIZED;
|
|
71
|
-
}
|
|
72
|
-
return nextEnv;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
65
|
function getExistingClaudeApiConfig(settings) {
|
|
76
66
|
const env = mergeClaudeSettingsWithDefaults(settings).env;
|
|
77
67
|
return {
|
|
78
68
|
baseUrl: env.ANTHROPIC_BASE_URL || "https://api.anthropic.com",
|
|
79
69
|
key: env.ANTHROPIC_API_KEY || "",
|
|
80
|
-
model: claudeModelEnvKeys.map((field) => env[field]).find(Boolean) || ""
|
|
81
|
-
insecureTls: env.NODE_TLS_REJECT_UNAUTHORIZED === "0"
|
|
70
|
+
model: claudeModelEnvKeys.map((field) => env[field]).find(Boolean) || ""
|
|
82
71
|
};
|
|
83
72
|
}
|
|
84
73
|
|
|
@@ -109,14 +98,9 @@ function ensureApprovedClaudeApiKey(config, apiKey) {
|
|
|
109
98
|
function buildClaudeApiSettings(settings, {
|
|
110
99
|
baseUrl,
|
|
111
100
|
key,
|
|
112
|
-
model
|
|
113
|
-
insecureTls = false
|
|
101
|
+
model
|
|
114
102
|
}) {
|
|
115
103
|
const nextSettings = mergeClaudeSettingsWithDefaults(settings);
|
|
116
|
-
nextSettings.env = applyClaudeInsecureTlsSetting(
|
|
117
|
-
nextSettings.env && typeof nextSettings.env === "object" ? nextSettings.env : {},
|
|
118
|
-
insecureTls
|
|
119
|
-
);
|
|
120
104
|
nextSettings.env.ANTHROPIC_BASE_URL = baseUrl;
|
|
121
105
|
nextSettings.env.ANTHROPIC_API_KEY = key;
|
|
122
106
|
delete nextSettings.env.ANTHROPIC_AUTH_TOKEN;
|
|
@@ -134,7 +118,6 @@ async function persistClaudeConfiguration(paths, { settings, metaConfig }) {
|
|
|
134
118
|
async function configureClaudeApi(paths = defaultPaths, promptApi) {
|
|
135
119
|
const {
|
|
136
120
|
assertNotCancelled,
|
|
137
|
-
confirmOrCancel,
|
|
138
121
|
passwordPromptOptions,
|
|
139
122
|
required,
|
|
140
123
|
requiredUnlessExisting,
|
|
@@ -158,11 +141,6 @@ async function configureClaudeApi(paths = defaultPaths, promptApi) {
|
|
|
158
141
|
assertNotCancelled(key);
|
|
159
142
|
const finalKey = resolvePasswordValue(key, existing.key);
|
|
160
143
|
|
|
161
|
-
const insecureTls = await confirmOrCancel({
|
|
162
|
-
message: "是否跳过 Claude Code TLS 证书校验?仅证书无法修复时启用(会放宽该进程全部 HTTPS 请求)",
|
|
163
|
-
initialValue: existing.insecureTls
|
|
164
|
-
});
|
|
165
|
-
|
|
166
144
|
const model = await p.text({
|
|
167
145
|
message: "Claude Code 模型",
|
|
168
146
|
initialValue: existing.model || undefined,
|
|
@@ -173,8 +151,7 @@ async function configureClaudeApi(paths = defaultPaths, promptApi) {
|
|
|
173
151
|
const apiSettings = buildClaudeApiSettings(settings, {
|
|
174
152
|
baseUrl,
|
|
175
153
|
key: finalKey,
|
|
176
|
-
model
|
|
177
|
-
insecureTls
|
|
154
|
+
model
|
|
178
155
|
});
|
|
179
156
|
const metaConfig = await readJsonFileSafe(paths.claudeMetaConfigPath, {}, { sensitive: true });
|
|
180
157
|
metaConfig.hasCompletedOnboarding = true;
|
|
@@ -188,7 +165,6 @@ async function configureClaudeApi(paths = defaultPaths, promptApi) {
|
|
|
188
165
|
}
|
|
189
166
|
|
|
190
167
|
export {
|
|
191
|
-
applyClaudeInsecureTlsSetting,
|
|
192
168
|
buildClaudeApiSettings,
|
|
193
169
|
buildDefaultClaudeSettings,
|
|
194
170
|
configureClaudeApi,
|
package/lib/providers/codex.mjs
CHANGED
|
@@ -25,6 +25,8 @@ import {
|
|
|
25
25
|
import { defaultPaths, maskSecret, pathToLabel } from "../paths.mjs";
|
|
26
26
|
import { normalizeOpenAiBaseUrl } from "./pi.mjs";
|
|
27
27
|
|
|
28
|
+
const CODEX_ENV_KEY = "OPENAI_API_KEY";
|
|
29
|
+
|
|
28
30
|
const publishedCodexDeveloperInstructionHashes = new Set([
|
|
29
31
|
"957adb227dd3fc298985acca38db4c22b8e018d387dab91f1ea8287db35385db"
|
|
30
32
|
]);
|
|
@@ -249,13 +251,6 @@ async function deployBundledCodexAgents(paths = defaultPaths, previousManagedFil
|
|
|
249
251
|
return result;
|
|
250
252
|
}
|
|
251
253
|
|
|
252
|
-
function getDefaultCodexEnvKey(providerId) {
|
|
253
|
-
if (providerId === "openai") return "OPENAI_API_KEY";
|
|
254
|
-
if (providerId === "abelworkflow") return "ABELWORKFLOW_API_KEY";
|
|
255
|
-
const digest = createHash("sha256").update(providerId).digest("hex").toUpperCase();
|
|
256
|
-
return `ABELWORKFLOW_PROVIDER_${digest}_API_KEY`;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
254
|
function getCodexProviderSectionName(providerId) {
|
|
260
255
|
return `model_providers.${formatTomlKeySegment(providerId)}`;
|
|
261
256
|
}
|
|
@@ -275,8 +270,7 @@ function assertCodexAuthKeyAvailable(auth, envKey) {
|
|
|
275
270
|
function resolveExistingCodexApiConfig(content, auth = {}) {
|
|
276
271
|
const providerId = readTopLevelTomlString(content, "model_provider") || "abelworkflow";
|
|
277
272
|
const provider = parseTomlSection(content, getCodexProviderSectionName(providerId));
|
|
278
|
-
const
|
|
279
|
-
const envKey = configuredEnvKey || getDefaultCodexEnvKey(providerId);
|
|
273
|
+
const envKey = CODEX_ENV_KEY;
|
|
280
274
|
assertCodexAuthKeyAvailable(auth, envKey);
|
|
281
275
|
const apiKey = typeof auth[envKey] === "string" ? auth[envKey] : "";
|
|
282
276
|
|
|
@@ -331,7 +325,7 @@ async function configureCodexApi(paths = defaultPaths, promptApi, ownership = {}
|
|
|
331
325
|
const shouldDeploySubagents = await confirmOrCancel({ message: "是否部署 Codex subagents 配置?", initialValue: true });
|
|
332
326
|
let managedCodexAgentFiles = { ...(ownership.managedCodexAgentFiles ?? {}) };
|
|
333
327
|
|
|
334
|
-
const envKey =
|
|
328
|
+
const envKey = CODEX_ENV_KEY;
|
|
335
329
|
const currentContent = await pathExists(paths.codexConfigPath) ? await readFile(paths.codexConfigPath, "utf8") : "";
|
|
336
330
|
const templateContent = await loadBundledCodexConfigTemplate(paths);
|
|
337
331
|
const content = buildCodexConfigContent(currentContent, {
|
|
@@ -340,8 +334,7 @@ async function configureCodexApi(paths = defaultPaths, promptApi, ownership = {}
|
|
|
340
334
|
includeSubagentDefaults: shouldDeploySubagents,
|
|
341
335
|
providerId,
|
|
342
336
|
providerName,
|
|
343
|
-
baseUrl
|
|
344
|
-
envKey
|
|
337
|
+
baseUrl
|
|
345
338
|
});
|
|
346
339
|
|
|
347
340
|
const auth = mergeCodexAuthData(
|
|
@@ -382,8 +375,7 @@ function buildCodexConfigContent(currentContent, {
|
|
|
382
375
|
includeSubagentDefaults = true,
|
|
383
376
|
providerId,
|
|
384
377
|
providerName,
|
|
385
|
-
baseUrl
|
|
386
|
-
envKey
|
|
378
|
+
baseUrl
|
|
387
379
|
}) {
|
|
388
380
|
const effectiveTemplateContent = includeSubagentDefaults
|
|
389
381
|
? templateContent
|
|
@@ -403,7 +395,7 @@ function buildCodexConfigContent(currentContent, {
|
|
|
403
395
|
name: providerName,
|
|
404
396
|
base_url: baseUrl,
|
|
405
397
|
wire_api: "responses",
|
|
406
|
-
temp_env_key:
|
|
398
|
+
temp_env_key: CODEX_ENV_KEY,
|
|
407
399
|
requires_openai_auth: true,
|
|
408
400
|
supports_websockets: true
|
|
409
401
|
});
|
package/lib/providers/pi.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import * as p from "@clack/prompts";
|
|
4
|
-
import { piInsecureTlsHeader } from "../../extensions/pi-gpt-responses-compat/tls-fetch.mjs";
|
|
5
4
|
import {
|
|
6
5
|
readJsonFileSafe,
|
|
7
6
|
readJsoncFileSafe,
|
|
@@ -310,36 +309,17 @@ function buildPiModelConfig(modelId, existingModel = {}) {
|
|
|
310
309
|
};
|
|
311
310
|
}
|
|
312
311
|
|
|
313
|
-
function hasPiInsecureTlsSetting(modelsConfig = {}, providerId) {
|
|
314
|
-
const headers = modelsConfig.providers?.[requirePiProviderId(providerId)]?.headers;
|
|
315
|
-
return headers && typeof headers === "object"
|
|
316
|
-
? Object.keys(headers).some((key) => key.toLowerCase() === piInsecureTlsHeader)
|
|
317
|
-
: false;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
312
|
function buildPiModelsConfig(modelsConfig = {}, {
|
|
321
313
|
providerId,
|
|
322
314
|
baseUrl,
|
|
323
315
|
api,
|
|
324
|
-
modelIds
|
|
325
|
-
insecureTls = false
|
|
316
|
+
modelIds
|
|
326
317
|
}) {
|
|
327
318
|
const targetProviderId = requirePiProviderId(providerId);
|
|
328
319
|
const providers = modelsConfig.providers && typeof modelsConfig.providers === "object" ? modelsConfig.providers : {};
|
|
329
320
|
const currentProvider = providers[targetProviderId] && typeof providers[targetProviderId] === "object"
|
|
330
321
|
? providers[targetProviderId]
|
|
331
322
|
: {};
|
|
332
|
-
const headers = currentProvider.headers && typeof currentProvider.headers === "object"
|
|
333
|
-
? { ...currentProvider.headers }
|
|
334
|
-
: {};
|
|
335
|
-
for (const key of Object.keys(headers)) {
|
|
336
|
-
if (key.toLowerCase() === piInsecureTlsHeader) {
|
|
337
|
-
delete headers[key];
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
if (insecureTls) {
|
|
341
|
-
headers[piInsecureTlsHeader] = new URL(baseUrl).origin;
|
|
342
|
-
}
|
|
343
323
|
const existingModels = new Map(
|
|
344
324
|
(Array.isArray(currentProvider.models) ? currentProvider.models : [])
|
|
345
325
|
.filter((model) => model?.id)
|
|
@@ -357,11 +337,6 @@ function buildPiModelsConfig(modelsConfig = {}, {
|
|
|
357
337
|
models: modelIds.map((modelId) => buildPiModelConfig(modelId, existingModels.get(modelId)))
|
|
358
338
|
};
|
|
359
339
|
delete provider.apiKey;
|
|
360
|
-
if (Object.keys(headers).length) {
|
|
361
|
-
provider.headers = headers;
|
|
362
|
-
} else {
|
|
363
|
-
delete provider.headers;
|
|
364
|
-
}
|
|
365
340
|
|
|
366
341
|
return {
|
|
367
342
|
...modelsConfig,
|
|
@@ -413,7 +388,6 @@ function buildPiConfiguration({ auth, models, settings }, {
|
|
|
413
388
|
baseUrl,
|
|
414
389
|
api,
|
|
415
390
|
modelIds,
|
|
416
|
-
insecureTls,
|
|
417
391
|
defaultModel
|
|
418
392
|
}) {
|
|
419
393
|
const targetProviderId = requirePiProviderId(providerId);
|
|
@@ -425,8 +399,7 @@ function buildPiConfiguration({ auth, models, settings }, {
|
|
|
425
399
|
providerId: targetProviderId,
|
|
426
400
|
baseUrl,
|
|
427
401
|
api,
|
|
428
|
-
modelIds
|
|
429
|
-
insecureTls
|
|
402
|
+
modelIds
|
|
430
403
|
}),
|
|
431
404
|
settings: buildPiSettingsConfig(settings, targetProviderId, defaultModel)
|
|
432
405
|
};
|
|
@@ -455,7 +428,6 @@ async function configurePiApi(paths = defaultPaths, ensurePiResourcesLinked = as
|
|
|
455
428
|
}
|
|
456
429
|
const {
|
|
457
430
|
assertNotCancelled,
|
|
458
|
-
confirmOrCancel,
|
|
459
431
|
passwordPromptOptions,
|
|
460
432
|
required,
|
|
461
433
|
requiredUnlessExisting,
|
|
@@ -495,11 +467,6 @@ async function configurePiApi(paths = defaultPaths, ensurePiResourcesLinked = as
|
|
|
495
467
|
const inferredApi = inferPiApiFromBaseUrl(baseUrlInput);
|
|
496
468
|
const baseUrl = normalizeOpenAiBaseUrl(baseUrlInput);
|
|
497
469
|
|
|
498
|
-
const insecureTls = await confirmOrCancel({
|
|
499
|
-
message: `是否仅为 ${providerLabel} 中转请求跳过 TLS 证书校验?仅证书无法修复时启用`,
|
|
500
|
-
initialValue: hasPiInsecureTlsSetting(modelsConfig, providerId)
|
|
501
|
-
});
|
|
502
|
-
|
|
503
470
|
const piApiOptions = getPiApiPromptOptions();
|
|
504
471
|
const initialApi = piApiOptions.some((option) => option.value === existing.api)
|
|
505
472
|
? existing.api
|
|
@@ -542,7 +509,6 @@ async function configurePiApi(paths = defaultPaths, ensurePiResourcesLinked = as
|
|
|
542
509
|
baseUrl,
|
|
543
510
|
api,
|
|
544
511
|
modelIds,
|
|
545
|
-
insecureTls,
|
|
546
512
|
defaultModel: finalDefaultModel
|
|
547
513
|
});
|
|
548
514
|
await ensurePiResourcesLinked(paths);
|
|
@@ -564,7 +530,6 @@ export {
|
|
|
564
530
|
configurePiApi,
|
|
565
531
|
detectPiEffectiveModel,
|
|
566
532
|
getPiApiPromptOptions,
|
|
567
|
-
hasPiInsecureTlsSetting,
|
|
568
533
|
inferPiApiFromBaseUrl,
|
|
569
534
|
normalizeOpenAiBaseUrl,
|
|
570
535
|
parsePiRpcEffectiveModel,
|
package/lib/providers/skills.mjs
CHANGED
|
@@ -56,6 +56,15 @@ async function configureGrokSearchEnv(paths, ensureSkillPresent = async () => {}
|
|
|
56
56
|
initialValue: Boolean(existing.TAVILY_API_KEY)
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
+
const tavilyUrl = useTavily
|
|
60
|
+
? await p.text({
|
|
61
|
+
message: "Tavily API URL",
|
|
62
|
+
initialValue: existing.TAVILY_API_URL || "https://api.tavily.com",
|
|
63
|
+
validate: required()
|
|
64
|
+
})
|
|
65
|
+
: null;
|
|
66
|
+
if (useTavily) assertNotCancelled(tavilyUrl);
|
|
67
|
+
|
|
59
68
|
const tavilyKey = useTavily
|
|
60
69
|
? await p.password(passwordPromptOptions(
|
|
61
70
|
"Tavily API Key",
|
|
@@ -70,6 +79,7 @@ async function configureGrokSearchEnv(paths, ensureSkillPresent = async () => {}
|
|
|
70
79
|
GROK_API_URL: baseUrl,
|
|
71
80
|
GROK_API_KEY: finalApiKey,
|
|
72
81
|
GROK_MODEL: model,
|
|
82
|
+
TAVILY_API_URL: tavilyUrl,
|
|
73
83
|
TAVILY_API_KEY: finalTavilyKey,
|
|
74
84
|
TAVILY_ENABLED: useTavily ? "true" : null
|
|
75
85
|
});
|
package/package.json
CHANGED
|
@@ -216,7 +216,17 @@ def _emit_tavily_warning(message: str) -> None:
|
|
|
216
216
|
RETRYABLE_STATUS_CODES = {408, 429, 500, 502, 503, 504}
|
|
217
217
|
|
|
218
218
|
|
|
219
|
+
class StreamEmbeddedError(Exception):
|
|
220
|
+
"""SSE 流内嵌错误事件(HTTP 200 + data: {"error": ...})。"""
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class EmptyStreamError(Exception):
|
|
224
|
+
"""流式响应解析完成但内容为空。"""
|
|
225
|
+
|
|
226
|
+
|
|
219
227
|
def _is_retryable_exception(exc) -> bool:
|
|
228
|
+
if isinstance(exc, (StreamEmbeddedError, EmptyStreamError)):
|
|
229
|
+
return True
|
|
220
230
|
if isinstance(exc, (httpx.TimeoutException, httpx.NetworkError, httpx.ConnectError, httpx.RemoteProtocolError)):
|
|
221
231
|
return True
|
|
222
232
|
if isinstance(exc, httpx.HTTPStatusError):
|
|
@@ -501,6 +511,7 @@ def _normalize_tavily_base_url(raw: str) -> str:
|
|
|
501
511
|
|
|
502
512
|
_http_client: Optional[httpx.AsyncClient] = None
|
|
503
513
|
_DEFAULT_TIMEOUT = httpx.Timeout(connect=6.0, read=60.0, write=10.0, pool=None)
|
|
514
|
+
_NON_STREAM_TIMEOUT = httpx.Timeout(connect=6.0, read=10.0, write=10.0, pool=None)
|
|
504
515
|
|
|
505
516
|
|
|
506
517
|
async def get_http_client() -> httpx.AsyncClient:
|
|
@@ -586,16 +597,23 @@ class GrokSearchProvider:
|
|
|
586
597
|
return await self._execute(payload)
|
|
587
598
|
|
|
588
599
|
async def _execute(self, payload: dict) -> str:
|
|
589
|
-
"""
|
|
600
|
+
"""执行请求:流式优先,失败时回退到非流式(10s 超时 fail-fast)。"""
|
|
590
601
|
try:
|
|
602
|
+
return await self._execute_stream(payload)
|
|
603
|
+
except httpx.HTTPStatusError as e:
|
|
604
|
+
if e.response.status_code not in RETRYABLE_STATUS_CODES:
|
|
605
|
+
raise
|
|
606
|
+
if config.debug_enabled:
|
|
607
|
+
print(f"[DEBUG] 流式失败: {e},回退到非流式", file=sys.stderr)
|
|
591
608
|
return await self._execute_non_stream(payload)
|
|
592
|
-
except (httpx.
|
|
609
|
+
except (httpx.TimeoutException, httpx.NetworkError, httpx.RemoteProtocolError,
|
|
610
|
+
StreamEmbeddedError, EmptyStreamError, json.JSONDecodeError) as e:
|
|
593
611
|
if config.debug_enabled:
|
|
594
|
-
print(f"[DEBUG]
|
|
595
|
-
return await self.
|
|
612
|
+
print(f"[DEBUG] 流式失败: {e},回退到非流式", file=sys.stderr)
|
|
613
|
+
return await self._execute_non_stream(payload)
|
|
596
614
|
|
|
597
615
|
async def _execute_non_stream(self, payload: dict) -> str:
|
|
598
|
-
"""
|
|
616
|
+
"""非流式请求(流式失败后的回退方案,10s read 超时 fail-fast)。"""
|
|
599
617
|
payload_copy = {**payload, "stream": False}
|
|
600
618
|
client = await get_http_client()
|
|
601
619
|
|
|
@@ -610,6 +628,7 @@ class GrokSearchProvider:
|
|
|
610
628
|
f"{self.api_url}/chat/completions",
|
|
611
629
|
headers=self._headers,
|
|
612
630
|
json=payload_copy,
|
|
631
|
+
timeout=_NON_STREAM_TIMEOUT,
|
|
613
632
|
)
|
|
614
633
|
response.raise_for_status()
|
|
615
634
|
data = response.json()
|
|
@@ -619,7 +638,7 @@ class GrokSearchProvider:
|
|
|
619
638
|
return ""
|
|
620
639
|
|
|
621
640
|
async def _execute_stream(self, payload: dict) -> str:
|
|
622
|
-
"""
|
|
641
|
+
"""流式请求(首选,chunk 保活避免网关超时)。"""
|
|
623
642
|
payload_copy = {**payload, "stream": True}
|
|
624
643
|
client = await get_http_client()
|
|
625
644
|
|
|
@@ -655,24 +674,30 @@ class GrokSearchProvider:
|
|
|
655
674
|
try:
|
|
656
675
|
json_str = line[5:].lstrip()
|
|
657
676
|
data = json.loads(json_str)
|
|
658
|
-
choices = data.get("choices", [])
|
|
659
|
-
if choices:
|
|
660
|
-
delta = choices[0].get("delta", {})
|
|
661
|
-
if "content" in delta:
|
|
662
|
-
content += delta["content"]
|
|
663
677
|
except (json.JSONDecodeError, IndexError):
|
|
664
678
|
continue
|
|
679
|
+
if isinstance(data, dict) and "error" in data:
|
|
680
|
+
raise StreamEmbeddedError(json.dumps(data["error"], ensure_ascii=False)[:300])
|
|
681
|
+
choices = data.get("choices", [])
|
|
682
|
+
if choices:
|
|
683
|
+
delta = choices[0].get("delta", {})
|
|
684
|
+
if "content" in delta:
|
|
685
|
+
content += delta["content"]
|
|
665
686
|
|
|
666
687
|
if not content and full_body_buffer:
|
|
667
688
|
try:
|
|
668
689
|
full_text = "".join(full_body_buffer)
|
|
669
690
|
data = json.loads(full_text)
|
|
691
|
+
if isinstance(data, dict) and "error" in data:
|
|
692
|
+
raise StreamEmbeddedError(json.dumps(data["error"], ensure_ascii=False)[:300])
|
|
670
693
|
if "choices" in data and data["choices"]:
|
|
671
694
|
message = data["choices"][0].get("message", {})
|
|
672
695
|
content = message.get("content", "")
|
|
673
696
|
except json.JSONDecodeError:
|
|
674
697
|
pass
|
|
675
698
|
|
|
699
|
+
if not content.strip():
|
|
700
|
+
raise EmptyStreamError("流式响应内容为空")
|
|
676
701
|
return content
|
|
677
702
|
|
|
678
703
|
|
|
@@ -935,8 +960,9 @@ async def cmd_web_search(args):
|
|
|
935
960
|
except ValueError as e:
|
|
936
961
|
print(json.dumps({"error": str(e)}, ensure_ascii=False), file=sys.stderr)
|
|
937
962
|
sys.exit(1)
|
|
938
|
-
except httpx.
|
|
939
|
-
|
|
963
|
+
except httpx.HTTPError as e:
|
|
964
|
+
detail = str(e.response.status_code) if isinstance(e, httpx.HTTPStatusError) else (str(e) or type(e).__name__)
|
|
965
|
+
print(json.dumps({"error": f"API错误: {detail}"}, ensure_ascii=False), file=sys.stderr)
|
|
940
966
|
sys.exit(1)
|
|
941
967
|
|
|
942
968
|
|
|
@@ -974,8 +1000,9 @@ async def cmd_web_fetch(args):
|
|
|
974
1000
|
except ValueError as e:
|
|
975
1001
|
print(f"错误: {e}", file=sys.stderr)
|
|
976
1002
|
sys.exit(1)
|
|
977
|
-
except httpx.
|
|
978
|
-
|
|
1003
|
+
except httpx.HTTPError as e:
|
|
1004
|
+
detail = str(e.response.status_code) if isinstance(e, httpx.HTTPStatusError) else (str(e) or type(e).__name__)
|
|
1005
|
+
print(f"API错误: {detail}", file=sys.stderr)
|
|
979
1006
|
sys.exit(1)
|
|
980
1007
|
|
|
981
1008
|
if not result and tavily_error and not use_grok_fallback:
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
export const piInsecureTlsHeader = "x-abelworkflow-insecure-tls";
|
|
2
|
-
|
|
3
|
-
const installedFetchMarker = Symbol.for("abelworkflow.pi-provider-tls-fetch");
|
|
4
|
-
const undiciGlobalDispatchers = [
|
|
5
|
-
Symbol.for("undici.globalDispatcher.2"),
|
|
6
|
-
Symbol.for("undici.globalDispatcher.1")
|
|
7
|
-
];
|
|
8
|
-
const redirectStatuses = new Set([301, 302, 303, 307, 308]);
|
|
9
|
-
const maxRedirects = 5;
|
|
10
|
-
|
|
11
|
-
function getMarkedHeaders(input, init) {
|
|
12
|
-
const source = init?.headers !== undefined
|
|
13
|
-
? init.headers
|
|
14
|
-
: typeof Request !== "undefined" && input instanceof Request
|
|
15
|
-
? input.headers
|
|
16
|
-
: undefined;
|
|
17
|
-
if (source === undefined) return null;
|
|
18
|
-
|
|
19
|
-
const headers = new Headers(source);
|
|
20
|
-
if (!headers.has(piInsecureTlsHeader)) return null;
|
|
21
|
-
const allowedOrigin = headers.get(piInsecureTlsHeader);
|
|
22
|
-
headers.delete(piInsecureTlsHeader);
|
|
23
|
-
return { allowedOrigin, headers };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function getRequestUrl(input) {
|
|
27
|
-
return typeof Request !== "undefined" && input instanceof Request
|
|
28
|
-
? input.url
|
|
29
|
-
: input instanceof URL
|
|
30
|
-
? input.href
|
|
31
|
-
: input;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function getRequestOrigin(input) {
|
|
35
|
-
try {
|
|
36
|
-
return new URL(getRequestUrl(input)).origin;
|
|
37
|
-
} catch {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function redirectRequestInit(input, init, status) {
|
|
43
|
-
const method = String(init.method ?? (
|
|
44
|
-
typeof Request !== "undefined" && input instanceof Request ? input.method : "GET"
|
|
45
|
-
)).toUpperCase();
|
|
46
|
-
const switchToGet = status === 303 && method !== "HEAD"
|
|
47
|
-
|| (status === 301 || status === 302) && method === "POST";
|
|
48
|
-
if (!switchToGet) {
|
|
49
|
-
if (typeof Request !== "undefined" && input instanceof Request && method !== "GET" && method !== "HEAD") {
|
|
50
|
-
throw new Error("Pi insecure TLS cannot safely replay a redirected Request body");
|
|
51
|
-
}
|
|
52
|
-
return init;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const headers = new Headers(init.headers);
|
|
56
|
-
for (const name of ["content-encoding", "content-language", "content-length", "content-location", "content-type"]) {
|
|
57
|
-
headers.delete(name);
|
|
58
|
-
}
|
|
59
|
-
const nextInit = { ...init, method: "GET", headers };
|
|
60
|
-
delete nextInit.body;
|
|
61
|
-
return nextInit;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function fetchWithSameOriginRedirects({ fetchOnce, input, init, allowedOrigin }) {
|
|
65
|
-
let nextInput = input;
|
|
66
|
-
let nextInit = init;
|
|
67
|
-
|
|
68
|
-
for (let redirectCount = 0; redirectCount <= maxRedirects; redirectCount += 1) {
|
|
69
|
-
const response = await fetchOnce(nextInput, nextInit);
|
|
70
|
-
const location = redirectStatuses.has(response?.status) ? response.headers?.get?.("location") : null;
|
|
71
|
-
if (!location) return response;
|
|
72
|
-
if (redirectCount === maxRedirects) {
|
|
73
|
-
await response.body?.cancel?.();
|
|
74
|
-
throw new Error(`Pi insecure TLS exceeded ${maxRedirects} same-origin redirects`);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const target = new URL(location, getRequestUrl(nextInput));
|
|
78
|
-
if (target.origin !== allowedOrigin) {
|
|
79
|
-
await response.body?.cancel?.();
|
|
80
|
-
throw new Error(`Pi insecure TLS blocked cross-origin redirect to ${target.origin}`);
|
|
81
|
-
}
|
|
82
|
-
await response.body?.cancel?.();
|
|
83
|
-
nextInit = redirectRequestInit(nextInput, nextInit, response.status);
|
|
84
|
-
nextInput = target.href;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function applyNodeDispatcher(fetchImpl, input, init, dispatcherSource) {
|
|
89
|
-
let dispatcher;
|
|
90
|
-
try {
|
|
91
|
-
dispatcher = typeof dispatcherSource === "function" ? dispatcherSource() : dispatcherSource;
|
|
92
|
-
} catch (error) {
|
|
93
|
-
return Promise.reject(error);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (dispatcher && typeof dispatcher.then === "function") {
|
|
97
|
-
return dispatcher.then((resolved) => applyNodeDispatcher(fetchImpl, input, init, resolved));
|
|
98
|
-
}
|
|
99
|
-
if (!dispatcher || typeof dispatcher.dispatch !== "function") {
|
|
100
|
-
return Promise.reject(new Error("Pi insecure TLS requires an injected Undici dispatcher"));
|
|
101
|
-
}
|
|
102
|
-
return fetchImpl(input, { ...init, dispatcher });
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function getUndiciDispatcherConstructor(target) {
|
|
106
|
-
return undiciGlobalDispatchers
|
|
107
|
-
.map((symbol) => target[symbol]?.constructor)
|
|
108
|
-
.find((constructor) => typeof constructor === "function");
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function initializeUndiciGlobalDispatcher(target) {
|
|
112
|
-
if (typeof target.fetch !== "function") return;
|
|
113
|
-
try {
|
|
114
|
-
void Promise.resolve(target.fetch("data:,")).catch(() => {});
|
|
115
|
-
} catch {
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function createNodeInsecureDispatcher(target = globalThis) {
|
|
120
|
-
let Dispatcher = getUndiciDispatcherConstructor(target);
|
|
121
|
-
if (typeof Dispatcher !== "function") {
|
|
122
|
-
initializeUndiciGlobalDispatcher(target);
|
|
123
|
-
Dispatcher = getUndiciDispatcherConstructor(target);
|
|
124
|
-
}
|
|
125
|
-
if (typeof Dispatcher !== "function") {
|
|
126
|
-
throw new Error("Pi insecure TLS requires the active Undici dispatcher");
|
|
127
|
-
}
|
|
128
|
-
return new Dispatcher({
|
|
129
|
-
allowH2: false,
|
|
130
|
-
connect: { rejectUnauthorized: false },
|
|
131
|
-
requestTls: { rejectUnauthorized: false }
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function createProviderTlsFetch({
|
|
136
|
-
fetchImpl = globalThis.fetch,
|
|
137
|
-
runtime = typeof globalThis.Bun === "undefined" ? "node" : "bun",
|
|
138
|
-
insecureDispatcher
|
|
139
|
-
} = {}) {
|
|
140
|
-
if (typeof fetchImpl !== "function") {
|
|
141
|
-
throw new TypeError("A fetch implementation is required");
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return function providerTlsFetch(input, init) {
|
|
145
|
-
const marked = getMarkedHeaders(input, init);
|
|
146
|
-
if (!marked) return fetchImpl(input, init);
|
|
147
|
-
|
|
148
|
-
const nextInit = { ...(init ?? {}), headers: marked.headers };
|
|
149
|
-
if (!marked.allowedOrigin || getRequestOrigin(input) !== marked.allowedOrigin) {
|
|
150
|
-
return fetchImpl(input, nextInit);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
nextInit.redirect = "manual";
|
|
154
|
-
const fetchOnce = runtime === "bun"
|
|
155
|
-
? (nextInput, redirectInit) => {
|
|
156
|
-
const tls = redirectInit.tls && typeof redirectInit.tls === "object" ? redirectInit.tls : {};
|
|
157
|
-
return fetchImpl(nextInput, {
|
|
158
|
-
...redirectInit,
|
|
159
|
-
tls: { ...tls, rejectUnauthorized: false }
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
: (nextInput, redirectInit) => applyNodeDispatcher(
|
|
163
|
-
fetchImpl,
|
|
164
|
-
nextInput,
|
|
165
|
-
redirectInit,
|
|
166
|
-
insecureDispatcher
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
return fetchWithSameOriginRedirects({
|
|
170
|
-
fetchOnce,
|
|
171
|
-
input,
|
|
172
|
-
init: nextInit,
|
|
173
|
-
allowedOrigin: marked.allowedOrigin
|
|
174
|
-
});
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export function installProviderTlsFetch({ target = globalThis, ...options } = {}) {
|
|
179
|
-
if (target.fetch?.[installedFetchMarker]) return target.fetch;
|
|
180
|
-
|
|
181
|
-
const wrapped = createProviderTlsFetch({
|
|
182
|
-
...options,
|
|
183
|
-
fetchImpl: target.fetch.bind(target)
|
|
184
|
-
});
|
|
185
|
-
Object.defineProperty(wrapped, installedFetchMarker, { value: true });
|
|
186
|
-
target.fetch = wrapped;
|
|
187
|
-
return wrapped;
|
|
188
|
-
}
|