@axiom-lattice/gateway 2.1.19 → 2.1.21

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiom-lattice/gateway",
3
- "version": "2.1.19",
3
+ "version": "2.1.21",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -32,9 +32,9 @@
32
32
  "pino-roll": "^3.1.0",
33
33
  "redis": "^5.0.1",
34
34
  "uuid": "^9.0.1",
35
- "@axiom-lattice/core": "2.1.14",
36
- "@axiom-lattice/protocols": "2.1.8",
37
- "@axiom-lattice/queue-redis": "1.0.7"
35
+ "@axiom-lattice/core": "2.1.16",
36
+ "@axiom-lattice/protocols": "2.1.10",
37
+ "@axiom-lattice/queue-redis": "1.0.9"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/jest": "^29.5.14",
@@ -0,0 +1,474 @@
1
+ import { FastifyRequest, FastifyReply } from "fastify";
2
+ import { getStoreLattice } from "@axiom-lattice/core";
3
+ import { validateSkillName } from "@axiom-lattice/core";
4
+ import type {
5
+ Skill,
6
+ CreateSkillRequest,
7
+ } from "@axiom-lattice/protocols";
8
+
9
+ /**
10
+ * Skills Controller
11
+ * Handles skill-related CRUD operations
12
+ */
13
+
14
+ /**
15
+ * Skill list response interface
16
+ */
17
+ interface SkillListResponse {
18
+ success: boolean;
19
+ message: string;
20
+ data: {
21
+ records: Skill[];
22
+ total: number;
23
+ };
24
+ }
25
+
26
+ /**
27
+ * Skill response interface
28
+ */
29
+ interface SkillResponse {
30
+ success: boolean;
31
+ message: string;
32
+ data?: Skill;
33
+ }
34
+
35
+ /**
36
+ * Skill update request body interface
37
+ */
38
+ interface SkillUpdateBody {
39
+ name?: string;
40
+ description?: string;
41
+ license?: string;
42
+ compatibility?: string;
43
+ metadata?: Record<string, string>;
44
+ content?: string;
45
+ subSkills?: string[];
46
+ }
47
+
48
+ /**
49
+ * Serialize Skill object for JSON response
50
+ * Converts Date objects to ISO strings
51
+ * Explicitly creates a plain object to ensure all fields are serializable
52
+ */
53
+ function serializeSkill(skill: Skill): any {
54
+ // Explicitly create a plain object with all fields
55
+ // This ensures Fastify can properly serialize the response
56
+ const serialized: any = {
57
+ id: skill.id,
58
+ name: skill.name,
59
+ description: skill.description,
60
+ license: skill.license,
61
+ compatibility: skill.compatibility,
62
+ metadata: skill.metadata || {},
63
+ content: skill.content,
64
+ subSkills: skill.subSkills,
65
+ createdAt: skill.createdAt instanceof Date ? skill.createdAt.toISOString() : (skill.createdAt ? new Date(skill.createdAt).toISOString() : new Date().toISOString()),
66
+ updatedAt: skill.updatedAt instanceof Date ? skill.updatedAt.toISOString() : (skill.updatedAt ? new Date(skill.updatedAt).toISOString() : new Date().toISOString()),
67
+ };
68
+
69
+ // Remove undefined fields to avoid serialization issues
70
+ Object.keys(serialized).forEach((key) => {
71
+ if (serialized[key] === undefined) {
72
+ delete serialized[key];
73
+ }
74
+ });
75
+
76
+ return serialized;
77
+ }
78
+
79
+ /**
80
+ * Get list of all skills
81
+ */
82
+ export async function getSkillList(
83
+ request: FastifyRequest,
84
+ reply: FastifyReply
85
+ ): Promise<SkillListResponse> {
86
+ try {
87
+ const storeLattice = getStoreLattice("default", "skill");
88
+ const skillStore = storeLattice.store;
89
+ const skills = await skillStore.getAllSkills();
90
+
91
+ // Serialize skills to convert Date objects to ISO strings
92
+ const serializedSkills = skills.map(serializeSkill);
93
+
94
+ return {
95
+ success: true,
96
+ message: "Successfully retrieved skill list",
97
+ data: {
98
+ records: serializedSkills,
99
+ total: serializedSkills.length,
100
+ },
101
+ };
102
+ } catch (error: any) {
103
+ return reply.status(500).send({
104
+ success: false,
105
+ message: `Failed to retrieve skills: ${error.message}`,
106
+ data: {
107
+ records: [],
108
+ total: 0,
109
+ },
110
+ });
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Get a single skill by ID
116
+ */
117
+ export async function getSkill(
118
+ request: FastifyRequest<{ Params: { id: string } }>,
119
+ reply: FastifyReply
120
+ ): Promise<SkillResponse> {
121
+ try {
122
+ const { id } = request.params;
123
+
124
+ const storeLattice = getStoreLattice("default", "skill");
125
+ const skillStore = storeLattice.store;
126
+ const skill = await skillStore.getSkillById(id);
127
+
128
+ if (!skill) {
129
+ return reply.status(404).send({
130
+ success: false,
131
+ message: "Skill not found",
132
+ });
133
+ }
134
+
135
+ return {
136
+ success: true,
137
+ message: "Successfully retrieved skill",
138
+ data: serializeSkill(skill),
139
+ };
140
+ } catch (error: any) {
141
+ return reply.status(500).send({
142
+ success: false,
143
+ message: `Failed to retrieve skill: ${error.message}`,
144
+ });
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Create a new skill
150
+ */
151
+ export async function createSkill(
152
+ request: FastifyRequest<{ Body: CreateSkillRequest }>,
153
+ reply: FastifyReply
154
+ ): Promise<SkillResponse> {
155
+ try {
156
+ const data = request.body;
157
+
158
+ // Validate required fields
159
+ if (!data.name) {
160
+ return reply.status(400).send({
161
+ success: false,
162
+ message: "name is required",
163
+ });
164
+ }
165
+
166
+ if (!data.description) {
167
+ return reply.status(400).send({
168
+ success: false,
169
+ message: "description is required",
170
+ });
171
+ }
172
+
173
+ // Validate name format
174
+ try {
175
+ validateSkillName(data.name);
176
+ } catch (error: any) {
177
+ return reply.status(400).send({
178
+ success: false,
179
+ message: error.message || "Invalid skill name format",
180
+ });
181
+ }
182
+
183
+ // ID must equal name (name is used for path addressing)
184
+ const id = (request.body as any).id || data.name;
185
+
186
+ if (id !== data.name) {
187
+ return reply.status(400).send({
188
+ success: false,
189
+ message: `id "${id}" must equal name "${data.name}" (name is used for path addressing)`,
190
+ });
191
+ }
192
+
193
+ const storeLattice = getStoreLattice("default", "skill");
194
+ const skillStore = storeLattice.store;
195
+
196
+ // Check if skill already exists
197
+ const exists = await skillStore.hasSkill(id);
198
+ if (exists) {
199
+ return reply.status(409).send({
200
+ success: false,
201
+ message: `Skill with id "${id}" already exists`,
202
+ });
203
+ }
204
+
205
+ // Create skill
206
+ const newSkill = await skillStore.createSkill(id, data);
207
+
208
+ return reply.status(201).send({
209
+ success: true,
210
+ message: "Successfully created skill",
211
+ data: serializeSkill(newSkill),
212
+ });
213
+ } catch (error: any) {
214
+ return reply.status(500).send({
215
+ success: false,
216
+ message: `Failed to create skill: ${error.message}`,
217
+ });
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Update an existing skill by ID
223
+ */
224
+ export async function updateSkill(
225
+ request: FastifyRequest<{
226
+ Params: { id: string };
227
+ Body: SkillUpdateBody;
228
+ }>,
229
+ reply: FastifyReply
230
+ ): Promise<SkillResponse> {
231
+ try {
232
+ const { id } = request.params;
233
+ const updates = request.body;
234
+
235
+ // Validate name format if name is being updated
236
+ if (updates.name !== undefined) {
237
+ try {
238
+ validateSkillName(updates.name);
239
+ } catch (error: any) {
240
+ return reply.status(400).send({
241
+ success: false,
242
+ message: error.message || "Invalid skill name format",
243
+ });
244
+ }
245
+ }
246
+
247
+ const storeLattice = getStoreLattice("default", "skill");
248
+ const skillStore = storeLattice.store;
249
+
250
+ // Check if skill exists
251
+ const exists = await skillStore.hasSkill(id);
252
+ if (!exists) {
253
+ return reply.status(404).send({
254
+ success: false,
255
+ message: "Skill not found",
256
+ });
257
+ }
258
+
259
+ // Update skill
260
+ const updatedSkill = await skillStore.updateSkill(id, updates);
261
+
262
+ if (!updatedSkill) {
263
+ return reply.status(500).send({
264
+ success: false,
265
+ message: "Failed to update skill",
266
+ });
267
+ }
268
+
269
+ return {
270
+ success: true,
271
+ message: "Successfully updated skill",
272
+ data: serializeSkill(updatedSkill),
273
+ };
274
+ } catch (error: any) {
275
+ return reply.status(500).send({
276
+ success: false,
277
+ message: `Failed to update skill: ${error.message}`,
278
+ });
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Delete a skill by ID
284
+ */
285
+ export async function deleteSkill(
286
+ request: FastifyRequest<{ Params: { id: string } }>,
287
+ reply: FastifyReply
288
+ ): Promise<{ success: boolean; message: string }> {
289
+ try {
290
+ const { id } = request.params;
291
+
292
+ const storeLattice = getStoreLattice("default", "skill");
293
+ const skillStore = storeLattice.store;
294
+
295
+ // Check if skill exists
296
+ const exists = await skillStore.hasSkill(id);
297
+ if (!exists) {
298
+ return reply.status(404).send({
299
+ success: false,
300
+ message: "Skill not found",
301
+ });
302
+ }
303
+
304
+ // Delete the skill
305
+ const deleted = await skillStore.deleteSkill(id);
306
+
307
+ if (!deleted) {
308
+ return reply.status(500).send({
309
+ success: false,
310
+ message: "Failed to delete skill",
311
+ });
312
+ }
313
+
314
+ return {
315
+ success: true,
316
+ message: "Successfully deleted skill",
317
+ };
318
+ } catch (error: any) {
319
+ return reply.status(500).send({
320
+ success: false,
321
+ message: `Failed to delete skill: ${error.message}`,
322
+ });
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Search skills by metadata
328
+ */
329
+ export async function searchSkillsByMetadata(
330
+ request: FastifyRequest<{
331
+ Querystring: { key: string; value: string };
332
+ }>,
333
+ reply: FastifyReply
334
+ ): Promise<SkillListResponse> {
335
+ try {
336
+ const { key, value } = request.query;
337
+
338
+ if (!key || !value) {
339
+ return reply.status(400).send({
340
+ success: false,
341
+ message: "key and value query parameters are required",
342
+ data: {
343
+ records: [],
344
+ total: 0,
345
+ },
346
+ });
347
+ }
348
+
349
+ const storeLattice = getStoreLattice("default", "skill");
350
+ const skillStore = storeLattice.store;
351
+ const skills = await skillStore.searchByMetadata(key, value);
352
+
353
+ // Serialize skills to convert Date objects to ISO strings
354
+ const serializedSkills = skills.map(serializeSkill);
355
+
356
+ return {
357
+ success: true,
358
+ message: "Successfully searched skills",
359
+ data: {
360
+ records: serializedSkills,
361
+ total: serializedSkills.length,
362
+ },
363
+ };
364
+ } catch (error: any) {
365
+ return reply.status(500).send({
366
+ success: false,
367
+ message: `Failed to search skills: ${error.message}`,
368
+ data: {
369
+ records: [],
370
+ total: 0,
371
+ },
372
+ });
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Filter skills by compatibility
378
+ */
379
+ export async function filterSkillsByCompatibility(
380
+ request: FastifyRequest<{
381
+ Querystring: { compatibility: string };
382
+ }>,
383
+ reply: FastifyReply
384
+ ): Promise<SkillListResponse> {
385
+ try {
386
+ const { compatibility } = request.query;
387
+
388
+ if (!compatibility) {
389
+ return reply.status(400).send({
390
+ success: false,
391
+ message: "compatibility query parameter is required",
392
+ data: {
393
+ records: [],
394
+ total: 0,
395
+ },
396
+ });
397
+ }
398
+
399
+ const storeLattice = getStoreLattice("default", "skill");
400
+ const skillStore = storeLattice.store;
401
+ const skills = await skillStore.filterByCompatibility(compatibility);
402
+
403
+ // Serialize skills to convert Date objects to ISO strings
404
+ const serializedSkills = skills.map(serializeSkill);
405
+
406
+ return {
407
+ success: true,
408
+ message: "Successfully filtered skills",
409
+ data: {
410
+ records: serializedSkills,
411
+ total: serializedSkills.length,
412
+ },
413
+ };
414
+ } catch (error: any) {
415
+ return reply.status(500).send({
416
+ success: false,
417
+ message: `Failed to filter skills: ${error.message}`,
418
+ data: {
419
+ records: [],
420
+ total: 0,
421
+ },
422
+ });
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Filter skills by license
428
+ */
429
+ export async function filterSkillsByLicense(
430
+ request: FastifyRequest<{
431
+ Querystring: { license: string };
432
+ }>,
433
+ reply: FastifyReply
434
+ ): Promise<SkillListResponse> {
435
+ try {
436
+ const { license } = request.query;
437
+
438
+ if (!license) {
439
+ return reply.status(400).send({
440
+ success: false,
441
+ message: "license query parameter is required",
442
+ data: {
443
+ records: [],
444
+ total: 0,
445
+ },
446
+ });
447
+ }
448
+
449
+ const storeLattice = getStoreLattice("default", "skill");
450
+ const skillStore = storeLattice.store;
451
+ const skills = await skillStore.filterByLicense(license);
452
+
453
+ // Serialize skills to convert Date objects to ISO strings
454
+ const serializedSkills = skills.map(serializeSkill);
455
+
456
+ return {
457
+ success: true,
458
+ message: "Successfully filtered skills",
459
+ data: {
460
+ records: serializedSkills,
461
+ total: serializedSkills.length,
462
+ },
463
+ };
464
+ } catch (error: any) {
465
+ return reply.status(500).send({
466
+ success: false,
467
+ message: `Failed to filter skills: ${error.message}`,
468
+ data: {
469
+ records: [],
470
+ total: 0,
471
+ },
472
+ });
473
+ }
474
+ }
package/src/index.ts CHANGED
@@ -2,25 +2,56 @@ import fastify from "fastify";
2
2
  import cors from "@fastify/cors";
3
3
  import sensible from "@fastify/sensible";
4
4
  import { registerLatticeRoutes } from "./routes";
5
- // 导入自定义 Logger 类
6
- import { Logger } from "./logger/Logger";
7
5
  import { configureSwagger } from "./swagger";
8
6
  import {
9
7
  setQueueServiceType,
10
8
  QueueServiceType,
11
9
  } from "./services/queue_service";
12
10
  import { AgentTaskConsumer } from "./services/agent_task_consumer";
11
+ import {
12
+ registerLoggerLattice,
13
+ getLoggerLattice,
14
+ loggerLatticeManager,
15
+ } from "@axiom-lattice/core";
16
+ import {
17
+ LoggerType,
18
+ LoggerConfig,
19
+ PinoFileOptions,
20
+ } from "@axiom-lattice/protocols";
13
21
 
14
22
  process.on("unhandledRejection", (reason, promise) => {
15
23
  console.error("未处理的Promise拒绝:", reason);
16
24
  // 可以在这里进行日志记录或其他处理
17
25
  });
18
26
 
19
- // 创建自定义日志记录器
20
- const logger = new Logger({
21
- serviceName: "lattice-gateway",
22
- name: "fastify-server",
23
- });
27
+ // Default logger configuration
28
+ const DEFAULT_LOGGER_CONFIG: LoggerConfig = {
29
+ name: "default",
30
+ description: "Default logger for lattice-gateway service",
31
+ type: LoggerType.PINO,
32
+ serviceName: "lattice/gateway",
33
+ loggerName: "lattice/gateway",
34
+ };
35
+
36
+ // Initialize logger with default config (can be overridden in start function)
37
+ let loggerLattice = initializeLogger(DEFAULT_LOGGER_CONFIG);
38
+ let logger = loggerLattice.client;
39
+
40
+ /**
41
+ * Initialize logger lattice with given configuration
42
+ */
43
+ function initializeLogger(config: LoggerConfig) {
44
+ // Remove existing logger if it exists
45
+ if (loggerLatticeManager.hasLattice("default")) {
46
+ loggerLatticeManager.removeLattice("default");
47
+ }
48
+
49
+ // Register logger with provided config
50
+ registerLoggerLattice("default", config);
51
+
52
+ // Get and return logger lattice instance
53
+ return getLoggerLattice("default");
54
+ }
24
55
 
25
56
  // 创建 Fastify 应用
26
57
  const app = fastify({
@@ -28,20 +59,45 @@ const app = fastify({
28
59
  bodyLimit: Number(process.env.BODY_LIMIT) || 50 * 1024 * 1024, // Default 50MB, configurable via BODY_LIMIT env var
29
60
  });
30
61
 
31
- // 添加自定义日志记录
62
+ // Add custom logging hooks
32
63
  app.addHook("onRequest", (request, reply, done) => {
64
+ // Convert headers to strings (Fastify headers can be string | string[])
65
+ const getHeaderValue = (
66
+ header: string | string[] | undefined
67
+ ): string | undefined => {
68
+ if (Array.isArray(header)) {
69
+ return header[0];
70
+ }
71
+ return header;
72
+ };
73
+
33
74
  const context = {
34
- "x-tenant-id": request.headers["x-tenant-id"],
35
- "x-request-id": request.headers["x-request-id"],
75
+ "x-tenant-id": getHeaderValue(request.headers["x-tenant-id"]),
76
+ "x-request-id": getHeaderValue(request.headers["x-request-id"]),
36
77
  };
78
+ // Update logger context for this request
79
+ if (loggerLattice.updateContext) {
80
+ loggerLattice.updateContext(context);
81
+ }
37
82
  done();
38
83
  });
39
84
 
40
85
  app.addHook("onResponse", (request, reply, done) => {
86
+ // Convert headers to strings (Fastify headers can be string | string[])
87
+ const getHeaderValue = (
88
+ header: string | string[] | undefined
89
+ ): string | undefined => {
90
+ if (Array.isArray(header)) {
91
+ return header[0];
92
+ }
93
+ return header;
94
+ };
95
+
41
96
  const context = {
42
- "x-tenant-id": request.headers["x-tenant-id"],
43
- "x-request-id": request.headers["x-request-id"],
97
+ "x-tenant-id": getHeaderValue(request.headers["x-tenant-id"]),
98
+ "x-request-id": getHeaderValue(request.headers["x-request-id"]),
44
99
  };
100
+ loggerLattice.info(`${request.method} ${request.url} - ${reply.statusCode}`);
45
101
  done();
46
102
  });
47
103
 
@@ -61,11 +117,21 @@ app.register(cors, {
61
117
  });
62
118
  app.register(sensible);
63
119
 
64
- // 错误处理
120
+ // Error handler
65
121
  app.setErrorHandler((error, request, reply) => {
122
+ // Convert headers to strings (Fastify headers can be string | string[])
123
+ const getHeaderValue = (
124
+ header: string | string[] | undefined
125
+ ): string | undefined => {
126
+ if (Array.isArray(header)) {
127
+ return header[0];
128
+ }
129
+ return header;
130
+ };
131
+
66
132
  const context = {
67
- "x-tenant-id": request.headers["x-tenant-id"],
68
- "x-request-id": request.headers["x-request-id"],
133
+ "x-tenant-id": getHeaderValue(request.headers["x-tenant-id"]),
134
+ "x-request-id": getHeaderValue(request.headers["x-request-id"]),
69
135
  };
70
136
  logger.error(
71
137
  `请求错误: ${request.method} ${request.url} error:${error.message}`,
@@ -82,8 +148,20 @@ app.setErrorHandler((error, request, reply) => {
82
148
  });
83
149
  });
84
150
 
85
- // 将日志记录器添加到应用实例中,以便在路由中使用
86
- app.decorate("logger", logger);
151
+ // Logger lattice will be decorated in start() function
152
+
153
+ /**
154
+ * Logger configuration for gateway
155
+ */
156
+ export interface GatewayLoggerConfig {
157
+ name?: string;
158
+ description?: string;
159
+ type?: LoggerType;
160
+ serviceName?: string;
161
+ loggerName?: string;
162
+ file?: string | PinoFileOptions;
163
+ context?: Record<string, any>;
164
+ }
87
165
 
88
166
  // Gateway configuration interface
89
167
  export interface LatticeGatewayConfig {
@@ -92,11 +170,28 @@ export interface LatticeGatewayConfig {
92
170
  type: QueueServiceType;
93
171
  defaultStartPollingQueue: boolean;
94
172
  };
173
+ loggerConfig?: Partial<GatewayLoggerConfig>; // Optional logger configuration to override defaults
95
174
  }
96
175
 
97
176
  // Start server
98
177
  const start = async (config?: LatticeGatewayConfig) => {
99
178
  try {
179
+ // Initialize or update logger configuration if provided
180
+ if (config?.loggerConfig) {
181
+ const loggerConfig: LoggerConfig = {
182
+ ...DEFAULT_LOGGER_CONFIG,
183
+ ...config.loggerConfig,
184
+ // Merge file config if provided
185
+ file: config.loggerConfig.file || DEFAULT_LOGGER_CONFIG.file,
186
+ };
187
+ loggerLattice = initializeLogger(loggerConfig);
188
+ logger = loggerLattice.client;
189
+ }
190
+
191
+ // Decorate app with logger lattice (only once, in start function)
192
+ // Access via: request.server.loggerLattice or app.loggerLattice
193
+ app.decorate("loggerLattice", loggerLattice);
194
+
100
195
  const target_port = config?.port || Number(process.env.PORT) || 4001;
101
196
 
102
197
  await app.listen({ port: target_port, host: "0.0.0.0" });