@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.
- package/dist/index.js +2501 -3301
- package/dist/index.js.map +1 -1
- package/package.json +2 -13
- package/src/config/config-loader.ts +5 -6
- package/src/config/config-reader.service.ts +6 -11
- package/src/config/config-reader.ts +75 -0
- package/src/config/index.ts +4 -1
- package/src/config/load-env.ts +15 -0
- package/src/config/schema-generator.service.ts +0 -2
- package/src/config/spaceflow.config.ts +7 -20
- package/src/extension-system/define-extension.ts +25 -0
- package/src/extension-system/extension.interface.ts +0 -63
- package/src/extension-system/index.ts +5 -0
- package/src/extension-system/types.ts +201 -0
- package/src/index.ts +15 -18
- package/src/shared/claude-setup/claude-setup.service.ts +3 -8
- package/src/shared/claude-setup/index.ts +0 -1
- package/src/shared/feishu-sdk/feishu-sdk.service.ts +33 -21
- package/src/shared/feishu-sdk/fieshu-card.service.ts +9 -11
- package/src/shared/feishu-sdk/index.ts +0 -1
- package/src/shared/git-provider/git-provider.service.ts +1 -6
- package/src/shared/git-provider/index.ts +0 -1
- package/src/shared/git-sdk/git-sdk.service.ts +0 -2
- package/src/shared/git-sdk/index.ts +0 -1
- package/src/shared/llm-proxy/adapters/claude-code.adapter.spec.ts +15 -32
- package/src/shared/llm-proxy/adapters/claude-code.adapter.ts +5 -6
- package/src/shared/llm-proxy/adapters/open-code.adapter.ts +1 -3
- package/src/shared/llm-proxy/adapters/openai.adapter.spec.ts +7 -21
- package/src/shared/llm-proxy/adapters/openai.adapter.ts +1 -3
- package/src/shared/llm-proxy/index.ts +0 -1
- package/src/shared/llm-proxy/interfaces/config.interface.ts +8 -0
- package/src/shared/llm-proxy/llm-proxy.service.spec.ts +91 -68
- package/src/shared/llm-proxy/llm-proxy.service.ts +5 -8
- package/src/shared/mcp/index.ts +1 -33
- package/src/shared/output/index.ts +0 -1
- package/src/shared/output/output.service.ts +43 -13
- package/src/shared/rspack-config/rspack-config.ts +0 -6
- package/src/shared/storage/index.ts +0 -1
- package/src/shared/storage/storage.service.ts +16 -8
- package/src/shared/storage/types.ts +0 -44
- package/src/shared/verbose/index.ts +15 -0
- package/src/app.module.ts +0 -18
- package/src/config/config-reader.module.ts +0 -16
- package/src/shared/claude-setup/claude-setup.module.ts +0 -8
- package/src/shared/feishu-sdk/feishu-sdk.module.ts +0 -77
- package/src/shared/git-provider/git-provider.module.ts +0 -73
- package/src/shared/git-sdk/git-sdk.module.ts +0 -8
- package/src/shared/llm-proxy/llm-proxy.module.ts +0 -140
- package/src/shared/output/output.module.ts +0 -9
- 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(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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(
|
|
104
|
-
expect(
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
284
|
-
openaiAdapter
|
|
285
|
-
opencodeAdapter
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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);
|
package/src/shared/mcp/index.ts
CHANGED
|
@@ -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
|
-
//
|
|
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,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
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
-
|
|
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,6 +1,15 @@
|
|
|
1
|
-
import { Inject, Injectable, OnModuleDestroy } from "@nestjs/common";
|
|
2
1
|
import { type StorageAdapter } from "./adapters/storage-adapter.interface";
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
21
|
-
protected readonly options: StorageModuleOptions,
|
|
27
|
+
protected readonly options: StorageServiceOptions = {},
|
|
22
28
|
) {
|
|
23
29
|
// 启动定期清理过期项的定时器
|
|
24
30
|
this.startCleanupTimer();
|
|
25
31
|
}
|
|
26
32
|
|
|
27
|
-
|
|
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 {}
|