@spaceflow/core 0.1.1

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 (116) hide show
  1. package/CHANGELOG.md +1176 -0
  2. package/README.md +105 -0
  3. package/nest-cli.json +10 -0
  4. package/package.json +128 -0
  5. package/rspack.config.mjs +62 -0
  6. package/src/__mocks__/@opencode-ai/sdk.js +9 -0
  7. package/src/__mocks__/c12.ts +3 -0
  8. package/src/app.module.ts +18 -0
  9. package/src/config/ci.config.ts +29 -0
  10. package/src/config/config-loader.ts +101 -0
  11. package/src/config/config-reader.module.ts +16 -0
  12. package/src/config/config-reader.service.ts +133 -0
  13. package/src/config/feishu.config.ts +35 -0
  14. package/src/config/git-provider.config.ts +29 -0
  15. package/src/config/index.ts +29 -0
  16. package/src/config/llm.config.ts +110 -0
  17. package/src/config/schema-generator.service.ts +129 -0
  18. package/src/config/spaceflow.config.ts +292 -0
  19. package/src/config/storage.config.ts +33 -0
  20. package/src/extension-system/extension.interface.ts +221 -0
  21. package/src/extension-system/index.ts +1 -0
  22. package/src/index.ts +80 -0
  23. package/src/locales/en/translation.json +11 -0
  24. package/src/locales/zh-cn/translation.json +11 -0
  25. package/src/shared/claude-setup/claude-setup.module.ts +8 -0
  26. package/src/shared/claude-setup/claude-setup.service.ts +131 -0
  27. package/src/shared/claude-setup/index.ts +2 -0
  28. package/src/shared/editor-config/index.ts +23 -0
  29. package/src/shared/feishu-sdk/feishu-sdk.module.ts +77 -0
  30. package/src/shared/feishu-sdk/feishu-sdk.service.ts +130 -0
  31. package/src/shared/feishu-sdk/fieshu-card.service.ts +139 -0
  32. package/src/shared/feishu-sdk/index.ts +4 -0
  33. package/src/shared/feishu-sdk/types/card-action.ts +132 -0
  34. package/src/shared/feishu-sdk/types/card.ts +64 -0
  35. package/src/shared/feishu-sdk/types/common.ts +22 -0
  36. package/src/shared/feishu-sdk/types/index.ts +46 -0
  37. package/src/shared/feishu-sdk/types/message.ts +35 -0
  38. package/src/shared/feishu-sdk/types/module.ts +21 -0
  39. package/src/shared/feishu-sdk/types/user.ts +77 -0
  40. package/src/shared/git-provider/adapters/gitea.adapter.spec.ts +473 -0
  41. package/src/shared/git-provider/adapters/gitea.adapter.ts +499 -0
  42. package/src/shared/git-provider/adapters/github.adapter.spec.ts +341 -0
  43. package/src/shared/git-provider/adapters/github.adapter.ts +830 -0
  44. package/src/shared/git-provider/adapters/gitlab.adapter.ts +839 -0
  45. package/src/shared/git-provider/adapters/index.ts +3 -0
  46. package/src/shared/git-provider/detect-provider.spec.ts +195 -0
  47. package/src/shared/git-provider/detect-provider.ts +112 -0
  48. package/src/shared/git-provider/git-provider.interface.ts +188 -0
  49. package/src/shared/git-provider/git-provider.module.ts +73 -0
  50. package/src/shared/git-provider/git-provider.service.spec.ts +282 -0
  51. package/src/shared/git-provider/git-provider.service.ts +309 -0
  52. package/src/shared/git-provider/index.ts +7 -0
  53. package/src/shared/git-provider/parse-repo-url.spec.ts +221 -0
  54. package/src/shared/git-provider/parse-repo-url.ts +155 -0
  55. package/src/shared/git-provider/types.ts +434 -0
  56. package/src/shared/git-sdk/git-sdk-diff.utils.spec.ts +344 -0
  57. package/src/shared/git-sdk/git-sdk-diff.utils.ts +151 -0
  58. package/src/shared/git-sdk/git-sdk.module.ts +8 -0
  59. package/src/shared/git-sdk/git-sdk.service.ts +235 -0
  60. package/src/shared/git-sdk/git-sdk.types.ts +25 -0
  61. package/src/shared/git-sdk/index.ts +4 -0
  62. package/src/shared/i18n/i18n.spec.ts +96 -0
  63. package/src/shared/i18n/i18n.ts +86 -0
  64. package/src/shared/i18n/index.ts +1 -0
  65. package/src/shared/i18n/locale-detect.ts +134 -0
  66. package/src/shared/llm-jsonput/index.ts +94 -0
  67. package/src/shared/llm-jsonput/types.ts +17 -0
  68. package/src/shared/llm-proxy/adapters/claude-code.adapter.spec.ts +131 -0
  69. package/src/shared/llm-proxy/adapters/claude-code.adapter.ts +208 -0
  70. package/src/shared/llm-proxy/adapters/index.ts +4 -0
  71. package/src/shared/llm-proxy/adapters/llm-adapter.interface.ts +23 -0
  72. package/src/shared/llm-proxy/adapters/open-code.adapter.ts +342 -0
  73. package/src/shared/llm-proxy/adapters/openai.adapter.spec.ts +215 -0
  74. package/src/shared/llm-proxy/adapters/openai.adapter.ts +153 -0
  75. package/src/shared/llm-proxy/index.ts +6 -0
  76. package/src/shared/llm-proxy/interfaces/config.interface.ts +32 -0
  77. package/src/shared/llm-proxy/interfaces/index.ts +4 -0
  78. package/src/shared/llm-proxy/interfaces/message.interface.ts +48 -0
  79. package/src/shared/llm-proxy/interfaces/session.interface.ts +28 -0
  80. package/src/shared/llm-proxy/llm-proxy.module.ts +140 -0
  81. package/src/shared/llm-proxy/llm-proxy.service.spec.ts +303 -0
  82. package/src/shared/llm-proxy/llm-proxy.service.ts +132 -0
  83. package/src/shared/llm-proxy/llm-session.spec.ts +111 -0
  84. package/src/shared/llm-proxy/llm-session.ts +109 -0
  85. package/src/shared/llm-proxy/stream-logger.ts +97 -0
  86. package/src/shared/logger/index.ts +11 -0
  87. package/src/shared/logger/logger.interface.ts +93 -0
  88. package/src/shared/logger/logger.spec.ts +178 -0
  89. package/src/shared/logger/logger.ts +175 -0
  90. package/src/shared/logger/renderers/plain.renderer.ts +116 -0
  91. package/src/shared/logger/renderers/tui.renderer.ts +162 -0
  92. package/src/shared/mcp/index.ts +332 -0
  93. package/src/shared/output/index.ts +2 -0
  94. package/src/shared/output/output.module.ts +9 -0
  95. package/src/shared/output/output.service.ts +97 -0
  96. package/src/shared/package-manager/index.ts +115 -0
  97. package/src/shared/parallel/index.ts +1 -0
  98. package/src/shared/parallel/parallel-executor.ts +169 -0
  99. package/src/shared/rspack-config/index.ts +1 -0
  100. package/src/shared/rspack-config/rspack-config.ts +157 -0
  101. package/src/shared/source-utils/index.ts +130 -0
  102. package/src/shared/spaceflow-dir/index.ts +158 -0
  103. package/src/shared/storage/adapters/file.adapter.ts +113 -0
  104. package/src/shared/storage/adapters/index.ts +3 -0
  105. package/src/shared/storage/adapters/memory.adapter.ts +50 -0
  106. package/src/shared/storage/adapters/storage-adapter.interface.ts +48 -0
  107. package/src/shared/storage/index.ts +4 -0
  108. package/src/shared/storage/storage.module.ts +150 -0
  109. package/src/shared/storage/storage.service.ts +293 -0
  110. package/src/shared/storage/types.ts +51 -0
  111. package/src/shared/verbose/index.ts +73 -0
  112. package/test/app.e2e-spec.ts +22 -0
  113. package/tsconfig.build.json +4 -0
  114. package/tsconfig.json +25 -0
  115. package/tsconfig.skill.json +18 -0
  116. package/vitest.config.ts +58 -0
@@ -0,0 +1,162 @@
1
+ import chalk from "chalk";
2
+ import ora from "ora";
3
+ import logUpdate from "log-update";
4
+ import type {
5
+ LogRenderer,
6
+ Spinner,
7
+ ProgressBar,
8
+ ProgressBarOptions,
9
+ TaskItem,
10
+ TaskControl,
11
+ TaskStatus,
12
+ } from "../logger.interface";
13
+
14
+ /** 进度条默认宽度 */
15
+ const DEFAULT_BAR_WIDTH = 30;
16
+
17
+ /** 任务状态图标 */
18
+ const TASK_ICONS: Record<TaskStatus, string> = {
19
+ pending: chalk.gray("○"),
20
+ running: chalk.cyan("◌"),
21
+ success: chalk.green("✔"),
22
+ failed: chalk.red("✖"),
23
+ skipped: chalk.yellow("⊘"),
24
+ };
25
+
26
+ /** 渲染进度条字符串 */
27
+ const renderBar = (current: number, total: number, width: number): string => {
28
+ const ratio = Math.min(current / total, 1);
29
+ const filled = Math.round(ratio * width);
30
+ const empty = width - filled;
31
+ const bar = chalk.green("█".repeat(filled)) + chalk.gray("░".repeat(empty));
32
+ const pct = Math.round(ratio * 100);
33
+ return `${bar} ${pct}%`;
34
+ };
35
+
36
+ /** TUI 模式渲染器:富交互输出,适合终端 */
37
+ export class TuiRenderer implements LogRenderer {
38
+ info(prefix: string, message: string): void {
39
+ console.log(`${chalk.gray(prefix)} ${message}`);
40
+ }
41
+
42
+ success(prefix: string, message: string): void {
43
+ console.log(`${chalk.gray(prefix)} ${chalk.green("✔")} ${message}`);
44
+ }
45
+
46
+ warn(prefix: string, message: string): void {
47
+ console.warn(`${chalk.gray(prefix)} ${chalk.yellow("⚠")} ${chalk.yellow(message)}`);
48
+ }
49
+
50
+ error(prefix: string, message: string): void {
51
+ console.error(`${chalk.gray(prefix)} ${chalk.red("✖")} ${chalk.red(message)}`);
52
+ }
53
+
54
+ debug(prefix: string, message: string): void {
55
+ console.log(`${chalk.gray(prefix)} ${chalk.magenta("[DEBUG]")} ${chalk.gray(message)}`);
56
+ }
57
+
58
+ verbose(prefix: string, message: string): void {
59
+ console.log(`${chalk.gray(prefix)} ${chalk.gray(message)}`);
60
+ }
61
+
62
+ createSpinner(prefix: string, message: string): Spinner {
63
+ const spinner = ora({
64
+ text: `${chalk.gray(prefix)} ${message}`,
65
+ prefixText: "",
66
+ }).start();
67
+ return {
68
+ update: (msg: string) => {
69
+ spinner.text = `${chalk.gray(prefix)} ${msg}`;
70
+ },
71
+ succeed: (msg?: string) => {
72
+ spinner.succeed(`${chalk.gray(prefix)} ${msg ?? message}`);
73
+ },
74
+ fail: (msg?: string) => {
75
+ spinner.fail(`${chalk.gray(prefix)} ${msg ?? message}`);
76
+ },
77
+ stop: () => {
78
+ spinner.stop();
79
+ },
80
+ };
81
+ }
82
+
83
+ createProgressBar(prefix: string, options: ProgressBarOptions): ProgressBar {
84
+ const { total, label = "", width = DEFAULT_BAR_WIDTH } = options;
85
+ const tag = label ? `${label} ` : "";
86
+ logUpdate(`${chalk.gray(prefix)} ${tag}${renderBar(0, total, width)} 0/${total}`);
87
+ return {
88
+ update: (current: number, msg?: string) => {
89
+ const suffix = msg ? ` ${chalk.gray(msg)}` : "";
90
+ logUpdate(
91
+ `${chalk.gray(prefix)} ${tag}${renderBar(current, total, width)} ${current}/${total}${suffix}`,
92
+ );
93
+ },
94
+ finish: (msg?: string) => {
95
+ const suffix = msg ? ` ${msg}` : "";
96
+ logUpdate.done();
97
+ console.log(
98
+ `${chalk.gray(prefix)} ${chalk.green("✔")} ${tag}${total}/${total} (100%)${suffix}`,
99
+ );
100
+ },
101
+ };
102
+ }
103
+
104
+ async runTasks<T>(prefix: string, items: TaskItem<T>[]): Promise<T[]> {
105
+ const enabledItems = items.filter((item) => item.enabled !== false);
106
+ const statuses: TaskStatus[] = enabledItems.map(() => "pending");
107
+ const messages: string[] = enabledItems.map((item) => item.title);
108
+ const results: T[] = [];
109
+ const renderTaskList = (): string => {
110
+ return enabledItems
111
+ .map((item, i) => {
112
+ const icon = TASK_ICONS[statuses[i]];
113
+ const title = statuses[i] === "running" ? chalk.cyan(item.title) : item.title;
114
+ const suffix = messages[i] !== item.title ? chalk.gray(` ${messages[i]}`) : "";
115
+ return `${chalk.gray(prefix)} ${icon} ${title}${suffix}`;
116
+ })
117
+ .join("\n");
118
+ };
119
+ logUpdate(renderTaskList());
120
+ for (let i = 0; i < enabledItems.length; i++) {
121
+ statuses[i] = "running";
122
+ logUpdate(renderTaskList());
123
+ let skipped = false;
124
+ let skipReason = "";
125
+ const control: TaskControl = {
126
+ update: (msg: string) => {
127
+ messages[i] = msg;
128
+ logUpdate(renderTaskList());
129
+ },
130
+ skip: (reason?: string) => {
131
+ skipped = true;
132
+ skipReason = reason ?? "";
133
+ },
134
+ };
135
+ try {
136
+ const ctx = (results.length > 0 ? results[results.length - 1] : undefined) as T;
137
+ const result = await enabledItems[i].task(ctx, control);
138
+ if (skipped) {
139
+ statuses[i] = "skipped";
140
+ messages[i] = skipReason ? `${enabledItems[i].title} (${skipReason})` : enabledItems[i].title;
141
+ } else {
142
+ statuses[i] = "success";
143
+ messages[i] = enabledItems[i].title;
144
+ }
145
+ results.push(result);
146
+ } catch (err) {
147
+ statuses[i] = "failed";
148
+ messages[i] = err instanceof Error ? err.message : String(err);
149
+ logUpdate(renderTaskList());
150
+ logUpdate.done();
151
+ throw err;
152
+ }
153
+ logUpdate(renderTaskList());
154
+ }
155
+ logUpdate.done();
156
+ return results;
157
+ }
158
+
159
+ table(_prefix: string, data: Record<string, unknown>[]): void {
160
+ console.table(data);
161
+ }
162
+ }
@@ -0,0 +1,332 @@
1
+ /**
2
+ * MCP (Model Context Protocol) 支持模块
3
+ *
4
+ * 提供装饰器和基础设施,用于在服务中定义 MCP 工具
5
+ *
6
+ * 使用方式:
7
+ * ```typescript
8
+ * class ListRulesInput {
9
+ * @ApiPropertyOptional({ description: "项目目录" })
10
+ * @IsString()
11
+ * @IsOptional()
12
+ * cwd?: string;
13
+ * }
14
+ *
15
+ * @McpServer({ name: "review-rules", version: "1.0.0" })
16
+ * export class ReviewMcpService {
17
+ * @McpTool({
18
+ * name: "list_rules",
19
+ * description: "获取所有审查规则",
20
+ * dto: ListRulesInput,
21
+ * })
22
+ * async listRules(input: ListRulesInput) {
23
+ * return { rules: [...] };
24
+ * }
25
+ * }
26
+ * ```
27
+ */
28
+
29
+ import "reflect-metadata";
30
+ import { Injectable } from "@nestjs/common";
31
+
32
+ /** MCP 服务元数据 key(使用 Symbol 确保唯一性) */
33
+ export const MCP_SERVER_METADATA = Symbol.for("spaceflow:mcp:server");
34
+
35
+ /** MCP 工具元数据 key(使用 Symbol 确保唯一性) */
36
+ export const MCP_TOOL_METADATA = Symbol.for("spaceflow:mcp:tool");
37
+
38
+ /** JSON Schema 类型 */
39
+ export interface JsonSchema {
40
+ type: string;
41
+ properties?: Record<string, JsonSchema & { description?: string }>;
42
+ required?: string[];
43
+ items?: JsonSchema;
44
+ description?: string;
45
+ }
46
+
47
+ /** MCP 服务定义 */
48
+ export interface McpServerDefinition {
49
+ /** 服务名称 */
50
+ name: string;
51
+ /** 服务版本 */
52
+ version?: string;
53
+ /** 服务描述 */
54
+ description?: string;
55
+ }
56
+
57
+ /** Swagger 元数据常量 */
58
+ const SWAGGER_API_MODEL_PROPERTIES = "swagger/apiModelProperties";
59
+ const SWAGGER_API_MODEL_PROPERTIES_ARRAY = "swagger/apiModelPropertiesArray";
60
+
61
+ /**
62
+ * 从 @nestjs/swagger 的 @ApiProperty / @ApiPropertyOptional 元数据生成 JSON Schema
63
+ * 直接读取 swagger 装饰器存储的 reflect-metadata,无需自定义装饰器
64
+ */
65
+ export function dtoToJsonSchema(dtoClass: new (...args: any[]) => any): JsonSchema {
66
+ const prototype = dtoClass.prototype;
67
+
68
+ // 读取属性名列表(swagger 存储格式为 ":propertyName")
69
+ const propertyKeys: string[] = (
70
+ Reflect.getMetadata(SWAGGER_API_MODEL_PROPERTIES_ARRAY, prototype) || []
71
+ ).map((key: string) => key.replace(/^:/, ""));
72
+
73
+ const properties: Record<string, any> = {};
74
+ const required: string[] = [];
75
+
76
+ for (const key of propertyKeys) {
77
+ const meta = Reflect.getMetadata(SWAGGER_API_MODEL_PROPERTIES, prototype, key) || {};
78
+
79
+ // 推断 JSON Schema type
80
+ const typeMap: Record<string, string> = {
81
+ String: "string",
82
+ Number: "number",
83
+ Boolean: "boolean",
84
+ Array: "array",
85
+ Object: "object",
86
+ };
87
+
88
+ let schemaType = meta.type;
89
+ // meta.type 可能是构造函数(如 Boolean)或字符串(如 "boolean")
90
+ if (typeof schemaType === "function") {
91
+ schemaType = typeMap[schemaType.name] || "string";
92
+ }
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 推断
123
+ if (!schemaType) {
124
+ const reflectedType = Reflect.getMetadata("design:type", prototype, key);
125
+ if (reflectedType) {
126
+ schemaType = typeMap[reflectedType.name] || "string";
127
+ }
128
+ }
129
+
130
+ const prop: Record<string, any> = {};
131
+ if (schemaType) prop.type = schemaType;
132
+ if (meta.description) prop.description = meta.description;
133
+ if (meta.default !== undefined) prop.default = meta.default;
134
+ if (meta.enum) prop.enum = meta.enum;
135
+ if (meta.example !== undefined) prop.example = meta.example;
136
+
137
+ properties[key] = prop;
138
+
139
+ // required 判断:swagger 的 @ApiProperty 默认 required=true,@ApiPropertyOptional 为 false
140
+ if (meta.required !== false) {
141
+ required.push(key);
142
+ }
143
+ }
144
+
145
+ const schema: JsonSchema = { type: "object", properties };
146
+ if (required.length > 0) schema.required = required;
147
+ return schema;
148
+ }
149
+
150
+ /** MCP 工具定义 */
151
+ export interface McpToolDefinition {
152
+ /** 工具名称 */
153
+ name: string;
154
+ /** 工具描述 */
155
+ description: string;
156
+ /** 输入参数 schema (JSON Schema 格式,与 dto 二选一) */
157
+ inputSchema?: JsonSchema;
158
+ /** 输入参数 DTO 类(与 inputSchema 二选一,优先级高于 inputSchema) */
159
+ dto?: new (...args: any[]) => any;
160
+ }
161
+
162
+ /** 存储的工具元数据 */
163
+ export interface McpToolMetadata extends McpToolDefinition {
164
+ /** 方法名 */
165
+ methodName: string;
166
+ }
167
+
168
+ /**
169
+ * MCP 服务装饰器
170
+ * 标记一个类为 MCP 服务,内部自动应用 @Injectable()
171
+ *
172
+ * @example
173
+ * ```typescript
174
+ * @McpServer({ name: "review-rules", version: "1.0.0" })
175
+ * export class ReviewMcpService {
176
+ * @McpTool({ name: "list_rules", description: "获取规则" })
177
+ * async listRules() { ... }
178
+ * }
179
+ * ```
180
+ */
181
+ export function McpServer(definition: McpServerDefinition): ClassDecorator {
182
+ return (target: Function) => {
183
+ // 应用 @Injectable() 装饰器
184
+ Injectable()(target);
185
+ // 使用静态属性存储元数据(跨模块可访问)
186
+ (target as any).__mcp_server__ = definition;
187
+ };
188
+ }
189
+
190
+ /**
191
+ * MCP 工具装饰器
192
+ * 标记一个方法为 MCP 工具
193
+ */
194
+ export function McpTool(definition: McpToolDefinition): MethodDecorator {
195
+ return (target: object, propertyKey: string | symbol, _descriptor: PropertyDescriptor) => {
196
+ const constructor = target.constructor as any;
197
+ // 使用静态属性存储工具列表(跨模块可访问)
198
+ if (!constructor.__mcp_tools__) {
199
+ constructor.__mcp_tools__ = [];
200
+ }
201
+
202
+ // 如果提供了 dto,自动从 swagger 元数据生成 inputSchema
203
+ const resolvedDefinition = { ...definition };
204
+ if (resolvedDefinition.dto) {
205
+ resolvedDefinition.inputSchema = dtoToJsonSchema(resolvedDefinition.dto);
206
+ }
207
+
208
+ constructor.__mcp_tools__.push({
209
+ ...resolvedDefinition,
210
+ methodName: String(propertyKey),
211
+ });
212
+ };
213
+ }
214
+
215
+ /**
216
+ * 检查一个类是否是 MCP 服务
217
+ */
218
+ export function isMcpServer(target: any): boolean {
219
+ const constructor = target?.constructor || target;
220
+ return !!constructor?.__mcp_server__;
221
+ }
222
+
223
+ /**
224
+ * 获取 MCP 服务元数据
225
+ */
226
+ export function getMcpServerMetadata(target: any): McpServerDefinition | undefined {
227
+ const constructor = target?.constructor || target;
228
+ return constructor?.__mcp_server__;
229
+ }
230
+
231
+ /**
232
+ * 从服务类获取所有 MCP 工具定义
233
+ */
234
+ export function getMcpTools(target: any): McpToolMetadata[] {
235
+ const constructor = target?.constructor || target;
236
+ return constructor?.__mcp_tools__ || [];
237
+ }
238
+
239
+ /**
240
+ * MCP Server 运行器
241
+ * 收集服务中的 MCP 工具并启动 stdio 服务
242
+ */
243
+ export async function runMcpServer(
244
+ service: any,
245
+ serverInfo: { name: string; version: string },
246
+ ): Promise<void> {
247
+ const tools = getMcpTools(service);
248
+
249
+ if (tools.length === 0) {
250
+ console.error("没有找到 MCP 工具定义");
251
+ process.exit(1);
252
+ }
253
+
254
+ // 动态导入 MCP SDK(避免在不需要时加载)
255
+ const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js");
256
+ const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
257
+
258
+ const server = new McpServer(serverInfo);
259
+
260
+ // 注册所有工具(使用 v1 API: server.tool)
261
+ for (const tool of tools) {
262
+ // v1 API: server.tool(name, description, schema, callback)
263
+ // 使用工具定义中的 inputSchema 转为 zod,如果没有则传空对象
264
+ const schema = tool.inputSchema ? jsonSchemaToZod(tool.inputSchema) : {};
265
+ server.tool(tool.name, tool.description, schema, async (args: any) => {
266
+ try {
267
+ const result = await service[tool.methodName](args || {});
268
+ return {
269
+ content: [
270
+ {
271
+ type: "text" as const,
272
+ text: typeof result === "string" ? result : JSON.stringify(result, null, 2),
273
+ },
274
+ ],
275
+ };
276
+ } catch (error) {
277
+ return {
278
+ content: [
279
+ {
280
+ type: "text" as const,
281
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
282
+ },
283
+ ],
284
+ isError: true,
285
+ };
286
+ }
287
+ });
288
+ }
289
+
290
+ /**
291
+ * 将 JSON Schema 转换为简单的 zod 对象(MCP SDK 需要 zod)
292
+ * 仅处理顶层 properties,满足 MCP 工具注册需求
293
+ */
294
+ function jsonSchemaToZod(jsonSchema: JsonSchema): Record<string, any> {
295
+ const { z } = require("zod") as typeof import("zod");
296
+ if (!jsonSchema.properties) return {};
297
+
298
+ const shape: Record<string, any> = {};
299
+ const requiredFields = jsonSchema.required || [];
300
+
301
+ for (const [key, prop] of Object.entries(jsonSchema.properties)) {
302
+ let field: any;
303
+ switch (prop.type) {
304
+ case "number":
305
+ field = z.number();
306
+ break;
307
+ case "boolean":
308
+ field = z.boolean();
309
+ break;
310
+ case "array":
311
+ field = z.array(z.any());
312
+ break;
313
+ case "object":
314
+ field = z.object({});
315
+ break;
316
+ default:
317
+ field = z.string();
318
+ }
319
+ if (prop.description) field = field.describe(prop.description);
320
+ if (!requiredFields.includes(key)) field = field.optional();
321
+ shape[key] = field;
322
+ }
323
+
324
+ return shape;
325
+ }
326
+
327
+ // 启动 stdio 传输
328
+ const transport = new StdioServerTransport();
329
+ await server.connect(transport);
330
+
331
+ console.error(`MCP Server "${serverInfo.name}" started with ${tools.length} tools`);
332
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./output.service";
2
+ export * from "./output.module";
@@ -0,0 +1,9 @@
1
+ import { Global, Module } from "@nestjs/common";
2
+ import { OutputService } from "./output.service";
3
+
4
+ @Global()
5
+ @Module({
6
+ providers: [OutputService],
7
+ exports: [OutputService],
8
+ })
9
+ export class OutputModule {}
@@ -0,0 +1,97 @@
1
+ import { Injectable, Scope } from "@nestjs/common";
2
+ import { randomUUID } from "crypto";
3
+
4
+ const OUTPUT_MARKER_START = "::spaceflow-output::";
5
+ const OUTPUT_MARKER_END = "::end::";
6
+
7
+ /**
8
+ * OutputService - 用于标准化命令输出
9
+ *
10
+ * 命令可以通过此服务设置输出值,这些值会在命令执行完成后
11
+ * 以特定格式输出到 stdout,供 CI 流程中的其他步骤使用。
12
+ *
13
+ * 输出格式: ::spaceflow-output::{"key":"value","_cacheId":"uuid"}::end::
14
+ *
15
+ * _cacheId 用于 actions/cache 在不同 job 之间传递数据
16
+ *
17
+ * 使用示例:
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
+ * }
29
+ * ```
30
+ */
31
+ @Injectable({ scope: Scope.DEFAULT })
32
+ export class OutputService {
33
+ protected outputs: Record<string, string> = {};
34
+ protected cacheId: string = randomUUID();
35
+
36
+ /**
37
+ * 设置单个输出值
38
+ */
39
+ set(key: string, value: string | number | boolean): void {
40
+ this.outputs[key] = String(value);
41
+ }
42
+
43
+ /**
44
+ * 批量设置输出值
45
+ */
46
+ setAll(values: Record<string, string | number | boolean>): void {
47
+ for (const [key, value] of Object.entries(values)) {
48
+ this.set(key, value);
49
+ }
50
+ }
51
+
52
+ /**
53
+ * 获取所有输出值
54
+ */
55
+ getAll(): Record<string, string> {
56
+ return { ...this.outputs };
57
+ }
58
+
59
+ /**
60
+ * 清空所有输出值
61
+ */
62
+ clear(): void {
63
+ this.outputs = {};
64
+ }
65
+
66
+ /**
67
+ * 输出所有值到 stdout(带标记格式)
68
+ * 通常在命令执行完成后调用
69
+ * _cacheId 会被 actions 捕获并用于 actions/cache
70
+ */
71
+ flush(): void {
72
+ if (Object.keys(this.outputs).length === 0) {
73
+ return;
74
+ }
75
+
76
+ // 输出到 stdout,包含 cacheId 供 actions/cache 使用
77
+ const outputWithCache = { ...this.outputs, _cacheId: this.cacheId };
78
+ const json = JSON.stringify(outputWithCache);
79
+ console.log(`${OUTPUT_MARKER_START}${json}${OUTPUT_MARKER_END}`);
80
+ }
81
+
82
+ /**
83
+ * 检查是否有输出值
84
+ */
85
+ hasOutputs(): boolean {
86
+ return Object.keys(this.outputs).length > 0;
87
+ }
88
+
89
+ /**
90
+ * 获取当前 cacheId
91
+ */
92
+ getCacheId(): string {
93
+ return this.cacheId;
94
+ }
95
+ }
96
+
97
+ export { OUTPUT_MARKER_START, OUTPUT_MARKER_END };