@jsonstudio/llms 0.6.2125 → 0.6.2172
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/compat/actions/deepseek-web-response.js +27 -3
- package/dist/conversion/compat/actions/strip-orphan-function-calls-tag.js +1 -1
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +9 -3
- package/dist/conversion/hub/process/chat-process.js +15 -18
- package/dist/conversion/responses/responses-openai-bridge.js +13 -12
- package/dist/conversion/shared/bridge-message-utils.js +92 -39
- package/dist/router/virtual-router/classifier.js +29 -5
- package/dist/router/virtual-router/engine/routing-pools/index.js +111 -5
- package/dist/router/virtual-router/engine-selection/multimodal-capability.d.ts +3 -0
- package/dist/router/virtual-router/engine-selection/multimodal-capability.js +26 -0
- package/dist/router/virtual-router/engine-selection/route-utils.js +6 -2
- package/dist/router/virtual-router/engine-selection/selection-deps.d.ts +1 -0
- package/dist/router/virtual-router/engine-selection/tier-selection.js +2 -0
- package/dist/router/virtual-router/engine.d.ts +2 -0
- package/dist/router/virtual-router/engine.js +57 -14
- package/dist/router/virtual-router/features.js +12 -4
- package/dist/router/virtual-router/message-utils.d.ts +8 -0
- package/dist/router/virtual-router/message-utils.js +170 -45
- package/dist/router/virtual-router/token-counter.js +51 -10
- package/dist/router/virtual-router/types.d.ts +3 -0
- package/dist/servertool/clock/session-scope.d.ts +3 -0
- package/dist/servertool/clock/session-scope.js +52 -0
- package/dist/servertool/engine.js +68 -8
- package/dist/servertool/handlers/clock-auto.js +2 -8
- package/dist/servertool/handlers/clock.js +3 -9
- package/dist/servertool/handlers/stop-message-auto/blocked-report.d.ts +16 -0
- package/dist/servertool/handlers/stop-message-auto/blocked-report.js +349 -0
- package/dist/servertool/handlers/stop-message-auto/iflow-followup.d.ts +23 -0
- package/dist/servertool/handlers/stop-message-auto/iflow-followup.js +503 -0
- package/dist/servertool/handlers/stop-message-auto/routing-state.d.ts +38 -0
- package/dist/servertool/handlers/stop-message-auto/routing-state.js +149 -0
- package/dist/servertool/handlers/stop-message-auto/runtime-utils.d.ts +67 -0
- package/dist/servertool/handlers/stop-message-auto/runtime-utils.js +387 -0
- package/dist/servertool/handlers/stop-message-auto.d.ts +1 -7
- package/dist/servertool/handlers/stop-message-auto.js +69 -971
- package/dist/servertool/handlers/web-search.js +117 -0
- package/package.json +1 -1
|
@@ -1,17 +1,30 @@
|
|
|
1
|
-
import * as childProcess from 'node:child_process';
|
|
2
|
-
import * as fs from 'node:fs';
|
|
3
|
-
import * as path from 'node:path';
|
|
4
1
|
import { registerServerToolHandler } from '../registry.js';
|
|
5
|
-
import { loadRoutingInstructionStateSync, saveRoutingInstructionStateAsync, saveRoutingInstructionStateSync } from '../../router/virtual-router/sticky-session-store.js';
|
|
6
|
-
import { DEFAULT_STOP_MESSAGE_MAX_REPEATS, ensureStopMessageModeMaxRepeats } from '../../router/virtual-router/routing-stop-message-state-codec.js';
|
|
7
2
|
import { isCompactionRequest } from './compaction-detect.js';
|
|
8
3
|
import { extractCapturedChatSeed } from './followup-request-builder.js';
|
|
9
|
-
import { resolveStopMessageStageDecision } from './stop-message-stage-policy.js';
|
|
10
4
|
import { readRuntimeMetadata } from '../../conversion/shared/runtime-metadata.js';
|
|
11
5
|
import { isStopEligibleForServerTool } from '../stop-gateway-context.js';
|
|
12
6
|
import { attachStopMessageCompareContext } from '../stop-message-compare-context.js';
|
|
7
|
+
import { extractStopMessageAutoResponseSnapshot, renderStopMessageAutoFollowupViaIflow } from './stop-message-auto/iflow-followup.js';
|
|
8
|
+
import { getCapturedRequest, hasCompactionFlag, readServerToolFollowupFlowId, resolveClientConnectionState, resolveEntryEndpoint, resolveImplicitGeminiStopMessageSnapshot, resolveStopMessageFollowupProviderKey, resolveStopMessageFollowupToolContentMaxChars } from './stop-message-auto/runtime-utils.js';
|
|
9
|
+
export { extractBlockedReportFromMessagesForTests } from './stop-message-auto/blocked-report.js';
|
|
13
10
|
const STOPMESSAGE_DEBUG = (process.env.ROUTECODEX_STOPMESSAGE_DEBUG || '').trim() === '1';
|
|
14
11
|
const STOPMESSAGE_IMPLICIT_GEMINI = (process.env.ROUTECODEX_STOPMESSAGE_IMPLICIT_GEMINI || '').trim() === '1';
|
|
12
|
+
const STOPMESSAGE_DEFAULT_ENABLED = (() => {
|
|
13
|
+
const raw = process.env.ROUTECODEX_STOPMESSAGE_DEFAULT_ENABLED;
|
|
14
|
+
if (typeof raw === 'string' && raw.trim().length > 0) {
|
|
15
|
+
return raw.trim() !== '0';
|
|
16
|
+
}
|
|
17
|
+
return process.env.NODE_ENV !== 'test';
|
|
18
|
+
})();
|
|
19
|
+
const STOPMESSAGE_DEFAULT_TEXT = (() => {
|
|
20
|
+
const raw = process.env.ROUTECODEX_STOPMESSAGE_DEFAULT_TEXT;
|
|
21
|
+
return typeof raw === 'string' && raw.trim().length > 0 ? raw.trim() : '继续执行';
|
|
22
|
+
})();
|
|
23
|
+
const STOPMESSAGE_DEFAULT_MAX_REPEATS = (() => {
|
|
24
|
+
const raw = process.env.ROUTECODEX_STOPMESSAGE_DEFAULT_MAX_REPEATS;
|
|
25
|
+
const parsed = typeof raw === 'string' ? Number(raw.trim()) : Number.NaN;
|
|
26
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 1;
|
|
27
|
+
})();
|
|
15
28
|
function debugLog(message, extra) {
|
|
16
29
|
if (!STOPMESSAGE_DEBUG) {
|
|
17
30
|
return;
|
|
@@ -35,9 +48,6 @@ const STOP_MESSAGE_EXECUTION_DIRECTIVE = [
|
|
|
35
48
|
'- 若遇阻塞,请按 JSON 输出结构化阻塞信息(type=blocked, summary, blocker, impact, next_action, evidence)。',
|
|
36
49
|
'- 如果即将返回 finish_reason=stop 或协议等价 end_turn,先调用 continue_execution 工具,不要直接结束会话。'
|
|
37
50
|
].join('\n');
|
|
38
|
-
const STOP_MESSAGE_BD_CREATE_TIMEOUT_MS = 2_000;
|
|
39
|
-
const STOP_MESSAGE_BLOCKED_TEXT_SCAN_LIMIT = 12;
|
|
40
|
-
const STOP_MESSAGE_BLOCKED_CANDIDATE_MAX_LENGTH = 12_000;
|
|
41
51
|
const handler = async (ctx) => {
|
|
42
52
|
const record = ctx.adapterContext;
|
|
43
53
|
const rt = readRuntimeMetadata(ctx.adapterContext);
|
|
@@ -101,185 +111,87 @@ const handler = async (ctx) => {
|
|
|
101
111
|
return markSkip('skip_compaction_flag');
|
|
102
112
|
}
|
|
103
113
|
const connectionState = resolveClientConnectionState(record.clientConnectionState);
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (!state || !hasArmedStopMessageState(state)) {
|
|
118
|
-
const fallback = resolveStopMessageSnapshot(rt?.stopMessageState);
|
|
119
|
-
if (fallback) {
|
|
120
|
-
state = createStopMessageState(fallback);
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
const implicit = STOPMESSAGE_IMPLICIT_GEMINI ? resolveImplicitGeminiStopMessageSnapshot(ctx, record) : null;
|
|
124
|
-
if (!implicit) {
|
|
125
|
-
debugLog('skip_no_state', { stickyKey });
|
|
126
|
-
return markSkip('skip_no_state', { armed: false, mode: 'off' });
|
|
127
|
-
}
|
|
128
|
-
state = createStopMessageState(implicit);
|
|
129
|
-
}
|
|
130
|
-
if (stickyKey) {
|
|
131
|
-
persistStopMessageState(stickyKey, state);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
if (state && ensureStopMessageModeMaxRepeats(state) && stickyKey) {
|
|
135
|
-
persistStopMessageState(stickyKey, state);
|
|
136
|
-
}
|
|
137
|
-
updateCompare({ armed: hasArmedStopMessageState(state) });
|
|
138
|
-
const text = typeof state.stopMessageText === 'string' ? state.stopMessageText.trim() : '';
|
|
139
|
-
const stageMode = normalizeStopMessageModeValue(state.stopMessageStageMode);
|
|
140
|
-
const maxRepeats = resolveStopMessageMaxRepeats(state.stopMessageMaxRepeats, stageMode);
|
|
141
|
-
const mode = stageMode === 'on' || stageMode === 'auto' || stageMode === 'off' ? stageMode : 'off';
|
|
142
|
-
const allowModeOnlyState = !text && maxRepeats > 0 && (stageMode === 'on' || stageMode === 'auto');
|
|
114
|
+
const implicit = STOPMESSAGE_IMPLICIT_GEMINI
|
|
115
|
+
? resolveImplicitGeminiStopMessageSnapshot(ctx, record)
|
|
116
|
+
: null;
|
|
117
|
+
if (!STOPMESSAGE_DEFAULT_ENABLED && !implicit) {
|
|
118
|
+
debugLog('skip_default_disabled');
|
|
119
|
+
return markSkip('skip_default_disabled', { armed: false, mode: 'off' });
|
|
120
|
+
}
|
|
121
|
+
const text = ((typeof implicit?.text === 'string' && implicit.text.trim().length > 0
|
|
122
|
+
? implicit.text
|
|
123
|
+
: STOPMESSAGE_DEFAULT_TEXT) || '继续执行').trim();
|
|
124
|
+
const maxRepeats = typeof implicit?.maxRepeats === 'number' && Number.isFinite(implicit.maxRepeats) && implicit.maxRepeats > 0
|
|
125
|
+
? Math.floor(implicit.maxRepeats)
|
|
126
|
+
: STOPMESSAGE_DEFAULT_MAX_REPEATS;
|
|
143
127
|
updateCompare({
|
|
144
|
-
|
|
145
|
-
|
|
128
|
+
armed: true,
|
|
129
|
+
mode: 'on',
|
|
130
|
+
allowModeOnly: text.length === 0,
|
|
146
131
|
textLength: text.length,
|
|
147
|
-
maxRepeats
|
|
132
|
+
maxRepeats: Math.max(1, maxRepeats),
|
|
133
|
+
used: 0
|
|
148
134
|
});
|
|
149
|
-
if ((!text && !allowModeOnlyState) || maxRepeats <= 0) {
|
|
150
|
-
debugLog('skip_invalid_text_or_maxRepeats', {
|
|
151
|
-
stickyKey,
|
|
152
|
-
textLength: text.length,
|
|
153
|
-
maxRepeats,
|
|
154
|
-
stageMode
|
|
155
|
-
});
|
|
156
|
-
return markSkip('skip_invalid_text_or_maxRepeats');
|
|
157
|
-
}
|
|
158
|
-
const used = typeof state.stopMessageUsed === 'number' && Number.isFinite(state.stopMessageUsed)
|
|
159
|
-
? Math.max(0, Math.floor(state.stopMessageUsed))
|
|
160
|
-
: 0;
|
|
161
|
-
updateCompare({ used });
|
|
162
|
-
if (used >= maxRepeats) {
|
|
163
|
-
debugLog('skip_reached_max_repeats', {
|
|
164
|
-
stickyKey,
|
|
165
|
-
used,
|
|
166
|
-
maxRepeats
|
|
167
|
-
});
|
|
168
|
-
clearStopMessageState(state, Date.now());
|
|
169
|
-
if (stickyKey) {
|
|
170
|
-
persistStopMessageState(stickyKey, state);
|
|
171
|
-
}
|
|
172
|
-
return markSkip('skip_reached_max_repeats');
|
|
173
|
-
}
|
|
174
135
|
const stopEligible = isStopEligibleForServerTool(ctx.base, ctx.adapterContext);
|
|
175
136
|
updateCompare({ stopEligible });
|
|
176
137
|
if (!stopEligible) {
|
|
177
|
-
debugLog('skip_not_stop_finish_reason'
|
|
178
|
-
stickyKey
|
|
179
|
-
});
|
|
138
|
+
debugLog('skip_not_stop_finish_reason');
|
|
180
139
|
return markSkip('skip_not_stop_finish_reason');
|
|
181
140
|
}
|
|
182
141
|
const captured = getCapturedRequest(ctx.adapterContext);
|
|
183
142
|
updateCompare({ hasCapturedRequest: Boolean(captured) });
|
|
184
143
|
if (!captured) {
|
|
185
|
-
debugLog('skip_no_captured_request'
|
|
186
|
-
stickyKey
|
|
187
|
-
});
|
|
144
|
+
debugLog('skip_no_captured_request');
|
|
188
145
|
return markSkip('skip_no_captured_request');
|
|
189
146
|
}
|
|
190
147
|
const compactionRequest = isCompactionRequest(captured);
|
|
191
148
|
updateCompare({ compactionRequest });
|
|
192
149
|
if (compactionRequest) {
|
|
193
|
-
debugLog('skip_compaction_request'
|
|
150
|
+
debugLog('skip_compaction_request');
|
|
194
151
|
return markSkip('skip_compaction_request');
|
|
195
152
|
}
|
|
196
153
|
const entryEndpoint = resolveEntryEndpoint(record);
|
|
197
154
|
const seed = extractCapturedChatSeed(captured);
|
|
198
155
|
updateCompare({ hasSeed: Boolean(seed) });
|
|
199
156
|
if (!seed) {
|
|
200
|
-
debugLog('skip_failed_build_followup'
|
|
157
|
+
debugLog('skip_failed_build_followup');
|
|
201
158
|
return markSkip('skip_failed_build_followup');
|
|
202
159
|
}
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
debugLog('blocked_report_issue_created', {
|
|
221
|
-
stickyKey,
|
|
222
|
-
issueId
|
|
223
|
-
});
|
|
224
|
-
return markSkip('blocked_issue_created');
|
|
225
|
-
}
|
|
226
|
-
debugLog('blocked_report_issue_create_failed', { stickyKey });
|
|
227
|
-
}
|
|
228
|
-
const bdWorkingDirectory = resolveBdWorkingDirectoryForRecord(record, rt);
|
|
229
|
-
const stageDecision = resolveStopMessageStageDecision({
|
|
230
|
-
baseText: text,
|
|
231
|
-
state,
|
|
232
|
-
capturedMessages,
|
|
233
|
-
bdWorkingDirectory
|
|
234
|
-
});
|
|
235
|
-
state.stopMessageObservationHash = stageDecision.observationHash;
|
|
236
|
-
state.stopMessageObservationStableCount = stageDecision.observationStableCount;
|
|
237
|
-
state.stopMessageBdWorkState = stageDecision.bdWorkState;
|
|
238
|
-
state.stopMessageStage = stageDecision.stage;
|
|
239
|
-
state.stopMessageAssignedIssueId = stageDecision.assignedIssueId;
|
|
240
|
-
state.stopMessageAssignedIssueSource = stageDecision.assignedIssueSource;
|
|
241
|
-
state.stopMessageNoTaskSummaryUsed = stageDecision.noTaskSummaryUsed;
|
|
242
|
-
updateCompare({
|
|
243
|
-
stage: stageDecision.stage,
|
|
244
|
-
bdWorkState: stageDecision.bdWorkState,
|
|
245
|
-
observationHash: stageDecision.observationHash,
|
|
246
|
-
observationStableCount: stageDecision.observationStableCount,
|
|
247
|
-
toolSignatureHash: stageDecision.toolSignatureHash
|
|
160
|
+
const used = 0;
|
|
161
|
+
let followupText = text || '继续执行';
|
|
162
|
+
const autoResponseSnapshot = extractStopMessageAutoResponseSnapshot(ctx.base, ctx.adapterContext);
|
|
163
|
+
const iflowFollowupText = renderStopMessageAutoFollowupViaIflow({
|
|
164
|
+
baseStopMessageText: text,
|
|
165
|
+
candidateFollowupText: followupText,
|
|
166
|
+
responseSnapshot: autoResponseSnapshot,
|
|
167
|
+
requestId: typeof record.requestId === 'string'
|
|
168
|
+
? record.requestId.trim()
|
|
169
|
+
: undefined,
|
|
170
|
+
sessionId: typeof record.sessionId === 'string'
|
|
171
|
+
? record.sessionId.trim()
|
|
172
|
+
: undefined,
|
|
173
|
+
providerKey: resolveStopMessageFollowupProviderKey({ record, runtimeMetadata: rt }),
|
|
174
|
+
model: typeof seed.model === 'string' ? seed.model : undefined,
|
|
175
|
+
usedRepeats: used,
|
|
176
|
+
maxRepeats
|
|
248
177
|
});
|
|
249
|
-
if (
|
|
250
|
-
|
|
251
|
-
if (
|
|
252
|
-
|
|
178
|
+
if (iflowFollowupText) {
|
|
179
|
+
followupText = iflowFollowupText.trim();
|
|
180
|
+
if (text && !followupText.includes(text)) {
|
|
181
|
+
followupText = `${text}\n${followupText}`.trim();
|
|
253
182
|
}
|
|
254
|
-
debugLog('
|
|
255
|
-
|
|
256
|
-
reason: stageDecision.stopReason,
|
|
257
|
-
observationStableCount: stageDecision.observationStableCount,
|
|
258
|
-
bdWorkState: stageDecision.bdWorkState
|
|
183
|
+
debugLog('iflow_automessage_followup_applied', {
|
|
184
|
+
textLength: followupText.length
|
|
259
185
|
});
|
|
260
|
-
return markSkip('stage_policy_stop');
|
|
261
186
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
187
|
+
else {
|
|
188
|
+
followupText = '继续执行';
|
|
189
|
+
debugLog('iflow_automessage_followup_fallback_to_continue_execution');
|
|
190
|
+
}
|
|
265
191
|
if (!followupText) {
|
|
266
|
-
debugLog('skip_empty_followup_text_after_stage'
|
|
267
|
-
stickyKey,
|
|
268
|
-
stage: stageDecision.stage,
|
|
269
|
-
bdWorkState: stageDecision.bdWorkState
|
|
270
|
-
});
|
|
192
|
+
debugLog('skip_empty_followup_text_after_stage');
|
|
271
193
|
return markSkip('skip_empty_followup_text_after_stage');
|
|
272
194
|
}
|
|
273
|
-
const nextUsed = used + 1;
|
|
274
|
-
state.stopMessageUsed = nextUsed;
|
|
275
|
-
state.stopMessageLastUsedAt = Date.now();
|
|
276
|
-
updateCompare({ used: nextUsed });
|
|
277
|
-
if (nextUsed >= maxRepeats) {
|
|
278
|
-
clearStopMessageState(state, Date.now());
|
|
279
|
-
}
|
|
280
|
-
if (stickyKey) {
|
|
281
|
-
persistStopMessageState(stickyKey, state);
|
|
282
|
-
}
|
|
283
195
|
const followupProviderKey = resolveStopMessageFollowupProviderKey({ record, runtimeMetadata: rt });
|
|
284
196
|
const followupToolContentMaxChars = resolveStopMessageFollowupToolContentMaxChars({
|
|
285
197
|
providerKey: followupProviderKey,
|
|
@@ -321,817 +233,3 @@ const handler = async (ctx) => {
|
|
|
321
233
|
}
|
|
322
234
|
};
|
|
323
235
|
registerServerToolHandler('stop_message_auto', handler, { trigger: 'auto', hook: { phase: 'default', priority: 40 } });
|
|
324
|
-
function resolveStickyKey(record, runtimeMetadata) {
|
|
325
|
-
const sessionId = readSessionScopeValue(record, runtimeMetadata, 'sessionId') ||
|
|
326
|
-
readSessionScopeValue(record, runtimeMetadata, 'session_id');
|
|
327
|
-
const conversationId = readSessionScopeValue(record, runtimeMetadata, 'conversationId') ||
|
|
328
|
-
readSessionScopeValue(record, runtimeMetadata, 'conversation_id');
|
|
329
|
-
if (sessionId) {
|
|
330
|
-
return `session:${sessionId}`;
|
|
331
|
-
}
|
|
332
|
-
if (conversationId) {
|
|
333
|
-
return `conversation:${conversationId}`;
|
|
334
|
-
}
|
|
335
|
-
return undefined;
|
|
336
|
-
}
|
|
337
|
-
function persistStopMessageState(stickyKey, state) {
|
|
338
|
-
if (!stickyKey) {
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
saveRoutingInstructionStateSync(stickyKey, state);
|
|
342
|
-
saveRoutingInstructionStateAsync(stickyKey, state);
|
|
343
|
-
}
|
|
344
|
-
function readSessionScopeValue(record, runtimeMetadata, key) {
|
|
345
|
-
const direct = toNonEmptyText(record[key]);
|
|
346
|
-
if (direct) {
|
|
347
|
-
return direct;
|
|
348
|
-
}
|
|
349
|
-
const metadata = asRecord(record.metadata);
|
|
350
|
-
const fromMetadata = metadata ? toNonEmptyText(metadata[key]) : '';
|
|
351
|
-
if (fromMetadata) {
|
|
352
|
-
return fromMetadata;
|
|
353
|
-
}
|
|
354
|
-
const fromMetadataContext = metadata ? toNonEmptyText(asRecord(metadata.context)?.[key]) : '';
|
|
355
|
-
if (fromMetadataContext) {
|
|
356
|
-
return fromMetadataContext;
|
|
357
|
-
}
|
|
358
|
-
const originalRequest = asRecord(record.originalRequest);
|
|
359
|
-
const fromOriginalMetadata = originalRequest
|
|
360
|
-
? toNonEmptyText(asRecord(originalRequest.metadata)?.[key])
|
|
361
|
-
: '';
|
|
362
|
-
if (fromOriginalMetadata) {
|
|
363
|
-
return fromOriginalMetadata;
|
|
364
|
-
}
|
|
365
|
-
const runtime = asRecord(runtimeMetadata);
|
|
366
|
-
const fromRuntime = runtime ? toNonEmptyText(runtime[key]) : '';
|
|
367
|
-
if (fromRuntime) {
|
|
368
|
-
return fromRuntime;
|
|
369
|
-
}
|
|
370
|
-
return '';
|
|
371
|
-
}
|
|
372
|
-
function resolveBdWorkingDirectoryForRecord(record, runtimeMetadata) {
|
|
373
|
-
const fromWorkdir = readSessionScopeValue(record, runtimeMetadata, 'workdir');
|
|
374
|
-
if (fromWorkdir) {
|
|
375
|
-
return fromWorkdir;
|
|
376
|
-
}
|
|
377
|
-
const fromCwd = readSessionScopeValue(record, runtimeMetadata, 'cwd');
|
|
378
|
-
if (fromCwd) {
|
|
379
|
-
return fromCwd;
|
|
380
|
-
}
|
|
381
|
-
const fromWorkingDirectory = readSessionScopeValue(record, runtimeMetadata, 'workingDirectory');
|
|
382
|
-
if (fromWorkingDirectory) {
|
|
383
|
-
return fromWorkingDirectory;
|
|
384
|
-
}
|
|
385
|
-
return undefined;
|
|
386
|
-
}
|
|
387
|
-
function asRecord(value) {
|
|
388
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
389
|
-
return null;
|
|
390
|
-
}
|
|
391
|
-
return value;
|
|
392
|
-
}
|
|
393
|
-
function toNonEmptyText(value) {
|
|
394
|
-
return typeof value === 'string' && value.trim().length ? value.trim() : '';
|
|
395
|
-
}
|
|
396
|
-
function readProviderKeyFromMetadata(value) {
|
|
397
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
398
|
-
return '';
|
|
399
|
-
}
|
|
400
|
-
const metadata = value;
|
|
401
|
-
const direct = toNonEmptyText(metadata.providerKey) ||
|
|
402
|
-
toNonEmptyText(metadata.providerId) ||
|
|
403
|
-
toNonEmptyText(metadata.targetProviderKey);
|
|
404
|
-
if (direct) {
|
|
405
|
-
return direct;
|
|
406
|
-
}
|
|
407
|
-
const target = metadata.target;
|
|
408
|
-
if (target && typeof target === 'object' && !Array.isArray(target)) {
|
|
409
|
-
const targetRecord = target;
|
|
410
|
-
return toNonEmptyText(targetRecord.providerKey) || toNonEmptyText(targetRecord.providerId);
|
|
411
|
-
}
|
|
412
|
-
return '';
|
|
413
|
-
}
|
|
414
|
-
function readServerToolFollowupFlowId(runtimeMetadata) {
|
|
415
|
-
const runtime = asRecord(runtimeMetadata);
|
|
416
|
-
const loopState = runtime ? asRecord(runtime.serverToolLoopState) : null;
|
|
417
|
-
const flowId = loopState ? toNonEmptyText(loopState.flowId) : '';
|
|
418
|
-
return flowId;
|
|
419
|
-
}
|
|
420
|
-
function hasArmedStopMessageState(state) {
|
|
421
|
-
const mode = normalizeStopMessageModeValue(state.stopMessageStageMode);
|
|
422
|
-
const maxRepeats = resolveStopMessageMaxRepeats(state.stopMessageMaxRepeats, mode);
|
|
423
|
-
if (maxRepeats <= 0) {
|
|
424
|
-
return false;
|
|
425
|
-
}
|
|
426
|
-
const text = typeof state.stopMessageText === 'string' ? state.stopMessageText.trim() : '';
|
|
427
|
-
if (text) {
|
|
428
|
-
return true;
|
|
429
|
-
}
|
|
430
|
-
return mode === 'on' || mode === 'auto';
|
|
431
|
-
}
|
|
432
|
-
function normalizeStopMessageModeValue(value) {
|
|
433
|
-
if (typeof value !== 'string') {
|
|
434
|
-
return undefined;
|
|
435
|
-
}
|
|
436
|
-
const normalized = value.trim().toLowerCase();
|
|
437
|
-
if (normalized === 'on' || normalized === 'off' || normalized === 'auto') {
|
|
438
|
-
return normalized;
|
|
439
|
-
}
|
|
440
|
-
return undefined;
|
|
441
|
-
}
|
|
442
|
-
function resolveStopMessageFollowupProviderKey(args) {
|
|
443
|
-
const direct = toNonEmptyText(args.record.providerKey) ||
|
|
444
|
-
toNonEmptyText(args.record.providerId) ||
|
|
445
|
-
readProviderKeyFromMetadata(args.record.metadata) ||
|
|
446
|
-
readProviderKeyFromMetadata(args.runtimeMetadata);
|
|
447
|
-
return direct;
|
|
448
|
-
}
|
|
449
|
-
function resolveStopMessageFollowupToolContentMaxChars(params) {
|
|
450
|
-
const raw = String(process.env.ROUTECODEX_STOPMESSAGE_FOLLOWUP_TOOL_CONTENT_MAX_CHARS || '').trim();
|
|
451
|
-
if (raw) {
|
|
452
|
-
const parsed = Number(raw);
|
|
453
|
-
if (Number.isFinite(parsed) && parsed > 0) {
|
|
454
|
-
return Math.max(64, Math.floor(parsed));
|
|
455
|
-
}
|
|
456
|
-
return undefined;
|
|
457
|
-
}
|
|
458
|
-
const providerKey = typeof params.providerKey === 'string' ? params.providerKey.trim().toLowerCase() : '';
|
|
459
|
-
if (providerKey.startsWith('iflow.')) {
|
|
460
|
-
return 1200;
|
|
461
|
-
}
|
|
462
|
-
const model = typeof params.model === 'string' ? params.model.trim().toLowerCase() : '';
|
|
463
|
-
if (model === 'kimi-k2.5' || model.startsWith('kimi-k2.5-')) {
|
|
464
|
-
return 1200;
|
|
465
|
-
}
|
|
466
|
-
return undefined;
|
|
467
|
-
}
|
|
468
|
-
function getCapturedRequest(adapterContext) {
|
|
469
|
-
if (!adapterContext || typeof adapterContext !== 'object') {
|
|
470
|
-
return null;
|
|
471
|
-
}
|
|
472
|
-
const contextRecord = adapterContext;
|
|
473
|
-
const direct = contextRecord.capturedChatRequest;
|
|
474
|
-
if (direct && typeof direct === 'object' && !Array.isArray(direct)) {
|
|
475
|
-
return direct;
|
|
476
|
-
}
|
|
477
|
-
const runtime = readRuntimeMetadata(contextRecord);
|
|
478
|
-
const runtimeCaptured = runtime && typeof runtime === 'object' && !Array.isArray(runtime)
|
|
479
|
-
? runtime.capturedChatRequest
|
|
480
|
-
: undefined;
|
|
481
|
-
if (runtimeCaptured && typeof runtimeCaptured === 'object' && !Array.isArray(runtimeCaptured)) {
|
|
482
|
-
return runtimeCaptured;
|
|
483
|
-
}
|
|
484
|
-
const originalRequest = contextRecord.originalRequest;
|
|
485
|
-
if (originalRequest && typeof originalRequest === 'object' && !Array.isArray(originalRequest)) {
|
|
486
|
-
return originalRequest;
|
|
487
|
-
}
|
|
488
|
-
return null;
|
|
489
|
-
}
|
|
490
|
-
function extractResponsesOutputText(base) {
|
|
491
|
-
const raw = base.output_text;
|
|
492
|
-
if (typeof raw === 'string') {
|
|
493
|
-
return raw.trim();
|
|
494
|
-
}
|
|
495
|
-
if (Array.isArray(raw)) {
|
|
496
|
-
const texts = raw
|
|
497
|
-
.map((entry) => (typeof entry === 'string' ? entry : ''))
|
|
498
|
-
.filter((entry) => entry.trim().length > 0);
|
|
499
|
-
if (texts.length > 0) {
|
|
500
|
-
return texts.join('\n').trim();
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
const output = Array.isArray(base.output) ? (base.output) : [];
|
|
504
|
-
const chunks = [];
|
|
505
|
-
for (const item of output) {
|
|
506
|
-
if (!item || typeof item !== 'object' || Array.isArray(item))
|
|
507
|
-
continue;
|
|
508
|
-
if (typeof item.type !== 'string')
|
|
509
|
-
continue;
|
|
510
|
-
const type = String(item.type).trim().toLowerCase();
|
|
511
|
-
if (type !== 'message')
|
|
512
|
-
continue;
|
|
513
|
-
const content = Array.isArray(item.content) ? (item.content) : [];
|
|
514
|
-
for (const part of content) {
|
|
515
|
-
if (!part || typeof part !== 'object' || Array.isArray(part))
|
|
516
|
-
continue;
|
|
517
|
-
const pType = typeof part.type === 'string'
|
|
518
|
-
? String(part.type).trim().toLowerCase()
|
|
519
|
-
: '';
|
|
520
|
-
if (pType === 'output_text') {
|
|
521
|
-
const text = typeof part.text === 'string' ? String(part.text) : '';
|
|
522
|
-
if (text.trim().length)
|
|
523
|
-
chunks.push(text.trim());
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
return chunks.join('\n').trim();
|
|
528
|
-
}
|
|
529
|
-
function hasToolLikeOutput(value) {
|
|
530
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
531
|
-
return false;
|
|
532
|
-
}
|
|
533
|
-
const typeRaw = value.type;
|
|
534
|
-
const type = typeof typeRaw === 'string' ? typeRaw.trim().toLowerCase() : '';
|
|
535
|
-
if (!type) {
|
|
536
|
-
return false;
|
|
537
|
-
}
|
|
538
|
-
return (type === 'tool_call' ||
|
|
539
|
-
type === 'tool_use' ||
|
|
540
|
-
type === 'function_call' ||
|
|
541
|
-
type.includes('tool'));
|
|
542
|
-
}
|
|
543
|
-
function resolveClientConnectionState(value) {
|
|
544
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
545
|
-
return null;
|
|
546
|
-
}
|
|
547
|
-
return value;
|
|
548
|
-
}
|
|
549
|
-
function resolveStopMessageSnapshot(raw) {
|
|
550
|
-
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
551
|
-
return null;
|
|
552
|
-
}
|
|
553
|
-
const record = raw;
|
|
554
|
-
const text = typeof record.stopMessageText === 'string' ? record.stopMessageText.trim() : '';
|
|
555
|
-
const stageMode = normalizeStopMessageStageMode(record.stopMessageStageMode);
|
|
556
|
-
const maxRepeats = resolveStopMessageMaxRepeats(record.stopMessageMaxRepeats, stageMode);
|
|
557
|
-
const allowModeOnlyState = !text && maxRepeats > 0 && (stageMode === 'on' || stageMode === 'auto');
|
|
558
|
-
if ((!text && !allowModeOnlyState) || maxRepeats <= 0) {
|
|
559
|
-
return null;
|
|
560
|
-
}
|
|
561
|
-
const used = typeof record.stopMessageUsed === 'number' && Number.isFinite(record.stopMessageUsed)
|
|
562
|
-
? Math.max(0, Math.floor(record.stopMessageUsed))
|
|
563
|
-
: 0;
|
|
564
|
-
const updatedAt = typeof record.stopMessageUpdatedAt === 'number' && Number.isFinite(record.stopMessageUpdatedAt)
|
|
565
|
-
? record.stopMessageUpdatedAt
|
|
566
|
-
: undefined;
|
|
567
|
-
const lastUsedAt = typeof record.stopMessageLastUsedAt === 'number' && Number.isFinite(record.stopMessageLastUsedAt)
|
|
568
|
-
? record.stopMessageLastUsedAt
|
|
569
|
-
: undefined;
|
|
570
|
-
const source = typeof record.stopMessageSource === 'string' && record.stopMessageSource.trim()
|
|
571
|
-
? record.stopMessageSource.trim()
|
|
572
|
-
: undefined;
|
|
573
|
-
const stage = typeof record.stopMessageStage === 'string' && record.stopMessageStage.trim()
|
|
574
|
-
? record.stopMessageStage.trim()
|
|
575
|
-
: undefined;
|
|
576
|
-
const observationHash = typeof record.stopMessageObservationHash === 'string' && record.stopMessageObservationHash.trim()
|
|
577
|
-
? record.stopMessageObservationHash.trim()
|
|
578
|
-
: undefined;
|
|
579
|
-
const observationStableCount = typeof record.stopMessageObservationStableCount === 'number' && Number.isFinite(record.stopMessageObservationStableCount)
|
|
580
|
-
? Math.max(0, Math.floor(record.stopMessageObservationStableCount))
|
|
581
|
-
: undefined;
|
|
582
|
-
const bdWorkState = typeof record.stopMessageBdWorkState === 'string' && record.stopMessageBdWorkState.trim()
|
|
583
|
-
? record.stopMessageBdWorkState.trim()
|
|
584
|
-
: undefined;
|
|
585
|
-
const assignedIssueId = typeof record.stopMessageAssignedIssueId === 'string' && record.stopMessageAssignedIssueId.trim()
|
|
586
|
-
? record.stopMessageAssignedIssueId.trim()
|
|
587
|
-
: undefined;
|
|
588
|
-
const sourceRaw = typeof record.stopMessageAssignedIssueSource === 'string'
|
|
589
|
-
? record.stopMessageAssignedIssueSource.trim().toLowerCase()
|
|
590
|
-
: '';
|
|
591
|
-
const assignedIssueSource = sourceRaw === 'in_progress' || sourceRaw === 'ready' || sourceRaw === 'open'
|
|
592
|
-
? sourceRaw
|
|
593
|
-
: undefined;
|
|
594
|
-
const noTaskSummaryUsed = typeof record.stopMessageNoTaskSummaryUsed === 'boolean'
|
|
595
|
-
? record.stopMessageNoTaskSummaryUsed
|
|
596
|
-
: undefined;
|
|
597
|
-
return {
|
|
598
|
-
text,
|
|
599
|
-
maxRepeats,
|
|
600
|
-
used,
|
|
601
|
-
...(source ? { source } : {}),
|
|
602
|
-
...(updatedAt ? { updatedAt } : {}),
|
|
603
|
-
...(lastUsedAt ? { lastUsedAt } : {}),
|
|
604
|
-
...(stage ? { stage } : {}),
|
|
605
|
-
...(stageMode ? { stageMode } : {}),
|
|
606
|
-
...(observationHash ? { observationHash } : {}),
|
|
607
|
-
...(typeof observationStableCount === 'number' ? { observationStableCount } : {}),
|
|
608
|
-
...(bdWorkState ? { bdWorkState } : {}),
|
|
609
|
-
...(assignedIssueId ? { assignedIssueId } : {}),
|
|
610
|
-
...(assignedIssueSource ? { assignedIssueSource } : {}),
|
|
611
|
-
...(typeof noTaskSummaryUsed === 'boolean' ? { noTaskSummaryUsed } : {})
|
|
612
|
-
};
|
|
613
|
-
}
|
|
614
|
-
function hasCompactionFlag(rt) {
|
|
615
|
-
const flag = rt && typeof rt === 'object' && !Array.isArray(rt) ? rt.compactionRequest : undefined;
|
|
616
|
-
if (flag === true) {
|
|
617
|
-
return true;
|
|
618
|
-
}
|
|
619
|
-
if (typeof flag === 'string' && flag.trim().toLowerCase() === 'true') {
|
|
620
|
-
return true;
|
|
621
|
-
}
|
|
622
|
-
return false;
|
|
623
|
-
}
|
|
624
|
-
function resolveImplicitGeminiStopMessageSnapshot(ctx, record) {
|
|
625
|
-
try {
|
|
626
|
-
const protoFromCtx = ctx.providerProtocol;
|
|
627
|
-
const protoFromRecord = typeof record.providerProtocol === 'string' && record.providerProtocol.trim()
|
|
628
|
-
? String(record.providerProtocol).trim()
|
|
629
|
-
: undefined;
|
|
630
|
-
const providerProtocol = (protoFromCtx || protoFromRecord || '').toString().toLowerCase();
|
|
631
|
-
if (providerProtocol !== 'gemini-chat') {
|
|
632
|
-
return null;
|
|
633
|
-
}
|
|
634
|
-
const entryFromRecord = typeof record.entryEndpoint === 'string' && record.entryEndpoint.trim()
|
|
635
|
-
? String(record.entryEndpoint).trim()
|
|
636
|
-
: undefined;
|
|
637
|
-
const metaEntry = record.metadata &&
|
|
638
|
-
typeof record.metadata === 'object' &&
|
|
639
|
-
record.metadata.entryEndpoint;
|
|
640
|
-
const entryFromMeta = typeof metaEntry === 'string' && metaEntry.trim() ? metaEntry.trim() : undefined;
|
|
641
|
-
const entryEndpoint = (entryFromRecord || entryFromMeta || '').toLowerCase();
|
|
642
|
-
if (!entryEndpoint.includes('/v1/responses')) {
|
|
643
|
-
return null;
|
|
644
|
-
}
|
|
645
|
-
// 仅在本轮响应被视为“自然结束”(stop/length,且无 tool_calls)时触发,避免干扰正常对话。
|
|
646
|
-
if (!isStopEligibleForServerTool(ctx.base, ctx.adapterContext)) {
|
|
647
|
-
return null;
|
|
648
|
-
}
|
|
649
|
-
// 仅在“空回复”时触发隐式 stopMessage:
|
|
650
|
-
// - 这个场景由 empty_reply_continue 专门处理;
|
|
651
|
-
// - stop_message_auto 里的隐式逻辑只作为兼容兜底(且默认关闭),避免对正常 stop 响应追加“继续执行”。
|
|
652
|
-
if (!isEmptyAssistantReply(ctx.base)) {
|
|
653
|
-
return null;
|
|
654
|
-
}
|
|
655
|
-
return {
|
|
656
|
-
text: '继续执行',
|
|
657
|
-
maxRepeats: 1,
|
|
658
|
-
used: 0,
|
|
659
|
-
source: 'auto'
|
|
660
|
-
};
|
|
661
|
-
}
|
|
662
|
-
catch {
|
|
663
|
-
return null;
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
function isEmptyAssistantReply(base) {
|
|
667
|
-
if (!base || typeof base !== 'object' || Array.isArray(base)) {
|
|
668
|
-
return false;
|
|
669
|
-
}
|
|
670
|
-
const payload = base;
|
|
671
|
-
const choicesRaw = payload.choices;
|
|
672
|
-
if (Array.isArray(choicesRaw) && choicesRaw.length) {
|
|
673
|
-
const first = choicesRaw[0];
|
|
674
|
-
if (!first || typeof first !== 'object' || Array.isArray(first)) {
|
|
675
|
-
return false;
|
|
676
|
-
}
|
|
677
|
-
const finishReasonRaw = first.finish_reason;
|
|
678
|
-
const finishReason = typeof finishReasonRaw === 'string' && finishReasonRaw.trim()
|
|
679
|
-
? finishReasonRaw.trim().toLowerCase()
|
|
680
|
-
: '';
|
|
681
|
-
// 仅接受 stop:length 截断通常并非“空回复”,而是需要续写(由 empty_reply_continue 负责)。
|
|
682
|
-
if (finishReason !== 'stop') {
|
|
683
|
-
return false;
|
|
684
|
-
}
|
|
685
|
-
const message = first.message &&
|
|
686
|
-
typeof first.message === 'object' &&
|
|
687
|
-
!Array.isArray(first.message)
|
|
688
|
-
? first.message
|
|
689
|
-
: null;
|
|
690
|
-
if (!message) {
|
|
691
|
-
return false;
|
|
692
|
-
}
|
|
693
|
-
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
694
|
-
if (toolCalls.length > 0) {
|
|
695
|
-
return false;
|
|
696
|
-
}
|
|
697
|
-
const contentRaw = message.content;
|
|
698
|
-
const text = typeof contentRaw === 'string' ? contentRaw.trim() : '';
|
|
699
|
-
return text.length === 0;
|
|
700
|
-
}
|
|
701
|
-
// OpenAI Responses shape: treat empty output_text + no tool-like output as empty reply.
|
|
702
|
-
const statusRaw = typeof payload.status === 'string' ? payload.status.trim().toLowerCase() : '';
|
|
703
|
-
if (statusRaw && statusRaw !== 'completed') {
|
|
704
|
-
return false;
|
|
705
|
-
}
|
|
706
|
-
if (payload.required_action && typeof payload.required_action === 'object') {
|
|
707
|
-
return false;
|
|
708
|
-
}
|
|
709
|
-
const outputText = extractResponsesOutputText(payload);
|
|
710
|
-
if (outputText.length > 0) {
|
|
711
|
-
return false;
|
|
712
|
-
}
|
|
713
|
-
const outputRaw = Array.isArray(payload.output) ? payload.output : [];
|
|
714
|
-
if (outputRaw.some((item) => hasToolLikeOutput(item))) {
|
|
715
|
-
return false;
|
|
716
|
-
}
|
|
717
|
-
return true;
|
|
718
|
-
}
|
|
719
|
-
function extractBlockedReportFromMessages(messages) {
|
|
720
|
-
if (!Array.isArray(messages) || messages.length === 0) {
|
|
721
|
-
return null;
|
|
722
|
-
}
|
|
723
|
-
const start = Math.max(0, messages.length - STOP_MESSAGE_BLOCKED_TEXT_SCAN_LIMIT);
|
|
724
|
-
for (let idx = messages.length - 1; idx >= start; idx -= 1) {
|
|
725
|
-
const text = extractCapturedMessageText(messages[idx]);
|
|
726
|
-
if (!text) {
|
|
727
|
-
continue;
|
|
728
|
-
}
|
|
729
|
-
const blockedReport = extractBlockedReportFromText(text);
|
|
730
|
-
if (blockedReport) {
|
|
731
|
-
return blockedReport;
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
return null;
|
|
735
|
-
}
|
|
736
|
-
export function extractBlockedReportFromMessagesForTests(messages) {
|
|
737
|
-
return extractBlockedReportFromMessages(messages);
|
|
738
|
-
}
|
|
739
|
-
function extractBlockedReportFromText(text) {
|
|
740
|
-
const trimmed = typeof text === 'string' ? text.trim() : '';
|
|
741
|
-
if (!trimmed) {
|
|
742
|
-
return null;
|
|
743
|
-
}
|
|
744
|
-
const candidates = [];
|
|
745
|
-
const pushCandidate = (candidate) => {
|
|
746
|
-
const normalized = candidate.trim();
|
|
747
|
-
if (!normalized || normalized.length > STOP_MESSAGE_BLOCKED_CANDIDATE_MAX_LENGTH) {
|
|
748
|
-
return;
|
|
749
|
-
}
|
|
750
|
-
if (candidates.includes(normalized)) {
|
|
751
|
-
return;
|
|
752
|
-
}
|
|
753
|
-
candidates.push(normalized);
|
|
754
|
-
};
|
|
755
|
-
pushCandidate(trimmed);
|
|
756
|
-
for (const codeBlock of extractJsonCodeBlocks(trimmed)) {
|
|
757
|
-
pushCandidate(codeBlock);
|
|
758
|
-
}
|
|
759
|
-
for (const objectText of extractBalancedJsonObjectStrings(trimmed)) {
|
|
760
|
-
if (objectText.includes('"type"') && objectText.toLowerCase().includes('"blocked"')) {
|
|
761
|
-
pushCandidate(objectText);
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
for (const candidate of candidates) {
|
|
765
|
-
let parsed;
|
|
766
|
-
try {
|
|
767
|
-
parsed = JSON.parse(candidate);
|
|
768
|
-
}
|
|
769
|
-
catch {
|
|
770
|
-
continue;
|
|
771
|
-
}
|
|
772
|
-
const report = normalizeBlockedReport(parsed);
|
|
773
|
-
if (report) {
|
|
774
|
-
return report;
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
return null;
|
|
778
|
-
}
|
|
779
|
-
function normalizeBlockedReport(value) {
|
|
780
|
-
if (Array.isArray(value)) {
|
|
781
|
-
for (const entry of value) {
|
|
782
|
-
const report = normalizeBlockedReport(entry);
|
|
783
|
-
if (report) {
|
|
784
|
-
return report;
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
return null;
|
|
788
|
-
}
|
|
789
|
-
if (!value || typeof value !== 'object') {
|
|
790
|
-
return null;
|
|
791
|
-
}
|
|
792
|
-
const record = value;
|
|
793
|
-
const type = toNonEmptyText(record.type).toLowerCase();
|
|
794
|
-
if (type !== 'blocked') {
|
|
795
|
-
return null;
|
|
796
|
-
}
|
|
797
|
-
const summary = toNonEmptyText(record.summary) ||
|
|
798
|
-
toNonEmptyText(record.title) ||
|
|
799
|
-
toNonEmptyText(record.problem);
|
|
800
|
-
const blocker = toNonEmptyText(record.blocker) ||
|
|
801
|
-
toNonEmptyText(record.reason) ||
|
|
802
|
-
toNonEmptyText(record.blocked_by);
|
|
803
|
-
if (!summary || !blocker) {
|
|
804
|
-
return null;
|
|
805
|
-
}
|
|
806
|
-
const impact = toNonEmptyText(record.impact) || toNonEmptyText(record.effect);
|
|
807
|
-
const nextAction = toNonEmptyText(record.next_action) ||
|
|
808
|
-
toNonEmptyText(record.nextAction) ||
|
|
809
|
-
toNonEmptyText(record.next_step);
|
|
810
|
-
const evidence = normalizeBlockedEvidence(record.evidence);
|
|
811
|
-
return {
|
|
812
|
-
summary: summary.slice(0, 1_000),
|
|
813
|
-
blocker: blocker.slice(0, 1_000),
|
|
814
|
-
...(impact ? { impact: impact.slice(0, 1_000) } : {}),
|
|
815
|
-
...(nextAction ? { nextAction: nextAction.slice(0, 1_000) } : {}),
|
|
816
|
-
evidence
|
|
817
|
-
};
|
|
818
|
-
}
|
|
819
|
-
function normalizeBlockedEvidence(raw) {
|
|
820
|
-
if (Array.isArray(raw)) {
|
|
821
|
-
const normalized = raw
|
|
822
|
-
.map((entry) => toNonEmptyText(entry))
|
|
823
|
-
.filter((entry) => entry.length > 0)
|
|
824
|
-
.map((entry) => entry.slice(0, 800));
|
|
825
|
-
return normalized.slice(0, 8);
|
|
826
|
-
}
|
|
827
|
-
const single = toNonEmptyText(raw);
|
|
828
|
-
return single ? [single.slice(0, 800)] : [];
|
|
829
|
-
}
|
|
830
|
-
function extractCapturedMessageText(message) {
|
|
831
|
-
if (!message) {
|
|
832
|
-
return '';
|
|
833
|
-
}
|
|
834
|
-
if (typeof message === 'string') {
|
|
835
|
-
return message.trim();
|
|
836
|
-
}
|
|
837
|
-
if (typeof message !== 'object' || Array.isArray(message)) {
|
|
838
|
-
return '';
|
|
839
|
-
}
|
|
840
|
-
const record = message;
|
|
841
|
-
const contentText = extractTextFromMessageContent(record.content);
|
|
842
|
-
if (contentText) {
|
|
843
|
-
return contentText;
|
|
844
|
-
}
|
|
845
|
-
const inputText = extractTextFromMessageContent(record.input);
|
|
846
|
-
if (inputText) {
|
|
847
|
-
return inputText;
|
|
848
|
-
}
|
|
849
|
-
const outputText = extractTextFromMessageContent(record.output);
|
|
850
|
-
if (outputText) {
|
|
851
|
-
return outputText;
|
|
852
|
-
}
|
|
853
|
-
const argumentsText = toNonEmptyText(record.arguments);
|
|
854
|
-
return argumentsText;
|
|
855
|
-
}
|
|
856
|
-
function extractTextFromMessageContent(content) {
|
|
857
|
-
if (typeof content === 'string') {
|
|
858
|
-
return content.trim();
|
|
859
|
-
}
|
|
860
|
-
if (!Array.isArray(content)) {
|
|
861
|
-
return '';
|
|
862
|
-
}
|
|
863
|
-
const chunks = [];
|
|
864
|
-
for (const item of content) {
|
|
865
|
-
if (typeof item === 'string') {
|
|
866
|
-
if (item.trim())
|
|
867
|
-
chunks.push(item.trim());
|
|
868
|
-
continue;
|
|
869
|
-
}
|
|
870
|
-
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
871
|
-
continue;
|
|
872
|
-
}
|
|
873
|
-
const record = item;
|
|
874
|
-
const type = toNonEmptyText(record.type).toLowerCase();
|
|
875
|
-
if (type === 'text' || type === 'output_text' || type === 'input_text' || !type) {
|
|
876
|
-
const text = toNonEmptyText(record.text);
|
|
877
|
-
if (text) {
|
|
878
|
-
chunks.push(text);
|
|
879
|
-
}
|
|
880
|
-
continue;
|
|
881
|
-
}
|
|
882
|
-
const fallbackText = toNonEmptyText(record.content) || toNonEmptyText(record.value);
|
|
883
|
-
if (fallbackText) {
|
|
884
|
-
chunks.push(fallbackText);
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
return chunks.join('\n').trim();
|
|
888
|
-
}
|
|
889
|
-
function extractJsonCodeBlocks(text) {
|
|
890
|
-
const candidates = [];
|
|
891
|
-
const regex = /```(?:json)?\s*([\s\S]*?)```/gi;
|
|
892
|
-
let match;
|
|
893
|
-
while ((match = regex.exec(text)) !== null) {
|
|
894
|
-
const body = (match[1] || '').trim();
|
|
895
|
-
if (!body) {
|
|
896
|
-
continue;
|
|
897
|
-
}
|
|
898
|
-
candidates.push(body);
|
|
899
|
-
}
|
|
900
|
-
return candidates;
|
|
901
|
-
}
|
|
902
|
-
function extractBalancedJsonObjectStrings(text) {
|
|
903
|
-
const results = [];
|
|
904
|
-
let start = -1;
|
|
905
|
-
let depth = 0;
|
|
906
|
-
let inString = false;
|
|
907
|
-
let escaped = false;
|
|
908
|
-
for (let idx = 0; idx < text.length; idx += 1) {
|
|
909
|
-
const ch = text[idx];
|
|
910
|
-
if (inString) {
|
|
911
|
-
if (escaped) {
|
|
912
|
-
escaped = false;
|
|
913
|
-
}
|
|
914
|
-
else if (ch === '\\') {
|
|
915
|
-
escaped = true;
|
|
916
|
-
}
|
|
917
|
-
else if (ch === '"') {
|
|
918
|
-
inString = false;
|
|
919
|
-
}
|
|
920
|
-
continue;
|
|
921
|
-
}
|
|
922
|
-
if (ch === '"') {
|
|
923
|
-
inString = true;
|
|
924
|
-
continue;
|
|
925
|
-
}
|
|
926
|
-
if (ch === '{') {
|
|
927
|
-
if (depth === 0) {
|
|
928
|
-
start = idx;
|
|
929
|
-
}
|
|
930
|
-
depth += 1;
|
|
931
|
-
continue;
|
|
932
|
-
}
|
|
933
|
-
if (ch === '}') {
|
|
934
|
-
if (depth <= 0) {
|
|
935
|
-
continue;
|
|
936
|
-
}
|
|
937
|
-
depth -= 1;
|
|
938
|
-
if (depth === 0 && start >= 0) {
|
|
939
|
-
results.push(text.slice(start, idx + 1));
|
|
940
|
-
start = -1;
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
return results;
|
|
945
|
-
}
|
|
946
|
-
function createBdIssueFromBlockedReport(blockedReport, context, cwdOverride) {
|
|
947
|
-
const cwd = resolveBdWorkingDirectoryForStopMessage(cwdOverride);
|
|
948
|
-
const title = buildBlockedIssueTitle(blockedReport);
|
|
949
|
-
const description = buildBlockedIssueDescription(blockedReport, context);
|
|
950
|
-
const acceptance = buildBlockedIssueAcceptance(blockedReport);
|
|
951
|
-
try {
|
|
952
|
-
const result = childProcess.spawnSync('bd', [
|
|
953
|
-
'--no-db',
|
|
954
|
-
'create',
|
|
955
|
-
'--json',
|
|
956
|
-
'-t',
|
|
957
|
-
'bug',
|
|
958
|
-
'-p',
|
|
959
|
-
'0',
|
|
960
|
-
'--title',
|
|
961
|
-
title,
|
|
962
|
-
'--description',
|
|
963
|
-
description,
|
|
964
|
-
'--acceptance',
|
|
965
|
-
acceptance
|
|
966
|
-
], {
|
|
967
|
-
cwd,
|
|
968
|
-
encoding: 'utf8',
|
|
969
|
-
timeout: STOP_MESSAGE_BD_CREATE_TIMEOUT_MS,
|
|
970
|
-
maxBuffer: 1024 * 1024
|
|
971
|
-
});
|
|
972
|
-
if (result.error || result.status !== 0) {
|
|
973
|
-
return null;
|
|
974
|
-
}
|
|
975
|
-
return parseCreatedIssueId(result.stdout);
|
|
976
|
-
}
|
|
977
|
-
catch {
|
|
978
|
-
return null;
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
function resolveBdWorkingDirectoryForStopMessage(cwdOverride) {
|
|
982
|
-
const fromOverride = toNonEmptyText(cwdOverride);
|
|
983
|
-
if (fromOverride) {
|
|
984
|
-
return path.resolve(fromOverride);
|
|
985
|
-
}
|
|
986
|
-
const fromEnv = toNonEmptyText(process.env.ROUTECODEX_STOPMESSAGE_BD_WORKDIR);
|
|
987
|
-
if (fromEnv) {
|
|
988
|
-
return path.resolve(fromEnv);
|
|
989
|
-
}
|
|
990
|
-
const current = process.cwd();
|
|
991
|
-
return findBdProjectRootForStopMessage(current) || current;
|
|
992
|
-
}
|
|
993
|
-
function findBdProjectRootForStopMessage(startDirectory) {
|
|
994
|
-
let current = path.resolve(startDirectory);
|
|
995
|
-
while (true) {
|
|
996
|
-
const beadsDir = path.join(current, '.beads');
|
|
997
|
-
const issuesFile = path.join(beadsDir, 'issues.jsonl');
|
|
998
|
-
if (fs.existsSync(issuesFile) || fs.existsSync(beadsDir)) {
|
|
999
|
-
return current;
|
|
1000
|
-
}
|
|
1001
|
-
const parent = path.dirname(current);
|
|
1002
|
-
if (parent === current) {
|
|
1003
|
-
return null;
|
|
1004
|
-
}
|
|
1005
|
-
current = parent;
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
function buildBlockedIssueTitle(report) {
|
|
1009
|
-
const summary = report.summary.replace(/\s+/g, ' ').trim();
|
|
1010
|
-
const title = summary.length > 96 ? `${summary.slice(0, 96)}...` : summary;
|
|
1011
|
-
return `stopMessage blocked: ${title}`;
|
|
1012
|
-
}
|
|
1013
|
-
function buildBlockedIssueDescription(report, context) {
|
|
1014
|
-
const lines = [
|
|
1015
|
-
'自动建单来源:stop_message_auto 结构化 blocked 报告',
|
|
1016
|
-
'',
|
|
1017
|
-
`Summary: ${report.summary}`,
|
|
1018
|
-
`Blocker: ${report.blocker}`,
|
|
1019
|
-
`Impact: ${report.impact || 'n/a'}`,
|
|
1020
|
-
`Next Action: ${report.nextAction || 'n/a'}`,
|
|
1021
|
-
'',
|
|
1022
|
-
'Evidence:'
|
|
1023
|
-
];
|
|
1024
|
-
if (report.evidence.length > 0) {
|
|
1025
|
-
for (const evidence of report.evidence) {
|
|
1026
|
-
lines.push(`- ${evidence}`);
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
else {
|
|
1030
|
-
lines.push('- n/a');
|
|
1031
|
-
}
|
|
1032
|
-
lines.push('', 'Context:');
|
|
1033
|
-
lines.push(`- requestId: ${context?.requestId || 'n/a'}`);
|
|
1034
|
-
lines.push(`- sessionId: ${context?.sessionId || 'n/a'}`);
|
|
1035
|
-
lines.push(`- createdAt: ${new Date().toISOString()}`);
|
|
1036
|
-
return lines.join('\n').trim();
|
|
1037
|
-
}
|
|
1038
|
-
function buildBlockedIssueAcceptance(report) {
|
|
1039
|
-
const summary = report.summary.replace(/\s+/g, ' ').trim();
|
|
1040
|
-
return [
|
|
1041
|
-
`1) 明确阻塞原因并给出可执行解法(${summary.slice(0, 80)})`,
|
|
1042
|
-
'2) 完成至少一条可验证动作(实现/测试/配置)并记录证据'
|
|
1043
|
-
].join(';');
|
|
1044
|
-
}
|
|
1045
|
-
function parseCreatedIssueId(stdout) {
|
|
1046
|
-
const output = typeof stdout === 'string' ? stdout.trim() : '';
|
|
1047
|
-
if (!output) {
|
|
1048
|
-
return null;
|
|
1049
|
-
}
|
|
1050
|
-
try {
|
|
1051
|
-
const parsed = JSON.parse(output);
|
|
1052
|
-
const id = toNonEmptyText(parsed.id);
|
|
1053
|
-
if (id) {
|
|
1054
|
-
return id;
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
catch {
|
|
1058
|
-
// ignore parse failures and continue with regex fallback.
|
|
1059
|
-
}
|
|
1060
|
-
const match = output.match(/\b(?:routecodex|bd)-[A-Za-z0-9._-]+\b/);
|
|
1061
|
-
return match ? match[0] : null;
|
|
1062
|
-
}
|
|
1063
|
-
function createStopMessageState(snapshot) {
|
|
1064
|
-
return {
|
|
1065
|
-
forcedTarget: undefined,
|
|
1066
|
-
stickyTarget: undefined,
|
|
1067
|
-
allowedProviders: new Set(),
|
|
1068
|
-
disabledProviders: new Set(),
|
|
1069
|
-
disabledKeys: new Map(),
|
|
1070
|
-
disabledModels: new Map(),
|
|
1071
|
-
stopMessageSource: snapshot.source && snapshot.source.trim() ? snapshot.source.trim() : 'explicit',
|
|
1072
|
-
stopMessageText: snapshot.text,
|
|
1073
|
-
stopMessageMaxRepeats: snapshot.maxRepeats,
|
|
1074
|
-
stopMessageUsed: snapshot.used,
|
|
1075
|
-
stopMessageUpdatedAt: snapshot.updatedAt,
|
|
1076
|
-
stopMessageLastUsedAt: snapshot.lastUsedAt,
|
|
1077
|
-
stopMessageStage: snapshot.stage,
|
|
1078
|
-
stopMessageStageMode: snapshot.stageMode,
|
|
1079
|
-
stopMessageObservationHash: snapshot.observationHash,
|
|
1080
|
-
stopMessageObservationStableCount: snapshot.observationStableCount,
|
|
1081
|
-
stopMessageBdWorkState: snapshot.bdWorkState,
|
|
1082
|
-
stopMessageAssignedIssueId: snapshot.assignedIssueId,
|
|
1083
|
-
stopMessageAssignedIssueSource: snapshot.assignedIssueSource,
|
|
1084
|
-
stopMessageNoTaskSummaryUsed: snapshot.noTaskSummaryUsed
|
|
1085
|
-
};
|
|
1086
|
-
}
|
|
1087
|
-
function normalizeStopMessageStageMode(value) {
|
|
1088
|
-
if (typeof value !== 'string') {
|
|
1089
|
-
return undefined;
|
|
1090
|
-
}
|
|
1091
|
-
const normalized = value.trim().toLowerCase();
|
|
1092
|
-
if (normalized === 'on' || normalized === 'off' || normalized === 'auto') {
|
|
1093
|
-
return normalized;
|
|
1094
|
-
}
|
|
1095
|
-
return undefined;
|
|
1096
|
-
}
|
|
1097
|
-
function resolveStopMessageMaxRepeats(value, stageMode) {
|
|
1098
|
-
const parsed = typeof value === 'number' && Number.isFinite(value)
|
|
1099
|
-
? Math.floor(value)
|
|
1100
|
-
: 0;
|
|
1101
|
-
if (parsed > 0) {
|
|
1102
|
-
return parsed;
|
|
1103
|
-
}
|
|
1104
|
-
if (stageMode === 'on' || stageMode === 'auto') {
|
|
1105
|
-
return DEFAULT_STOP_MESSAGE_MAX_REPEATS;
|
|
1106
|
-
}
|
|
1107
|
-
return 0;
|
|
1108
|
-
}
|
|
1109
|
-
function clearStopMessageState(state, now) {
|
|
1110
|
-
state.stopMessageText = undefined;
|
|
1111
|
-
state.stopMessageMaxRepeats = undefined;
|
|
1112
|
-
state.stopMessageUsed = undefined;
|
|
1113
|
-
state.stopMessageSource = undefined;
|
|
1114
|
-
state.stopMessageStage = undefined;
|
|
1115
|
-
state.stopMessageStageMode = undefined;
|
|
1116
|
-
state.stopMessageObservationHash = undefined;
|
|
1117
|
-
state.stopMessageObservationStableCount = undefined;
|
|
1118
|
-
state.stopMessageBdWorkState = undefined;
|
|
1119
|
-
state.stopMessageAssignedIssueId = undefined;
|
|
1120
|
-
state.stopMessageAssignedIssueSource = undefined;
|
|
1121
|
-
state.stopMessageNoTaskSummaryUsed = undefined;
|
|
1122
|
-
state.stopMessageUpdatedAt = now;
|
|
1123
|
-
state.stopMessageLastUsedAt = now;
|
|
1124
|
-
}
|
|
1125
|
-
function resolveEntryEndpoint(record) {
|
|
1126
|
-
const raw = typeof record.entryEndpoint === 'string' && record.entryEndpoint.trim()
|
|
1127
|
-
? record.entryEndpoint.trim()
|
|
1128
|
-
: undefined;
|
|
1129
|
-
if (raw) {
|
|
1130
|
-
return raw;
|
|
1131
|
-
}
|
|
1132
|
-
const metaEntry = record.metadata && typeof record.metadata === 'object' && record.metadata.entryEndpoint;
|
|
1133
|
-
if (typeof metaEntry === 'string' && metaEntry.trim()) {
|
|
1134
|
-
return metaEntry.trim();
|
|
1135
|
-
}
|
|
1136
|
-
return '/v1/chat/completions';
|
|
1137
|
-
}
|