@dahawa/hawa-cli-analysis 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.tools.json +7 -0
- package/.vscode/launch.json +27 -0
- package/LICENSE +21 -0
- package/README.md +196 -0
- package/_uclaude.js +165 -0
- package/anthropic-transformer.js +986 -0
- package/api-anthropic.js +279 -0
- package/api-openai.js +539 -0
- package/claude/claude-openai-proxy.js +305 -0
- package/claude/claude-proxy.js +341 -0
- package/clogger-openai.js +190 -0
- package/clogger.js +318 -0
- package/codex/mcp-client.js +556 -0
- package/codex/mcpclient.js +118 -0
- package/codex/mcpserver.js +374 -0
- package/codex/mcpserverproxy.js +144 -0
- package/codex/test.js +30 -0
- package/config.js +105 -0
- package/index.js +0 -0
- package/logger-manager.js +85 -0
- package/logger.js +112 -0
- package/mcp/claude-mcpproxy-launcher.js +5 -0
- package/mcp_oauth_tokens.js +40 -0
- package/package.json +36 -0
- package/port-manager.js +80 -0
- package/simple-transform-example.js +213 -0
- package/tests/test-lazy-load.js +36 -0
- package/tests/test.js +30 -0
- package/tests/test_mcp_proxy.js +51 -0
- package/tests/test_supabase_mcp.js +42 -0
- package/uclaude.js +221 -0
- package/ucodex-proxy.js +173 -0
- package/ucodex.js +129 -0
- package/untils.js +261 -0
package/api-openai.js
ADDED
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
/**
|
|
2
|
+
prompt
|
|
3
|
+
|
|
4
|
+
生成一个方法使用 nodejs JavaScript 语言,其可以把 OpenAI Responses 响应合并成一个完整的相应 JSON,方法入参为 events ,events 是一个文本数组,数据每一个元素是一个 event 原始文本
|
|
5
|
+
用以下测试数据,生成测试例子
|
|
6
|
+
|
|
7
|
+
event: response.created
|
|
8
|
+
data: {"type":"response.created","response":{"id":"resp_67c9fdcecf488190bdd9a0409de3a1ec07b8b0ad4e5eb654","object":"response","created_at":1741290958,"status":"in_progress","error":null,"incomplete_details":null,"instructions":"你是一个有帮助的助手。","max_output_tokens":null,"model":"gpt-4.1-2025-04-14","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
|
|
9
|
+
|
|
10
|
+
event: response.in_progress
|
|
11
|
+
data: {"type":"response.in_progress","response":{"id":"resp_67c9fdcecf488190bdd9a0409de3a1ec07b8b0ad4e5eb654","object":"response","created_at":1741290958,"status":"in_progress","error":null,"incomplete_details":null,"instructions":"你是一个有帮助的助手。","max_output_tokens":null,"model":"gpt-4.1-2025-04-14","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
|
|
12
|
+
|
|
13
|
+
event: response.output_item.added
|
|
14
|
+
data: {"type":"response.output_item.added","output_index":0,"item":{"id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","type":"message","status":"in_progress","role":"assistant","content":[]}}
|
|
15
|
+
|
|
16
|
+
event: response.content_part.added
|
|
17
|
+
data: {"type":"response.content_part.added","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"part":{"type":"output_text","text":"","annotations":[]}}
|
|
18
|
+
|
|
19
|
+
event: response.output_text.delta
|
|
20
|
+
data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"你好"}
|
|
21
|
+
|
|
22
|
+
event: response.output_text.delta
|
|
23
|
+
data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"!"}
|
|
24
|
+
|
|
25
|
+
event: response.output_text.delta
|
|
26
|
+
data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":" 我"}
|
|
27
|
+
|
|
28
|
+
event: response.output_text.delta
|
|
29
|
+
data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"能"}
|
|
30
|
+
|
|
31
|
+
event: response.output_text.delta
|
|
32
|
+
data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"为"}
|
|
33
|
+
|
|
34
|
+
event: response.output_text.delta
|
|
35
|
+
data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"您"}
|
|
36
|
+
|
|
37
|
+
event: response.output_text.delta
|
|
38
|
+
data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"提供"}
|
|
39
|
+
|
|
40
|
+
event: response.output_text.delta
|
|
41
|
+
data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"什么"}
|
|
42
|
+
|
|
43
|
+
event: response.output_text.delta
|
|
44
|
+
data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"帮助"}
|
|
45
|
+
|
|
46
|
+
event: response.output_text.delta
|
|
47
|
+
data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"吗"}
|
|
48
|
+
|
|
49
|
+
event: response.output_text.delta
|
|
50
|
+
data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"?"}
|
|
51
|
+
|
|
52
|
+
event: response.output_text.done
|
|
53
|
+
data: {"type":"response.output_text.done","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"text":"你好! 我能为您提供什么帮助吗?"}
|
|
54
|
+
|
|
55
|
+
event: response.content_part.done
|
|
56
|
+
data: {"type":"response.content_part.done","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"part":{"type":"output_text","text":"你好! 我能为您提供什么帮助吗?","annotations":[]}}
|
|
57
|
+
|
|
58
|
+
event: response.output_item.done
|
|
59
|
+
data: {"type":"response.output_item.done","output_index":0,"item":{"id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"你好! 我能为您提供什么帮助吗?","annotations":[]}]}}
|
|
60
|
+
|
|
61
|
+
event: response.completed
|
|
62
|
+
data: {"type":"response.completed","response":{"id":"resp_67c9fdcecf488190bdd9a0409de3a1ec07b8b0ad4e5eb654","object":"response","created_at":1741290958,"status":"completed","error":null,"incomplete_details":null,"instructions":"你是一个有帮助的助手。","max_output_tokens":null,"model":"gpt-4.1-2025-04-14","output":[{"id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"你好! 我能为您提供什么帮助吗?","annotations":[]}]}],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":37,"output_tokens":11,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":48},"user":null,"metadata":{}}}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
prompt
|
|
66
|
+
|
|
67
|
+
生成一个方法使用 nodejs JavaScript 语言,其可以把 Chat Completions API 流式响应合并成一个完整的相应 JSON,方法入参为 events ,events 是一个文本数组,数据每一个元素是一个 event 原始文本
|
|
68
|
+
生成测试例子
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
以下为测试响应数据
|
|
72
|
+
{"id":"chatcmpl-123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4o-mini", "system_fingerprint": "fp_44709d6fcb", "choices":[{"index":0,"delta":{"role":"assistant","content":""},"logprobs":null,"finish_reason":null}]}
|
|
73
|
+
|
|
74
|
+
{"id":"chatcmpl-123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4o-mini", "system_fingerprint": "fp_44709d6fcb", "choices":[{"index":0,"delta":{"content":"Hello"},"logprobs":null,"finish_reason":null}]}
|
|
75
|
+
|
|
76
|
+
....
|
|
77
|
+
|
|
78
|
+
{"id":"chatcmpl-123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4o-mini", "system_fingerprint": "fp_44709d6fcb", "choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}]}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
*/
|
|
83
|
+
async function readAll(reader){
|
|
84
|
+
const decoder = new TextDecoder('utf-8');
|
|
85
|
+
let buffer = '';
|
|
86
|
+
while (true) {
|
|
87
|
+
const { done, value } = await reader.read();
|
|
88
|
+
if(value){
|
|
89
|
+
buffer += decoder.decode(value, { stream: true });
|
|
90
|
+
}
|
|
91
|
+
if (done) {
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return buffer;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 通过流进行读取
|
|
100
|
+
* 这个流最好是 fork 的一份,不要影响输出
|
|
101
|
+
|
|
102
|
+
async function parseOpenAIResponse(reader){
|
|
103
|
+
const buffer = await readAll(reader);
|
|
104
|
+
return _parseResponseObject(buffer);
|
|
105
|
+
}
|
|
106
|
+
*/
|
|
107
|
+
/**
|
|
108
|
+
* 直接转换所有文本
|
|
109
|
+
*/
|
|
110
|
+
export async function parseOpenAIResponse(body){
|
|
111
|
+
return mergeOpenAIResponseEvents(body.split('\n\n'));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 将 OpenAI Responses SSE 事件合并为一个完整的响应 JSON
|
|
116
|
+
* @param {string[]} events - 每个元素是一段原始事件文本(通常包含 "event: ..." 和 "data: {...}")
|
|
117
|
+
* @returns {object} 合并后的响应对象
|
|
118
|
+
*/
|
|
119
|
+
export function mergeOpenAIResponseEvents(events) {
|
|
120
|
+
// 内部状态
|
|
121
|
+
const items = new Map(); // item_id -> { ...item, content:[{...}] }
|
|
122
|
+
let latestResponseObj = null; // 最近一次出现于 response.* 的 response 对象
|
|
123
|
+
let completedResponseObj = null; // response.completed 的 response 对象(若出现则直接返回)
|
|
124
|
+
|
|
125
|
+
// 工具:从一段原始事件文本里取出 JSON(data: ...)
|
|
126
|
+
function parseEventBlock(raw) {
|
|
127
|
+
if (typeof raw !== 'string') return null;
|
|
128
|
+
// SSE 事件通常是多行,data: 后面可能就是 JSON
|
|
129
|
+
// 也允许 raw 就是纯 JSON 字符串(兼容性)
|
|
130
|
+
const dataLine = raw
|
|
131
|
+
.split(/\r?\n/)
|
|
132
|
+
.map(s => s.trim())
|
|
133
|
+
.find(line => line.startsWith('data:'));
|
|
134
|
+
const jsonText = dataLine ? dataLine.slice('data:'.length).trim() : raw.trim();
|
|
135
|
+
try {
|
|
136
|
+
return JSON.parse(jsonText);
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 确保 item 与 content 索引存在
|
|
143
|
+
function ensureItemContent(itemId, contentIndex) {
|
|
144
|
+
if (!items.has(itemId)) {
|
|
145
|
+
items.set(itemId, { id: itemId, type: 'message', status: 'in_progress', role: 'assistant', content: [] });
|
|
146
|
+
}
|
|
147
|
+
const it = items.get(itemId);
|
|
148
|
+
while (it.content.length <= contentIndex) {
|
|
149
|
+
it.content.push({ type: 'output_text', text: '', annotations: [] });
|
|
150
|
+
}
|
|
151
|
+
return it;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const raw of events) {
|
|
155
|
+
const data = parseEventBlock(raw);
|
|
156
|
+
if (!data || !data.type) continue;
|
|
157
|
+
|
|
158
|
+
switch (data.type) {
|
|
159
|
+
case 'response.created':
|
|
160
|
+
case 'response.in_progress':
|
|
161
|
+
if (data.response) latestResponseObj = data.response;
|
|
162
|
+
break;
|
|
163
|
+
|
|
164
|
+
case 'response.output_item.added': {
|
|
165
|
+
const { item, output_index } = data;
|
|
166
|
+
if (item && item.id) {
|
|
167
|
+
// 以服务端给的 item 为准
|
|
168
|
+
items.set(item.id, JSON.parse(JSON.stringify(item)));
|
|
169
|
+
// 但为安全起见,若没有 content 初始化为 []
|
|
170
|
+
if (!Array.isArray(items.get(item.id).content)) {
|
|
171
|
+
items.get(item.id).content = [];
|
|
172
|
+
}
|
|
173
|
+
// output_index 暂未直接使用;所有内容通过 item_id/content_index 关联
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
case 'response.content_part.added': {
|
|
179
|
+
const { item_id, content_index, part } = data;
|
|
180
|
+
if (item_id != null && content_index != null && part) {
|
|
181
|
+
const it = ensureItemContent(item_id, content_index);
|
|
182
|
+
// 如果是 output_text,确保有 text/annotations
|
|
183
|
+
if (part.type === 'output_text') {
|
|
184
|
+
it.content[content_index] = {
|
|
185
|
+
type: 'output_text',
|
|
186
|
+
text: part.text || '',
|
|
187
|
+
annotations: Array.isArray(part.annotations) ? part.annotations : []
|
|
188
|
+
};
|
|
189
|
+
} else {
|
|
190
|
+
// 其它类型按原样塞进去
|
|
191
|
+
it.content[content_index] = part;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
case 'response.output_text.delta': {
|
|
198
|
+
const { item_id, content_index, delta } = data;
|
|
199
|
+
if (item_id != null && content_index != null && typeof delta === 'string') {
|
|
200
|
+
const it = ensureItemContent(item_id, content_index);
|
|
201
|
+
const part = it.content[content_index];
|
|
202
|
+
if (part && part.type === 'output_text') {
|
|
203
|
+
part.text = (part.text || '') + delta;
|
|
204
|
+
} else {
|
|
205
|
+
// 若不是 output_text,强制转为 output_text 累加(保底)
|
|
206
|
+
it.content[content_index] = {
|
|
207
|
+
type: 'output_text',
|
|
208
|
+
text: (part && part.text ? part.text : '') + delta,
|
|
209
|
+
annotations: (part && Array.isArray(part.annotations)) ? part.annotations : []
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
case 'response.output_text.done': {
|
|
217
|
+
const { item_id, content_index, text } = data;
|
|
218
|
+
if (item_id != null && content_index != null) {
|
|
219
|
+
const it = ensureItemContent(item_id, content_index);
|
|
220
|
+
const part = it.content[content_index] || { type: 'output_text', text: '', annotations: [] };
|
|
221
|
+
// 以 done 提供的最终文本为准(一般与 delta 累计一致)
|
|
222
|
+
it.content[content_index] = {
|
|
223
|
+
...part,
|
|
224
|
+
type: 'output_text',
|
|
225
|
+
text: typeof text === 'string' ? text : (part.text || ''),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
case 'response.content_part.done': {
|
|
232
|
+
const { item_id, content_index, part } = data;
|
|
233
|
+
if (item_id != null && content_index != null && part) {
|
|
234
|
+
const it = ensureItemContent(item_id, content_index);
|
|
235
|
+
// 用最终的 part 覆盖(可能带 annotations)
|
|
236
|
+
it.content[content_index] = {
|
|
237
|
+
type: part.type || 'output_text',
|
|
238
|
+
text: part.text || (it.content[content_index]?.text ?? ''),
|
|
239
|
+
annotations: Array.isArray(part.annotations) ? part.annotations : (it.content[content_index]?.annotations ?? [])
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
case 'response.output_item.done': {
|
|
246
|
+
const { item } = data;
|
|
247
|
+
if (item && item.id) {
|
|
248
|
+
// 用服务端最终 item 覆盖,但如果我们本地已累计了文本,把本地 content 合并进去(避免丢增量)
|
|
249
|
+
const local = items.get(item.id);
|
|
250
|
+
const merged = JSON.parse(JSON.stringify(item));
|
|
251
|
+
if (local && Array.isArray(local.content) && local.content.length) {
|
|
252
|
+
merged.content = local.content;
|
|
253
|
+
}
|
|
254
|
+
items.set(item.id, merged);
|
|
255
|
+
}
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
case 'response.completed':
|
|
260
|
+
if (data.response) {
|
|
261
|
+
completedResponseObj = data.response;
|
|
262
|
+
latestResponseObj = data.response;
|
|
263
|
+
}
|
|
264
|
+
break;
|
|
265
|
+
|
|
266
|
+
default:
|
|
267
|
+
// 其它类型(如 tool 调用等)可以在此扩展
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 若已经拿到服务端的最终 completed 响应,直接返回它
|
|
273
|
+
if (completedResponseObj) {
|
|
274
|
+
// 保险起见,将我们积累的 items 覆写到 response.output(若 output 是 message 们)
|
|
275
|
+
try {
|
|
276
|
+
const resp = JSON.parse(JSON.stringify(completedResponseObj));
|
|
277
|
+
if (Array.isArray(resp.output)) {
|
|
278
|
+
// 将同 id 的内容替换为累积版本
|
|
279
|
+
const byId = new Map(resp.output.map(o => [o.id, o]));
|
|
280
|
+
for (const [id, it] of items.entries()) {
|
|
281
|
+
if (byId.has(id)) byId.set(id, it);
|
|
282
|
+
else byId.set(id, it);
|
|
283
|
+
}
|
|
284
|
+
resp.output = Array.from(byId.values());
|
|
285
|
+
} else if (items.size) {
|
|
286
|
+
resp.output = Array.from(items.values());
|
|
287
|
+
}
|
|
288
|
+
return resp;
|
|
289
|
+
} catch {
|
|
290
|
+
return completedResponseObj;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 否则:合成一个尽可能完整的响应对象(基于 latestResponseObj 或最小骨架)
|
|
295
|
+
const synthetic = latestResponseObj
|
|
296
|
+
? JSON.parse(JSON.stringify(latestResponseObj))
|
|
297
|
+
: {
|
|
298
|
+
id: null,
|
|
299
|
+
object: 'response',
|
|
300
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
301
|
+
status: 'in_progress',
|
|
302
|
+
output: [],
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// 用我们累积到的 items 作为 output
|
|
306
|
+
synthetic.output = Array.from(items.values());
|
|
307
|
+
|
|
308
|
+
// 如果没有状态但看起来文本已结束,设置为 completed
|
|
309
|
+
const hasAnyText = synthetic.output.some(
|
|
310
|
+
it => Array.isArray(it.content) && it.content.some(p => p?.type === 'output_text' && p.text && p.text.length > 0)
|
|
311
|
+
);
|
|
312
|
+
if (synthetic.status === 'in_progress' && hasAnyText) {
|
|
313
|
+
// 不武断改状态,保留 in_progress;如需可改为 'completed'
|
|
314
|
+
// synthetic.status = 'completed';
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return synthetic;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/* ------------------------- 测试用例(你的示例数据) ------------------------- */
|
|
321
|
+
|
|
322
|
+
const testEvents = [
|
|
323
|
+
`event: response.created
|
|
324
|
+
data: {"type":"response.created","response":{"id":"resp_67c9fdcecf488190bdd9a0409de3a1ec07b8b0ad4e5eb654","object":"response","created_at":1741290958,"status":"in_progress","error":null,"incomplete_details":null,"instructions":"你是一个有帮助的助手。","max_output_tokens":null,"model":"gpt-4.1-2025-04-14","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
|
|
325
|
+
`,
|
|
326
|
+
|
|
327
|
+
`event: response.in_progress
|
|
328
|
+
data: {"type":"response.in_progress","response":{"id":"resp_67c9fdcecf488190bdd9a0409de3a1ec07b8b0ad4e5eb654","object":"response","created_at":1741290958,"status":"in_progress","error":null,"incomplete_details":null,"instructions":"你是一个有帮助的助手。","max_output_tokens":null,"model":"gpt-4.1-2025-04-14","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
|
|
329
|
+
`,
|
|
330
|
+
|
|
331
|
+
`event: response.output_item.added
|
|
332
|
+
data: {"type":"response.output_item.added","output_index":0,"item":{"id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","type":"message","status":"in_progress","role":"assistant","content":[]}}
|
|
333
|
+
`,
|
|
334
|
+
|
|
335
|
+
`event: response.content_part.added
|
|
336
|
+
data: {"type":"response.content_part.added","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"part":{"type":"output_text","text":"","annotations":[]}}
|
|
337
|
+
`,
|
|
338
|
+
|
|
339
|
+
`event: response.output_text.delta
|
|
340
|
+
data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"你好"}
|
|
341
|
+
`,
|
|
342
|
+
|
|
343
|
+
`event: response.output_text.delta
|
|
344
|
+
data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"!"}
|
|
345
|
+
`,
|
|
346
|
+
|
|
347
|
+
`event: response.output_text.delta
|
|
348
|
+
data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":" 我"}
|
|
349
|
+
`,
|
|
350
|
+
|
|
351
|
+
`event: response.output_text.delta
|
|
352
|
+
data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"能"}
|
|
353
|
+
`,
|
|
354
|
+
|
|
355
|
+
`event: response.output_text.delta
|
|
356
|
+
data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"为"}
|
|
357
|
+
`,
|
|
358
|
+
|
|
359
|
+
`event: response.output_text.delta
|
|
360
|
+
data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"您"}
|
|
361
|
+
`,
|
|
362
|
+
|
|
363
|
+
`event: response.output_text.delta
|
|
364
|
+
data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"提供"}
|
|
365
|
+
`,
|
|
366
|
+
|
|
367
|
+
`event: response.output_text.delta
|
|
368
|
+
data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"什么"}
|
|
369
|
+
`,
|
|
370
|
+
|
|
371
|
+
`event: response.output_text.delta
|
|
372
|
+
data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"帮助"}
|
|
373
|
+
`,
|
|
374
|
+
|
|
375
|
+
`event: response.output_text.delta
|
|
376
|
+
data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"吗"}
|
|
377
|
+
`,
|
|
378
|
+
|
|
379
|
+
`event: response.output_text.delta
|
|
380
|
+
data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"?"}
|
|
381
|
+
`,
|
|
382
|
+
|
|
383
|
+
`event: response.output_text.done
|
|
384
|
+
data: {"type":"response.output_text.done","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"text":"你好! 我能为您提供什么帮助吗?"}
|
|
385
|
+
`,
|
|
386
|
+
|
|
387
|
+
`event: response.content_part.done
|
|
388
|
+
data: {"type":"response.content_part.done","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"part":{"type":"output_text","text":"你好! 我能为您提供什么帮助吗?","annotations":[]}}
|
|
389
|
+
`,
|
|
390
|
+
|
|
391
|
+
`event: response.output_item.done
|
|
392
|
+
data: {"type":"response.output_item.done","output_index":0,"item":{"id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"你好! 我能为您提供什么帮助吗?","annotations":[]}]}}
|
|
393
|
+
`,
|
|
394
|
+
|
|
395
|
+
`event: response.completed
|
|
396
|
+
data: {"type":"response.completed","response":{"id":"resp_67c9fdcecf488190bdd9a0409de3a1ec07b8b0ad4e5eb654","object":"response","created_at":1741290958,"status":"completed","error":null,"incomplete_details":null,"instructions":"你是一个有帮助的助手。","max_output_tokens":null,"model":"gpt-4.1-2025-04-14","output":[{"id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"你好! 我能为您提供什么帮助吗?","annotations":[]}]}],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":37,"output_tokens":11,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":48},"user":null,"metadata":{}}}
|
|
397
|
+
`,
|
|
398
|
+
];
|
|
399
|
+
|
|
400
|
+
// 运行合并并打印结果
|
|
401
|
+
const merged = mergeOpenAIResponseEvents(testEvents);
|
|
402
|
+
//console.log(JSON.stringify(merged, null, 2));
|
|
403
|
+
|
|
404
|
+
// 如果你只想拿到纯文本(例如第一条 message 的 output_text)
|
|
405
|
+
const firstMsg = Array.isArray(merged.output) ? merged.output[0] : null;
|
|
406
|
+
const text = firstMsg?.content?.find?.(p => p.type === 'output_text')?.text ?? '';
|
|
407
|
+
//console.log('--- Extracted Text ---\n' + text);
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* 通过流进行读取
|
|
414
|
+
* 这个流最好是 fork 的一份,不要影响输出
|
|
415
|
+
|
|
416
|
+
export async function parseOpenAIChatCompletion(reader){
|
|
417
|
+
const buffer = await readAll(reader);
|
|
418
|
+
return _parseOpenAIChatCompletion(buffer);
|
|
419
|
+
}
|
|
420
|
+
*/
|
|
421
|
+
/**
|
|
422
|
+
* 直接转换所有文本
|
|
423
|
+
*/
|
|
424
|
+
export async function parseOpenAIChatCompletion(body){
|
|
425
|
+
return mergeOpenAIChatCompletionEvents(body.split('\n\n'));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
*
|
|
433
|
+
* 合并 Chat Completions API 流式分片为完整响应。
|
|
434
|
+
* @param {string[]} events - 每个元素是一个 event 的原始文本(通常是一行 JSON 字符串;若带 "data: " 前缀也能处理)
|
|
435
|
+
* @returns {object} - 近似非流式的 chat.completion 响应对象
|
|
436
|
+
*/
|
|
437
|
+
function mergeOpenAIChatCompletionEvents(events) {
|
|
438
|
+
const acc = {
|
|
439
|
+
id: null,
|
|
440
|
+
object: "chat.completion",
|
|
441
|
+
created: null,
|
|
442
|
+
model: null,
|
|
443
|
+
system_fingerprint: null,
|
|
444
|
+
choices: [], // index 对应 choices 的 index
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
// 用于 choices 按 index 累积
|
|
448
|
+
const choiceMap = new Map();
|
|
449
|
+
|
|
450
|
+
const parseMaybeJSON = (line) => {
|
|
451
|
+
if (!line || !line.trim()) return null;
|
|
452
|
+
const cleaned = line.trim().startsWith("data:")
|
|
453
|
+
? line.trim().slice(5).trim()
|
|
454
|
+
: line.trim();
|
|
455
|
+
if (!cleaned || cleaned === "[DONE]") return null;
|
|
456
|
+
try {
|
|
457
|
+
return JSON.parse(cleaned);
|
|
458
|
+
} catch {
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
for (const raw of events) {
|
|
464
|
+
const chunk = parseMaybeJSON(raw);
|
|
465
|
+
if (!chunk || chunk.object !== "chat.completion.chunk") continue;
|
|
466
|
+
|
|
467
|
+
// 顶层元信息以「第一次出现」为准(大多数情况下都一致)
|
|
468
|
+
if (acc.id === null) acc.id = chunk.id ?? null;
|
|
469
|
+
if (acc.created === null) acc.created = chunk.created ?? null;
|
|
470
|
+
if (acc.model === null) acc.model = chunk.model ?? null;
|
|
471
|
+
if (acc.system_fingerprint === null) {
|
|
472
|
+
acc.system_fingerprint = chunk.system_fingerprint ?? null;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// 处理 choices
|
|
476
|
+
if (Array.isArray(chunk.choices)) {
|
|
477
|
+
for (const ch of chunk.choices) {
|
|
478
|
+
const idx = ch.index ?? 0;
|
|
479
|
+
if (!choiceMap.has(idx)) {
|
|
480
|
+
choiceMap.set(idx, {
|
|
481
|
+
index: idx,
|
|
482
|
+
message: { role: "assistant", content: "" },
|
|
483
|
+
logprobs: null,
|
|
484
|
+
finish_reason: null,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
const target = choiceMap.get(idx);
|
|
488
|
+
|
|
489
|
+
// 累积 role / content
|
|
490
|
+
const delta = ch.delta ?? {};
|
|
491
|
+
if (typeof delta.role === "string") {
|
|
492
|
+
target.message.role = delta.role;
|
|
493
|
+
}
|
|
494
|
+
if (typeof delta.content === "string") {
|
|
495
|
+
target.message.content += delta.content;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// 累积 tool_calls(如果有)
|
|
499
|
+
// 说明:OpenAI 流式时 tool_calls 可能分片到多个 delta 中;这里把 function.arguments 逐步拼接
|
|
500
|
+
if (Array.isArray(delta.tool_calls) && delta.tool_calls.length > 0) {
|
|
501
|
+
if (!Array.isArray(target.message.tool_calls)) {
|
|
502
|
+
target.message.tool_calls = [];
|
|
503
|
+
}
|
|
504
|
+
delta.tool_calls.forEach((tc, i) => {
|
|
505
|
+
// 目标位置:若已有则取已有的对应项,否则 push 新项
|
|
506
|
+
const existing = target.message.tool_calls[i] ?? {
|
|
507
|
+
id: tc.id ?? null,
|
|
508
|
+
type: tc.type ?? "function",
|
|
509
|
+
function: { name: tc.function?.name ?? "", arguments: "" },
|
|
510
|
+
};
|
|
511
|
+
// 更新 id / name(如果本分片提供)
|
|
512
|
+
if (tc.id && !existing.id) existing.id = tc.id;
|
|
513
|
+
if (tc.function?.name) existing.function.name = tc.function.name;
|
|
514
|
+
// 追加 arguments 片段
|
|
515
|
+
if (typeof tc.function?.arguments === "string") {
|
|
516
|
+
existing.function.arguments += tc.function.arguments;
|
|
517
|
+
}
|
|
518
|
+
target.message.tool_calls[i] = existing;
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// 结束原因(通常只在最后一个分片才出现)
|
|
523
|
+
if (typeof ch.finish_reason === "string" && ch.finish_reason) {
|
|
524
|
+
target.finish_reason = ch.finish_reason;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// logprobs(若 API 返回)
|
|
528
|
+
if (ch.logprobs != null) {
|
|
529
|
+
target.logprobs = ch.logprobs;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// 将 Map 转为数组并按 index 排序
|
|
536
|
+
acc.choices = Array.from(choiceMap.values()).sort((a, b) => a.index - b.index);
|
|
537
|
+
|
|
538
|
+
return acc;
|
|
539
|
+
}
|