deepccc 0.2.3 → 0.2.5
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 +15 -5
- package/dist/attachments.d.ts +42 -0
- package/dist/attachments.js +192 -0
- package/dist/cli.js +47 -6
- package/dist/file-tools.d.ts +12 -0
- package/dist/file-tools.js +33 -0
- package/dist/web-page.d.ts +1 -1
- package/dist/web-page.js +42 -20
- package/dist/web-runtime.d.ts +24 -2
- package/dist/web-runtime.js +174 -47
- package/dist/web-server.d.ts +1 -1
- package/dist/web-server.js +77 -1
- package/docs/deepccc-web-ui.png +0 -0
- package/package.json +1 -1
package/dist/web-runtime.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile, realpath, stat } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { AttachmentStore, buildAttachmentPrompt, detectImageMime, MAX_ATTACHMENTS_PER_MESSAGE, MAX_ATTACHMENT_BYTES, } from "./attachments.js";
|
|
2
5
|
import { ChatSession } from "./index.js";
|
|
3
|
-
import { readBuiltinContextState } from "./context.js";
|
|
6
|
+
import { DEFAULT_BUILTIN_CONTEXT_DIR, normalizeBuiltinSessionId, readBuiltinContextState } from "./context.js";
|
|
4
7
|
import { WebSessionStore, } from "./web-session-store.js";
|
|
5
8
|
export class DeepCccWebRuntime {
|
|
6
9
|
store;
|
|
10
|
+
attachmentStore;
|
|
7
11
|
loadConfig;
|
|
8
12
|
sessionFactory;
|
|
9
13
|
approvalTimeoutMs;
|
|
@@ -11,12 +15,18 @@ export class DeepCccWebRuntime {
|
|
|
11
15
|
idFactory;
|
|
12
16
|
agents = new Map();
|
|
13
17
|
activeRuns = new Map();
|
|
18
|
+
startingRuns = new Map();
|
|
19
|
+
sessionMutations = new Set();
|
|
14
20
|
events = new Map();
|
|
15
21
|
listeners = new Map();
|
|
22
|
+
globalListeners = new Set();
|
|
16
23
|
approvals = new Map();
|
|
17
24
|
nextEventId = 1;
|
|
18
25
|
constructor(options) {
|
|
19
26
|
this.store = options.store ?? new WebSessionStore();
|
|
27
|
+
this.attachmentStore = options.attachmentStore ?? new AttachmentStore(resolve(this.store.rootDir) === resolve(DEFAULT_BUILTIN_CONTEXT_DIR)
|
|
28
|
+
? {}
|
|
29
|
+
: { rootDir: join(this.store.rootDir, ".attachments") });
|
|
20
30
|
this.loadConfig = options.loadConfig;
|
|
21
31
|
this.approvalTimeoutMs = options.approvalTimeoutMs ?? 5 * 60_000;
|
|
22
32
|
this.now = options.now ?? (() => new Date());
|
|
@@ -70,48 +80,134 @@ export class DeepCccWebRuntime {
|
|
|
70
80
|
};
|
|
71
81
|
}
|
|
72
82
|
async updateSession(sessionId, patch) {
|
|
73
|
-
if (this.activeRuns.has(sessionId))
|
|
74
|
-
throw new Error("Cannot change session settings while
|
|
75
|
-
|
|
76
|
-
this.
|
|
77
|
-
|
|
78
|
-
|
|
83
|
+
if (this.activeRuns.has(sessionId) || this.startingRuns.has(sessionId) || this.sessionMutations.has(sessionId)) {
|
|
84
|
+
throw new Error("Cannot change session settings while another session operation is running");
|
|
85
|
+
}
|
|
86
|
+
this.sessionMutations.add(sessionId);
|
|
87
|
+
try {
|
|
88
|
+
const updated = await this.store.update(sessionId, patch);
|
|
89
|
+
this.agents.delete(sessionId);
|
|
90
|
+
this.emit(sessionId, "session_updated", updated);
|
|
91
|
+
return updated;
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
this.sessionMutations.delete(sessionId);
|
|
95
|
+
}
|
|
79
96
|
}
|
|
80
97
|
async deleteSession(sessionId) {
|
|
81
|
-
if (this.activeRuns.has(sessionId))
|
|
98
|
+
if (this.activeRuns.has(sessionId) || this.startingRuns.has(sessionId) || this.sessionMutations.has(sessionId)) {
|
|
82
99
|
throw new Error("Stop the running session before deleting it");
|
|
83
|
-
|
|
84
|
-
this.
|
|
85
|
-
|
|
100
|
+
}
|
|
101
|
+
this.sessionMutations.add(sessionId);
|
|
102
|
+
try {
|
|
103
|
+
this.agents.delete(sessionId);
|
|
104
|
+
const deleted = await this.store.delete(sessionId);
|
|
105
|
+
if (deleted) {
|
|
106
|
+
await this.attachmentStore.deleteSession(sessionId);
|
|
107
|
+
this.emit(sessionId, "session_deleted", { sessionId });
|
|
108
|
+
this.events.delete(sessionId);
|
|
109
|
+
}
|
|
110
|
+
return deleted;
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
this.sessionMutations.delete(sessionId);
|
|
114
|
+
}
|
|
86
115
|
}
|
|
87
|
-
async sendMessage(sessionId, text) {
|
|
88
|
-
const
|
|
89
|
-
|
|
116
|
+
async sendMessage(sessionId, text, attachmentIds = []) {
|
|
117
|
+
const displayPrompt = text.trim();
|
|
118
|
+
const uniqueAttachmentIds = [...new Set(attachmentIds)];
|
|
119
|
+
if (uniqueAttachmentIds.length > MAX_ATTACHMENTS_PER_MESSAGE) {
|
|
120
|
+
throw new Error(`A message can include at most ${MAX_ATTACHMENTS_PER_MESSAGE} images`);
|
|
121
|
+
}
|
|
122
|
+
if (!displayPrompt && !uniqueAttachmentIds.length)
|
|
90
123
|
throw new Error("Message must not be empty");
|
|
91
|
-
|
|
124
|
+
const controller = new AbortController();
|
|
125
|
+
if (this.activeRuns.has(sessionId) || this.startingRuns.has(sessionId) || this.sessionMutations.has(sessionId)) {
|
|
92
126
|
throw new Error("This session is already running");
|
|
93
|
-
let meta = await this.requireSession(sessionId);
|
|
94
|
-
if (meta.title === "新会话" || meta.title === "New session" || meta.title === meta.cwd.split(/[\\/]/).at(-1)) {
|
|
95
|
-
meta = await this.store.update(sessionId, { title: prompt.replace(/\s+/g, " ").slice(0, 42) });
|
|
96
127
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
128
|
+
this.startingRuns.set(sessionId, controller);
|
|
129
|
+
try {
|
|
130
|
+
let meta = await this.requireSession(sessionId);
|
|
131
|
+
const attachments = [];
|
|
132
|
+
for (const attachmentId of uniqueAttachmentIds) {
|
|
133
|
+
const attachment = await this.attachmentStore.get(sessionId, attachmentId);
|
|
134
|
+
if (!attachment)
|
|
135
|
+
throw new Error(`Image attachment not found: ${attachmentId}`);
|
|
136
|
+
attachments.push(attachment);
|
|
137
|
+
}
|
|
138
|
+
const prompt = buildAttachmentPrompt(displayPrompt, attachments);
|
|
139
|
+
if (meta.title === "新会话" || meta.title === "New session" || meta.title === meta.cwd.split(/[\\/]/).at(-1)) {
|
|
140
|
+
const title = displayPrompt || attachments.map((attachment) => attachment.originalName).join(", ") || "图片任务";
|
|
141
|
+
meta = await this.store.update(sessionId, { title: title.replace(/\s+/g, " ").slice(0, 42) });
|
|
142
|
+
}
|
|
143
|
+
if (controller.signal.aborted)
|
|
144
|
+
throw new DOMException("The session start was stopped", "AbortError");
|
|
145
|
+
const runId = `run-${this.idFactory()}`;
|
|
146
|
+
this.events.set(sessionId, []);
|
|
147
|
+
this.emit(sessionId, "user", { text: displayPrompt || "请分析这些图片。", attachments });
|
|
148
|
+
const promise = Promise.resolve()
|
|
149
|
+
.then(() => this.run(meta, prompt, runId, controller.signal))
|
|
150
|
+
.finally(() => {
|
|
151
|
+
this.activeRuns.delete(sessionId);
|
|
152
|
+
this.emit(sessionId, "session_updated", { sessionId, status: "idle" });
|
|
153
|
+
});
|
|
154
|
+
this.activeRuns.set(sessionId, { runId, controller, promise });
|
|
155
|
+
this.emit(sessionId, "run_started", { runId });
|
|
156
|
+
void promise.catch(() => { });
|
|
157
|
+
return { runId };
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
this.startingRuns.delete(sessionId);
|
|
161
|
+
}
|
|
107
162
|
}
|
|
108
163
|
async stopSession(sessionId) {
|
|
109
164
|
const active = this.activeRuns.get(sessionId);
|
|
110
|
-
|
|
165
|
+
const starting = this.startingRuns.get(sessionId);
|
|
166
|
+
if (!active && !starting)
|
|
111
167
|
return false;
|
|
168
|
+
starting?.abort();
|
|
169
|
+
if (!active)
|
|
170
|
+
return true;
|
|
112
171
|
active.controller.abort();
|
|
172
|
+
const pending = [...this.approvals.entries()]
|
|
173
|
+
.filter(([, waiter]) => waiter.approval.sessionId === sessionId);
|
|
174
|
+
await Promise.all(pending.map(([approvalId, waiter]) => this.finishApproval(approvalId, waiter, "deny", { stopped: true }).catch(() => { })));
|
|
113
175
|
return true;
|
|
114
176
|
}
|
|
177
|
+
async addAttachment(sessionId, input) {
|
|
178
|
+
await this.requireSession(sessionId);
|
|
179
|
+
return this.attachmentStore.save(sessionId, input);
|
|
180
|
+
}
|
|
181
|
+
async readAttachment(sessionId, attachmentId) {
|
|
182
|
+
await this.requireSession(sessionId);
|
|
183
|
+
return this.attachmentStore.read(sessionId, attachmentId);
|
|
184
|
+
}
|
|
185
|
+
async deleteAttachment(sessionId, attachmentId) {
|
|
186
|
+
await this.requireSession(sessionId);
|
|
187
|
+
return this.attachmentStore.delete(sessionId, attachmentId);
|
|
188
|
+
}
|
|
189
|
+
async readArtifact(sessionId, value) {
|
|
190
|
+
const meta = await this.requireSession(sessionId);
|
|
191
|
+
const path = await realpath(resolve(value)).catch(() => null);
|
|
192
|
+
if (!path)
|
|
193
|
+
return null;
|
|
194
|
+
const workspaceRoot = await realpath(meta.cwd).catch(() => resolve(meta.cwd));
|
|
195
|
+
const attachmentDir = join(this.attachmentStore.rootDir, normalizeBuiltinSessionId(sessionId));
|
|
196
|
+
const attachmentRoot = await realpath(attachmentDir).catch(() => resolve(attachmentDir));
|
|
197
|
+
if (!isPathInside(workspaceRoot, path) && !isPathInside(attachmentRoot, path)) {
|
|
198
|
+
throw new Error("Artifact path is outside the session workspace and attachment directory");
|
|
199
|
+
}
|
|
200
|
+
const info = await stat(path).catch(() => null);
|
|
201
|
+
if (!info?.isFile())
|
|
202
|
+
return null;
|
|
203
|
+
if (info.size > MAX_ATTACHMENT_BYTES)
|
|
204
|
+
throw new Error("Artifact image exceeds the 20 MB limit");
|
|
205
|
+
const bytes = await readFile(path);
|
|
206
|
+
const mimeType = detectImageMime(bytes);
|
|
207
|
+
if (!mimeType)
|
|
208
|
+
throw new Error("Artifact is not a supported PNG, JPEG, or WebP image");
|
|
209
|
+
return { path, mimeType, size: bytes.length, bytes };
|
|
210
|
+
}
|
|
115
211
|
async waitForIdle(sessionId) {
|
|
116
212
|
await this.activeRuns.get(sessionId)?.promise;
|
|
117
213
|
}
|
|
@@ -119,11 +215,7 @@ export class DeepCccWebRuntime {
|
|
|
119
215
|
const waiter = this.approvals.get(approvalId);
|
|
120
216
|
if (!waiter)
|
|
121
217
|
return false;
|
|
122
|
-
|
|
123
|
-
this.approvals.delete(approvalId);
|
|
124
|
-
await this.store.resolveApproval(waiter.approval.sessionId, approvalId, answer);
|
|
125
|
-
waiter.resolve(answer);
|
|
126
|
-
this.emit(waiter.approval.sessionId, "approval_resolved", { approvalId, answer });
|
|
218
|
+
await this.finishApproval(approvalId, waiter, answer);
|
|
127
219
|
return true;
|
|
128
220
|
}
|
|
129
221
|
subscribe(sessionId, listener) {
|
|
@@ -136,6 +228,10 @@ export class DeepCccWebRuntime {
|
|
|
136
228
|
this.listeners.delete(sessionId);
|
|
137
229
|
};
|
|
138
230
|
}
|
|
231
|
+
subscribeAll(listener) {
|
|
232
|
+
this.globalListeners.add(listener);
|
|
233
|
+
return () => this.globalListeners.delete(listener);
|
|
234
|
+
}
|
|
139
235
|
async run(meta, prompt, runId, signal) {
|
|
140
236
|
try {
|
|
141
237
|
const agent = this.agents.get(meta.sessionId) ?? this.createAgent(meta);
|
|
@@ -167,33 +263,60 @@ export class DeepCccWebRuntime {
|
|
|
167
263
|
createdAt: this.now().toISOString(),
|
|
168
264
|
};
|
|
169
265
|
return new Promise((resolve) => {
|
|
266
|
+
const ready = this.store.addApproval(sessionId, {
|
|
267
|
+
...approval,
|
|
268
|
+
status: "pending",
|
|
269
|
+
});
|
|
270
|
+
let waiter;
|
|
170
271
|
const timeout = setTimeout(() => {
|
|
171
|
-
this.
|
|
172
|
-
void this.store.resolveApproval(sessionId, approvalId, "deny").finally(() => {
|
|
173
|
-
resolve("deny");
|
|
174
|
-
this.emit(sessionId, "approval_resolved", { approvalId, answer: "deny", timedOut: true });
|
|
175
|
-
});
|
|
272
|
+
void this.finishApproval(approvalId, waiter, "deny", { timedOut: true }).catch(() => { });
|
|
176
273
|
}, this.approvalTimeoutMs);
|
|
177
274
|
timeout.unref?.();
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
275
|
+
waiter = { approval, resolve, timeout, ready };
|
|
276
|
+
this.approvals.set(approvalId, waiter);
|
|
277
|
+
void ready.then(() => {
|
|
278
|
+
if (this.approvals.get(approvalId) !== waiter)
|
|
279
|
+
return;
|
|
183
280
|
this.emit(sessionId, "approval", approval);
|
|
184
281
|
}).catch(() => {
|
|
282
|
+
if (this.approvals.get(approvalId) !== waiter)
|
|
283
|
+
return;
|
|
185
284
|
clearTimeout(timeout);
|
|
186
285
|
this.approvals.delete(approvalId);
|
|
187
286
|
resolve("deny");
|
|
188
287
|
});
|
|
189
288
|
});
|
|
190
289
|
}
|
|
290
|
+
async finishApproval(approvalId, waiter, answer, extra = {}) {
|
|
291
|
+
if (this.approvals.get(approvalId) !== waiter)
|
|
292
|
+
return;
|
|
293
|
+
clearTimeout(waiter.timeout);
|
|
294
|
+
this.approvals.delete(approvalId);
|
|
295
|
+
try {
|
|
296
|
+
await waiter.ready;
|
|
297
|
+
await this.store.resolveApproval(waiter.approval.sessionId, approvalId, answer);
|
|
298
|
+
}
|
|
299
|
+
finally {
|
|
300
|
+
waiter.resolve(answer);
|
|
301
|
+
this.emit(waiter.approval.sessionId, "approval_resolved", { approvalId, answer, ...extra });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
191
304
|
emit(sessionId, type, data) {
|
|
192
|
-
const event = { eventId: this.nextEventId++, at: this.now().toISOString(), type, data };
|
|
305
|
+
const event = { eventId: this.nextEventId++, sessionId, at: this.now().toISOString(), type, data };
|
|
193
306
|
const events = [...(this.events.get(sessionId) ?? []), event].slice(-500);
|
|
194
307
|
this.events.set(sessionId, events);
|
|
195
|
-
|
|
196
|
-
|
|
308
|
+
this.notifyListeners(this.listeners.get(sessionId) ?? [], event);
|
|
309
|
+
if (["run_started", "run_finished", "session_updated", "session_deleted"].includes(type)) {
|
|
310
|
+
this.notifyListeners(this.globalListeners, event);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
notifyListeners(listeners, event) {
|
|
314
|
+
for (const listener of listeners) {
|
|
315
|
+
try {
|
|
316
|
+
listener(event);
|
|
317
|
+
}
|
|
318
|
+
catch { /* a disconnected SSE client must not affect the Agent run */ }
|
|
319
|
+
}
|
|
197
320
|
}
|
|
198
321
|
async requireSession(sessionId) {
|
|
199
322
|
const meta = await this.store.get(sessionId);
|
|
@@ -202,3 +325,7 @@ export class DeepCccWebRuntime {
|
|
|
202
325
|
return meta;
|
|
203
326
|
}
|
|
204
327
|
}
|
|
328
|
+
function isPathInside(root, candidate) {
|
|
329
|
+
const rel = relative(resolve(root), resolve(candidate));
|
|
330
|
+
return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
|
|
331
|
+
}
|
package/dist/web-server.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export interface PublicDeepCccConfig {
|
|
|
19
19
|
defaultCwd?: string;
|
|
20
20
|
}
|
|
21
21
|
export interface DeepCccWebRequestHandlerOptions {
|
|
22
|
-
runtime: Pick<DeepCccWebRuntime, "listSessions" | "createSession" | "getSession" | "updateSession" | "deleteSession" | "sendMessage" | "stopSession" | "resolveApproval" | "subscribe">;
|
|
22
|
+
runtime: Pick<DeepCccWebRuntime, "listSessions" | "createSession" | "getSession" | "updateSession" | "deleteSession" | "sendMessage" | "stopSession" | "resolveApproval" | "subscribe" | "subscribeAll" | "addAttachment" | "readAttachment" | "deleteAttachment" | "readArtifact">;
|
|
23
23
|
getPublicConfig: () => PublicDeepCccConfig;
|
|
24
24
|
saveConfig: (patch: DeepCccConfigPatch) => Promise<PublicDeepCccConfig> | PublicDeepCccConfig;
|
|
25
25
|
defaultCwd: string;
|
package/dist/web-server.js
CHANGED
|
@@ -6,6 +6,7 @@ import { stat } from "node:fs/promises";
|
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
7
|
import { dirname, join } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { MAX_ATTACHMENT_BYTES } from "./attachments.js";
|
|
9
10
|
import { DEEPCCC_HOME, loadConfig, saveConfigPatch, } from "./config.js";
|
|
10
11
|
import { killProcessTree } from "./proc-tree-kill.js";
|
|
11
12
|
import { DeepCccWebRuntime } from "./web-runtime.js";
|
|
@@ -84,6 +85,9 @@ export function createDeepCccWebRequestHandler(options) {
|
|
|
84
85
|
if (method === "GET" && path === "/api/sessions") {
|
|
85
86
|
return jsonReply(res, 200, { ok: true, sessions: await options.runtime.listSessions() });
|
|
86
87
|
}
|
|
88
|
+
if (method === "GET" && path === "/api/events") {
|
|
89
|
+
return openGlobalEventStream(req, res, options.runtime);
|
|
90
|
+
}
|
|
87
91
|
if (method === "POST" && path === "/api/sessions") {
|
|
88
92
|
const body = await readJson(req);
|
|
89
93
|
const cwd = body.cwd?.trim() || options.defaultCwd;
|
|
@@ -91,6 +95,37 @@ export function createDeepCccWebRequestHandler(options) {
|
|
|
91
95
|
const session = await options.runtime.createSession({ ...body, cwd });
|
|
92
96
|
return jsonReply(res, 201, { ok: true, session });
|
|
93
97
|
}
|
|
98
|
+
const attachmentMatch = path.match(/^\/api\/sessions\/([^/]+)\/attachments(?:\/([^/]+))?$/);
|
|
99
|
+
if (attachmentMatch) {
|
|
100
|
+
const sessionId = decodeURIComponent(attachmentMatch[1]);
|
|
101
|
+
const attachmentId = attachmentMatch[2] ? decodeURIComponent(attachmentMatch[2]) : "";
|
|
102
|
+
if (method === "POST" && !attachmentId) {
|
|
103
|
+
const originalName = url.searchParams.get("name")?.trim() || "image";
|
|
104
|
+
const bytes = await readBinary(req, MAX_ATTACHMENT_BYTES);
|
|
105
|
+
const attachment = await options.runtime.addAttachment(sessionId, { originalName, bytes });
|
|
106
|
+
return jsonReply(res, 201, { ok: true, attachment });
|
|
107
|
+
}
|
|
108
|
+
if (method === "GET" && attachmentId) {
|
|
109
|
+
const stored = await options.runtime.readAttachment(sessionId, attachmentId);
|
|
110
|
+
if (!stored)
|
|
111
|
+
return jsonReply(res, 404, { ok: false, error: "Image attachment not found" });
|
|
112
|
+
return binaryReply(res, 200, stored.bytes, stored.attachment.mimeType, stored.attachment.originalName);
|
|
113
|
+
}
|
|
114
|
+
if (method === "DELETE" && attachmentId) {
|
|
115
|
+
const deleted = await options.runtime.deleteAttachment(sessionId, attachmentId);
|
|
116
|
+
return jsonReply(res, deleted ? 200 : 404, { ok: deleted });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const artifactMatch = path.match(/^\/api\/sessions\/([^/]+)\/artifact$/);
|
|
120
|
+
if (artifactMatch && method === "GET") {
|
|
121
|
+
const artifactPath = url.searchParams.get("path") ?? "";
|
|
122
|
+
if (!artifactPath)
|
|
123
|
+
throw new WebHttpError(400, "Artifact path is required");
|
|
124
|
+
const artifact = await options.runtime.readArtifact(decodeURIComponent(artifactMatch[1]), artifactPath);
|
|
125
|
+
if (!artifact)
|
|
126
|
+
return jsonReply(res, 404, { ok: false, error: "Artifact image not found" });
|
|
127
|
+
return binaryReply(res, 200, artifact.bytes, artifact.mimeType, artifact.path.split(/[\\/]/).at(-1) ?? "image");
|
|
128
|
+
}
|
|
94
129
|
const sessionMatch = path.match(/^\/api\/sessions\/([^/]+)$/);
|
|
95
130
|
if (sessionMatch && method === "GET") {
|
|
96
131
|
return jsonReply(res, 200, { ok: true, session: await options.runtime.getSession(decodeURIComponent(sessionMatch[1])) });
|
|
@@ -112,7 +147,10 @@ export function createDeepCccWebRequestHandler(options) {
|
|
|
112
147
|
const messagesMatch = path.match(/^\/api\/sessions\/([^/]+)\/messages$/);
|
|
113
148
|
if (messagesMatch && method === "POST") {
|
|
114
149
|
const body = await readJson(req);
|
|
115
|
-
const
|
|
150
|
+
const attachmentIds = Array.isArray(body.attachmentIds)
|
|
151
|
+
? body.attachmentIds.filter((value) => typeof value === "string")
|
|
152
|
+
: [];
|
|
153
|
+
const result = await options.runtime.sendMessage(decodeURIComponent(messagesMatch[1]), body.text ?? "", attachmentIds);
|
|
116
154
|
return jsonReply(res, 202, { ok: true, ...result });
|
|
117
155
|
}
|
|
118
156
|
const stopMatch = path.match(/^\/api\/sessions\/([^/]+)\/stop$/);
|
|
@@ -265,6 +303,20 @@ function openEventStream(req, res, sessionId, runtime) {
|
|
|
265
303
|
heartbeat.unref?.();
|
|
266
304
|
req.on("close", () => { clearInterval(heartbeat); unsubscribe(); });
|
|
267
305
|
}
|
|
306
|
+
function openGlobalEventStream(req, res, runtime) {
|
|
307
|
+
res.writeHead(200, {
|
|
308
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
309
|
+
"cache-control": "no-cache, no-transform",
|
|
310
|
+
connection: "keep-alive",
|
|
311
|
+
"x-accel-buffering": "no",
|
|
312
|
+
});
|
|
313
|
+
res.write(": connected\n\n");
|
|
314
|
+
const send = (event) => res.write(`id: ${event.eventId}\ndata: ${JSON.stringify(event)}\n\n`);
|
|
315
|
+
const unsubscribe = runtime.subscribeAll(send);
|
|
316
|
+
const heartbeat = setInterval(() => res.write(": heartbeat\n\n"), 15_000);
|
|
317
|
+
heartbeat.unref?.();
|
|
318
|
+
req.on("close", () => { clearInterval(heartbeat); unsubscribe(); });
|
|
319
|
+
}
|
|
268
320
|
async function readJson(req) {
|
|
269
321
|
const chunks = [];
|
|
270
322
|
let bytes = 0;
|
|
@@ -284,6 +336,20 @@ async function readJson(req) {
|
|
|
284
336
|
throw new WebHttpError(400, "Invalid JSON body");
|
|
285
337
|
}
|
|
286
338
|
}
|
|
339
|
+
async function readBinary(req, maxBytes) {
|
|
340
|
+
const chunks = [];
|
|
341
|
+
let bytes = 0;
|
|
342
|
+
for await (const chunk of req) {
|
|
343
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
344
|
+
bytes += buffer.length;
|
|
345
|
+
if (bytes > maxBytes)
|
|
346
|
+
throw new WebHttpError(413, "Image attachment exceeds the 20 MB limit");
|
|
347
|
+
chunks.push(buffer);
|
|
348
|
+
}
|
|
349
|
+
if (!chunks.length)
|
|
350
|
+
throw new WebHttpError(400, "Image attachment must not be empty");
|
|
351
|
+
return Buffer.concat(chunks);
|
|
352
|
+
}
|
|
287
353
|
async function assertDirectory(path) {
|
|
288
354
|
try {
|
|
289
355
|
if (!(await stat(path)).isDirectory())
|
|
@@ -307,6 +373,16 @@ function textReply(res, status, value, contentType) {
|
|
|
307
373
|
res.writeHead(status, { "content-type": contentType, "cache-control": "no-store" });
|
|
308
374
|
res.end(value);
|
|
309
375
|
}
|
|
376
|
+
function binaryReply(res, status, value, contentType, fileName) {
|
|
377
|
+
res.writeHead(status, {
|
|
378
|
+
"content-type": contentType,
|
|
379
|
+
"content-length": String(value.byteLength),
|
|
380
|
+
"content-disposition": `inline; filename*=UTF-8''${encodeURIComponent(fileName)}`,
|
|
381
|
+
"cache-control": "no-store",
|
|
382
|
+
"x-content-type-options": "nosniff",
|
|
383
|
+
});
|
|
384
|
+
res.end(value);
|
|
385
|
+
}
|
|
310
386
|
class WebHttpError extends Error {
|
|
311
387
|
status;
|
|
312
388
|
constructor(status, message) {
|
package/docs/deepccc-web-ui.png
CHANGED
|
Binary file
|