@axiom-lattice/gateway 2.1.92 → 2.1.94
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 +10 -10
- package/CHANGELOG.md +22 -0
- package/dist/index.js +972 -385
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +876 -288
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
- package/scripts/seed-lark-installation.ts +54 -0
- package/src/channels/lark/LarkChannelAdapter.ts +140 -18
- package/src/channels/lark/types.ts +0 -13
- package/src/channels/lark/verification.ts +1 -36
- package/src/controllers/__tests__/run.test.ts +115 -0
- package/src/controllers/__tests__/tasks.test.ts +215 -0
- package/src/controllers/agent_task.ts +7 -0
- package/src/controllers/assistant.ts +15 -2
- package/src/controllers/auth.ts +9 -0
- package/src/controllers/menu-items.ts +114 -0
- package/src/controllers/personal-assistant.ts +191 -0
- package/src/controllers/run.ts +47 -0
- package/src/controllers/tasks.ts +187 -0
- package/src/controllers/workspace.ts +12 -0
- package/src/index.ts +55 -20
- package/src/router/MessageRouter.ts +107 -41
- package/src/routes/index.ts +23 -0
- package/src/routes/menu-items.ts +10 -0
- package/src/services/agent_task_consumer.ts +2 -8
- package/src/channels/lark/__tests__/aggregator.test.ts +0 -23
- package/src/channels/lark/aggregator.ts +0 -16
- package/src/channels/lark/config.ts +0 -44
- package/src/channels/lark/runner.ts +0 -37
|
@@ -31,6 +31,12 @@ function getTenantId(request: FastifyRequest): string {
|
|
|
31
31
|
return (request.headers["x-tenant-id"] as string) || "default";
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
function getUserId(request: FastifyRequest): string | undefined {
|
|
35
|
+
const userId = (request as any).user?.id;
|
|
36
|
+
if (userId) return userId;
|
|
37
|
+
return (request.headers["x-user-id"] as string) || undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
34
40
|
/**
|
|
35
41
|
* Convert AgentConfig to Assistant format
|
|
36
42
|
*/
|
|
@@ -112,12 +118,19 @@ export async function getAssistantList(
|
|
|
112
118
|
|
|
113
119
|
const allAssistants = Array.from(assistantMap.values());
|
|
114
120
|
|
|
121
|
+
// Filter: exclude personal assistants owned by other users
|
|
122
|
+
const userId = getUserId(request);
|
|
123
|
+
const filtered = allAssistants.filter((a) => {
|
|
124
|
+
if (!a.ownerUserId) return true;
|
|
125
|
+
return a.ownerUserId === userId;
|
|
126
|
+
});
|
|
127
|
+
|
|
115
128
|
return {
|
|
116
129
|
success: true,
|
|
117
130
|
message: "Successfully retrieved assistant list",
|
|
118
131
|
data: {
|
|
119
|
-
records:
|
|
120
|
-
total:
|
|
132
|
+
records: filtered,
|
|
133
|
+
total: filtered.length,
|
|
121
134
|
},
|
|
122
135
|
};
|
|
123
136
|
}
|
package/src/controllers/auth.ts
CHANGED
|
@@ -251,6 +251,10 @@ export class AuthController {
|
|
|
251
251
|
|
|
252
252
|
const token = await this.generateToken(userId, tenantId);
|
|
253
253
|
|
|
254
|
+
// Check for personal assistant in this tenant
|
|
255
|
+
const link = await this.userTenantLinkStore.getLink(userId, tenantId);
|
|
256
|
+
const linkMeta = (link?.metadata || {}) as Record<string, unknown>;
|
|
257
|
+
|
|
254
258
|
return {
|
|
255
259
|
success: true,
|
|
256
260
|
data: {
|
|
@@ -260,6 +264,11 @@ export class AuthController {
|
|
|
260
264
|
status: tenant.status,
|
|
261
265
|
},
|
|
262
266
|
token,
|
|
267
|
+
personalAssistant: linkMeta.personalAssistantId ? {
|
|
268
|
+
assistantId: linkMeta.personalAssistantId as string,
|
|
269
|
+
projectId: linkMeta.personalProjectId as string || "default",
|
|
270
|
+
workspaceId: linkMeta.personalWorkspaceId as string || "default",
|
|
271
|
+
} : null,
|
|
263
272
|
},
|
|
264
273
|
};
|
|
265
274
|
} catch (error) {
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { FastifyRequest, FastifyReply } from "fastify";
|
|
2
|
+
import type { CreateMenuItemInput } from "@axiom-lattice/protocols";
|
|
3
|
+
import { getMenuRegistry } from "@axiom-lattice/core";
|
|
4
|
+
|
|
5
|
+
function getTenantId(request: FastifyRequest): string {
|
|
6
|
+
const userTenantId = (request as any).user?.tenantId;
|
|
7
|
+
if (userTenantId) return userTenantId;
|
|
8
|
+
return (request.headers["x-tenant-id"] as string) || "default";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function errorMessage(err: unknown): string {
|
|
12
|
+
return err instanceof Error ? err.message : "Unexpected error";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function getMenuItemList(
|
|
16
|
+
request: FastifyRequest<{
|
|
17
|
+
Querystring: { menuTarget?: string };
|
|
18
|
+
}>,
|
|
19
|
+
_reply: FastifyReply,
|
|
20
|
+
) {
|
|
21
|
+
const tenantId = getTenantId(request);
|
|
22
|
+
try {
|
|
23
|
+
const registry = getMenuRegistry();
|
|
24
|
+
const items = await registry.list({ tenantId, menuTarget: request.query.menuTarget as "sidebar" | "workspace" | undefined });
|
|
25
|
+
return { success: true, message: "Menu items retrieved", data: { records: items, total: items.length } };
|
|
26
|
+
} catch (error) {
|
|
27
|
+
const msg = errorMessage(error);
|
|
28
|
+
console.error("Failed to get menu items:", msg);
|
|
29
|
+
return { success: false, message: msg, data: { records: [], total: 0 } };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function getMenuItem(
|
|
34
|
+
request: FastifyRequest<{ Params: { id: string } }>,
|
|
35
|
+
reply: FastifyReply,
|
|
36
|
+
) {
|
|
37
|
+
const tenantId = getTenantId(request);
|
|
38
|
+
try {
|
|
39
|
+
const registry = getMenuRegistry();
|
|
40
|
+
const item = await registry.getById(request.params.id);
|
|
41
|
+
if (!item || item.tenantId !== tenantId) {
|
|
42
|
+
reply.status(404);
|
|
43
|
+
return { success: false, message: "Menu item not found" };
|
|
44
|
+
}
|
|
45
|
+
return { success: true, message: "Menu item retrieved", data: item };
|
|
46
|
+
} catch (error) {
|
|
47
|
+
const msg = errorMessage(error);
|
|
48
|
+
console.error("Failed to get menu item:", msg);
|
|
49
|
+
reply.status(500);
|
|
50
|
+
return { success: false, message: msg };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function createMenuItem(
|
|
55
|
+
request: FastifyRequest<{ Body: CreateMenuItemInput }>,
|
|
56
|
+
reply: FastifyReply,
|
|
57
|
+
) {
|
|
58
|
+
const tenantId = getTenantId(request);
|
|
59
|
+
try {
|
|
60
|
+
const registry = getMenuRegistry();
|
|
61
|
+
const item = await registry.create({ ...request.body, tenantId });
|
|
62
|
+
reply.status(201);
|
|
63
|
+
return { success: true, message: "Menu item created", data: item };
|
|
64
|
+
} catch (error) {
|
|
65
|
+
const msg = errorMessage(error);
|
|
66
|
+
console.error("Failed to create menu item:", msg);
|
|
67
|
+
reply.status(500);
|
|
68
|
+
return { success: false, message: msg };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function updateMenuItem(
|
|
73
|
+
request: FastifyRequest<{ Params: { id: string }; Body: Partial<CreateMenuItemInput & { enabled: boolean }> }>,
|
|
74
|
+
reply: FastifyReply,
|
|
75
|
+
) {
|
|
76
|
+
try {
|
|
77
|
+
const tenantId = getTenantId(request);
|
|
78
|
+
const registry = getMenuRegistry();
|
|
79
|
+
const existing = await registry.getById(request.params.id);
|
|
80
|
+
if (!existing || existing.tenantId !== tenantId) {
|
|
81
|
+
reply.status(404);
|
|
82
|
+
return { success: false, message: "Menu item not found" };
|
|
83
|
+
}
|
|
84
|
+
const item = await registry.update(request.params.id, request.body);
|
|
85
|
+
return { success: true, message: "Menu item updated", data: item };
|
|
86
|
+
} catch (error) {
|
|
87
|
+
const msg = errorMessage(error);
|
|
88
|
+
console.error("Failed to update menu item:", msg);
|
|
89
|
+
reply.status(500);
|
|
90
|
+
return { success: false, message: msg };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function deleteMenuItem(
|
|
95
|
+
request: FastifyRequest<{ Params: { id: string } }>,
|
|
96
|
+
reply: FastifyReply,
|
|
97
|
+
) {
|
|
98
|
+
try {
|
|
99
|
+
const tenantId = getTenantId(request);
|
|
100
|
+
const registry = getMenuRegistry();
|
|
101
|
+
const existing = await registry.getById(request.params.id);
|
|
102
|
+
if (!existing || existing.tenantId !== tenantId) {
|
|
103
|
+
reply.status(404);
|
|
104
|
+
return { success: false, message: "Menu item not found" };
|
|
105
|
+
}
|
|
106
|
+
await registry.delete(request.params.id);
|
|
107
|
+
return { success: true, message: "Menu item deleted" };
|
|
108
|
+
} catch (error) {
|
|
109
|
+
const msg = errorMessage(error);
|
|
110
|
+
console.error("Failed to delete menu item:", msg);
|
|
111
|
+
reply.status(500);
|
|
112
|
+
return { success: false, message: msg };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { FastifyRequest, FastifyReply } from "fastify";
|
|
2
|
+
import { getStoreLattice, PersonalAssistantConfig } from "@axiom-lattice/core";
|
|
3
|
+
import type { Assistant, CreateAssistantRequest } from "@axiom-lattice/protocols";
|
|
4
|
+
import { randomUUID } from "crypto";
|
|
5
|
+
function getWorkspaceId(request: FastifyRequest): string {
|
|
6
|
+
return (request.headers["x-workspace-id"] as string) || "default";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function getTenantId(request: FastifyRequest): string {
|
|
10
|
+
const userTenantId = (request as any).user?.tenantId;
|
|
11
|
+
if (userTenantId) return userTenantId;
|
|
12
|
+
return (request.headers["x-tenant-id"] as string) || "default";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getUserId(request: FastifyRequest): string {
|
|
16
|
+
const userId = (request as any).user?.id
|
|
17
|
+
|| (request.headers["x-user-id"] as string);
|
|
18
|
+
if (!userId) throw new Error("User ID is required");
|
|
19
|
+
return userId;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface CreatePersonalAssistantBody {
|
|
23
|
+
name: string;
|
|
24
|
+
personality: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface PersonalAssistantStatus {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
projectId: string;
|
|
31
|
+
workspaceId?: string;
|
|
32
|
+
createdAt: Date;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface PersonalAssistantResponse {
|
|
36
|
+
success: boolean;
|
|
37
|
+
message: string;
|
|
38
|
+
data?: PersonalAssistantStatus;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getLinkStore() {
|
|
42
|
+
return getStoreLattice("default", "userTenantLink").store;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getAssistantStore() {
|
|
46
|
+
return getStoreLattice("default", "assistant").store;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getProjectStore() {
|
|
50
|
+
return getStoreLattice("default", "project").store;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function getLinkMeta(userId: string, tenantId: string) {
|
|
54
|
+
const link = await getLinkStore().getLink(userId, tenantId);
|
|
55
|
+
return (link?.metadata || {}) as Record<string, unknown>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function updateLinkMeta(userId: string, tenantId: string, meta: Record<string, unknown>) {
|
|
59
|
+
await getLinkStore().updateLink(userId, tenantId, { metadata: meta } as any);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Create a personal assistant for the authenticated user.
|
|
64
|
+
* Random IDs for both assistant and project, stored in UserTenantLink.metadata.
|
|
65
|
+
*/
|
|
66
|
+
export async function createPersonalAssistant(
|
|
67
|
+
request: FastifyRequest<{ Body: CreatePersonalAssistantBody }>,
|
|
68
|
+
reply: FastifyReply
|
|
69
|
+
): Promise<PersonalAssistantResponse> {
|
|
70
|
+
const tenantId = getTenantId(request);
|
|
71
|
+
const userId = getUserId(request);
|
|
72
|
+
const { name, personality } = request.body;
|
|
73
|
+
|
|
74
|
+
if (!name || !personality) {
|
|
75
|
+
return reply.status(400).send({ success: false, message: "name and personality are required" });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const store = getAssistantStore();
|
|
79
|
+
|
|
80
|
+
const existing = await store.getByOwner(tenantId, userId);
|
|
81
|
+
if (existing) {
|
|
82
|
+
return reply.status(409).send({ success: false, message: "You already have a personal assistant" });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const assistantId = randomUUID();
|
|
86
|
+
|
|
87
|
+
// Create a personal project under the current workspace
|
|
88
|
+
const projectStore = getProjectStore();
|
|
89
|
+
const projectId = randomUUID();
|
|
90
|
+
const wsId = getWorkspaceId(request);
|
|
91
|
+
await projectStore.createProject(tenantId, wsId, projectId, {
|
|
92
|
+
name: `${name}'s Space`,
|
|
93
|
+
description: "Personal workspace for your assistant",
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const config = PersonalAssistantConfig.get();
|
|
97
|
+
config.key = assistantId;
|
|
98
|
+
PersonalAssistantConfig.render(config, name, personality);
|
|
99
|
+
|
|
100
|
+
const data: CreateAssistantRequest = { name, graphDefinition: config, ownerUserId: userId };
|
|
101
|
+
const assistant = await store.createAssistant(tenantId, assistantId, data);
|
|
102
|
+
|
|
103
|
+
// Store in UserTenantLink.metadata
|
|
104
|
+
const meta = await getLinkMeta(userId, tenantId);
|
|
105
|
+
meta.personalAssistantId = assistantId;
|
|
106
|
+
meta.personalProjectId = projectId;
|
|
107
|
+
meta.personalWorkspaceId = wsId;
|
|
108
|
+
await updateLinkMeta(userId, tenantId, meta);
|
|
109
|
+
|
|
110
|
+
return reply.status(201).send({
|
|
111
|
+
success: true,
|
|
112
|
+
message: "Personal assistant created",
|
|
113
|
+
data: { id: assistant.id, name: assistant.name, projectId, workspaceId: wsId, createdAt: assistant.createdAt },
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get the authenticated user's personal assistant.
|
|
119
|
+
*/
|
|
120
|
+
export async function getPersonalAssistant(
|
|
121
|
+
request: FastifyRequest,
|
|
122
|
+
reply: FastifyReply
|
|
123
|
+
): Promise<PersonalAssistantResponse> {
|
|
124
|
+
const tenantId = getTenantId(request);
|
|
125
|
+
const userId = getUserId(request);
|
|
126
|
+
|
|
127
|
+
const store = getAssistantStore();
|
|
128
|
+
const assistant = await store.getByOwner(tenantId, userId);
|
|
129
|
+
|
|
130
|
+
if (!assistant) {
|
|
131
|
+
return reply.status(404).send({ success: false, message: "No personal assistant found" });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const meta = await getLinkMeta(userId, tenantId);
|
|
135
|
+
const projectId = (meta.personalProjectId as string) || "default";
|
|
136
|
+
const workspaceId = (meta.personalWorkspaceId as string) || "default";
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
success: true,
|
|
140
|
+
message: "Personal assistant found",
|
|
141
|
+
data: { id: assistant.id, name: assistant.name, projectId, workspaceId, createdAt: assistant.createdAt },
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Delete the authenticated user's personal assistant.
|
|
147
|
+
*/
|
|
148
|
+
export async function deletePersonalAssistant(
|
|
149
|
+
request: FastifyRequest,
|
|
150
|
+
reply: FastifyReply
|
|
151
|
+
): Promise<{ success: boolean; message: string }> {
|
|
152
|
+
const tenantId = getTenantId(request);
|
|
153
|
+
const userId = getUserId(request);
|
|
154
|
+
|
|
155
|
+
const store = getAssistantStore();
|
|
156
|
+
const assistant = await store.getByOwner(tenantId, userId);
|
|
157
|
+
|
|
158
|
+
if (!assistant) {
|
|
159
|
+
return reply.status(404).send({ success: false, message: "No personal assistant found" });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const deleted = await store.deleteAssistant(tenantId, assistant.id);
|
|
163
|
+
if (!deleted) {
|
|
164
|
+
return reply.status(500).send({ success: false, message: "Failed to delete personal assistant" });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Delete all threads
|
|
168
|
+
const threadStore = getStoreLattice("default", "thread").store;
|
|
169
|
+
try {
|
|
170
|
+
const threads = await threadStore.getThreadsByAssistantId(tenantId, assistant.id);
|
|
171
|
+
for (const t of threads) {
|
|
172
|
+
await threadStore.deleteThread(tenantId, t.id);
|
|
173
|
+
}
|
|
174
|
+
} catch { /* best effort */ }
|
|
175
|
+
|
|
176
|
+
// Clean up the project
|
|
177
|
+
const meta = await getLinkMeta(userId, tenantId);
|
|
178
|
+
const projectId = meta.personalProjectId as string;
|
|
179
|
+
const wsId = meta.personalWorkspaceId as string;
|
|
180
|
+
if (projectId && wsId) {
|
|
181
|
+
try { await getProjectStore().deleteProject(tenantId, projectId); } catch { /* best effort */ }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Clean up UserTenantLink.metadata
|
|
185
|
+
delete meta.personalAssistantId;
|
|
186
|
+
delete meta.personalProjectId;
|
|
187
|
+
delete meta.personalWorkspaceId;
|
|
188
|
+
await updateLinkMeta(userId, tenantId, meta);
|
|
189
|
+
|
|
190
|
+
return { success: true, message: "Personal assistant deleted" };
|
|
191
|
+
}
|
package/src/controllers/run.ts
CHANGED
|
@@ -262,3 +262,50 @@ export const abortRun = async (
|
|
|
262
262
|
});
|
|
263
263
|
}
|
|
264
264
|
};
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Recover agent execution after a crash or manual abort.
|
|
268
|
+
*
|
|
269
|
+
* Resumes the LangGraph from its last checkpoint if the graph was
|
|
270
|
+
* mid-execution (BUSY). Otherwise cleans up stuck queue messages
|
|
271
|
+
* and restarts the queue processor.
|
|
272
|
+
*
|
|
273
|
+
* Pairs with {@link abortRun} — use abort to stop, recover to continue.
|
|
274
|
+
*/
|
|
275
|
+
export const recoverRun = async (
|
|
276
|
+
request: FastifyRequest,
|
|
277
|
+
reply: FastifyReply
|
|
278
|
+
): Promise<void> => {
|
|
279
|
+
try {
|
|
280
|
+
const { assistantId, threadId } = request.params as {
|
|
281
|
+
assistantId: string;
|
|
282
|
+
threadId: string;
|
|
283
|
+
};
|
|
284
|
+
const tenant_id = request.headers["x-tenant-id"] as string;
|
|
285
|
+
const workspace_id = (request.headers["x-workspace-id"] as string) || "default";
|
|
286
|
+
const project_id = (request.headers["x-project-id"] as string) || "default";
|
|
287
|
+
|
|
288
|
+
const agent = agentInstanceManager.getAgent({
|
|
289
|
+
assistant_id: assistantId,
|
|
290
|
+
thread_id: threadId,
|
|
291
|
+
tenant_id,
|
|
292
|
+
workspace_id,
|
|
293
|
+
project_id,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
await agent.resumeTask();
|
|
297
|
+
|
|
298
|
+
const status = await agent.getRunStatus();
|
|
299
|
+
|
|
300
|
+
reply.status(200).send({
|
|
301
|
+
success: true,
|
|
302
|
+
threadId,
|
|
303
|
+
status,
|
|
304
|
+
});
|
|
305
|
+
} catch (error: any) {
|
|
306
|
+
reply.status(500).send({
|
|
307
|
+
success: false,
|
|
308
|
+
error: `Recover failed: ${error.message}`,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { FastifyRequest, FastifyReply } from "fastify";
|
|
2
|
+
import { getStoreLattice } from "@axiom-lattice/core";
|
|
3
|
+
import type { TaskStore, TaskItem, CreateTaskRequest, UpdateTaskRequest } from "@axiom-lattice/protocols";
|
|
4
|
+
|
|
5
|
+
function getTenantId(request: FastifyRequest): string {
|
|
6
|
+
const userTenantId = (request as any).user?.tenantId;
|
|
7
|
+
if (userTenantId) return userTenantId;
|
|
8
|
+
return (request.headers["x-tenant-id"] as string) || "default";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getUserId(request: FastifyRequest): string {
|
|
12
|
+
const userId = (request as any).user?.id
|
|
13
|
+
|| (request.headers["x-user-id"] as string);
|
|
14
|
+
if (!userId) throw new Error("User ID is required");
|
|
15
|
+
return userId;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getStore(): TaskStore {
|
|
19
|
+
return getStoreLattice("default", "task").store as TaskStore;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function listTasks(
|
|
23
|
+
request: FastifyRequest,
|
|
24
|
+
reply: FastifyReply
|
|
25
|
+
): Promise<{ success: boolean; data?: TaskItem[]; count?: number; error?: string }> {
|
|
26
|
+
try {
|
|
27
|
+
const tenantId = getTenantId(request);
|
|
28
|
+
const userId = getUserId(request);
|
|
29
|
+
const query = request.query as Record<string, string>;
|
|
30
|
+
|
|
31
|
+
const store = getStore();
|
|
32
|
+
const tasks = await store.list({
|
|
33
|
+
tenantId,
|
|
34
|
+
ownerId: query.ownerId || userId,
|
|
35
|
+
status: query.status,
|
|
36
|
+
priority: query.priority,
|
|
37
|
+
ownerType: query.ownerType as 'user' | 'agent' | undefined,
|
|
38
|
+
metadata: query.metadata ? JSON.parse(query.metadata) : undefined,
|
|
39
|
+
limit: query.limit ? parseInt(query.limit) : undefined,
|
|
40
|
+
offset: query.offset ? parseInt(query.offset) : undefined,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return { success: true, data: tasks, count: tasks.length };
|
|
44
|
+
} catch (error: any) {
|
|
45
|
+
return { success: false, error: error.message };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function getTask(
|
|
50
|
+
request: FastifyRequest<{ Params: { id: string } }>,
|
|
51
|
+
reply: FastifyReply
|
|
52
|
+
): Promise<{ success: boolean; data?: TaskItem; error?: string }> {
|
|
53
|
+
try {
|
|
54
|
+
const tenantId = getTenantId(request);
|
|
55
|
+
const userId = getUserId(request);
|
|
56
|
+
const { id } = request.params;
|
|
57
|
+
|
|
58
|
+
const store = getStore();
|
|
59
|
+
const task = await store.getById(tenantId, id);
|
|
60
|
+
if (!task) {
|
|
61
|
+
return reply.status(404).send({ success: false, error: "Task not found" });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (task.ownerType === 'user' && task.ownerId !== userId) {
|
|
65
|
+
return reply.status(403).send({ success: false, error: "Access denied" });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { success: true, data: task };
|
|
69
|
+
} catch (error: any) {
|
|
70
|
+
return { success: false, error: error.message };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function createTask(
|
|
75
|
+
request: FastifyRequest<{ Body: CreateTaskRequest }>,
|
|
76
|
+
reply: FastifyReply
|
|
77
|
+
): Promise<{ success: boolean; data?: TaskItem; error?: string }> {
|
|
78
|
+
try {
|
|
79
|
+
const tenantId = getTenantId(request);
|
|
80
|
+
const userId = getUserId(request);
|
|
81
|
+
const body = request.body;
|
|
82
|
+
|
|
83
|
+
if (!body.title) {
|
|
84
|
+
return reply.status(400).send({ success: false, error: "title is required" });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const store = getStore();
|
|
88
|
+
const task = await store.create({
|
|
89
|
+
...body,
|
|
90
|
+
tenantId,
|
|
91
|
+
ownerType: (body.ownerType || "user") as 'user' | 'agent',
|
|
92
|
+
ownerId: body.ownerId || userId,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return reply.status(201).send({ success: true, data: task });
|
|
96
|
+
} catch (error: any) {
|
|
97
|
+
return reply.status(500).send({ success: false, error: error.message });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function updateTask(
|
|
102
|
+
request: FastifyRequest<{ Params: { id: string }; Body: UpdateTaskRequest }>,
|
|
103
|
+
reply: FastifyReply
|
|
104
|
+
): Promise<{ success: boolean; data?: TaskItem; error?: string }> {
|
|
105
|
+
try {
|
|
106
|
+
const tenantId = getTenantId(request);
|
|
107
|
+
const userId = getUserId(request);
|
|
108
|
+
const { id } = request.params;
|
|
109
|
+
|
|
110
|
+
const store = getStore();
|
|
111
|
+
const existing = await store.getById(tenantId, id);
|
|
112
|
+
if (!existing) {
|
|
113
|
+
return reply.status(404).send({ success: false, error: "Task not found" });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (existing.ownerType === 'user' && existing.ownerId !== userId) {
|
|
117
|
+
return reply.status(403).send({ success: false, error: "Access denied" });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const task = await store.update(tenantId, id, request.body);
|
|
121
|
+
if (!task) {
|
|
122
|
+
return reply.status(404).send({ success: false, error: "Task not found" });
|
|
123
|
+
}
|
|
124
|
+
return { success: true, data: task };
|
|
125
|
+
} catch (error: any) {
|
|
126
|
+
return reply.status(500).send({ success: false, error: error.message });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function deleteTask(
|
|
131
|
+
request: FastifyRequest<{ Params: { id: string } }>,
|
|
132
|
+
reply: FastifyReply
|
|
133
|
+
): Promise<{ success: boolean; message?: string; error?: string }> {
|
|
134
|
+
try {
|
|
135
|
+
const tenantId = getTenantId(request);
|
|
136
|
+
const userId = getUserId(request);
|
|
137
|
+
const { id } = request.params;
|
|
138
|
+
|
|
139
|
+
const store = getStore();
|
|
140
|
+
const existing = await store.getById(tenantId, id);
|
|
141
|
+
if (!existing) {
|
|
142
|
+
return reply.status(404).send({ success: false, error: "Task not found" });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (existing.ownerType === 'user' && existing.ownerId !== userId) {
|
|
146
|
+
return reply.status(403).send({ success: false, error: "Access denied" });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const deleted = await store.delete(tenantId, id);
|
|
150
|
+
if (!deleted) {
|
|
151
|
+
return reply.status(404).send({ success: false, error: "Task not found" });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { success: true, message: "Task deleted" };
|
|
155
|
+
} catch (error: any) {
|
|
156
|
+
return reply.status(500).send({ success: false, error: error.message });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function completeTask(
|
|
161
|
+
request: FastifyRequest<{ Params: { id: string } }>,
|
|
162
|
+
reply: FastifyReply
|
|
163
|
+
): Promise<{ success: boolean; data?: TaskItem; error?: string }> {
|
|
164
|
+
try {
|
|
165
|
+
const tenantId = getTenantId(request);
|
|
166
|
+
const userId = getUserId(request);
|
|
167
|
+
const { id } = request.params;
|
|
168
|
+
|
|
169
|
+
const store = getStore();
|
|
170
|
+
const existing = await store.getById(tenantId, id);
|
|
171
|
+
if (!existing) {
|
|
172
|
+
return reply.status(404).send({ success: false, error: "Task not found" });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (existing.ownerType === 'user' && existing.ownerId !== userId) {
|
|
176
|
+
return reply.status(403).send({ success: false, error: "Access denied" });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const task = await store.update(tenantId, id, { status: "completed" });
|
|
180
|
+
if (!task) {
|
|
181
|
+
return reply.status(404).send({ success: false, error: "Task not found" });
|
|
182
|
+
}
|
|
183
|
+
return { success: true, data: task };
|
|
184
|
+
} catch (error: any) {
|
|
185
|
+
return reply.status(500).send({ success: false, error: error.message });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -262,6 +262,8 @@ export class WorkspaceController {
|
|
|
262
262
|
throw new Error("Workspace not found");
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
+
console.log(`[getBackend] storageType=${workspace.storageType} filePath=${filePath}`);
|
|
266
|
+
|
|
265
267
|
if (workspace.storageType === "sandbox") {
|
|
266
268
|
const sandboxManager = getSandBoxManager();
|
|
267
269
|
|
|
@@ -273,18 +275,23 @@ export class WorkspaceController {
|
|
|
273
275
|
projectId,
|
|
274
276
|
};
|
|
275
277
|
|
|
278
|
+
console.log(`[getBackend] trying volume backend for path=${filePath} assistant_id=${assistantId}`);
|
|
276
279
|
const volumeBackend = await sandboxManager.getVolumeBackendForPath(volumeConfig, filePath || "/project");
|
|
277
280
|
if (volumeBackend) {
|
|
281
|
+
console.log(`[getBackend] using VolumeFilesystem`);
|
|
278
282
|
return { backend: volumeBackend, workspace };
|
|
279
283
|
}
|
|
280
284
|
|
|
285
|
+
console.log(`[getBackend] volume not found, falling back to SandboxFilesystem`);
|
|
281
286
|
const sandbox = await sandboxManager.getSandboxFromConfig(volumeConfig);
|
|
287
|
+
console.log(`[getBackend] sandbox acquired, name=${(sandbox as any).name || "unknown"}`);
|
|
282
288
|
return {
|
|
283
289
|
backend: new SandboxFilesystem({
|
|
284
290
|
sandboxInstance: sandbox,
|
|
285
291
|
}), workspace
|
|
286
292
|
};
|
|
287
293
|
} else {
|
|
294
|
+
console.log(`[getBackend] using FilesystemBackend rootDir=/lattice_store/tenants/${tenantId}/workspaces/${workspaceId}/${projectId}`);
|
|
288
295
|
return {
|
|
289
296
|
backend: new FilesystemBackend({
|
|
290
297
|
rootDir: `/lattice_store/tenants/${tenantId}/workspaces/${workspaceId}/${projectId}`,
|
|
@@ -549,8 +556,13 @@ export class WorkspaceController {
|
|
|
549
556
|
const path = (request.query.path as string) || "/";
|
|
550
557
|
const assistantId = request.query.assistantId;
|
|
551
558
|
|
|
559
|
+
console.log(`[listPath] tenantId=${tenantId} workspaceId=${workspaceId} projectId=${projectId} path=${path} assistantId=${assistantId}`);
|
|
560
|
+
|
|
552
561
|
const { backend } = await this.getBackend(tenantId, workspaceId, projectId, assistantId, path);
|
|
562
|
+
console.log(`[listPath] backend type=${backend.constructor.name} calling lsInfo(${path})`);
|
|
563
|
+
|
|
553
564
|
const files = await backend.lsInfo(path);
|
|
565
|
+
console.log(`[listPath] result count=${files.length}`);
|
|
554
566
|
|
|
555
567
|
return { success: true, data: files };
|
|
556
568
|
}
|