@spaceflow/core 0.1.3 → 0.3.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 (56) hide show
  1. package/dist/index.js +2642 -3247
  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/adapters/gitea.adapter.ts +59 -22
  22. package/src/shared/git-provider/adapters/github.adapter.spec.ts +2 -1
  23. package/src/shared/git-provider/adapters/github.adapter.ts +200 -29
  24. package/src/shared/git-provider/adapters/gitlab.adapter.ts +78 -26
  25. package/src/shared/git-provider/git-provider.interface.ts +20 -1
  26. package/src/shared/git-provider/git-provider.service.ts +28 -6
  27. package/src/shared/git-provider/index.ts +0 -1
  28. package/src/shared/git-provider/types.ts +27 -0
  29. package/src/shared/git-sdk/git-sdk.service.ts +0 -2
  30. package/src/shared/git-sdk/index.ts +0 -1
  31. package/src/shared/llm-proxy/adapters/claude-code.adapter.spec.ts +15 -32
  32. package/src/shared/llm-proxy/adapters/claude-code.adapter.ts +5 -6
  33. package/src/shared/llm-proxy/adapters/open-code.adapter.ts +1 -3
  34. package/src/shared/llm-proxy/adapters/openai.adapter.spec.ts +7 -21
  35. package/src/shared/llm-proxy/adapters/openai.adapter.ts +1 -3
  36. package/src/shared/llm-proxy/index.ts +0 -1
  37. package/src/shared/llm-proxy/interfaces/config.interface.ts +8 -0
  38. package/src/shared/llm-proxy/llm-proxy.service.spec.ts +91 -68
  39. package/src/shared/llm-proxy/llm-proxy.service.ts +5 -8
  40. package/src/shared/mcp/index.ts +1 -33
  41. package/src/shared/output/index.ts +0 -1
  42. package/src/shared/output/output.service.ts +43 -13
  43. package/src/shared/rspack-config/rspack-config.ts +0 -6
  44. package/src/shared/storage/index.ts +0 -1
  45. package/src/shared/storage/storage.service.ts +16 -8
  46. package/src/shared/storage/types.ts +0 -44
  47. package/src/shared/verbose/index.ts +15 -0
  48. package/src/app.module.ts +0 -18
  49. package/src/config/config-reader.module.ts +0 -16
  50. package/src/shared/claude-setup/claude-setup.module.ts +0 -8
  51. package/src/shared/feishu-sdk/feishu-sdk.module.ts +0 -77
  52. package/src/shared/git-provider/git-provider.module.ts +0 -73
  53. package/src/shared/git-sdk/git-sdk.module.ts +0 -8
  54. package/src/shared/llm-proxy/llm-proxy.module.ts +0 -140
  55. package/src/shared/output/output.module.ts +0 -9
  56. package/src/shared/storage/storage.module.ts +0 -150
@@ -19,6 +19,7 @@ import type {
19
19
  EditPullRequestOption,
20
20
  User,
21
21
  RepositoryContent,
22
+ ResolvedThread,
22
23
  } from "./types";
23
24
 
24
25
  /** PR 列表查询选项 */
@@ -164,7 +165,15 @@ export interface GitProvider {
164
165
  ): Promise<PullReview>;
165
166
  /** 列出 PR 的所有 Reviews */
166
167
  listPullReviews(owner: string, repo: string, index: number): Promise<PullReview[]>;
167
- /** 删除 PR Review */
168
+ /** 更新 PR Review 的内容 */
169
+ updatePullReview(
170
+ owner: string,
171
+ repo: string,
172
+ index: number,
173
+ reviewId: number,
174
+ body: string,
175
+ ): Promise<PullReview>;
176
+ /** 删除 PR Review(仅支持 PENDING 状态) */
168
177
  deletePullReview(owner: string, repo: string, index: number, reviewId: number): Promise<void>;
169
178
  /** 获取 PR Review 的行级评论列表 */
170
179
  listPullReviewComments(
@@ -173,10 +182,20 @@ export interface GitProvider {
173
182
  index: number,
174
183
  reviewId: number,
175
184
  ): Promise<PullReviewComment[]>;
185
+ /** 删除 PR Review 的单条行级评论 */
186
+ deletePullReviewComment(owner: string, repo: string, commentId: number): Promise<void>;
187
+ /** 获取 PR 中所有已解决的 review threads(含文件路径和行号) */
188
+ listResolvedThreads(owner: string, repo: string, index: number): Promise<ResolvedThread[]>;
176
189
 
177
190
  // ============ Reaction 操作 ============
178
191
  /** 获取 Issue/PR 评论的 reactions */
179
192
  getIssueCommentReactions(owner: string, repo: string, commentId: number): Promise<Reaction[]>;
193
+ /** 获取 PR Review 行级评论的 reactions */
194
+ getPullReviewCommentReactions(
195
+ owner: string,
196
+ repo: string,
197
+ commentId: number,
198
+ ): Promise<Reaction[]>;
180
199
  /** 获取 Issue/PR 的 reactions */
181
200
  getIssueReactions(owner: string, repo: string, index: number): Promise<Reaction[]>;
182
201
 
@@ -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,
@@ -27,6 +25,7 @@ import {
27
25
  type EditPullRequestOption,
28
26
  type User,
29
27
  type RepositoryContent,
28
+ type ResolvedThread,
30
29
  } from "./types";
31
30
  import { GiteaAdapter } from "./adapters/gitea.adapter";
32
31
  import { GithubAdapter } from "./adapters/github.adapter";
@@ -36,13 +35,10 @@ import { GitlabAdapter } from "./adapters/gitlab.adapter";
36
35
  * Git Provider 统一服务
37
36
  * 根据配置的 provider 类型代理到对应的适配器实现
38
37
  */
39
- @Injectable()
40
38
  export class GitProviderService implements GitProvider {
41
39
  protected readonly adapter: GitProvider;
42
40
 
43
- constructor(
44
- @Inject(GIT_PROVIDER_MODULE_OPTIONS) protected readonly options: GitProviderModuleOptions,
45
- ) {
41
+ constructor(protected readonly options: GitProviderModuleOptions) {
46
42
  this.adapter = this.createAdapter(options);
47
43
  }
48
44
 
@@ -265,6 +261,16 @@ export class GitProviderService implements GitProvider {
265
261
  return this.adapter.listPullReviews(owner, repo, index);
266
262
  }
267
263
 
264
+ async updatePullReview(
265
+ owner: string,
266
+ repo: string,
267
+ index: number,
268
+ reviewId: number,
269
+ body: string,
270
+ ): Promise<PullReview> {
271
+ return this.adapter.updatePullReview(owner, repo, index, reviewId, body);
272
+ }
273
+
268
274
  async deletePullReview(
269
275
  owner: string,
270
276
  repo: string,
@@ -283,6 +289,14 @@ export class GitProviderService implements GitProvider {
283
289
  return this.adapter.listPullReviewComments(owner, repo, index, reviewId);
284
290
  }
285
291
 
292
+ async deletePullReviewComment(owner: string, repo: string, commentId: number): Promise<void> {
293
+ return this.adapter.deletePullReviewComment(owner, repo, commentId);
294
+ }
295
+
296
+ async listResolvedThreads(owner: string, repo: string, index: number): Promise<ResolvedThread[]> {
297
+ return this.adapter.listResolvedThreads(owner, repo, index);
298
+ }
299
+
286
300
  // ============ Reaction 操作 ============
287
301
 
288
302
  async getIssueCommentReactions(
@@ -293,6 +307,14 @@ export class GitProviderService implements GitProvider {
293
307
  return this.adapter.getIssueCommentReactions(owner, repo, commentId);
294
308
  }
295
309
 
310
+ async getPullReviewCommentReactions(
311
+ owner: string,
312
+ repo: string,
313
+ commentId: number,
314
+ ): Promise<Reaction[]> {
315
+ return this.adapter.getPullReviewCommentReactions(owner, repo, commentId);
316
+ }
317
+
296
318
  async getIssueReactions(owner: string, repo: string, index: number): Promise<Reaction[]> {
297
319
  return this.adapter.getIssueReactions(owner, repo, index);
298
320
  }
@@ -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";
@@ -318,6 +318,20 @@ export interface Issue {
318
318
  /** PR Review 事件类型 */
319
319
  export type ReviewStateType = "APPROVE" | "REQUEST_CHANGES" | "COMMENT" | "PENDING";
320
320
 
321
+ /** PR Review 事件常量 */
322
+ export const REVIEW_STATE = {
323
+ APPROVE: "APPROVE",
324
+ REQUEST_CHANGES: "REQUEST_CHANGES",
325
+ COMMENT: "COMMENT",
326
+ PENDING: "PENDING",
327
+ } as const;
328
+
329
+ /** PR Review 行级评论 Diff 方向 */
330
+ export const DIFF_SIDE = {
331
+ RIGHT: "RIGHT",
332
+ LEFT: "LEFT",
333
+ } as const;
334
+
321
335
  /** PR Review 行级评论 */
322
336
  export interface CreatePullReviewComment {
323
337
  /** 文件路径 */
@@ -381,6 +395,19 @@ export interface PullReviewComment {
381
395
  html_url?: string;
382
396
  }
383
397
 
398
+ /** PR Review Thread 已解决信息 */
399
+ export interface ResolvedThread {
400
+ /** 文件路径 */
401
+ path?: string;
402
+ /** 行号(新文件中的行号) */
403
+ line?: number;
404
+ /** 解决者 */
405
+ resolvedBy?: {
406
+ id?: number;
407
+ login?: string;
408
+ } | null;
409
+ }
410
+
384
411
  /** 用户信息 */
385
412
  export interface User {
386
413
  id?: number;
@@ -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");