SDK 扩展开发指南

📑 目录

这是「GSD 全景代码解析」专题的第 46 篇。

在前 45 篇文章中,我们系统梳理了 GSD 的命令系统、工作流编排层、Agent 执行层、上下文工程体系、SDK 核心模块、钩子系统和引擎层。但有一个关键问题尚未深入探讨:当 GSD 内置的 15 个 Transport、标准状态处理器和固定钩子集无法满足特定场景需求时,开发者该如何扩展 SDK?

答案就是SDK 扩展机制——GSD 在设计之初就遵循 Open/Closed 原则,通过清晰的接口契约和插件生命周期,让开发者能够在不修改核心代码的前提下,为 SDK 注入新的能力。


一、SDK 扩展点概述

1.1 为什么需要扩展机制

GSD SDK 虽然已经覆盖了 15+ 个 AI Coding 运行时和完整的项目生命周期,但在实际落地中,团队往往面临以下个性化需求:

场景说明对应扩展点
新运行时接入公司自研的 AI IDE 或内部工具Transport
自定义状态流转团队特定的审批流程或质量门禁State Processor
审计与合规需要记录每次工具调用的完整日志Plugin
外部系统集成与 Jira、Confluence、企业微信等打通Plugin
自定义验证规则团队编码规范自动化检查State Processor + Plugin

如果这些需求都通过修改 SDK 源码来实现,将导致:

  • 维护成本激增:每次 SDK 升级都需要手动合并冲突
  • 测试覆盖面爆炸:核心代码与业务代码耦合,回归测试难以进行
  • 社区贡献壁垒:外部开发者无法便捷地共享自己的扩展

1.2 三大扩展接口

GSD SDK 提供了三个层级的扩展接口,分别对应不同的定制深度:

flowchart TB
    subgraph Transport_Layer["Transport 层"]
        T1[内置 Transport
Claude / Codex / Copilot ...] T2[自定义 Transport
CustomTransport] end subgraph Engine_Layer["引擎层"] E1[Phase Runner] E2[State Processor
默认 / 自定义] E3[Context Engine] end subgraph Plugin_Layer["Plugin 层"] P1[生命周期钩子] P2[事件订阅] P3[自定义工具注册] end T2 --> E1 E2 --> E1 P1 --> E1 P2 --> E3 P3 --> E1

Transport 扩展:lowest-level,负责将 SDK 的抽象调用翻译为特定运行时的原生指令。每一个新接入的 AI IDE 都需要实现一个 Transport。

State Processor 扩展:mid-level,负责自定义状态文件的解析、验证和转换逻辑。当你需要改变 STATE.mdROADMAP.md 的结构和流转规则时,扩展 State Processor。

Plugin 扩展:highest-level,负责在整个 SDK 生命周期中注入横切关注点(cross-cutting concerns),如日志、监控、审计、通知等。

1.3 扩展与核心代码的边界

GSD 对扩展接口有一个硬性约束:扩展代码不允许直接修改核心模块的内部状态。所有交互必须通过公开的 API 契约进行:

// 正确:通过公开 API 与 SDK 交互
class MyPlugin implements GSDPlugin {
  async onPhaseComplete(ctx: PhaseContext, result: PhaseResult) {
    await ctx.sdk.query('state.append', { key: 'audit.log', value: result.summary });
  }
}

// 错误:直接修改内部状态
class BadPlugin implements GSDPlugin {
  async onPhaseComplete(ctx: PhaseContext, result: PhaseResult) {
    (ctx.sdk as any)._internalState.phaseCount++; // 禁止!
  }
}

这条边界保证了核心引擎的稳定性,即使某个扩展出现崩溃,也不会导致整个 SDK 不可用。


二、添加新 Transport

2.1 Transport 接口契约

第 38 篇 中,我们介绍了 cli.ts 作为 CLI Transport 的主实现。所有 Transport 都必须实现以下接口:

// sdk/src/types.ts
export interface Transport {
  /** Transport 唯一标识 */
  readonly name: string;

  /** 检测当前环境是否支持该 Transport */
  detect(): boolean | Promise<boolean>;

  /** 执行一个 SDK 命令 */
  execute(command: string, args?: Record<string, unknown>): Promise<TransportResult>;

  /** 读取运行时上下文信息 */
  getRuntimeInfo(): RuntimeInfo;

  /** 发送提示词到运行时 */
  sendPrompt(prompt: string, options?: PromptOptions): Promise<PromptResult>;

  /** 注册工具调用处理器 */
  onToolUse(handler: ToolUseHandler): void;

  /** 清理资源 */
  dispose(): void | Promise<void>;
}

export interface TransportResult {
  success: boolean;
  output?: string;
  error?: string;
  exitCode?: number;
}

export interface RuntimeInfo {
  name: string;
  version: string;
  capabilities: string[];
  workspacePath: string;
}

2.2 实现自定义 Transport

假设你的团队开发了一个名为 Nova 的内部 AI IDE,需要为其编写 Transport。步骤如下:

第一步:创建 Transport 文件

// sdk/src/transports/nova.ts
import {
  Transport, TransportResult, RuntimeInfo,
  PromptOptions, PromptResult, ToolUseHandler
} from '../types';

export class NovaTransport implements Transport {
  readonly name = 'nova';
  private toolHandler?: ToolUseHandler;
  private novaRpcClient: NovaRpcClient;

  constructor() {
    this.novaRpcClient = new NovaRpcClient({
      endpoint: process.env.NOVA_RPC_ENDPOINT
    });
  }

  /** 检测当前是否在 Nova 环境中 */
  detect(): boolean {
    return !!process.env.NOVA_SESSION_ID && !!process.env.NOVA_RPC_ENDPOINT;
  }

  /** 获取运行时信息 */
  getRuntimeInfo(): RuntimeInfo {
    return {
      name: 'Nova',
      version: process.env.NOVA_VERSION || 'unknown',
      capabilities: ['Read', 'Write', 'Edit', 'Bash', 'Grep', 'CustomTool'],
      workspacePath: process.env.NOVA_WORKSPACE || process.cwd(),
    };
  }

  /** 执行 SDK 命令 */
  async execute(command: string, args?: Record<string, unknown>): Promise<TransportResult> {
    try {
      const result = await this.novaRpcClient.invoke('gsd.execute', { command, args });
      return {
        success: result.status === 'ok',
        output: result.stdout,
        error: result.stderr,
        exitCode: result.code,
      };
    } catch (err) {
      return {
        success: false,
        error: err instanceof Error ? err.message : String(err),
        exitCode: 1,
      };
    }
  }

  /** 发送提示词 */
  async sendPrompt(prompt: string, options?: PromptOptions): Promise<PromptResult> {
    const response = await this.novaRpcClient.invoke('prompt.send', {
      content: prompt,
      model: options?.model,
      temperature: options?.temperature ?? 0.7,
      maxTokens: options?.maxTokens,
    });

    return {
      content: response.content,
      usage: response.tokenUsage,
      finishReason: response.finishReason,
    };
  }

  /** 注册工具调用处理器 */
  onToolUse(handler: ToolUseHandler): void {
    this.toolHandler = handler;
    this.novaRpcClient.on('tool.use', async (toolCall) => {
      const result = await handler(toolCall.name, toolCall.arguments);
      await this.novaRpcClient.invoke('tool.result', { id: toolCall.id, result });
    });
  }

  /** 清理资源 */
  async dispose(): Promise<void> {
    await this.novaRpcClient.close();
  }
}

第二步:注册到 Transport 工厂

// sdk/src/transports/index.ts
import { ClaudeTransport } from './claude';
import { CodexTransport } from './codex';
import { CopilotTransport } from './copilot';
import { NovaTransport } from './nova'; // 新增

export const transports = [
  new ClaudeTransport(),
  new CodexTransport(),
  new CopilotTransport(),
  new NovaTransport(), // 注册
];

export function detectTransport(): Transport | undefined {
  for (const transport of transports) {
    if (transport.detect()) {
      return transport;
    }
  }
  return undefined;
}

第三步:在配置中启用

# gsd.yaml
sdk:
  transport: nova  # 显式指定使用 Nova Transport
  # 如果不指定,SDK 会按注册顺序自动检测

2.3 Transport 适配的常见问题

问题原因解决方案
工具调用格式不兼容不同运行时的 Tool Use 协议差异在 Transport 层做格式转换,不泄露到上层
路径格式差异Windows vs Unix vs 远程环境getRuntimeInfo().workspacePath 必须返回绝对路径
并发限制某些运行时对并发请求有限制在 Transport 内部实现请求队列
长连接断开RPC/WebSocket 可能中断实现自动重连和幂等请求

三、自定义状态处理器

3.1 State Processor 的作用

第 42 篇 中,我们介绍了 gsd-tools.ts 如何通过 ReadWrite 工具管理 STATE.md。State Processor 是这一逻辑的策略抽象层——它决定了:

  1. 如何解析状态文件(Markdown 表格、YAML frontmatter、JSON)
  2. 如何验证状态转换是否合法(状态机规则)
  3. 如何合并多个来源的状态更新(并发写冲突解决)

3.2 State Processor 接口

// sdk/src/types.ts
export interface StateProcessor {
  /** Processor 名称 */
  readonly name: string;

  /** 支持的文件模式 */
  readonly filePatterns: string[];

  /** 解析状态文件内容 */
  parse(content: string, filePath: string): StateSnapshot;

  /** 序列化状态对象为文件内容 */
  serialize(snapshot: StateSnapshot): string;

  /** 验证状态转换是否合法 */
  validateTransition(current: StateSnapshot, next: StateSnapshot): ValidationResult;

  /** 合并两个状态快照(用于解决并发冲突) */
  merge(base: StateSnapshot, local: StateSnapshot, remote: StateSnapshot): MergeResult;

  /** 获取状态变更的 human-readable diff */
  diff(oldSnapshot: StateSnapshot, newSnapshot: StateSnapshot): StateDiff[];
}

export interface StateSnapshot {
  version: number;
  metadata: Record<string, unknown>;
  sections: StateSection[];
}

export interface StateSection {
  id: string;
  title: string;
  status: 'pending' | 'active' | 'completed' | 'blocked';
  data: Record<string, unknown>;
}

3.3 实战:自定义 ROADMAP 状态处理器

假设你的团队使用 Jira 风格的史诗(Epic)/故事(Story)结构,需要将 ROADMAP.md 的状态流转与 Jira 工作流对齐:

// sdk/src/state-processors/jira-roadmap.ts
import { StateProcessor, StateSnapshot, StateSection, ValidationResult, MergeResult } from '../types';

/** Jira 风格的状态流转规则 */
const JIRA_TRANSITIONS: Record<string, string[]> = {
  'Backlog': ['Todo', 'Canceled'],
  'Todo': ['In Progress', 'Backlog'],
  'In Progress': ['In Review', 'Todo', 'Blocked'],
  'In Review': ['Done', 'In Progress'],
  'Blocked': ['In Progress', 'Canceled'],
  'Done': ['In Review'],
  'Canceled': ['Backlog'],
};

export class JiraRoadmapProcessor implements StateProcessor {
  readonly name = 'jira-roadmap';
  readonly filePatterns = ['ROADMAP.md', 'roadmap.md'];

  parse(content: string, filePath: string): StateSnapshot {
    const sections: StateSection[] = [];
    const lines = content.split('\n');
    let currentEpic: string | null = null;

    for (const line of lines) {
      const epicMatch = line.match(/^##\s+(.+)/);
      if (epicMatch) {
        currentEpic = epicMatch[1].trim();
        continue;
      }

      const rowMatch = line.match(/^\|\s*([^|]+)\|\s*([^|]+)\|\s*([^|]+)\|/);
      if (rowMatch && currentEpic) {
        const [, id, title, status] = rowMatch.map(s => s.trim());
        if (id === 'ID') continue; // 跳过表头

        sections.push({
          id: `${currentEpic}::${id}`,
          title,
          status: this.normalizeStatus(status),
          data: { epic: currentEpic, jiraId: id },
        });
      }
    }

    return {
      version: this.extractVersion(content),
      metadata: { source: filePath, processor: this.name },
      sections,
    };
  }

  serialize(snapshot: StateSnapshot): string {
    const epicMap = new Map<string, StateSection[]>();
    for (const section of snapshot.sections) {
      const epic = (section.data.epic as string) || 'Uncategorized';
      if (!epicMap.has(epic)) epicMap.set(epic, []);
      epicMap.get(epic)!.push(section);
    }

    const lines: string[] = [`# ROADMAP v${snapshot.version}`, ''];
    for (const [epic, stories] of epicMap) {
      lines.push(`## ${epic}`, '');
      lines.push('| ID | Title | Status |');
      lines.push('|---|---|---|');
      for (const story of stories) {
        const jiraId = story.data.jiraId as string;
        lines.push(`| ${jiraId} | ${story.title} | ${story.status} |`);
      }
      lines.push('');
    }
    return lines.join('\n');
  }

  validateTransition(current: StateSnapshot, next: StateSnapshot): ValidationResult {
    const errors: string[] = [];

    for (const nextSection of next.sections) {
      const currentSection = current.sections.find(s => s.id === nextSection.id);
      if (!currentSection) continue; // 新增 section,允许

      const oldStatus = currentSection.status;
      const newStatus = nextSection.status;

      if (oldStatus === newStatus) continue;

      const allowed = JIRA_TRANSITIONS[oldStatus] || [];
      if (!allowed.includes(newStatus)) {
        errors.push(
          `非法状态转换: ${nextSection.id} 不能从 "${oldStatus}" 变为 "${newStatus}". 允许的目标: [${allowed.join(', ')}]`
        );
      }
    }

    return { valid: errors.length === 0, errors };
  }

  merge(base: StateSnapshot, local: StateSnapshot, remote: StateSnapshot): MergeResult {
    const merged: StateSnapshot = {
      version: Math.max(base.version, local.version, remote.version) + 1,
      metadata: { ...base.metadata, merged: true },
      sections: [],
    };

    const allIds = new Set([...base.sections, ...local.sections, ...remote.sections].map(s => s.id));
    const conflicts: string[] = [];

    for (const id of allIds) {
      const baseSec = base.sections.find(s => s.id === id);
      const localSec = local.sections.find(s => s.id === id);
      const remoteSec = remote.sections.find(s => s.id === id);

      if (!localSec && !remoteSec) continue; // 三方都删除
      if (!localSec) { merged.sections.push(remoteSec!); continue; }
      if (!remoteSec) { merged.sections.push(localSec!); continue; }
      if (localSec.status === remoteSec.status) {
        merged.sections.push(localSec);
        continue;
      }

      // 冲突:local 和 remote 都修改了状态但不同
      conflicts.push(id);
      // 策略:优先采用版本号更高的
      merged.sections.push(localSec.version >= remoteSec.version ? localSec : remoteSec);
    }

    return { snapshot: merged, conflicts: conflicts.length > 0 ? conflicts : undefined };
  }

  diff(oldSnapshot: StateSnapshot, newSnapshot: StateSnapshot): StateDiff[] {
    const diffs: StateDiff[] = [];
    const oldMap = new Map(oldSnapshot.sections.map(s => [s.id, s]));
    const newMap = new Map(newSnapshot.sections.map(s => [s.id, s]));

    for (const [id, newSec] of newMap) {
      const oldSec = oldMap.get(id);
      if (!oldSec) {
        diffs.push({ type: 'added', sectionId: id, message: `新增: ${newSec.title}` });
      } else if (oldSec.status !== newSec.status) {
        diffs.push({
          type: 'modified',
          sectionId: id,
          message: `状态变更: ${oldSec.status}${newSec.status}`,
          oldValue: oldSec.status,
          newValue: newSec.status,
        });
      }
    }

    for (const [id, oldSec] of oldMap) {
      if (!newMap.has(id)) {
        diffs.push({ type: 'removed', sectionId: id, message: `删除: ${oldSec.title}` });
      }
    }

    return diffs;
  }

  private normalizeStatus(status: string): string {
    const map: Record<string, string> = {
      'backlog': 'Backlog', 'todo': 'Todo',
      'in progress': 'In Progress', 'in-review': 'In Review',
      'done': 'Done', 'completed': 'Done',
      'blocked': 'Blocked', 'canceled': 'Canceled',
    };
    return map[status.toLowerCase()] || status;
  }

  private extractVersion(content: string): number {
    const match = content.match(/# ROADMAP v(\d+)/);
    return match ? parseInt(match[1], 10) : 1;
  }
}

3.4 注册自定义 State Processor

// sdk/src/state-processors/index.ts
import { DefaultStateProcessor } from './default';
import { JiraRoadmapProcessor } from './jira-roadmap';

export const stateProcessors: StateProcessor[] = [
  new JiraRoadmapProcessor(),
  new DefaultStateProcessor(), // 作为 fallback
];

export function resolveProcessor(filePath: string): StateProcessor {
  for (const processor of stateProcessors) {
    if (processor.filePatterns.some(p => filePath.endsWith(p))) {
      return processor;
    }
  }
  return stateProcessors[stateProcessors.length - 1];
}

四、插件机制设计

4.1 Plugin 生命周期

Plugin 是最高层级的扩展机制,它可以在 SDK 的完整生命周期中注入逻辑:

sequenceDiagram
    participant U as 用户代码
    participant SDK as GSD SDK
    participant P as Plugin
    participant T as Transport

    U->>SDK: sdk.loadPlugins([myPlugin])
    SDK->>P: plugin.load(config)
    P-->>SDK: 注册感兴趣的事件

    U->>SDK: sdk.runPhase('plan')
    SDK->>P: onPhaseStart(ctx)
    SDK->>T: sendPrompt(prompt)
    T-->>SDK: response
    SDK->>P: onPhaseComplete(ctx, result)

    U->>SDK: sdk.query('state.read')
    SDK->>P: onQuery(ctx, query, result)

    U->>SDK: sdk.dispose()
    SDK->>P: plugin.destroy()

4.2 Plugin API 契约

// sdk/src/types.ts
export interface GSDPlugin {
  /** 插件名称 */
  readonly name: string;

  /** 插件版本 */
  readonly version: string;

  /** 加载插件时调用 */
  load?(config: PluginConfig): void | Promise<void>;

  /** Phase 开始时调用 */
  onPhaseStart?(ctx: PhaseContext): void | Promise<void>;

  /** Phase 完成时调用 */
  onPhaseComplete?(ctx: PhaseContext, result: PhaseResult): void | Promise<void>;

  /** Phase 发生错误时调用 */
  onPhaseError?(ctx: PhaseContext, error: Error): void | Promise<void>;

  /** 查询执行后调用 */
  onQuery?(ctx: QueryContext, query: string, result: unknown): void | Promise<void>;

  /** 工具调用前后调用 */
  beforeToolUse?(ctx: ToolContext, toolName: string, args: unknown): void | Promise<void>;
  afterToolUse?(ctx: ToolContext, toolName: string, result: unknown): void | Promise<void>;

  /** 清理资源 */
  destroy?(): void | Promise<void>;
}

export interface PluginConfig {
  enabled: boolean;
  options: Record<string, unknown>;
}

4.3 实战:开发一个日志审计插件

以下是一个完整的审计插件示例,它会将每次 Phase 执行和工具调用的关键信息写入审计日志:

// plugins/audit-logger.ts
import {
  GSDPlugin, PhaseContext, PhaseResult,
  ToolContext, PluginConfig
} from '@gsd/sdk';
import { appendFile, mkdir } from 'fs/promises';
import { dirname, join } from 'path';

interface AuditEntry {
  timestamp: string;
  type: 'phase' | 'tool' | 'query';
  sessionId: string;
  details: Record<string, unknown>;
}

export class AuditLoggerPlugin implements GSDPlugin {
  readonly name = 'audit-logger';
  readonly version = '1.0.0';

  private logDir: string;
  private sessionId: string;
  private logFile: string;
  private buffer: AuditEntry[] = [];
  private flushInterval?: NodeJS.Timeout;

  constructor() {
    this.sessionId = this.generateSessionId();
    this.logDir = join(process.cwd(), '.gsd', 'audit-logs');
    this.logFile = join(this.logDir, `${this.sessionId}.jsonl`);
  }

  async load(config: PluginConfig): Promise<void> {
    await mkdir(this.logDir, { recursive: true });
    const flushMs = (config.options.flushIntervalMs as number) || 5000;
    this.flushInterval = setInterval(() => this.flush(), flushMs);
  }

  async onPhaseStart(ctx: PhaseContext): Promise<void> {
    this.buffer.push({
      timestamp: new Date().toISOString(),
      type: 'phase',
      sessionId: this.sessionId,
      details: {
        event: 'start',
        phase: ctx.phaseName,
        workflow: ctx.workflowId,
        runtime: ctx.transport.name,
      },
    });
  }

  async onPhaseComplete(ctx: PhaseContext, result: PhaseResult): Promise<void> {
    this.buffer.push({
      timestamp: new Date().toISOString(),
      type: 'phase',
      sessionId: this.sessionId,
      details: {
        event: 'complete',
        phase: ctx.phaseName,
        durationMs: result.durationMs,
        success: result.success,
        outputLength: result.output?.length,
      },
    });
  }

  async beforeToolUse(ctx: ToolContext, toolName: string, args: unknown): Promise<void> {
    this.buffer.push({
      timestamp: new Date().toISOString(),
      type: 'tool',
      sessionId: this.sessionId,
      details: {
        event: 'before',
        tool: toolName,
        args: this.sanitizeArgs(args),
      },
    });
  }

  async afterToolUse(ctx: ToolContext, toolName: string, result: unknown): Promise<void> {
    this.buffer.push({
      timestamp: new Date().toISOString(),
      type: 'tool',
      sessionId: this.sessionId,
      details: {
        event: 'after',
        tool: toolName,
        resultType: typeof result,
      },
    });
  }

  async destroy(): Promise<void> {
    if (this.flushInterval) {
      clearInterval(this.flushInterval);
    }
    await this.flush();
  }

  /** 将缓冲区写入磁盘 */
  private async flush(): Promise<void> {
    if (this.buffer.length === 0) return;
    const lines = this.buffer.map(e => JSON.stringify(e)).join('\n') + '\n';
    this.buffer = [];
    await appendFile(this.logFile, lines);
  }

  /** 生成会话 ID */
  private generateSessionId(): string {
    return `gsd-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
  }

  /** 脱敏处理:移除敏感参数 */
  private sanitizeArgs(args: unknown): unknown {
    if (typeof args !== 'object' || args === null) return args;
    const sensitiveKeys = ['token', 'password', 'secret', 'apiKey'];
    const sanitized: Record<string, unknown> = {};
    for (const [key, value] of Object.entries(args as Record<string, unknown>)) {
      if (sensitiveKeys.includes(key.toLowerCase())) {
        sanitized[key] = '***REDACTED***';
      } else {
        sanitized[key] = value;
      }
    }
    return sanitized;
  }
}

4.4 插件注册与配置

// gsd.config.ts
import { defineConfig } from '@gsd/sdk';
import { AuditLoggerPlugin } from './plugins/audit-logger';

export default defineConfig({
  plugins: [
    {
      plugin: new AuditLoggerPlugin(),
      config: {
        enabled: true,
        options: {
          flushIntervalMs: 3000,
        },
      },
    },
  ],
});

五、扩展开发最佳实践

5.1 向后兼容策略

SDK 的版本迭代可能导致接口变化。扩展代码应当遵循以下兼容策略:

// 使用类型守卫检测 API 版本
function isNewAPI(sdk: unknown): sdk is GSDSDK_v2 {
  return typeof sdk === 'object'
    && sdk !== null
    && 'getAPIVersion' in sdk
    && (sdk as any).getAPIVersion() >= 2;
}

class CompatiblePlugin implements GSDPlugin {
  async onPhaseComplete(ctx: PhaseContext, result: PhaseResult) {
    if (isNewAPI(ctx.sdk)) {
      // 使用 v2 API
      await ctx.sdk.telemetry.emit('phase.complete', result);
    } else {
      // 降级到 v1 API
      await ctx.sdk.query('state.append', {
        key: 'audit.log',
        value: JSON.stringify(result),
      });
    }
  }
}

5.2 错误处理

扩展代码的错误绝不能影响主流程。所有扩展方法都应包装在 try-catch 中:

class SafePlugin implements GSDPlugin {
  private async safeCall<T>(
    fn: () => Promise<T>,
    fallback?: T
  ): Promise<T | undefined> {
    try {
      return await fn();
    } catch (err) {
      // 记录到插件内部日志,不抛给 SDK
      console.error(`[${this.name}] 扩展执行失败:`, err);
      return fallback;
    }
  }

  async onPhaseComplete(ctx: PhaseContext, result: PhaseResult) {
    await this.safeCall(async () => {
      await this.sendNotification(ctx, result);
    });
  }
}

5.3 测试策略

扩展代码应当独立可测,不依赖真实的 SDK 实例:

// __tests__/audit-logger.spec.ts
import { AuditLoggerPlugin } from '../plugins/audit-logger';
import { mkdtempSync, readFileSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';

describe('AuditLoggerPlugin', () => {
  let plugin: AuditLoggerPlugin;
  let tmpDir: string;

  beforeEach(() => {
    tmpDir = mkdtempSync(join(tmpdir(), 'gsd-test-'));
    plugin = new AuditLoggerPlugin();
    // 通过环境变量或依赖注入覆盖日志目录
    (plugin as any).logDir = join(tmpDir, 'audit');
    (plugin as any).logFile = join((plugin as any).logDir, 'test.jsonl');
  });

  it('应记录 phase 开始事件', async () => {
    await plugin.load({ enabled: true, options: {} });
    await plugin.onPhaseStart({
      phaseName: 'plan',
      workflowId: 'wf-001',
      transport: { name: 'claude' } as any,
    } as any);
    await plugin.destroy();

    const logContent = readFileSync((plugin as any).logFile, 'utf-8');
    const entries = logContent.trim().split('\n').map(line => JSON.parse(line));
    expect(entries).toHaveLength(1);
    expect(entries[0].type).toBe('phase');
    expect(entries[0].details.event).toBe('start');
  });
});

5.4 性能考虑

扩展代码运行在主线程中,不当的实现会拖慢整个 SDK:

反模式影响正确做法
同步 I/O 操作阻塞事件循环使用 fs/promises 或流式 API
未节制的日志输出磁盘 I/O 瓶颈批量写入 + 缓冲区
高频网络请求延迟叠加合并请求或使用本地队列
深度克隆大对象CPU 占用高使用引用或浅拷贝,必要时才深克隆

六、小结

本文系统介绍了 GSD SDK 的三大扩展机制,从接口契约到实战代码,从注册方式到最佳实践:

Transport 扩展让 GSD 能够接入任意新的 AI Coding 运行时。只要实现 detect()execute()sendPrompt() 等六个方法,即可完成对接。Transport 层的核心价值是隔离运行时的差异性,让上层引擎可以无感知地切换底层环境。

State Processor 扩展赋予团队自定义状态流转规则的能力。通过 parse()serialize()validateTransition()merge() 四个方法,你可以将 Jira、Linear、Asana 等外部系统的状态模型映射到 GSD 的 STATE.mdROADMAP.md 中,实现双向同步。

Plugin 扩展是最灵活的横切关注点注入机制。通过生命周期钩子(onPhaseStartonPhaseCompletebeforeToolUseafterToolUse 等),你可以在不修改核心代码的前提下,为 SDK 增加审计、监控、通知、合规检查等能力。

最佳实践总结

  1. 边界清晰:扩展代码只通过公开 API 与 SDK 交互,绝不触碰内部状态
  2. 失败隔离:所有扩展方法都应有错误兜底,避免一个插件崩溃拖垮整个系统
  3. 向后兼容:使用类型守卫检测 API 版本, graceful degradation
  4. 独立可测:通过依赖注入和 Mock 对象,让扩展代码可以脱离真实 SDK 进行单元测试
  5. 性能敏感:避免同步 I/O、未节制日志和深度克隆,必要时使用缓冲区和批量操作

GSD 的扩展机制不是事后补丁,而是架构设计的一等公民。Open/Closed 原则在这里得到了充分体现:核心引擎对扩展开放,对修改封闭。


下一篇预告:「GSD 全景代码解析」专题至此已完成对命令系统、工作流编排层、Agent 系统、上下文工程、SDK 核心模块、钩子系统、引擎层和扩展机制的全面解析。后续篇章将进入实战案例与高级主题,通过真实的项目落地场景,展示如何将 GSD 的理论知识转化为生产力。敬请期待第 47 篇《GSD 实战:从零搭建一个完整的 AI 驱动项目》——我们将以一个小型 SaaS 项目为例,完整演示从需求分析到生产部署的 GSD 全流程。