abelworkflow 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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/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:
|