@axiom-lattice/gateway 2.1.28 → 2.1.30
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/.turbo/turbo-build.log +12 -8
- package/AGENTS.md +50 -0
- package/CHANGELOG.md +20 -0
- package/dist/chunk-FSASG3SB.mjs +94 -0
- package/dist/chunk-FSASG3SB.mjs.map +1 -0
- package/dist/config-F3FCBSPH.mjs +9 -0
- package/dist/config-F3FCBSPH.mjs.map +1 -0
- package/dist/index.js +1996 -209
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1878 -186
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/controllers/auth.ts +443 -0
- package/src/controllers/database-configs.ts +432 -0
- package/src/controllers/metrics-configs.ts +989 -0
- package/src/controllers/run.ts +6 -0
- package/src/controllers/tenants.ts +121 -0
- package/src/controllers/users.ts +135 -0
- package/src/controllers/workspace.ts +598 -0
- package/src/index.ts +2 -10
- package/src/routes/index.ts +21 -0
- package/src/services/agent_service.ts +71 -5
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkspaceController
|
|
3
|
+
*
|
|
4
|
+
* Controller for workspace and project management
|
|
5
|
+
* Handles CRUD operations, file browsing, and file uploads
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
|
|
9
|
+
import * as fs from "fs/promises";
|
|
10
|
+
import * as path from "path";
|
|
11
|
+
import { Readable } from "stream";
|
|
12
|
+
import { getStoreLattice } from "@axiom-lattice/core";
|
|
13
|
+
import { SandboxFilesystem, FilesystemBackend } from "@axiom-lattice/core";
|
|
14
|
+
import { getSandBoxManager } from "@axiom-lattice/core";
|
|
15
|
+
import { sandboxService } from "../services/sandbox_service";
|
|
16
|
+
import { v4 as uuidv4 } from "uuid";
|
|
17
|
+
import type {
|
|
18
|
+
CreateWorkspaceRequest,
|
|
19
|
+
UpdateWorkspaceRequest,
|
|
20
|
+
CreateProjectRequest,
|
|
21
|
+
UpdateProjectRequest,
|
|
22
|
+
Workspace,
|
|
23
|
+
Project,
|
|
24
|
+
} from "@axiom-lattice/protocols";
|
|
25
|
+
|
|
26
|
+
interface WorkspaceParams {
|
|
27
|
+
workspaceId: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ProjectParams {
|
|
31
|
+
workspaceId: string;
|
|
32
|
+
projectId: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ListPathQuery {
|
|
36
|
+
path?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ReadFileQuery {
|
|
40
|
+
path: string;
|
|
41
|
+
offset?: number;
|
|
42
|
+
limit?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class WorkspaceController {
|
|
46
|
+
private workspaceStore;
|
|
47
|
+
private projectStore;
|
|
48
|
+
|
|
49
|
+
constructor() {
|
|
50
|
+
this.workspaceStore = getStoreLattice("default", "workspace").store;
|
|
51
|
+
this.projectStore = getStoreLattice("default", "project").store;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private getTenantId(request: FastifyRequest): string {
|
|
55
|
+
return (request.headers["x-tenant-id"] as string) || "default";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ==================== Workspace CRUD ====================
|
|
59
|
+
|
|
60
|
+
async listWorkspaces(request: FastifyRequest, reply: FastifyReply) {
|
|
61
|
+
const tenantId = this.getTenantId(request);
|
|
62
|
+
const workspaces = await this.workspaceStore.getAllWorkspaces(tenantId);
|
|
63
|
+
return { success: true, data: workspaces };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async createWorkspace(
|
|
67
|
+
request: FastifyRequest<{ Body: CreateWorkspaceRequest }>,
|
|
68
|
+
reply: FastifyReply
|
|
69
|
+
) {
|
|
70
|
+
const tenantId = this.getTenantId(request);
|
|
71
|
+
const data = request.body;
|
|
72
|
+
const id = uuidv4();
|
|
73
|
+
|
|
74
|
+
const workspace = await this.workspaceStore.createWorkspace(
|
|
75
|
+
tenantId,
|
|
76
|
+
id,
|
|
77
|
+
data
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return reply.status(201).send({ success: true, data: workspace });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async getWorkspace(
|
|
84
|
+
request: FastifyRequest<{ Params: WorkspaceParams }>,
|
|
85
|
+
reply: FastifyReply
|
|
86
|
+
) {
|
|
87
|
+
const tenantId = this.getTenantId(request);
|
|
88
|
+
const { workspaceId } = request.params;
|
|
89
|
+
|
|
90
|
+
const workspace = await this.workspaceStore.getWorkspaceById(
|
|
91
|
+
tenantId,
|
|
92
|
+
workspaceId
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
if (!workspace) {
|
|
96
|
+
return reply.status(404).send({ success: false, error: "Workspace not found" });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { success: true, data: workspace };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async updateWorkspace(
|
|
103
|
+
request: FastifyRequest<{
|
|
104
|
+
Params: WorkspaceParams;
|
|
105
|
+
Body: UpdateWorkspaceRequest;
|
|
106
|
+
}>,
|
|
107
|
+
reply: FastifyReply
|
|
108
|
+
) {
|
|
109
|
+
const tenantId = this.getTenantId(request);
|
|
110
|
+
const { workspaceId } = request.params;
|
|
111
|
+
const updates = request.body;
|
|
112
|
+
|
|
113
|
+
const workspace = await this.workspaceStore.updateWorkspace(
|
|
114
|
+
tenantId,
|
|
115
|
+
workspaceId,
|
|
116
|
+
updates
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
if (!workspace) {
|
|
120
|
+
return reply.status(404).send({ success: false, error: "Workspace not found" });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { success: true, data: workspace };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async deleteWorkspace(
|
|
127
|
+
request: FastifyRequest<{ Params: WorkspaceParams }>,
|
|
128
|
+
reply: FastifyReply
|
|
129
|
+
) {
|
|
130
|
+
const tenantId = this.getTenantId(request);
|
|
131
|
+
const { workspaceId } = request.params;
|
|
132
|
+
|
|
133
|
+
const deleted = await this.workspaceStore.deleteWorkspace(
|
|
134
|
+
tenantId,
|
|
135
|
+
workspaceId
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
if (!deleted) {
|
|
139
|
+
return reply.status(404).send({ success: false, error: "Workspace not found" });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { success: true };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ==================== Project CRUD ====================
|
|
146
|
+
|
|
147
|
+
async listProjects(
|
|
148
|
+
request: FastifyRequest<{ Params: WorkspaceParams }>,
|
|
149
|
+
reply: FastifyReply
|
|
150
|
+
) {
|
|
151
|
+
const tenantId = this.getTenantId(request);
|
|
152
|
+
const { workspaceId } = request.params;
|
|
153
|
+
|
|
154
|
+
const projects = await this.projectStore.getProjectsByWorkspace(
|
|
155
|
+
tenantId,
|
|
156
|
+
workspaceId
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
return { success: true, data: projects };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async createProject(
|
|
163
|
+
request: FastifyRequest<{
|
|
164
|
+
Params: WorkspaceParams;
|
|
165
|
+
Body: CreateProjectRequest;
|
|
166
|
+
}>,
|
|
167
|
+
reply: FastifyReply
|
|
168
|
+
) {
|
|
169
|
+
const tenantId = this.getTenantId(request);
|
|
170
|
+
const { workspaceId } = request.params;
|
|
171
|
+
const data = request.body;
|
|
172
|
+
const id = uuidv4();
|
|
173
|
+
|
|
174
|
+
const project = await this.projectStore.createProject(
|
|
175
|
+
tenantId,
|
|
176
|
+
workspaceId,
|
|
177
|
+
id,
|
|
178
|
+
data
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
return reply.status(201).send({ success: true, data: project });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async getProject(
|
|
185
|
+
request: FastifyRequest<{ Params: ProjectParams }>,
|
|
186
|
+
reply: FastifyReply
|
|
187
|
+
) {
|
|
188
|
+
const tenantId = this.getTenantId(request);
|
|
189
|
+
const { projectId } = request.params;
|
|
190
|
+
|
|
191
|
+
const project = await this.projectStore.getProjectById(tenantId, projectId);
|
|
192
|
+
|
|
193
|
+
if (!project) {
|
|
194
|
+
return reply.status(404).send({ success: false, error: "Project not found" });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { success: true, data: project };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async updateProject(
|
|
201
|
+
request: FastifyRequest<{
|
|
202
|
+
Params: ProjectParams;
|
|
203
|
+
Body: UpdateProjectRequest;
|
|
204
|
+
}>,
|
|
205
|
+
reply: FastifyReply
|
|
206
|
+
) {
|
|
207
|
+
const tenantId = this.getTenantId(request);
|
|
208
|
+
const { projectId } = request.params;
|
|
209
|
+
const updates = request.body;
|
|
210
|
+
|
|
211
|
+
const project = await this.projectStore.updateProject(
|
|
212
|
+
tenantId,
|
|
213
|
+
projectId,
|
|
214
|
+
updates
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
if (!project) {
|
|
218
|
+
return reply.status(404).send({ success: false, error: "Project not found" });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return { success: true, data: project };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async deleteProject(
|
|
225
|
+
request: FastifyRequest<{ Params: ProjectParams }>,
|
|
226
|
+
reply: FastifyReply
|
|
227
|
+
) {
|
|
228
|
+
const tenantId = this.getTenantId(request);
|
|
229
|
+
const { projectId } = request.params;
|
|
230
|
+
|
|
231
|
+
const deleted = await this.projectStore.deleteProject(tenantId, projectId);
|
|
232
|
+
|
|
233
|
+
if (!deleted) {
|
|
234
|
+
return reply.status(404).send({ success: false, error: "Project not found" });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return { success: true };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ==================== File Operations ====================
|
|
241
|
+
|
|
242
|
+
private async getBackend(workspaceId: string, projectId: string) {
|
|
243
|
+
const tenantId = "default";
|
|
244
|
+
const workspace = await this.workspaceStore.getWorkspaceById(
|
|
245
|
+
tenantId,
|
|
246
|
+
workspaceId
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
if (!workspace) {
|
|
250
|
+
throw new Error("Workspace not found");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (workspace.storageType === "sandbox") {
|
|
254
|
+
const sandboxManager = getSandBoxManager("default");
|
|
255
|
+
const sandboxName = "global";
|
|
256
|
+
const sandbox = await sandboxManager.createSandbox(sandboxName);
|
|
257
|
+
return { backend: new SandboxFilesystem({
|
|
258
|
+
sandboxInstance: sandbox,
|
|
259
|
+
workingDirectory: `/workspaces/${workspaceId}/${projectId}`,
|
|
260
|
+
}), workspace };
|
|
261
|
+
} else {
|
|
262
|
+
return { backend: new FilesystemBackend({
|
|
263
|
+
rootDir: `/lattice_store/workspaces/${workspaceId}/${projectId}`,
|
|
264
|
+
virtualMode: true,
|
|
265
|
+
}), workspace };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private getFilenameFromPath(filePath: string): string {
|
|
270
|
+
const segments = filePath.split("/");
|
|
271
|
+
return segments[segments.length - 1] || "download";
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private getMimeType(filename: string): string {
|
|
275
|
+
const ext = filename.split(".").pop()?.toLowerCase() || "";
|
|
276
|
+
const mimeTypes: Record<string, string> = {
|
|
277
|
+
pdf: "application/pdf",
|
|
278
|
+
csv: "text/csv",
|
|
279
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
280
|
+
xls: "application/vnd.ms-excel",
|
|
281
|
+
txt: "text/plain",
|
|
282
|
+
json: "application/json",
|
|
283
|
+
png: "image/png",
|
|
284
|
+
jpg: "image/jpeg",
|
|
285
|
+
jpeg: "image/jpeg",
|
|
286
|
+
gif: "image/gif",
|
|
287
|
+
svg: "image/svg+xml",
|
|
288
|
+
html: "text/html",
|
|
289
|
+
htm: "text/html",
|
|
290
|
+
mp3: "audio/mpeg",
|
|
291
|
+
mp4: "video/mp4",
|
|
292
|
+
webm: "video/webm",
|
|
293
|
+
};
|
|
294
|
+
return mimeTypes[ext] || "application/octet-stream";
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async downloadFile(
|
|
298
|
+
request: FastifyRequest<{
|
|
299
|
+
Params: ProjectParams;
|
|
300
|
+
Querystring: { path: string };
|
|
301
|
+
}>,
|
|
302
|
+
reply: FastifyReply
|
|
303
|
+
) {
|
|
304
|
+
const { workspaceId, projectId } = request.params;
|
|
305
|
+
const filePath = request.query.path;
|
|
306
|
+
|
|
307
|
+
if (!filePath) {
|
|
308
|
+
return reply.status(400).send({ success: false, error: "Path is required" });
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
const { workspace } = await this.getBackend(workspaceId, projectId);
|
|
313
|
+
const resolvedPath = filePath.startsWith("/") ? filePath : `/${filePath}`;
|
|
314
|
+
|
|
315
|
+
if (workspace.storageType === "sandbox") {
|
|
316
|
+
const sandboxManager = getSandBoxManager("default");
|
|
317
|
+
const sandbox = await sandboxManager.createSandbox("global");
|
|
318
|
+
const realPath = path.join("/home/gem/workspaces", workspaceId, projectId, resolvedPath);
|
|
319
|
+
const filename = this.getFilenameFromPath(resolvedPath);
|
|
320
|
+
const inferredContentType = this.getMimeType(filename);
|
|
321
|
+
|
|
322
|
+
const downloadResult = await sandbox.file.downloadFile({
|
|
323
|
+
path: realPath,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
if (!downloadResult.ok) {
|
|
327
|
+
return reply.status(502).send({
|
|
328
|
+
success: false,
|
|
329
|
+
error: `Download error: ${JSON.stringify(downloadResult.error)}`,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const body = downloadResult.body as unknown as {
|
|
334
|
+
stream?: () => ReadableStream<Uint8Array>;
|
|
335
|
+
contentType?: string;
|
|
336
|
+
contentDisposition?: string;
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
if (typeof body?.stream === "function") {
|
|
340
|
+
const webStream = body.stream();
|
|
341
|
+
const nodeStream = Readable.fromWeb(webStream);
|
|
342
|
+
const contentType = body.contentType ?? inferredContentType;
|
|
343
|
+
const contentDisposition =
|
|
344
|
+
body.contentDisposition ?? `inline; filename*=UTF-8''${encodeURIComponent(filename)}`;
|
|
345
|
+
return reply.status(200).type(contentType).header("Content-Disposition", contentDisposition).send(nodeStream);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const bodyUnknown = downloadResult.body as unknown;
|
|
349
|
+
let buf: Buffer;
|
|
350
|
+
let contentType = inferredContentType;
|
|
351
|
+
let contentDisposition = `inline; filename*=UTF-8''${encodeURIComponent(filename)}`;
|
|
352
|
+
|
|
353
|
+
if (bodyUnknown instanceof ArrayBuffer) {
|
|
354
|
+
buf = Buffer.from(bodyUnknown);
|
|
355
|
+
} else if (bodyUnknown instanceof Buffer) {
|
|
356
|
+
buf = bodyUnknown;
|
|
357
|
+
} else if (
|
|
358
|
+
bodyUnknown &&
|
|
359
|
+
typeof (bodyUnknown as { arrayBuffer?: () => Promise<ArrayBuffer> }).arrayBuffer === "function"
|
|
360
|
+
) {
|
|
361
|
+
const res = bodyUnknown as { arrayBuffer: () => Promise<ArrayBuffer>; headers?: Headers };
|
|
362
|
+
buf = Buffer.from(await res.arrayBuffer());
|
|
363
|
+
if (res.headers?.get("content-type")) contentType = res.headers.get("content-type")!;
|
|
364
|
+
if (res.headers?.get("content-disposition")) contentDisposition = res.headers.get("content-disposition")!;
|
|
365
|
+
} else if (bodyUnknown && typeof (bodyUnknown as { blob?: () => Promise<Blob> }).blob === "function") {
|
|
366
|
+
const blob = await (bodyUnknown as { blob: () => Promise<Blob> }).blob();
|
|
367
|
+
buf = Buffer.from(await blob.arrayBuffer());
|
|
368
|
+
} else {
|
|
369
|
+
return reply.status(502).send({ success: false, error: "Unexpected download response format" });
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return reply.status(200).type(contentType).header("Content-Disposition", contentDisposition).send(buf);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const { backend } = await this.getBackend(workspaceId, projectId);
|
|
376
|
+
const content = await backend.read(resolvedPath, 0, Infinity);
|
|
377
|
+
const filename = this.getFilenameFromPath(resolvedPath);
|
|
378
|
+
const mimeType = this.getMimeType(filename);
|
|
379
|
+
const buffer = Buffer.from(content, "utf-8");
|
|
380
|
+
|
|
381
|
+
return reply
|
|
382
|
+
.status(200)
|
|
383
|
+
.type(mimeType)
|
|
384
|
+
.header("Content-Disposition", `inline; filename*=UTF-8''${encodeURIComponent(filename)}`)
|
|
385
|
+
.send(buffer);
|
|
386
|
+
} catch (error: unknown) {
|
|
387
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
388
|
+
return reply.status(502).send({ success: false, error: `Download proxy error: ${message}` });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async listPath(
|
|
393
|
+
request: FastifyRequest<{
|
|
394
|
+
Params: ProjectParams;
|
|
395
|
+
Querystring: ListPathQuery;
|
|
396
|
+
}>
|
|
397
|
+
) {
|
|
398
|
+
const { workspaceId, projectId } = request.params;
|
|
399
|
+
const path = (request.query.path as string) || "/";
|
|
400
|
+
|
|
401
|
+
const { backend } = await this.getBackend(workspaceId, projectId);
|
|
402
|
+
const files = await backend.lsInfo(path);
|
|
403
|
+
|
|
404
|
+
return { success: true, data: files };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async readFile(
|
|
408
|
+
request: FastifyRequest<{
|
|
409
|
+
Params: ProjectParams;
|
|
410
|
+
Querystring: ReadFileQuery;
|
|
411
|
+
}>
|
|
412
|
+
) {
|
|
413
|
+
const { workspaceId, projectId } = request.params;
|
|
414
|
+
const { path, offset = 0, limit = 1000 } = request.query;
|
|
415
|
+
|
|
416
|
+
const { backend } = await this.getBackend(workspaceId, projectId);
|
|
417
|
+
const content = await backend.read(path, Number(offset), Number(limit));
|
|
418
|
+
|
|
419
|
+
return { success: true, data: { content, offset, limit } };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Upload a file to the workspace project storage.
|
|
424
|
+
*
|
|
425
|
+
* Route: POST /api/workspaces/:workspaceId/projects/:projectId/uploadfile
|
|
426
|
+
* Accepts multipart/form-data with:
|
|
427
|
+
* - `file`: the file to upload (required)
|
|
428
|
+
* - `path`: optional directory path relative to project root (e.g., "docs", "src/utils")
|
|
429
|
+
* For sandbox storage, delegates to the sandbox upload API.
|
|
430
|
+
* For filesystem storage, writes directly under /lattice_store/workspaces/{workspaceId}/{projectId}.
|
|
431
|
+
*/
|
|
432
|
+
async uploadFile(
|
|
433
|
+
request: FastifyRequest<{
|
|
434
|
+
Params: ProjectParams;
|
|
435
|
+
}>,
|
|
436
|
+
reply: FastifyReply
|
|
437
|
+
) {
|
|
438
|
+
const tenantId = this.getTenantId(request);
|
|
439
|
+
const { workspaceId, projectId } = request.params;
|
|
440
|
+
|
|
441
|
+
const workspace = await this.workspaceStore.getWorkspaceById(
|
|
442
|
+
tenantId,
|
|
443
|
+
workspaceId
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
if (!workspace) {
|
|
447
|
+
return reply
|
|
448
|
+
.status(404)
|
|
449
|
+
.send({ success: false, error: "Workspace not found" });
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
const data = await (request as any).file();
|
|
454
|
+
if (!data) {
|
|
455
|
+
return reply
|
|
456
|
+
.status(400)
|
|
457
|
+
.send({ success: false, error: "No file in request" });
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const buffer: Buffer = await data.toBuffer();
|
|
461
|
+
const filename: string = data.filename || "file";
|
|
462
|
+
|
|
463
|
+
const pathEntry = data.fields?.path;
|
|
464
|
+
const pathValue =
|
|
465
|
+
pathEntry && typeof pathEntry === "object" && "value" in pathEntry
|
|
466
|
+
? String((pathEntry as { value: unknown }).value)
|
|
467
|
+
: typeof pathEntry === "string"
|
|
468
|
+
? pathEntry
|
|
469
|
+
: undefined;
|
|
470
|
+
|
|
471
|
+
if (pathValue && !/^[a-zA-Z0-9_./-]+$/.test(pathValue)) {
|
|
472
|
+
return reply
|
|
473
|
+
.status(400)
|
|
474
|
+
.send({ success: false, error: "Invalid path parameter" });
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (workspace.storageType === "sandbox") {
|
|
478
|
+
const sandboxManager = getSandBoxManager("default");
|
|
479
|
+
const sandboxName = "global";
|
|
480
|
+
const sandbox = await sandboxManager.createSandbox(sandboxName);
|
|
481
|
+
|
|
482
|
+
const baseDir = path.join("/home/gem/workspaces", workspaceId, projectId);
|
|
483
|
+
const realPath = pathValue
|
|
484
|
+
? path.join(baseDir, pathValue, filename)
|
|
485
|
+
: path.join(baseDir, filename);
|
|
486
|
+
|
|
487
|
+
const uploadResult = await sandbox.file.uploadFile({
|
|
488
|
+
file: buffer,
|
|
489
|
+
path: realPath,
|
|
490
|
+
}, { timeoutInSeconds: 300 });
|
|
491
|
+
|
|
492
|
+
if (!uploadResult.ok) {
|
|
493
|
+
return reply.status(502).send({
|
|
494
|
+
success: false,
|
|
495
|
+
error: `Upload error: ${JSON.stringify(uploadResult.error)}`,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const relativePath =
|
|
500
|
+
uploadResult.body?.data?.file_path?.replace(path.join("/home/gem/workspaces", workspaceId, projectId), "") ||
|
|
501
|
+
(pathValue ? `/${pathValue}/${filename}` : `/${filename}`);
|
|
502
|
+
|
|
503
|
+
const result = {
|
|
504
|
+
path: relativePath,
|
|
505
|
+
name: filename,
|
|
506
|
+
is_dir: false,
|
|
507
|
+
size: buffer.length,
|
|
508
|
+
modified_at: new Date().toISOString(),
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
return reply.status(200).send({ success: true, data: result });
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Filesystem storage: write under /lattice_store/workspaces/{workspaceId}/{projectId} and expose as virtual path "/<filename>"
|
|
515
|
+
const rootDir = `/lattice_store/workspaces/${workspaceId}/${projectId}`;
|
|
516
|
+
const targetDir = pathValue ? path.join(rootDir, pathValue) : rootDir;
|
|
517
|
+
const targetPath = path.join(targetDir, filename);
|
|
518
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
519
|
+
await fs.writeFile(targetPath, buffer);
|
|
520
|
+
const stat = await fs.stat(targetPath);
|
|
521
|
+
|
|
522
|
+
const result = {
|
|
523
|
+
path: pathValue ? `/${pathValue}/${filename}` : `/${filename}`,
|
|
524
|
+
name: filename,
|
|
525
|
+
is_dir: false,
|
|
526
|
+
size: stat.size,
|
|
527
|
+
modified_at: stat.mtime.toISOString(),
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
return reply.status(200).send({ success: true, data: result });
|
|
531
|
+
} catch (error: any) {
|
|
532
|
+
return reply.status(500).send({
|
|
533
|
+
success: false,
|
|
534
|
+
error: `Upload handler error: ${error?.message || String(error)}`,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export function registerWorkspaceRoutes(app: FastifyInstance) {
|
|
541
|
+
const controller = new WorkspaceController();
|
|
542
|
+
|
|
543
|
+
// Workspace routes
|
|
544
|
+
app.get("/api/workspaces", controller.listWorkspaces.bind(controller));
|
|
545
|
+
app.post("/api/workspaces", controller.createWorkspace.bind(controller));
|
|
546
|
+
app.get(
|
|
547
|
+
"/api/workspaces/:workspaceId",
|
|
548
|
+
controller.getWorkspace.bind(controller)
|
|
549
|
+
);
|
|
550
|
+
app.patch(
|
|
551
|
+
"/api/workspaces/:workspaceId",
|
|
552
|
+
controller.updateWorkspace.bind(controller)
|
|
553
|
+
);
|
|
554
|
+
app.delete(
|
|
555
|
+
"/api/workspaces/:workspaceId",
|
|
556
|
+
controller.deleteWorkspace.bind(controller)
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
// Project routes
|
|
560
|
+
app.get(
|
|
561
|
+
"/api/workspaces/:workspaceId/projects",
|
|
562
|
+
controller.listProjects.bind(controller)
|
|
563
|
+
);
|
|
564
|
+
app.post(
|
|
565
|
+
"/api/workspaces/:workspaceId/projects",
|
|
566
|
+
controller.createProject.bind(controller)
|
|
567
|
+
);
|
|
568
|
+
app.get(
|
|
569
|
+
"/api/workspaces/:workspaceId/projects/:projectId",
|
|
570
|
+
controller.getProject.bind(controller)
|
|
571
|
+
);
|
|
572
|
+
app.patch(
|
|
573
|
+
"/api/workspaces/:workspaceId/projects/:projectId",
|
|
574
|
+
controller.updateProject.bind(controller)
|
|
575
|
+
);
|
|
576
|
+
app.delete(
|
|
577
|
+
"/api/workspaces/:workspaceId/projects/:projectId",
|
|
578
|
+
controller.deleteProject.bind(controller)
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
// File operations
|
|
582
|
+
app.get(
|
|
583
|
+
"/api/workspaces/:workspaceId/projects/:projectId/listpath",
|
|
584
|
+
controller.listPath.bind(controller)
|
|
585
|
+
);
|
|
586
|
+
app.get(
|
|
587
|
+
"/api/workspaces/:workspaceId/projects/:projectId/readfile",
|
|
588
|
+
controller.readFile.bind(controller)
|
|
589
|
+
);
|
|
590
|
+
app.post(
|
|
591
|
+
"/api/workspaces/:workspaceId/projects/:projectId/uploadfile",
|
|
592
|
+
controller.uploadFile.bind(controller)
|
|
593
|
+
);
|
|
594
|
+
app.get(
|
|
595
|
+
"/api/workspaces/:workspaceId/projects/:projectId/downloadfile",
|
|
596
|
+
controller.downloadFile.bind(controller)
|
|
597
|
+
);
|
|
598
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -108,16 +108,8 @@ app.addHook("onResponse", (request, reply, done) => {
|
|
|
108
108
|
// cors
|
|
109
109
|
app.register(cors, {
|
|
110
110
|
origin: true,
|
|
111
|
-
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
|
112
|
-
allowedHeaders:
|
|
113
|
-
"Content-Type",
|
|
114
|
-
"Authorization",
|
|
115
|
-
"X-Requested-With",
|
|
116
|
-
"x-tenant-id",
|
|
117
|
-
"x-request-id",
|
|
118
|
-
"x-assistant-id",
|
|
119
|
-
"x-thread-id",
|
|
120
|
-
],
|
|
111
|
+
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"],
|
|
112
|
+
allowedHeaders: "*",
|
|
121
113
|
exposedHeaders: ["Content-Type"],
|
|
122
114
|
credentials: true,
|
|
123
115
|
});
|
package/src/routes/index.ts
CHANGED
|
@@ -29,6 +29,12 @@ import {
|
|
|
29
29
|
getSandboxUrlSchema,
|
|
30
30
|
} from "../schemas";
|
|
31
31
|
import { registerSandboxProxyRoutes } from "../controllers/sandbox";
|
|
32
|
+
import { registerWorkspaceRoutes } from "../controllers/workspace";
|
|
33
|
+
import { registerDatabaseConfigRoutes } from "../controllers/database-configs";
|
|
34
|
+
import { registerMetricsServerConfigRoutes } from "../controllers/metrics-configs";
|
|
35
|
+
import { registerUserRoutes } from "../controllers/users";
|
|
36
|
+
import { registerTenantRoutes } from "../controllers/tenants";
|
|
37
|
+
import { registerAuthRoutes } from "../controllers/auth";
|
|
32
38
|
|
|
33
39
|
export const registerLatticeRoutes = (app: FastifyInstance): void => {
|
|
34
40
|
// 运行路由
|
|
@@ -293,4 +299,19 @@ export const registerLatticeRoutes = (app: FastifyInstance): void => {
|
|
|
293
299
|
);
|
|
294
300
|
|
|
295
301
|
registerSandboxProxyRoutes(app);
|
|
302
|
+
|
|
303
|
+
registerWorkspaceRoutes(app);
|
|
304
|
+
|
|
305
|
+
registerDatabaseConfigRoutes(app);
|
|
306
|
+
|
|
307
|
+
registerMetricsServerConfigRoutes(app);
|
|
308
|
+
|
|
309
|
+
registerUserRoutes(app);
|
|
310
|
+
|
|
311
|
+
registerTenantRoutes(app);
|
|
312
|
+
|
|
313
|
+
registerAuthRoutes(app, {
|
|
314
|
+
autoApproveUsers: process.env.AUTO_APPROVE_USERS !== "false",
|
|
315
|
+
allowTenantRegistration: process.env.ALLOW_TENANT_REGISTRATION !== "false",
|
|
316
|
+
});
|
|
296
317
|
};
|