@jsonstudio/llms 0.6.633 → 0.6.743
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/dist/conversion/codecs/anthropic-openai-codec.js +0 -5
- package/dist/conversion/codecs/openai-openai-codec.js +0 -6
- package/dist/conversion/codecs/responses-openai-codec.js +1 -7
- package/dist/conversion/hub/node-support.js +5 -4
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +14 -1
- package/dist/conversion/hub/pipeline/hub-pipeline.js +82 -18
- package/dist/conversion/hub/pipeline/session-identifiers.js +132 -2
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +23 -19
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +47 -0
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +4 -2
- package/dist/conversion/hub/process/chat-process.js +2 -0
- package/dist/conversion/hub/response/provider-response.js +6 -1
- package/dist/conversion/hub/snapshot-recorder.js +8 -1
- package/dist/conversion/pipeline/codecs/v2/shared/openai-chat-helpers.js +0 -7
- package/dist/conversion/responses/responses-openai-bridge.js +47 -7
- package/dist/conversion/shared/compaction-detect.d.ts +2 -0
- package/dist/conversion/shared/compaction-detect.js +53 -0
- package/dist/conversion/shared/errors.d.ts +1 -1
- package/dist/conversion/shared/reasoning-tool-normalizer.js +7 -0
- package/dist/conversion/shared/snapshot-hooks.d.ts +2 -0
- package/dist/conversion/shared/snapshot-hooks.js +180 -4
- package/dist/conversion/shared/snapshot-utils.d.ts +4 -0
- package/dist/conversion/shared/snapshot-utils.js +4 -0
- package/dist/conversion/shared/tool-filter-pipeline.js +3 -9
- package/dist/conversion/shared/tool-governor.d.ts +2 -0
- package/dist/conversion/shared/tool-governor.js +101 -13
- package/dist/conversion/shared/tool-harvester.js +42 -2
- package/dist/filters/index.d.ts +0 -2
- package/dist/filters/index.js +0 -2
- package/dist/filters/special/request-tools-normalize.d.ts +11 -0
- package/dist/filters/special/request-tools-normalize.js +13 -50
- package/dist/filters/special/response-apply-patch-toon-decode.js +403 -82
- package/dist/filters/special/response-tool-arguments-toon-decode.js +6 -75
- package/dist/filters/utils/snapshot-writer.js +42 -4
- package/dist/guidance/index.js +8 -2
- package/dist/router/virtual-router/engine-health.js +0 -4
- package/dist/router/virtual-router/engine-selection.d.ts +2 -1
- package/dist/router/virtual-router/engine-selection.js +101 -9
- package/dist/router/virtual-router/engine.d.ts +5 -1
- package/dist/router/virtual-router/engine.js +188 -5
- package/dist/router/virtual-router/routing-instructions.d.ts +6 -0
- package/dist/router/virtual-router/routing-instructions.js +18 -3
- package/dist/router/virtual-router/sticky-session-store.d.ts +1 -0
- package/dist/router/virtual-router/sticky-session-store.js +36 -0
- package/dist/router/virtual-router/types.d.ts +22 -0
- package/dist/servertool/engine.js +335 -9
- package/dist/servertool/handlers/compaction-detect.d.ts +1 -0
- package/dist/servertool/handlers/compaction-detect.js +1 -0
- package/dist/servertool/handlers/gemini-empty-reply-continue.js +29 -5
- package/dist/servertool/handlers/iflow-model-error-retry.js +17 -0
- package/dist/servertool/handlers/stop-message-auto.js +199 -19
- package/dist/servertool/server-side-tools.d.ts +0 -1
- package/dist/servertool/server-side-tools.js +0 -1
- package/dist/servertool/types.d.ts +1 -0
- package/dist/tools/apply-patch-structured.js +52 -15
- package/dist/tools/tool-registry.js +537 -15
- package/dist/utils/toon.d.ts +4 -0
- package/dist/utils/toon.js +75 -0
- package/package.json +4 -2
- package/dist/test-output/virtual-router/results.json +0 -1
- package/dist/test-output/virtual-router/summary.json +0 -12
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { registerServerToolHandler } from '../registry.js';
|
|
2
2
|
import { cloneJson } from '../server-side-tools.js';
|
|
3
3
|
import { loadRoutingInstructionStateSync, saveRoutingInstructionStateAsync } from '../../router/virtual-router/sticky-session-store.js';
|
|
4
|
+
import { isCompactionRequest } from './compaction-detect.js';
|
|
4
5
|
const STOPMESSAGE_DEBUG = (process.env.ROUTECODEX_STOPMESSAGE_DEBUG || '').trim() === '1';
|
|
5
6
|
function debugLog(message, extra) {
|
|
6
7
|
if (!STOPMESSAGE_DEBUG) {
|
|
@@ -23,9 +24,14 @@ const handler = async (ctx) => {
|
|
|
23
24
|
requestId: record.requestId,
|
|
24
25
|
providerProtocol: record.providerProtocol
|
|
25
26
|
});
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
28
|
-
|
|
27
|
+
const followupFlagRaw = record.serverToolFollowup;
|
|
28
|
+
if (followupFlagRaw === true ||
|
|
29
|
+
(typeof followupFlagRaw === 'string' && followupFlagRaw.trim().toLowerCase() === 'true')) {
|
|
30
|
+
debugLog('skip_followup_loop');
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
if (hasCompactionFlag(record)) {
|
|
34
|
+
debugLog('skip_compaction_flag');
|
|
29
35
|
return null;
|
|
30
36
|
}
|
|
31
37
|
const connectionState = resolveClientConnectionState(record.clientConnectionState);
|
|
@@ -44,10 +50,21 @@ const handler = async (ctx) => {
|
|
|
44
50
|
debugLog('skip_no_sticky_key');
|
|
45
51
|
return null;
|
|
46
52
|
}
|
|
47
|
-
|
|
53
|
+
let state = loadRoutingInstructionStateSync(stickyKey);
|
|
48
54
|
if (!state || !state.stopMessageText || !state.stopMessageMaxRepeats) {
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
const fallback = resolveStopMessageSnapshot(record.stopMessageState);
|
|
56
|
+
if (fallback) {
|
|
57
|
+
state = createStopMessageState(fallback);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const implicit = resolveImplicitGeminiStopMessageSnapshot(ctx, record);
|
|
61
|
+
if (!implicit) {
|
|
62
|
+
debugLog('skip_no_state', { stickyKey });
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
state = createStopMessageState(implicit);
|
|
66
|
+
}
|
|
67
|
+
saveRoutingInstructionStateAsync(stickyKey, state);
|
|
51
68
|
}
|
|
52
69
|
const text = typeof state.stopMessageText === 'string' ? state.stopMessageText.trim() : '';
|
|
53
70
|
const maxRepeats = typeof state.stopMessageMaxRepeats === 'number' && Number.isFinite(state.stopMessageMaxRepeats)
|
|
@@ -70,6 +87,12 @@ const handler = async (ctx) => {
|
|
|
70
87
|
used,
|
|
71
88
|
maxRepeats
|
|
72
89
|
});
|
|
90
|
+
state.stopMessageText = undefined;
|
|
91
|
+
state.stopMessageMaxRepeats = undefined;
|
|
92
|
+
state.stopMessageUsed = undefined;
|
|
93
|
+
state.stopMessageUpdatedAt = undefined;
|
|
94
|
+
state.stopMessageLastUsedAt = undefined;
|
|
95
|
+
saveRoutingInstructionStateAsync(stickyKey, state);
|
|
73
96
|
return null;
|
|
74
97
|
}
|
|
75
98
|
if (!isStopFinishReason(ctx.base)) {
|
|
@@ -85,16 +108,33 @@ const handler = async (ctx) => {
|
|
|
85
108
|
});
|
|
86
109
|
return null;
|
|
87
110
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
111
|
+
if (isCompactionRequest(captured)) {
|
|
112
|
+
debugLog('skip_compaction_request', { stickyKey });
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const entryEndpoint = resolveEntryEndpoint(record);
|
|
116
|
+
const followupPayload = buildStopMessageFollowupPayload(captured, text, ctx.base);
|
|
92
117
|
if (!followupPayload) {
|
|
93
118
|
debugLog('skip_failed_build_followup', {
|
|
94
119
|
stickyKey
|
|
95
120
|
});
|
|
96
121
|
return null;
|
|
97
122
|
}
|
|
123
|
+
const nextUsed = used + 1;
|
|
124
|
+
state.stopMessageUsed = nextUsed;
|
|
125
|
+
state.stopMessageLastUsedAt = Date.now();
|
|
126
|
+
saveRoutingInstructionStateAsync(stickyKey, state);
|
|
127
|
+
const followupMetadata = {
|
|
128
|
+
serverToolFollowup: true,
|
|
129
|
+
stream: false,
|
|
130
|
+
preserveRouteHint: false,
|
|
131
|
+
disableStickyRoutes: true,
|
|
132
|
+
serverToolOriginalEntryEndpoint: entryEndpoint,
|
|
133
|
+
...(connectionState ? { clientConnectionState: connectionState } : {})
|
|
134
|
+
};
|
|
135
|
+
if (shouldForceChatProtocol(entryEndpoint)) {
|
|
136
|
+
followupMetadata.serverToolFollowupProtocol = 'openai-chat';
|
|
137
|
+
}
|
|
98
138
|
return {
|
|
99
139
|
chatResponse: ctx.base,
|
|
100
140
|
execution: {
|
|
@@ -102,13 +142,8 @@ const handler = async (ctx) => {
|
|
|
102
142
|
followup: {
|
|
103
143
|
requestIdSuffix: ':stop_followup',
|
|
104
144
|
payload: followupPayload,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
stream: false,
|
|
108
|
-
preserveRouteHint: false,
|
|
109
|
-
disableStickyRoutes: true,
|
|
110
|
-
...(connectionState ? { clientConnectionState: connectionState } : {})
|
|
111
|
-
}
|
|
145
|
+
entryEndpoint,
|
|
146
|
+
metadata: followupMetadata
|
|
112
147
|
}
|
|
113
148
|
}
|
|
114
149
|
};
|
|
@@ -144,7 +179,14 @@ function isStopFinishReason(base) {
|
|
|
144
179
|
const finishReason = typeof finishReasonRaw === 'string' && finishReasonRaw.trim()
|
|
145
180
|
? finishReasonRaw.trim().toLowerCase()
|
|
146
181
|
: '';
|
|
147
|
-
|
|
182
|
+
// 将模型视为“自然结束”的场景:
|
|
183
|
+
// - OpenAI 兼容:finish_reason === 'stop'
|
|
184
|
+
// - 截断场景:finish_reason === 'length'(例如 Gemini/Claude 流式输出被 max token 截断)
|
|
185
|
+
// 统一视作可触发 stopMessage 的终止点;仅排除显式的 tool_calls。
|
|
186
|
+
if (!finishReason || finishReason === 'tool_calls') {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
if (finishReason !== 'stop' && finishReason !== 'length') {
|
|
148
190
|
return false;
|
|
149
191
|
}
|
|
150
192
|
const message = first.message &&
|
|
@@ -157,6 +199,7 @@ function isStopFinishReason(base) {
|
|
|
157
199
|
}
|
|
158
200
|
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
159
201
|
if (toolCalls.length > 0) {
|
|
202
|
+
// 如果当前响应仍在发起工具调用,则由工具执行驱动后续轮次,不触发 stopMessage。
|
|
160
203
|
return false;
|
|
161
204
|
}
|
|
162
205
|
return true;
|
|
@@ -171,7 +214,7 @@ function getCapturedRequest(adapterContext) {
|
|
|
171
214
|
}
|
|
172
215
|
return captured;
|
|
173
216
|
}
|
|
174
|
-
function buildStopMessageFollowupPayload(source, text) {
|
|
217
|
+
function buildStopMessageFollowupPayload(source, text, baseResponse) {
|
|
175
218
|
if (!source || typeof source !== 'object') {
|
|
176
219
|
return null;
|
|
177
220
|
}
|
|
@@ -181,6 +224,25 @@ function buildStopMessageFollowupPayload(source, text) {
|
|
|
181
224
|
}
|
|
182
225
|
const rawMessages = source.messages;
|
|
183
226
|
const messages = Array.isArray(rawMessages) ? cloneJson(rawMessages) : [];
|
|
227
|
+
try {
|
|
228
|
+
if (baseResponse && typeof baseResponse === 'object' && !Array.isArray(baseResponse)) {
|
|
229
|
+
const base = baseResponse;
|
|
230
|
+
const choices = Array.isArray(base.choices) ? base.choices : [];
|
|
231
|
+
const primary = choices[0] && typeof choices[0] === 'object' ? choices[0] : null;
|
|
232
|
+
const msg = primary &&
|
|
233
|
+
primary.message &&
|
|
234
|
+
typeof primary.message === 'object' &&
|
|
235
|
+
!Array.isArray(primary.message)
|
|
236
|
+
? primary.message
|
|
237
|
+
: null;
|
|
238
|
+
if (msg) {
|
|
239
|
+
messages.push(cloneJson(msg));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// best-effort: 如果无法解析上一条响应,就只使用已捕获的请求消息
|
|
245
|
+
}
|
|
184
246
|
messages.push({
|
|
185
247
|
role: 'user',
|
|
186
248
|
content: text
|
|
@@ -202,3 +264,121 @@ function resolveClientConnectionState(value) {
|
|
|
202
264
|
}
|
|
203
265
|
return value;
|
|
204
266
|
}
|
|
267
|
+
function resolveStopMessageSnapshot(raw) {
|
|
268
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
const record = raw;
|
|
272
|
+
const text = typeof record.stopMessageText === 'string' ? record.stopMessageText.trim() : '';
|
|
273
|
+
const maxRepeats = typeof record.stopMessageMaxRepeats === 'number' && Number.isFinite(record.stopMessageMaxRepeats)
|
|
274
|
+
? Math.max(1, Math.floor(record.stopMessageMaxRepeats))
|
|
275
|
+
: 0;
|
|
276
|
+
if (!text || maxRepeats <= 0) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
const used = typeof record.stopMessageUsed === 'number' && Number.isFinite(record.stopMessageUsed)
|
|
280
|
+
? Math.max(0, Math.floor(record.stopMessageUsed))
|
|
281
|
+
: 0;
|
|
282
|
+
const updatedAt = typeof record.stopMessageUpdatedAt === 'number' && Number.isFinite(record.stopMessageUpdatedAt)
|
|
283
|
+
? record.stopMessageUpdatedAt
|
|
284
|
+
: undefined;
|
|
285
|
+
const lastUsedAt = typeof record.stopMessageLastUsedAt === 'number' && Number.isFinite(record.stopMessageLastUsedAt)
|
|
286
|
+
? record.stopMessageLastUsedAt
|
|
287
|
+
: undefined;
|
|
288
|
+
const source = typeof record.stopMessageSource === 'string' && record.stopMessageSource.trim()
|
|
289
|
+
? record.stopMessageSource.trim()
|
|
290
|
+
: undefined;
|
|
291
|
+
return {
|
|
292
|
+
text,
|
|
293
|
+
maxRepeats,
|
|
294
|
+
used,
|
|
295
|
+
...(source ? { source } : {}),
|
|
296
|
+
...(updatedAt ? { updatedAt } : {}),
|
|
297
|
+
...(lastUsedAt ? { lastUsedAt } : {})
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function hasCompactionFlag(record) {
|
|
301
|
+
const flag = record.compactionRequest;
|
|
302
|
+
if (flag === true) {
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
if (typeof flag === 'string' && flag.trim().toLowerCase() === 'true') {
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
function resolveImplicitGeminiStopMessageSnapshot(ctx, record) {
|
|
311
|
+
try {
|
|
312
|
+
const protoFromCtx = ctx.options?.providerProtocol;
|
|
313
|
+
const protoFromRecord = typeof record.providerProtocol === 'string' && record.providerProtocol.trim()
|
|
314
|
+
? String(record.providerProtocol).trim()
|
|
315
|
+
: undefined;
|
|
316
|
+
const providerProtocol = (protoFromCtx || protoFromRecord || '').toString().toLowerCase();
|
|
317
|
+
if (providerProtocol !== 'gemini-chat') {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
const entryFromRecord = typeof record.entryEndpoint === 'string' && record.entryEndpoint.trim()
|
|
321
|
+
? String(record.entryEndpoint).trim()
|
|
322
|
+
: undefined;
|
|
323
|
+
const metaEntry = record.metadata &&
|
|
324
|
+
typeof record.metadata === 'object' &&
|
|
325
|
+
record.metadata.entryEndpoint;
|
|
326
|
+
const entryFromMeta = typeof metaEntry === 'string' && metaEntry.trim() ? metaEntry.trim() : undefined;
|
|
327
|
+
const entryEndpoint = (entryFromRecord || entryFromMeta || '').toLowerCase();
|
|
328
|
+
if (!entryEndpoint.includes('/v1/responses')) {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
// 仅在本轮响应被视为“自然结束”(stop/length,且无 tool_calls)时触发,避免干扰正常对话。
|
|
332
|
+
if (!isStopFinishReason(ctx.base)) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
text: '继续执行',
|
|
337
|
+
maxRepeats: 1,
|
|
338
|
+
used: 0,
|
|
339
|
+
source: 'auto'
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function createStopMessageState(snapshot) {
|
|
347
|
+
return {
|
|
348
|
+
forcedTarget: undefined,
|
|
349
|
+
stickyTarget: undefined,
|
|
350
|
+
allowedProviders: new Set(),
|
|
351
|
+
disabledProviders: new Set(),
|
|
352
|
+
disabledKeys: new Map(),
|
|
353
|
+
disabledModels: new Map(),
|
|
354
|
+
stopMessageSource: snapshot.source && snapshot.source.trim() ? snapshot.source.trim() : 'explicit',
|
|
355
|
+
stopMessageText: snapshot.text,
|
|
356
|
+
stopMessageMaxRepeats: snapshot.maxRepeats,
|
|
357
|
+
stopMessageUsed: snapshot.used,
|
|
358
|
+
stopMessageUpdatedAt: snapshot.updatedAt,
|
|
359
|
+
stopMessageLastUsedAt: snapshot.lastUsedAt
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
function resolveEntryEndpoint(record) {
|
|
363
|
+
const raw = typeof record.entryEndpoint === 'string' && record.entryEndpoint.trim()
|
|
364
|
+
? record.entryEndpoint.trim()
|
|
365
|
+
: undefined;
|
|
366
|
+
if (raw) {
|
|
367
|
+
return raw;
|
|
368
|
+
}
|
|
369
|
+
const metaEntry = record.metadata && typeof record.metadata === 'object' && record.metadata.entryEndpoint;
|
|
370
|
+
if (typeof metaEntry === 'string' && metaEntry.trim()) {
|
|
371
|
+
return metaEntry.trim();
|
|
372
|
+
}
|
|
373
|
+
return '/v1/chat/completions';
|
|
374
|
+
}
|
|
375
|
+
function shouldForceChatProtocol(entryEndpoint) {
|
|
376
|
+
if (typeof entryEndpoint !== 'string') {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
const normalized = entryEndpoint.trim().toLowerCase();
|
|
380
|
+
if (!normalized) {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
return !normalized.includes('/v1/chat/completions');
|
|
384
|
+
}
|
|
@@ -2,7 +2,6 @@ import type { JsonObject } from '../conversion/hub/types/json.js';
|
|
|
2
2
|
import type { ServerSideToolEngineOptions, ServerSideToolEngineResult, ToolCall } from './types.js';
|
|
3
3
|
import './handlers/web-search.js';
|
|
4
4
|
import './handlers/vision.js';
|
|
5
|
-
import './handlers/gemini-empty-reply-continue.js';
|
|
6
5
|
import './handlers/iflow-model-error-retry.js';
|
|
7
6
|
import './handlers/stop-message-auto.js';
|
|
8
7
|
export declare function runServerSideToolEngine(options: ServerSideToolEngineOptions): Promise<ServerSideToolEngineResult>;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { getServerToolHandler, listAutoServerToolHandlers } from './registry.js';
|
|
2
2
|
import './handlers/web-search.js';
|
|
3
3
|
import './handlers/vision.js';
|
|
4
|
-
import './handlers/gemini-empty-reply-continue.js';
|
|
5
4
|
import './handlers/iflow-model-error-retry.js';
|
|
6
5
|
import './handlers/stop-message-auto.js';
|
|
7
6
|
export async function runServerSideToolEngine(options) {
|
|
@@ -14,7 +14,27 @@ const SUPPORTED_KINDS = [
|
|
|
14
14
|
'delete_file'
|
|
15
15
|
];
|
|
16
16
|
const FILE_PATH_INVALID_RE = /[\r\n]/;
|
|
17
|
-
const
|
|
17
|
+
const decodeEscapedNewlinesIfObvious = (value) => {
|
|
18
|
+
if (!value)
|
|
19
|
+
return value;
|
|
20
|
+
if (value.includes('\n'))
|
|
21
|
+
return value;
|
|
22
|
+
const lower = value.toLowerCase();
|
|
23
|
+
const looksEscaped = value.includes('\\r\\n') ||
|
|
24
|
+
(value.includes('\\n') && /\\n[ \t]/.test(value)) ||
|
|
25
|
+
lower.includes('\\u000a') ||
|
|
26
|
+
lower.includes('\\u000d');
|
|
27
|
+
if (!looksEscaped) {
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
let out = value;
|
|
31
|
+
out = out.replace(/\\r\\n/g, '\n');
|
|
32
|
+
out = out.replace(/\\n/g, '\n');
|
|
33
|
+
out = out.replace(/\\r/g, '\n');
|
|
34
|
+
out = out.replace(/\\u000a/gi, '\n');
|
|
35
|
+
out = out.replace(/\\u000d/gi, '\n');
|
|
36
|
+
return out;
|
|
37
|
+
};
|
|
18
38
|
const toSafeString = (value, label) => {
|
|
19
39
|
const str = typeof value === 'string' ? value : '';
|
|
20
40
|
if (!str.trim()) {
|
|
@@ -23,20 +43,22 @@ const toSafeString = (value, label) => {
|
|
|
23
43
|
return str;
|
|
24
44
|
};
|
|
25
45
|
const normalizeFilePath = (raw, label) => {
|
|
26
|
-
|
|
46
|
+
let trimmed = raw.trim();
|
|
27
47
|
if (!trimmed) {
|
|
28
48
|
throw new StructuredApplyPatchError('invalid_file', `${label} must not be empty`);
|
|
29
49
|
}
|
|
30
50
|
if (FILE_PATH_INVALID_RE.test(trimmed)) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
51
|
+
const firstLine = trimmed.split(/[\r\n]/)[0]?.trim() ?? '';
|
|
52
|
+
if (!firstLine) {
|
|
53
|
+
throw new StructuredApplyPatchError('invalid_file', `${label} must be a single-line path`);
|
|
54
|
+
}
|
|
55
|
+
trimmed = firstLine;
|
|
35
56
|
}
|
|
36
57
|
return trimmed.replace(/\\/g, '/');
|
|
37
58
|
};
|
|
38
59
|
const splitTextIntoLines = (input) => {
|
|
39
|
-
const
|
|
60
|
+
const decoded = decodeEscapedNewlinesIfObvious(input);
|
|
61
|
+
const normalized = decoded.replace(/\r/g, '');
|
|
40
62
|
const parts = normalized.split('\n');
|
|
41
63
|
if (parts.length && parts[parts.length - 1] === '') {
|
|
42
64
|
parts.pop();
|
|
@@ -48,16 +70,27 @@ const normalizeLines = (value, label) => {
|
|
|
48
70
|
if (!value.length) {
|
|
49
71
|
return [];
|
|
50
72
|
}
|
|
51
|
-
|
|
73
|
+
const out = [];
|
|
74
|
+
for (const [idx, entry] of value.entries()) {
|
|
52
75
|
if (typeof entry !== 'string') {
|
|
53
76
|
if (entry === null || entry === undefined) {
|
|
54
|
-
|
|
77
|
+
out.push('');
|
|
78
|
+
continue;
|
|
55
79
|
}
|
|
56
|
-
|
|
80
|
+
out.push(String(entry));
|
|
81
|
+
continue;
|
|
57
82
|
}
|
|
58
83
|
// Preserve intentional whitespace
|
|
59
|
-
|
|
60
|
-
|
|
84
|
+
const normalized = entry.replace(/\r/g, '');
|
|
85
|
+
const decoded = decodeEscapedNewlinesIfObvious(normalized);
|
|
86
|
+
if (decoded.includes('\n')) {
|
|
87
|
+
out.push(...splitTextIntoLines(decoded));
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
out.push(decoded);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
61
94
|
}
|
|
62
95
|
if (typeof value === 'string') {
|
|
63
96
|
return splitTextIntoLines(value);
|
|
@@ -176,7 +209,9 @@ export function buildStructuredPatch(payload) {
|
|
|
176
209
|
break;
|
|
177
210
|
}
|
|
178
211
|
case 'replace': {
|
|
179
|
-
|
|
212
|
+
// 兼容仅提供 anchor 的 replace 形态:将 anchor 视为 target 以尽可能保留用户意图。
|
|
213
|
+
const targetSource = change.target ?? change.anchor;
|
|
214
|
+
const target = toSafeString(targetSource, `changes[${index}].target`);
|
|
180
215
|
const replacements = normalizeLines(change.lines, `changes[${index}].lines`);
|
|
181
216
|
const hunkBody = [
|
|
182
217
|
...buildPrefixedLines(splitTextIntoLines(target), '-'),
|
|
@@ -215,8 +250,10 @@ export function buildStructuredPatch(payload) {
|
|
|
215
250
|
}
|
|
216
251
|
else {
|
|
217
252
|
lines.push(`*** Update File: ${file}`);
|
|
218
|
-
|
|
219
|
-
|
|
253
|
+
const hunks = section.hunks || [];
|
|
254
|
+
for (const hunk of hunks) {
|
|
255
|
+
// 结构化补丁仅负责生成统一 diff 形态,不对多段 hunk 做逻辑裁剪;
|
|
256
|
+
// 具体哪些 hunk 能成功应用由 apply_patch 客户端自行校验并返回错误信息。
|
|
220
257
|
for (const entry of hunk) {
|
|
221
258
|
if (!entry.startsWith('@@')) {
|
|
222
259
|
lines.push(entry);
|