@armadra/agent 0.7.2 → 0.7.4
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/CHANGELOG.md +41 -0
- package/CHANGELOG.zh-CN.md +41 -0
- package/dist/agent/retry.d.ts +14 -5
- package/dist/agent/retry.js +30 -8
- package/dist/agent/session-cache-key.d.ts +8 -0
- package/dist/agent/session-cache-key.js +46 -0
- package/dist/agent/session-cache.d.ts +2 -3
- package/dist/agent/session-cache.js +19 -37
- package/dist/agent/session-classifier.d.ts +3 -2
- package/dist/agent/session-classifier.js +36 -10
- package/dist/agent/session-compaction.d.ts +30 -5
- package/dist/agent/session-compaction.js +69 -29
- package/dist/agent/session-core.d.ts +2 -0
- package/dist/agent/session-run.d.ts +12 -0
- package/dist/agent/session-run.js +29 -10
- package/dist/agent/session-subagent.d.ts +8 -1
- package/dist/agent/session-subagent.js +37 -14
- package/dist/agent/session-sync.d.ts +8 -2
- package/dist/agent/session-sync.js +7 -10
- package/dist/agent/session-tools.js +3 -2
- package/dist/agent/subagent-fork.d.ts +67 -0
- package/dist/agent/subagent-fork.js +87 -0
- package/dist/agent/subagent-registry.d.ts +2 -2
- package/dist/agent/subagent-registry.js +2 -2
- package/dist/agent/tool-availability.d.ts +8 -0
- package/dist/agent/tool-availability.js +8 -0
- package/dist/agent/tool-runner.js +13 -4
- package/dist/agent/worktree.d.ts +8 -0
- package/dist/agent/worktree.js +51 -4
- package/dist/agents/parse.js +6 -1
- package/dist/agents/task-record.js +3 -0
- package/dist/agents/types.d.ts +2 -0
- package/dist/ai/apis/anthropic-messages.js +10 -5
- package/dist/ai/apis/anthropic-request.d.ts +2 -1
- package/dist/ai/apis/anthropic-request.js +9 -3
- package/dist/ai/apis/google-generative-ai.js +4 -2
- package/dist/ai/apis/max-tokens.d.ts +42 -0
- package/dist/ai/apis/max-tokens.js +121 -0
- package/dist/ai/apis/openai-completions.js +10 -5
- package/dist/ai/apis/openai-request.js +1 -1
- package/dist/ai/apis/openai-responses-request.js +1 -1
- package/dist/ai/apis/openai-responses.js +9 -5
- package/dist/ai/apis/shared.d.ts +3 -1
- package/dist/ai/apis/shared.js +21 -3
- package/dist/ai/cache/fingerprint.d.ts +4 -1
- package/dist/ai/cache/fingerprint.js +18 -1
- package/dist/ai/cache/miss.d.ts +2 -1
- package/dist/ai/cache/miss.js +9 -3
- package/dist/ai/cache/reporting.js +5 -1
- package/dist/ai/cache/types.d.ts +10 -2
- package/dist/ai/cache/warmer.d.ts +6 -1
- package/dist/ai/cache/warmer.js +7 -0
- package/dist/ai/context.d.ts +12 -4
- package/dist/ai/context.js +43 -24
- package/dist/ai/fake/fake-provider.d.ts +8 -1
- package/dist/ai/fake/fake-provider.js +19 -7
- package/dist/ai/fake/fake-script.d.ts +2 -0
- package/dist/ai/http.d.ts +17 -9
- package/dist/ai/http.js +33 -13
- package/dist/ai/image-intern.d.ts +29 -0
- package/dist/ai/image-intern.js +58 -0
- package/dist/ai/json-body.d.ts +31 -0
- package/dist/ai/json-body.js +208 -0
- package/dist/ai/overflow.js +5 -0
- package/dist/ai/providers/catalog-data.js +13 -13
- package/dist/ai/providers/catalog.d.ts +33 -0
- package/dist/ai/providers/catalog.js +105 -6
- package/dist/ai/providers/enrich.d.ts +13 -3
- package/dist/ai/providers/enrich.js +87 -31
- package/dist/ai/sse.d.ts +3 -2
- package/dist/ai/sse.js +3 -2
- package/dist/ai/types.d.ts +9 -2
- package/dist/bundle/ama.cjs +3142 -1805
- package/dist/cli/compose-agents.js +3 -0
- package/dist/cli/compose-extensions.js +2 -0
- package/dist/cli/compose-request.d.ts +20 -0
- package/dist/cli/compose-request.js +37 -0
- package/dist/cli/compose-session.js +2 -2
- package/dist/cli/subcommands/model-meta.d.ts +2 -1
- package/dist/cli/subcommands/model-meta.js +3 -2
- package/dist/codemode/host-side.d.ts +11 -4
- package/dist/codemode/host-side.js +25 -6
- package/dist/codemode/tool.d.ts +2 -0
- package/dist/codemode/tool.js +4 -0
- package/dist/compaction/prefix-estimate.d.ts +3 -1
- package/dist/compaction/prefix-estimate.js +7 -3
- package/dist/config/json-schema.js +9 -2
- package/dist/config/key-docs.js +13 -4
- package/dist/config/schema-w5.js +9 -1
- package/dist/config/schema.d.ts +1 -1
- package/dist/config/schema.js +19 -3
- package/dist/config/settings-registry.js +6 -0
- package/dist/config/types-w5.d.ts +2 -0
- package/dist/config/types.d.ts +19 -1
- package/dist/config/types.js +4 -0
- package/dist/i18n/catalog.d.ts +15 -2
- package/dist/i18n/messages/cli.d.ts +2 -0
- package/dist/i18n/messages/cli.js +2 -0
- package/dist/i18n/messages/config-keys.d.ts +10 -0
- package/dist/i18n/messages/config-keys.js +12 -2
- package/dist/i18n/messages/config.d.ts +14 -0
- package/dist/i18n/messages/config.js +4 -0
- package/dist/i18n/messages/errors.d.ts +3 -2
- package/dist/i18n/messages/errors.js +15 -2
- package/dist/i18n/messages/report-context.d.ts +4 -0
- package/dist/i18n/messages/report-context.js +4 -0
- package/dist/i18n/messages/report.d.ts +6 -0
- package/dist/i18n/messages/report.js +2 -0
- package/dist/i18n/messages/settings.d.ts +4 -0
- package/dist/i18n/messages/settings.js +6 -2
- package/dist/i18n/messages/subcommands.d.ts +2 -2
- package/dist/i18n/messages/subcommands.js +4 -2
- package/dist/modes/acp/acp-server.js +15 -5
- package/dist/modes/acp/acp-sessions.d.ts +9 -0
- package/dist/modes/acp/acp-sessions.js +25 -0
- package/dist/modes/context-report.d.ts +2 -0
- package/dist/modes/context-report.js +25 -8
- package/dist/modes/print/json-event.d.ts +12 -1
- package/dist/modes/print/json-event.js +48 -1
- package/dist/modes/rpc/rpc-mode.d.ts +1 -0
- package/dist/modes/rpc/rpc-mode.js +9 -2
- package/dist/modes/session-report.d.ts +1 -1
- package/dist/modes/session-report.js +12 -6
- package/dist/rpc.d.ts +5 -2
- package/dist/session/line-reader.d.ts +28 -0
- package/dist/session/line-reader.js +82 -0
- package/dist/session/list.d.ts +14 -0
- package/dist/session/list.js +150 -0
- package/dist/session/manager.d.ts +7 -3
- package/dist/session/manager.js +21 -48
- package/dist/session/migrate.d.ts +2 -1
- package/dist/session/migrate.js +2 -1
- package/dist/session/projection.d.ts +16 -3
- package/dist/session/projection.js +96 -29
- package/dist/session/scan.d.ts +3 -3
- package/dist/session/scan.js +9 -18
- package/dist/session/store.d.ts +3 -0
- package/dist/session/store.js +37 -31
- package/dist/session/types.d.ts +7 -2
- package/dist/tools/bash.d.ts +1 -1
- package/dist/tools/bash.js +5 -4
- package/dist/tools/glob.d.ts +2 -2
- package/dist/tools/glob.js +2 -2
- package/dist/tools/grep.js +4 -6
- package/dist/tools/image-file.js +2 -1
- package/dist/tools/read-lines.d.ts +32 -0
- package/dist/tools/read-lines.js +147 -0
- package/dist/tools/read.d.ts +6 -2
- package/dist/tools/read.js +47 -27
- package/dist/tools/task.d.ts +9 -0
- package/dist/tools/task.js +21 -8
- package/dist/tools/truncate.d.ts +7 -0
- package/dist/tools/truncate.js +11 -0
- package/dist/tools/types.d.ts +13 -0
- package/dist/trace/build-index.js +13 -1
- package/dist/tui/ansi.d.ts +2 -0
- package/dist/tui/ansi.js +7 -3
- package/dist/tui/components/editor-buffer.js +2 -2
- package/dist/tui/components/editor.js +5 -6
- package/dist/tui/keys.js +2 -2
- package/dist/tui/stdin-buffer.js +3 -3
- package/docs/acp.md +5 -1
- package/docs/agents.md +42 -6
- package/docs/codemode.md +4 -2
- package/docs/en/acp.md +3 -2
- package/docs/en/host-api.md +1 -1
- package/docs/en/permissions.md +2 -2
- package/docs/en/providers.md +54 -20
- package/docs/en/rpc.md +23 -7
- package/docs/en/sessions.md +4 -2
- package/docs/en/tui.md +3 -3
- package/docs/permissions.md +4 -1
- package/docs/providers.md +85 -22
- package/docs/rpc.md +22 -6
- package/docs/session-format.md +3 -2
- package/docs/sessions.md +3 -1
- package/docs/tui.md +2 -2
- package/package.json +2 -2
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
* `responses[n]` 产出文本 / 思考 / 工具调用 / 429 / 溢出 / 断流 / 延迟。
|
|
5
5
|
*
|
|
6
6
|
* 事件形状与真实协议完全一致(同一个 BlockTracker),契约测试对两条协议与 fake 跑同一套断言。
|
|
7
|
+
* [ME-C] 工具参数按 JSON 串分块喂给 BlockTracker,所以消息同样带 `rawArguments`;start 之前的错误
|
|
8
|
+
* 以 `HttpError` 抛出(状态 ≥ 400 时),脚本的 `error.retryAfterMs` 进失败消息的 `retryAfterMs`。
|
|
7
9
|
*
|
|
8
10
|
* 用法:
|
|
9
11
|
* - 测试:`const fake = new FakeProvider([...])`,把 `fake.api` 注册进 ApiRegistry(或直接
|
|
10
12
|
* `fake.api.stream(...)`),`fake.calls` 记录每次调用的上下文与选项;
|
|
11
|
-
* - CLI:`--provider fake`;`AMA_FAKE_SCRIPT=<path.json>`
|
|
13
|
+
* - CLI:`--provider fake`;`AMA_FAKE_SCRIPT=<path.json>` 指定脚本(首次调用时读取)。缺省实例
|
|
14
|
+
* `keepCalls: false`:只计数、不留 `calls`(否则整个进程的转录都被它引用,内存测量多出几十 MB)。
|
|
12
15
|
* - 录制(第三波 §2.4,bundle 级缓存测试):`AMA_FAKE_RECORD=<file>` 时每次请求向文件追加一行
|
|
13
16
|
* JSON `{ index, purpose, model, system, tools, messagesCount }`——`system` 是折叠后的系统提示
|
|
14
17
|
* 原文,`tools` 是发给供应商的工具表(按注册顺序),与真实协议请求体的前缀同口径。
|
|
@@ -17,6 +20,7 @@ import { appendFileSync } from "node:fs";
|
|
|
17
20
|
import { AssistantEventStreamImpl } from "../event-stream.js";
|
|
18
21
|
import { BlockTracker, ProviderStopError, createOutput, finishDone, finishError, } from "../apis/shared.js";
|
|
19
22
|
import { contentText, normalizeContext } from "../context.js";
|
|
23
|
+
import { HttpError } from "../http.js";
|
|
20
24
|
import { describeFakeError, loadFakeScript, parseFakeScript, } from "./fake-script.js";
|
|
21
25
|
export const FAKE_PROVIDER_ID = "fake";
|
|
22
26
|
export const FAKE_API_ID = "fake";
|
|
@@ -100,6 +104,8 @@ export class FakeProvider {
|
|
|
100
104
|
scriptLoader;
|
|
101
105
|
loaded = false;
|
|
102
106
|
recordFile;
|
|
107
|
+
keepCalls;
|
|
108
|
+
count = 0;
|
|
103
109
|
constructor(script, options = {}) {
|
|
104
110
|
if (typeof script === "function")
|
|
105
111
|
this.scriptLoader = script;
|
|
@@ -107,13 +113,14 @@ export class FakeProvider {
|
|
|
107
113
|
this.setScript(script);
|
|
108
114
|
const record = options.recordFile;
|
|
109
115
|
this.recordFile = typeof record === "function" ? record : () => record;
|
|
116
|
+
this.keepCalls = options.keepCalls !== false;
|
|
110
117
|
}
|
|
111
118
|
api = {
|
|
112
119
|
id: FAKE_API_ID,
|
|
113
120
|
stream: (model, context, options) => this.stream(model, context, options),
|
|
114
121
|
};
|
|
115
122
|
get callCount() {
|
|
116
|
-
return this.
|
|
123
|
+
return this.count;
|
|
117
124
|
}
|
|
118
125
|
setScript(script) {
|
|
119
126
|
this.script = parseFakeScript(script);
|
|
@@ -126,6 +133,7 @@ export class FakeProvider {
|
|
|
126
133
|
}
|
|
127
134
|
reset() {
|
|
128
135
|
this.calls.length = 0;
|
|
136
|
+
this.count = 0;
|
|
129
137
|
}
|
|
130
138
|
ensureLoaded() {
|
|
131
139
|
if (this.loaded)
|
|
@@ -151,9 +159,11 @@ export class FakeProvider {
|
|
|
151
159
|
return undefined;
|
|
152
160
|
}
|
|
153
161
|
stream(model, context, options) {
|
|
154
|
-
const index = this.
|
|
155
|
-
|
|
156
|
-
|
|
162
|
+
const index = this.count++;
|
|
163
|
+
if (this.keepCalls) {
|
|
164
|
+
const { signal: _s, onPayload: _p, onResponse: _r, ...rest } = options;
|
|
165
|
+
this.calls.push({ index, model, context, options: rest });
|
|
166
|
+
}
|
|
157
167
|
this.record(index, model, context, options);
|
|
158
168
|
const response = this.responseFor(index) ?? {
|
|
159
169
|
text: lastUserText(context) || "(no input)",
|
|
@@ -198,7 +208,9 @@ export class FakeProvider {
|
|
|
198
208
|
const error = response.error ? describeFakeError(response.error) : undefined;
|
|
199
209
|
if (error && response.error?.kind !== "disconnect") {
|
|
200
210
|
options.onResponse?.(error.status, new Headers());
|
|
201
|
-
|
|
211
|
+
if (error.status < 400)
|
|
212
|
+
throw new Error(error.message);
|
|
213
|
+
throw new HttpError(error.status, error.message, "", response.error?.retryAfterMs);
|
|
202
214
|
}
|
|
203
215
|
options.onResponse?.(200, new Headers({ "content-type": "text/event-stream" }));
|
|
204
216
|
if (options.thinkingLevel)
|
|
@@ -278,5 +290,5 @@ export class FakeProvider {
|
|
|
278
290
|
export const defaultFakeProvider = new FakeProvider(() => {
|
|
279
291
|
const path = process.env[FAKE_SCRIPT_ENV];
|
|
280
292
|
return path ? loadFakeScript(path) : undefined;
|
|
281
|
-
}, { recordFile: () => process.env[FAKE_RECORD_ENV] || undefined });
|
|
293
|
+
}, { recordFile: () => process.env[FAKE_RECORD_ENV] || undefined, keepCalls: false });
|
|
282
294
|
export const fakeApi = defaultFakeProvider.api;
|
|
@@ -41,6 +41,8 @@ export interface FakeError {
|
|
|
41
41
|
kind: FakeErrorKind;
|
|
42
42
|
message?: string;
|
|
43
43
|
status?: number;
|
|
44
|
+
/** [ME-C] 模拟 `Retry-After`(毫秒):写进失败消息的 `retryAfterMs`。 */
|
|
45
|
+
retryAfterMs?: number;
|
|
44
46
|
}
|
|
45
47
|
export interface FakeResponse {
|
|
46
48
|
/** 依次产出的块;与 `text` 简写二选一(都给时 text 在后)。 */
|
package/dist/ai/http.d.ts
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* `http.setGlobalProxyFromEnv()`(等价于 `NODE_USE_ENV_PROXY=1`);SDK 嵌入方自行决定。
|
|
6
6
|
* 本模块不自行实现代理。
|
|
7
7
|
*
|
|
8
|
-
* 超时:`timeoutMs` 只管到拿到响应头为止;`idleTimeoutMs`(缺省 300 s,0
|
|
9
|
-
*
|
|
10
|
-
* `IdleTimeoutError`,文案含 `idle timeout
|
|
8
|
+
* 超时:`timeoutMs` 只管到拿到响应头为止;`idleTimeoutMs`(缺省 300 s,0 关闭)管等响应头;
|
|
9
|
+
* `streamIdleTimeoutMs`(缺省 180 s,0 关闭)管流式读取期间两次收到数据之间的间隔(每收到一块字节
|
|
10
|
+
* 即重新计时,见 sse.ts)。[ME-C] D18 空闲超时抛 `IdleTimeoutError`,文案含 `idle timeout`,会话层按
|
|
11
|
+
* 可重试错误处理。
|
|
11
12
|
*
|
|
12
13
|
* 协议层自身**不重试**(重试在会话层,§3.6)。
|
|
13
14
|
*/
|
|
@@ -33,15 +34,22 @@ export declare class HttpError extends Error {
|
|
|
33
34
|
export declare class RequestTimeoutError extends Error {
|
|
34
35
|
constructor(timeoutMs: number);
|
|
35
36
|
}
|
|
36
|
-
/**
|
|
37
|
+
/** 等响应头的空闲超时缺省值:与 `request.idleTimeoutMs` / `AMA_IDLE_TIMEOUT_MS` 的缺省一致。 */
|
|
37
38
|
export declare const DEFAULT_IDLE_TIMEOUT_MS = 300000;
|
|
38
|
-
/**
|
|
39
|
+
/** 流中两块数据之间的空闲超时缺省值:与 `request.streamIdleTimeoutMs` 的缺省一致。 */
|
|
40
|
+
export declare const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 180000;
|
|
41
|
+
/** 请求选项里等响应头的空闲超时;未给出用缺省值,0 或负数表示关闭(返回 undefined)。 */
|
|
39
42
|
export declare function idleTimeoutOf(options: {
|
|
40
43
|
idleTimeoutMs?: number | undefined;
|
|
41
44
|
}): number | undefined;
|
|
45
|
+
/** 请求选项里流中的空闲超时;未给出用缺省值,0 或负数表示关闭(返回 undefined)。 */
|
|
46
|
+
export declare function streamIdleTimeoutOf(options: {
|
|
47
|
+
streamIdleTimeoutMs?: number | undefined;
|
|
48
|
+
}): number | undefined;
|
|
42
49
|
/**
|
|
43
|
-
*
|
|
44
|
-
* `stream` =
|
|
50
|
+
* 服务端在空闲上限内没有任何字节:`response` = 发出请求后迟迟没有响应头(`idleTimeoutMs`),
|
|
51
|
+
* `stream` = 流读到一半停住(`streamIdleTimeoutMs`)。文案含 `idle timeout`(重试分类据此判为可重试),
|
|
52
|
+
* 并点名对应的配置键与环境变量。
|
|
45
53
|
*/
|
|
46
54
|
export declare class IdleTimeoutError extends Error {
|
|
47
55
|
readonly idleTimeoutMs: number;
|
|
@@ -60,7 +68,7 @@ export interface PostOptions {
|
|
|
60
68
|
body: unknown;
|
|
61
69
|
signal: AbortSignal;
|
|
62
70
|
timeoutMs?: number | undefined;
|
|
63
|
-
/** 等响应头的空闲上限(`timeoutMs` 未给时生效);undefined
|
|
71
|
+
/** 等响应头的空闲上限(`timeoutMs` 未给时生效);undefined 不限。流中的空闲见 `readSseEvents`。 */
|
|
64
72
|
idleTimeoutMs?: number | undefined;
|
|
65
73
|
onResponse?: ((status: number, headers: Headers) => void) | undefined;
|
|
66
74
|
}
|
|
@@ -68,7 +76,7 @@ export interface PostOptions {
|
|
|
68
76
|
* POST JSON 并返回 2xx 响应(body 是 SSE 字节流)。非 2xx → HttpError;超时 → RequestTimeoutError
|
|
69
77
|
* (`timeoutMs`)或 IdleTimeoutError(`idleTimeoutMs`);调用方的 signal 中止 → 原样抛 AbortError
|
|
70
78
|
* (调用方据 signal.aborted 判 aborted)。超时只覆盖到拿到响应头为止,流式读取期间的空闲由
|
|
71
|
-
* `readSseEvents`
|
|
79
|
+
* `readSseEvents` 的空闲上限(`streamIdleTimeoutOf`)控制。
|
|
72
80
|
*/
|
|
73
81
|
export declare function postJson(url: string, options: PostOptions): Promise<Response>;
|
|
74
82
|
/** undici 的 `fetch failed` 把真正原因藏在 cause 里;拼出来便于重试分类与排错。 */
|
package/dist/ai/http.js
CHANGED
|
@@ -5,13 +5,15 @@
|
|
|
5
5
|
* `http.setGlobalProxyFromEnv()`(等价于 `NODE_USE_ENV_PROXY=1`);SDK 嵌入方自行决定。
|
|
6
6
|
* 本模块不自行实现代理。
|
|
7
7
|
*
|
|
8
|
-
* 超时:`timeoutMs` 只管到拿到响应头为止;`idleTimeoutMs`(缺省 300 s,0
|
|
9
|
-
*
|
|
10
|
-
* `IdleTimeoutError`,文案含 `idle timeout
|
|
8
|
+
* 超时:`timeoutMs` 只管到拿到响应头为止;`idleTimeoutMs`(缺省 300 s,0 关闭)管等响应头;
|
|
9
|
+
* `streamIdleTimeoutMs`(缺省 180 s,0 关闭)管流式读取期间两次收到数据之间的间隔(每收到一块字节
|
|
10
|
+
* 即重新计时,见 sse.ts)。[ME-C] D18 空闲超时抛 `IdleTimeoutError`,文案含 `idle timeout`,会话层按
|
|
11
|
+
* 可重试错误处理。
|
|
11
12
|
*
|
|
12
13
|
* 协议层自身**不重试**(重试在会话层,§3.6)。
|
|
13
14
|
*/
|
|
14
15
|
import { AMA_VERSION } from "../version.js";
|
|
16
|
+
import { jsonFetchBody } from "./json-body.js";
|
|
15
17
|
/** 大小写不敏感地合并头;后者覆盖前者;值为 null 表示删除该头。键保留最后一次出现的写法。 */
|
|
16
18
|
export function mergeHeaders(...sources) {
|
|
17
19
|
const merged = new Map();
|
|
@@ -80,19 +82,26 @@ export class RequestTimeoutError extends Error {
|
|
|
80
82
|
this.name = "RequestTimeoutError";
|
|
81
83
|
}
|
|
82
84
|
}
|
|
83
|
-
/**
|
|
85
|
+
/** 等响应头的空闲超时缺省值:与 `request.idleTimeoutMs` / `AMA_IDLE_TIMEOUT_MS` 的缺省一致。 */
|
|
84
86
|
export const DEFAULT_IDLE_TIMEOUT_MS = 300_000;
|
|
85
|
-
/**
|
|
87
|
+
/** 流中两块数据之间的空闲超时缺省值:与 `request.streamIdleTimeoutMs` 的缺省一致。 */
|
|
88
|
+
export const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 180_000;
|
|
89
|
+
const positive = (value) => Number.isFinite(value) && value > 0 ? value : undefined;
|
|
90
|
+
/** 请求选项里等响应头的空闲超时;未给出用缺省值,0 或负数表示关闭(返回 undefined)。 */
|
|
86
91
|
export function idleTimeoutOf(options) {
|
|
87
|
-
|
|
88
|
-
|
|
92
|
+
return positive(options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS);
|
|
93
|
+
}
|
|
94
|
+
/** 请求选项里流中的空闲超时;未给出用缺省值,0 或负数表示关闭(返回 undefined)。 */
|
|
95
|
+
export function streamIdleTimeoutOf(options) {
|
|
96
|
+
return positive(options.streamIdleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS);
|
|
89
97
|
}
|
|
90
98
|
function formatDuration(ms) {
|
|
91
99
|
return ms >= 1000 && ms % 1000 === 0 ? `${ms / 1000} s` : `${ms} ms`;
|
|
92
100
|
}
|
|
93
101
|
/**
|
|
94
|
-
*
|
|
95
|
-
* `stream` =
|
|
102
|
+
* 服务端在空闲上限内没有任何字节:`response` = 发出请求后迟迟没有响应头(`idleTimeoutMs`),
|
|
103
|
+
* `stream` = 流读到一半停住(`streamIdleTimeoutMs`)。文案含 `idle timeout`(重试分类据此判为可重试),
|
|
104
|
+
* 并点名对应的配置键与环境变量。
|
|
96
105
|
*/
|
|
97
106
|
export class IdleTimeoutError extends Error {
|
|
98
107
|
idleTimeoutMs;
|
|
@@ -101,7 +110,10 @@ export class IdleTimeoutError extends Error {
|
|
|
101
110
|
const what = phase === "response"
|
|
102
111
|
? `No response from the server within ${formatDuration(idleTimeoutMs)}`
|
|
103
112
|
: `Stream stalled: no data from the server for ${formatDuration(idleTimeoutMs)}`;
|
|
104
|
-
|
|
113
|
+
const knob = phase === "response"
|
|
114
|
+
? "request.idleTimeoutMs or AMA_IDLE_TIMEOUT_MS"
|
|
115
|
+
: "request.streamIdleTimeoutMs or AMA_STREAM_IDLE_TIMEOUT_MS";
|
|
116
|
+
super(`${what} (idle timeout; adjust with ${knob})`);
|
|
105
117
|
this.name = "IdleTimeoutError";
|
|
106
118
|
this.idleTimeoutMs = idleTimeoutMs;
|
|
107
119
|
this.phase = phase;
|
|
@@ -171,11 +183,17 @@ function parseRetryAfter(headers) {
|
|
|
171
183
|
const date = Date.parse(value);
|
|
172
184
|
return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());
|
|
173
185
|
}
|
|
186
|
+
/** 流式请求体须带 `content-length`(不走 chunked,部分中转不收);已有同名头时以算出的字节数为准。 */
|
|
187
|
+
function withContentLength(headers, length) {
|
|
188
|
+
return length === undefined
|
|
189
|
+
? headers
|
|
190
|
+
: mergeHeaders(headers, { "content-length": String(length) });
|
|
191
|
+
}
|
|
174
192
|
/**
|
|
175
193
|
* POST JSON 并返回 2xx 响应(body 是 SSE 字节流)。非 2xx → HttpError;超时 → RequestTimeoutError
|
|
176
194
|
* (`timeoutMs`)或 IdleTimeoutError(`idleTimeoutMs`);调用方的 signal 中止 → 原样抛 AbortError
|
|
177
195
|
* (调用方据 signal.aborted 判 aborted)。超时只覆盖到拿到响应头为止,流式读取期间的空闲由
|
|
178
|
-
* `readSseEvents`
|
|
196
|
+
* `readSseEvents` 的空闲上限(`streamIdleTimeoutOf`)控制。
|
|
179
197
|
*/
|
|
180
198
|
export async function postJson(url, options) {
|
|
181
199
|
const explicit = options.timeoutMs !== undefined && options.timeoutMs > 0;
|
|
@@ -185,10 +203,12 @@ export async function postJson(url, options) {
|
|
|
185
203
|
const signal = AbortSignal.any([options.signal, timer.signal]);
|
|
186
204
|
let response;
|
|
187
205
|
try {
|
|
206
|
+
const payload = jsonFetchBody(options.body);
|
|
188
207
|
response = await fetch(url, {
|
|
189
208
|
method: "POST",
|
|
190
|
-
headers: options.headers,
|
|
191
|
-
body:
|
|
209
|
+
headers: withContentLength(options.headers, payload.contentLength),
|
|
210
|
+
body: payload.body,
|
|
211
|
+
...(payload.contentLength === undefined ? {} : { duplex: "half" }),
|
|
192
212
|
signal,
|
|
193
213
|
});
|
|
194
214
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 图片 base64 按内容哈希驻留(docs/memory-plan.md D5、§2.5)。
|
|
3
|
+
*
|
|
4
|
+
* `WeakRef` 的目标不能是字符串,所以驻留单元是 `ImageBlock`:表为 `Map<键, WeakRef<ImageBlock>>`,
|
|
5
|
+
* 键 = `mimeType` + base64 字符串的 sha256;`FinalizationRegistry` 在 block 被回收后清键。命中时返回
|
|
6
|
+
* 已驻留的 block,调用方把它放进内容数组,其中的 base64 字符串随之共享;未命中登记并返回入参。
|
|
7
|
+
* 驻留 block 只在仍被某条消息引用时存活,表本身不增加常驻。block 当作不可变值:没有任何代码就地改它。
|
|
8
|
+
*
|
|
9
|
+
* 哈希一律对 base64 字符串计算(base64 与原始字节一一对应),`read` / 附图与会话恢复的图片才能互相命中。
|
|
10
|
+
*/
|
|
11
|
+
import type { ImageBlock } from "./types.js";
|
|
12
|
+
/**
|
|
13
|
+
* 会话条目的最小形状:ai 不 import session(docs/design.md §1.1),`SessionEntry[]` 可直接传入。
|
|
14
|
+
* 只有 `type === "message"` 且 `message.content` 为数组的条目里的 `image` 块参与驻留。
|
|
15
|
+
*/
|
|
16
|
+
export interface ImageCarrier {
|
|
17
|
+
readonly type: string;
|
|
18
|
+
readonly message?: object;
|
|
19
|
+
}
|
|
20
|
+
/** 内容哈希(小写 hex);字符串按 UTF-8 计算。 */
|
|
21
|
+
export declare function sha256Hex(data: Buffer | string): string;
|
|
22
|
+
/**
|
|
23
|
+
* 返回与 `block` 内容相同的驻留 block;`hash` 是 `block.data`(base64 字符串)的 sha256,缺省时现算。
|
|
24
|
+
*/
|
|
25
|
+
export declare function internImage(block: ImageBlock, hash?: string): ImageBlock;
|
|
26
|
+
/** 就地把会话条目里的图片 block 换成驻留 block,返回替换数(已是驻留对象的不计)。 */
|
|
27
|
+
export declare function internSessionImages(entries: readonly ImageCarrier[]): number;
|
|
28
|
+
/** 驻留表当前的键数(测试与诊断用;包括目标已回收、清理回调尚未运行的键)。 */
|
|
29
|
+
export declare function internedImageCount(): number;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 图片 base64 按内容哈希驻留(docs/memory-plan.md D5、§2.5)。
|
|
3
|
+
*
|
|
4
|
+
* `WeakRef` 的目标不能是字符串,所以驻留单元是 `ImageBlock`:表为 `Map<键, WeakRef<ImageBlock>>`,
|
|
5
|
+
* 键 = `mimeType` + base64 字符串的 sha256;`FinalizationRegistry` 在 block 被回收后清键。命中时返回
|
|
6
|
+
* 已驻留的 block,调用方把它放进内容数组,其中的 base64 字符串随之共享;未命中登记并返回入参。
|
|
7
|
+
* 驻留 block 只在仍被某条消息引用时存活,表本身不增加常驻。block 当作不可变值:没有任何代码就地改它。
|
|
8
|
+
*
|
|
9
|
+
* 哈希一律对 base64 字符串计算(base64 与原始字节一一对应),`read` / 附图与会话恢复的图片才能互相命中。
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
/** 内容哈希(小写 hex);字符串按 UTF-8 计算。 */
|
|
13
|
+
export function sha256Hex(data) {
|
|
14
|
+
return createHash("sha256").update(data).digest("hex");
|
|
15
|
+
}
|
|
16
|
+
const table = new Map();
|
|
17
|
+
const registry = new FinalizationRegistry((key) => {
|
|
18
|
+
// 同一键可能已换成新 block:只在旧引用确已失效时删
|
|
19
|
+
if (table.get(key)?.deref() === undefined)
|
|
20
|
+
table.delete(key);
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* 返回与 `block` 内容相同的驻留 block;`hash` 是 `block.data`(base64 字符串)的 sha256,缺省时现算。
|
|
24
|
+
*/
|
|
25
|
+
export function internImage(block, hash) {
|
|
26
|
+
const key = `${block.mimeType}:${hash ?? sha256Hex(block.data)}`;
|
|
27
|
+
const existing = table.get(key)?.deref();
|
|
28
|
+
if (existing !== undefined && existing.data.length === block.data.length)
|
|
29
|
+
return existing;
|
|
30
|
+
table.set(key, new WeakRef(block));
|
|
31
|
+
registry.register(block, key);
|
|
32
|
+
return block;
|
|
33
|
+
}
|
|
34
|
+
/** 就地把会话条目里的图片 block 换成驻留 block,返回替换数(已是驻留对象的不计)。 */
|
|
35
|
+
export function internSessionImages(entries) {
|
|
36
|
+
let replaced = 0;
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
const message = entry.type === "message" ? entry.message : undefined;
|
|
39
|
+
const content = message?.content;
|
|
40
|
+
if (!Array.isArray(content))
|
|
41
|
+
continue;
|
|
42
|
+
for (let i = 0; i < content.length; i++) {
|
|
43
|
+
const block = content[i];
|
|
44
|
+
if (block?.type !== "image" || typeof block.data !== "string")
|
|
45
|
+
continue;
|
|
46
|
+
const interned = internImage(block);
|
|
47
|
+
if (interned !== block) {
|
|
48
|
+
content[i] = interned;
|
|
49
|
+
replaced++;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return replaced;
|
|
54
|
+
}
|
|
55
|
+
/** 驻留表当前的键数(测试与诊断用;包括目标已回收、清理回调尚未运行的键)。 */
|
|
56
|
+
export function internedImageCount() {
|
|
57
|
+
return table.size;
|
|
58
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 请求体序列化(docs/memory-plan.md D3、§2.3)。
|
|
3
|
+
*
|
|
4
|
+
* 带图请求体里 base64 占绝大部分字节;`JSON.stringify(body)` 会先拼出整份中间字符串,再由 fetch
|
|
5
|
+
* 编码成 UTF-8。这里先廉价扫一遍:没有 ≥ {@link LARGE_STRING_BYTES} 的字符串就照旧走原生;有就只沿
|
|
6
|
+
* 「普通对象 / 数组」结构拼片段——小子树仍交给原生 `JSON.stringify`,大字符串经检查无需转义时原样
|
|
7
|
+
* 作为一个片段。片段两种用法:`serializeJsonBody` 一次分配结果 Buffer 依次写入;`jsonFetchBody`
|
|
8
|
+
* (`postJson` 用)逐片段编码成流、声明总字节数,不在内存里留整份请求体。
|
|
9
|
+
*
|
|
10
|
+
* 结果与 `Buffer.from(JSON.stringify(body), "utf8")` 逐字节相同:自写部分只有结构(`{`、`,`、
|
|
11
|
+
* 键、`[`)与已验证无需转义的大字符串(需要转义的大字符串整个交给原生);`toJSON`、`undefined` /
|
|
12
|
+
* 函数 / symbol 的跳过与 `null`、数字格式、转义全部由原生处理(带 `toJSON` 的值经单键容器序列化,
|
|
13
|
+
* 保证 `toJSON(key)` 收到与原生相同的键)。
|
|
14
|
+
*/
|
|
15
|
+
/** 达到这个长度(UTF-16 码元)的字符串才走片段路径。 */
|
|
16
|
+
export declare const LARGE_STRING_BYTES: number;
|
|
17
|
+
/** 与 `Buffer.from(JSON.stringify(body), "utf8")` 逐字节相同;大字符串不经中间字符串。 */
|
|
18
|
+
export declare function serializeJsonBody(body: unknown): Buffer;
|
|
19
|
+
/** `postJson` 交给 fetch 的请求体。 */
|
|
20
|
+
export interface JsonFetchBody {
|
|
21
|
+
body: string | ReadableStream<Uint8Array>;
|
|
22
|
+
/** 流式请求体的字节数:调用方须作为 `content-length` 头发出(不走 chunked)。 */
|
|
23
|
+
contentLength?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 给 fetch 的请求体。不含大字符串时就是 `JSON.stringify(body)`(与以前完全相同);含大字符串时是
|
|
27
|
+
* 逐片段编码的流,并给出总字节数。不交给 fetch 一个 Buffer:Node 的 fetch 会把 BufferSource
|
|
28
|
+
* 请求体再复制两份(实测 mock 300 步带图峰值反而从约 1.0 GB 升到 1.85 GB),流则按片段边读边发,
|
|
29
|
+
* 同一时刻只多出一个片段的 UTF-8 字节。字节序列与 `serializeJsonBody` 相同。
|
|
30
|
+
*/
|
|
31
|
+
export declare function jsonFetchBody(body: unknown): JsonFetchBody;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 请求体序列化(docs/memory-plan.md D3、§2.3)。
|
|
3
|
+
*
|
|
4
|
+
* 带图请求体里 base64 占绝大部分字节;`JSON.stringify(body)` 会先拼出整份中间字符串,再由 fetch
|
|
5
|
+
* 编码成 UTF-8。这里先廉价扫一遍:没有 ≥ {@link LARGE_STRING_BYTES} 的字符串就照旧走原生;有就只沿
|
|
6
|
+
* 「普通对象 / 数组」结构拼片段——小子树仍交给原生 `JSON.stringify`,大字符串经检查无需转义时原样
|
|
7
|
+
* 作为一个片段。片段两种用法:`serializeJsonBody` 一次分配结果 Buffer 依次写入;`jsonFetchBody`
|
|
8
|
+
* (`postJson` 用)逐片段编码成流、声明总字节数,不在内存里留整份请求体。
|
|
9
|
+
*
|
|
10
|
+
* 结果与 `Buffer.from(JSON.stringify(body), "utf8")` 逐字节相同:自写部分只有结构(`{`、`,`、
|
|
11
|
+
* 键、`[`)与已验证无需转义的大字符串(需要转义的大字符串整个交给原生);`toJSON`、`undefined` /
|
|
12
|
+
* 函数 / symbol 的跳过与 `null`、数字格式、转义全部由原生处理(带 `toJSON` 的值经单键容器序列化,
|
|
13
|
+
* 保证 `toJSON(key)` 收到与原生相同的键)。
|
|
14
|
+
*/
|
|
15
|
+
/** 达到这个长度(UTF-16 码元)的字符串才走片段路径。 */
|
|
16
|
+
export const LARGE_STRING_BYTES = 64 * 1024;
|
|
17
|
+
/** 检查是否需要转义时每次交给原生的长度:结果是新生代里的小字符串,随即可回收。 */
|
|
18
|
+
const CHECK_CHUNK = 16 * 1024;
|
|
19
|
+
/**
|
|
20
|
+
* 大字符串无需转义、可原样写入:逐段让原生 `JSON.stringify` 判断(长度恰好多出两个引号即没有
|
|
21
|
+
* 任何转义)。引号、反斜杠、控制字符、孤立代理项都会让长度变化;成对代理项若恰被切在段界上
|
|
22
|
+
* 也会被判为需要转义——只会多走原生,不会错。实测比同义的正则快一倍多(原生的转义扫描是向量化的)。
|
|
23
|
+
* 每个大字符串每次序列化只查这一次。
|
|
24
|
+
*/
|
|
25
|
+
function isPlain(value) {
|
|
26
|
+
for (let i = 0; i < value.length; i += CHECK_CHUNK) {
|
|
27
|
+
const chunk = value.slice(i, i + CHECK_CHUNK);
|
|
28
|
+
if (JSON.stringify(chunk).length !== chunk.length + 2)
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
/** 结构遍历的深度上限:更深(或有环)时整棵子树交给原生,环由原生照常抛 TypeError。 */
|
|
34
|
+
const MAX_DEPTH = 64;
|
|
35
|
+
const isLargeString = (value) => typeof value === "string" && value.length >= LARGE_STRING_BYTES;
|
|
36
|
+
/** 只遍历原型为 Object.prototype / null 的普通对象和数组,且自身与原型链上都没有 `toJSON`。 */
|
|
37
|
+
function isContainer(value) {
|
|
38
|
+
if (typeof value !== "object" || value === null)
|
|
39
|
+
return false;
|
|
40
|
+
if (typeof value.toJSON === "function")
|
|
41
|
+
return false;
|
|
42
|
+
if (Array.isArray(value))
|
|
43
|
+
return true;
|
|
44
|
+
const proto = Object.getPrototypeOf(value);
|
|
45
|
+
return proto === Object.prototype || proto === null;
|
|
46
|
+
}
|
|
47
|
+
/** 收集含大字符串的容器(片段路径只深入这些容器);返回 `value` 本身是否含大字符串。 */
|
|
48
|
+
function collectLarge(value, marked, depth) {
|
|
49
|
+
if (isLargeString(value))
|
|
50
|
+
return true;
|
|
51
|
+
if (depth >= MAX_DEPTH || !isContainer(value))
|
|
52
|
+
return false;
|
|
53
|
+
let found = false;
|
|
54
|
+
if (Array.isArray(value)) {
|
|
55
|
+
for (let i = 0; i < value.length; i++) {
|
|
56
|
+
if (collectLarge(value[i], marked, depth + 1))
|
|
57
|
+
found = true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
for (const key of Object.keys(value)) {
|
|
62
|
+
if (collectLarge(value[key], marked, depth + 1))
|
|
63
|
+
found = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (found)
|
|
67
|
+
marked.add(value);
|
|
68
|
+
return found;
|
|
69
|
+
}
|
|
70
|
+
/** 原生序列化 `holder[key] = value` 这一项的值;被原生跳过(undefined / 函数 / symbol)时 undefined。 */
|
|
71
|
+
function nativeValue(key, value) {
|
|
72
|
+
if (typeof value !== "object" && typeof value !== "function" && typeof value !== "bigint") {
|
|
73
|
+
return JSON.stringify(value);
|
|
74
|
+
}
|
|
75
|
+
// 对象、函数与 bigint 可能有 toJSON(key):放进单键容器,让原生以同一个键调用
|
|
76
|
+
const wrapped = JSON.stringify({ [key]: value });
|
|
77
|
+
if (wrapped === "{}")
|
|
78
|
+
return undefined;
|
|
79
|
+
return wrapped.slice(JSON.stringify(key).length + 2, -1);
|
|
80
|
+
}
|
|
81
|
+
/** 片段:已拼好的 JSON 文本与大字符串交替;连续的小文本先在字符串里累积。 */
|
|
82
|
+
class Pieces {
|
|
83
|
+
parts = [];
|
|
84
|
+
pending = "";
|
|
85
|
+
text(chunk) {
|
|
86
|
+
this.pending += chunk;
|
|
87
|
+
}
|
|
88
|
+
/** 大字符串原样写入(引号进相邻的小片段)。 */
|
|
89
|
+
large(value) {
|
|
90
|
+
this.parts.push(`${this.pending}"`, value);
|
|
91
|
+
this.pending = '"';
|
|
92
|
+
}
|
|
93
|
+
finish() {
|
|
94
|
+
if (this.pending.length > 0)
|
|
95
|
+
this.parts.push(this.pending);
|
|
96
|
+
this.pending = "";
|
|
97
|
+
return this.parts;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** 写一项的值;返回是否写了(对象里被原生跳过的属性不写)。数组里跳过的值写 `null`。 */
|
|
101
|
+
function writeEntry(key, value, marked, out, prefix, depth) {
|
|
102
|
+
if (isLargeString(value)) {
|
|
103
|
+
if (isPlain(value)) {
|
|
104
|
+
out.text(prefix);
|
|
105
|
+
out.large(value);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
out.text(prefix + JSON.stringify(value)); // 需要转义:只有这一个字符串交给原生
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
// 深度与 collectLarge 一致;有环时越过上限交给原生,由它抛 TypeError
|
|
113
|
+
if (depth < MAX_DEPTH && isContainer(value) && marked.has(value)) {
|
|
114
|
+
out.text(prefix);
|
|
115
|
+
writeContainer(value, marked, out, depth);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
const text = nativeValue(key, value);
|
|
119
|
+
if (text === undefined)
|
|
120
|
+
return false;
|
|
121
|
+
out.text(prefix + text);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
function writeContainer(value, marked, out, depth) {
|
|
125
|
+
if (Array.isArray(value)) {
|
|
126
|
+
out.text("[");
|
|
127
|
+
for (let i = 0; i < value.length; i++) {
|
|
128
|
+
const prefix = i > 0 ? "," : "";
|
|
129
|
+
if (!writeEntry(String(i), value[i], marked, out, prefix, depth + 1))
|
|
130
|
+
out.text(`${prefix}null`);
|
|
131
|
+
}
|
|
132
|
+
out.text("]");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
out.text("{");
|
|
136
|
+
let first = true;
|
|
137
|
+
for (const key of Object.keys(value)) {
|
|
138
|
+
const prefix = `${first ? "" : ","}${JSON.stringify(key)}:`;
|
|
139
|
+
if (writeEntry(key, value[key], marked, out, prefix, depth + 1))
|
|
140
|
+
first = false;
|
|
141
|
+
}
|
|
142
|
+
out.text("}");
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 含大字符串时按顺序给出各片段(拼起来就是 `JSON.stringify(body)`)与 UTF-8 总字节数;
|
|
146
|
+
* 不含时返回 undefined(调用方照旧走原生)。
|
|
147
|
+
*/
|
|
148
|
+
function jsonParts(body) {
|
|
149
|
+
const marked = new Set();
|
|
150
|
+
if (!collectLarge(body, marked, 0))
|
|
151
|
+
return undefined;
|
|
152
|
+
const out = new Pieces();
|
|
153
|
+
if (isLargeString(body)) {
|
|
154
|
+
if (!isPlain(body))
|
|
155
|
+
return undefined;
|
|
156
|
+
out.large(body);
|
|
157
|
+
}
|
|
158
|
+
else
|
|
159
|
+
writeContainer(body, marked, out, 0);
|
|
160
|
+
const parts = out.finish();
|
|
161
|
+
let length = 0;
|
|
162
|
+
for (const part of parts)
|
|
163
|
+
length += Buffer.byteLength(part, "utf8");
|
|
164
|
+
return { parts, length };
|
|
165
|
+
}
|
|
166
|
+
/** 与 `Buffer.from(JSON.stringify(body), "utf8")` 逐字节相同;大字符串不经中间字符串。 */
|
|
167
|
+
export function serializeJsonBody(body) {
|
|
168
|
+
const split = jsonParts(body);
|
|
169
|
+
if (split === undefined)
|
|
170
|
+
return Buffer.from(JSON.stringify(body), "utf8");
|
|
171
|
+
// 一次分配、依次写入:不再有整份中间字符串,也不需要 Buffer.concat 的第二份拷贝
|
|
172
|
+
const buf = Buffer.allocUnsafe(split.length);
|
|
173
|
+
let offset = 0;
|
|
174
|
+
for (const part of split.parts)
|
|
175
|
+
offset += buf.write(part, offset, "utf8");
|
|
176
|
+
if (offset !== split.length) {
|
|
177
|
+
throw new Error(`serializeJsonBody: wrote ${offset} of ${split.length} bytes`);
|
|
178
|
+
}
|
|
179
|
+
return buf;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* 给 fetch 的请求体。不含大字符串时就是 `JSON.stringify(body)`(与以前完全相同);含大字符串时是
|
|
183
|
+
* 逐片段编码的流,并给出总字节数。不交给 fetch 一个 Buffer:Node 的 fetch 会把 BufferSource
|
|
184
|
+
* 请求体再复制两份(实测 mock 300 步带图峰值反而从约 1.0 GB 升到 1.85 GB),流则按片段边读边发,
|
|
185
|
+
* 同一时刻只多出一个片段的 UTF-8 字节。字节序列与 `serializeJsonBody` 相同。
|
|
186
|
+
*/
|
|
187
|
+
export function jsonFetchBody(body) {
|
|
188
|
+
const split = jsonParts(body);
|
|
189
|
+
if (split === undefined)
|
|
190
|
+
return { body: JSON.stringify(body) };
|
|
191
|
+
const { parts } = split;
|
|
192
|
+
let next = 0;
|
|
193
|
+
const stream = new ReadableStream({
|
|
194
|
+
pull(controller) {
|
|
195
|
+
const part = parts[next];
|
|
196
|
+
if (part === undefined) {
|
|
197
|
+
controller.close();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
parts[next++] = ""; // 发出即放手,片段(拼出来的小文本)可尽早回收
|
|
201
|
+
controller.enqueue(Buffer.from(part, "utf8"));
|
|
202
|
+
},
|
|
203
|
+
cancel() {
|
|
204
|
+
parts.length = 0;
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
return { body: stream, contentLength: split.length };
|
|
208
|
+
}
|
package/dist/ai/overflow.js
CHANGED
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
export const OVERFLOW_PATTERNS = [
|
|
14
14
|
[/prompt is too long/i, "Anthropic: prompt is too long: N tokens > M maximum"],
|
|
15
15
|
[/request_too_large/i, "Anthropic: 413 request_too_large"],
|
|
16
|
+
// [ME-C] 只有 max-tokens.ts 判定「可用输出 < 1024、不能收紧重发」时这条错误才会冒到会话层
|
|
17
|
+
[
|
|
18
|
+
/input length and `?max_tokens`? exceed context limit/i,
|
|
19
|
+
"Anthropic: input length and max_tokens exceed context limit: X + Y > Z",
|
|
20
|
+
],
|
|
16
21
|
[/exceeds the context window/i, "OpenAI: Your input exceeds the context window of this model"],
|
|
17
22
|
[/maximum context length is \d+ tokens/i, "OpenAI / OpenRouter / DeepSeek"],
|
|
18
23
|
[
|