@axiom-lattice/gateway 3.0.2 → 3.0.4
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/dist/index.js +37 -9
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +37 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -6
- package/.eslintrc.json +0 -22
- package/.turbo/turbo-build.log +0 -46
- package/AGENTS.md +0 -112
- package/CHANGELOG.md +0 -1190
- package/LOGGER_CONFIG.md +0 -135
- package/RESUME_STREAM_CONTENT_BASED.md +0 -326
- package/RESUME_STREAM_README.md +0 -388
- package/jest.config.js +0 -72
- package/public/sdk/README.md +0 -695
- package/public/sdk/dashboard-engine-skill.md +0 -1122
- package/public/sdk/dashboard-single-file-spec.md +0 -357
- package/public/sdk/data-query-sdk-skill.md +0 -307
- package/public/sdk/data-query-sdk.d.ts +0 -252
- package/public/sdk/data-query-sdk.js +0 -970
- package/public/sdk/occupancy-dashboard.html +0 -363
- package/public/sdk/test-dashboard.html +0 -690
- package/scripts/seed-lark-installation.ts +0 -54
- package/src/__tests__/__mocks__/e2b.ts +0 -1
- package/src/__tests__/a2a.test.ts +0 -428
- package/src/__tests__/channel-installations.test.ts +0 -199
- package/src/__tests__/data-query.test.ts +0 -77
- package/src/__tests__/sandbox-provider-registration.test.ts +0 -74
- package/src/__tests__/setup.ts +0 -5
- package/src/__tests__/workspace.test.ts +0 -400
- package/src/channels/__tests__/routes.test.ts +0 -50
- package/src/channels/lark/LarkChannelAdapter.ts +0 -217
- package/src/channels/lark/README.md +0 -187
- package/src/channels/lark/__tests__/controller.test.ts +0 -136
- package/src/channels/lark/__tests__/mapping-service.test.ts +0 -118
- package/src/channels/lark/__tests__/parser.test.ts +0 -72
- package/src/channels/lark/__tests__/sender.test.ts +0 -37
- package/src/channels/lark/__tests__/verification.test.ts +0 -157
- package/src/channels/lark/controller.ts +0 -48
- package/src/channels/lark/mapping-service.ts +0 -138
- package/src/channels/lark/parser.ts +0 -68
- package/src/channels/lark/routes.ts +0 -22
- package/src/channels/lark/sender.ts +0 -58
- package/src/channels/lark/types.ts +0 -20
- package/src/channels/lark/verification.ts +0 -32
- package/src/channels/registry.ts +0 -20
- package/src/channels/routes.ts +0 -30
- package/src/channels/wechat/WechatChannelAdapter.ts +0 -294
- package/src/channels/wechat/__tests__/context-store.test.ts +0 -148
- package/src/channels/wechat/context-store.ts +0 -54
- package/src/channels/wechat/controller.ts +0 -59
- package/src/channels/wechat/routes.ts +0 -7
- package/src/channels/wechat/types.ts +0 -62
- package/src/channels/wechat/wechat-client.ts +0 -162
- package/src/config/local-a2a-templates.ts +0 -59
- package/src/config.ts +0 -124
- package/src/controllers/__tests__/assistant-local-a2a.test.ts +0 -147
- package/src/controllers/__tests__/local-a2a-templates.test.ts +0 -112
- package/src/controllers/__tests__/mime.test.ts +0 -74
- package/src/controllers/__tests__/resources.test.ts +0 -122
- package/src/controllers/__tests__/run.test.ts +0 -115
- package/src/controllers/__tests__/tasks.test.ts +0 -215
- package/src/controllers/__tests__/workflow-tracking.test.ts +0 -113
- package/src/controllers/agent_task.ts +0 -72
- package/src/controllers/assistant.ts +0 -490
- package/src/controllers/auth.ts +0 -524
- package/src/controllers/channel-bindings.ts +0 -135
- package/src/controllers/channel-installations.ts +0 -409
- package/src/controllers/collections.ts +0 -176
- package/src/controllers/config.ts +0 -126
- package/src/controllers/connections.ts +0 -181
- package/src/controllers/data-query.ts +0 -236
- package/src/controllers/database-configs.ts +0 -440
- package/src/controllers/embeddings.ts +0 -22
- package/src/controllers/entries.ts +0 -167
- package/src/controllers/eval.ts +0 -513
- package/src/controllers/export-import.ts +0 -166
- package/src/controllers/health.ts +0 -36
- package/src/controllers/local-a2a-templates.ts +0 -18
- package/src/controllers/local-a2a.ts +0 -149
- package/src/controllers/mcp-configs.ts +0 -673
- package/src/controllers/memory.ts +0 -257
- package/src/controllers/menu-items.ts +0 -114
- package/src/controllers/metrics-configs.ts +0 -1061
- package/src/controllers/middleware-types.ts +0 -15
- package/src/controllers/models.ts +0 -199
- package/src/controllers/personal-assistant.ts +0 -191
- package/src/controllers/resources.ts +0 -477
- package/src/controllers/run.ts +0 -338
- package/src/controllers/sandbox.ts +0 -255
- package/src/controllers/schedules.ts +0 -336
- package/src/controllers/skills.ts +0 -256
- package/src/controllers/tasks.ts +0 -213
- package/src/controllers/tenants.ts +0 -121
- package/src/controllers/thread_status.ts +0 -66
- package/src/controllers/threads.ts +0 -260
- package/src/controllers/tools.ts +0 -139
- package/src/controllers/users.ts +0 -135
- package/src/controllers/workflow-tracking.ts +0 -693
- package/src/controllers/workspace.ts +0 -879
- package/src/export_registrations/a2a-api-key.registration.ts +0 -89
- package/src/export_registrations/agent.registration.ts +0 -80
- package/src/export_registrations/binding.registration.ts +0 -106
- package/src/export_registrations/channel-installation.registration.ts +0 -88
- package/src/export_registrations/collection.registration.ts +0 -85
- package/src/export_registrations/connection.registration.ts +0 -97
- package/src/export_registrations/database-config.registration.ts +0 -94
- package/src/export_registrations/eval.registration.ts +0 -334
- package/src/export_registrations/index.ts +0 -31
- package/src/export_registrations/mcp-config.registration.ts +0 -97
- package/src/export_registrations/menu.registration.ts +0 -91
- package/src/export_registrations/metrics-config.registration.ts +0 -94
- package/src/export_registrations/skill.registration.ts +0 -88
- package/src/export_registrations/task.registration.ts +0 -99
- package/src/index.ts +0 -497
- package/src/logger/Logger.ts +0 -186
- package/src/router/MessageContext.ts +0 -14
- package/src/router/MessageRouter.ts +0 -490
- package/src/router/middlewares/auditLogger.ts +0 -34
- package/src/router/middlewares/deduplication.ts +0 -38
- package/src/router/middlewares/index.ts +0 -3
- package/src/router/middlewares/rateLimit.ts +0 -46
- package/src/routes/__tests__/a2a-test-connection.test.ts +0 -177
- package/src/routes/a2a-bridge.ts +0 -266
- package/src/routes/a2a-test-connection.ts +0 -126
- package/src/routes/a2a.ts +0 -851
- package/src/routes/channel-bindings.ts +0 -11
- package/src/routes/channel-installations.ts +0 -33
- package/src/routes/index.ts +0 -640
- package/src/routes/menu-items.ts +0 -10
- package/src/routes/resource-routes.ts +0 -35
- package/src/routes/stt.ts +0 -102
- package/src/schemas/data-query.ts +0 -69
- package/src/schemas/index.ts +0 -852
- package/src/services/ExportImportService.ts +0 -296
- package/src/services/__tests__/ExportImportService.test.ts +0 -130
- package/src/services/__tests__/eval-runner.test.ts +0 -131
- package/src/services/__tests__/local-a2a.test.ts +0 -124
- package/src/services/agent_task_consumer.ts +0 -348
- package/src/services/collection_service.ts +0 -258
- package/src/services/eval-runner.ts +0 -283
- package/src/services/local-a2a.ts +0 -354
- package/src/services/queue_service.ts +0 -104
- package/src/services/sandbox_service.ts +0 -223
- package/src/services/skill_service.ts +0 -97
- package/src/services/supabase.ts +0 -60
- package/src/swagger.ts +0 -77
- package/src/types/index.ts +0 -118
- package/src/types/koa.d.ts +0 -7
- package/src/utils/__tests__/local-a2a-runtime.test.ts +0 -77
- package/src/utils/__tests__/path-containment.test.ts +0 -35
- package/src/utils/a2a-url.ts +0 -20
- package/src/utils/local-a2a-runtime.ts +0 -107
- package/src/utils/mime.ts +0 -44
- package/src/utils/path-containment.ts +0 -38
- package/tsconfig.json +0 -26
|
@@ -1,879 +0,0 @@
|
|
|
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 { constants as fsConstants } from "fs";
|
|
12
|
-
import * as os from "os";
|
|
13
|
-
import { getStoreLattice } from "@axiom-lattice/core";
|
|
14
|
-
import { SandboxFilesystem, FilesystemBackend } from "@axiom-lattice/core";
|
|
15
|
-
import { getSandBoxManager } from "@axiom-lattice/core";
|
|
16
|
-
import { v4 as uuidv4 } from "uuid";
|
|
17
|
-
import { getContentTypeFromFilename, getFilenameFromPath } from "../utils/mime";
|
|
18
|
-
import {
|
|
19
|
-
isPathWithinRoot,
|
|
20
|
-
PathOutsideRootError,
|
|
21
|
-
resolvePathWithinRoot,
|
|
22
|
-
} from "../utils/path-containment";
|
|
23
|
-
import type {
|
|
24
|
-
CreateWorkspaceRequest,
|
|
25
|
-
UpdateWorkspaceRequest,
|
|
26
|
-
CreateProjectRequest,
|
|
27
|
-
UpdateProjectRequest,
|
|
28
|
-
Workspace,
|
|
29
|
-
Project,
|
|
30
|
-
} from "@axiom-lattice/protocols";
|
|
31
|
-
|
|
32
|
-
interface WorkspaceParams {
|
|
33
|
-
workspaceId: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface ProjectParams {
|
|
37
|
-
workspaceId: string;
|
|
38
|
-
projectId: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
interface ListPathQuery {
|
|
42
|
-
path?: string;
|
|
43
|
-
assistantId?: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
interface ReadFileQuery {
|
|
47
|
-
path: string;
|
|
48
|
-
offset?: number;
|
|
49
|
-
limit?: number;
|
|
50
|
-
assistantId?: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export class WorkspaceController {
|
|
54
|
-
private workspaceStore;
|
|
55
|
-
private projectStore;
|
|
56
|
-
|
|
57
|
-
constructor() {
|
|
58
|
-
this.workspaceStore = getStoreLattice("default", "workspace").store;
|
|
59
|
-
this.projectStore = getStoreLattice("default", "project").store;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
private getTenantId(request: FastifyRequest): string {
|
|
63
|
-
// First try to get from authenticated user context
|
|
64
|
-
const userTenantId = (request as any).user?.tenantId;
|
|
65
|
-
if (userTenantId) {
|
|
66
|
-
return userTenantId;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Try to get from query parameters (for direct URL access like file downloads)
|
|
70
|
-
const queryTenantId = (request.query as any)?.tenantId;
|
|
71
|
-
if (queryTenantId) {
|
|
72
|
-
return String(queryTenantId);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Fallback to request headers for backward compatibility
|
|
76
|
-
return (request.headers["x-tenant-id"] as string) || "default";
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ==================== Workspace CRUD ====================
|
|
80
|
-
|
|
81
|
-
async listWorkspaces(request: FastifyRequest, reply: FastifyReply) {
|
|
82
|
-
const tenantId = this.getTenantId(request);
|
|
83
|
-
const workspaces = await this.workspaceStore.getAllWorkspaces(tenantId);
|
|
84
|
-
return { success: true, data: workspaces };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async createWorkspace(
|
|
88
|
-
request: FastifyRequest<{ Body: CreateWorkspaceRequest }>,
|
|
89
|
-
reply: FastifyReply
|
|
90
|
-
) {
|
|
91
|
-
const tenantId = this.getTenantId(request);
|
|
92
|
-
const data = request.body;
|
|
93
|
-
const id = uuidv4();
|
|
94
|
-
|
|
95
|
-
const workspace = await this.workspaceStore.createWorkspace(
|
|
96
|
-
tenantId,
|
|
97
|
-
id,
|
|
98
|
-
data
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
return reply.status(201).send({ success: true, data: workspace });
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async getWorkspace(
|
|
105
|
-
request: FastifyRequest<{ Params: WorkspaceParams }>,
|
|
106
|
-
reply: FastifyReply
|
|
107
|
-
) {
|
|
108
|
-
const tenantId = this.getTenantId(request);
|
|
109
|
-
const { workspaceId } = request.params;
|
|
110
|
-
|
|
111
|
-
const workspace = await this.workspaceStore.getWorkspaceById(
|
|
112
|
-
tenantId,
|
|
113
|
-
workspaceId
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
if (!workspace) {
|
|
117
|
-
return reply.status(404).send({ success: false, error: "Workspace not found" });
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return { success: true, data: workspace };
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async updateWorkspace(
|
|
124
|
-
request: FastifyRequest<{
|
|
125
|
-
Params: WorkspaceParams;
|
|
126
|
-
Body: UpdateWorkspaceRequest;
|
|
127
|
-
}>,
|
|
128
|
-
reply: FastifyReply
|
|
129
|
-
) {
|
|
130
|
-
const tenantId = this.getTenantId(request);
|
|
131
|
-
const { workspaceId } = request.params;
|
|
132
|
-
const updates = request.body;
|
|
133
|
-
|
|
134
|
-
const workspace = await this.workspaceStore.updateWorkspace(
|
|
135
|
-
tenantId,
|
|
136
|
-
workspaceId,
|
|
137
|
-
updates
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
if (!workspace) {
|
|
141
|
-
return reply.status(404).send({ success: false, error: "Workspace not found" });
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return { success: true, data: workspace };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async deleteWorkspace(
|
|
148
|
-
request: FastifyRequest<{ Params: WorkspaceParams }>,
|
|
149
|
-
reply: FastifyReply
|
|
150
|
-
) {
|
|
151
|
-
const tenantId = this.getTenantId(request);
|
|
152
|
-
const { workspaceId } = request.params;
|
|
153
|
-
|
|
154
|
-
const deleted = await this.workspaceStore.deleteWorkspace(
|
|
155
|
-
tenantId,
|
|
156
|
-
workspaceId
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
if (!deleted) {
|
|
160
|
-
return reply.status(404).send({ success: false, error: "Workspace not found" });
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return { success: true };
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// ==================== Project CRUD ====================
|
|
167
|
-
|
|
168
|
-
async listProjects(
|
|
169
|
-
request: FastifyRequest<{ Params: WorkspaceParams }>,
|
|
170
|
-
reply: FastifyReply
|
|
171
|
-
) {
|
|
172
|
-
const tenantId = this.getTenantId(request);
|
|
173
|
-
const { workspaceId } = request.params;
|
|
174
|
-
|
|
175
|
-
const projects = await this.projectStore.getProjectsByWorkspace(
|
|
176
|
-
tenantId,
|
|
177
|
-
workspaceId
|
|
178
|
-
);
|
|
179
|
-
|
|
180
|
-
return { success: true, data: projects };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
async createProject(
|
|
184
|
-
request: FastifyRequest<{
|
|
185
|
-
Params: WorkspaceParams;
|
|
186
|
-
Body: CreateProjectRequest;
|
|
187
|
-
}>,
|
|
188
|
-
reply: FastifyReply
|
|
189
|
-
) {
|
|
190
|
-
const tenantId = this.getTenantId(request);
|
|
191
|
-
const { workspaceId } = request.params;
|
|
192
|
-
const data = request.body;
|
|
193
|
-
const id = uuidv4();
|
|
194
|
-
|
|
195
|
-
const project = await this.projectStore.createProject(
|
|
196
|
-
tenantId,
|
|
197
|
-
workspaceId,
|
|
198
|
-
id,
|
|
199
|
-
data
|
|
200
|
-
);
|
|
201
|
-
|
|
202
|
-
return reply.status(201).send({ success: true, data: project });
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
async getProject(
|
|
206
|
-
request: FastifyRequest<{ Params: ProjectParams }>,
|
|
207
|
-
reply: FastifyReply
|
|
208
|
-
) {
|
|
209
|
-
const tenantId = this.getTenantId(request);
|
|
210
|
-
const { projectId } = request.params;
|
|
211
|
-
|
|
212
|
-
const project = await this.projectStore.getProjectById(tenantId, projectId);
|
|
213
|
-
|
|
214
|
-
if (!project) {
|
|
215
|
-
return reply.status(404).send({ success: false, error: "Project not found" });
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return { success: true, data: project };
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
async updateProject(
|
|
222
|
-
request: FastifyRequest<{
|
|
223
|
-
Params: ProjectParams;
|
|
224
|
-
Body: UpdateProjectRequest;
|
|
225
|
-
}>,
|
|
226
|
-
reply: FastifyReply
|
|
227
|
-
) {
|
|
228
|
-
const tenantId = this.getTenantId(request);
|
|
229
|
-
const { projectId } = request.params;
|
|
230
|
-
const updates = request.body;
|
|
231
|
-
|
|
232
|
-
const project = await this.projectStore.updateProject(
|
|
233
|
-
tenantId,
|
|
234
|
-
projectId,
|
|
235
|
-
updates
|
|
236
|
-
);
|
|
237
|
-
|
|
238
|
-
if (!project) {
|
|
239
|
-
return reply.status(404).send({ success: false, error: "Project not found" });
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
return { success: true, data: project };
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
async deleteProject(
|
|
246
|
-
request: FastifyRequest<{ Params: ProjectParams }>,
|
|
247
|
-
reply: FastifyReply
|
|
248
|
-
) {
|
|
249
|
-
const tenantId = this.getTenantId(request);
|
|
250
|
-
const { projectId } = request.params;
|
|
251
|
-
|
|
252
|
-
const deleted = await this.projectStore.deleteProject(tenantId, projectId);
|
|
253
|
-
|
|
254
|
-
if (!deleted) {
|
|
255
|
-
return reply.status(404).send({ success: false, error: "Project not found" });
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
return { success: true };
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// ==================== File Operations ====================
|
|
262
|
-
|
|
263
|
-
private async getBackend(tenantId: string, workspaceId: string, projectId: string, assistantId?: string, filePath?: string) {
|
|
264
|
-
const workspace = await this.workspaceStore.getWorkspaceById(
|
|
265
|
-
tenantId,
|
|
266
|
-
workspaceId
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
if (!workspace) {
|
|
270
|
-
throw new Error("Workspace not found");
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
console.log(`[getBackend] storageType=${workspace.storageType} filePath=${filePath}`);
|
|
274
|
-
|
|
275
|
-
if (workspace.storageType === "sandbox") {
|
|
276
|
-
const sandboxManager = getSandBoxManager();
|
|
277
|
-
|
|
278
|
-
const volumeConfig = {
|
|
279
|
-
assistant_id: assistantId || "",
|
|
280
|
-
thread_id: "",
|
|
281
|
-
tenantId,
|
|
282
|
-
workspaceId,
|
|
283
|
-
projectId,
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
console.log(`[getBackend] trying volume backend for path=${filePath} assistant_id=${assistantId}`);
|
|
287
|
-
const volumeBackend = await sandboxManager.getVolumeBackendForPath(volumeConfig, filePath || "/project");
|
|
288
|
-
if (volumeBackend) {
|
|
289
|
-
console.log(`[getBackend] using VolumeFilesystem`);
|
|
290
|
-
return { backend: volumeBackend, workspace };
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
console.log(`[getBackend] volume not found, falling back to SandboxFilesystem`);
|
|
294
|
-
const sandbox = await sandboxManager.getSandboxFromConfig(volumeConfig);
|
|
295
|
-
console.log(`[getBackend] sandbox acquired, name=${(sandbox as any).name || "unknown"}`);
|
|
296
|
-
return {
|
|
297
|
-
backend: new SandboxFilesystem({
|
|
298
|
-
sandboxInstance: sandbox,
|
|
299
|
-
}), workspace
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Filesystem storage: use default /lattice_store path
|
|
304
|
-
const rootDir = await this.getFilesystemRootDirectory(
|
|
305
|
-
tenantId,
|
|
306
|
-
workspaceId,
|
|
307
|
-
projectId,
|
|
308
|
-
);
|
|
309
|
-
|
|
310
|
-
console.log(`[getBackend] using FilesystemBackend rootDir=${rootDir}`);
|
|
311
|
-
return {
|
|
312
|
-
backend: new FilesystemBackend({
|
|
313
|
-
rootDir,
|
|
314
|
-
virtualMode: true,
|
|
315
|
-
}), workspace
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
private async getFilesystemRootDirectory(
|
|
320
|
-
tenantId: string,
|
|
321
|
-
workspaceId: string,
|
|
322
|
-
projectId: string,
|
|
323
|
-
): Promise<string> {
|
|
324
|
-
return `/lattice_store/tenants/${tenantId}/workspaces/${workspaceId}/${projectId}`;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
private isBinaryContentType(filename: string): boolean {
|
|
328
|
-
const ext = filename.split(".").pop()?.toLowerCase() || "";
|
|
329
|
-
const binaryExtensions = new Set([
|
|
330
|
-
"pdf", "xlsx", "xls", "docx", "doc", "pptx", "ppt",
|
|
331
|
-
"png", "jpg", "jpeg", "gif", "bmp", "ico", "webp",
|
|
332
|
-
"mp3", "mp4", "webm", "ogg", "wav", "avi", "mov",
|
|
333
|
-
"zip", "tar", "gz", "rar", "7z",
|
|
334
|
-
]);
|
|
335
|
-
return binaryExtensions.has(ext);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
async downloadFile(
|
|
339
|
-
request: FastifyRequest<{
|
|
340
|
-
Params: ProjectParams;
|
|
341
|
-
Querystring: { path: string; assistantId?: string };
|
|
342
|
-
}>,
|
|
343
|
-
reply: FastifyReply
|
|
344
|
-
) {
|
|
345
|
-
const tenantId = this.getTenantId(request);
|
|
346
|
-
const { workspaceId, projectId } = request.params;
|
|
347
|
-
const filePath = request.query.path;
|
|
348
|
-
const assistantId = request.query.assistantId;
|
|
349
|
-
|
|
350
|
-
if (!filePath) {
|
|
351
|
-
return reply.status(400).send({ success: false, error: "Path is required" });
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
try {
|
|
355
|
-
const { workspace } = await this.getBackend(tenantId, workspaceId, projectId, assistantId);
|
|
356
|
-
const resolvedPath = filePath;
|
|
357
|
-
|
|
358
|
-
if (workspace.storageType === "sandbox") {
|
|
359
|
-
const sandboxManager = getSandBoxManager();
|
|
360
|
-
const volumeConfig = {
|
|
361
|
-
assistant_id: assistantId || "",
|
|
362
|
-
thread_id: "",
|
|
363
|
-
tenantId,
|
|
364
|
-
workspaceId,
|
|
365
|
-
projectId,
|
|
366
|
-
};
|
|
367
|
-
|
|
368
|
-
const filename = getFilenameFromPath(resolvedPath);
|
|
369
|
-
const isBinary = this.isBinaryContentType(filename);
|
|
370
|
-
|
|
371
|
-
// Use volume backend when available; fall back to sandbox
|
|
372
|
-
const volumeBackend = await sandboxManager.getVolumeBackendForPath(volumeConfig, resolvedPath);
|
|
373
|
-
|
|
374
|
-
let buf: Buffer;
|
|
375
|
-
if (volumeBackend) {
|
|
376
|
-
if (isBinary && volumeBackend.readBinary) {
|
|
377
|
-
buf = await volumeBackend.readBinary(resolvedPath);
|
|
378
|
-
} else {
|
|
379
|
-
const fileData = await volumeBackend.readRaw(resolvedPath);
|
|
380
|
-
buf = Buffer.from(fileData.content.join("\n"), "utf-8");
|
|
381
|
-
}
|
|
382
|
-
} else {
|
|
383
|
-
const sandbox = await sandboxManager.getSandboxFromConfig(volumeConfig);
|
|
384
|
-
buf = await sandbox.file.downloadFile({ file: resolvedPath });
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const inferredContentType = getContentTypeFromFilename(filename);
|
|
388
|
-
|
|
389
|
-
const contentDisposition = `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`;
|
|
390
|
-
return reply.status(200).type(inferredContentType).header("Content-Disposition", contentDisposition).send(buf);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const { backend } = await this.getBackend(tenantId, workspaceId, projectId, assistantId, resolvedPath);
|
|
394
|
-
const content = await backend.read(resolvedPath, 0, Infinity);
|
|
395
|
-
const filename = getFilenameFromPath(resolvedPath);
|
|
396
|
-
const mimeType = getContentTypeFromFilename(filename);
|
|
397
|
-
const buffer = Buffer.from(content, "utf-8");
|
|
398
|
-
|
|
399
|
-
return reply
|
|
400
|
-
.status(200)
|
|
401
|
-
.type(mimeType)
|
|
402
|
-
.header("Content-Disposition", `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`)
|
|
403
|
-
.send(buffer);
|
|
404
|
-
} catch (error: unknown) {
|
|
405
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
406
|
-
return reply.status(502).send({ success: false, error: `Download proxy error: ${message}` });
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/**
|
|
411
|
-
* View a file inline in the browser (not download).
|
|
412
|
-
* Returns file content with inline Content-Disposition for preview.
|
|
413
|
-
*/
|
|
414
|
-
async viewFile(
|
|
415
|
-
request: FastifyRequest<{
|
|
416
|
-
Params: ProjectParams;
|
|
417
|
-
Querystring: { path: string; assistantId?: string };
|
|
418
|
-
}>,
|
|
419
|
-
reply: FastifyReply
|
|
420
|
-
) {
|
|
421
|
-
const tenantId = this.getTenantId(request);
|
|
422
|
-
const { workspaceId, projectId } = request.params;
|
|
423
|
-
const filePath = request.query.path;
|
|
424
|
-
const assistantId = request.query.assistantId;
|
|
425
|
-
|
|
426
|
-
if (!filePath) {
|
|
427
|
-
return reply.status(400).send({ success: false, error: "Path is required" });
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
try {
|
|
431
|
-
const { workspace } = await this.getBackend(tenantId, workspaceId, projectId, assistantId);
|
|
432
|
-
const resolvedPath = filePath;
|
|
433
|
-
|
|
434
|
-
if (workspace.storageType === "sandbox") {
|
|
435
|
-
const sandboxManager = getSandBoxManager();
|
|
436
|
-
const volumeConfig = {
|
|
437
|
-
assistant_id: assistantId || "",
|
|
438
|
-
thread_id: "",
|
|
439
|
-
tenantId,
|
|
440
|
-
workspaceId,
|
|
441
|
-
projectId,
|
|
442
|
-
};
|
|
443
|
-
|
|
444
|
-
const filename = getFilenameFromPath(resolvedPath);
|
|
445
|
-
const isBinary = this.isBinaryContentType(filename);
|
|
446
|
-
|
|
447
|
-
// Use volume backend when available; fall back to sandbox
|
|
448
|
-
const volumeBackend = await sandboxManager.getVolumeBackendForPath(volumeConfig, resolvedPath);
|
|
449
|
-
|
|
450
|
-
let buf: Buffer;
|
|
451
|
-
if (volumeBackend) {
|
|
452
|
-
if (isBinary && volumeBackend.readBinary) {
|
|
453
|
-
buf = await volumeBackend.readBinary(resolvedPath);
|
|
454
|
-
} else {
|
|
455
|
-
const fileData = await volumeBackend.readRaw(resolvedPath);
|
|
456
|
-
buf = Buffer.from(fileData.content.join("\n"), "utf-8");
|
|
457
|
-
}
|
|
458
|
-
} else {
|
|
459
|
-
const sandbox = await sandboxManager.getSandboxFromConfig(volumeConfig);
|
|
460
|
-
buf = await sandbox.file.downloadFile({ file: resolvedPath });
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
const inferredContentType = getContentTypeFromFilename(filename);
|
|
464
|
-
|
|
465
|
-
try {
|
|
466
|
-
const contentType = inferredContentType;
|
|
467
|
-
|
|
468
|
-
// Inject AI2APP context script for HTML files (sandbox storage)
|
|
469
|
-
const isHtml = contentType?.toLowerCase().includes("text/html") ||
|
|
470
|
-
filename.toLowerCase().endsWith(".html") ||
|
|
471
|
-
filename.toLowerCase().endsWith(".htm");
|
|
472
|
-
let outputBuf = buf;
|
|
473
|
-
if (isHtml) {
|
|
474
|
-
console.log(`[viewFile] Injecting AI2APP context for sandbox HTML file: ${filename}, tenantId: ${tenantId}, contentType: ${contentType}`);
|
|
475
|
-
let content = buf.toString("utf-8");
|
|
476
|
-
const contextScript = `<script>window.__AI2APP_CONTEXT__=${JSON.stringify({
|
|
477
|
-
tenantId,
|
|
478
|
-
workspaceId,
|
|
479
|
-
projectId,
|
|
480
|
-
timestamp: Date.now()
|
|
481
|
-
})};</script>`;
|
|
482
|
-
|
|
483
|
-
if (content.toLowerCase().includes("</head>")) {
|
|
484
|
-
content = content.replace(/<\/head>/i, `${contextScript}</head>`);
|
|
485
|
-
console.log(`[viewFile] Context script injected before </head>`);
|
|
486
|
-
} else if (content.toLowerCase().includes("<html>")) {
|
|
487
|
-
content = content.replace(/<html>/i, `<html>${contextScript}`);
|
|
488
|
-
console.log(`[viewFile] Context script injected after <html>`);
|
|
489
|
-
} else {
|
|
490
|
-
content = contextScript + content;
|
|
491
|
-
console.log(`[viewFile] Context script prepended to content`);
|
|
492
|
-
}
|
|
493
|
-
outputBuf = Buffer.from(content, "utf-8");
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
return reply
|
|
497
|
-
.status(200)
|
|
498
|
-
.type(contentType)
|
|
499
|
-
.header("Content-Disposition", "inline")
|
|
500
|
-
.send(outputBuf);
|
|
501
|
-
} catch (err) {
|
|
502
|
-
return reply.status(502).send({ success: false, error: String(err) });
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
const { backend } = await this.getBackend(tenantId, workspaceId, projectId, assistantId, resolvedPath);
|
|
507
|
-
const content = await backend.read(resolvedPath, 0, Infinity);
|
|
508
|
-
const filename = getFilenameFromPath(resolvedPath);
|
|
509
|
-
const mimeType = getContentTypeFromFilename(filename);
|
|
510
|
-
|
|
511
|
-
// Inject AI2APP context script for HTML files
|
|
512
|
-
let finalContent = content;
|
|
513
|
-
const isHtmlFs = mimeType?.toLowerCase().includes("text/html") ||
|
|
514
|
-
filename.toLowerCase().endsWith(".html") ||
|
|
515
|
-
filename.toLowerCase().endsWith(".htm");
|
|
516
|
-
if (isHtmlFs) {
|
|
517
|
-
console.log(`[viewFile] Injecting AI2APP context for filesystem HTML file: ${filename}, tenantId: ${tenantId}, mimeType: ${mimeType}`);
|
|
518
|
-
const contextScript = `<script>window.__AI2APP_CONTEXT__=${JSON.stringify({
|
|
519
|
-
tenantId,
|
|
520
|
-
workspaceId,
|
|
521
|
-
projectId,
|
|
522
|
-
timestamp: Date.now()
|
|
523
|
-
})};</script>`;
|
|
524
|
-
// Insert before </head> or </html>
|
|
525
|
-
if (content.toLowerCase().includes("</head>")) {
|
|
526
|
-
finalContent = content.replace(/<\/head>/i, `${contextScript}</head>`);
|
|
527
|
-
console.log(`[viewFile] Context script injected before </head>`);
|
|
528
|
-
} else if (content.toLowerCase().includes("</html>")) {
|
|
529
|
-
finalContent = content.replace(/<\/html>/i, `${contextScript}</html>`);
|
|
530
|
-
console.log(`[viewFile] Context script injected before </html>`);
|
|
531
|
-
} else {
|
|
532
|
-
finalContent = content + contextScript;
|
|
533
|
-
console.log(`[viewFile] Context script appended to content`);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
const buffer = Buffer.from(finalContent, "utf-8");
|
|
538
|
-
|
|
539
|
-
return reply
|
|
540
|
-
.status(200)
|
|
541
|
-
.type(mimeType)
|
|
542
|
-
.header("Content-Disposition", "inline")
|
|
543
|
-
.send(buffer);
|
|
544
|
-
} catch (error: unknown) {
|
|
545
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
546
|
-
return reply.status(502).send({ success: false, error: `View proxy error: ${message}` });
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
async listPath(
|
|
551
|
-
request: FastifyRequest<{
|
|
552
|
-
Params: ProjectParams;
|
|
553
|
-
Querystring: ListPathQuery;
|
|
554
|
-
}>
|
|
555
|
-
) {
|
|
556
|
-
const tenantId = this.getTenantId(request);
|
|
557
|
-
const { workspaceId, projectId } = request.params;
|
|
558
|
-
const path = (request.query.path as string) || "/";
|
|
559
|
-
const assistantId = request.query.assistantId;
|
|
560
|
-
|
|
561
|
-
console.log(`[listPath] tenantId=${tenantId} workspaceId=${workspaceId} projectId=${projectId} path=${path} assistantId=${assistantId}`);
|
|
562
|
-
|
|
563
|
-
const { backend } = await this.getBackend(tenantId, workspaceId, projectId, assistantId, path);
|
|
564
|
-
console.log(`[listPath] backend type=${backend.constructor.name} calling lsInfo(${path})`);
|
|
565
|
-
|
|
566
|
-
const files = await backend.lsInfo(path);
|
|
567
|
-
console.log(`[listPath] result count=${files.length}`);
|
|
568
|
-
|
|
569
|
-
return { success: true, data: files };
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
async readFile(
|
|
573
|
-
request: FastifyRequest<{
|
|
574
|
-
Params: ProjectParams;
|
|
575
|
-
Querystring: ReadFileQuery;
|
|
576
|
-
}>
|
|
577
|
-
) {
|
|
578
|
-
const tenantId = this.getTenantId(request);
|
|
579
|
-
const { workspaceId, projectId } = request.params;
|
|
580
|
-
const { path, offset = 0, limit = 1000, assistantId } = request.query;
|
|
581
|
-
|
|
582
|
-
const { backend } = await this.getBackend(tenantId, workspaceId, projectId, assistantId, path);
|
|
583
|
-
const content = await backend.read(path, Number(offset), Number(limit));
|
|
584
|
-
|
|
585
|
-
return { success: true, data: { content, offset, limit } };
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
/**
|
|
589
|
-
* Upload a file to the workspace project storage.
|
|
590
|
-
*
|
|
591
|
-
* Route: POST /api/workspaces/:workspaceId/projects/:projectId/uploadfile
|
|
592
|
-
* Accepts multipart/form-data with:
|
|
593
|
-
* - `file`: the file to upload (required)
|
|
594
|
-
* - `path`: optional directory path relative to project root (e.g., "docs", "src/utils")
|
|
595
|
-
* - `assistantId`: optional query parameter for sandbox identity matching
|
|
596
|
-
* For sandbox storage, delegates to the sandbox upload API.
|
|
597
|
-
* For filesystem storage, writes under the same project root used by the file backend.
|
|
598
|
-
*/
|
|
599
|
-
async uploadFile(
|
|
600
|
-
request: FastifyRequest<{
|
|
601
|
-
Params: ProjectParams;
|
|
602
|
-
Querystring: { assistantId?: string };
|
|
603
|
-
}>,
|
|
604
|
-
reply: FastifyReply
|
|
605
|
-
) {
|
|
606
|
-
const tenantId = this.getTenantId(request);
|
|
607
|
-
const { workspaceId, projectId } = request.params;
|
|
608
|
-
const assistantId = (request.query as any).assistantId || (request.headers["x-assistant-id"] as string);
|
|
609
|
-
|
|
610
|
-
const workspace = await this.workspaceStore.getWorkspaceById(
|
|
611
|
-
tenantId,
|
|
612
|
-
workspaceId
|
|
613
|
-
);
|
|
614
|
-
|
|
615
|
-
if (!workspace) {
|
|
616
|
-
return reply
|
|
617
|
-
.status(404)
|
|
618
|
-
.send({ success: false, error: "Workspace not found" });
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
try {
|
|
622
|
-
const data = await (request as any).file();
|
|
623
|
-
if (!data) {
|
|
624
|
-
return reply
|
|
625
|
-
.status(400)
|
|
626
|
-
.send({ success: false, error: "No file in request" });
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
const buffer: Buffer = await data.toBuffer();
|
|
630
|
-
const filename: string = data.filename || "file";
|
|
631
|
-
|
|
632
|
-
const pathEntry = data.fields?.path;
|
|
633
|
-
const pathValue =
|
|
634
|
-
pathEntry && typeof pathEntry === "object" && "value" in pathEntry
|
|
635
|
-
? String((pathEntry as { value: unknown }).value)
|
|
636
|
-
: typeof pathEntry === "string"
|
|
637
|
-
? pathEntry
|
|
638
|
-
: undefined;
|
|
639
|
-
|
|
640
|
-
if (pathValue && !/^[a-zA-Z0-9_./~-]+$/.test(pathValue)) {
|
|
641
|
-
return reply
|
|
642
|
-
.status(400)
|
|
643
|
-
.send({ success: false, error: "Invalid path parameter" });
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
if (workspace.storageType === "sandbox") {
|
|
647
|
-
const sandboxManager = getSandBoxManager();
|
|
648
|
-
const volumeConfig = {
|
|
649
|
-
assistant_id: assistantId || "",
|
|
650
|
-
thread_id: "",
|
|
651
|
-
tenantId,
|
|
652
|
-
workspaceId,
|
|
653
|
-
projectId,
|
|
654
|
-
};
|
|
655
|
-
|
|
656
|
-
const realPath = pathValue
|
|
657
|
-
? path.posix.join(pathValue, filename)
|
|
658
|
-
: path.posix.join("/project/uploads", filename);
|
|
659
|
-
|
|
660
|
-
const isBinary = this.isBinaryContentType(filename);
|
|
661
|
-
const volumeBackend = await sandboxManager.getVolumeBackendForPath(volumeConfig, realPath);
|
|
662
|
-
if (volumeBackend && !isBinary) {
|
|
663
|
-
await volumeBackend.write(realPath, buffer.toString("utf-8"));
|
|
664
|
-
} else if (volumeBackend && volumeBackend.writeBinary) {
|
|
665
|
-
await volumeBackend.writeBinary(realPath, buffer);
|
|
666
|
-
} else {
|
|
667
|
-
const sandbox = await sandboxManager.getSandboxFromConfig(volumeConfig);
|
|
668
|
-
await sandbox.file.uploadFile({ file: realPath, data: buffer });
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
const relativePath = pathValue
|
|
672
|
-
? path.posix.join(pathValue, filename)
|
|
673
|
-
: path.posix.join("/project/uploads", filename);
|
|
674
|
-
|
|
675
|
-
const result = {
|
|
676
|
-
path: relativePath,
|
|
677
|
-
name: filename,
|
|
678
|
-
is_dir: false,
|
|
679
|
-
size: buffer.length,
|
|
680
|
-
modified_at: new Date().toISOString(),
|
|
681
|
-
};
|
|
682
|
-
|
|
683
|
-
return reply.status(200).send({ success: true, data: result });
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
const rootDir = await this.getFilesystemRootDirectory(
|
|
687
|
-
tenantId,
|
|
688
|
-
workspaceId,
|
|
689
|
-
projectId,
|
|
690
|
-
);
|
|
691
|
-
await fs.mkdir(rootDir, { recursive: true });
|
|
692
|
-
const relativeDirectory = pathValue?.replace(/^[/\\]+/, "") || ".";
|
|
693
|
-
const targetDir = path.resolve(rootDir, relativeDirectory);
|
|
694
|
-
if (!isPathWithinRoot(path.resolve(rootDir), targetDir)) {
|
|
695
|
-
return reply.status(400).send({
|
|
696
|
-
success: false,
|
|
697
|
-
error: "Upload path must stay within the project directory",
|
|
698
|
-
});
|
|
699
|
-
}
|
|
700
|
-
await fs.mkdir(targetDir, { recursive: true });
|
|
701
|
-
|
|
702
|
-
const [resolvedRoot, resolvedTargetDir] = await Promise.all([
|
|
703
|
-
fs.realpath(rootDir),
|
|
704
|
-
fs.realpath(targetDir),
|
|
705
|
-
]);
|
|
706
|
-
if (!isPathWithinRoot(resolvedRoot, resolvedTargetDir)) {
|
|
707
|
-
return reply.status(400).send({
|
|
708
|
-
success: false,
|
|
709
|
-
error: "Upload path must stay within the project directory",
|
|
710
|
-
});
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
const safeFilename = path.basename(filename);
|
|
714
|
-
if (safeFilename !== filename) {
|
|
715
|
-
return reply.status(400).send({
|
|
716
|
-
success: false,
|
|
717
|
-
error: "Invalid filename",
|
|
718
|
-
});
|
|
719
|
-
}
|
|
720
|
-
const targetPath = path.join(resolvedTargetDir, safeFilename);
|
|
721
|
-
await fs.writeFile(targetPath, buffer);
|
|
722
|
-
const stat = await fs.stat(targetPath);
|
|
723
|
-
|
|
724
|
-
const result = {
|
|
725
|
-
path: path.posix.join("/", relativeDirectory, safeFilename),
|
|
726
|
-
name: filename,
|
|
727
|
-
is_dir: false,
|
|
728
|
-
size: stat.size,
|
|
729
|
-
modified_at: stat.mtime.toISOString(),
|
|
730
|
-
};
|
|
731
|
-
|
|
732
|
-
return reply.status(200).send({ success: true, data: result });
|
|
733
|
-
} catch (error: unknown) {
|
|
734
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
735
|
-
return reply.status(500).send({
|
|
736
|
-
success: false,
|
|
737
|
-
error: `Upload handler error: ${message}`,
|
|
738
|
-
});
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
/**
|
|
742
|
-
* Browse local filesystem directories for the folder picker.
|
|
743
|
-
*
|
|
744
|
-
* Security constraints:
|
|
745
|
-
* - Restricted to os.homedir()
|
|
746
|
-
* - Blacklists system directories
|
|
747
|
-
* - Returns only directory names, no file contents or hidden files
|
|
748
|
-
*/
|
|
749
|
-
async browseFilesystem(
|
|
750
|
-
request: FastifyRequest<{
|
|
751
|
-
Querystring: { path?: string };
|
|
752
|
-
}>,
|
|
753
|
-
reply: FastifyReply
|
|
754
|
-
) {
|
|
755
|
-
const homeDir = os.homedir();
|
|
756
|
-
const requestedPath = request.query.path || homeDir;
|
|
757
|
-
|
|
758
|
-
const SYSTEM_BLACKLIST = ["/etc", "/sys", "/proc", "/var", "/tmp", "/dev"];
|
|
759
|
-
|
|
760
|
-
try {
|
|
761
|
-
const resolved = await resolvePathWithinRoot(homeDir, requestedPath);
|
|
762
|
-
const normalized = resolved.targetPath;
|
|
763
|
-
|
|
764
|
-
for (const blacklisted of SYSTEM_BLACKLIST) {
|
|
765
|
-
if (isPathWithinRoot(blacklisted, normalized)) {
|
|
766
|
-
return reply.status(403).send({
|
|
767
|
-
success: false,
|
|
768
|
-
error: `Cannot browse system directory: ${blacklisted}`,
|
|
769
|
-
});
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
const entries = await fs.readdir(normalized, { withFileTypes: true });
|
|
774
|
-
const dirEntries = entries
|
|
775
|
-
.filter((entry) => !entry.name.startsWith(".") && entry.isDirectory())
|
|
776
|
-
.map((entry) => ({
|
|
777
|
-
name: entry.name,
|
|
778
|
-
path: path.join(normalized, entry.name),
|
|
779
|
-
isDirectory: true,
|
|
780
|
-
}));
|
|
781
|
-
|
|
782
|
-
const parentPath = path.dirname(normalized);
|
|
783
|
-
const hasParent =
|
|
784
|
-
parentPath !== normalized &&
|
|
785
|
-
isPathWithinRoot(resolved.rootPath, parentPath);
|
|
786
|
-
|
|
787
|
-
return {
|
|
788
|
-
success: true,
|
|
789
|
-
data: {
|
|
790
|
-
path: normalized,
|
|
791
|
-
parentPath: hasParent ? parentPath : undefined,
|
|
792
|
-
entries: dirEntries,
|
|
793
|
-
},
|
|
794
|
-
};
|
|
795
|
-
} catch (error: unknown) {
|
|
796
|
-
if (error instanceof PathOutsideRootError) {
|
|
797
|
-
return reply.status(403).send({
|
|
798
|
-
success: false,
|
|
799
|
-
error: "Browse restricted to home directory",
|
|
800
|
-
});
|
|
801
|
-
}
|
|
802
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
803
|
-
return reply.status(400).send({
|
|
804
|
-
success: false,
|
|
805
|
-
error: `Cannot read directory: ${message}`,
|
|
806
|
-
});
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
export function registerWorkspaceRoutes(app: FastifyInstance) {
|
|
812
|
-
const controller = new WorkspaceController();
|
|
813
|
-
|
|
814
|
-
// Workspace routes
|
|
815
|
-
app.get("/api/workspaces", controller.listWorkspaces.bind(controller));
|
|
816
|
-
app.post("/api/workspaces", controller.createWorkspace.bind(controller));
|
|
817
|
-
app.get(
|
|
818
|
-
"/api/workspaces/:workspaceId",
|
|
819
|
-
controller.getWorkspace.bind(controller)
|
|
820
|
-
);
|
|
821
|
-
app.patch(
|
|
822
|
-
"/api/workspaces/:workspaceId",
|
|
823
|
-
controller.updateWorkspace.bind(controller)
|
|
824
|
-
);
|
|
825
|
-
app.delete(
|
|
826
|
-
"/api/workspaces/:workspaceId",
|
|
827
|
-
controller.deleteWorkspace.bind(controller)
|
|
828
|
-
);
|
|
829
|
-
|
|
830
|
-
// Project routes
|
|
831
|
-
app.get(
|
|
832
|
-
"/api/workspaces/:workspaceId/projects",
|
|
833
|
-
controller.listProjects.bind(controller)
|
|
834
|
-
);
|
|
835
|
-
app.post(
|
|
836
|
-
"/api/workspaces/:workspaceId/projects",
|
|
837
|
-
controller.createProject.bind(controller)
|
|
838
|
-
);
|
|
839
|
-
app.get(
|
|
840
|
-
"/api/workspaces/:workspaceId/projects/:projectId",
|
|
841
|
-
controller.getProject.bind(controller)
|
|
842
|
-
);
|
|
843
|
-
app.patch(
|
|
844
|
-
"/api/workspaces/:workspaceId/projects/:projectId",
|
|
845
|
-
controller.updateProject.bind(controller)
|
|
846
|
-
);
|
|
847
|
-
app.delete(
|
|
848
|
-
"/api/workspaces/:workspaceId/projects/:projectId",
|
|
849
|
-
controller.deleteProject.bind(controller)
|
|
850
|
-
);
|
|
851
|
-
|
|
852
|
-
// File operations
|
|
853
|
-
app.get(
|
|
854
|
-
"/api/workspaces/:workspaceId/projects/:projectId/listpath",
|
|
855
|
-
controller.listPath.bind(controller)
|
|
856
|
-
);
|
|
857
|
-
app.get(
|
|
858
|
-
"/api/workspaces/:workspaceId/projects/:projectId/readfile",
|
|
859
|
-
controller.readFile.bind(controller)
|
|
860
|
-
);
|
|
861
|
-
app.post(
|
|
862
|
-
"/api/workspaces/:workspaceId/projects/:projectId/uploadfile",
|
|
863
|
-
controller.uploadFile.bind(controller)
|
|
864
|
-
);
|
|
865
|
-
app.get(
|
|
866
|
-
"/api/workspaces/:workspaceId/projects/:projectId/downloadfile",
|
|
867
|
-
controller.downloadFile.bind(controller)
|
|
868
|
-
);
|
|
869
|
-
app.get(
|
|
870
|
-
"/api/workspaces/:workspaceId/projects/:projectId/viewfile",
|
|
871
|
-
controller.viewFile.bind(controller)
|
|
872
|
-
);
|
|
873
|
-
|
|
874
|
-
// Filesystem browsing for folder picker
|
|
875
|
-
app.get(
|
|
876
|
-
"/api/filesystem/browse",
|
|
877
|
-
controller.browseFilesystem.bind(controller)
|
|
878
|
-
);
|
|
879
|
-
}
|