Token导航 LogoToken导航TokenDH.com
开发权限需确认github未标认证来源可访问clear审计通过

content-versioning内容版本控制

Agent Skill

用于辅助文档、README、Markdown、说明文和内容稿件的整理与改写。它适合让 Agent 提炼结构、补齐章节、统一术语、检查链接或把零散材料整理成可读文档。使用时应保留项目已有事实、命令和路径,不要把未确认的信息写成确定结论;涉及对外文案时,还需要控制语气,避免过度营销或夸大能力。

总安装

216

周安装

9

GitHub Stars

61

下载量

72
CodexClaudeCursorGemini CLI

安装说明

本站只整理中文说明和来源信息,不托管安装包,也不代用户安装。

GitHub

来源数

3

许可证

MIT

最后核验

2026-05-01

来源状态

来源可访问

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:content-versioning(内容版本控制)
来源仓库:https://github.com/melodic-software/claude-code-plugins
仓库路径:skills/content-versioning
安装命令:
npx skills add https://github.com/melodic-software/claude-code-plugins --skill content-versioning
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。不同来源提供的安装方式可能略有差异;本站展示可直接复制的安装命令,安装前请核对来源页面。

skills.shnpx skills
npx skills add https://github.com/melodic-software/claude-code-plugins --skill content-versioning

简介

为 CMS 内容实现草稿/发布分离与版本历史追踪功能。

  • 支持回滚、审计日志与合规性检查的版本控制策略。
  • 适用于需要多人协作与内容追溯的企业级内容管理系统。
  • 输出包含状态流转图与数据库 schema 的设计建议。
  • content-versioning 属于开发类 Skill,可作为该场景下的辅助能力补充。

SKILL.md

Content Versioning

Guidance for implementing version control, draft/publish workflows, and audit trails for CMS content.

When to Use This Skill

  • Implementing draft/publish workflows
  • Adding version history to content types
  • Building content rollback features
  • Creating audit trails for compliance
  • Comparing content versions

Versioning Strategies

Strategy 1: Separate Draft/Published Records

public class ContentItem
{
    public Guid Id { get; set; }
    public string ContentType { get; set; } = string.Empty;
    public ContentStatus Status { get; set; }

    // Version tracking
    public int Version { get; set; }
    public Guid? PublishedVersionId { get; set; }
    public Guid? DraftVersionId { get; set; }

    // Timestamps
    public DateTime CreatedUtc { get; set; }
    public DateTime ModifiedUtc { get; set; }
    public DateTime? PublishedUtc { get; set; }
}

public class ContentVersion
{
    public Guid Id { get; set; }
    public Guid ContentItemId { get; set; }
    public int VersionNumber { get; set; }

    // Snapshot of content at this version
    public string DataJson { get; set; } = string.Empty;

    // Metadata
    public string CreatedBy { get; set; } = string.Empty;
    public DateTime CreatedUtc { get; set; }
    public string? ChangeNote { get; set; }
    public bool IsPublished { get; set; }
}

public enum ContentStatus
{
    Draft,
    Published,
    Unpublished,
    Archived
}

Strategy 2: History Table Pattern

// Current content (always latest)
public class Article
{
    public Guid Id { get; set; }
    public string Title { get; set; } = string.Empty;
    public string Body { get; set; } = string.Empty;
    public int CurrentVersion { get; set; }
    public ContentStatus Status { get; set; }
}

// Automatic history tracking
public class ArticleHistory
{
    public Guid Id { get; set; }
    public Guid ArticleId { get; set; }
    public int VersionNumber { get; set; }

    // Copy of all fields at this version
    public string Title { get; set; } = string.Empty;
    public string Body { get; set; } = string.Empty;

    // Audit info
    public DateTime ValidFrom { get; set; }
    public DateTime ValidTo { get; set; }
    public string ModifiedBy { get; set; } = string.Empty;
    public ChangeType ChangeType { get; set; }
}

public enum ChangeType
{
    Created,
    Updated,
    Published,
    Unpublished,
    Deleted
}

Strategy 3: Event Sourcing

public abstract class ContentEvent
{
    public Guid Id { get; set; }
    public Guid ContentItemId { get; set; }
    public DateTime OccurredUtc { get; set; }
    public string UserId { get; set; } = string.Empty;
    public int SequenceNumber { get; set; }
}

public class ContentCreatedEvent : ContentEvent
{
    public string ContentType { get; set; } = string.Empty;
    public string InitialDataJson { get; set; } = string.Empty;
}

public class ContentUpdatedEvent : ContentEvent
{
    public Dictionary<string, FieldChange> Changes { get; set; } = new();
}

public class ContentPublishedEvent : ContentEvent
{
    public int PublishedVersion { get; set; }
}

public class FieldChange
{
    public object? OldValue { get; set; }
    public object? NewValue { get; set; }
}

Draft/Publish Workflow

Basic Implementation

public class ContentPublishingService
{
    public async Task<ContentItem> CreateDraftAsync(
        string contentType,
        object data,
        string userId)
    {
        var item = new ContentItem
        {
            Id = Guid.NewGuid(),
            ContentType = contentType,
            Status = ContentStatus.Draft,
            Version = 1,
            CreatedUtc = DateTime.UtcNow,
            ModifiedUtc = DateTime.UtcNow
        };

        var version = new ContentVersion
        {
            Id = Guid.NewGuid(),
            ContentItemId = item.Id,
            VersionNumber = 1,
            DataJson = JsonSerializer.Serialize(data),
            CreatedBy = userId,
            CreatedUtc = DateTime.UtcNow,
            IsPublished = false
        };

        item.DraftVersionId = version.Id;

        await _repository.AddAsync(item);
        await _versionRepository.AddAsync(version);

        return item;
    }

    public async Task PublishAsync(Guid contentItemId, string userId)
    {
        var item = await _repository.GetAsync(contentItemId);
        if (item == null || item.DraftVersionId == null)
            throw new InvalidOperationException("No draft to publish");

        var draft = await _versionRepository.GetAsync(item.DraftVersionId.Value);

        // Create published version from draft
        var published = new ContentVersion
        {
            Id = Guid.NewGuid(),
            ContentItemId = item.Id,
            VersionNumber = item.Version + 1,
            DataJson = draft!.DataJson,
            CreatedBy = userId,
            CreatedUtc = DateTime.UtcNow,
            IsPublished = true
        };

        await _versionRepository.AddAsync(published);

        // Update content item
        item.Version = published.VersionNumber;
        item.PublishedVersionId = published.Id;
        item.Status = ContentStatus.Published;
        item.PublishedUtc = DateTime.UtcNow;
        item.ModifiedUtc = DateTime.UtcNow;

        await _repository.UpdateAsync(item);

        // Raise event
        await _mediator.Publish(new ContentPublishedEvent(item.Id));
    }

    public async Task UnpublishAsync(Guid contentItemId, string userId)
    {
        var item = await _repository.GetAsync(contentItemId);
        if (item == null)
            throw new InvalidOperationException("Content not found");

        item.Status = ContentStatus.Unpublished;
        item.PublishedVersionId = null;
        item.ModifiedUtc = DateTime.UtcNow;

        await _repository.UpdateAsync(item);
        await _mediator.Publish(new ContentUnpublishedEvent(item.Id));
    }
}

Simultaneous Draft and Published

public class ContentQueryService
{
    public async Task<ContentVersion?> GetPublishedAsync(Guid contentItemId)
    {
        var item = await _repository.GetAsync(contentItemId);
        if (item?.PublishedVersionId == null)
            return null;

        return await _versionRepository.GetAsync(item.PublishedVersionId.Value);
    }

    public async Task<ContentVersion?> GetDraftAsync(Guid contentItemId)
    {
        var item = await _repository.GetAsync(contentItemId);
        if (item?.DraftVersionId == null)
            return null;

        return await _versionRepository.GetAsync(item.DraftVersionId.Value);
    }

    public async Task<ContentVersion?> GetLatestAsync(
        Guid contentItemId,
        bool preferDraft = false)
    {
        var item = await _repository.GetAsync(contentItemId);
        if (item == null) return null;

        if (preferDraft && item.DraftVersionId != null)
            return await _versionRepository.GetAsync(item.DraftVersionId.Value);

        if (item.PublishedVersionId != null)
            return await _versionRepository.GetAsync(item.PublishedVersionId.Value);

        return null;
    }
}

Version History

Retrieving History

public async Task<List<ContentVersionSummary>> GetVersionHistoryAsync(
    Guid contentItemId,
    int page = 1,
    int pageSize = 20)
{
    return await _context.ContentVersions
        .Where(v => v.ContentItemId == contentItemId)
        .OrderByDescending(v => v.VersionNumber)
        .Skip((page - 1) * pageSize)
        .Take(pageSize)
        .Select(v => new ContentVersionSummary
        {
            Id = v.Id,
            VersionNumber = v.VersionNumber,
            CreatedBy = v.CreatedBy,
            CreatedUtc = v.CreatedUtc,
            ChangeNote = v.ChangeNote,
            IsPublished = v.IsPublished
        })
        .ToListAsync();
}

Rollback

public async Task RollbackToVersionAsync(
    Guid contentItemId,
    int targetVersion,
    string userId)
{
    var item = await _repository.GetAsync(contentItemId);
    var targetVersionRecord = await _versionRepository
        .GetByVersionNumberAsync(contentItemId, targetVersion);

    if (item == null || targetVersionRecord == null)
        throw new InvalidOperationException("Invalid rollback target");

    // Create new version from old data
    var rollbackVersion = new ContentVersion
    {
        Id = Guid.NewGuid(),
        ContentItemId = item.Id,
        VersionNumber = item.Version + 1,
        DataJson = targetVersionRecord.DataJson,
        CreatedBy = userId,
        CreatedUtc = DateTime.UtcNow,
        ChangeNote = $"Rolled back to version {targetVersion}",
        IsPublished = false
    };

    await _versionRepository.AddAsync(rollbackVersion);

    item.Version = rollbackVersion.VersionNumber;
    item.DraftVersionId = rollbackVersion.Id;
    item.ModifiedUtc = DateTime.UtcNow;

    await _repository.UpdateAsync(item);
}

Version Comparison

Generating Diffs

public class VersionComparisonService
{
    public VersionDiff Compare(ContentVersion older, ContentVersion newer)
    {
        var oldData = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
            older.DataJson);
        var newData = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
            newer.DataJson);

        var diff = new VersionDiff
        {
            OlderVersion = older.VersionNumber,
            NewerVersion = newer.VersionNumber
        };

        // Find added fields
        foreach (var key in newData!.Keys.Except(oldData!.Keys))
        {
            diff.Changes.Add(new FieldDiff
            {
                FieldName = key,
                ChangeType = DiffChangeType.Added,
                NewValue = newData[key].ToString()
            });
        }

        // Find removed fields
        foreach (var key in oldData.Keys.Except(newData.Keys))
        {
            diff.Changes.Add(new FieldDiff
            {
                FieldName = key,
                ChangeType = DiffChangeType.Removed,
                OldValue = oldData[key].ToString()
            });
        }

        // Find modified fields
        foreach (var key in oldData.Keys.Intersect(newData.Keys))
        {
            var oldJson = oldData[key].ToString();
            var newJson = newData[key].ToString();

            if (oldJson != newJson)
            {
                diff.Changes.Add(new FieldDiff
                {
                    FieldName = key,
                    ChangeType = DiffChangeType.Modified,
                    OldValue = oldJson,
                    NewValue = newJson
                });
            }
        }

        return diff;
    }
}

public class VersionDiff
{
    public int OlderVersion { get; set; }
    public int NewerVersion { get; set; }
    public List<FieldDiff> Changes { get; set; } = new();
}

public class FieldDiff
{
    public string FieldName { get; set; } = string.Empty;
    public DiffChangeType ChangeType { get; set; }
    public string? OldValue { get; set; }
    public string? NewValue { get; set; }
}

public enum DiffChangeType
{
    Added,
    Removed,
    Modified
}

Audit Trail

Audit Log Entry

public class ContentAuditEntry
{
    public Guid Id { get; set; }
    public Guid ContentItemId { get; set; }
    public string ContentType { get; set; } = string.Empty;
    public string Action { get; set; } = string.Empty; // Created, Updated, Published, etc.
    public string UserId { get; set; } = string.Empty;
    public string UserName { get; set; } = string.Empty;
    public DateTime OccurredUtc { get; set; }
    public string? IpAddress { get; set; }
    public string? UserAgent { get; set; }
    public string? ChangeSummary { get; set; }
    public string? DataBefore { get; set; }
    public string? DataAfter { get; set; }
}

Automatic Audit Logging

public class AuditInterceptor : SaveChangesInterceptor
{
    private readonly ICurrentUserService _currentUser;
    private readonly IHttpContextAccessor _httpContext;

    public override async ValueTask<InterceptionResult<int>> SavingChangesAsync(
        DbContextEventData eventData,
        InterceptionResult<int> result,
        CancellationToken cancellationToken = default)
    {
        var context = eventData.Context;
        if (context == null) return result;

        var entries = context.ChangeTracker.Entries<ContentItem>()
            .Where(e => e.State is EntityState.Added
                     or EntityState.Modified
                     or EntityState.Deleted);

        foreach (var entry in entries)
        {
            var audit = new ContentAuditEntry
            {
                Id = Guid.NewGuid(),
                ContentItemId = entry.Entity.Id,
                ContentType = entry.Entity.ContentType,
                Action = entry.State.ToString(),
                UserId = _currentUser.UserId,
                UserName = _currentUser.UserName,
                OccurredUtc = DateTime.UtcNow,
                IpAddress = _httpContext.HttpContext?.Connection.RemoteIpAddress?.ToString()
            };

            if (entry.State == EntityState.Modified)
            {
                audit.DataBefore = JsonSerializer.Serialize(
                    entry.OriginalValues.ToObject());
                audit.DataAfter = JsonSerializer.Serialize(
                    entry.CurrentValues.ToObject());
            }

            context.Set<ContentAuditEntry>().Add(audit);
        }

        return result;
    }
}

API Design

Version Endpoints

GET    /api/content/{id}/versions              # List version history
GET    /api/content/{id}/versions/{version}    # Get specific version
GET    /api/content/{id}/versions/compare?v1=1&v2=2  # Compare versions
POST   /api/content/{id}/versions/{version}/restore  # Rollback
GET    /api/content/{id}/audit                 # Audit trail

Related Skills

  • content-type-modeling - Versionable content types
  • content-workflow - Editorial approval workflows
  • headless-api-design - Version API endpoints

适合场景

01

用户想查找某类 Agent Skill 时

02

需要根据任务场景推荐可安装能力包时

03

需要对比不同来源的安装命令和来源信息时

04

需要参考平台分布和安装热度时

能力概览

能力 1

按任务关键词查找相关 Skills

能力 2

展示可复制的安装命令

能力 3

保留来源站点、仓库和原始说明,方便继续核验

能力 4

补充不同宿主或平台的使用分布数据

能力 5

展示第三方安全扫描或审计结果

安装后应在对应宿主中按原始 README 的触发条件使用;具体调用方式请以来源页面和 README 为准。

平台分布

trae

29.14%
按下载量换算21

Antigravity

21.48%
按下载量换算15

windsurf

15.65%
按下载量换算11

Claude Code

11.3%
按下载量换算8

Codex

7.5%
按下载量换算5

Gemini CLI

2.99%
按下载量换算2

安全审计

Gen Agent Trust Hub

通过

Socket

通过

Snyk

通过

权限和风险

权限需确认

当前来源未能明确判断权限范围,默认进入异常复核队列。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。

来源信息

继续浏览同类 Skills