@jsonstudio/llms 0.6.1892 → 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.
Files changed (159) hide show
  1. package/dist/conversion/compat/actions/deepseek-web-request.js +16 -2
  2. package/dist/conversion/compat/actions/deepseek-web-response.d.ts +7 -1
  3. package/dist/conversion/compat/actions/deepseek-web-response.js +302 -40
  4. package/dist/conversion/compat/actions/harvest-tool-calls-from-text.d.ts +5 -0
  5. package/dist/conversion/compat/actions/harvest-tool-calls-from-text.js +7 -4
  6. package/dist/conversion/compat/actions/iflow-tool-text-fallback.d.ts +1 -0
  7. package/dist/conversion/compat/actions/iflow-tool-text-fallback.js +12 -0
  8. package/dist/conversion/compat/actions/strip-orphan-function-calls-tag.js +1 -1
  9. package/dist/conversion/compat/actions/tool-text-request-guidance.d.ts +9 -0
  10. package/dist/conversion/compat/actions/tool-text-request-guidance.js +177 -0
  11. package/dist/conversion/compat/antigravity-session-signature.d.ts +6 -0
  12. package/dist/conversion/compat/antigravity-session-signature.js +15 -0
  13. package/dist/conversion/compat/profiles/chat-deepseek-web.json +52 -1
  14. package/dist/conversion/compat/profiles/chat-glm.json +22 -0
  15. package/dist/conversion/compat/profiles/chat-iflow.json +4 -0
  16. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +13 -27
  17. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +10 -1
  18. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +13 -4
  19. package/dist/conversion/hub/pipeline/compat/compat-profile-resolver.js +1 -53
  20. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +8 -0
  21. package/dist/conversion/hub/pipeline/hub-pipeline.js +8 -4
  22. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +191 -9
  23. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +118 -15
  24. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +65 -2
  25. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage3_servertool_orchestration/index.d.ts +34 -0
  26. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage3_servertool_orchestration/index.js +75 -0
  27. package/dist/conversion/hub/process/chat-process.js +85 -18
  28. package/dist/conversion/hub/response/provider-response.js +21 -50
  29. package/dist/conversion/hub/response/response-runtime.js +71 -10
  30. package/dist/conversion/responses/responses-openai-bridge/response-payload.d.ts +3 -0
  31. package/dist/conversion/responses/responses-openai-bridge/response-payload.js +576 -0
  32. package/dist/conversion/responses/responses-openai-bridge/types.d.ts +42 -0
  33. package/dist/conversion/responses/responses-openai-bridge/types.js +1 -0
  34. package/dist/conversion/responses/responses-openai-bridge.d.ts +3 -44
  35. package/dist/conversion/responses/responses-openai-bridge.js +193 -504
  36. package/dist/conversion/shared/anthropic-message-utils.js +82 -2
  37. package/dist/conversion/shared/bridge-message-utils.js +92 -39
  38. package/dist/conversion/shared/snapshot-hooks.js +8 -13
  39. package/dist/conversion/shared/text-markup-normalizer/extractors-apply-patch.d.ts +2 -0
  40. package/dist/conversion/shared/text-markup-normalizer/extractors-apply-patch.js +129 -0
  41. package/dist/conversion/shared/text-markup-normalizer/extractors-json.d.ts +4 -0
  42. package/dist/conversion/shared/text-markup-normalizer/extractors-json.js +637 -0
  43. package/dist/conversion/shared/text-markup-normalizer/extractors-shared.d.ts +21 -0
  44. package/dist/conversion/shared/text-markup-normalizer/extractors-shared.js +177 -0
  45. package/dist/conversion/shared/text-markup-normalizer/extractors-transcript.d.ts +5 -0
  46. package/dist/conversion/shared/text-markup-normalizer/extractors-transcript.js +385 -0
  47. package/dist/conversion/shared/text-markup-normalizer/extractors-xml.d.ts +10 -0
  48. package/dist/conversion/shared/text-markup-normalizer/extractors-xml.js +602 -0
  49. package/dist/conversion/shared/text-markup-normalizer/extractors.d.ts +5 -0
  50. package/dist/conversion/shared/text-markup-normalizer/extractors.js +4 -0
  51. package/dist/conversion/shared/text-markup-normalizer/normalize.d.ts +2 -0
  52. package/dist/conversion/shared/text-markup-normalizer/normalize.js +76 -0
  53. package/dist/conversion/shared/text-markup-normalizer.d.ts +3 -25
  54. package/dist/conversion/shared/text-markup-normalizer.js +2 -1386
  55. package/dist/conversion/shared/tool-governor.js +136 -10
  56. package/dist/filters/utils/snapshot-writer.js +3 -3
  57. package/dist/router/virtual-router/bootstrap/auth-utils.d.ts +6 -0
  58. package/dist/router/virtual-router/bootstrap/auth-utils.js +288 -0
  59. package/dist/router/virtual-router/bootstrap/claude-code-helpers.d.ts +11 -0
  60. package/dist/router/virtual-router/bootstrap/claude-code-helpers.js +18 -0
  61. package/dist/router/virtual-router/bootstrap/config-defaults.d.ts +5 -0
  62. package/dist/router/virtual-router/bootstrap/config-defaults.js +13 -0
  63. package/dist/router/virtual-router/bootstrap/config-normalizers.d.ts +4 -0
  64. package/dist/router/virtual-router/bootstrap/config-normalizers.js +106 -0
  65. package/dist/router/virtual-router/bootstrap/profile-builder.d.ts +7 -0
  66. package/dist/router/virtual-router/bootstrap/profile-builder.js +68 -0
  67. package/dist/router/virtual-router/bootstrap/provider-normalization.d.ts +40 -0
  68. package/dist/router/virtual-router/bootstrap/provider-normalization.js +212 -0
  69. package/dist/router/virtual-router/bootstrap/responses-helpers.d.ts +15 -0
  70. package/dist/router/virtual-router/bootstrap/responses-helpers.js +65 -0
  71. package/dist/router/virtual-router/bootstrap/routing-config.d.ts +23 -0
  72. package/dist/router/virtual-router/bootstrap/routing-config.js +293 -0
  73. package/dist/router/virtual-router/bootstrap/streaming-helpers.d.ts +12 -0
  74. package/dist/router/virtual-router/bootstrap/streaming-helpers.js +128 -0
  75. package/dist/router/virtual-router/bootstrap/utils.d.ts +5 -0
  76. package/dist/router/virtual-router/bootstrap/utils.js +41 -0
  77. package/dist/router/virtual-router/bootstrap/web-search-config.d.ts +4 -0
  78. package/dist/router/virtual-router/bootstrap/web-search-config.js +131 -0
  79. package/dist/router/virtual-router/bootstrap.d.ts +0 -4
  80. package/dist/router/virtual-router/bootstrap.js +31 -1275
  81. package/dist/router/virtual-router/classifier.js +32 -14
  82. package/dist/router/virtual-router/engine/antigravity/alias-lease.js +2 -2
  83. package/dist/router/virtual-router/engine/cooldown-manager.d.ts +34 -0
  84. package/dist/router/virtual-router/engine/cooldown-manager.js +118 -0
  85. package/dist/router/virtual-router/engine/route-analytics.d.ts +28 -0
  86. package/dist/router/virtual-router/engine/route-analytics.js +44 -0
  87. package/dist/router/virtual-router/engine/routing-pools/index.js +165 -4
  88. package/dist/router/virtual-router/engine/sticky-session-manager.d.ts +29 -0
  89. package/dist/router/virtual-router/engine/sticky-session-manager.js +55 -0
  90. package/dist/router/virtual-router/engine-logging.d.ts +42 -1
  91. package/dist/router/virtual-router/engine-logging.js +82 -15
  92. package/dist/router/virtual-router/engine-selection/multimodal-capability.d.ts +3 -0
  93. package/dist/router/virtual-router/engine-selection/multimodal-capability.js +26 -0
  94. package/dist/router/virtual-router/engine-selection/route-utils.js +6 -2
  95. package/dist/router/virtual-router/engine-selection/selection-deps.d.ts +1 -0
  96. package/dist/router/virtual-router/engine-selection/tier-selection.js +31 -1
  97. package/dist/router/virtual-router/engine.d.ts +21 -7
  98. package/dist/router/virtual-router/engine.js +198 -194
  99. package/dist/router/virtual-router/features.js +12 -4
  100. package/dist/router/virtual-router/message-utils.d.ts +8 -0
  101. package/dist/router/virtual-router/message-utils.js +170 -45
  102. package/dist/router/virtual-router/pre-command-file-resolver.js +40 -2
  103. package/dist/router/virtual-router/routing-instructions.d.ts +8 -0
  104. package/dist/router/virtual-router/routing-instructions.js +18 -2
  105. package/dist/router/virtual-router/routing-stop-message-actions.js +34 -10
  106. package/dist/router/virtual-router/routing-stop-message-state-codec.d.ts +2 -0
  107. package/dist/router/virtual-router/routing-stop-message-state-codec.js +50 -1
  108. package/dist/router/virtual-router/stop-message-state-sync.d.ts +1 -1
  109. package/dist/router/virtual-router/stop-message-state-sync.js +3 -0
  110. package/dist/router/virtual-router/token-counter.js +51 -10
  111. package/dist/router/virtual-router/tool-signals.js +4 -0
  112. package/dist/router/virtual-router/types.d.ts +15 -0
  113. package/dist/servertool/clock/session-scope.d.ts +3 -0
  114. package/dist/servertool/clock/session-scope.js +52 -0
  115. package/dist/servertool/clock/state.js +9 -0
  116. package/dist/servertool/clock/tasks.js +12 -1
  117. package/dist/servertool/clock/types.d.ts +3 -0
  118. package/dist/servertool/engine.js +177 -31
  119. package/dist/servertool/handlers/clock-auto.js +2 -8
  120. package/dist/servertool/handlers/clock.js +6 -9
  121. package/dist/servertool/handlers/recursive-detection-guard.js +53 -14
  122. package/dist/servertool/handlers/stop-message-auto/blocked-report.d.ts +16 -0
  123. package/dist/servertool/handlers/stop-message-auto/blocked-report.js +349 -0
  124. package/dist/servertool/handlers/stop-message-auto/iflow-followup.d.ts +23 -0
  125. package/dist/servertool/handlers/stop-message-auto/iflow-followup.js +503 -0
  126. package/dist/servertool/handlers/stop-message-auto/routing-state.d.ts +38 -0
  127. package/dist/servertool/handlers/stop-message-auto/routing-state.js +149 -0
  128. package/dist/servertool/handlers/stop-message-auto/runtime-utils.d.ts +67 -0
  129. package/dist/servertool/handlers/stop-message-auto/runtime-utils.js +387 -0
  130. package/dist/servertool/handlers/stop-message-auto.d.ts +1 -1
  131. package/dist/servertool/handlers/stop-message-auto.js +80 -556
  132. package/dist/servertool/handlers/stop-message-stage-policy/bd-runtime.d.ts +18 -0
  133. package/dist/servertool/handlers/stop-message-stage-policy/bd-runtime.js +398 -0
  134. package/dist/servertool/handlers/stop-message-stage-policy/decision.d.ts +9 -0
  135. package/dist/servertool/handlers/stop-message-stage-policy/decision.js +127 -0
  136. package/dist/servertool/handlers/stop-message-stage-policy/observation.d.ts +2 -0
  137. package/dist/servertool/handlers/stop-message-stage-policy/observation.js +179 -0
  138. package/dist/servertool/handlers/stop-message-stage-policy/templates.d.ts +4 -0
  139. package/dist/servertool/handlers/stop-message-stage-policy/templates.js +96 -0
  140. package/dist/servertool/handlers/stop-message-stage-policy/text-utils.d.ts +9 -0
  141. package/dist/servertool/handlers/stop-message-stage-policy/text-utils.js +89 -0
  142. package/dist/servertool/handlers/stop-message-stage-policy/types.d.ts +59 -0
  143. package/dist/servertool/handlers/stop-message-stage-policy/types.js +1 -0
  144. package/dist/servertool/handlers/stop-message-stage-policy.d.ts +3 -43
  145. package/dist/servertool/handlers/stop-message-stage-policy.js +2 -684
  146. package/dist/servertool/handlers/web-search.js +117 -0
  147. package/dist/servertool/server-side-tools.d.ts +0 -1
  148. package/dist/servertool/server-side-tools.js +4 -3
  149. package/dist/sse/sse-to-json/builders/response-builder.js +16 -0
  150. package/dist/sse/sse-to-json/chat-sse-to-json-converter.d.ts +1 -0
  151. package/dist/sse/sse-to-json/chat-sse-to-json-converter.js +110 -37
  152. package/dist/telemetry/stats-center.d.ts +9 -0
  153. package/dist/telemetry/stats-center.js +29 -1
  154. package/dist/tools/apply-patch/structured/coercion.js +3 -11
  155. package/dist/tools/exec-command/validator.d.ts +1 -0
  156. package/dist/tools/exec-command/validator.js +132 -0
  157. package/dist/tools/tool-registry.d.ts +1 -0
  158. package/dist/tools/tool-registry.js +1 -1
  159. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { detectExtendedThinkingKeyword, detectImageAttachment, detectKeyword, extractMessageText, getLatestMessageRole, getLatestUserMessage } from './message-utils.js';
1
+ import { analyzeMediaAttachments, detectExtendedThinkingKeyword, detectKeyword, extractMessageText, getLatestMessageRole, } from './message-utils.js';
2
2
  import { extractAntigravityGeminiSessionId } from '../../conversion/compat/antigravity-session-signature.js';
3
3
  import { detectCodingTool, detectLastAssistantToolCategory, detectVisionTool, detectWebSearchToolDeclared, detectWebTool, extractMeaningfulDeclaredToolNames } from './tool-signals.js';
4
4
  import { computeRequestTokens } from './token-estimator.js';
@@ -18,10 +18,14 @@ export function buildRoutingFeatures(request, metadata) {
18
18
  return undefined;
19
19
  }
20
20
  })();
21
- const latestUserMessage = getLatestUserMessage(request.messages);
22
21
  const latestMessageRole = getLatestMessageRole(request.messages);
22
+ const latestMessage = Array.isArray(request.messages) && request.messages.length
23
+ ? request.messages[request.messages.length - 1]
24
+ : undefined;
23
25
  const assistantMessages = request.messages.filter((msg) => msg.role === 'assistant');
24
- const latestUserText = latestUserMessage ? extractMessageText(latestUserMessage) : '';
26
+ const latestUserText = latestMessageRole === 'user' && latestMessage
27
+ ? extractMessageText(latestMessage)
28
+ : '';
25
29
  const normalizedUserText = latestUserText.toLowerCase();
26
30
  const meaningfulDeclaredTools = extractMeaningfulDeclaredToolNames(request.tools);
27
31
  const hasTools = meaningfulDeclaredTools.length > 0;
@@ -31,7 +35,8 @@ export function buildRoutingFeatures(request, metadata) {
31
35
  const hasVisionTool = detectVisionTool(request);
32
36
  // Vision routing must only trigger for the current user turn (latest message),
33
37
  // not for historical user messages carrying images during tool/assistant followups.
34
- const hasImageAttachment = latestMessageRole === 'user' && detectImageAttachment(latestUserMessage);
38
+ const mediaSignals = latestMessageRole === 'user' ? analyzeMediaAttachments(latestMessage) : analyzeMediaAttachments(undefined);
39
+ const hasImageAttachment = mediaSignals.hasAnyMedia;
35
40
  const hasCodingTool = detectCodingTool(request);
36
41
  const hasWebTool = detectWebTool(request);
37
42
  const hasThinkingKeyword = hasThinking || detectExtendedThinkingKeyword(normalizedUserText);
@@ -58,6 +63,9 @@ export function buildRoutingFeatures(request, metadata) {
58
63
  hasToolCallResponses,
59
64
  hasVisionTool,
60
65
  hasImageAttachment,
66
+ hasVideoAttachment: mediaSignals.hasVideo,
67
+ hasRemoteVideoAttachment: mediaSignals.hasRemoteVideo,
68
+ hasLocalVideoAttachment: mediaSignals.hasLocalVideo,
61
69
  hasWebTool,
62
70
  hasWebSearchToolDeclared: detectWebSearchToolDeclared(request),
63
71
  hasCodingTool,
@@ -4,4 +4,12 @@ export declare function getLatestMessageRole(messages: StandardizedMessage[]): s
4
4
  export declare function extractMessageText(message: StandardizedMessage): string;
5
5
  export declare function detectKeyword(text: string, keywords: string[]): boolean;
6
6
  export declare function detectExtendedThinkingKeyword(text: string): boolean;
7
+ export interface MediaAttachmentSignals {
8
+ hasAnyMedia: boolean;
9
+ hasImage: boolean;
10
+ hasVideo: boolean;
11
+ hasRemoteVideo: boolean;
12
+ hasLocalVideo: boolean;
13
+ }
14
+ export declare function analyzeMediaAttachments(message: StandardizedMessage | undefined): MediaAttachmentSignals;
7
15
  export declare function detectImageAttachment(message: StandardizedMessage | undefined): boolean;
@@ -1,3 +1,4 @@
1
+ import { isIP } from 'node:net';
1
2
  export function getLatestUserMessage(messages) {
2
3
  for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
3
4
  if (messages[idx]?.role === 'user') {
@@ -56,55 +57,179 @@ export function detectExtendedThinkingKeyword(text) {
56
57
  const keywords = ['仔细分析', '思考', '超级思考', '深度思考', 'careful analysis', 'deep thinking', 'deliberate'];
57
58
  return keywords.some((keyword) => text.includes(keyword));
58
59
  }
59
- export function detectImageAttachment(message) {
60
- if (!message)
60
+ const LOCAL_URL_SCHEMES = ['data:', 'file:', 'blob:'];
61
+ function extractMediaUrlCandidate(record) {
62
+ if (typeof record.image_url === 'string') {
63
+ return record.image_url ?? '';
64
+ }
65
+ if (typeof record.video_url === 'string') {
66
+ return record.video_url ?? '';
67
+ }
68
+ if (record.image_url &&
69
+ typeof record.image_url?.url === 'string') {
70
+ return record.image_url?.url ?? '';
71
+ }
72
+ if (record.video_url &&
73
+ typeof record.video_url?.url === 'string') {
74
+ return record.video_url?.url ?? '';
75
+ }
76
+ if (typeof record.url === 'string') {
77
+ return record.url ?? '';
78
+ }
79
+ if (typeof record.uri === 'string') {
80
+ return record.uri ?? '';
81
+ }
82
+ if (typeof record.data === 'string') {
83
+ return record.data ?? '';
84
+ }
85
+ return '';
86
+ }
87
+ function resolveMediaKind(typeValue, record) {
88
+ if (typeValue.includes('video')) {
89
+ return 'video';
90
+ }
91
+ if (typeValue.includes('image')) {
92
+ return 'image';
93
+ }
94
+ if (Object.prototype.hasOwnProperty.call(record, 'video_url')) {
95
+ return 'video';
96
+ }
97
+ if (Object.prototype.hasOwnProperty.call(record, 'image_url')) {
98
+ return 'image';
99
+ }
100
+ return null;
101
+ }
102
+ function isPrivateHost(host) {
103
+ const normalized = host.trim().toLowerCase();
104
+ if (!normalized) {
105
+ return true;
106
+ }
107
+ if (normalized === 'localhost' || normalized.endsWith('.local')) {
108
+ return true;
109
+ }
110
+ const ipType = isIP(normalized);
111
+ if (ipType === 4) {
112
+ const octets = normalized.split('.').map((part) => Number.parseInt(part, 10));
113
+ if (octets.length !== 4 || octets.some((value) => !Number.isFinite(value))) {
114
+ return true;
115
+ }
116
+ if (octets[0] === 10)
117
+ return true;
118
+ if (octets[0] === 127)
119
+ return true;
120
+ if (octets[0] === 0)
121
+ return true;
122
+ if (octets[0] === 169 && octets[1] === 254)
123
+ return true;
124
+ if (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31)
125
+ return true;
126
+ if (octets[0] === 192 && octets[1] === 168)
127
+ return true;
61
128
  return false;
62
- // 仅基于标准 Chat 语义判断是否携带视觉媒体(图片/视频):
63
- // - content 为数组时查找 { type: 'image' | 'image_url' | 'input_image' | 'video' | 'video_url' | 'input_video', ... } 块;
64
- // - 不再依赖 metadata.attachments,也不再用纯文本关键字或剪贴板标记作为信号。
65
- if (Array.isArray(message.content)) {
66
- for (const part of message.content) {
67
- if (!part || typeof part !== 'object') {
68
- continue;
69
- }
70
- const record = part;
71
- const typeValue = typeof record.type === 'string' ? record.type.toLowerCase() : '';
72
- // For chat/standardized content, media may appear as:
73
- // - { type: "image_url", image_url: { url } }
74
- // - { type: "image", uri: "...", data: "...", url: "..." }
75
- // - { type: "input_image", image_url: "data:..." }
76
- // - { type: "video_url", video_url: { url } }
77
- // - { type: "video", uri: "...", data: "...", url: "..." }
78
- // - { type: "input_video", video_url: "data:..." }
79
- // Treat any non-empty URL/URI/data on a media-* block as a signal.
80
- let imageCandidate = '';
81
- if (typeof record.image_url === 'string') {
82
- imageCandidate = record.image_url ?? '';
83
- }
84
- else if (typeof record.video_url === 'string') {
85
- imageCandidate = record.video_url ?? '';
86
- }
87
- else if (record.image_url &&
88
- typeof record.image_url?.url === 'string') {
89
- imageCandidate = record.image_url?.url ?? '';
90
- }
91
- else if (record.video_url &&
92
- typeof record.video_url?.url === 'string') {
93
- imageCandidate = record.video_url?.url ?? '';
94
- }
95
- else if (typeof record.url === 'string') {
96
- imageCandidate = record.url ?? '';
97
- }
98
- else if (typeof record.uri === 'string') {
99
- imageCandidate = record.uri ?? '';
129
+ }
130
+ if (ipType === 6) {
131
+ if (normalized === '::1')
132
+ return true;
133
+ if (normalized.startsWith('fc') || normalized.startsWith('fd'))
134
+ return true;
135
+ if (normalized.startsWith('fe80:'))
136
+ return true;
137
+ return false;
138
+ }
139
+ return false;
140
+ }
141
+ function isRemotePublicHttpUrl(raw) {
142
+ const value = raw.trim();
143
+ if (!value) {
144
+ return false;
145
+ }
146
+ const lowered = value.toLowerCase();
147
+ if (LOCAL_URL_SCHEMES.some((prefix) => lowered.startsWith(prefix))) {
148
+ return false;
149
+ }
150
+ let parsed;
151
+ try {
152
+ parsed = new URL(value);
153
+ }
154
+ catch {
155
+ return false;
156
+ }
157
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
158
+ return false;
159
+ }
160
+ return !isPrivateHost(parsed.hostname);
161
+ }
162
+ export function analyzeMediaAttachments(message) {
163
+ const result = {
164
+ hasAnyMedia: false,
165
+ hasImage: false,
166
+ hasVideo: false,
167
+ hasRemoteVideo: false,
168
+ hasLocalVideo: false
169
+ };
170
+ if (!message) {
171
+ return result;
172
+ }
173
+ if (typeof message.content === 'string' && message.content.trim()) {
174
+ const raw = message.content;
175
+ const hasImageBlock = /"type"\s*:\s*"(?:input_)?image(?:_url)?"/iu.test(raw);
176
+ const hasVideoBlock = /"type"\s*:\s*"(?:input_)?video(?:_url)?"/iu.test(raw);
177
+ const hasDataVideo = /data:video\//iu.test(raw);
178
+ const hasRemoteVideo = /https?:\/\/[^\s"'\\]+/iu.test(raw);
179
+ if (hasImageBlock || hasVideoBlock) {
180
+ result.hasAnyMedia = true;
181
+ }
182
+ if (hasImageBlock) {
183
+ result.hasImage = true;
184
+ }
185
+ if (hasVideoBlock) {
186
+ result.hasVideo = true;
187
+ if (hasDataVideo) {
188
+ result.hasLocalVideo = true;
100
189
  }
101
- else if (typeof record.data === 'string') {
102
- imageCandidate = record.data ?? '';
190
+ if (hasRemoteVideo) {
191
+ result.hasRemoteVideo = true;
103
192
  }
104
- if ((typeValue.includes('image') || typeValue.includes('video')) && imageCandidate.trim().length > 0) {
105
- return true;
193
+ if (!hasDataVideo && !hasRemoteVideo) {
194
+ result.hasLocalVideo = true;
106
195
  }
107
196
  }
197
+ if (result.hasAnyMedia) {
198
+ return result;
199
+ }
108
200
  }
109
- return false;
201
+ if (!Array.isArray(message.content)) {
202
+ return result;
203
+ }
204
+ for (const part of message.content) {
205
+ if (!part || typeof part !== 'object') {
206
+ continue;
207
+ }
208
+ const record = part;
209
+ const typeValue = typeof record.type === 'string' ? record.type.toLowerCase() : '';
210
+ const mediaKind = resolveMediaKind(typeValue, record);
211
+ if (!mediaKind) {
212
+ continue;
213
+ }
214
+ const mediaUrl = extractMediaUrlCandidate(record).trim();
215
+ if (!mediaUrl) {
216
+ continue;
217
+ }
218
+ result.hasAnyMedia = true;
219
+ if (mediaKind === 'image') {
220
+ result.hasImage = true;
221
+ continue;
222
+ }
223
+ result.hasVideo = true;
224
+ if (isRemotePublicHttpUrl(mediaUrl)) {
225
+ result.hasRemoteVideo = true;
226
+ }
227
+ else {
228
+ result.hasLocalVideo = true;
229
+ }
230
+ }
231
+ return result;
232
+ }
233
+ export function detectImageAttachment(message) {
234
+ return analyzeMediaAttachments(message).hasAnyMedia;
110
235
  }
@@ -1,6 +1,14 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
  import os from 'node:os';
4
+ const DEFAULT_PRECOMMAND_SCRIPT = 'default.sh';
5
+ const DEFAULT_PRECOMMAND_SCRIPT_CONTENT = [
6
+ '#!/usr/bin/env bash',
7
+ '# RouteCodex default precommand hook (no-op).',
8
+ '# You can edit this file to customize precommand behavior.',
9
+ 'exit 0',
10
+ ''
11
+ ].join('\n');
4
12
  function resolveRoutecodexUserDir() {
5
13
  const override = process.env.ROUTECODEX_USER_DIR;
6
14
  if (override && override.trim()) {
@@ -62,14 +70,44 @@ export function resolvePreCommandScriptPath(raw) {
62
70
  stat = fs.statSync(abs);
63
71
  }
64
72
  catch (err) {
65
- const message = err && typeof err.message === 'string' ? err.message : String(err || 'unknown error');
66
- throw new Error(`precommand: cannot stat ${abs}: ${message}`);
73
+ if (shouldAutoCreateDefaultScript(relToPreCommand, err)) {
74
+ tryCreateDefaultPreCommandScript(base, abs);
75
+ try {
76
+ stat = fs.statSync(abs);
77
+ }
78
+ catch (retryErr) {
79
+ const retryMessage = retryErr && typeof retryErr.message === 'string' ? retryErr.message : String(retryErr || 'unknown error');
80
+ throw new Error(`precommand: cannot stat ${abs}: ${retryMessage}`);
81
+ }
82
+ }
83
+ else {
84
+ const message = err && typeof err.message === 'string' ? err.message : String(err || 'unknown error');
85
+ throw new Error(`precommand: cannot stat ${abs}: ${message}`);
86
+ }
67
87
  }
68
88
  if (!stat.isFile()) {
69
89
  throw new Error(`precommand: not a file: ${abs}`);
70
90
  }
71
91
  return abs;
72
92
  }
93
+ function shouldAutoCreateDefaultScript(relToPreCommand, err) {
94
+ const code = err?.code;
95
+ if (code !== 'ENOENT') {
96
+ return false;
97
+ }
98
+ return relToPreCommand === DEFAULT_PRECOMMAND_SCRIPT;
99
+ }
100
+ function tryCreateDefaultPreCommandScript(baseDir, scriptPath) {
101
+ try {
102
+ fs.mkdirSync(baseDir, { recursive: true });
103
+ if (!fs.existsSync(scriptPath)) {
104
+ fs.writeFileSync(scriptPath, DEFAULT_PRECOMMAND_SCRIPT_CONTENT, { encoding: 'utf8', mode: 0o755 });
105
+ }
106
+ }
107
+ catch {
108
+ // Keep original resolver contract: caller gets a stat error on failure.
109
+ }
110
+ }
73
111
  export function isPreCommandScriptPathAllowed(rawPath) {
74
112
  const scriptPath = typeof rawPath === 'string' ? rawPath.trim() : '';
75
113
  if (!scriptPath) {
@@ -10,6 +10,11 @@ export interface RoutingInstruction {
10
10
  stopMessageText?: string;
11
11
  stopMessageMaxRepeats?: number;
12
12
  stopMessageStageMode?: 'on' | 'off' | 'auto';
13
+ /**
14
+ * True when the instruction is parsed from an older user message
15
+ * (not the latest user turn in current request).
16
+ */
17
+ fromHistoricalUserMessage?: boolean;
13
18
  preCommandScriptPath?: string;
14
19
  }
15
20
  export interface RoutingInstructionState {
@@ -57,6 +62,9 @@ export interface RoutingInstructionState {
57
62
  stopMessageObservationHash?: string;
58
63
  stopMessageObservationStableCount?: number;
59
64
  stopMessageBdWorkState?: string;
65
+ stopMessageAssignedIssueId?: string;
66
+ stopMessageAssignedIssueSource?: 'in_progress' | 'ready' | 'open';
67
+ stopMessageNoTaskSummaryUsed?: boolean;
60
68
  preCommandSource?: string;
61
69
  preCommandScriptPath?: string;
62
70
  preCommandUpdatedAt?: number;
@@ -50,14 +50,23 @@ export function parseRoutingInstructions(messages) {
50
50
  for (const segment of segments) {
51
51
  const parsed = parseSingleInstruction(segment);
52
52
  if (parsed) {
53
+ const fromHistoricalUserMessage = lastUserIndex >= 0 &&
54
+ sanitizedIndex >= 0 &&
55
+ sanitizedIndex !== lastUserIndex;
56
+ const normalizedParsed = fromHistoricalUserMessage &&
57
+ (parsed.type === 'stopMessageSet' ||
58
+ parsed.type === 'stopMessageMode' ||
59
+ parsed.type === 'stopMessageClear')
60
+ ? { ...parsed, fromHistoricalUserMessage: true }
61
+ : parsed;
53
62
  // preCommand 是强副作用命令(执行本地脚本),仅允许从 latest user message 生效。
54
- if ((parsed.type === 'preCommandSet' || parsed.type === 'preCommandClear') &&
63
+ if ((normalizedParsed.type === 'preCommandSet' || normalizedParsed.type === 'preCommandClear') &&
55
64
  lastUserIndex >= 0 &&
56
65
  sanitizedIndex >= 0 &&
57
66
  sanitizedIndex !== lastUserIndex) {
58
67
  continue;
59
68
  }
60
- instructions.push(parsed);
69
+ instructions.push(normalizedParsed);
61
70
  }
62
71
  }
63
72
  }
@@ -371,6 +380,9 @@ export function applyRoutingInstructions(instructions, currentState) {
371
380
  stopMessageObservationHash: currentState.stopMessageObservationHash,
372
381
  stopMessageObservationStableCount: currentState.stopMessageObservationStableCount,
373
382
  stopMessageBdWorkState: currentState.stopMessageBdWorkState,
383
+ stopMessageAssignedIssueId: currentState.stopMessageAssignedIssueId,
384
+ stopMessageAssignedIssueSource: currentState.stopMessageAssignedIssueSource,
385
+ stopMessageNoTaskSummaryUsed: currentState.stopMessageNoTaskSummaryUsed,
374
386
  preCommandSource: currentState.preCommandSource,
375
387
  preCommandScriptPath: currentState.preCommandScriptPath,
376
388
  preCommandUpdatedAt: currentState.preCommandUpdatedAt
@@ -500,6 +512,7 @@ export function applyRoutingInstructions(instructions, currentState) {
500
512
  newState.disabledProviders.clear();
501
513
  newState.disabledKeys.clear();
502
514
  newState.disabledModels.clear();
515
+ applyStopMessageInstructionToState({ type: 'stopMessageClear' }, newState);
503
516
  clearPreCommandState(newState);
504
517
  break;
505
518
  case 'stopMessageSet':
@@ -573,6 +586,9 @@ export function deserializeRoutingInstructionState(data) {
573
586
  stopMessageStageMode: undefined,
574
587
  stopMessageObservationHash: undefined,
575
588
  stopMessageBdWorkState: undefined,
589
+ stopMessageAssignedIssueId: undefined,
590
+ stopMessageAssignedIssueSource: undefined,
591
+ stopMessageNoTaskSummaryUsed: undefined,
576
592
  preCommandSource: undefined,
577
593
  preCommandScriptPath: undefined,
578
594
  preCommandUpdatedAt: undefined
@@ -1,4 +1,4 @@
1
- import { normalizeStopMessageStageMode } from './routing-stop-message-state-codec.js';
1
+ import { DEFAULT_STOP_MESSAGE_MAX_REPEATS, ensureStopMessageModeMaxRepeats, normalizeStopMessageStageMode } from './routing-stop-message-state-codec.js';
2
2
  export function applyStopMessageInstructionToState(instruction, state) {
3
3
  switch (instruction.type) {
4
4
  case 'stopMessageSet': {
@@ -24,13 +24,16 @@ export function applyStopMessageInstructionToState(instruction, state) {
24
24
  : 0;
25
25
  const hasLastUsedAt = typeof state.stopMessageLastUsedAt === 'number' && Number.isFinite(state.stopMessageLastUsedAt);
26
26
  const shouldRearm = !isSameInstruction || used > 0 || hasLastUsedAt;
27
+ const shouldRearmForSource = instruction.fromHistoricalUserMessage
28
+ ? !isSameInstruction
29
+ : shouldRearm;
27
30
  state.stopMessageText = text;
28
31
  state.stopMessageMaxRepeats = maxRepeats;
29
32
  state.stopMessageSource = 'explicit';
30
33
  if (targetMode) {
31
34
  state.stopMessageStageMode = targetMode;
32
35
  }
33
- if (shouldRearm) {
36
+ if (shouldRearmForSource) {
34
37
  resetStopMessageRuntimeState(state, Date.now());
35
38
  }
36
39
  return true;
@@ -40,9 +43,9 @@ export function applyStopMessageInstructionToState(instruction, state) {
40
43
  if (!mode) {
41
44
  return true;
42
45
  }
43
- const maxRepeats = typeof instruction.stopMessageMaxRepeats === 'number' && Number.isFinite(instruction.stopMessageMaxRepeats)
46
+ const explicitMaxRepeats = typeof instruction.stopMessageMaxRepeats === 'number' && Number.isFinite(instruction.stopMessageMaxRepeats)
44
47
  ? Math.floor(instruction.stopMessageMaxRepeats)
45
- : 0;
48
+ : undefined;
46
49
  if (mode === 'off') {
47
50
  clearStopMessageState(state, { keepMode: 'off' });
48
51
  return true;
@@ -50,17 +53,32 @@ export function applyStopMessageInstructionToState(instruction, state) {
50
53
  const previousMode = normalizeStopMessageStageMode(state.stopMessageStageMode);
51
54
  const previousMax = typeof state.stopMessageMaxRepeats === 'number' && Number.isFinite(state.stopMessageMaxRepeats)
52
55
  ? Math.floor(state.stopMessageMaxRepeats)
53
- : 0;
56
+ : undefined;
57
+ const preservedMax = typeof state.stopMessageMaxRepeats === 'number' && Number.isFinite(state.stopMessageMaxRepeats)
58
+ ? Math.floor(state.stopMessageMaxRepeats)
59
+ : undefined;
60
+ const resolvedMax = typeof explicitMaxRepeats === 'number' && explicitMaxRepeats > 0
61
+ ? explicitMaxRepeats
62
+ : typeof preservedMax === 'number' && preservedMax > 0
63
+ ? preservedMax
64
+ : mode === 'on' || mode === 'auto'
65
+ ? DEFAULT_STOP_MESSAGE_MAX_REPEATS
66
+ : undefined;
54
67
  state.stopMessageStageMode = mode;
55
68
  state.stopMessageSource = 'explicit';
56
- if (maxRepeats > 0) {
57
- state.stopMessageMaxRepeats = maxRepeats;
58
- }
69
+ state.stopMessageMaxRepeats = resolvedMax;
70
+ ensureStopMessageModeMaxRepeats(state);
71
+ const nextMax = typeof state.stopMessageMaxRepeats === 'number' && Number.isFinite(state.stopMessageMaxRepeats)
72
+ ? Math.floor(state.stopMessageMaxRepeats)
73
+ : undefined;
59
74
  const shouldRearm = previousMode !== mode ||
60
- (maxRepeats > 0 && previousMax !== maxRepeats) ||
75
+ previousMax !== nextMax ||
61
76
  typeof state.stopMessageUsed !== 'number' ||
62
77
  !Number.isFinite(state.stopMessageUsed);
63
- if (shouldRearm) {
78
+ const shouldRearmForSource = instruction.fromHistoricalUserMessage
79
+ ? (previousMode !== mode || previousMax !== nextMax || typeof state.stopMessageUsed !== 'number' || !Number.isFinite(state.stopMessageUsed))
80
+ : shouldRearm;
81
+ if (shouldRearmForSource) {
64
82
  resetStopMessageRuntimeState(state, Date.now());
65
83
  }
66
84
  return true;
@@ -80,6 +98,9 @@ function resetStopMessageRuntimeState(state, atMs) {
80
98
  state.stopMessageObservationHash = undefined;
81
99
  state.stopMessageObservationStableCount = 0;
82
100
  state.stopMessageBdWorkState = undefined;
101
+ state.stopMessageAssignedIssueId = undefined;
102
+ state.stopMessageAssignedIssueSource = undefined;
103
+ state.stopMessageNoTaskSummaryUsed = undefined;
83
104
  }
84
105
  function clearStopMessageState(state, options) {
85
106
  state.stopMessageText = undefined;
@@ -93,4 +114,7 @@ function clearStopMessageState(state, options) {
93
114
  state.stopMessageObservationHash = undefined;
94
115
  state.stopMessageObservationStableCount = undefined;
95
116
  state.stopMessageBdWorkState = undefined;
117
+ state.stopMessageAssignedIssueId = undefined;
118
+ state.stopMessageAssignedIssueSource = undefined;
119
+ state.stopMessageNoTaskSummaryUsed = undefined;
96
120
  }
@@ -1,4 +1,6 @@
1
1
  import type { RoutingInstructionState } from './routing-instructions.js';
2
+ export declare const DEFAULT_STOP_MESSAGE_MAX_REPEATS = 10;
2
3
  export declare function serializeStopMessageState(state: RoutingInstructionState): Record<string, unknown>;
3
4
  export declare function deserializeStopMessageState(data: Record<string, unknown>, state: RoutingInstructionState): void;
4
5
  export declare function normalizeStopMessageStageMode(value: unknown): 'on' | 'off' | 'auto' | undefined;
6
+ export declare function ensureStopMessageModeMaxRepeats(state: RoutingInstructionState): boolean;
@@ -1,3 +1,4 @@
1
+ export const DEFAULT_STOP_MESSAGE_MAX_REPEATS = 10;
1
2
  export function serializeStopMessageState(state) {
2
3
  return {
3
4
  ...(typeof state.stopMessageSource === 'string' && state.stopMessageSource.trim()
@@ -32,6 +33,18 @@ export function serializeStopMessageState(state) {
32
33
  : {}),
33
34
  ...(typeof state.stopMessageBdWorkState === 'string' && state.stopMessageBdWorkState.trim()
34
35
  ? { stopMessageBdWorkState: state.stopMessageBdWorkState.trim() }
36
+ : {}),
37
+ ...(typeof state.stopMessageAssignedIssueId === 'string' && state.stopMessageAssignedIssueId.trim()
38
+ ? { stopMessageAssignedIssueId: state.stopMessageAssignedIssueId.trim() }
39
+ : {}),
40
+ ...(typeof state.stopMessageAssignedIssueSource === 'string' &&
41
+ (state.stopMessageAssignedIssueSource === 'in_progress' ||
42
+ state.stopMessageAssignedIssueSource === 'ready' ||
43
+ state.stopMessageAssignedIssueSource === 'open')
44
+ ? { stopMessageAssignedIssueSource: state.stopMessageAssignedIssueSource }
45
+ : {}),
46
+ ...(typeof state.stopMessageNoTaskSummaryUsed === 'boolean'
47
+ ? { stopMessageNoTaskSummaryUsed: state.stopMessageNoTaskSummaryUsed }
35
48
  : {})
36
49
  };
37
50
  }
@@ -42,7 +55,8 @@ export function deserializeStopMessageState(data, state) {
42
55
  if (typeof data.stopMessageText === 'string' && data.stopMessageText.trim()) {
43
56
  state.stopMessageText = data.stopMessageText;
44
57
  }
45
- if (typeof data.stopMessageMaxRepeats === 'number' && Number.isFinite(data.stopMessageMaxRepeats)) {
58
+ const hasPersistedMaxRepeats = typeof data.stopMessageMaxRepeats === 'number' && Number.isFinite(data.stopMessageMaxRepeats);
59
+ if (hasPersistedMaxRepeats) {
46
60
  state.stopMessageMaxRepeats = Math.floor(data.stopMessageMaxRepeats);
47
61
  }
48
62
  if (typeof data.stopMessageUsed === 'number' && Number.isFinite(data.stopMessageUsed)) {
@@ -63,6 +77,10 @@ export function deserializeStopMessageState(data, state) {
63
77
  state.stopMessageStageMode = normalized;
64
78
  }
65
79
  }
80
+ // Keep stopMessage mode state armed consistently across old/new snapshots.
81
+ if (!hasPersistedMaxRepeats) {
82
+ ensureStopMessageModeMaxRepeats(state);
83
+ }
66
84
  if (typeof data.stopMessageObservationHash === 'string' && data.stopMessageObservationHash.trim()) {
67
85
  state.stopMessageObservationHash = data.stopMessageObservationHash.trim();
68
86
  }
@@ -72,6 +90,18 @@ export function deserializeStopMessageState(data, state) {
72
90
  if (typeof data.stopMessageBdWorkState === 'string' && data.stopMessageBdWorkState.trim()) {
73
91
  state.stopMessageBdWorkState = data.stopMessageBdWorkState.trim();
74
92
  }
93
+ if (typeof data.stopMessageAssignedIssueId === 'string' && data.stopMessageAssignedIssueId.trim()) {
94
+ state.stopMessageAssignedIssueId = data.stopMessageAssignedIssueId.trim();
95
+ }
96
+ if (typeof data.stopMessageAssignedIssueSource === 'string') {
97
+ const source = data.stopMessageAssignedIssueSource.trim().toLowerCase();
98
+ if (source === 'in_progress' || source === 'ready' || source === 'open') {
99
+ state.stopMessageAssignedIssueSource = source;
100
+ }
101
+ }
102
+ if (typeof data.stopMessageNoTaskSummaryUsed === 'boolean') {
103
+ state.stopMessageNoTaskSummaryUsed = data.stopMessageNoTaskSummaryUsed;
104
+ }
75
105
  }
76
106
  export function normalizeStopMessageStageMode(value) {
77
107
  if (typeof value !== 'string') {
@@ -83,3 +113,22 @@ export function normalizeStopMessageStageMode(value) {
83
113
  }
84
114
  return undefined;
85
115
  }
116
+ export function ensureStopMessageModeMaxRepeats(state) {
117
+ const mode = normalizeStopMessageStageMode(state.stopMessageStageMode);
118
+ if (mode !== 'on' && mode !== 'auto') {
119
+ return false;
120
+ }
121
+ const hasValidRepeats = typeof state.stopMessageMaxRepeats === 'number' &&
122
+ Number.isFinite(state.stopMessageMaxRepeats) &&
123
+ Math.floor(state.stopMessageMaxRepeats) > 0;
124
+ if (hasValidRepeats) {
125
+ const normalized = Math.floor(state.stopMessageMaxRepeats);
126
+ if (state.stopMessageMaxRepeats !== normalized) {
127
+ state.stopMessageMaxRepeats = normalized;
128
+ return true;
129
+ }
130
+ return false;
131
+ }
132
+ state.stopMessageMaxRepeats = DEFAULT_STOP_MESSAGE_MAX_REPEATS;
133
+ return true;
134
+ }
@@ -1,5 +1,5 @@
1
1
  import type { RoutingInstructionState } from './routing-instructions.js';
2
- type StopMessageSubset = Pick<RoutingInstructionState, 'stopMessageSource' | 'stopMessageText' | 'stopMessageMaxRepeats' | 'stopMessageUsed' | 'stopMessageStage' | 'stopMessageStageMode' | 'stopMessageObservationHash' | 'stopMessageObservationStableCount' | 'stopMessageBdWorkState' | 'stopMessageUpdatedAt' | 'stopMessageLastUsedAt'>;
2
+ type StopMessageSubset = Pick<RoutingInstructionState, 'stopMessageSource' | 'stopMessageText' | 'stopMessageMaxRepeats' | 'stopMessageUsed' | 'stopMessageStage' | 'stopMessageStageMode' | 'stopMessageObservationHash' | 'stopMessageObservationStableCount' | 'stopMessageBdWorkState' | 'stopMessageAssignedIssueId' | 'stopMessageAssignedIssueSource' | 'stopMessageNoTaskSummaryUsed' | 'stopMessageUpdatedAt' | 'stopMessageLastUsedAt'>;
3
3
  /**
4
4
  * Decide whether we should overwrite in-memory stopMessage fields with persisted ones.
5
5
  *
@@ -46,6 +46,9 @@ export function mergeStopMessageFromPersisted(existing, persisted) {
46
46
  stopMessageObservationHash: persisted.stopMessageObservationHash,
47
47
  stopMessageObservationStableCount: persisted.stopMessageObservationStableCount,
48
48
  stopMessageBdWorkState: persisted.stopMessageBdWorkState,
49
+ stopMessageAssignedIssueId: persisted.stopMessageAssignedIssueId,
50
+ stopMessageAssignedIssueSource: persisted.stopMessageAssignedIssueSource,
51
+ stopMessageNoTaskSummaryUsed: persisted.stopMessageNoTaskSummaryUsed,
49
52
  stopMessageUpdatedAt: persisted.stopMessageUpdatedAt,
50
53
  stopMessageLastUsedAt: persisted.stopMessageLastUsedAt
51
54
  };