@spaceflow/core 0.1.3 → 0.2.0

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.
Files changed (50) hide show
  1. package/dist/index.js +2501 -3301
  2. package/dist/index.js.map +1 -1
  3. package/package.json +2 -13
  4. package/src/config/config-loader.ts +5 -6
  5. package/src/config/config-reader.service.ts +6 -11
  6. package/src/config/config-reader.ts +75 -0
  7. package/src/config/index.ts +4 -1
  8. package/src/config/load-env.ts +15 -0
  9. package/src/config/schema-generator.service.ts +0 -2
  10. package/src/config/spaceflow.config.ts +7 -20
  11. package/src/extension-system/define-extension.ts +25 -0
  12. package/src/extension-system/extension.interface.ts +0 -63
  13. package/src/extension-system/index.ts +5 -0
  14. package/src/extension-system/types.ts +201 -0
  15. package/src/index.ts +15 -18
  16. package/src/shared/claude-setup/claude-setup.service.ts +3 -8
  17. package/src/shared/claude-setup/index.ts +0 -1
  18. package/src/shared/feishu-sdk/feishu-sdk.service.ts +33 -21
  19. package/src/shared/feishu-sdk/fieshu-card.service.ts +9 -11
  20. package/src/shared/feishu-sdk/index.ts +0 -1
  21. package/src/shared/git-provider/git-provider.service.ts +1 -6
  22. package/src/shared/git-provider/index.ts +0 -1
  23. package/src/shared/git-sdk/git-sdk.service.ts +0 -2
  24. package/src/shared/git-sdk/index.ts +0 -1
  25. package/src/shared/llm-proxy/adapters/claude-code.adapter.spec.ts +15 -32
  26. package/src/shared/llm-proxy/adapters/claude-code.adapter.ts +5 -6
  27. package/src/shared/llm-proxy/adapters/open-code.adapter.ts +1 -3
  28. package/src/shared/llm-proxy/adapters/openai.adapter.spec.ts +7 -21
  29. package/src/shared/llm-proxy/adapters/openai.adapter.ts +1 -3
  30. package/src/shared/llm-proxy/index.ts +0 -1
  31. package/src/shared/llm-proxy/interfaces/config.interface.ts +8 -0
  32. package/src/shared/llm-proxy/llm-proxy.service.spec.ts +91 -68
  33. package/src/shared/llm-proxy/llm-proxy.service.ts +5 -8
  34. package/src/shared/mcp/index.ts +1 -33
  35. package/src/shared/output/index.ts +0 -1
  36. package/src/shared/output/output.service.ts +43 -13
  37. package/src/shared/rspack-config/rspack-config.ts +0 -6
  38. package/src/shared/storage/index.ts +0 -1
  39. package/src/shared/storage/storage.service.ts +16 -8
  40. package/src/shared/storage/types.ts +0 -44
  41. package/src/shared/verbose/index.ts +15 -0
  42. package/src/app.module.ts +0 -18
  43. package/src/config/config-reader.module.ts +0 -16
  44. package/src/shared/claude-setup/claude-setup.module.ts +0 -8
  45. package/src/shared/feishu-sdk/feishu-sdk.module.ts +0 -77
  46. package/src/shared/git-provider/git-provider.module.ts +0 -73
  47. package/src/shared/git-sdk/git-sdk.module.ts +0 -8
  48. package/src/shared/llm-proxy/llm-proxy.module.ts +0 -140
  49. package/src/shared/output/output.module.ts +0 -9
  50. package/src/shared/storage/storage.module.ts +0 -150
@@ -1,13 +1,30 @@
1
1
  import { vi, type Mocked } from "vitest";
2
- import { Test, TestingModule } from "@nestjs/testing";
3
2
  import { LlmProxyService, ChatOptions } from "./llm-proxy.service";
4
- import { ClaudeCodeAdapter } from "./adapters/claude-code.adapter";
5
- import { OpenAIAdapter } from "./adapters/openai.adapter";
6
- import { OpenCodeAdapter } from "./adapters/open-code.adapter";
3
+ import type { ClaudeCodeAdapter } from "./adapters/claude-code.adapter";
4
+ import type { OpenAIAdapter } from "./adapters/openai.adapter";
5
+ import type { OpenCodeAdapter } from "./adapters/open-code.adapter";
7
6
 
8
7
  vi.mock("@anthropic-ai/claude-agent-sdk", () => ({
9
8
  query: vi.fn(),
10
9
  }));
10
+ vi.mock("../../claude-setup", () => ({
11
+ ClaudeSetupService: class MockClaudeSetupService {
12
+ configure = vi.fn().mockResolvedValue(undefined);
13
+ },
14
+ }));
15
+
16
+ vi.mock("./adapters/claude-code.adapter", async (importOriginal) => {
17
+ const actual = await importOriginal<typeof import("./adapters/claude-code.adapter")>();
18
+ return { ...actual };
19
+ });
20
+ vi.mock("./adapters/openai.adapter", async (importOriginal) => {
21
+ const actual = await importOriginal<typeof import("./adapters/openai.adapter")>();
22
+ return { ...actual };
23
+ });
24
+ vi.mock("./adapters/open-code.adapter", async (importOriginal) => {
25
+ const actual = await importOriginal<typeof import("./adapters/open-code.adapter")>();
26
+ return { ...actual };
27
+ });
11
28
 
12
29
  describe("LlmProxyService", () => {
13
30
  let service: LlmProxyService;
@@ -17,45 +34,18 @@ describe("LlmProxyService", () => {
17
34
 
18
35
  const mockConfig = {
19
36
  defaultAdapter: "claude-code" as const,
37
+ claudeCode: { model: "claude-3-5-sonnet" },
38
+ openai: { apiKey: "test-key", model: "gpt-4o" },
39
+ openCode: { apiKey: "test-key" },
20
40
  };
21
41
 
22
- beforeEach(async () => {
23
- const mockClaude = {
24
- name: "claude-code",
25
- chat: vi.fn(),
26
- chatStream: vi.fn(),
27
- isConfigured: vi.fn().mockReturnValue(true),
28
- isSupportJsonSchema: vi.fn().mockReturnValue(true),
29
- };
30
- const mockOpenAI = {
31
- name: "openai",
32
- chat: vi.fn(),
33
- chatStream: vi.fn(),
34
- isConfigured: vi.fn().mockReturnValue(true),
35
- isSupportJsonSchema: vi.fn().mockReturnValue(true),
36
- };
37
- const mockOpenCode = {
38
- name: "open-code",
39
- chat: vi.fn(),
40
- chatStream: vi.fn(),
41
- isConfigured: vi.fn().mockReturnValue(true),
42
- isSupportJsonSchema: vi.fn().mockReturnValue(true),
43
- };
44
-
45
- const module: TestingModule = await Test.createTestingModule({
46
- providers: [
47
- LlmProxyService,
48
- { provide: "LLM_PROXY_CONFIG", useValue: mockConfig },
49
- { provide: ClaudeCodeAdapter, useValue: mockClaude },
50
- { provide: OpenAIAdapter, useValue: mockOpenAI },
51
- { provide: OpenCodeAdapter, useValue: mockOpenCode },
52
- ],
53
- }).compile();
54
-
55
- service = module.get<LlmProxyService>(LlmProxyService);
56
- claudeAdapter = module.get(ClaudeCodeAdapter);
57
- openaiAdapter = module.get(OpenAIAdapter);
58
- opencodeAdapter = module.get(OpenCodeAdapter);
42
+ beforeEach(() => {
43
+ vi.clearAllMocks();
44
+ service = new LlmProxyService(mockConfig as any);
45
+ // 获取内部适配器的引用(通过 getAdapter)
46
+ claudeAdapter = (service as any).adapters.get("claude-code");
47
+ openaiAdapter = (service as any).adapters.get("openai");
48
+ opencodeAdapter = (service as any).adapters.get("open-code");
59
49
  });
60
50
 
61
51
  it("should be defined", () => {
@@ -76,8 +66,9 @@ describe("LlmProxyService", () => {
76
66
  });
77
67
 
78
68
  it("should throw error if adapter is not configured", () => {
79
- claudeAdapter.isConfigured.mockReturnValue(false);
69
+ const spy = vi.spyOn(claudeAdapter, "isConfigured").mockReturnValue(false);
80
70
  expect(() => service.createSession("claude-code")).toThrow('适配器 "claude-code" 未配置');
71
+ spy.mockRestore();
81
72
  });
82
73
  });
83
74
 
@@ -85,23 +76,29 @@ describe("LlmProxyService", () => {
85
76
  it("should call adapter.chat and return response", async () => {
86
77
  const messages = [{ role: "user", content: "hello" }] as any;
87
78
  const mockResponse = { content: "hi", role: "assistant" };
88
- claudeAdapter.chat.mockResolvedValue(mockResponse as any);
79
+ const chatSpy = vi.spyOn(claudeAdapter, "chat").mockResolvedValue(mockResponse as any);
89
80
 
90
81
  const result = await service.chat(messages);
91
82
 
92
- expect(claudeAdapter.chat).toHaveBeenCalledWith(messages, undefined);
83
+ expect(chatSpy).toHaveBeenCalledWith(messages, undefined);
93
84
  expect(result).toEqual(mockResponse);
85
+ chatSpy.mockRestore();
94
86
  });
95
87
 
96
88
  it("should use specified adapter", async () => {
97
89
  const messages = [{ role: "user", content: "hello" }] as any;
98
90
  const options: ChatOptions = { adapter: "openai" };
99
- openaiAdapter.chat.mockResolvedValue({ content: "hi" } as any);
91
+ const openaiChatSpy = vi
92
+ .spyOn(openaiAdapter, "chat")
93
+ .mockResolvedValue({ content: "hi" } as any);
94
+ const claudeChatSpy = vi.spyOn(claudeAdapter, "chat");
100
95
 
101
96
  await service.chat(messages, options);
102
97
 
103
- expect(openaiAdapter.chat).toHaveBeenCalled();
104
- expect(claudeAdapter.chat).not.toHaveBeenCalled();
98
+ expect(openaiChatSpy).toHaveBeenCalled();
99
+ expect(claudeChatSpy).not.toHaveBeenCalled();
100
+ openaiChatSpy.mockRestore();
101
+ claudeChatSpy.mockRestore();
105
102
  });
106
103
 
107
104
  it("should handle jsonSchema and parse output", async () => {
@@ -113,12 +110,13 @@ describe("LlmProxyService", () => {
113
110
  };
114
111
  const options = { jsonSchema: mockJsonSchema };
115
112
  const mockResponse = { content: '{"foo":"bar"}', role: "assistant" };
116
- claudeAdapter.chat.mockResolvedValue(mockResponse as any);
113
+ const chatSpy = vi.spyOn(claudeAdapter, "chat").mockResolvedValue(mockResponse as any);
117
114
 
118
115
  const result = await service.chat(messages, options as any);
119
116
 
120
117
  expect(result.structuredOutput).toEqual(mockParsed);
121
118
  expect(mockJsonSchema.parse).toHaveBeenCalledWith(mockResponse.content);
119
+ chatSpy.mockRestore();
122
120
  });
123
121
  });
124
122
 
@@ -128,7 +126,9 @@ describe("LlmProxyService", () => {
128
126
  const mockStream = (async function* () {
129
127
  yield { type: "text", content: "hi" };
130
128
  })();
131
- claudeAdapter.chatStream.mockReturnValue(mockStream as any);
129
+ const chatStreamSpy = vi
130
+ .spyOn(claudeAdapter, "chatStream")
131
+ .mockReturnValue(mockStream as any);
132
132
 
133
133
  const stream = service.chatStream(messages);
134
134
  const chunks: any[] = [];
@@ -137,13 +137,17 @@ describe("LlmProxyService", () => {
137
137
  }
138
138
 
139
139
  expect(chunks).toEqual([{ type: "text", content: "hi" }]);
140
- expect(claudeAdapter.chatStream).toHaveBeenCalledWith(messages, undefined);
140
+ expect(chatStreamSpy).toHaveBeenCalledWith(messages, undefined);
141
+ chatStreamSpy.mockRestore();
141
142
  });
142
143
  });
143
144
 
144
145
  describe("chat with jsonSchema fallback", () => {
145
146
  it("should append jsonSchema prompt when adapter does not support jsonSchema", async () => {
146
- claudeAdapter.isSupportJsonSchema.mockReturnValue(false);
147
+ const jsonSchemaSpy = vi.spyOn(claudeAdapter, "isSupportJsonSchema").mockReturnValue(false);
148
+ const chatSpy = vi
149
+ .spyOn(claudeAdapter, "chat")
150
+ .mockResolvedValue({ content: '{"foo":"bar"}' } as any);
147
151
  const mockJsonSchema = {
148
152
  parse: vi.fn().mockResolvedValue({ foo: "bar" }),
149
153
  getSchema: vi.fn(),
@@ -154,14 +158,16 @@ describe("LlmProxyService", () => {
154
158
  { role: "system", content: "你是助手" },
155
159
  { role: "user", content: "hello" },
156
160
  ] as any;
157
- claudeAdapter.chat.mockResolvedValue({ content: '{"foo":"bar"}' } as any);
158
161
  const result = await service.chat(messages, { jsonSchema: mockJsonSchema } as any);
159
162
  expect(messages[0].content).toContain("请返回 JSON");
160
163
  expect(result.structuredOutput).toEqual({ foo: "bar" });
164
+ jsonSchemaSpy.mockRestore();
165
+ chatSpy.mockRestore();
161
166
  });
162
167
 
163
168
  it("should not append jsonSchema prompt if already matched", async () => {
164
- claudeAdapter.isSupportJsonSchema.mockReturnValue(false);
169
+ const jsonSchemaSpy = vi.spyOn(claudeAdapter, "isSupportJsonSchema").mockReturnValue(false);
170
+ const chatSpy = vi.spyOn(claudeAdapter, "chat").mockResolvedValue({ content: "{}" } as any);
165
171
  const mockJsonSchema = {
166
172
  parse: vi.fn().mockResolvedValue({}),
167
173
  getSchema: vi.fn(),
@@ -169,13 +175,15 @@ describe("LlmProxyService", () => {
169
175
  isMatched: vi.fn().mockReturnValue(true),
170
176
  };
171
177
  const messages = [{ role: "system", content: "已包含 JSON 指令" }] as any;
172
- claudeAdapter.chat.mockResolvedValue({ content: "{}" } as any);
173
178
  await service.chat(messages, { jsonSchema: mockJsonSchema } as any);
174
179
  expect(messages[0].content).toBe("已包含 JSON 指令");
180
+ jsonSchemaSpy.mockRestore();
181
+ chatSpy.mockRestore();
175
182
  });
176
183
 
177
184
  it("should add system message if none exists", async () => {
178
- claudeAdapter.isSupportJsonSchema.mockReturnValue(false);
185
+ const jsonSchemaSpy = vi.spyOn(claudeAdapter, "isSupportJsonSchema").mockReturnValue(false);
186
+ const chatSpy = vi.spyOn(claudeAdapter, "chat").mockResolvedValue({ content: "{}" } as any);
179
187
  const mockJsonSchema = {
180
188
  parse: vi.fn().mockResolvedValue({}),
181
189
  getSchema: vi.fn(),
@@ -183,41 +191,44 @@ describe("LlmProxyService", () => {
183
191
  isMatched: vi.fn().mockReturnValue(false),
184
192
  };
185
193
  const messages = [{ role: "user", content: "hello" }] as any;
186
- claudeAdapter.chat.mockResolvedValue({ content: "{}" } as any);
187
194
  await service.chat(messages, { jsonSchema: mockJsonSchema } as any);
188
195
  expect(messages[0].role).toBe("system");
189
196
  expect(messages[0].content).toBe("请返回 JSON");
197
+ jsonSchemaSpy.mockRestore();
198
+ chatSpy.mockRestore();
190
199
  });
191
200
 
192
201
  it("should not parse if response has no content", async () => {
202
+ const chatSpy = vi.spyOn(claudeAdapter, "chat").mockResolvedValue({ content: "" } as any);
193
203
  const mockJsonSchema = {
194
204
  parse: vi.fn(),
195
205
  getSchema: vi.fn(),
196
206
  };
197
- claudeAdapter.chat.mockResolvedValue({ content: "" } as any);
198
207
  const result = await service.chat(
199
208
  [{ role: "user", content: "hello" }] as any,
200
209
  { jsonSchema: mockJsonSchema } as any,
201
210
  );
202
211
  expect(mockJsonSchema.parse).not.toHaveBeenCalled();
203
212
  expect(result.structuredOutput).toBeUndefined();
213
+ chatSpy.mockRestore();
204
214
  });
205
215
 
206
216
  it("should not parse if structuredOutput already exists", async () => {
217
+ const chatSpy = vi.spyOn(claudeAdapter, "chat").mockResolvedValue({
218
+ content: "{}",
219
+ structuredOutput: { existing: true },
220
+ } as any);
207
221
  const mockJsonSchema = {
208
222
  parse: vi.fn(),
209
223
  getSchema: vi.fn(),
210
224
  };
211
- claudeAdapter.chat.mockResolvedValue({
212
- content: "{}",
213
- structuredOutput: { existing: true },
214
- } as any);
215
225
  const result = await service.chat(
216
226
  [{ role: "user", content: "hello" }] as any,
217
227
  { jsonSchema: mockJsonSchema } as any,
218
228
  );
219
229
  expect(mockJsonSchema.parse).not.toHaveBeenCalled();
220
230
  expect(result.structuredOutput).toEqual({ existing: true });
231
+ chatSpy.mockRestore();
221
232
  });
222
233
  });
223
234
 
@@ -230,7 +241,9 @@ describe("LlmProxyService", () => {
230
241
  const mockStream = (async function* () {
231
242
  yield { type: "result", response: { content: '{"parsed":true}' } };
232
243
  })();
233
- claudeAdapter.chatStream.mockReturnValue(mockStream as any);
244
+ const chatStreamSpy = vi
245
+ .spyOn(claudeAdapter, "chatStream")
246
+ .mockReturnValue(mockStream as any);
234
247
  const chunks: any[] = [];
235
248
  for await (const chunk of service.chatStream(
236
249
  [{ role: "user", content: "hello" }] as any,
@@ -239,6 +252,7 @@ describe("LlmProxyService", () => {
239
252
  chunks.push(chunk);
240
253
  }
241
254
  expect(chunks[0].response.structuredOutput).toEqual({ parsed: true });
255
+ chatStreamSpy.mockRestore();
242
256
  });
243
257
 
244
258
  it("should handle jsonSchema parse error gracefully", async () => {
@@ -249,7 +263,9 @@ describe("LlmProxyService", () => {
249
263
  const mockStream = (async function* () {
250
264
  yield { type: "result", response: { content: "invalid json" } };
251
265
  })();
252
- claudeAdapter.chatStream.mockReturnValue(mockStream as any);
266
+ const chatStreamSpy = vi
267
+ .spyOn(claudeAdapter, "chatStream")
268
+ .mockReturnValue(mockStream as any);
253
269
  const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
254
270
  const chunks: any[] = [];
255
271
  for await (const chunk of service.chatStream(
@@ -261,32 +277,39 @@ describe("LlmProxyService", () => {
261
277
  expect(consoleSpy).toHaveBeenCalled();
262
278
  expect(chunks[0].response.structuredOutput).toBeUndefined();
263
279
  consoleSpy.mockRestore();
280
+ chatStreamSpy.mockRestore();
264
281
  });
265
282
 
266
283
  it("should use specified adapter for chatStream", async () => {
267
284
  const mockStream = (async function* () {
268
285
  yield { type: "text", content: "hi" };
269
286
  })();
270
- openaiAdapter.chatStream.mockReturnValue(mockStream as any);
287
+ const chatStreamSpy = vi
288
+ .spyOn(openaiAdapter, "chatStream")
289
+ .mockReturnValue(mockStream as any);
271
290
  const chunks: any[] = [];
272
291
  for await (const chunk of service.chatStream([{ role: "user", content: "hello" }] as any, {
273
292
  adapter: "openai",
274
293
  })) {
275
294
  chunks.push(chunk);
276
295
  }
277
- expect(openaiAdapter.chatStream).toHaveBeenCalled();
296
+ expect(chatStreamSpy).toHaveBeenCalled();
297
+ chatStreamSpy.mockRestore();
278
298
  });
279
299
  });
280
300
 
281
301
  describe("getAvailableAdapters", () => {
282
302
  it("should return list of configured adapters", () => {
283
- claudeAdapter.isConfigured.mockReturnValue(true);
284
- openaiAdapter.isConfigured.mockReturnValue(false);
285
- opencodeAdapter.isConfigured.mockReturnValue(false);
303
+ const claudeSpy = vi.spyOn(claudeAdapter, "isConfigured").mockReturnValue(true);
304
+ const openaiSpy = vi.spyOn(openaiAdapter, "isConfigured").mockReturnValue(false);
305
+ const opencodeSpy = vi.spyOn(opencodeAdapter, "isConfigured").mockReturnValue(false);
286
306
 
287
307
  const available = service.getAvailableAdapters();
288
308
 
289
309
  expect(available).toEqual(["claude-code"]);
310
+ claudeSpy.mockRestore();
311
+ openaiSpy.mockRestore();
312
+ opencodeSpy.mockRestore();
290
313
  });
291
314
 
292
315
  it("should return all adapters when all configured", () => {
@@ -1,4 +1,3 @@
1
- import { Injectable, Inject } from "@nestjs/common";
2
1
  import type { LlmAdapter } from "./adapters";
3
2
  import { ClaudeCodeAdapter } from "./adapters/claude-code.adapter";
4
3
  import { OpenAIAdapter } from "./adapters/openai.adapter";
@@ -19,16 +18,14 @@ export interface ChatOptions extends LlmRequestOptions {
19
18
  adapter?: LLMMode;
20
19
  }
21
20
 
22
- @Injectable()
23
21
  export class LlmProxyService {
24
22
  private adapters: Map<LLMMode, LlmAdapter> = new Map();
25
23
 
26
- constructor(
27
- @Inject("LLM_PROXY_CONFIG") private readonly config: LlmProxyConfig,
28
- private readonly claudeCodeAdapter: ClaudeCodeAdapter,
29
- private readonly openaiAdapter: OpenAIAdapter,
30
- private readonly openCodeAdapter: OpenCodeAdapter,
31
- ) {
24
+ constructor(private readonly config: LlmProxyConfig) {
25
+ // 适配器接收完整配置,内部自行读取所需部分
26
+ const claudeCodeAdapter = new ClaudeCodeAdapter(config);
27
+ const openaiAdapter = new OpenAIAdapter(config);
28
+ const openCodeAdapter = new OpenCodeAdapter(config);
32
29
  this.adapters.set("claude-code", claudeCodeAdapter);
33
30
  this.adapters.set("openai", openaiAdapter);
34
31
  this.adapters.set("open-code", openCodeAdapter);
@@ -27,7 +27,6 @@
27
27
  */
28
28
 
29
29
  import "reflect-metadata";
30
- import { Injectable } from "@nestjs/common";
31
30
 
32
31
  /** MCP 服务元数据 key(使用 Symbol 确保唯一性) */
33
32
  export const MCP_SERVER_METADATA = Symbol.for("spaceflow:mcp:server");
@@ -90,36 +89,7 @@ export function dtoToJsonSchema(dtoClass: new (...args: any[]) => any): JsonSche
90
89
  if (typeof schemaType === "function") {
91
90
  schemaType = typeMap[schemaType.name] || "string";
92
91
  }
93
- // 如果 swagger 没有显式 type,从 class-validator 元数据推断
94
- if (!schemaType) {
95
- try {
96
- const { getMetadataStorage } = require("class-validator");
97
- const validationMetas = getMetadataStorage().getTargetValidationMetadatas(
98
- dtoClass,
99
- "",
100
- false,
101
- false,
102
- );
103
- const validatorTypeMap: Record<string, string> = {
104
- isString: "string",
105
- isNumber: "number",
106
- isBoolean: "boolean",
107
- isArray: "array",
108
- isObject: "object",
109
- isInt: "number",
110
- isEnum: "string",
111
- };
112
- const propMeta = validationMetas.find(
113
- (m: any) => m.propertyName === key && validatorTypeMap[m.name],
114
- );
115
- if (propMeta) {
116
- schemaType = validatorTypeMap[propMeta.name];
117
- }
118
- } catch {
119
- // class-validator 不可用时忽略
120
- }
121
- }
122
- // 最后从 reflect-metadata 的 design:type 推断
92
+ // reflect-metadata design:type 推断
123
93
  if (!schemaType) {
124
94
  const reflectedType = Reflect.getMetadata("design:type", prototype, key);
125
95
  if (reflectedType) {
@@ -180,8 +150,6 @@ export interface McpToolMetadata extends McpToolDefinition {
180
150
  */
181
151
  export function McpServer(definition: McpServerDefinition): ClassDecorator {
182
152
  return (target: Function) => {
183
- // 应用 @Injectable() 装饰器
184
- Injectable()(target);
185
153
  // 使用静态属性存储元数据(跨模块可访问)
186
154
  (target as any).__mcp_server__ = definition;
187
155
  };
@@ -1,2 +1 @@
1
1
  export * from "./output.service";
2
- export * from "./output.module";
@@ -1,5 +1,5 @@
1
- import { Injectable, Scope } from "@nestjs/common";
2
1
  import { randomUUID } from "crypto";
2
+ import type { IOutputService } from "../../extension-system/types";
3
3
 
4
4
  const OUTPUT_MARKER_START = "::spaceflow-output::";
5
5
  const OUTPUT_MARKER_END = "::end::";
@@ -16,20 +16,13 @@ const OUTPUT_MARKER_END = "::end::";
16
16
  *
17
17
  * 使用示例:
18
18
  * ```typescript
19
- * @Injectable()
20
- * export class MyService {
21
- * constructor(protected readonly output: OutputService) {}
22
- *
23
- * async execute() {
24
- * // ... 执行逻辑
25
- * this.output.set("version", "1.0.0");
26
- * this.output.set("tag", "v1.0.0");
27
- * }
28
- * }
19
+ * const output = new OutputService();
20
+ * output.set("version", "1.0.0");
21
+ * output.set("tag", "v1.0.0");
22
+ * output.flush();
29
23
  * ```
30
24
  */
31
- @Injectable({ scope: Scope.DEFAULT })
32
- export class OutputService {
25
+ export class OutputService implements IOutputService {
33
26
  protected outputs: Record<string, string> = {};
34
27
  protected cacheId: string = randomUUID();
35
28
 
@@ -92,6 +85,43 @@ export class OutputService {
92
85
  getCacheId(): string {
93
86
  return this.cacheId;
94
87
  }
88
+
89
+ /**
90
+ * 输出信息
91
+ */
92
+ info(message: string): void {
93
+ console.log(message);
94
+ }
95
+
96
+ /**
97
+ * 输出成功信息
98
+ */
99
+ success(message: string): void {
100
+ console.log(`✅ ${message}`);
101
+ }
102
+
103
+ /**
104
+ * 输出警告
105
+ */
106
+ warn(message: string): void {
107
+ console.warn(`⚠️ ${message}`);
108
+ }
109
+
110
+ /**
111
+ * 输出错误
112
+ */
113
+ error(message: string): void {
114
+ console.error(`❌ ${message}`);
115
+ }
116
+
117
+ /**
118
+ * 输出调试信息
119
+ */
120
+ debug(message: string): void {
121
+ if (process.env.DEBUG) {
122
+ console.debug(`🔍 ${message}`);
123
+ }
124
+ }
95
125
  }
96
126
 
97
127
  export { OUTPUT_MARKER_START, OUTPUT_MARKER_END };
@@ -37,12 +37,6 @@ export interface CorePathOptions {
37
37
  export const DEFAULT_EXTERNALS: Configuration["externals"] = [
38
38
  // Spaceflow 核心 - 运行时从 core 加载
39
39
  { "@spaceflow/core": "module @spaceflow/core" },
40
- // NestJS 相关 - 这些由 core 提供
41
- { "@nestjs/common": "module @nestjs/common" },
42
- { "@nestjs/config": "module @nestjs/config" },
43
- { "@nestjs/core": "module @nestjs/core" },
44
- { "nest-commander": "module nest-commander" },
45
- { "reflect-metadata": "module reflect-metadata" },
46
40
  // 排除所有 node_modules(除了相对路径和 src/ 别名)
47
41
  /^(?!\.\/|\.\.\/|src\/)[^./]/,
48
42
  ];
@@ -1,4 +1,3 @@
1
- export * from "./storage.module";
2
1
  export * from "./storage.service";
3
2
  export * from "./types";
4
3
  export * from "./adapters";
@@ -1,6 +1,15 @@
1
- import { Inject, Injectable, OnModuleDestroy } from "@nestjs/common";
2
1
  import { type StorageAdapter } from "./adapters/storage-adapter.interface";
3
- import { STORAGE_ADAPTER, STORAGE_MODULE_OPTIONS, type StorageModuleOptions } from "./types";
2
+ import type { IStorageService } from "../../extension-system/types";
3
+
4
+ /**
5
+ * Storage 服务配置选项
6
+ */
7
+ export interface StorageServiceOptions {
8
+ /** 默认过期时间(毫秒),0 表示永不过期 */
9
+ defaultTtl?: number;
10
+ /** 最大 key 数量,超过时会淘汰最早过期的 key,0 或 undefined 表示不限制 */
11
+ maxKeys?: number;
12
+ }
4
13
 
5
14
  /**
6
15
  * Storage 服务
@@ -10,21 +19,18 @@ import { STORAGE_ADAPTER, STORAGE_MODULE_OPTIONS, type StorageModuleOptions } fr
10
19
  * - user:123:profile
11
20
  * - cache:api:users
12
21
  */
13
- @Injectable()
14
- export class StorageService implements OnModuleDestroy {
22
+ export class StorageService implements IStorageService {
15
23
  protected cleanupTimer: ReturnType<typeof setInterval> | null = null;
16
24
 
17
25
  constructor(
18
- @Inject(STORAGE_ADAPTER)
19
26
  protected readonly adapter: StorageAdapter,
20
- @Inject(STORAGE_MODULE_OPTIONS)
21
- protected readonly options: StorageModuleOptions,
27
+ protected readonly options: StorageServiceOptions = {},
22
28
  ) {
23
29
  // 启动定期清理过期项的定时器
24
30
  this.startCleanupTimer();
25
31
  }
26
32
 
27
- onModuleDestroy() {
33
+ destroy(): void {
28
34
  if (this.cleanupTimer) {
29
35
  clearInterval(this.cleanupTimer);
30
36
  this.cleanupTimer = null;
@@ -39,6 +45,8 @@ export class StorageService implements OnModuleDestroy {
39
45
  this.cleanupTimer = setInterval(() => {
40
46
  this.cleanup().catch(console.error);
41
47
  }, 60 * 1000);
48
+ // 使用 unref() 让定时器不阻止进程退出
49
+ this.cleanupTimer.unref();
42
50
  }
43
51
 
44
52
  /**
@@ -1,37 +1,3 @@
1
- /**
2
- * Storage 模块配置选项
3
- */
4
- export interface StorageModuleOptions {
5
- /**
6
- * 适配器类型
7
- */
8
- adapter: "memory" | "file";
9
-
10
- /**
11
- * 文件适配器的存储路径(仅 file 适配器需要)
12
- */
13
- filePath?: string;
14
-
15
- /**
16
- * 默认过期时间(毫秒),0 表示永不过期
17
- */
18
- defaultTtl?: number;
19
-
20
- /**
21
- * 最大 key 数量,超过时会淘汰最早过期的 key
22
- * 0 或 undefined 表示不限制
23
- */
24
- maxKeys?: number;
25
- }
26
-
27
- /**
28
- * 异步模块配置选项
29
- */
30
- export interface StorageModuleAsyncOptions {
31
- useFactory: (...args: any[]) => Promise<StorageModuleOptions> | StorageModuleOptions;
32
- inject?: any[];
33
- }
34
-
35
1
  /**
36
2
  * 存储项元数据
37
3
  */
@@ -39,13 +5,3 @@ export interface StorageItem<T = any> {
39
5
  value: T;
40
6
  expireAt?: number; // 过期时间戳,undefined 表示永不过期
41
7
  }
42
-
43
- /**
44
- * 模块配置注入 Token
45
- */
46
- export const STORAGE_MODULE_OPTIONS = Symbol("STORAGE_MODULE_OPTIONS");
47
-
48
- /**
49
- * 适配器注入 Token
50
- */
51
- export const STORAGE_ADAPTER = Symbol("STORAGE_ADAPTER");
@@ -71,3 +71,18 @@ export function shouldLog(
71
71
  ): boolean {
72
72
  return normalizeVerbose(verbose) >= requiredLevel;
73
73
  }
74
+
75
+ /**
76
+ * 解析命令行 verbose 参数
77
+ * 支持: -v, -v 2, -v 3, --verbose, --verbose 2, -vvv (计数模式)
78
+ * @param val 命令行参数值(字符串、布尔值、数字或 undefined)
79
+ * @returns 规范化后的 VerboseLevel
80
+ */
81
+ export function parseVerbose(val: string | boolean | number | undefined): 0 | 1 | 2 | 3 {
82
+ if (val === undefined || val === 0) return 1;
83
+ if (val === true || val === "") return 1;
84
+ if (typeof val === "number") return normalizeVerbose(val as VerboseLevel);
85
+ const level = parseInt(val as string, 10);
86
+ if (isNaN(level)) return 1;
87
+ return normalizeVerbose(level as VerboseLevel);
88
+ }
package/src/app.module.ts DELETED
@@ -1,18 +0,0 @@
1
- import { Module } from "@nestjs/common";
2
- import { StorageModule } from "./shared/storage/storage.module";
3
- import { OutputModule } from "./shared/output";
4
- import { ConfigModule } from "@nestjs/config";
5
- import { configLoaders, getEnvFilePaths } from "./config";
6
-
7
- @Module({
8
- imports: [
9
- ConfigModule.forRoot({
10
- isGlobal: true,
11
- load: configLoaders,
12
- envFilePath: getEnvFilePaths(),
13
- }),
14
- StorageModule.forFeature(),
15
- OutputModule,
16
- ],
17
- })
18
- export class AppModule {}