@axiom-lattice/gateway 2.1.39 → 2.1.41
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 +8 -8
- package/CHANGELOG.md +15 -0
- package/dist/index.js +464 -170
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +415 -118
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
- package/public/sdk/README.md +695 -0
- package/public/sdk/dashboard-engine-skill.md +1122 -0
- package/public/sdk/dashboard-single-file-spec.md +357 -0
- package/public/sdk/data-query-sdk-skill.md +307 -0
- package/public/sdk/data-query-sdk.d.ts +252 -0
- package/public/sdk/data-query-sdk.js +970 -0
- package/public/sdk/occupancy-dashboard.html +363 -0
- package/public/sdk/test-dashboard.html +690 -0
- package/src/__tests__/data-query.test.ts +77 -0
- package/src/controllers/data-query.ts +236 -0
- package/src/controllers/metrics-configs.ts +29 -25
- package/src/controllers/workspace.ts +95 -1
- package/src/index.ts +11 -0
- package/src/routes/index.ts +11 -0
- package/src/schemas/data-query.ts +69 -0
- package/src/schemas/index.ts +3 -0
- package/src/services/agent_task_consumer.ts +2 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, jest } from "@jest/globals";
|
|
2
|
+
import { FastifyRequest, FastifyReply } from "fastify";
|
|
3
|
+
import { executeDataQuery } from "../controllers/data-query";
|
|
4
|
+
|
|
5
|
+
// Mock @axiom-lattice/core
|
|
6
|
+
jest.mock("@axiom-lattice/core", () => ({
|
|
7
|
+
getStoreLattice: jest.fn(),
|
|
8
|
+
metricsServerManager: {
|
|
9
|
+
hasServer: jest.fn(),
|
|
10
|
+
registerServer: jest.fn(),
|
|
11
|
+
},
|
|
12
|
+
SemanticMetricsClient: jest.fn(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
describe("Data Query Controller", () => {
|
|
16
|
+
let mockRequest: Partial<FastifyRequest>;
|
|
17
|
+
let mockReply: Partial<FastifyReply>;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
mockRequest = {
|
|
21
|
+
headers: { "x-tenant-id": "test-tenant" },
|
|
22
|
+
body: {},
|
|
23
|
+
};
|
|
24
|
+
mockReply = {
|
|
25
|
+
code: jest.fn().mockReturnThis(),
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("Request validation", () => {
|
|
30
|
+
it("should return 400 if neither serverKey nor datasourceId is provided", async () => {
|
|
31
|
+
mockRequest.body = {};
|
|
32
|
+
|
|
33
|
+
const result = await executeDataQuery(
|
|
34
|
+
mockRequest as FastifyRequest,
|
|
35
|
+
mockReply as FastifyReply
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
expect(mockReply.code).toHaveBeenCalledWith(400);
|
|
39
|
+
expect(result.success).toBe(false);
|
|
40
|
+
expect(result.message).toContain("serverKey or datasourceId");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should return 400 if neither metrics nor customSql is provided", async () => {
|
|
44
|
+
mockRequest.body = {
|
|
45
|
+
serverKey: "test-server",
|
|
46
|
+
datasourceId: "1",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const result = await executeDataQuery(
|
|
50
|
+
mockRequest as FastifyRequest,
|
|
51
|
+
mockReply as FastifyReply
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
expect(mockReply.code).toHaveBeenCalledWith(400);
|
|
55
|
+
expect(result.success).toBe(false);
|
|
56
|
+
expect(result.message).toContain("metrics (for semantic query) or customSql (for SQL query)");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should return 400 if both metrics and customSql are provided", async () => {
|
|
60
|
+
mockRequest.body = {
|
|
61
|
+
serverKey: "test-server",
|
|
62
|
+
datasourceId: "1",
|
|
63
|
+
metrics: ["test_metric"],
|
|
64
|
+
customSql: "SELECT * FROM test",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const result = await executeDataQuery(
|
|
68
|
+
mockRequest as FastifyRequest,
|
|
69
|
+
mockReply as FastifyReply
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
expect(mockReply.code).toHaveBeenCalledWith(400);
|
|
73
|
+
expect(result.success).toBe(false);
|
|
74
|
+
expect(result.message).toContain("Cannot provide both");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { FastifyRequest, FastifyReply } from "fastify";
|
|
2
|
+
import {
|
|
3
|
+
getStoreLattice,
|
|
4
|
+
metricsServerManager,
|
|
5
|
+
SemanticMetricsClient,
|
|
6
|
+
} from "@axiom-lattice/core";
|
|
7
|
+
import type {
|
|
8
|
+
MetricsServerConfigStore,
|
|
9
|
+
SemanticMetricsServerConfig,
|
|
10
|
+
SemanticMetricsFilter,
|
|
11
|
+
} from "@axiom-lattice/protocols";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get tenant ID from request headers
|
|
15
|
+
*/
|
|
16
|
+
function getTenantId(request: FastifyRequest): string {
|
|
17
|
+
return (request.headers["x-tenant-id"] as string) || "default";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Data query request body
|
|
22
|
+
*/
|
|
23
|
+
interface DataQueryRequest {
|
|
24
|
+
serverKey?: string;
|
|
25
|
+
datasourceId?: string;
|
|
26
|
+
metrics?: string[];
|
|
27
|
+
groupBy?: string[];
|
|
28
|
+
filters?: Array<{
|
|
29
|
+
dimension: string;
|
|
30
|
+
operator: string;
|
|
31
|
+
values: (string | number | boolean)[];
|
|
32
|
+
}>;
|
|
33
|
+
customSql?: string;
|
|
34
|
+
params?: Record<string, string | number | boolean>;
|
|
35
|
+
limit?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Data query response - 只返回原始数据
|
|
40
|
+
*/
|
|
41
|
+
interface DataQueryResponse {
|
|
42
|
+
success: boolean;
|
|
43
|
+
message: string;
|
|
44
|
+
data?: {
|
|
45
|
+
// 语义查询结果
|
|
46
|
+
semanticModel?: string;
|
|
47
|
+
columns: Array<{ name: string; type: string }>;
|
|
48
|
+
rows?: Array<Array<unknown>>;
|
|
49
|
+
rowsObject?: Array<Record<string, unknown>>;
|
|
50
|
+
// SQL 查询结果
|
|
51
|
+
tableName?: string;
|
|
52
|
+
executedSql?: string;
|
|
53
|
+
// 通用元数据
|
|
54
|
+
metadata: {
|
|
55
|
+
rowCount: number;
|
|
56
|
+
executionTimeMs?: number;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Execute data query (semantic or SQL)
|
|
63
|
+
*/
|
|
64
|
+
export async function executeDataQuery(
|
|
65
|
+
request: FastifyRequest,
|
|
66
|
+
reply: FastifyReply
|
|
67
|
+
): Promise<DataQueryResponse> {
|
|
68
|
+
const tenantId = getTenantId(request);
|
|
69
|
+
const body = request.body as DataQueryRequest;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
// Validate request
|
|
73
|
+
if (!body.serverKey && !body.datasourceId) {
|
|
74
|
+
reply.code(400);
|
|
75
|
+
return {
|
|
76
|
+
success: false,
|
|
77
|
+
message: "Either serverKey or datasourceId must be provided",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Determine query type
|
|
82
|
+
const isSemanticQuery = body.metrics && body.metrics.length > 0;
|
|
83
|
+
const isSqlQuery = body.customSql && body.customSql.trim().length > 0;
|
|
84
|
+
|
|
85
|
+
if (!isSemanticQuery && !isSqlQuery) {
|
|
86
|
+
reply.code(400);
|
|
87
|
+
return {
|
|
88
|
+
success: false,
|
|
89
|
+
message: "Either metrics (for semantic query) or customSql (for SQL query) must be provided",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (isSemanticQuery && isSqlQuery) {
|
|
94
|
+
reply.code(400);
|
|
95
|
+
return {
|
|
96
|
+
success: false,
|
|
97
|
+
message: "Cannot provide both metrics and customSql. Use one query type only.",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Get server config
|
|
102
|
+
const storeLattice = getStoreLattice("default", "metrics");
|
|
103
|
+
const store: MetricsServerConfigStore = storeLattice.store;
|
|
104
|
+
|
|
105
|
+
if (!body.serverKey) {
|
|
106
|
+
reply.code(400);
|
|
107
|
+
return {
|
|
108
|
+
success: false,
|
|
109
|
+
message: "serverKey is required",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const config = await store.getConfigByKey(tenantId, body.serverKey);
|
|
114
|
+
if (!config) {
|
|
115
|
+
reply.code(404);
|
|
116
|
+
return {
|
|
117
|
+
success: false,
|
|
118
|
+
message: `Metrics server configuration not found: ${body.serverKey}`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (config.config.type !== "semantic") {
|
|
123
|
+
reply.code(400);
|
|
124
|
+
return {
|
|
125
|
+
success: false,
|
|
126
|
+
message: "This endpoint only supports semantic metrics servers",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!body.datasourceId) {
|
|
131
|
+
reply.code(400);
|
|
132
|
+
return {
|
|
133
|
+
success: false,
|
|
134
|
+
message: "datasourceId is required",
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check if server is registered
|
|
139
|
+
if (!metricsServerManager.hasServer(tenantId, body.serverKey)) {
|
|
140
|
+
reply.code(400);
|
|
141
|
+
return {
|
|
142
|
+
success: false,
|
|
143
|
+
message: `Metrics server not registered: ${body.serverKey}. Please register the server first.`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Get client from manager (read-only, no registration)
|
|
148
|
+
const client = metricsServerManager.getClient(tenantId, body.serverKey) as SemanticMetricsClient;
|
|
149
|
+
|
|
150
|
+
// Execute query based on type
|
|
151
|
+
if (isSemanticQuery) {
|
|
152
|
+
return await executeSemanticQuery(client, body, reply);
|
|
153
|
+
} else {
|
|
154
|
+
return await executeSqlQuery(client, body, reply);
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.error("Failed to execute data query:", error);
|
|
158
|
+
reply.code(500);
|
|
159
|
+
return {
|
|
160
|
+
success: false,
|
|
161
|
+
message: `Failed to execute data query: ${error instanceof Error ? error.message : String(error)}`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Execute semantic query
|
|
168
|
+
*/
|
|
169
|
+
async function executeSemanticQuery(
|
|
170
|
+
client: SemanticMetricsClient,
|
|
171
|
+
body: DataQueryRequest,
|
|
172
|
+
reply: FastifyReply
|
|
173
|
+
): Promise<DataQueryResponse> {
|
|
174
|
+
const semanticFilters: SemanticMetricsFilter[] = (body.filters || []).map(f => ({
|
|
175
|
+
dimension: f.dimension,
|
|
176
|
+
operator: f.operator,
|
|
177
|
+
values: f.values,
|
|
178
|
+
}));
|
|
179
|
+
|
|
180
|
+
const result = await client.semanticQuery({
|
|
181
|
+
datasourceId: body.datasourceId!,
|
|
182
|
+
metrics: body.metrics!,
|
|
183
|
+
groupBy: body.groupBy,
|
|
184
|
+
filters: semanticFilters,
|
|
185
|
+
limit: body.limit || 1000,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// 直接返回原始数据,不做 ECharts 转换
|
|
189
|
+
return {
|
|
190
|
+
success: true,
|
|
191
|
+
message: "Semantic query executed successfully",
|
|
192
|
+
data: {
|
|
193
|
+
semanticModel: result.semanticModel,
|
|
194
|
+
columns: result.columns,
|
|
195
|
+
rows: result.rows,
|
|
196
|
+
rowsObject: result.rowsObject,
|
|
197
|
+
metadata: {
|
|
198
|
+
rowCount: result.rowCount,
|
|
199
|
+
executionTimeMs: result.executionTimeMs,
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Execute SQL query
|
|
207
|
+
*/
|
|
208
|
+
async function executeSqlQuery(
|
|
209
|
+
client: SemanticMetricsClient,
|
|
210
|
+
body: DataQueryRequest,
|
|
211
|
+
reply: FastifyReply
|
|
212
|
+
): Promise<DataQueryResponse> {
|
|
213
|
+
const result = await client.executeSqlQuery({
|
|
214
|
+
datasourceId: body.datasourceId!,
|
|
215
|
+
customSql: body.customSql!,
|
|
216
|
+
params: body.params,
|
|
217
|
+
limit: body.limit || 100,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// 直接返回原始数据,不做 ECharts 转换
|
|
221
|
+
return {
|
|
222
|
+
success: true,
|
|
223
|
+
message: "SQL query executed successfully",
|
|
224
|
+
data: {
|
|
225
|
+
tableName: result.tableName,
|
|
226
|
+
columns: result.columns.map(name => ({ name, type: 'unknown' })),
|
|
227
|
+
rows: result.rows,
|
|
228
|
+
rowsObject: result.rowsObject,
|
|
229
|
+
executedSql: result.executedSql,
|
|
230
|
+
metadata: {
|
|
231
|
+
rowCount: result.rowCount,
|
|
232
|
+
executionTimeMs: result.executionTimeMs,
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
@@ -220,17 +220,16 @@ interface SemanticQueryResponse {
|
|
|
220
220
|
success: boolean;
|
|
221
221
|
message: string;
|
|
222
222
|
data?: {
|
|
223
|
-
|
|
224
|
-
|
|
223
|
+
semanticModel: string;
|
|
224
|
+
columns: string[];
|
|
225
225
|
dataPoints: Array<{
|
|
226
226
|
timestamp?: number;
|
|
227
227
|
value: number;
|
|
228
|
-
metricName?: string;
|
|
229
228
|
labels?: Record<string, string>;
|
|
230
229
|
}>;
|
|
231
230
|
metadata?: {
|
|
232
231
|
rowCount?: number;
|
|
233
|
-
|
|
232
|
+
columnCount?: number;
|
|
234
233
|
};
|
|
235
234
|
};
|
|
236
235
|
}
|
|
@@ -857,40 +856,45 @@ export async function querySemanticMetrics(
|
|
|
857
856
|
const result = await client.semanticQuery(body);
|
|
858
857
|
|
|
859
858
|
// Transform SemanticMetricsQueryResponse to response format
|
|
860
|
-
// The response contains
|
|
859
|
+
// The response contains columns and rows arrays
|
|
860
|
+
const columnNames = result.columns.map(col => col.name);
|
|
861
861
|
const allDataPoints: Array<{
|
|
862
862
|
timestamp?: number;
|
|
863
863
|
value: number;
|
|
864
|
-
metricName?: string;
|
|
865
864
|
labels?: Record<string, string>;
|
|
866
865
|
}> = [];
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
866
|
+
|
|
867
|
+
// Find timestamp column and value column indices
|
|
868
|
+
const timestampColIdx = columnNames.findIndex(col =>
|
|
869
|
+
col.toLowerCase().includes('date') || col.toLowerCase().includes('time')
|
|
870
|
+
);
|
|
871
|
+
const valueColIdx = columnNames.findIndex(col =>
|
|
872
|
+
col.toLowerCase().includes('value') || col.toLowerCase().includes('rate') || col.toLowerCase().includes('amt')
|
|
873
|
+
);
|
|
874
|
+
|
|
875
|
+
for (const row of result.rows) {
|
|
876
|
+
const timestamp = timestampColIdx >= 0 ? row[timestampColIdx] : undefined;
|
|
877
|
+
const value = valueColIdx >= 0 ? row[valueColIdx] : row[row.length - 1];
|
|
878
|
+
|
|
879
|
+
allDataPoints.push({
|
|
880
|
+
timestamp: timestamp ? new Date(String(timestamp)).getTime() : undefined,
|
|
881
|
+
value: typeof value === 'number' ? value : Number(value) || 0,
|
|
882
|
+
labels: Object.fromEntries(
|
|
883
|
+
columnNames.map((col, idx) => [col, String(row[idx] ?? '')])
|
|
884
|
+
),
|
|
885
|
+
});
|
|
882
886
|
}
|
|
883
887
|
|
|
884
888
|
return {
|
|
885
889
|
success: true,
|
|
886
890
|
message: "Semantic query executed successfully",
|
|
887
891
|
data: {
|
|
888
|
-
|
|
889
|
-
|
|
892
|
+
semanticModel: result.semanticModel,
|
|
893
|
+
columns: columnNames,
|
|
890
894
|
dataPoints: allDataPoints,
|
|
891
895
|
metadata: {
|
|
892
|
-
rowCount:
|
|
893
|
-
|
|
896
|
+
rowCount: result.rows.length,
|
|
897
|
+
columnCount: result.columns.length,
|
|
894
898
|
},
|
|
895
899
|
},
|
|
896
900
|
};
|
|
@@ -457,6 +457,46 @@ export class WorkspaceController {
|
|
|
457
457
|
const webStream = body.stream();
|
|
458
458
|
const nodeStream = Readable.fromWeb(webStream);
|
|
459
459
|
const contentType = body.contentType ?? inferredContentType;
|
|
460
|
+
console.log(`[viewFile] Sandbox returned stream, contentType: ${contentType}, filename: ${filename}`);
|
|
461
|
+
|
|
462
|
+
// Check if it's HTML and needs context injection
|
|
463
|
+
const isHtml = contentType?.toLowerCase().includes("text/html") ||
|
|
464
|
+
filename.toLowerCase().endsWith(".html") ||
|
|
465
|
+
filename.toLowerCase().endsWith(".htm");
|
|
466
|
+
|
|
467
|
+
if (isHtml) {
|
|
468
|
+
console.log(`[viewFile] HTML stream detected, collecting for context injection`);
|
|
469
|
+
// Collect stream content
|
|
470
|
+
const chunks: Buffer[] = [];
|
|
471
|
+
for await (const chunk of nodeStream) {
|
|
472
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
473
|
+
}
|
|
474
|
+
let content = Buffer.concat(chunks).toString("utf-8");
|
|
475
|
+
const contextScript = `<script>window.__AI2APP_CONTEXT__=${JSON.stringify({
|
|
476
|
+
tenantId,
|
|
477
|
+
workspaceId,
|
|
478
|
+
projectId,
|
|
479
|
+
timestamp: Date.now()
|
|
480
|
+
})};</script>`;
|
|
481
|
+
|
|
482
|
+
if (content.toLowerCase().includes("</head>")) {
|
|
483
|
+
content = content.replace(/<\/head>/i, `${contextScript}</head>`);
|
|
484
|
+
console.log(`[viewFile] Context script injected before </head> (stream)`);
|
|
485
|
+
} else if (content.toLowerCase().includes("<html>")) {
|
|
486
|
+
content = content.replace(/<html>/i, `<html>${contextScript}`);
|
|
487
|
+
console.log(`[viewFile] Context script injected after <html> (stream)`);
|
|
488
|
+
} else {
|
|
489
|
+
content = contextScript + content;
|
|
490
|
+
console.log(`[viewFile] Context script prepended to content (stream)`);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return reply
|
|
494
|
+
.status(200)
|
|
495
|
+
.type(contentType)
|
|
496
|
+
.header("Content-Disposition", "inline")
|
|
497
|
+
.send(Buffer.from(content, "utf-8"));
|
|
498
|
+
}
|
|
499
|
+
|
|
460
500
|
// inline for viewing, not attachment for download
|
|
461
501
|
return reply
|
|
462
502
|
.status(200)
|
|
@@ -487,6 +527,33 @@ export class WorkspaceController {
|
|
|
487
527
|
return reply.status(502).send({ success: false, error: "Unexpected view response format" });
|
|
488
528
|
}
|
|
489
529
|
|
|
530
|
+
// Inject AI2APP context script for HTML files (sandbox storage)
|
|
531
|
+
const isHtml = contentType?.toLowerCase().includes("text/html") ||
|
|
532
|
+
filename.toLowerCase().endsWith(".html") ||
|
|
533
|
+
filename.toLowerCase().endsWith(".htm");
|
|
534
|
+
if (isHtml) {
|
|
535
|
+
console.log(`[viewFile] Injecting AI2APP context for sandbox HTML file: ${filename}, tenantId: ${tenantId}, contentType: ${contentType}`);
|
|
536
|
+
let content = buf.toString("utf-8");
|
|
537
|
+
const contextScript = `<script>window.__AI2APP_CONTEXT__=${JSON.stringify({
|
|
538
|
+
tenantId,
|
|
539
|
+
workspaceId,
|
|
540
|
+
projectId,
|
|
541
|
+
timestamp: Date.now()
|
|
542
|
+
})};</script>`;
|
|
543
|
+
|
|
544
|
+
if (content.toLowerCase().includes("</head>")) {
|
|
545
|
+
content = content.replace(/<\/head>/i, `${contextScript}</head>`);
|
|
546
|
+
console.log(`[viewFile] Context script injected before </head>`);
|
|
547
|
+
} else if (content.toLowerCase().includes("<html>")) {
|
|
548
|
+
content = content.replace(/<html>/i, `<html>${contextScript}`);
|
|
549
|
+
console.log(`[viewFile] Context script injected after <html>`);
|
|
550
|
+
} else {
|
|
551
|
+
content = contextScript + content;
|
|
552
|
+
console.log(`[viewFile] Context script prepended to content`);
|
|
553
|
+
}
|
|
554
|
+
buf = Buffer.from(content, "utf-8");
|
|
555
|
+
}
|
|
556
|
+
|
|
490
557
|
return reply
|
|
491
558
|
.status(200)
|
|
492
559
|
.type(contentType)
|
|
@@ -498,7 +565,34 @@ export class WorkspaceController {
|
|
|
498
565
|
const content = await backend.read(resolvedPath, 0, Infinity);
|
|
499
566
|
const filename = this.getFilenameFromPath(resolvedPath);
|
|
500
567
|
const mimeType = this.getMimeType(filename);
|
|
501
|
-
|
|
568
|
+
|
|
569
|
+
// Inject AI2APP context script for HTML files
|
|
570
|
+
let finalContent = content;
|
|
571
|
+
const isHtmlFs = mimeType?.toLowerCase().includes("text/html") ||
|
|
572
|
+
filename.toLowerCase().endsWith(".html") ||
|
|
573
|
+
filename.toLowerCase().endsWith(".htm");
|
|
574
|
+
if (isHtmlFs) {
|
|
575
|
+
console.log(`[viewFile] Injecting AI2APP context for filesystem HTML file: ${filename}, tenantId: ${tenantId}, mimeType: ${mimeType}`);
|
|
576
|
+
const contextScript = `<script>window.__AI2APP_CONTEXT__=${JSON.stringify({
|
|
577
|
+
tenantId,
|
|
578
|
+
workspaceId,
|
|
579
|
+
projectId,
|
|
580
|
+
timestamp: Date.now()
|
|
581
|
+
})};</script>`;
|
|
582
|
+
// Insert before </head> or </html>
|
|
583
|
+
if (content.toLowerCase().includes("</head>")) {
|
|
584
|
+
finalContent = content.replace(/<\/head>/i, `${contextScript}</head>`);
|
|
585
|
+
console.log(`[viewFile] Context script injected before </head>`);
|
|
586
|
+
} else if (content.toLowerCase().includes("</html>")) {
|
|
587
|
+
finalContent = content.replace(/<\/html>/i, `${contextScript}</html>`);
|
|
588
|
+
console.log(`[viewFile] Context script injected before </html>`);
|
|
589
|
+
} else {
|
|
590
|
+
finalContent = content + contextScript;
|
|
591
|
+
console.log(`[viewFile] Context script appended to content`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const buffer = Buffer.from(finalContent, "utf-8");
|
|
502
596
|
|
|
503
597
|
return reply
|
|
504
598
|
.status(200)
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,8 @@ import cors from "@fastify/cors";
|
|
|
3
3
|
import multipart from "@fastify/multipart";
|
|
4
4
|
import sensible from "@fastify/sensible";
|
|
5
5
|
import websocket from "@fastify/websocket";
|
|
6
|
+
import staticPlugin from "@fastify/static";
|
|
7
|
+
import path from "path";
|
|
6
8
|
import { registerLatticeRoutes } from "./routes";
|
|
7
9
|
import { configureSwagger } from "./swagger";
|
|
8
10
|
import {
|
|
@@ -122,6 +124,12 @@ app.register(multipart, {
|
|
|
122
124
|
});
|
|
123
125
|
app.register(websocket);
|
|
124
126
|
|
|
127
|
+
// Register static file serving for SDK
|
|
128
|
+
app.register(staticPlugin, {
|
|
129
|
+
root: path.join(__dirname, "../public"),
|
|
130
|
+
prefix: "/",
|
|
131
|
+
});
|
|
132
|
+
|
|
125
133
|
// Error handler
|
|
126
134
|
app.setErrorHandler((error, request, reply) => {
|
|
127
135
|
// Convert headers to strings (Fastify headers can be string | string[])
|
|
@@ -197,6 +205,9 @@ const start = async (config?: LatticeGatewayConfig) => {
|
|
|
197
205
|
// Access via: request.server.loggerLattice or app.loggerLattice
|
|
198
206
|
app.decorate("loggerLattice", loggerLattice);
|
|
199
207
|
|
|
208
|
+
// Register all routes
|
|
209
|
+
registerLatticeRoutes(app);
|
|
210
|
+
|
|
200
211
|
// Register sandbox manager if not already registered
|
|
201
212
|
if (!sandboxLatticeManager.hasLattice("default")) {
|
|
202
213
|
const sandboxBaseURL = process.env.SANDBOX_BASE_URL || "http://localhost:8080";
|
package/src/routes/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import * as modelsController from "../controllers/models";
|
|
|
12
12
|
import * as healthController from "../controllers/health";
|
|
13
13
|
import * as skillsController from "../controllers/skills";
|
|
14
14
|
import * as toolsController from "../controllers/tools";
|
|
15
|
+
import * as dataQueryController from "../controllers/data-query";
|
|
15
16
|
import {
|
|
16
17
|
createRunSchema,
|
|
17
18
|
getAllMemoryItemsSchema,
|
|
@@ -27,6 +28,7 @@ import {
|
|
|
27
28
|
getConfigSchema,
|
|
28
29
|
getHealthSchema,
|
|
29
30
|
getSandboxUrlSchema,
|
|
31
|
+
dataQuerySchema,
|
|
30
32
|
} from "../schemas";
|
|
31
33
|
import { registerSandboxProxyRoutes } from "../controllers/sandbox";
|
|
32
34
|
import { registerWorkspaceRoutes } from "../controllers/workspace";
|
|
@@ -307,6 +309,15 @@ export const registerLatticeRoutes = (app: FastifyInstance): void => {
|
|
|
307
309
|
|
|
308
310
|
registerMetricsServerConfigRoutes(app);
|
|
309
311
|
|
|
312
|
+
// Data query route
|
|
313
|
+
app.post<{
|
|
314
|
+
Body: any;
|
|
315
|
+
}>(
|
|
316
|
+
"/api/data/query",
|
|
317
|
+
{ schema: dataQuerySchema },
|
|
318
|
+
dataQueryController.executeDataQuery
|
|
319
|
+
);
|
|
320
|
+
|
|
310
321
|
registerMcpServerConfigRoutes(app);
|
|
311
322
|
|
|
312
323
|
registerUserRoutes(app);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { FastifySchema } from "fastify";
|
|
2
|
+
|
|
3
|
+
export const dataQuerySchema: FastifySchema = {
|
|
4
|
+
description: "Execute data query (semantic or SQL)",
|
|
5
|
+
tags: ["Data Query"],
|
|
6
|
+
summary: "Query Data",
|
|
7
|
+
body: {
|
|
8
|
+
type: "object",
|
|
9
|
+
properties: {
|
|
10
|
+
serverKey: {
|
|
11
|
+
type: "string",
|
|
12
|
+
description: "Target semantic metrics server key (optional if configured in runConfig)"
|
|
13
|
+
},
|
|
14
|
+
datasourceId: {
|
|
15
|
+
type: "string",
|
|
16
|
+
description: "Data source ID (optional if configured in runConfig)"
|
|
17
|
+
},
|
|
18
|
+
// Semantic query parameters
|
|
19
|
+
metrics: {
|
|
20
|
+
type: "array",
|
|
21
|
+
items: { type: "string" },
|
|
22
|
+
description: "Array of metric names for semantic query"
|
|
23
|
+
},
|
|
24
|
+
groupBy: {
|
|
25
|
+
type: "array",
|
|
26
|
+
items: { type: "string" },
|
|
27
|
+
description: "Optional array of dimensions to group by"
|
|
28
|
+
},
|
|
29
|
+
filters: {
|
|
30
|
+
type: "array",
|
|
31
|
+
items: {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: {
|
|
34
|
+
dimension: { type: "string" },
|
|
35
|
+
operator: { type: "string" },
|
|
36
|
+
values: {
|
|
37
|
+
type: "array",
|
|
38
|
+
items: { type: ["string", "number", "boolean"] }
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
required: ["dimension", "operator", "values"]
|
|
42
|
+
},
|
|
43
|
+
description: "Optional array of filters"
|
|
44
|
+
},
|
|
45
|
+
// SQL query parameters
|
|
46
|
+
customSql: {
|
|
47
|
+
type: "string",
|
|
48
|
+
description: "Custom SQL query string with named parameters"
|
|
49
|
+
},
|
|
50
|
+
params: {
|
|
51
|
+
type: "object",
|
|
52
|
+
additionalProperties: { type: ["string", "number", "boolean"] },
|
|
53
|
+
description: "Optional parameters for SQL query"
|
|
54
|
+
},
|
|
55
|
+
// Common parameters
|
|
56
|
+
limit: {
|
|
57
|
+
type: "number",
|
|
58
|
+
description: "Maximum number of results (default: 1000)"
|
|
59
|
+
},
|
|
60
|
+
format: {
|
|
61
|
+
type: "string",
|
|
62
|
+
enum: ["echarts", "raw"],
|
|
63
|
+
description: "Response format (default: echarts)"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
// Response schema temporarily removed - format not yet finalized
|
|
68
|
+
// TODO: Add proper response schema once data format is confirmed
|
|
69
|
+
};
|
package/src/schemas/index.ts
CHANGED
|
@@ -55,6 +55,8 @@ const handleAgentTask = async (
|
|
|
55
55
|
headers: {
|
|
56
56
|
"Content-Type": "application/json",
|
|
57
57
|
"x-tenant-id": tenant_id,
|
|
58
|
+
"x-workspace-id": runConfig?.workspaceId as string,
|
|
59
|
+
"x-project-id": runConfig?.projectId as string,
|
|
58
60
|
},
|
|
59
61
|
}).catch((err) => {
|
|
60
62
|
console.error(`fetch请求失败: ${err.message || String(err)}`);
|