22.5 插件发布与维护

21 分钟阅读

22.5.1 插件打包#

基本打包配置#

// package.json { "name": "my-claude-plugin", "version": "1.0.0", "description": "My Claude Code Plugin", "main": "dist/index.js", "types": "dist/index.d.ts", "files": [ "dist", "plugin.yaml" ], "scripts": { "build": "tsc", "prepack": "npm run build", "pack": "npm pack", "publish": "npm publish" }, "keywords": [ "claude-code", "plugin" ], "author": "Your Name", "license": "MIT", "devDependencies": { "@claude-code/plugin-sdk": "^1.0.0", "typescript": "^4.9.0" } }

TypeScript 配置#

bash
json

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "moduleResolution": "node"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}

### 插件清单

# plugin.yaml
name: my-claude-plugin
version: 1.0.0
description: My Claude Code Plugin
author: Your Name
license: MIT
homepage: https://github.com/yourname/my-claude-plugin
repository: https://github.com/yourname/my-claude-plugin
# 插件入口
main: dist/index.js
types: dist/index.d.ts
# 插件权限
permissions:
file:
- read: "/"
network:
- https: ["api.example.com"]
# 插件依赖
dependencies:
claude-code: ">=1.0.0"
# 插件元数据
metadata:
category: development
tags:
- code-generation
- productivity
keywords:
- plugin
- claude-code

打包脚本#

bash
bash

#!/bin/bash
# scripts/build.sh

echo "Building plugin..."

# 清理构建目录
rm -rf dist

# 编译 TypeScript
npm run build

# 复制插件清单
cp plugin.yaml dist/

# 复制 README
cp README.md dist/

# 复制 LICENSE
cp LICENSE dist/ 2>/dev/null || true

echo "Build complete!"

#!/bin/bash
# scripts/pack.sh
echo "Packing plugin..."
# 构建
./scripts/build.sh
# 打包
cd dist
npm pack
# 移动包到项目根目录
mv *.tgz ../
echo "Pack complete!"

22.5.2 版本管理#

语义化版本#

bash
typescript

// src/version.ts

/**
 * 语义化版本
 */
export class SemanticVersion {
  major: number;
  minor: number;
  patch: number;
  prerelease?: string;
  build?: string;

  constructor(version: string) {
    const parsed = this.parse(version);
    this.major = parsed.major;
    this.minor = parsed.minor;
    this.patch = parsed.patch;
    this.prerelease = parsed.prerelease;
    this.build = parsed.build;
  }

  /**
   * 解析版本字符串
   */
  private parse(version: string): {
    major: number;
    minor: number;
    patch: number;
    prerelease?: string;
    build?: string;
  } {
    const regex = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+))?(?:\+([0-9A-Za-z-]+))?$/;
    const match = version.match(regex);

    if (!match) {
      throw new Error(`Invalid version: ${version}`);
    }

    return {
      major: parseInt(match[1]),
      minor: parseInt(match[2]),
      patch: parseInt(match[3]),
      prerelease: match[4],
      build: match[5]
    };
  }

  /**
   * 转换为字符串
   */
  toString(): string {
    let version = `${this.major}.${this.minor}.${this.patch}`;

    if (this.prerelease) {
      version += `-${this.prerelease}`;
    }

    if (this.build) {
      version += `+${this.build}`;
    }

    return version;
  }

  /**
   * 主版本升级
   */
  bumpMajor(): SemanticVersion {
    return new SemanticVersion(
      `${this.major + 1}.0.0`
    );
  }

  /**
   * 次版本升级
   */
  bumpMinor(): SemanticVersion {
    return new SemanticVersion(
      `${this.major}.${this.minor + 1}.0`
    );
  }

  /**
   * 补丁版本升级
   */
  bumpPatch(): SemanticVersion {
    return new SemanticVersion(
      `${this.major}.${this.minor}.${this.patch + 1}`
    );
  }

  /**
   * 比较版本
   */
  compare(other: SemanticVersion): number {
    if (this.major !== other.major) {
      return this.major - other.major;
    }

    if (this.minor !== other.minor) {
      return this.minor - other.minor;
    }

    if (this.patch !== other.patch) {
      return this.patch - other.patch;
    }

    return 0;
  }

  /**
   * 检查是否大于
   */
  greaterThan(other: SemanticVersion): boolean {
    return this.compare(other) > 0;
  }

  /**
   * 检查是否小于
   */
  lessThan(other: SemanticVersion): boolean {
    return this.compare(other) < 0;
  }

  /**
   * 检查是否等于
   */
  equals(other: SemanticVersion): boolean {
    return this.compare(other) === 0;
  }
}

// 使用示例
const version = new SemanticVersion('1.2.3');
console.log(version.toString()); // 1.2.3

const nextMajor = version.bumpMajor();
console.log(nextMajor.toString()); // 2.0.0

const nextMinor = version.bumpMinor();
console.log(nextMinor.toString()); // 1.3.0

const nextPatch = version.bumpPatch();
console.log(nextPatch.toString()); // 1.2.4

const v1 = new SemanticVersion('1.2.3');
const v2 = new SemanticVersion('1.2.4');

console.log(v1.lessThan(v2)); // true
console.log(v2.greaterThan(v1)); // true

### 版本发布脚本

#!/bin/bash
# scripts/release.sh
VERSION=$1
if [ -z "$VERSION" ]; then
echo "Usage: ./scripts/release.sh <version>"
exit 1
fi
echo "Releasing version $VERSION..."
# 更新 package.json
npm version $VERSION --no-git-tag-version
# 更新 plugin.yaml
sed -i.bak "s/^version: .*/version: $VERSION/" plugin.yaml
rm plugin.yaml.bak
# 构建和打包
./scripts/pack.sh
# 提交更改
git add package.json plugin.yaml
git commit -m "Release version $VERSION"
# 创建标签
git tag -a "v$VERSION" -m "Release version $VERSION"
# 推送到远程
git push origin main
git push origin "v$VERSION"
# 发布到 npm
npm publish
echo "Release $VERSION complete!"

22.5.3 文档生成#

API 文档生成#

bash
typescript

// scripts/generate-docs.ts

import { Project, TSConfigReader, TypeDocReader } from 'typedoc';
import { MarkdownRenderer } from 'typedoc-plugin-markdown';

/**
 * 生成 API 文档
 */
async function generateDocs() {
  const project = new Project({
    tsconfig: 'tsconfig.json',
    entryPoints: ['src/index.ts'],
    readme: 'README.md',
    out: 'docs/api',
    plugin: [TypeDocReader, MarkdownRenderer],
    exclude: ['**/*.test.ts', '**/node_modules/**'],
    theme: 'markdown',
    gitRevision: 'main'
  });

  await project.generate();
}

generateDocs().catch(console.error);

### README 模板

# My Claude Code Plugin
[![NPM Version](https://img.shields.io/npm/v/my-claude-plugin.svg)](https://www.npmjs.com/package/my-claude-plugin)
[![License](https://img.shields.io/npm/l/my-claude-plugin.svg)](LICENSE)
My Claude Code Plugin is a powerful plugin for Claude Code that provides [brief description].
## Features
- Feature 1
- Feature 2
- Feature 3
## Installation
````bash
`bash

claude plugin install my-claude-plugin

```> Or install from npm:

bash

npm install -g my-claude-plugin

Usage#

Basic Usage#

typescript
````typescript import { MyPlugin } from 'my-claude-plugin'; const plugin = new MyPlugin(); await plugin.initialize({}); await plugin.start(); ```### Advanced Usage ``` typescript const plugin = new MyPlugin({ option1: 'value1', option2: 'value2' }); await plugin.initialize(config); await plugin.start(); ## Configuration The plugin can be configured with the following options: | Option | Type | Default | Description | |--------|------|---------|-------------| | option1 | string | 'default' | Description of option1 | | option2 | number | 100 | Description of option2 | ## API ### Methods #### `initialize(config: PluginConfig): Promise<void>` Initializes the plugin with the given configuration. #### `start(): Promise<void>` Starts the plugin. #### `stop(): Promise<void>` Stops the plugin. ## Development ### Prerequisites > - Node.js >= 14 > - npm >= 6 ### Setup ````bash ````bash # Clone the repository git clone https://github.com/yourname/my-claude-plugin.git cd my-claude-plugin # Install dependencies npm install # Build the project npm run build ```### Testing ``` bash # Run tests npm test # Run tests with coverage npm run test:coverage ### Building ````bash ````bash # Build the project npm run build # Pack the project npm run pack ```## Contributing Contributions are welcome! Please feel free to submit a Pull Request. ## License MIT © [Your Name] ## Support For support, please open an issue on GitHub or contact [your-email@example.com]. ``` ## 22.5.4 持续集成 ### GitHub Actions 配置 # .github/workflows/ci.yml name: CI on: push: branches: [ main, develop ] pull_request: branches: [ main, develop ] jobs: test: runs-on: ubuntu-latest strategy: matrix: node-version: [14.x, 16.x, 18.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} cache: 'npm' - name: Install dependencies run: npm ci - name: Run linter run: npm run lint - name: Run tests run: npm test - name: Generate coverage run: npm run test:coverage - name: Upload coverage uses: codecov/codecov-action@v2 with: files: ./coverage/lcov.info build: needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Use Node.js uses: actions/setup-node@v2 with: node-version: '18.x' cache: 'npm' - name: Install dependencies run: npm ci - name: Build run: npm run build - name: Pack run: npm run pack - name: Upload artifact uses: actions/upload-artifact@v2 with: name: package path: '*.tgz' ``` ### 发布工作流 ``` yaml # .github/workflows/release.yml name: Release on: push: tags: - 'v*' jobs: release: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Use Node.js uses: actions/setup-node@v2 with: node-version: '18.x' cache: 'npm' registry-url: 'https://registry.npmjs.org' - name: Install dependencies run: npm ci - name: Build run: npm run build - name: Run tests run: npm test - name: Publish to npm run: npm publish env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Create GitHub Release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ github.ref }} release_name: Release ${{ github.ref }} draft: false prerelease: false ## 22.5.5 错误监控 ### 错误追踪集成 // src/monitoring/error-tracker.ts /** * 错误追踪器 */ export class ErrorTracker { private errors: ErrorReport[] = []; private maxErrors: number = 100; /** * 追踪错误 */ track(error: Error, context?: ErrorContext): void { const report: ErrorReport = { id: this.generateId(), message: error.message, stack: error.stack, timestamp: new Date(), context: context || {} }; this.errors.push(report); // 限制错误数量 if (this.errors.length > this.maxErrors) { this.errors.shift(); } // 发送到错误监控服务 this.sendToMonitoringService(report); } /** * 生成 ID */ private generateId(): string { return `error-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } /** * 发送到监控服务 */ private sendToMonitoringService(report: ErrorReport): void { // 集成到 Sentry、Bugsnag 等 console.log('Sending error to monitoring service:', report.id); } /** * 获取错误报告 */ getReports(limit?: number): ErrorReport[] { if (limit) { return this.errors.slice(-limit); } return [...this.errors]; } /** * 清除错误报告 */ clearReports(): void { this.errors = []; } /** * 获取错误统计 */ getStats(): ErrorStats { const stats: ErrorStats = { total: this.errors.length, byType: {}, byHour: {} }; for (const error of this.errors) { // 按类型统计 const type = error.context.type || 'unknown'; stats.byType[type] = (stats.byType[type] || 0) + 1; // 按小时统计 const hour = error.timestamp.getHours(); stats.byHour[hour] = (stats.byHour[hour] || 0) + 1; } return stats; } } /** * 错误报告 */ interface ErrorReport { id: string; message: string; stack?: string; timestamp: Date; context: ErrorContext; } /** * 错误上下文 */ interface ErrorContext { type?: string; plugin?: string; user?: string; [key: string]: any; } /** * 错误统计 */ interface ErrorStats { total: number; byType: Record<string, number>; byHour: Record<number, number>; } // 使用示例 const tracker = new ErrorTracker(); try { // 可能出错的代码 throw new Error('Something went wrong'); } catch (error) { tracker.track(error, { type: 'runtime', plugin: 'my-plugin', user: 'user123' }); } // 获取错误报告 const reports = tracker.getReports(10); console.log('Recent errors:', reports); // 获取错误统计 const stats = tracker.getStats(); console.log('Error stats:', stats); ``` ### 性能监控 ``` typescript // src/monitoring/performance-monitor.ts /** * 性能监控器 */ export class PerformanceMonitor { private metrics: Map<string, Metric[]> = new Map(); private maxMetrics: number = 1000; /** * 记录指标 */ record(name: string, value: number, tags?: Record<string, string>): void { const metric: Metric = { name, value, timestamp: new Date(), tags: tags || {} }; if (!this.metrics.has(name)) { this.metrics.set(name, []); } const metrics = this.metrics.get(name)!; metrics.push(metric); // 限制指标数量 if (metrics.length > this.maxMetrics) { metrics.shift(); } } /** * 测量函数执行时间 */ async measure<T>(name: string, fn: () => Promise<T>, tags?: Record<string, string>): Promise<T> { const start = Date.now(); try { const result = await fn(); const duration = Date.now() - start; this.record(name, duration, tags); return result; } catch (error) { const duration = Date.now() - start; this.record(`${name}.error`, duration, { ...tags, error: error.message }); throw error; }

}

/**

  • 获取指标 */ getMetrics(name: string, limit?: number): Metric[] { const metrics = this.metrics.get(name);
bash
if (!metrics) {
bash
  return [];
}

if (limit) {
  return metrics.slice(-limit);
}

return [...metrics];

}

/**

  • 获取指标统计 */ getStats(name: string): MetricStats | null { const metrics = this.metrics.get(name);
bash
if (!metrics || metrics.length === 0) {
bash
  return null;
}

const values = metrics.map(m => m.value);
const sum = values.reduce((a, b) => a + b, 0);
const avg = sum / values.length;
const min = Math.min(...values);
const max = Math.max(...values);

// 计算百分位数
const sorted = [...values].sort((a, b) => a - b);
const p50 = sorted[Math.floor(sorted.length * 0.5)];
const p95 = sorted[Math.floor(sorted.length * 0.95)];
const p99 = sorted[Math.floor(sorted.length * 0.99)];

return {
  count: metrics.length,
  sum,
  avg,
  min,
  max,
  p50,
  p95,
  p99
};

}

/**

  • 清除指标 */ clearMetrics(name?: string): void { if (name) { this.metrics.delete(name); } else { this.metrics.clear(); } } }

/**

  • 指标 */ interface Metric { name: string; value: number; timestamp: Date; tags: Record<string, string>; }

/**

  • 指标统计 */ interface MetricStats { count: number; sum: number; avg: number; min: number; max: number; p50: number; p95: number; p99: number; }

// 使用示例 const monitor = new PerformanceMonitor();

// 记录指标 monitor.record('request.duration', 123, { method: 'GET', endpoint: '/api/users' });

// 测量函数执行时间 const result = await monitor.measure('database.query', async () => { // 数据库查询 return { id: 1, name: 'John' }; });

// 获取指标统计 const stats = monitor.getStats('request.duration'); console.log('Stats:', stats);

22.5.6 用户反馈#

反馈收集#

// src/feedback/feedback-collector.ts /**

  • 反馈收集器 / export class FeedbackCollector { private feedbacks: Feedback[] = []; /*
  • 收集反馈 / collect(feedback: Omit<Feedback, 'id' | 'timestamp'>): string { const newFeedback: Feedback = { id: this.generateId(), timestamp: new Date(), ...feedback }; this.feedbacks.push(newFeedback); // 发送到反馈服务 this.sendToFeedbackService(newFeedback); return newFeedback.id; } /*
  • 生成 ID / private generateId(): string { return feedback-${Date.now()}-${Math.random().toString(36).substr(2, 9)}; } /*
  • 发送到反馈服务 / private sendToFeedbackService(feedback: Feedback): void { // 发送到反馈服务 console.log('Sending feedback:', feedback.id); } /*
  • 获取反馈 / getFeedbacks(limit?: number): Feedback[] { if (limit) { return this.feedbacks.slice(-limit); } return [...this.feedbacks]; } /*
  • 获取反馈统计 / getStats(): FeedbackStats { const stats: FeedbackStats = { total: this.feedbacks.length, byType: {}, byRating: {}, averageRating: 0 }; let totalRating = 0; let ratingCount = 0; for (const feedback of this.feedbacks) { // 按类型统计 stats.byType[feedback.type] = (stats.byType[feedback.type] || 0) + 1; // 按评分统计 if (feedback.rating) { stats.byRating[feedback.rating] = (stats.byRating[feedback.rating] || 0) + 1; totalRating += feedback.rating; ratingCount++; } } // 计算平均评分 if (ratingCount > 0) { stats.averageRating = totalRating / ratingCount; } return stats; } } /*
  • 反馈 / interface Feedback { id: string; type: 'bug' | 'feature' | 'improvement' | 'other'; rating?: number; title: string; description: string; user?: string; timestamp: Date; } /*
  • 反馈统计 */ interface FeedbackStats { total: number; byType: Record<string, number>; byRating: Record<number, number>; averageRating: number; } // 使用示例 const collector = new FeedbackCollector(); // 收集反馈 const feedbackId = collector.collect({ type: 'feature', rating: 5, title: 'Add new feature', description: 'Please add this feature', user: 'user123' }); console.log('Feedback ID:', feedbackId); // 获取反馈统计 const stats = collector.getStats(); console.log('Feedback stats:', stats);
bash
### 用户调查

typescript

// src/feedback/survey.ts

/**

  • 用户调查 */ export class UserSurvey { private questions: SurveyQuestion[] = []; private responses: SurveyResponse[] = [];

/**

  • 添加问题 */ addQuestion(question: SurveyQuestion): void { this.questions.push(question); }

/**

  • 提交响应 */ submitResponse(response: Omit<SurveyResponse, 'id' | 'timestamp'>): string { const newResponse: SurveyResponse = { id: this.generateId(), timestamp: new Date(), ...response };
bash
this.responses.push(newResponse);
bash
// 发送到调查服务
this.sendToSurveyService(newResponse);

return newResponse.id;

}

/**

  • 生成 ID */ private generateId(): string { return response-${Date.now()}-${Math.random().toString(36).substr(2, 9)}; }

/**

  • 发送到调查服务 */ private sendToSurveyService(response: SurveyResponse): void { // 发送到调查服务 console.log('Sending survey response:', response.id); }

/**

  • 获取响应 */ getResponses(limit?: number): SurveyResponse[] { if (limit) { return this.responses.slice(-limit); } return [...this.responses]; }

/**

  • 分析响应 */ analyzeResponses(): SurveyAnalysis { const analysis: SurveyAnalysis = { totalResponses: this.responses.length, averageRating: 0, byQuestion: {} };
bash
let totalRating = 0;
bash
let ratingCount = 0;

for (const question of this.questions) {
  const questionResponses = this.responses.filter(
    r => r.answers[question.id] !== undefined
  );

  if (question.type === 'rating') {
    const ratings = questionResponses.map(
      r => r.answers[question.id] as number
    );

    const sum = ratings.reduce((a, b) => a + b, 0);
    const avg = ratings.length > 0 ? sum / ratings.length : 0;

    analysis.byQuestion[question.id] = {
      count: ratings.length,
      average: avg,
      min: Math.min(...ratings),
      max: Math.max(...ratings)
    };

    totalRating += sum;
    ratingCount += ratings.length;
  }
}

// 计算平均评分
if (ratingCount > 0) {
  analysis.averageRating = totalRating / ratingCount;
}

return analysis;

} }

/**

  • 调查问题 */ interface SurveyQuestion { id: string; type: 'text' | 'rating' | 'choice' | 'multiple'; question: string; options?: string[]; required: boolean; }

/**

  • 调查响应 */ interface SurveyResponse { id: string; userId?: string; answers: Record<string, any>; timestamp: Date; }

/**

  • 调查分析 */ interface SurveyAnalysis { totalResponses: number; averageRating: number; byQuestion: Record<string, any>; }

// 使用示例 const survey = new UserSurvey();

// 添加问题 survey.addQuestion({ id: 'q1', type: 'rating', question: 'How satisfied are you with the plugin?', required: true });

survey.addQuestion({ id: 'q2', type: 'text', question: 'What do you like most about the plugin?', required: false });

survey.addQuestion({ id: 'q3', type: 'choice', question: 'How often do you use the plugin?', options: ['Daily', 'Weekly', 'Monthly', 'Rarely'], required: true });

// 提交响应 const responseId = survey.submitResponse({ userId: 'user123', answers: { q1: 5, q2: 'Easy to use and powerful', q3: 'Daily' } });

console.log('Response ID:', responseId);

// 分析响应 const analysis = survey.analyzeResponses(); console.log('Survey analysis:', analysis);

标记本节教程为已读

记录您的学习进度,方便后续查看。