@miruzero/zgpt 0.1.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/README.md +38 -0
- package/docs/architecture.md +51 -0
- package/package.json +29 -0
- package/src/api/catalog.json +17002 -0
- package/src/api/index.js +153 -0
- package/src/auth/index.js +87 -0
- package/src/cli/formatter.js +111 -0
- package/src/cli.js +117 -0
- package/src/config/paths.js +19 -0
- package/src/connector/index.js +6 -0
- package/src/connector/read.js +56 -0
- package/src/connector/write.js +26 -0
- package/src/conversation/attachments.js +77 -0
- package/src/conversation/index.js +65 -0
- package/src/conversation/model.js +117 -0
- package/src/conversation/read.js +378 -0
- package/src/conversation/temp-state.js +43 -0
- package/src/conversation/temporary.js +59 -0
- package/src/conversation/write.js +508 -0
- package/src/index.js +8 -0
- package/src/project/index.js +6 -0
- package/src/project/read.js +91 -0
- package/src/project/write.js +50 -0
- package/src/protocol/conduit.js +26 -0
- package/src/protocol/conversation-payload.js +214 -0
- package/src/protocol/sentinel-client.js +35 -0
- package/src/protocol/sentinel-proof.js +19 -0
- package/src/protocol/sentinel-requirements.js +25 -0
- package/src/protocol/sse.js +56 -0
- package/src/session/exchange.js +12 -0
- package/src/session/index.js +70 -0
- package/src/session/local-storage.js +75 -0
- package/src/transport/direct/http-client.js +166 -0
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const direct = require('../transport/direct/http-client');
|
|
5
|
+
const sentinel = require('../protocol/sentinel-client');
|
|
6
|
+
const prepare = require('../protocol/conversation-payload');
|
|
7
|
+
const conduit = require('../protocol/conduit');
|
|
8
|
+
const sse = require('../protocol/sse');
|
|
9
|
+
const read = require('./read');
|
|
10
|
+
const connector = require('../connector');
|
|
11
|
+
|
|
12
|
+
const requestConversationInit = async ({
|
|
13
|
+
conversationId = null,
|
|
14
|
+
projectId = null,
|
|
15
|
+
model = null,
|
|
16
|
+
conversationOrigin = null,
|
|
17
|
+
systemHints = [],
|
|
18
|
+
token,
|
|
19
|
+
sessionHeaders,
|
|
20
|
+
requestJson = direct.requestJson
|
|
21
|
+
}) => {
|
|
22
|
+
const target = '/backend-api/conversation/init';
|
|
23
|
+
const body = {
|
|
24
|
+
gizmo_id: projectId || null,
|
|
25
|
+
requested_default_model: model || null,
|
|
26
|
+
conversation_id: conversationId || null,
|
|
27
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || null,
|
|
28
|
+
timezone_offset_min: new Date().getTimezoneOffset(),
|
|
29
|
+
conversation_origin: conversationOrigin,
|
|
30
|
+
...(Array.isArray(systemHints) && systemHints.length ? { system_hints: [...systemHints] } : {})
|
|
31
|
+
};
|
|
32
|
+
return requireSuccess(await requestJson(target, token, sessionHeaders, {
|
|
33
|
+
method: 'POST', body, route: target
|
|
34
|
+
}), target);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
exports.initConversation = async (input) => {
|
|
38
|
+
if (!input?.conversationId) throw new Error('conversationId is required');
|
|
39
|
+
return requestConversationInit(input);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const initNewConversation = (input) => requestConversationInit({
|
|
43
|
+
...input,
|
|
44
|
+
conversationId: null
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const requireSuccess = (response, target) => {
|
|
48
|
+
if (!response || response.status < 200 || response.status >= 300) {
|
|
49
|
+
const error = new Error(`ChatGPT request failed (${response?.status || 'no-status'}): ${target}`);
|
|
50
|
+
error.status = response?.status || null;
|
|
51
|
+
error.target = target;
|
|
52
|
+
error.retryAfterMs = direct.parseRetryAfterMs(response?.headers || {});
|
|
53
|
+
if (error.status === 429) error.code = 'RATE_LIMITED';
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
return response.json || { success: true };
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const mutateConversation = async ({ conversationId, body, token, sessionHeaders, requestJson }) => {
|
|
60
|
+
if (!conversationId) throw new Error('conversationId is required');
|
|
61
|
+
const target = `/backend-api/conversation/${encodeURIComponent(conversationId)}`;
|
|
62
|
+
return requireSuccess(await requestJson(target, token, sessionHeaders, {
|
|
63
|
+
method: 'PATCH', body, route: '/backend-api/conversation/{conversation_id}'
|
|
64
|
+
}), target);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
exports.renameConversation = ({ conversationId, title, token, sessionHeaders, requestJson = direct.requestJson }) =>
|
|
68
|
+
mutateConversation({ conversationId, body: { title }, token, sessionHeaders, requestJson });
|
|
69
|
+
|
|
70
|
+
exports.deleteConversation = ({ conversationId, token, sessionHeaders, requestJson = direct.requestJson }) =>
|
|
71
|
+
mutateConversation({ conversationId, body: { is_visible: false }, token, sessionHeaders, requestJson });
|
|
72
|
+
|
|
73
|
+
exports.moveConversation = ({ conversationId, projectId, token, sessionHeaders, requestJson = direct.requestJson }) =>
|
|
74
|
+
mutateConversation({ conversationId, body: { gizmo_id: projectId || null }, token, sessionHeaders, requestJson });
|
|
75
|
+
|
|
76
|
+
exports.setConversationAsyncStatus = async ({ conversationId, status, token, sessionHeaders, requestJson = direct.requestJson }) => {
|
|
77
|
+
if (!conversationId) throw new Error('conversationId is required');
|
|
78
|
+
const target = `/backend-api/conversation/${encodeURIComponent(conversationId)}/async-status`;
|
|
79
|
+
return requireSuccess(await requestJson(target, token, sessionHeaders, {
|
|
80
|
+
method: 'POST', body: { status }, route: '/backend-api/conversation/{conversation_id}/async-status'
|
|
81
|
+
}), target);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
exports.saveTemporaryConversation = async ({ conversationId, selectedLeafId, token, sessionHeaders, requestJson = direct.requestJson }) => {
|
|
85
|
+
if (!conversationId) throw new Error('conversationId is required');
|
|
86
|
+
if (!selectedLeafId) throw new Error('selectedLeafId is required');
|
|
87
|
+
const target = `/backend-api/conversation/${encodeURIComponent(conversationId)}/save`;
|
|
88
|
+
return requireSuccess(await requestJson(target, token, sessionHeaders, {
|
|
89
|
+
method: 'POST', body: { selected_leaf_id: selectedLeafId }, route: '/backend-api/conversation/{conversation_id}/save'
|
|
90
|
+
}), target);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
exports.setMessageReaction = async ({ conversationId, messageId, reaction, token, sessionHeaders, requestJson = direct.requestJson }) => {
|
|
94
|
+
if (!conversationId) throw new Error('conversationId is required');
|
|
95
|
+
if (!messageId) throw new Error('messageId is required');
|
|
96
|
+
if (reaction == null) throw new Error('reaction is required');
|
|
97
|
+
const target = `/backend-api/conversation/${encodeURIComponent(conversationId)}/messages/${encodeURIComponent(messageId)}/reactions`;
|
|
98
|
+
return requireSuccess(await requestJson(target, token, sessionHeaders, {
|
|
99
|
+
method: 'POST', body: { reaction, entrypoint: 'message_action_button' }, route: '/backend-api/conversation/{conversation_id}/messages/{message_id}/reactions'
|
|
100
|
+
}), target);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
exports.removeMessageReaction = async ({ conversationId, messageId, token, sessionHeaders, requestJson = direct.requestJson }) => {
|
|
104
|
+
if (!conversationId) throw new Error('conversationId is required');
|
|
105
|
+
if (!messageId) throw new Error('messageId is required');
|
|
106
|
+
const target = `/backend-api/conversation/${encodeURIComponent(conversationId)}/messages/${encodeURIComponent(messageId)}/reactions`;
|
|
107
|
+
return requireSuccess(await requestJson(target, token, sessionHeaders, {
|
|
108
|
+
method: 'DELETE', route: '/backend-api/conversation/{conversation_id}/messages/{message_id}/reactions'
|
|
109
|
+
}), target);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
exports.rateConversation = async ({ conversationId, messageId, rating, shownAtAssistantTurn, prompt, popupUiMessage, token, sessionHeaders, requestJson = direct.requestJson }) => {
|
|
113
|
+
if (!conversationId) throw new Error('conversationId is required');
|
|
114
|
+
if (!messageId) throw new Error('messageId is required');
|
|
115
|
+
const target = `/backend-api/conversation/${encodeURIComponent(conversationId)}/rating`;
|
|
116
|
+
const body = { message_id: messageId, rating, shown_at_assistant_turn: shownAtAssistantTurn, prompt, popup_ui_message: popupUiMessage };
|
|
117
|
+
return requireSuccess(await requestJson(target, token, sessionHeaders, {
|
|
118
|
+
method: 'POST', body, route: '/backend-api/conversation/{conversation_id}/rating'
|
|
119
|
+
}), target);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
exports.generateConversationTitle = async ({ conversationId, messageId, token, sessionHeaders, requestJson = direct.requestJson }) => {
|
|
123
|
+
if (!conversationId) throw new Error('conversationId is required');
|
|
124
|
+
if (!messageId) throw new Error('messageId is required');
|
|
125
|
+
const target = `/backend-api/conversation/gen_title/${encodeURIComponent(conversationId)}`;
|
|
126
|
+
return requireSuccess(await requestJson(target, token, sessionHeaders, {
|
|
127
|
+
method: 'POST', body: { message_id: messageId }, route: '/backend-api/conversation/gen_title/{conversation_id}'
|
|
128
|
+
}), target);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const sendWithRecovery = async ({
|
|
132
|
+
target, options, conversationId = null, conduitToken, turnTraceId,
|
|
133
|
+
token, sessionHeaders, requestJson, requestText
|
|
134
|
+
}) => {
|
|
135
|
+
const progress = sse.createParser();
|
|
136
|
+
let sawChunk = false;
|
|
137
|
+
let response = null;
|
|
138
|
+
let upstreamError = null;
|
|
139
|
+
try {
|
|
140
|
+
response = await requestText(target, token, sessionHeaders, {
|
|
141
|
+
...options, retry: false, idleTimeoutMs: 60_000,
|
|
142
|
+
onChunk: (chunk) => { sawChunk = true; progress.push(chunk); options.onChunk?.(chunk); }
|
|
143
|
+
});
|
|
144
|
+
} catch (error) { upstreamError = error; }
|
|
145
|
+
if (response && (response.status < 200 || response.status >= 300)) requireSuccess(response, target);
|
|
146
|
+
if (response?.text && !sawChunk) progress.push(response.text);
|
|
147
|
+
const state = progress.finish();
|
|
148
|
+
const observedId = state.conversationId || conversationId;
|
|
149
|
+
if (state.completed) return { response: response || { status: 200, text: '' }, conversationId: observedId, currentNode: state.currentNode };
|
|
150
|
+
if (!observedId) { if (upstreamError) throw upstreamError; throw Object.assign(new Error('conversation stream ended before conversation_id was available'), { code: 'STREAM_INCOMPLETE' }); }
|
|
151
|
+
const streamState = await read.getConversationStreamStatus({
|
|
152
|
+
conversationId: observedId, token, sessionHeaders, requestJson
|
|
153
|
+
});
|
|
154
|
+
if (streamState?.status !== 'IS_STREAMING') {
|
|
155
|
+
return { response: response || { status: 200, text: '' }, conversationId: observedId, currentNode: state.currentNode };
|
|
156
|
+
}
|
|
157
|
+
const resumedProgress = sse.createParser();
|
|
158
|
+
let resumedSawChunk = false;
|
|
159
|
+
const resumed = await read.resumeConversation({
|
|
160
|
+
conversationId: observedId, offset: 0, conduitToken: state.resumeToken || conduitToken, turnTraceId,
|
|
161
|
+
token, sessionHeaders, requestJson, requestText,
|
|
162
|
+
onChunk: (chunk) => { resumedSawChunk = true; resumedProgress.push(chunk); options.onChunk?.(chunk); }
|
|
163
|
+
});
|
|
164
|
+
if (resumed.text && !resumedSawChunk) resumedProgress.push(resumed.text);
|
|
165
|
+
const resumedState = resumedProgress.finish();
|
|
166
|
+
if (!resumedState.completed) throw Object.assign(new Error('resumed conversation stream ended before [DONE]'), { code: 'STREAM_INCOMPLETE' });
|
|
167
|
+
return { response: response || { status: 200, text: '' }, conversationId: observedId, currentNode: resumedState.currentNode || state.currentNode };
|
|
168
|
+
};
|
|
169
|
+
exports.createConversation = async ({
|
|
170
|
+
message,
|
|
171
|
+
projectId = null,
|
|
172
|
+
model = prepare.DEFAULT_MODEL,
|
|
173
|
+
thinkingEffort = prepare.DEFAULT_THINKING_EFFORT,
|
|
174
|
+
systemHints = null,
|
|
175
|
+
systemHintMentions = null,
|
|
176
|
+
targetedReply = null,
|
|
177
|
+
hiddenSystemMessages = [],
|
|
178
|
+
hideUserMessage = false,
|
|
179
|
+
localFunctionNames = null,
|
|
180
|
+
timezone = null,
|
|
181
|
+
timezoneOffsetMin = null,
|
|
182
|
+
modelResponseContracts = null,
|
|
183
|
+
clientContextualInfo = null,
|
|
184
|
+
temporaryChat = false,
|
|
185
|
+
historyAndTrainingDisabled = null,
|
|
186
|
+
isDoNotRemember = null,
|
|
187
|
+
temporaryChatRequestsPersonalization = null,
|
|
188
|
+
browserContext = null,
|
|
189
|
+
attachments = [],
|
|
190
|
+
enableMessageFollowups = true,
|
|
191
|
+
forceParallelSwitch = 'auto',
|
|
192
|
+
paragenCotSummaryDisplayOverride = 'allow',
|
|
193
|
+
onChunk = null,
|
|
194
|
+
recoverStream = false,
|
|
195
|
+
token,
|
|
196
|
+
sessionHeaders,
|
|
197
|
+
resolveSystemHintSelections = connector.resolveSystemHintSelections,
|
|
198
|
+
requestJson = direct.requestJson,
|
|
199
|
+
requestText = direct.requestText
|
|
200
|
+
}) => {
|
|
201
|
+
if (!message) throw new Error('message is required');
|
|
202
|
+
if (!Array.isArray(systemHints) && !Array.isArray(systemHintMentions)) ({ systemHints, systemHintMentions } = await resolveSystemHintSelections({ message, token, sessionHeaders }));
|
|
203
|
+
systemHints = Array.isArray(systemHints) ? systemHints : [];
|
|
204
|
+
systemHintMentions = Array.isArray(systemHintMentions) ? systemHintMentions : [];
|
|
205
|
+
|
|
206
|
+
let preInit = null;
|
|
207
|
+
let preInitError = null;
|
|
208
|
+
if (model === 'auto') {
|
|
209
|
+
try {
|
|
210
|
+
preInit = await initNewConversation({
|
|
211
|
+
projectId,
|
|
212
|
+
model: null,
|
|
213
|
+
systemHints,
|
|
214
|
+
token,
|
|
215
|
+
sessionHeaders,
|
|
216
|
+
requestJson
|
|
217
|
+
});
|
|
218
|
+
model = preInit?.intended_default_model_slug || preInit?.default_model_slug || prepare.DEFAULT_MODEL;
|
|
219
|
+
} catch (error) {
|
|
220
|
+
preInitError = error.message || String(error);
|
|
221
|
+
model = prepare.DEFAULT_MODEL;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const traceId = crypto.randomUUID();
|
|
226
|
+
const prepareTarget = '/backend-api/f/conversation/prepare';
|
|
227
|
+
const prepareResponse = await requestJson(prepareTarget, token, sessionHeaders, {
|
|
228
|
+
method: 'POST',
|
|
229
|
+
body: prepare.buildPrepareBody({ projectId, model, thinkingEffort, systemHints, localFunctionNames, timezone, timezoneOffsetMin, modelResponseContracts, clientContextualInfo, temporaryChat, attachments }),
|
|
230
|
+
route: prepareTarget,
|
|
231
|
+
headers: { 'x-oai-turn-trace-id': traceId }
|
|
232
|
+
});
|
|
233
|
+
if (!prepareResponse || prepareResponse.status < 200 || prepareResponse.status >= 300) {
|
|
234
|
+
requireSuccess(prepareResponse, prepareTarget);
|
|
235
|
+
}
|
|
236
|
+
const conduitToken = prepareResponse?.json?.conduit_token
|
|
237
|
+
|| prepareResponse?.headers?.['x-conduit-token'];
|
|
238
|
+
if (!conduitToken) throw new Error('conversation prepare response missing conduit token');
|
|
239
|
+
let sentinelHeaders = conduit.takeSentinelHeaders(sessionHeaders || {});
|
|
240
|
+
try {
|
|
241
|
+
const fresh = await sentinel.fetchChatRequirements({ token, sessionHeaders: sessionHeaders || {}, requestJson });
|
|
242
|
+
if (fresh?.token) sentinelHeaders = { ...sentinelHeaders, 'openai-sentinel-chat-requirements-token': fresh.token };
|
|
243
|
+
if (fresh?.proofToken) sentinelHeaders = { ...sentinelHeaders, 'openai-sentinel-proof-token': fresh.proofToken };
|
|
244
|
+
} catch {
|
|
245
|
+
// fallback: à ¹ƒà ¸Šà ¹‰ sentinel headers à ¸ˆà ¸²à ¸ session à ¹€à ¸â€Ã ¸´à ¸¡à ¸–à ¹‰à ¸² fetch à ¸ªà ¸â€Ã ¹„à ¸¡à ¹ˆà ¸ªà ¸³à ¹€à ¸£à ¹‡à ¸ˆ
|
|
246
|
+
}
|
|
247
|
+
const sendTarget = '/backend-api/f/conversation';
|
|
248
|
+
const sendOptions = {
|
|
249
|
+
method: 'POST',
|
|
250
|
+
body: prepare.buildSendBody({ message, projectId, model, thinkingEffort, systemHints, systemHintMentions, targetedReply, hiddenSystemMessages, hideUserMessage, timezone, timezoneOffsetMin, modelResponseContracts, clientContextualInfo, localFunctionNames, temporaryChat, historyAndTrainingDisabled, isDoNotRemember, temporaryChatRequestsPersonalization, browserContext, attachments, enableMessageFollowups, forceParallelSwitch, paragenCotSummaryDisplayOverride }),
|
|
251
|
+
route: sendTarget,
|
|
252
|
+
headers: {
|
|
253
|
+
...sentinelHeaders,
|
|
254
|
+
'x-conduit-token': conduitToken,
|
|
255
|
+
'x-oai-turn-trace-id': traceId,
|
|
256
|
+
accept: 'text/event-stream'
|
|
257
|
+
},
|
|
258
|
+
onChunk
|
|
259
|
+
};
|
|
260
|
+
const sent = recoverStream
|
|
261
|
+
? await sendWithRecovery({
|
|
262
|
+
target: sendTarget, options: sendOptions, conduitToken, turnTraceId: traceId,
|
|
263
|
+
token, sessionHeaders, requestJson, requestText
|
|
264
|
+
})
|
|
265
|
+
: { response: await requestText(sendTarget, token, sessionHeaders, sendOptions) };
|
|
266
|
+
const sendResponse = sent.response;
|
|
267
|
+
if (!sendResponse || sendResponse.status < 200 || sendResponse.status >= 300) {
|
|
268
|
+
requireSuccess(sendResponse, sendTarget);
|
|
269
|
+
}
|
|
270
|
+
const parsedStream = sse.parse(sendResponse.text);
|
|
271
|
+
const conversationId = sent.conversationId || parsedStream.conversationId;
|
|
272
|
+
if (!conversationId) throw new Error('conversation response missing conversation_id');
|
|
273
|
+
if (temporaryChat) {
|
|
274
|
+
const currentNode = sent.currentNode || parsedStream.currentNode;
|
|
275
|
+
if (!currentNode) throw Object.assign(new Error('temporary conversation response missing current node'), { code: 'TEMP_CURRENT_NODE_MISSING' });
|
|
276
|
+
return { conversation_id: conversationId, current_node: currentNode, temporary_chat: true, messages: [] };
|
|
277
|
+
}
|
|
278
|
+
const conversation = await read.getConversation({ conversationId, token, sessionHeaders, requestJson });
|
|
279
|
+
if (preInit) {
|
|
280
|
+
conversation.init = preInit;
|
|
281
|
+
} else {
|
|
282
|
+
try {
|
|
283
|
+
conversation.init = await exports.initConversation({ conversationId, projectId, model, token, sessionHeaders, requestJson });
|
|
284
|
+
} catch (initError) {
|
|
285
|
+
conversation.init_error = initError.message || String(initError);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (preInitError) conversation.init_preflight_error = preInitError;
|
|
289
|
+
return conversation;
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const sendConversationDirect = async ({
|
|
293
|
+
conversationId,
|
|
294
|
+
message,
|
|
295
|
+
parentMessageId = null,
|
|
296
|
+
projectId = null,
|
|
297
|
+
model = prepare.DEFAULT_MODEL,
|
|
298
|
+
thinkingEffort = prepare.DEFAULT_THINKING_EFFORT,
|
|
299
|
+
systemHints = null,
|
|
300
|
+
systemHintMentions = null,
|
|
301
|
+
targetedReply = null,
|
|
302
|
+
hiddenSystemMessages = [],
|
|
303
|
+
hideUserMessage = false,
|
|
304
|
+
localFunctionNames = null,
|
|
305
|
+
timezone = null,
|
|
306
|
+
timezoneOffsetMin = null,
|
|
307
|
+
modelResponseContracts = null,
|
|
308
|
+
clientContextualInfo = null,
|
|
309
|
+
temporaryChat = false,
|
|
310
|
+
historyAndTrainingDisabled = null,
|
|
311
|
+
isDoNotRemember = null,
|
|
312
|
+
temporaryChatRequestsPersonalization = null,
|
|
313
|
+
browserContext = null,
|
|
314
|
+
attachments = [],
|
|
315
|
+
enableMessageFollowups = true,
|
|
316
|
+
forceParallelSwitch = 'auto',
|
|
317
|
+
paragenCotSummaryDisplayOverride = 'allow',
|
|
318
|
+
onChunk = null,
|
|
319
|
+
recoverStream = false,
|
|
320
|
+
token,
|
|
321
|
+
sessionHeaders,
|
|
322
|
+
resolveSystemHintSelections = connector.resolveSystemHintSelections,
|
|
323
|
+
requestJson = direct.requestJson,
|
|
324
|
+
requestText = direct.requestText
|
|
325
|
+
}) => {
|
|
326
|
+
if (!conversationId) throw new Error('conversationId is required');
|
|
327
|
+
if (!message) throw new Error('message is required');
|
|
328
|
+
if (!Array.isArray(systemHints) && !Array.isArray(systemHintMentions)) ({ systemHints, systemHintMentions } = await resolveSystemHintSelections({ message, token, sessionHeaders }));
|
|
329
|
+
systemHints = Array.isArray(systemHints) ? systemHints : [];
|
|
330
|
+
systemHintMentions = Array.isArray(systemHintMentions) ? systemHintMentions : [];
|
|
331
|
+
let resolvedParentMessageId = parentMessageId;
|
|
332
|
+
let resolvedProjectId = projectId;
|
|
333
|
+
if (!temporaryChat) {
|
|
334
|
+
const before = await read.getConversation({ conversationId, token, sessionHeaders, requestJson });
|
|
335
|
+
resolvedParentMessageId = before.current_node;
|
|
336
|
+
resolvedProjectId = before.gizmo_id || null;
|
|
337
|
+
}
|
|
338
|
+
if (!resolvedParentMessageId) throw new Error('conversation current_node is required');
|
|
339
|
+
let sentinelHeaders = conduit.takeSentinelHeaders(sessionHeaders);
|
|
340
|
+
try {
|
|
341
|
+
const fresh = await sentinel.fetchChatRequirements({ token, sessionHeaders, requestJson });
|
|
342
|
+
if (fresh?.token) sentinelHeaders = { ...sentinelHeaders, 'openai-sentinel-chat-requirements-token': fresh.token };
|
|
343
|
+
if (fresh?.proofToken) sentinelHeaders = { ...sentinelHeaders, 'openai-sentinel-proof-token': fresh.proofToken };
|
|
344
|
+
} catch {
|
|
345
|
+
// fallback: à ¹ƒà ¸Šà ¹‰ sentinel headers à ¸ˆà ¸²à ¸ session à ¹€à ¸â€Ã ¸´à ¸¡à ¸–à ¹‰à ¸² fetch à ¸ªà ¸â€Ã ¹„à ¸¡à ¹ˆà ¸ªà ¸³à ¹€à ¸£à ¹‡à ¸ˆ
|
|
346
|
+
}
|
|
347
|
+
const turnHeaders = { ...sessionHeaders };
|
|
348
|
+
for (const name of conduit.SENTINEL_HEADER_NAMES) delete turnHeaders[name];
|
|
349
|
+
const traceId = crypto.randomUUID();
|
|
350
|
+
const prepareTarget = '/backend-api/f/conversation/prepare';
|
|
351
|
+
const prepareBody = {
|
|
352
|
+
...prepare.buildPrepareBody({ projectId: resolvedProjectId, model, thinkingEffort, systemHints, localFunctionNames, timezone, timezoneOffsetMin, modelResponseContracts, clientContextualInfo, temporaryChat, attachments, includeForkFromSharedPost: false }),
|
|
353
|
+
conversation_id: conversationId,
|
|
354
|
+
parent_message_id: resolvedParentMessageId,
|
|
355
|
+
client_prepare_dispatch: 'immediate',
|
|
356
|
+
client_prepare_source: 'context_change'
|
|
357
|
+
};
|
|
358
|
+
const prepareResponse = await requestJson(prepareTarget, token, turnHeaders, {
|
|
359
|
+
method: 'POST', body: prepareBody, route: prepareTarget,
|
|
360
|
+
headers: { 'x-oai-turn-trace-id': traceId }
|
|
361
|
+
});
|
|
362
|
+
if (!prepareResponse || prepareResponse.status < 200 || prepareResponse.status >= 300) requireSuccess(prepareResponse, prepareTarget);
|
|
363
|
+
conduit.applyIntegrityUpdate(turnHeaders, prepareResponse.headers, 'r');
|
|
364
|
+
let conduitToken = prepareResponse?.json?.conduit_token || prepareResponse?.headers?.['x-conduit-token'];
|
|
365
|
+
if (!conduitToken) throw new Error('conversation prepare response missing conduit token');
|
|
366
|
+
const preparedBody = {
|
|
367
|
+
...prepareBody,
|
|
368
|
+
client_prepare_state: 'success',
|
|
369
|
+
client_prepare_dispatch: 'debounced',
|
|
370
|
+
client_prepare_source: 'composer_editor_state'
|
|
371
|
+
};
|
|
372
|
+
const preparedResponse = await requestJson(prepareTarget, token, turnHeaders, {
|
|
373
|
+
method: 'POST', body: preparedBody, route: prepareTarget,
|
|
374
|
+
headers: {
|
|
375
|
+
'x-conduit-token': conduitToken,
|
|
376
|
+
'x-oai-turn-trace-id': traceId,
|
|
377
|
+
'x-oai-is-client-observation': turnHeaders['x-oai-is-client-observation']
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
if (!preparedResponse || preparedResponse.status < 200 || preparedResponse.status >= 300) requireSuccess(preparedResponse, prepareTarget);
|
|
381
|
+
conduit.applyIntegrityUpdate(turnHeaders, preparedResponse.headers, 'r');
|
|
382
|
+
conduitToken = preparedResponse?.headers?.['x-conduit-token'] || conduitToken;
|
|
383
|
+
const sendTarget = '/backend-api/f/conversation';
|
|
384
|
+
const sendBody = {
|
|
385
|
+
...prepare.buildSendBody({ message, projectId: resolvedProjectId, model, thinkingEffort, systemHints, systemHintMentions, targetedReply, hiddenSystemMessages, hideUserMessage, timezone, timezoneOffsetMin, modelResponseContracts, clientContextualInfo, localFunctionNames, temporaryChat, historyAndTrainingDisabled, isDoNotRemember, temporaryChatRequestsPersonalization, browserContext, attachments, enableMessageFollowups, forceParallelSwitch, paragenCotSummaryDisplayOverride }),
|
|
386
|
+
conversation_id: conversationId,
|
|
387
|
+
parent_message_id: resolvedParentMessageId
|
|
388
|
+
};
|
|
389
|
+
const sendOptions = {
|
|
390
|
+
method: 'POST', body: sendBody, route: sendTarget,
|
|
391
|
+
headers: {
|
|
392
|
+
...sentinelHeaders,
|
|
393
|
+
'x-conduit-token': conduitToken,
|
|
394
|
+
'x-oai-turn-trace-id': traceId,
|
|
395
|
+
'x-oai-is-client-observation': conduit.streamObservation(turnHeaders['x-oai-is-client-observation']),
|
|
396
|
+
accept: 'text/event-stream'
|
|
397
|
+
},
|
|
398
|
+
onChunk
|
|
399
|
+
};
|
|
400
|
+
const sent = recoverStream
|
|
401
|
+
? await sendWithRecovery({
|
|
402
|
+
target: sendTarget, options: sendOptions, conversationId, conduitToken, turnTraceId: traceId,
|
|
403
|
+
token, sessionHeaders: turnHeaders, requestJson, requestText
|
|
404
|
+
})
|
|
405
|
+
: { response: await requestText(sendTarget, token, turnHeaders, sendOptions) };
|
|
406
|
+
const sendResponse = sent.response;
|
|
407
|
+
if (!sendResponse || sendResponse.status < 200 || sendResponse.status >= 300) requireSuccess(sendResponse, sendTarget);
|
|
408
|
+
if (temporaryChat) {
|
|
409
|
+
const parsedStream = sse.parse(sendResponse.text);
|
|
410
|
+
const currentNode = sent.currentNode || parsedStream.currentNode;
|
|
411
|
+
if (!currentNode) throw Object.assign(new Error('temporary conversation response missing current node'), { code: 'TEMP_CURRENT_NODE_MISSING' });
|
|
412
|
+
return {
|
|
413
|
+
conversation_id: conversationId,
|
|
414
|
+
current_node: currentNode,
|
|
415
|
+
previous_node: resolvedParentMessageId,
|
|
416
|
+
temporary_chat: true,
|
|
417
|
+
messages: []
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
const after = await read.getConversation({ conversationId, token, sessionHeaders, requestJson });
|
|
421
|
+
if (after && typeof after === 'object') after.previous_node = resolvedParentMessageId;
|
|
422
|
+
return after;
|
|
423
|
+
};
|
|
424
|
+
exports.sendConversation = async ({ transport = 'direct', ...input }) => {
|
|
425
|
+
if (transport !== 'direct') {
|
|
426
|
+
const error = new Error('Unsupported ChatGPT transport: ' + transport);
|
|
427
|
+
error.code = 'UNSUPPORTED_TRANSPORT';
|
|
428
|
+
throw error;
|
|
429
|
+
}
|
|
430
|
+
return sendConversationDirect(input);
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
exports.setConversationAsyncStatus = async ({
|
|
434
|
+
conversationId, status = null, token, sessionHeaders, requestJson = direct.requestJson
|
|
435
|
+
}) => {
|
|
436
|
+
if (!conversationId) throw new Error('conversationId is required');
|
|
437
|
+
const target = `/backend-api/conversation/${encodeURIComponent(conversationId)}/async-status`;
|
|
438
|
+
return requireSuccess(await requestJson(target, token, sessionHeaders, {
|
|
439
|
+
method: 'POST', body: { status }, route: '/backend-api/conversation/{conversation_id}/async-status', retry: false
|
|
440
|
+
}), target);
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
exports.setMessageReaction = async ({
|
|
444
|
+
conversationId, messageId, reaction, entrypoint = 'message_action_button',
|
|
445
|
+
token, sessionHeaders, requestJson = direct.requestJson
|
|
446
|
+
}) => {
|
|
447
|
+
if (!conversationId) throw new Error('conversationId is required');
|
|
448
|
+
if (!messageId) throw new Error('messageId is required');
|
|
449
|
+
if (!reaction) throw new Error('reaction is required');
|
|
450
|
+
const target = `/backend-api/conversation/${encodeURIComponent(conversationId)}/messages/${encodeURIComponent(messageId)}/reactions`;
|
|
451
|
+
return requireSuccess(await requestJson(target, token, sessionHeaders, {
|
|
452
|
+
method: 'POST', body: { reaction, entrypoint },
|
|
453
|
+
route: '/backend-api/conversation/{conversation_id}/messages/{message_id}/reactions', retry: false
|
|
454
|
+
}), target);
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
exports.removeMessageReaction = async ({
|
|
458
|
+
conversationId, messageId, token, sessionHeaders, requestJson = direct.requestJson
|
|
459
|
+
}) => {
|
|
460
|
+
if (!conversationId) throw new Error('conversationId is required');
|
|
461
|
+
if (!messageId) throw new Error('messageId is required');
|
|
462
|
+
const target = `/backend-api/conversation/${encodeURIComponent(conversationId)}/messages/${encodeURIComponent(messageId)}/reactions`;
|
|
463
|
+
return requireSuccess(await requestJson(target, token, sessionHeaders, {
|
|
464
|
+
method: 'DELETE', route: '/backend-api/conversation/{conversation_id}/messages/{message_id}/reactions', retry: false
|
|
465
|
+
}), target);
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
exports.rateConversation = async ({
|
|
469
|
+
conversationId, messageId, rating, shownAtAssistantTurn = -1,
|
|
470
|
+
prompt = null, popupUiMessage = null,
|
|
471
|
+
token, sessionHeaders, requestJson = direct.requestJson
|
|
472
|
+
}) => {
|
|
473
|
+
if (!conversationId) throw new Error('conversationId is required');
|
|
474
|
+
if (!messageId) throw new Error('messageId is required');
|
|
475
|
+
const target = `/backend-api/conversation/${encodeURIComponent(conversationId)}/rating`;
|
|
476
|
+
const body = {
|
|
477
|
+
message_id: messageId, rating,
|
|
478
|
+
shown_at_assistant_turn: shownAtAssistantTurn,
|
|
479
|
+
prompt, popup_ui_message: popupUiMessage
|
|
480
|
+
};
|
|
481
|
+
return requireSuccess(await requestJson(target, token, sessionHeaders, {
|
|
482
|
+
method: 'POST', body, route: '/backend-api/conversation/{conversation_id}/rating', retry: false
|
|
483
|
+
}), target);
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
exports.generateConversationTitle = async ({
|
|
487
|
+
conversationId, messageId, token, sessionHeaders, requestJson = direct.requestJson
|
|
488
|
+
}) => {
|
|
489
|
+
if (!conversationId) throw new Error('conversationId is required');
|
|
490
|
+
if (!messageId) throw new Error('messageId is required');
|
|
491
|
+
const target = `/backend-api/conversation/gen_title/${encodeURIComponent(conversationId)}`;
|
|
492
|
+
return requireSuccess(await requestJson(target, token, sessionHeaders, {
|
|
493
|
+
method: 'POST', body: { message_id: messageId },
|
|
494
|
+
route: '/backend-api/conversation/gen_title/{conversation_id}', retry: false
|
|
495
|
+
}), target);
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
exports.saveTemporaryConversation = async ({
|
|
499
|
+
conversationId, selectedLeafId, token, sessionHeaders, requestJson = direct.requestJson
|
|
500
|
+
}) => {
|
|
501
|
+
if (!conversationId) throw new Error('conversationId is required');
|
|
502
|
+
if (!selectedLeafId) throw new Error('selectedLeafId is required');
|
|
503
|
+
const target = `/backend-api/conversation/${encodeURIComponent(conversationId)}/save`;
|
|
504
|
+
return requireSuccess(await requestJson(target, token, sessionHeaders, {
|
|
505
|
+
method: 'POST', body: { selected_leaf_id: selectedLeafId },
|
|
506
|
+
route: '/backend-api/conversation/{conversation_id}/save', retry: false
|
|
507
|
+
}), target);
|
|
508
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
exports.auth = require('./auth');
|
|
4
|
+
exports.session = require('./session');
|
|
5
|
+
exports.conversation = require('./conversation');
|
|
6
|
+
exports.project = require('./project');
|
|
7
|
+
exports.connector = require('./connector');
|
|
8
|
+
exports.api = require('./api');
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const direct = require('../transport/direct/http-client');
|
|
4
|
+
|
|
5
|
+
const requireOk = (response, target) => {
|
|
6
|
+
if (!response || response.status !== 200 || !response.json) {
|
|
7
|
+
const error = new Error(`ChatGPT request failed (${response?.status || 'no-status'}): ${target}`);
|
|
8
|
+
error.status = response?.status || null;
|
|
9
|
+
error.target = target;
|
|
10
|
+
error.retryAfterMs = direct.parseRetryAfterMs(response?.headers || {});
|
|
11
|
+
if (error.status === 429) error.code = 'RATE_LIMITED';
|
|
12
|
+
throw error;
|
|
13
|
+
}
|
|
14
|
+
return response.json;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
exports.listProjects = async ({ token, sessionHeaders, requestJson = direct.requestJson }) => {
|
|
18
|
+
const projects = [];
|
|
19
|
+
let cursor = null;
|
|
20
|
+
for (let pageNumber = 0; pageNumber < 1000; pageNumber += 1) {
|
|
21
|
+
const cursorPart = cursor ? `&cursor=${encodeURIComponent(cursor)}` : '';
|
|
22
|
+
const target = `/backend-api/gizmos/snorlax/sidebar?owned_only=true&conversations_per_gizmo=5&limit=20${cursorPart}`;
|
|
23
|
+
let page;
|
|
24
|
+
try {
|
|
25
|
+
page = requireOk(await requestJson(target, token, sessionHeaders), target);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (error.status === 401 || error.status === 403 || error.status === 404) return projects;
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
for (const item of Array.isArray(page.items) ? page.items : []) {
|
|
31
|
+
const gizmo = item?.gizmo?.gizmo;
|
|
32
|
+
if (!gizmo?.id) continue;
|
|
33
|
+
projects.push({ id: gizmo.id, name: gizmo.display?.name || gizmo.short_url || gizmo.id });
|
|
34
|
+
}
|
|
35
|
+
if (!page.cursor || page.cursor === cursor) return projects;
|
|
36
|
+
cursor = page.cursor;
|
|
37
|
+
}
|
|
38
|
+
throw Object.assign(new Error('project pagination exceeded 1000 pages'), { code: 'PAGINATION_LIMIT' });
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
exports.getProject = async ({ projectId, token, sessionHeaders, requestJson = direct.requestJson }) => {
|
|
42
|
+
if (!projectId) throw new Error('projectId is required');
|
|
43
|
+
const target = `/backend-api/gizmos/${encodeURIComponent(projectId)}`;
|
|
44
|
+
return requireOk(await requestJson(target, token, sessionHeaders, { route: '/backend-api/gizmos/{gizmo_id}' }), target);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
exports.listProjectConversationPage = async ({ projectId, token, sessionHeaders, cursor = '0', limit = 50, requestJson = direct.requestJson }) => {
|
|
48
|
+
if (!projectId) throw new Error('projectId is required');
|
|
49
|
+
const target = `/backend-api/gizmos/${encodeURIComponent(projectId)}/conversations?cursor=${encodeURIComponent(cursor)}&limit=${limit}&owned_only=true`;
|
|
50
|
+
const page = requireOk(await requestJson(target, token, sessionHeaders, { route: '/backend-api/gizmos/{gizmo_id}/conversations' }), target);
|
|
51
|
+
return { items: Array.isArray(page.items) ? page.items : [], cursor, nextCursor: page.cursor && page.cursor !== cursor ? page.cursor : null };
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
exports.listProjectConversations = async ({ projectId, token, sessionHeaders, requestJson = direct.requestJson }) => {
|
|
55
|
+
const items = [];
|
|
56
|
+
let cursor = '0';
|
|
57
|
+
for (let pageNumber = 0; pageNumber < 1000; pageNumber += 1) {
|
|
58
|
+
const page = await exports.listProjectConversationPage({ projectId, token, sessionHeaders, cursor, limit: 50, requestJson });
|
|
59
|
+
items.push(...page.items);
|
|
60
|
+
if (!page.nextCursor) return items;
|
|
61
|
+
cursor = page.nextCursor;
|
|
62
|
+
}
|
|
63
|
+
throw Object.assign(new Error('project conversation pagination exceeded 1000 pages'), { code: 'PAGINATION_LIMIT' });
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
exports.listAll = async ({ token, sessionHeaders, requestJson = direct.requestJson, onProgress = () => {} }) => {
|
|
67
|
+
const conversation = require('../conversation/read');
|
|
68
|
+
const conversationPage = await conversation.listConversationPage({ token, sessionHeaders, requestJson, onProgress });
|
|
69
|
+
onProgress({ type: 'standalone-loaded', count: conversationPage.items.length, total: conversationPage.total });
|
|
70
|
+
const projectList = await exports.listProjects({ token, sessionHeaders, requestJson });
|
|
71
|
+
onProgress({ type: 'projects-loaded', count: projectList.length });
|
|
72
|
+
const projects = projectList.map((project) => ({ ...project, conversations: [] }));
|
|
73
|
+
const projectsById = new Map(projects.map((project) => [project.id, project]));
|
|
74
|
+
const outside = [];
|
|
75
|
+
for (const item of conversationPage.items) {
|
|
76
|
+
const project = item.gizmo_id ? projectsById.get(item.gizmo_id) : null;
|
|
77
|
+
if (project) project.conversations.push(item);
|
|
78
|
+
else outside.push(item);
|
|
79
|
+
}
|
|
80
|
+
const result = {
|
|
81
|
+
outside,
|
|
82
|
+
projects,
|
|
83
|
+
returned: conversationPage.items.length,
|
|
84
|
+
total: conversationPage.total,
|
|
85
|
+
offset: conversationPage.offset,
|
|
86
|
+
limit: conversationPage.limit,
|
|
87
|
+
nextOffset: conversationPage.nextOffset
|
|
88
|
+
};
|
|
89
|
+
onProgress({ type: 'done', returned: result.returned, total: result.total });
|
|
90
|
+
return result;
|
|
91
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const direct = require('../transport/direct/http-client');
|
|
4
|
+
const read = require('./read');
|
|
5
|
+
|
|
6
|
+
const requireSuccess = (response, target) => {
|
|
7
|
+
if (!response || response.status < 200 || response.status >= 300) {
|
|
8
|
+
const error = new Error(`ChatGPT request failed (${response?.status || 'no-status'}): ${target}`);
|
|
9
|
+
error.status = response?.status || null;
|
|
10
|
+
error.target = target;
|
|
11
|
+
error.retryAfterMs = direct.parseRetryAfterMs(response?.headers || {});
|
|
12
|
+
if (error.status === 429) error.code = 'RATE_LIMITED';
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
return response.json || { success: true };
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
exports.createProject = async ({ name, token, sessionHeaders, requestJson = direct.requestJson }) => {
|
|
19
|
+
const target = '/backend-api/projects';
|
|
20
|
+
return requireSuccess(await requestJson(target, token, sessionHeaders, {
|
|
21
|
+
method: 'POST', body: { name, instructions: '', memory_scope: 'project_v2' }, route: target
|
|
22
|
+
}), target);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
exports.renameProject = async ({ projectId, name, token, sessionHeaders, requestJson = direct.requestJson }) => {
|
|
26
|
+
const raw = await read.getProject({ projectId, token, sessionHeaders, requestJson });
|
|
27
|
+
const g = raw?.gizmo?.gizmo || raw?.gizmo || raw || {};
|
|
28
|
+
const display = g.display || {};
|
|
29
|
+
const target = `/backend-api/projects/${encodeURIComponent(projectId)}`;
|
|
30
|
+
const body = { name, instructions: g.instructions || '', emoji: display.emoji ?? null, theme: display.theme ?? null };
|
|
31
|
+
return requireSuccess(await requestJson(target, token, sessionHeaders, {
|
|
32
|
+
method: 'PATCH', body, route: '/backend-api/projects/{project_id}'
|
|
33
|
+
}), target);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
exports.saveProjectMessage = async ({ projectId, conversationId, messageId, token, sessionHeaders, requestJson = direct.requestJson }) => {
|
|
37
|
+
if (!projectId) throw new Error('projectId is required');
|
|
38
|
+
if (!conversationId) throw new Error('conversationId is required');
|
|
39
|
+
if (!messageId) throw new Error('messageId is required');
|
|
40
|
+
const target = `/backend-api/projects/${encodeURIComponent(projectId)}/saves`;
|
|
41
|
+
return requireSuccess(await requestJson(target, token, sessionHeaders, {
|
|
42
|
+
method: 'POST', body: { conversation_id: conversationId, message_id: messageId }, route: '/backend-api/projects/{project_id}/saves', retry: false
|
|
43
|
+
}), target);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
exports.deleteProject = async ({ projectId, token, sessionHeaders, requestJson = direct.requestJson }) => {
|
|
47
|
+
if (!projectId) throw new Error('projectId is required');
|
|
48
|
+
const target = `/backend-api/gizmos/${encodeURIComponent(projectId)}`;
|
|
49
|
+
return requireSuccess(await requestJson(target, token, sessionHeaders, { method: 'DELETE', route: '/backend-api/gizmos/{gizmo_id}' }), target);
|
|
50
|
+
};
|