@wlv-zedd/dsh-chatgpt-web 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/assets/demo.gif +0 -0
- package/assets/hero-demo.png +0 -0
- package/assets/promo-dshmarket-official.png +0 -0
- package/cordis.patch.yml +4 -0
- package/lib/cli.js +239642 -0
- package/lib/plugin.js +195 -0
- package/package.json +88 -0
- package/screenshots.json +5 -0
- package/src/adapters/base.ts +16 -0
- package/src/adapters/chatgpt-web/adapter-error.ts +59 -0
- package/src/adapters/chatgpt-web/browser-helper-main.ts +513 -0
- package/src/adapters/chatgpt-web/browser-helper-prompt-selection.ts +27 -0
- package/src/adapters/chatgpt-web/browser-worker.ts +4944 -0
- package/src/adapters/chatgpt-web/codex-rollout-environment.ts +628 -0
- package/src/adapters/chatgpt-web/compaction-handoff.ts +533 -0
- package/src/adapters/chatgpt-web/compaction-transaction.ts +142 -0
- package/src/adapters/chatgpt-web/concurrency.ts +6 -0
- package/src/adapters/chatgpt-web/conversation-key.ts +58 -0
- package/src/adapters/chatgpt-web/environment.ts +669 -0
- package/src/adapters/chatgpt-web/index.ts +1544 -0
- package/src/adapters/chatgpt-web/input-tokens.ts +74 -0
- package/src/adapters/chatgpt-web/launcher-helper-client.ts +695 -0
- package/src/adapters/chatgpt-web/markdown.ts +418 -0
- package/src/adapters/chatgpt-web/mcp-main.ts +25 -0
- package/src/adapters/chatgpt-web/mcp-server.ts +933 -0
- package/src/adapters/chatgpt-web/model.ts +70 -0
- package/src/adapters/chatgpt-web/native-compaction-control.ts +74 -0
- package/src/adapters/chatgpt-web/output-validation.ts +62 -0
- package/src/adapters/chatgpt-web/process-line-writer.ts +46 -0
- package/src/adapters/chatgpt-web/prompt.ts +702 -0
- package/src/adapters/chatgpt-web/retry-policy.ts +73 -0
- package/src/adapters/chatgpt-web/rolling-checkpoint.ts +384 -0
- package/src/adapters/chatgpt-web/thread-environment.ts +238 -0
- package/src/adapters/chatgpt-web/tool-stream-parser.ts +601 -0
- package/src/adapters/chatgpt-web/turn-broker.ts +1481 -0
- package/src/adapters/chatgpt-web/turn-execution.ts +816 -0
- package/src/adapters/chatgpt-web/turn-progress.ts +292 -0
- package/src/adapters/chatgpt-web/usage.ts +121 -0
- package/src/adapters/image.ts +9 -0
- package/src/bridge.ts +1083 -0
- package/src/browser-login.ts +521 -0
- package/src/chatgpt-session.ts +240 -0
- package/src/chatgpt-web-models.ts +400 -0
- package/src/cli.ts +568 -0
- package/src/codex-integration-document.ts +824 -0
- package/src/codex-integration-journal.ts +212 -0
- package/src/codex-integration-route.ts +515 -0
- package/src/codex-integration-shared.ts +332 -0
- package/src/codex-integration.ts +529 -0
- package/src/codex-interrupt-hook.ts +158 -0
- package/src/config.ts +616 -0
- package/src/dev-chat/cli.ts +432 -0
- package/src/dev-chat/constants.ts +3 -0
- package/src/dev-chat/driver.ts +655 -0
- package/src/dev-chat/profile.ts +223 -0
- package/src/dev-chat/session.ts +287 -0
- package/src/dev-chat/transport.ts +54 -0
- package/src/doctor.ts +237 -0
- package/src/event-queue.ts +45 -0
- package/src/http-body.ts +30 -0
- package/src/launcher-browser-host.ts +695 -0
- package/src/lib/errors.ts +281 -0
- package/src/lib/token-estimate.ts +42 -0
- package/src/login-helper.cjs +140 -0
- package/src/model-catalog.ts +197 -0
- package/src/native-passthrough.ts +261 -0
- package/src/plugin.ts +191 -0
- package/src/process.ts +45 -0
- package/src/responses/compaction.ts +199 -0
- package/src/responses/parser.ts +633 -0
- package/src/responses/reasoning-envelope.ts +49 -0
- package/src/responses/schema.ts +172 -0
- package/src/responses/state.ts +230 -0
- package/src/server.ts +1111 -0
- package/src/service.ts +315 -0
- package/src/setup.ts +671 -0
- package/src/stall-timeout.ts +23 -0
- package/src/tunnel-service.ts +160 -0
- package/src/tunnel.ts +417 -0
- package/src/turndown-plugin-gfm.d.ts +5 -0
- package/src/types.ts +307 -0
- package/src/usage/totals.ts +12 -0
- package/src/version.ts +1 -0
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export interface ParsedToolCall {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
arguments: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface StreamParseChunk {
|
|
10
|
+
text: string;
|
|
11
|
+
thinking: string;
|
|
12
|
+
toolCalls: ParsedToolCall[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const THINKING_OPEN = "<thinking>";
|
|
16
|
+
const THINKING_CLOSE = "</thinking>";
|
|
17
|
+
const TOOL_OPEN_TAGS = ["<tool_call>", "<tool\\_call>", "<toolcall>", "<tool-call>"] as const;
|
|
18
|
+
const TOOL_CLOSE_TAGS = ["</tool_call>", "</tool\\_call>", "</toolcall>", "</tool-call>"] as const;
|
|
19
|
+
|
|
20
|
+
function findEarliestTag(str: string, tags: readonly string[]): { tag: string; index: number } | null {
|
|
21
|
+
let earliestTag: string | null = null;
|
|
22
|
+
let earliestIdx = -1;
|
|
23
|
+
for (const tag of tags) {
|
|
24
|
+
const idx = str.indexOf(tag);
|
|
25
|
+
if (idx !== -1 && (earliestIdx === -1 || idx < earliestIdx)) {
|
|
26
|
+
earliestIdx = idx;
|
|
27
|
+
earliestTag = tag;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return earliestTag !== null ? { tag: earliestTag, index: earliestIdx } : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function findPartialTagPrefix(str: string, tag: string): number {
|
|
34
|
+
for (let len = tag.length - 1; len >= 1; len--) {
|
|
35
|
+
if (str.endsWith(tag.slice(0, len))) {
|
|
36
|
+
return len;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function findMaxPartialPrefix(str: string, tags: readonly string[]): number {
|
|
43
|
+
let max = 0;
|
|
44
|
+
for (const tag of tags) {
|
|
45
|
+
const len = findPartialTagPrefix(str, tag);
|
|
46
|
+
if (len > max) max = len;
|
|
47
|
+
}
|
|
48
|
+
return max;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizeToolArguments(
|
|
52
|
+
toolName: string,
|
|
53
|
+
args: Record<string, unknown>,
|
|
54
|
+
userContext?: string,
|
|
55
|
+
): Record<string, unknown> {
|
|
56
|
+
const normalized = { ...args };
|
|
57
|
+
if (toolName === "write" || toolName === "read" || toolName === "edit") {
|
|
58
|
+
if (typeof normalized.file_path !== "string" || !normalized.file_path.trim()) {
|
|
59
|
+
const candidates = [
|
|
60
|
+
normalized.path,
|
|
61
|
+
normalized.filePath,
|
|
62
|
+
normalized.filepath,
|
|
63
|
+
normalized.file,
|
|
64
|
+
normalized.filename,
|
|
65
|
+
normalized.fileName,
|
|
66
|
+
normalized.target_file,
|
|
67
|
+
normalized.targetFile,
|
|
68
|
+
normalized.dest,
|
|
69
|
+
normalized.destination,
|
|
70
|
+
];
|
|
71
|
+
for (const candidate of candidates) {
|
|
72
|
+
if (typeof candidate === "string" && candidate.trim().length > 0) {
|
|
73
|
+
normalized.file_path = candidate.trim();
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Fallback: If file_path is still missing, extract filename from justification, description, or user prompt context
|
|
80
|
+
if (typeof normalized.file_path !== "string" || !normalized.file_path.trim()) {
|
|
81
|
+
const searchContext = [
|
|
82
|
+
typeof normalized.justification === "string" ? normalized.justification : "",
|
|
83
|
+
typeof normalized.description === "string" ? normalized.description : "",
|
|
84
|
+
userContext || "",
|
|
85
|
+
].filter(Boolean).join(" ");
|
|
86
|
+
|
|
87
|
+
if (searchContext) {
|
|
88
|
+
const cleaned = searchContext
|
|
89
|
+
.replace(/https?:\/\/[^\s]+/g, "")
|
|
90
|
+
.replace(/\b(?:AGENTS|CLAUDE)\.md\b/gi, "");
|
|
91
|
+
const match = cleaned.match(/\b([a-zA-Z0-9_.\-\\/]+\.[a-zA-Z0-9]{1,10})\b/);
|
|
92
|
+
if (match && match[1]) {
|
|
93
|
+
normalized.file_path = match[1];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (typeof normalized.file_path === "string") {
|
|
98
|
+
normalized.file_path = normalized.file_path.replace(/\\([_*[\]])/g, "$1").trim();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (toolName === "write") {
|
|
103
|
+
if (typeof normalized.content !== "string") {
|
|
104
|
+
const contentCandidates = [
|
|
105
|
+
normalized.text,
|
|
106
|
+
normalized.data,
|
|
107
|
+
normalized.body,
|
|
108
|
+
];
|
|
109
|
+
for (const candidate of contentCandidates) {
|
|
110
|
+
if (typeof candidate === "string") {
|
|
111
|
+
normalized.content = candidate;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (typeof normalized.content === "string") {
|
|
117
|
+
let content = normalized.content;
|
|
118
|
+
if (content.includes("\\n") && !content.includes("\n")) {
|
|
119
|
+
content = content.replace(/\\r\\n/g, "\n").replace(/\\n/g, "\n");
|
|
120
|
+
}
|
|
121
|
+
normalized.content = content.replace(/\*\*([a-zA-Z0-9_]+)\*\*/g, "__$1__");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (toolName === "edit") {
|
|
126
|
+
if (typeof normalized.old_string !== "string") {
|
|
127
|
+
const oldCandidates = [
|
|
128
|
+
normalized.old_str,
|
|
129
|
+
normalized.oldStr,
|
|
130
|
+
normalized.old_text,
|
|
131
|
+
normalized.oldText,
|
|
132
|
+
normalized["old-string"],
|
|
133
|
+
normalized["old-str"],
|
|
134
|
+
normalized.old,
|
|
135
|
+
normalized.search,
|
|
136
|
+
normalized.target,
|
|
137
|
+
normalized.original,
|
|
138
|
+
normalized.from,
|
|
139
|
+
normalized.find,
|
|
140
|
+
];
|
|
141
|
+
for (const candidate of oldCandidates) {
|
|
142
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
143
|
+
normalized.old_string = candidate;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (typeof normalized.new_string !== "string") {
|
|
149
|
+
const newCandidates = [
|
|
150
|
+
normalized.new_str,
|
|
151
|
+
normalized.newStr,
|
|
152
|
+
normalized.new_text,
|
|
153
|
+
normalized.newText,
|
|
154
|
+
normalized["new-string"],
|
|
155
|
+
normalized["new-str"],
|
|
156
|
+
normalized.new,
|
|
157
|
+
normalized.replace,
|
|
158
|
+
normalized.replacement,
|
|
159
|
+
normalized.update,
|
|
160
|
+
normalized.to,
|
|
161
|
+
];
|
|
162
|
+
for (const candidate of newCandidates) {
|
|
163
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
164
|
+
normalized.new_string = candidate;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (
|
|
171
|
+
(typeof normalized.old_string !== "string" || !normalized.old_string.trim())
|
|
172
|
+
|| (typeof normalized.new_string !== "string" || !normalized.new_string.trim())
|
|
173
|
+
) {
|
|
174
|
+
const searchContext = [
|
|
175
|
+
typeof normalized.justification === "string" ? normalized.justification : "",
|
|
176
|
+
typeof normalized.description === "string" ? normalized.description : "",
|
|
177
|
+
userContext || "",
|
|
178
|
+
].filter(Boolean).join(" ");
|
|
179
|
+
|
|
180
|
+
if (searchContext) {
|
|
181
|
+
const englishRegex = /(?:replace|change)\s+['"‘“]?([a-zA-Z0-9_.-]+)['"’”]?\s+(?:with|to|into)\s+['"‘“]?([a-zA-Z0-9_.-]+)['"’”]?/i;
|
|
182
|
+
const match = searchContext.match(englishRegex);
|
|
183
|
+
if (match && match[1] && match[2]) {
|
|
184
|
+
if (!normalized.old_string) normalized.old_string = match[1];
|
|
185
|
+
if (!normalized.new_string) normalized.new_string = match[2];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const oldStr = normalized.old_string;
|
|
190
|
+
if (typeof oldStr === "string") {
|
|
191
|
+
normalized.old_string = oldStr.replace(/\\([_*[\]])/g, "$1").replace(/\*\*([a-zA-Z0-9_]+)\*\*/g, "__$1__");
|
|
192
|
+
if (normalized.replace_all === undefined && oldStr.trim().length <= 2) {
|
|
193
|
+
normalized.replace_all = true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (typeof normalized.new_string === "string") {
|
|
197
|
+
normalized.new_string = normalized.new_string.replace(/\\([_*[\]])/g, "$1").replace(/\*\*([a-zA-Z0-9_]+)\*\*/g, "__$1__");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (toolName === "pwsh" || toolName === "bash") {
|
|
202
|
+
if (typeof normalized.command !== "string" || !normalized.command.trim()) {
|
|
203
|
+
const cmdCandidates = [
|
|
204
|
+
normalized.cmd,
|
|
205
|
+
normalized.script,
|
|
206
|
+
normalized.code,
|
|
207
|
+
normalized.exec,
|
|
208
|
+
];
|
|
209
|
+
for (const candidate of cmdCandidates) {
|
|
210
|
+
if (typeof candidate === "string" && candidate.trim().length > 0) {
|
|
211
|
+
normalized.command = candidate.trim();
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (typeof normalized.command === "string") {
|
|
217
|
+
normalized.command = normalized.command.replace(/\\([_*[\]])/g, "$1").trim();
|
|
218
|
+
}
|
|
219
|
+
// Auto-supply description if missing so DSH schema validation never rejects it
|
|
220
|
+
if (typeof normalized.description !== "string" || !normalized.description.trim()) {
|
|
221
|
+
const descCandidates = [
|
|
222
|
+
normalized.desc,
|
|
223
|
+
normalized.justification,
|
|
224
|
+
normalized.explanation,
|
|
225
|
+
normalized.reason,
|
|
226
|
+
];
|
|
227
|
+
for (const candidate of descCandidates) {
|
|
228
|
+
if (typeof candidate === "string" && candidate.trim().length > 0) {
|
|
229
|
+
normalized.description = candidate.trim();
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (typeof normalized.description !== "string" || !normalized.description.trim()) {
|
|
234
|
+
normalized.description = typeof normalized.command === "string" && normalized.command.trim()
|
|
235
|
+
? `Run: ${normalized.command.trim().replace(/\s+/g, " ").slice(0, 60)}`
|
|
236
|
+
: `Execute ${toolName} command`;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (toolName === "glob") {
|
|
242
|
+
if (typeof normalized.pattern !== "string" || !normalized.pattern.trim()) {
|
|
243
|
+
const patternCandidates = [
|
|
244
|
+
normalized.glob,
|
|
245
|
+
normalized.query,
|
|
246
|
+
normalized.path,
|
|
247
|
+
normalized.search,
|
|
248
|
+
normalized.filter,
|
|
249
|
+
];
|
|
250
|
+
for (const candidate of patternCandidates) {
|
|
251
|
+
if (typeof candidate === "string" && candidate.trim().length > 0) {
|
|
252
|
+
normalized.pattern = candidate.trim();
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (!normalized.pattern) {
|
|
257
|
+
normalized.pattern = "**/*";
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (typeof normalized.pattern === "string") {
|
|
261
|
+
normalized.pattern = normalized.pattern.replace(/\\([_*[\]])/g, "$1");
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (toolName === "grep") {
|
|
266
|
+
const patternCandidates = [
|
|
267
|
+
normalized.pattern,
|
|
268
|
+
normalized.query,
|
|
269
|
+
normalized.search,
|
|
270
|
+
normalized.regex,
|
|
271
|
+
normalized.text,
|
|
272
|
+
normalized.find,
|
|
273
|
+
];
|
|
274
|
+
for (const candidate of patternCandidates) {
|
|
275
|
+
if (typeof candidate === "string" && candidate.trim().length > 0) {
|
|
276
|
+
normalized.pattern = candidate.trim();
|
|
277
|
+
normalized.query = candidate.trim();
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (!normalized.pattern) {
|
|
282
|
+
normalized.pattern = "";
|
|
283
|
+
normalized.query = "";
|
|
284
|
+
}
|
|
285
|
+
if (typeof normalized.pattern === "string") {
|
|
286
|
+
normalized.pattern = normalized.pattern.replace(/\\([_*[\]])/g, "$1");
|
|
287
|
+
normalized.query = normalized.pattern;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (toolName === "web_search") {
|
|
292
|
+
if (!Array.isArray(normalized.queries)) {
|
|
293
|
+
const q = normalized.query || normalized.q || normalized.search;
|
|
294
|
+
if (typeof q === "string" && q.trim()) {
|
|
295
|
+
normalized.queries = [q.trim()];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (toolName === "web_fetch") {
|
|
301
|
+
if (typeof normalized.url !== "string" || !normalized.url.trim()) {
|
|
302
|
+
const urlCandidates = [normalized.link, normalized.href, normalized.uri, normalized.target];
|
|
303
|
+
for (const candidate of urlCandidates) {
|
|
304
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
305
|
+
normalized.url = candidate.trim();
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return normalized;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export class ChatGptToolStreamParser {
|
|
316
|
+
private buffer = "";
|
|
317
|
+
private mode: "text" | "thinking" | "tool_call" = "text";
|
|
318
|
+
private currentTagContent = "";
|
|
319
|
+
|
|
320
|
+
constructor(private readonly userContext?: string) {}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Process a text delta chunk from the browser Markdown feed.
|
|
324
|
+
*/
|
|
325
|
+
feed(delta: string): StreamParseChunk {
|
|
326
|
+
this.buffer += delta;
|
|
327
|
+
let outputText = "";
|
|
328
|
+
let outputThinking = "";
|
|
329
|
+
const toolCalls: ParsedToolCall[] = [];
|
|
330
|
+
|
|
331
|
+
while (this.buffer.length > 0) {
|
|
332
|
+
if (this.mode === "text") {
|
|
333
|
+
const thinkingIdx = this.buffer.indexOf(THINKING_OPEN);
|
|
334
|
+
const toolTag = findEarliestTag(this.buffer, TOOL_OPEN_TAGS);
|
|
335
|
+
|
|
336
|
+
let nextTag: string | null = null;
|
|
337
|
+
let nextIdx = -1;
|
|
338
|
+
let isThinking = false;
|
|
339
|
+
|
|
340
|
+
if (thinkingIdx !== -1 && toolTag !== null) {
|
|
341
|
+
if (thinkingIdx < toolTag.index) {
|
|
342
|
+
nextTag = THINKING_OPEN;
|
|
343
|
+
nextIdx = thinkingIdx;
|
|
344
|
+
isThinking = true;
|
|
345
|
+
} else {
|
|
346
|
+
nextTag = toolTag.tag;
|
|
347
|
+
nextIdx = toolTag.index;
|
|
348
|
+
}
|
|
349
|
+
} else if (thinkingIdx !== -1) {
|
|
350
|
+
nextTag = THINKING_OPEN;
|
|
351
|
+
nextIdx = thinkingIdx;
|
|
352
|
+
isThinking = true;
|
|
353
|
+
} else if (toolTag !== null) {
|
|
354
|
+
nextTag = toolTag.tag;
|
|
355
|
+
nextIdx = toolTag.index;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (nextTag === null) {
|
|
359
|
+
// Check for partial tag prefixes
|
|
360
|
+
const partialThinking = findPartialTagPrefix(this.buffer, THINKING_OPEN);
|
|
361
|
+
const partialTool = findMaxPartialPrefix(this.buffer, TOOL_OPEN_TAGS);
|
|
362
|
+
const partialLen = Math.max(partialThinking, partialTool);
|
|
363
|
+
|
|
364
|
+
if (partialLen > 0) {
|
|
365
|
+
outputText += this.buffer.slice(0, this.buffer.length - partialLen);
|
|
366
|
+
this.buffer = this.buffer.slice(this.buffer.length - partialLen);
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
outputText += this.buffer;
|
|
371
|
+
this.buffer = "";
|
|
372
|
+
break;
|
|
373
|
+
} else {
|
|
374
|
+
// Output text before the tag
|
|
375
|
+
outputText += this.buffer.slice(0, nextIdx);
|
|
376
|
+
this.buffer = this.buffer.slice(nextIdx + nextTag.length);
|
|
377
|
+
this.mode = isThinking ? "thinking" : "tool_call";
|
|
378
|
+
this.currentTagContent = "";
|
|
379
|
+
}
|
|
380
|
+
} else if (this.mode === "thinking") {
|
|
381
|
+
const endIdx = this.buffer.indexOf(THINKING_CLOSE);
|
|
382
|
+
if (endIdx === -1) {
|
|
383
|
+
const partialEnd = findPartialTagPrefix(this.buffer, THINKING_CLOSE);
|
|
384
|
+
if (partialEnd > 0) {
|
|
385
|
+
outputThinking += this.buffer.slice(0, this.buffer.length - partialEnd);
|
|
386
|
+
this.currentTagContent += this.buffer.slice(0, this.buffer.length - partialEnd);
|
|
387
|
+
this.buffer = this.buffer.slice(this.buffer.length - partialEnd);
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
outputThinking += this.buffer;
|
|
391
|
+
this.currentTagContent += this.buffer;
|
|
392
|
+
this.buffer = "";
|
|
393
|
+
break;
|
|
394
|
+
} else {
|
|
395
|
+
outputThinking += this.buffer.slice(0, endIdx);
|
|
396
|
+
this.buffer = this.buffer.slice(endIdx + THINKING_CLOSE.length);
|
|
397
|
+
this.mode = "text";
|
|
398
|
+
this.currentTagContent = "";
|
|
399
|
+
}
|
|
400
|
+
} else if (this.mode === "tool_call") {
|
|
401
|
+
const endTag = findEarliestTag(this.buffer, TOOL_CLOSE_TAGS);
|
|
402
|
+
if (endTag === null) {
|
|
403
|
+
const partialEnd = findMaxPartialPrefix(this.buffer, TOOL_CLOSE_TAGS);
|
|
404
|
+
if (partialEnd > 0) {
|
|
405
|
+
this.currentTagContent += this.buffer.slice(0, this.buffer.length - partialEnd);
|
|
406
|
+
this.buffer = this.buffer.slice(this.buffer.length - partialEnd);
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
this.currentTagContent += this.buffer;
|
|
410
|
+
this.buffer = "";
|
|
411
|
+
break;
|
|
412
|
+
} else {
|
|
413
|
+
this.currentTagContent += this.buffer.slice(0, endTag.index);
|
|
414
|
+
this.buffer = this.buffer.slice(endTag.index + endTag.tag.length);
|
|
415
|
+
this.mode = "text";
|
|
416
|
+
|
|
417
|
+
const parsedCall = this.parseToolCallJson(this.currentTagContent);
|
|
418
|
+
if (parsedCall) {
|
|
419
|
+
toolCalls.push(parsedCall);
|
|
420
|
+
} else {
|
|
421
|
+
outputText += `<tool_call>${this.currentTagContent}</tool_call>`;
|
|
422
|
+
}
|
|
423
|
+
this.currentTagContent = "";
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return { text: outputText, thinking: outputThinking, toolCalls };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Flush any remaining buffered content at end of stream.
|
|
433
|
+
*/
|
|
434
|
+
flush(): StreamParseChunk {
|
|
435
|
+
const remaining = this.buffer;
|
|
436
|
+
this.buffer = "";
|
|
437
|
+
let outputText = "";
|
|
438
|
+
let outputThinking = "";
|
|
439
|
+
const toolCalls: ParsedToolCall[] = [];
|
|
440
|
+
|
|
441
|
+
if (this.mode === "thinking") {
|
|
442
|
+
outputThinking = remaining;
|
|
443
|
+
this.mode = "text";
|
|
444
|
+
} else if (this.mode === "tool_call") {
|
|
445
|
+
this.currentTagContent += remaining;
|
|
446
|
+
const parsedCall = this.parseToolCallJson(this.currentTagContent);
|
|
447
|
+
if (parsedCall) {
|
|
448
|
+
toolCalls.push(parsedCall);
|
|
449
|
+
} else {
|
|
450
|
+
outputText = `<tool_call>${this.currentTagContent}`;
|
|
451
|
+
}
|
|
452
|
+
this.mode = "text";
|
|
453
|
+
this.currentTagContent = "";
|
|
454
|
+
} else {
|
|
455
|
+
outputText = remaining;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return { text: outputText, thinking: outputThinking, toolCalls };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private parseToolCallJson(rawContent: string): ParsedToolCall | null {
|
|
462
|
+
const raw = rawContent.trim();
|
|
463
|
+
let cleaned = raw.replace(/^```(?:json|xml)?\s*/i, "").replace(/\s*```$/, "").trim();
|
|
464
|
+
// Unescape markdown-escaped characters (e.g. math\_test.py -> math_test.py, \*.py -> *.py)
|
|
465
|
+
cleaned = cleaned.replace(/\\([_*[\]])/g, "$1");
|
|
466
|
+
try {
|
|
467
|
+
const parsed = JSON.parse(cleaned);
|
|
468
|
+
if (parsed && typeof parsed === "object" && typeof parsed.name === "string") {
|
|
469
|
+
const id = typeof parsed.id === "string" && parsed.id.trim()
|
|
470
|
+
? parsed.id.trim()
|
|
471
|
+
: `call_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
472
|
+
let args: Record<string, unknown> = {};
|
|
473
|
+
if (parsed.arguments && typeof parsed.arguments === "object" && !Array.isArray(parsed.arguments)) {
|
|
474
|
+
args = parsed.arguments as Record<string, unknown>;
|
|
475
|
+
} else if (typeof parsed.parameters === "object" && parsed.parameters !== null && !Array.isArray(parsed.parameters)) {
|
|
476
|
+
args = parsed.parameters as Record<string, unknown>;
|
|
477
|
+
} else {
|
|
478
|
+
args = { ...(parsed as Record<string, unknown>) };
|
|
479
|
+
delete args.id;
|
|
480
|
+
delete args.name;
|
|
481
|
+
}
|
|
482
|
+
const name = parsed.name.trim();
|
|
483
|
+
const normalized = normalizeToolArguments(name, args, this.userContext);
|
|
484
|
+
console.info(`[chatgpt-web] parseToolCallJson parsed tool=${name}, id=${id}, args=${JSON.stringify(normalized)}`);
|
|
485
|
+
return {
|
|
486
|
+
id,
|
|
487
|
+
name,
|
|
488
|
+
arguments: normalized,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
} catch {
|
|
492
|
+
// Fallback to resilient regex parsing for unescaped quotes or invalid escapes
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Resilient fallback for unescaped quotes, Windows backslashes, etc.
|
|
496
|
+
const nameMatch = cleaned.match(/"name"\s*:\s*"([^"]+)"/);
|
|
497
|
+
if (nameMatch) {
|
|
498
|
+
const name = nameMatch[1]!.trim();
|
|
499
|
+
const idMatch = cleaned.match(/"id"\s*:\s*"([^"]+)"/);
|
|
500
|
+
const id = idMatch && idMatch[1]!.trim()
|
|
501
|
+
? idMatch[1]!.trim()
|
|
502
|
+
: `call_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
503
|
+
|
|
504
|
+
const argsMatch = cleaned.match(/"(?:arguments|parameters)"\s*:\s*\{([\s\S]*)\}\s*\}?$/);
|
|
505
|
+
let args: Record<string, unknown> = {};
|
|
506
|
+
if (argsMatch && argsMatch[1]) {
|
|
507
|
+
args = this.extractRobustArguments(argsMatch[1]);
|
|
508
|
+
} else {
|
|
509
|
+
args = this.extractRobustArguments(cleaned);
|
|
510
|
+
delete args.name;
|
|
511
|
+
delete args.id;
|
|
512
|
+
}
|
|
513
|
+
const normalized = normalizeToolArguments(name, args, this.userContext);
|
|
514
|
+
return { id, name, arguments: normalized };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// XML Tag format support (e.g. <name>write</name><file_path>hello.txt</file_path><content>...</content> or <invoke name="write">)
|
|
518
|
+
const xmlNameMatch = cleaned.match(/<name>([\s\S]*?)<\/name>/i)
|
|
519
|
+
|| cleaned.match(/<tool_name>([\s\S]*?)<\/tool_name>/i)
|
|
520
|
+
|| cleaned.match(/<invoke\s+name=["']([^"']+)["']/i);
|
|
521
|
+
if (xmlNameMatch) {
|
|
522
|
+
const name = (xmlNameMatch[1] || "").trim();
|
|
523
|
+
const id = `call_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
524
|
+
const args: Record<string, unknown> = {};
|
|
525
|
+
|
|
526
|
+
const tagRegex = /<([a-zA-Z0-9_-]+)>([\s\S]*?)<\/\1>/g;
|
|
527
|
+
let tm: RegExpExecArray | null;
|
|
528
|
+
while ((tm = tagRegex.exec(cleaned)) !== null) {
|
|
529
|
+
const key = tm[1]!;
|
|
530
|
+
if (key !== "name" && key !== "tool_name" && key !== "id") {
|
|
531
|
+
args[key] = tm[2]!.trim();
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const paramRegex = /<parameter\s+name=["']([a-zA-Z0-9_-]+)["']>([\s\S]*?)<\/parameter>/gi;
|
|
536
|
+
let pm: RegExpExecArray | null;
|
|
537
|
+
while ((pm = paramRegex.exec(cleaned)) !== null) {
|
|
538
|
+
args[pm[1]!] = pm[2]!.trim();
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (name) {
|
|
542
|
+
const normalized = normalizeToolArguments(name, args, this.userContext);
|
|
543
|
+
console.info(`[chatgpt-web] parseToolCallJson (xml) parsed tool=${name}, id=${id}, args=${JSON.stringify(normalized)}`);
|
|
544
|
+
return { id, name, arguments: normalized };
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
private extractRobustArguments(innerArgs: string): Record<string, unknown> {
|
|
552
|
+
const trimmed = innerArgs.replace(/\}\s*$/, "").trim();
|
|
553
|
+
const args: Record<string, unknown> = {};
|
|
554
|
+
|
|
555
|
+
const keyRegex = /(?:^|,)\s*(?:"([a-zA-Z0-9_]+)"|([a-zA-Z0-9_]+))\s*:\s*/g;
|
|
556
|
+
const matches: Array<{ key: string; valueStart: number; keyStart: number }> = [];
|
|
557
|
+
let m: RegExpExecArray | null;
|
|
558
|
+
while ((m = keyRegex.exec(trimmed)) !== null) {
|
|
559
|
+
const key = m[1] || m[2];
|
|
560
|
+
if (key) {
|
|
561
|
+
matches.push({
|
|
562
|
+
key,
|
|
563
|
+
keyStart: m.index,
|
|
564
|
+
valueStart: m.index + m[0].length,
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (matches.length === 0) return args;
|
|
570
|
+
|
|
571
|
+
for (let i = 0; i < matches.length; i++) {
|
|
572
|
+
const current = matches[i]!;
|
|
573
|
+
const next = matches[i + 1];
|
|
574
|
+
let rawValue = next
|
|
575
|
+
? trimmed.slice(current.valueStart, next.keyStart).trim()
|
|
576
|
+
: trimmed.slice(current.valueStart).trim();
|
|
577
|
+
|
|
578
|
+
rawValue = rawValue.replace(/,\s*$/, "").trim();
|
|
579
|
+
|
|
580
|
+
if ((rawValue.startsWith('"') && rawValue.endsWith('"')) || (rawValue.startsWith("'") && rawValue.endsWith("'"))) {
|
|
581
|
+
args[current.key] = rawValue.slice(1, -1);
|
|
582
|
+
} else if (rawValue === "true") {
|
|
583
|
+
args[current.key] = true;
|
|
584
|
+
} else if (rawValue === "false") {
|
|
585
|
+
args[current.key] = false;
|
|
586
|
+
} else if (rawValue === "null") {
|
|
587
|
+
args[current.key] = null;
|
|
588
|
+
} else if (!Number.isNaN(Number(rawValue)) && rawValue !== "") {
|
|
589
|
+
args[current.key] = Number(rawValue);
|
|
590
|
+
} else {
|
|
591
|
+
try {
|
|
592
|
+
args[current.key] = JSON.parse(rawValue);
|
|
593
|
+
} catch {
|
|
594
|
+
args[current.key] = rawValue;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return args;
|
|
600
|
+
}
|
|
601
|
+
}
|