codex.ts
9.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
import { service as http, API_BASE_URL } from "@/utils/request";
import type { AxiosPromise } from "axios";
// Helper to get token for Authorization header
function getToken(): string {
return localStorage.getItem("auth_token") || "";
}
function authHeaders(): { Authorization?: string } {
const t = getToken();
return t ? { Authorization: `Bearer ${t}` } : {};
}
/** ---------------- Codex Session API ---------------- **/
/**
* 会话开始结果
*/
export interface StartResult {
status: "active" | "starting" | "queued" | "unavailable" | "forbidden";
position?: number;
sessionId?: number;
}
/**
* 开始会话请求
*/
export interface StartSessionRequest {
fileIds: number[];
}
/**
* 开始会话
* POST /api/codex/session/start
*/
export function startSession(
payload: StartSessionRequest,
): AxiosPromise<StartResult> {
return http.post("/codex/session/start", payload);
}
/**
* 添加文件到会话请求
*/
export interface AddFileToSessionRequest {
sessionId: number;
fileIds: number[];
}
/**
* 添加文件到会话
* POST /api/codex/session/add-file
*/
export function addFileToSession(
payload: AddFileToSessionRequest,
): AxiosPromise<{ message: string }> {
return http.post("/codex/session/add-file", payload);
}
/**
* 从会话中删除文件请求
*/
export interface RemoveFileFromSessionRequest {
sessionId: number;
fileIds: number[];
}
/**
* 从会话中删除文件
* POST /api/codex/session/remove-file
*/
export function removeFileFromSession(
payload: RemoveFileFromSessionRequest,
): AxiosPromise<{ message: string }> {
return http.post("/codex/session/remove-file", payload);
}
/**
* 会话列表项
*/
export interface CodexSessionItem {
id: number;
userId: number;
status: string;
createdAt: string;
updatedAt?: string;
[key: string]: any;
}
/**
* 分页响应
*/
export interface PageResponse<T> {
content: T[];
totalElements: number;
totalPages: number;
size: number;
number: number;
[key: string]: any;
}
/**
* 列出会话(分页)
* GET /api/codex/session
*/
export function listSessions(params?: {
page?: number;
size?: number;
}): AxiosPromise<PageResponse<CodexSessionItem>> {
return http.get("/codex/session", {
params: {
page: params?.page ?? 0,
size: params?.size ?? 20,
},
});
}
/**
* 重启会话
* POST /api/codex/session/restart
*/
export function restartSession(sessionId: number): AxiosPromise<StartResult> {
return http.post("/codex/session/restart", null, {
params: { sessionId },
});
}
/**
* 回合响应
*/
export interface TurnResponse {
status: "starting" | "started" | "queue" | "failed";
pos?: number;
}
/**
* 用户输入请求
*/
export interface TurnRequest {
sessionId: number;
input: string;
/** null: 沿用会话范围;[]: 清空;[...]: 本轮覆盖(若后端支持) */
fileIds?: number[] | null;
}
/**
* 用户输入内容(开启一轮会话turn)
* POST /api/codex/session/turn
*/
export function startTurn(payload: TurnRequest): AxiosPromise<TurnResponse> {
return http.post("/codex/session/turn", payload);
}
/**
* 停止会话
* DELETE /api/codex/session
*/
export function stopSession(): AxiosPromise<{ message: string }> {
return http.delete("/codex/session");
}
/**
* 删除单个会话
* DELETE /api/codex/session
*/
export function deleteSession(sessionId: number | string): AxiosPromise<any> {
return http.delete("/codex/session", {
params: { sessionId },
});
}
/**
* Codex 回合数据(后端实际返回的结构)
*/
export interface CodexTurn {
id: number;
sessionId: number;
role: string; // "user" | "assistant"
inputType: string; // "turn/start" 等
contentJson: string; // JSON 字符串
createdAt: string;
reply: string; // 包含多行响应数据
codexTurnId?: number;
[key: string]: any;
}
/**
* 获取会话回合列表
* GET /api/codex/session/turns
*/
export function listTurns(sessionId: number): AxiosPromise<CodexTurn[]> {
return http.get("/codex/session/turns", {
params: { sessionId },
});
}
/**
* 下载工作区文件内容
* GET /api/codex/workspace/download
*/
export function downloadWorkspaceFile(path: string): AxiosPromise<Blob> {
return http.get("/codex/workspace/download", {
params: { path },
responseType: "blob",
});
}
/**
* SSE 流式事件选项
*/
export interface StreamOptions {
sessionId: number;
onMessage?: (chunk: string | object) => void;
onOpen?: () => void;
onError?: (err: any) => void;
onEnd?: () => void;
signal?: AbortSignal;
}
/**
* 建立与前端的SSE流连接
* GET /api/codex/session/stream
*/
export function streamSession({
sessionId,
onMessage,
onOpen,
onError,
onEnd,
signal,
}: StreamOptions): { close: () => void } {
const url = `${API_BASE_URL}/codex/session/stream?sessionId=${encodeURIComponent(sessionId)}`;
const controller = new AbortController();
let closed = false;
if (signal) {
if (signal.aborted) controller.abort();
else
signal.addEventListener("abort", () => controller.abort(), {
once: true,
});
}
(async () => {
let reader: ReadableStreamDefaultReader | undefined;
const decoder = new TextDecoder("utf-8");
let buf = "";
try {
const extraHeaders = authHeaders();
const res = await fetch(url, {
method: "GET",
headers: {
Accept: "text/event-stream",
"X-Accel-Buffering": "no",
...extraHeaders,
},
credentials: "include",
signal: controller.signal,
});
if (!res.ok || !res.body)
throw new Error(`SSE failed: ${res.status} ${res.statusText}`);
onOpen && onOpen();
reader = res.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
let idx;
while ((idx = buf.indexOf("\n\n")) !== -1) {
const chunk = buf.slice(0, idx);
buf = buf.slice(idx + 2);
if (chunk.startsWith(":")) continue;
const data = chunk
.split("\n")
.filter((l) => l.startsWith("data:"))
.map((l) => l.replace(/^data:\s?/, ""))
.join("\n");
if (data) {
let payload: string | object = data;
try {
payload = JSON.parse(data);
} catch (_) {}
onMessage && onMessage(payload);
}
}
}
if (buf.trim()) {
const data = buf
.split("\n")
.filter((l) => l.startsWith("data:"))
.map((l) => l.replace(/^data:\s?/, ""))
.join("\n");
if (data) {
let payload: string | object = data;
try {
payload = JSON.parse(data);
} catch (_) {}
onMessage && onMessage(payload);
}
}
onEnd && onEnd();
} catch (err: any) {
const msg = (err && (err.name || err.message || ""))
.toString()
.toLowerCase();
const isAbort =
err?.name === "AbortError" ||
msg.includes("aborted") ||
msg.includes("bodystreambuffer") ||
closed;
if (isAbort) {
onEnd && onEnd();
} else {
onError && onError(err);
}
} finally {
try {
reader && reader.releaseLock && reader.releaseLock();
} catch {}
}
})();
return {
close: () => {
closed = true;
controller.abort();
},
};
}
/** ---------------- Codex Approval API ---------------- **/
/**
* 命令决策请求
*/
export interface CommandDecisionRequest {
approvalId: number;
decision: "approved" | "approved_for_session" | "denied" | "abort";
}
/**
* 提交命令决策
* POST /api/codex/approval/commandDecision
*/
export function submitCommandDecision(
payload: CommandDecisionRequest,
): AxiosPromise<{ message: string }> {
return http.post("/codex/approval/commandDecision", payload);
}
/**
* 补丁决策请求
*/
export interface PatchDecisionRequest {
approvalId: number;
decision: "approved" | "denied";
fileChangeId?: number;
}
/**
* 提交补丁决策
* POST /api/codex/approval/patchDecision
*/
export function submitPatchDecision(
payload: PatchDecisionRequest,
): AxiosPromise<{ message: string }> {
return http.post("/codex/approval/patchDecision", payload);
}
/** ---------------- Export Default ---------------- **/
export default {
// Session APIs
startSession,
addFileToSession,
removeFileFromSession,
listSessions,
restartSession,
startTurn,
stopSession,
listTurns,
streamSession,
deleteSession,
downloadWorkspaceFile,
// Approval APIs
submitCommandDecision,
submitPatchDecision,
submitFileDecision,
};
/**
* 文件处理决策请求
*/
export interface FileDecisionRequest {
sessionId: number;
tmpPath: string;
/**
* 父文件夹ID(可选,null/undefined 表示保存到根目录)
* 后端为 Long 类型,这里使用 number | null 与其他文件相关 API 对齐
*/
parentId?: number | null;
}
/**
* 提交文件处理决策
* POST /api/codex/session/file-decision
*/
export function submitFileDecision(
payload: FileDecisionRequest,
): AxiosPromise<{ message: string }> {
return http.post("/codex/session/file-decision", payload);
}