@deepseek-ai/dsh-client-file-upload 0.1.3-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +101 -0
- package/README.zh.md +101 -0
- package/lib/client.js +333 -0
- package/lib/index.js +318 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +614 -0
- package/lib/typert.remote-client.d.ts +26 -0
- package/lib/typert.remote-client.js +65 -0
- package/lib/types/client/contract.d.ts +27 -0
- package/lib/types/client/contract.js +2 -0
- package/lib/types/client/index.d.ts +19 -0
- package/lib/types/client/index.js +12 -0
- package/lib/types/client/runtime.d.ts +92 -0
- package/lib/types/client/runtime.js +270 -0
- package/lib/types/http-route.d.ts +10 -0
- package/lib/types/http-route.js +73 -0
- package/lib/types/index.d.ts +84 -0
- package/lib/types/index.js +243 -0
- package/lib/types/protocol.d.ts +3 -0
- package/lib/types/protocol.js +3 -0
- package/lib/types/types.d.ts +30 -0
- package/lib/types/types.js +3 -0
- package/package.json +84 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { scopeOf } from "@deepseek-ai/dsh-scope";
|
|
3
|
+
import { Remote, RemoteError, TypertRemoteService, remoteErrorOf } from "@deepseek-ai/dsh-typert-protocol";
|
|
4
|
+
import { brandString } from "@deepseek-ai/dsh-brand";
|
|
5
|
+
//#region lib/types/http-route.js
|
|
6
|
+
/** Authenticated raw-byte upload route registered on the Connection fetch registry. */
|
|
7
|
+
/**
|
|
8
|
+
* Handle one authenticated raw-byte upload.
|
|
9
|
+
* @param service - Host upload service receiving streamed bytes.
|
|
10
|
+
* @param request - authenticated HTTP request from Connection.
|
|
11
|
+
* @returns JSON result using HTTP status 200 after request validation.
|
|
12
|
+
*/
|
|
13
|
+
async function handleFileUploadHttp(service, request) {
|
|
14
|
+
if (request.method !== "POST") return new Response(null, {
|
|
15
|
+
status: 405,
|
|
16
|
+
headers: { allow: "POST" }
|
|
17
|
+
});
|
|
18
|
+
if (request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() !== "application/octet-stream") return new Response("content type must be application/octet-stream", { status: 415 });
|
|
19
|
+
const url = new URL(request.url);
|
|
20
|
+
const sessionId = url.searchParams.get("sessionId");
|
|
21
|
+
if (sessionId === null || sessionId === "") return new Response("sessionId is required", { status: 400 });
|
|
22
|
+
const name = url.searchParams.get("name") ?? void 0;
|
|
23
|
+
let result;
|
|
24
|
+
try {
|
|
25
|
+
result = {
|
|
26
|
+
ok: true,
|
|
27
|
+
value: await service.uploadStream({
|
|
28
|
+
sessionId: brandString(sessionId),
|
|
29
|
+
data: requestBodyChunks(request.body),
|
|
30
|
+
signal: request.signal,
|
|
31
|
+
...name === void 0 ? {} : { name }
|
|
32
|
+
})
|
|
33
|
+
};
|
|
34
|
+
} catch (error) {
|
|
35
|
+
const failure = remoteErrorOf(error);
|
|
36
|
+
result = {
|
|
37
|
+
ok: false,
|
|
38
|
+
error: failure !== void 0 ? {
|
|
39
|
+
code: failure.code,
|
|
40
|
+
message: failure.message,
|
|
41
|
+
details: failure.details
|
|
42
|
+
} : {
|
|
43
|
+
code: "gateway/internal",
|
|
44
|
+
message: error instanceof Error ? error.message : String(error),
|
|
45
|
+
details: {}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return new Response(JSON.stringify(result), {
|
|
50
|
+
status: 200,
|
|
51
|
+
headers: {
|
|
52
|
+
"content-type": "application/json; charset=utf-8",
|
|
53
|
+
"cache-control": "no-store"
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async function* requestBodyChunks(body) {
|
|
58
|
+
if (body === null) return;
|
|
59
|
+
const reader = body.getReader();
|
|
60
|
+
try {
|
|
61
|
+
while (true) {
|
|
62
|
+
const chunk = await reader.read();
|
|
63
|
+
if (chunk.done) return;
|
|
64
|
+
yield chunk.value;
|
|
65
|
+
}
|
|
66
|
+
} finally {
|
|
67
|
+
reader.releaseLock();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region lib/types/protocol.js
|
|
72
|
+
/** Authenticated raw-byte route owned by the file-upload service. */
|
|
73
|
+
const FILE_UPLOAD_PATH = "/api/session/uploadFileBinary";
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region lib/types/index.js
|
|
76
|
+
/** Host file-upload service: streamed intake and Agent-scoped staged receipts. */
|
|
77
|
+
var __runInitializers = function(thisArg, initializers, value) {
|
|
78
|
+
var useValue = arguments.length > 2;
|
|
79
|
+
for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
80
|
+
return useValue ? value : void 0;
|
|
81
|
+
};
|
|
82
|
+
var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
83
|
+
function accept(f) {
|
|
84
|
+
if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
|
|
85
|
+
return f;
|
|
86
|
+
}
|
|
87
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
88
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
89
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
90
|
+
var _, done = false;
|
|
91
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
92
|
+
var context = {};
|
|
93
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
94
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
95
|
+
context.addInitializer = function(f) {
|
|
96
|
+
if (done) throw new TypeError("Cannot add initializers after decoration has completed");
|
|
97
|
+
extraInitializers.push(accept(f || null));
|
|
98
|
+
};
|
|
99
|
+
var result = (0, decorators[i])(kind === "accessor" ? {
|
|
100
|
+
get: descriptor.get,
|
|
101
|
+
set: descriptor.set
|
|
102
|
+
} : descriptor[key], context);
|
|
103
|
+
if (kind === "accessor") {
|
|
104
|
+
if (result === void 0) continue;
|
|
105
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
106
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
107
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
108
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
109
|
+
} else if (_ = accept(result)) if (kind === "field") initializers.unshift(_);
|
|
110
|
+
else descriptor[key] = _;
|
|
111
|
+
}
|
|
112
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
113
|
+
done = true;
|
|
114
|
+
};
|
|
115
|
+
var PromptFileBindingGuard = class {
|
|
116
|
+
rollback;
|
|
117
|
+
settled = false;
|
|
118
|
+
constructor(rollback) {
|
|
119
|
+
this.rollback = rollback;
|
|
120
|
+
}
|
|
121
|
+
commit() {
|
|
122
|
+
this.settled = true;
|
|
123
|
+
}
|
|
124
|
+
[Symbol.dispose]() {
|
|
125
|
+
if (this.settled) return;
|
|
126
|
+
this.settled = true;
|
|
127
|
+
this.rollback();
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
/** Host service owning upload storage and Agent-scoped staged receipts. */
|
|
131
|
+
let FileUploads = (() => {
|
|
132
|
+
let _classSuper = TypertRemoteService;
|
|
133
|
+
let _instanceExtraInitializers = [];
|
|
134
|
+
let _upload_decorators;
|
|
135
|
+
return class FileUploads extends _classSuper {
|
|
136
|
+
static {
|
|
137
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
138
|
+
_upload_decorators = [Remote("upload")];
|
|
139
|
+
__esDecorate(this, null, _upload_decorators, {
|
|
140
|
+
kind: "method",
|
|
141
|
+
name: "upload",
|
|
142
|
+
static: false,
|
|
143
|
+
private: false,
|
|
144
|
+
access: {
|
|
145
|
+
has: (obj) => "upload" in obj,
|
|
146
|
+
get: (obj) => obj.upload
|
|
147
|
+
},
|
|
148
|
+
metadata: _metadata
|
|
149
|
+
}, null, _instanceExtraInitializers);
|
|
150
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, {
|
|
151
|
+
enumerable: true,
|
|
152
|
+
configurable: true,
|
|
153
|
+
writable: true,
|
|
154
|
+
value: _metadata
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
static inject = [
|
|
158
|
+
"agents",
|
|
159
|
+
"attachments",
|
|
160
|
+
"commands",
|
|
161
|
+
"connection"
|
|
162
|
+
];
|
|
163
|
+
stagedFiles = (__runInitializers(this, _instanceExtraInitializers), /* @__PURE__ */ new WeakMap());
|
|
164
|
+
agentResolver;
|
|
165
|
+
/** @param ctx - Host context carrying Agent, attachment, command, and Connection services. */
|
|
166
|
+
constructor(ctx) {
|
|
167
|
+
super(ctx, "fileUploads");
|
|
168
|
+
const resolve = (agent, receiptId) => this.resolve(agent, receiptId);
|
|
169
|
+
ctx.effect(() => ctx.commands.registerFileReceiptResolver(resolve), "file-upload: command file receipt resolver");
|
|
170
|
+
ctx.effect(() => ctx.connection.fetch.register({
|
|
171
|
+
path: FILE_UPLOAD_PATH,
|
|
172
|
+
methods: ["POST"],
|
|
173
|
+
requestBody: "streaming",
|
|
174
|
+
fetch: (request) => handleFileUploadHttp(this, request)
|
|
175
|
+
}), "file-upload: streaming route");
|
|
176
|
+
ctx.on("session/event", (session, event) => {
|
|
177
|
+
this.observeSessionEvent(session, event);
|
|
178
|
+
});
|
|
179
|
+
ctx.on("session/disposed", (session) => {
|
|
180
|
+
this.stagedFiles.delete(session);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Register the ordinary-Session resolver used when a raw upload addresses a cold Session.
|
|
185
|
+
* @param resolve - resolver that returns the exact live Agent or throws a Remote error.
|
|
186
|
+
* @returns disposer removing this resolver.
|
|
187
|
+
*/
|
|
188
|
+
registerAgentResolver(resolve) {
|
|
189
|
+
if (this.agentResolver !== void 0) throw new Error("file-upload: Agent resolver is already registered");
|
|
190
|
+
this.agentResolver = resolve;
|
|
191
|
+
return () => {
|
|
192
|
+
if (this.agentResolver === resolve) this.agentResolver = void 0;
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Persist one encoded upload and stage it under the Agent receiver selected by Typert.
|
|
197
|
+
* @param agent - receiving Agent resolved from the Remote Agent scope.
|
|
198
|
+
* @param request - canonical base64 bytes and optional display name.
|
|
199
|
+
* @param signal - caller cancellation before storage begins.
|
|
200
|
+
* @returns the staged receipt and durable file reference.
|
|
201
|
+
*/
|
|
202
|
+
upload(agent, request, signal) {
|
|
203
|
+
signal.throwIfAborted();
|
|
204
|
+
return this.commit(agent, async () => this.ctx.attachments.admitEncodedFile({
|
|
205
|
+
data: request.data,
|
|
206
|
+
...request.name === void 0 ? {} : { name: request.name }
|
|
207
|
+
}));
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Persist raw chunks for one Session without aggregating the upload.
|
|
211
|
+
* @param request - Session identity, ordered bytes, cancellation, and optional display name.
|
|
212
|
+
* @returns the staged receipt and durable file reference.
|
|
213
|
+
*/
|
|
214
|
+
async uploadStream(request) {
|
|
215
|
+
const agent = await this.resolveAgent(request.sessionId);
|
|
216
|
+
return this.commit(agent, async () => this.ctx.attachments.saveFileStream({
|
|
217
|
+
data: request.data,
|
|
218
|
+
...request.signal === void 0 ? {} : { signal: request.signal },
|
|
219
|
+
...request.name === void 0 ? {} : { name: request.name }
|
|
220
|
+
}));
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Resolve one staged receipt inside its receiving Agent scope.
|
|
224
|
+
* @param agent - receiving Agent.
|
|
225
|
+
* @param receiptId - opaque receipt minted for one completed upload.
|
|
226
|
+
* @returns durable file reference, or `undefined` for an unknown or foreign receipt.
|
|
227
|
+
*/
|
|
228
|
+
resolve(agent, receiptId) {
|
|
229
|
+
this.assertAgentScope(agent);
|
|
230
|
+
return this.stagedFiles.get(agent.session)?.get(receiptId)?.file;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Bind receipts while one prompt enters an Agent inbox.
|
|
234
|
+
* Disposal restores every prior binding unless the caller commits successful delivery.
|
|
235
|
+
* @param agent - receiving Agent.
|
|
236
|
+
* @param receiptIds - distinct staged receipts referenced by the prompt.
|
|
237
|
+
* @param requestId - prompt identity later observed in queue or history.
|
|
238
|
+
* @returns binding kept after commit until queue or history observation retires its receipts.
|
|
239
|
+
*/
|
|
240
|
+
bindPrompt(agent, receiptIds, requestId) {
|
|
241
|
+
this.assertAgentScope(agent);
|
|
242
|
+
const staged = this.stagedFiles.get(agent.session);
|
|
243
|
+
const bound = receiptIds.map((receiptId) => {
|
|
244
|
+
const upload = staged?.get(receiptId);
|
|
245
|
+
if (upload === void 0) throw fileNotStaged();
|
|
246
|
+
return {
|
|
247
|
+
upload,
|
|
248
|
+
previous: upload.requestId
|
|
249
|
+
};
|
|
250
|
+
});
|
|
251
|
+
for (const { upload } of bound) upload.requestId = requestId;
|
|
252
|
+
return new PromptFileBindingGuard(() => {
|
|
253
|
+
for (const { upload, previous } of bound) if (previous === void 0) delete upload.requestId;
|
|
254
|
+
else upload.requestId = previous;
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Retire every receipt accepted by one removed queue occurrence.
|
|
259
|
+
* @param agent - receiving Agent.
|
|
260
|
+
* @param requestId - prompt identity carried by the queue occurrence.
|
|
261
|
+
*/
|
|
262
|
+
retirePrompt(agent, requestId) {
|
|
263
|
+
this.assertAgentScope(agent);
|
|
264
|
+
this.retire(agent.session, requestId);
|
|
265
|
+
}
|
|
266
|
+
async commit(agent, save) {
|
|
267
|
+
this.assertOrdinaryAgent(agent);
|
|
268
|
+
let file;
|
|
269
|
+
try {
|
|
270
|
+
file = await save();
|
|
271
|
+
} catch (error) {
|
|
272
|
+
if (this.ctx.attachments.isAttachmentError(error)) throw new RemoteError("session/attachment-invalid", error.message, { reason: error.code });
|
|
273
|
+
throw new RemoteError("gateway/internal", `failed to store file upload: ${String(error)}`, {}, { cause: error });
|
|
274
|
+
}
|
|
275
|
+
if (this.ctx.agents.get(agent.id) !== agent) throw new RemoteError("session/not-found", `session "${agent.id}" was disposed before its file upload completed`, { sessionId: agent.id });
|
|
276
|
+
let staged = this.stagedFiles.get(agent.session);
|
|
277
|
+
if (staged === void 0) {
|
|
278
|
+
staged = /* @__PURE__ */ new Map();
|
|
279
|
+
this.stagedFiles.set(agent.session, staged);
|
|
280
|
+
}
|
|
281
|
+
const receiptId = randomUUID();
|
|
282
|
+
staged.set(receiptId, { file });
|
|
283
|
+
return {
|
|
284
|
+
receiptId,
|
|
285
|
+
file
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
async resolveAgent(sessionId) {
|
|
289
|
+
const live = this.ctx.agents.get(sessionId);
|
|
290
|
+
if (live !== void 0) return live;
|
|
291
|
+
const resolver = this.agentResolver;
|
|
292
|
+
if (resolver === void 0) throw new RemoteError("session/not-found", `session "${sessionId}" is not attached`, { sessionId });
|
|
293
|
+
return resolver(sessionId);
|
|
294
|
+
}
|
|
295
|
+
assertAgentScope(agent) {
|
|
296
|
+
if (scopeOf(agent.ctx) !== agent) throw new Error("file-upload: operation requires the Agent's own scope");
|
|
297
|
+
}
|
|
298
|
+
assertOrdinaryAgent(agent) {
|
|
299
|
+
this.assertAgentScope(agent);
|
|
300
|
+
if (agent.session.header.origin === "subagent") throw new RemoteError("subagent/attachment-invalid", "subagent conversations do not accept file uploads", { reason: "SUBAGENT_FILE_UNSUPPORTED" });
|
|
301
|
+
}
|
|
302
|
+
observeSessionEvent(session, event) {
|
|
303
|
+
if (event.type !== "user/message" || event.data.source.kind !== "user" || !("rpcId" in event.data.source)) return;
|
|
304
|
+
if (typeof event.data.source.rpcId === "string") this.retire(session, event.data.source.rpcId);
|
|
305
|
+
}
|
|
306
|
+
retire(session, requestId) {
|
|
307
|
+
const staged = this.stagedFiles.get(session);
|
|
308
|
+
if (staged === void 0) return;
|
|
309
|
+
for (const [receiptId, upload] of staged) if (upload.requestId === requestId) staged.delete(receiptId);
|
|
310
|
+
if (staged.size === 0) this.stagedFiles.delete(session);
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
})();
|
|
314
|
+
function fileNotStaged() {
|
|
315
|
+
return new RemoteError("session/attachment-invalid", "File was not uploaded for this session.", { reason: "FILE_NOT_STAGED" });
|
|
316
|
+
}
|
|
317
|
+
//#endregion
|
|
318
|
+
export { FileUploads, FileUploads as default };
|