@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,9 +1,7 @@
1
- import { Inject, Injectable, type OnModuleDestroy, type OnModuleInit } from "@nestjs/common";
2
- import { EventEmitter2 } from "@nestjs/event-emitter";
1
+ import { EventEmitter } from "events";
3
2
  import * as lark from "@larksuiteoapi/node-sdk";
4
3
  import {
5
4
  FeishuModuleOptions,
6
- FEISHU_MODULE_OPTIONS,
7
5
  FeishuUser,
8
6
  GetUserParams,
9
7
  FEISHU_CARD_ACTION_TRIGGER,
@@ -15,12 +13,39 @@ import {
15
13
  /**
16
14
  * 飞书 API 服务
17
15
  */
18
- @Injectable()
19
- export class FeishuSdkService implements OnModuleInit, OnModuleDestroy {
16
+ export class FeishuSdkService {
20
17
  protected readonly client: lark.Client;
18
+ protected readonly eventEmitter: EventEmitter;
19
+ private eventDispatcher: lark.EventDispatcher | null = null;
21
20
 
22
- onModuleInit(): void {
23
- const eventDispatcher = new lark.EventDispatcher({
21
+ constructor(protected readonly options: FeishuModuleOptions) {
22
+ this.eventEmitter = new EventEmitter();
23
+ this.client = new lark.Client({
24
+ appId: options.appId,
25
+ appSecret: options.appSecret,
26
+ appType: options.appType === "store" ? lark.AppType.ISV : lark.AppType.SelfBuild,
27
+ domain: options.domain === "lark" ? lark.Domain.Lark : lark.Domain.Feishu,
28
+ });
29
+
30
+ this.initEventDispatcher();
31
+ }
32
+
33
+ /**
34
+ * 注册事件监听器
35
+ */
36
+ on(eventName: string, listener: (...args: any[]) => void): void {
37
+ this.eventEmitter.on(eventName, listener);
38
+ }
39
+
40
+ /**
41
+ * 移除事件监听器
42
+ */
43
+ off(eventName: string, listener: (...args: any[]) => void): void {
44
+ this.eventEmitter.off(eventName, listener);
45
+ }
46
+
47
+ private initEventDispatcher(): void {
48
+ this.eventDispatcher = new lark.EventDispatcher({
24
49
  encryptKey: "encrypt key",
25
50
  }).register<CardEvents>({
26
51
  [FEISHU_CARD_ACTION_TRIGGER]: async (data) => {
@@ -41,23 +66,10 @@ export class FeishuSdkService implements OnModuleInit, OnModuleDestroy {
41
66
  });
42
67
  }
43
68
 
44
- onModuleDestroy(): void {
69
+ destroy(): void {
45
70
  this.eventEmitter.removeAllListeners();
46
71
  }
47
72
 
48
- constructor(
49
- @Inject(FEISHU_MODULE_OPTIONS)
50
- protected readonly options: FeishuModuleOptions,
51
- protected readonly eventEmitter: EventEmitter2,
52
- ) {
53
- this.client = new lark.Client({
54
- appId: options.appId,
55
- appSecret: options.appSecret,
56
- appType: options.appType === "store" ? lark.AppType.ISV : lark.AppType.SelfBuild,
57
- domain: options.domain === "lark" ? lark.Domain.Lark : lark.Domain.Feishu,
58
- });
59
- }
60
-
61
73
  /**
62
74
  * 验证飞书配置
63
75
  */
@@ -1,4 +1,3 @@
1
- import { Injectable } from "@nestjs/common";
2
1
  import { FeishuSdkService } from "./feishu-sdk.service";
3
2
  import {
4
3
  SendCardParams,
@@ -8,25 +7,24 @@ import {
8
7
  CardContent,
9
8
  type CardActionTriggerCallback,
10
9
  } from "./types";
11
- import { OnEvent, type EventEmitter2 } from "@nestjs/event-emitter";
12
10
  import { FEISHU_CARD_ACTION_TRIGGER } from "./types";
13
11
 
14
12
  /**
15
13
  * 飞书卡片消息服务
16
14
  * 提供卡片消息的发送、回复、更新功能
17
15
  */
18
- @Injectable()
19
16
  export class FeishuCardService {
20
- constructor(
21
- protected readonly feishuSdkService: FeishuSdkService,
22
- protected readonly eventEmitter: EventEmitter2,
23
- ) {}
17
+ constructor(protected readonly feishuSdkService: FeishuSdkService) {
18
+ // 监听卡片动作触发事件
19
+ feishuSdkService.on(FEISHU_CARD_ACTION_TRIGGER, (event) => {
20
+ this.handleCardActionTrigger(event);
21
+ });
22
+ }
24
23
 
25
- @OnEvent(FEISHU_CARD_ACTION_TRIGGER)
26
- async handleCardActionTrigger(event: CardActionTriggerCallback) {
27
- const event_hook = event.header.event_type ?? event.event.action.tag;
24
+ async handleCardActionTrigger(event: CardActionTriggerCallback): Promise<void> {
25
+ const _eventHook = event.header.event_type ?? event.event.action.tag;
28
26
 
29
- // console.log(event_hook);
27
+ // console.log(_eventHook);
30
28
  }
31
29
 
32
30
  /**
@@ -1,4 +1,3 @@
1
- export * from "./feishu-sdk.module";
2
1
  export * from "./feishu-sdk.service";
3
2
  export * from "./fieshu-card.service";
4
3
  export * from "./types/index";
@@ -1,4 +1,3 @@
1
- import { Inject, Injectable } from "@nestjs/common";
2
1
  import type {
3
2
  GitProvider,
4
3
  LockBranchOptions,
@@ -6,7 +5,6 @@ import type {
6
5
  } from "./git-provider.interface";
7
6
  import {
8
7
  type GitProviderModuleOptions,
9
- GIT_PROVIDER_MODULE_OPTIONS,
10
8
  type BranchProtection,
11
9
  type CreateBranchProtectionOption,
12
10
  type EditBranchProtectionOption,
@@ -36,13 +34,10 @@ import { GitlabAdapter } from "./adapters/gitlab.adapter";
36
34
  * Git Provider 统一服务
37
35
  * 根据配置的 provider 类型代理到对应的适配器实现
38
36
  */
39
- @Injectable()
40
37
  export class GitProviderService implements GitProvider {
41
38
  protected readonly adapter: GitProvider;
42
39
 
43
- constructor(
44
- @Inject(GIT_PROVIDER_MODULE_OPTIONS) protected readonly options: GitProviderModuleOptions,
45
- ) {
40
+ constructor(protected readonly options: GitProviderModuleOptions) {
46
41
  this.adapter = this.createAdapter(options);
47
42
  }
48
43
 
@@ -3,5 +3,4 @@ export * from "./detect-provider";
3
3
  export * from "./parse-repo-url";
4
4
  export * from "./git-provider.interface";
5
5
  export * from "./git-provider.service";
6
- export * from "./git-provider.module";
7
6
  export * from "./adapters";
@@ -1,9 +1,7 @@
1
- import { Injectable } from "@nestjs/common";
2
1
  import { spawn, execSync } from "child_process";
3
2
  import type { GitCommit, GitChangedFile, GitDiffFile, GitRunOptions } from "./git-sdk.types";
4
3
  import { mapGitStatus, parseDiffText } from "./git-sdk-diff.utils";
5
4
 
6
- @Injectable()
7
5
  export class GitSdkService {
8
6
  protected readonly defaultOptions: GitRunOptions = {
9
7
  cwd: process.cwd(),
@@ -1,4 +1,3 @@
1
1
  export * from "./git-sdk.types";
2
2
  export * from "./git-sdk-diff.utils";
3
3
  export { GitSdkService } from "./git-sdk.service";
4
- export { GitSdkModule } from "./git-sdk.module";
@@ -1,18 +1,24 @@
1
- import { vi, type Mocked, type Mock } from "vitest";
2
- import { Test, TestingModule } from "@nestjs/testing";
1
+ import { vi, type Mock } from "vitest";
3
2
  import { ClaudeCodeAdapter } from "./claude-code.adapter";
4
- import { ClaudeSetupService } from "../../claude-setup";
5
3
  import { LlmStreamEvent } from "../interfaces";
6
4
 
7
5
  vi.mock("@anthropic-ai/claude-agent-sdk", () => ({
8
6
  query: vi.fn(),
9
7
  }));
10
8
 
9
+ vi.mock("../../claude-setup", () => ({
10
+ ClaudeSetupService: class MockClaudeSetupService {
11
+ configure = vi.fn().mockResolvedValue(undefined);
12
+ backup = vi.fn().mockResolvedValue(undefined);
13
+ restore = vi.fn().mockResolvedValue(undefined);
14
+ withTemporaryConfig = vi.fn();
15
+ },
16
+ }));
17
+
11
18
  import { query } from "@anthropic-ai/claude-agent-sdk";
12
19
 
13
20
  describe("ClaudeAdapter", () => {
14
21
  let adapter: ClaudeCodeAdapter;
15
- let claudeSetupService: Mocked<ClaudeSetupService>;
16
22
 
17
23
  const mockConfig = {
18
24
  claudeCode: {
@@ -21,24 +27,9 @@ describe("ClaudeAdapter", () => {
21
27
  },
22
28
  };
23
29
 
24
- beforeEach(async () => {
25
- const mockSetup = {
26
- configure: vi.fn().mockResolvedValue(undefined),
27
- backup: vi.fn().mockResolvedValue(undefined),
28
- restore: vi.fn().mockResolvedValue(undefined),
29
- withTemporaryConfig: vi.fn(),
30
- };
31
-
32
- const module: TestingModule = await Test.createTestingModule({
33
- providers: [
34
- ClaudeCodeAdapter,
35
- { provide: "LLM_PROXY_CONFIG", useValue: mockConfig },
36
- { provide: ClaudeSetupService, useValue: mockSetup },
37
- ],
38
- }).compile();
39
-
40
- adapter = module.get<ClaudeCodeAdapter>(ClaudeCodeAdapter);
41
- claudeSetupService = module.get(ClaudeSetupService);
30
+ beforeEach(() => {
31
+ vi.clearAllMocks();
32
+ adapter = new ClaudeCodeAdapter(mockConfig as any);
42
33
  });
43
34
 
44
35
  it("should be defined", () => {
@@ -51,15 +42,8 @@ describe("ClaudeAdapter", () => {
51
42
  expect(adapter.isConfigured()).toBe(true);
52
43
  });
53
44
 
54
- it("should return false if claude config is missing", async () => {
55
- const module: TestingModule = await Test.createTestingModule({
56
- providers: [
57
- ClaudeCodeAdapter,
58
- { provide: "LLM_PROXY_CONFIG", useValue: {} },
59
- { provide: ClaudeSetupService, useValue: { configure: vi.fn() } },
60
- ],
61
- }).compile();
62
- const unconfiguredAdapter = module.get<ClaudeCodeAdapter>(ClaudeCodeAdapter);
45
+ it("should return false if claude config is missing", () => {
46
+ const unconfiguredAdapter = new ClaudeCodeAdapter({} as any);
63
47
  expect(unconfiguredAdapter.isConfigured()).toBe(false);
64
48
  });
65
49
  });
@@ -79,7 +63,6 @@ describe("ClaudeAdapter", () => {
79
63
  events.push(event);
80
64
  }
81
65
 
82
- expect(claudeSetupService.configure).toHaveBeenCalled();
83
66
  expect(query).toHaveBeenCalledWith(
84
67
  expect.objectContaining({
85
68
  prompt: "hello",
@@ -1,4 +1,3 @@
1
- import { Injectable, Inject } from "@nestjs/common";
2
1
  import { query, type SpawnOptions } from "@anthropic-ai/claude-agent-sdk";
3
2
  import { spawn } from "child_process";
4
3
  import type { LlmAdapter } from "./llm-adapter.interface";
@@ -12,14 +11,14 @@ import type {
12
11
  import { ClaudeSetupService } from "../../claude-setup";
13
12
  import { shouldLog } from "../../verbose";
14
13
 
15
- @Injectable()
16
14
  export class ClaudeCodeAdapter implements LlmAdapter {
17
15
  readonly name = "claude-code";
16
+ private readonly claudeSetupService: ClaudeSetupService;
18
17
 
19
- constructor(
20
- @Inject("LLM_PROXY_CONFIG") private readonly config: LlmProxyConfig,
21
- private readonly claudeSetupService: ClaudeSetupService,
22
- ) {}
18
+ constructor(private readonly config: LlmProxyConfig) {
19
+ // 创建 ClaudeSetupService 实例,传入 LLM 配置
20
+ this.claudeSetupService = new ClaudeSetupService(config);
21
+ }
23
22
 
24
23
  isConfigured(): boolean {
25
24
  return !!this.config.claudeCode;
@@ -1,4 +1,3 @@
1
- import { Injectable, Inject } from "@nestjs/common";
2
1
  import { createOpencode } from "@opencode-ai/sdk";
3
2
  import type { LlmAdapter } from "./llm-adapter.interface";
4
3
  import type {
@@ -11,11 +10,10 @@ import type {
11
10
  } from "../interfaces";
12
11
  import { shouldLog } from "../../verbose";
13
12
 
14
- @Injectable()
15
13
  export class OpenCodeAdapter implements LlmAdapter {
16
14
  readonly name = "open-code";
17
15
 
18
- constructor(@Inject("LLM_PROXY_CONFIG") private readonly config: LlmProxyConfig) {}
16
+ constructor(private readonly config: LlmProxyConfig) {}
19
17
 
20
18
  isConfigured(): boolean {
21
19
  return !!this.config.openCode;
@@ -1,5 +1,4 @@
1
1
  import { vi, type Mock } from "vitest";
2
- import { Test, TestingModule } from "@nestjs/testing";
3
2
  import { OpenAIAdapter } from "./openai.adapter";
4
3
  import OpenAI from "openai";
5
4
 
@@ -17,7 +16,8 @@ describe("OpenAIAdapter", () => {
17
16
  },
18
17
  };
19
18
 
20
- beforeEach(async () => {
19
+ beforeEach(() => {
20
+ vi.clearAllMocks();
21
21
  mockOpenAIInstance = {
22
22
  chat: {
23
23
  completions: {
@@ -28,12 +28,7 @@ describe("OpenAIAdapter", () => {
28
28
  (OpenAI as unknown as Mock).mockImplementation(function () {
29
29
  return mockOpenAIInstance;
30
30
  });
31
-
32
- const module: TestingModule = await Test.createTestingModule({
33
- providers: [OpenAIAdapter, { provide: "LLM_PROXY_CONFIG", useValue: mockConfig }],
34
- }).compile();
35
-
36
- adapter = module.get<OpenAIAdapter>(OpenAIAdapter);
31
+ adapter = new OpenAIAdapter(mockConfig as any);
37
32
  });
38
33
 
39
34
  it("should be defined", () => {
@@ -46,11 +41,8 @@ describe("OpenAIAdapter", () => {
46
41
  expect(adapter.isConfigured()).toBe(true);
47
42
  });
48
43
 
49
- it("should return false if openai config is missing", async () => {
50
- const module: TestingModule = await Test.createTestingModule({
51
- providers: [OpenAIAdapter, { provide: "LLM_PROXY_CONFIG", useValue: {} }],
52
- }).compile();
53
- const unconfiguredAdapter = module.get<OpenAIAdapter>(OpenAIAdapter);
44
+ it("should return false if openai config is missing", () => {
45
+ const unconfiguredAdapter = new OpenAIAdapter({} as any);
54
46
  expect(unconfiguredAdapter.isConfigured()).toBe(false);
55
47
  });
56
48
  });
@@ -156,10 +148,7 @@ describe("OpenAIAdapter", () => {
156
148
  });
157
149
 
158
150
  it("should yield error when openai not configured", async () => {
159
- const module: TestingModule = await Test.createTestingModule({
160
- providers: [OpenAIAdapter, { provide: "LLM_PROXY_CONFIG", useValue: {} }],
161
- }).compile();
162
- const unconfigured = module.get<OpenAIAdapter>(OpenAIAdapter);
151
+ const unconfigured = new OpenAIAdapter({} as any);
163
152
  const events: any[] = [];
164
153
  for await (const event of unconfigured.chatStream([{ role: "user", content: "hi" }] as any)) {
165
154
  events.push(event);
@@ -176,10 +165,7 @@ describe("OpenAIAdapter", () => {
176
165
  });
177
166
 
178
167
  it("should throw when openai not configured for chat", async () => {
179
- const module: TestingModule = await Test.createTestingModule({
180
- providers: [OpenAIAdapter, { provide: "LLM_PROXY_CONFIG", useValue: {} }],
181
- }).compile();
182
- const unconfigured = module.get<OpenAIAdapter>(OpenAIAdapter);
168
+ const unconfigured = new OpenAIAdapter({} as any);
183
169
  await expect(unconfigured.chat([{ role: "user", content: "hi" }] as any)).rejects.toThrow(
184
170
  "未配置 openai",
185
171
  );
@@ -1,4 +1,3 @@
1
- import { Injectable, Inject } from "@nestjs/common";
2
1
  import OpenAI from "openai";
3
2
  import type { LlmAdapter } from "./llm-adapter.interface";
4
3
  import type {
@@ -10,13 +9,12 @@ import type {
10
9
  } from "../interfaces";
11
10
  import { shouldLog } from "../../verbose";
12
11
 
13
- @Injectable()
14
12
  export class OpenAIAdapter implements LlmAdapter {
15
13
  readonly name = "openai";
16
14
 
17
15
  private client: OpenAI | null = null;
18
16
 
19
- constructor(@Inject("LLM_PROXY_CONFIG") private readonly config: LlmProxyConfig) {}
17
+ constructor(private readonly config: LlmProxyConfig) {}
20
18
 
21
19
  isConfigured(): boolean {
22
20
  return !!this.config.openai;
@@ -2,5 +2,4 @@ export * from "./interfaces";
2
2
  export * from "./adapters";
3
3
  export * from "./llm-session";
4
4
  export * from "./llm-proxy.service";
5
- export * from "./llm-proxy.module";
6
5
  export * from "./stream-logger";
@@ -2,6 +2,7 @@ export interface ClaudeAdapterConfig {
2
2
  model?: string;
3
3
  baseUrl?: string;
4
4
  authToken?: string;
5
+ hasCompletedOnboarding?: boolean;
5
6
  }
6
7
 
7
8
  export interface OpenAIAdapterConfig {
@@ -22,11 +23,18 @@ export interface OpenCodeAdapterConfig {
22
23
 
23
24
  export type LLMMode = "claude-code" | "openai" | "gemini" | "open-code";
24
25
 
26
+ export interface GeminiAdapterConfig {
27
+ model?: string;
28
+ baseUrl?: string;
29
+ apiKey?: string;
30
+ }
31
+
25
32
  export interface LlmProxyConfig {
26
33
  defaultAdapter?: LLMMode;
27
34
  claudeCode?: ClaudeAdapterConfig;
28
35
  openai?: OpenAIAdapterConfig;
29
36
  openCode?: OpenCodeAdapterConfig;
37
+ gemini?: GeminiAdapterConfig;
30
38
  }
31
39
 
32
40
  export const LLM_PROXY_CONFIG = Symbol("LLM_PROXY_CONFIG");