背景 & 目标
当前 1skills 中央库(skills)对包的追踪键是 package_dir(文件夹名);version 只是一个单调计数器(5a35643),不存历史内容、无法恢复。这带来两个根本缺陷:
- 按名称追踪 → 改名/fork 即断链:workspace 拷贝、push-back、manifest 全靠名字匹配(
mutations.py push_skill_from_path)。一旦改名或 fork,就失去溯源链。
- 无快照 → 无法回退:
version 只是计数器,推错了/想回到旧版无从下手。
本 Issue 目标:给 skills 引入 ① 稳定 Skill ID(身份层)+ 血缘追踪 和 ② 整包版本历史 + 非破坏性恢复,并重构 push 流程为永不丢数据的分支模型。
范围:仅 skills。agents / hooks 本期不动(它们沿用现有简单模型)。
核心设计决策(已讨论锁定)
| 维度 |
决定 |
| ID 落点 |
sidecar .skillmeta.json(包目录内),不碰 SKILL.md |
| 中央库目录命名 |
保持人类可读 dir;同名时自动 <name>-<shortid> 消歧 |
| 历史存储 |
整包拷贝 history/<id>/v<N>/,全留(skill 体积很小) |
| 包名唯一性 |
允许同名;ID 是唯一键 |
| 「覆盖」操作 |
移除——任何操作都不再破坏历史 |
| 追踪范围 |
仅 skills |
数据模型
sidecar:<pkg>/.skillmeta.json
随包流转(安装时写入 workspace 拷贝、push 时读取):
关键:.skillmeta.json 必须排除出内容指纹 fingerprint_package(像 .DS_Store 一样 skip),否则会污染 revision、每次误判「已修改」。install / status-from-path / import 扫描同样忽略它。Claude Code 只读 SKILL.md,忽略该 sidecar,安全。
manifest 条目增量(SkillStoreEntry)
在现有 version 基础上增加:id、forkedFrom、forkedFromVersion、isPrimary(布尔;同一血缘树内恰有一个为 main)。
历史存储
~/.1agents/skill-manager/history/<id>/v<N>/ — 整包快照(每次内容变化的推送落一版)。
~/.1agents/skill-manager/history/<id>/index.json — [{version, revision, createdAt, source: push|import|fork|restore|promote, note?}]。
血缘(lineage)
按 forkedFrom 连成树;每个连通分量恰有一个 isPrimary=true(主分支,决定安装/展示默认拿哪条)。
Push 决策树(重构后:只剩一个交互点)
push 时读工作区 sidecar 的 id + baseVersion,对比中央库:
├─ 有 id 且在库:
│ ├─ 库version == baseVersion → 线性更新(新版本 + 快照) ← 静默
│ └─ 库version > baseVersion → 并发编辑:落成 fork C,弹窗「main / fork」 ← ★唯一交互
└─ 无 id / id 不在库:
├─ 内容哈希(revision)命中已有包 → 「已存在,无需更新」+ 链到该 id ← 静默(告知)
└─ 无命中 → 新建包(新 id,允许同名) ← 静默
并发编辑 → 永不覆盖,只选「谁是 main」
推上来的内容总是先落成一个 fork(新 id C,forkedFrom = A @ baseVersion,其历史 v1 = 本次推送),然后只问:「你这次推的是 main 还是 fork?」
- 选 main → C 标
isPrimary;原 A(含 v6/v7/v8)自动降为 fork 分支;
- 选 fork → A 仍 primary,C 为 fork。
两侧历史都原样保留、只追加、永不重写;"main" 只是 isPrimary 标志位。零数据丢失。
内容哈希去重(MD5 / revision)
无 id 的包在 push/导入前算 revision 跟库内所有包比对:内容完全相同 → 提示「已存在(内容一致),无需导入」,不新建,并把工作区 sidecar 链到那个已有 id(以后编辑可追踪)。解决「同一 code-review 从多处第三方下载、内容一样、都无 id」的重复上传。
同名共存
包名不再唯一;推一个无 id 的 code-review、库里已有无关的 code-review → 不弹窗、直接共存,靠 ID 血缘区分,dir 用 <name>-<shortid> 消歧、展示名可完全相同。
API
POST /api/skills/push-from-path → {status: updated|created|exists|conflict, id, version, conflict?}
POST /api/skills/push-from-path/resolve → {sourcePath, resolution: main|fork, name?} // fork 时 name = 用户填的新展示名
GET /api/skills/{id}/versions → [{version, revision, createdAt, source, note}]
GET /api/skills/{id}/versions/{v} → 该版内容 / 与当前的 diff(预览)
POST /api/skills/{id}/restore/{v} → 恢复 vK 为新版本(非破坏)
GET /api/skills/{id}/lineage → 血缘树 {id, forkedFrom, forkedFromVersion, branches[], primaryId}
POST /api/skills/{id}/promote → 将该 id 提升为其血缘的 main(切换 isPrimary)
Go 网关需放行 /api/skills/... 新子路由(已有 /api/skills 前缀代理,子路径自然覆盖)。路由主键从 {skill_ref} 迁到 {id}(或双轨兼容一段)。
恢复语义:restore(vK) = 把 vK 内容写回 live 包并 bump 成新版本(source=restore),历史只追加。
Fork 的新 dir:弹窗里由用户填新展示名,id 自动生成,dir 用 <新名>-<shortid>。
前端
- 推送遇
conflict:弹「main / fork(填新名)」对话框(替代原「覆盖/fork」)。
- 技能详情:新增**「版本历史」抽屉**——版本列表(时间 +
source 徽标)+ 每版 diff 预览 + 恢复按钮。
- 血缘徽标:「fork 自 X · v3」;主/分支标识;可从这里 切换 main(promote)。
- 「已存在(内容一致)」的 push 给 toast 告知,不新建。
迁移(一次性、幂等)
对所有现有 manifest 条目:分配 id → 写入包内 sidecar .skillmeta.json → 把当前内容作为 v(当前 version) 快照进 history/<id>/。可重复执行(已存在则跳过)。
验收标准
非目标(本期不做)
- agents / hooks 的版本化(沿用现有模型)。
- 内容寻址去重(v1 用整包拷贝;未来可优化)。
- 跨机器/多设备的历史同步(仅本地中央库)。
关联:skill 弱同步 / 中央库(#360 系列)、版本计数器 5a35643、agents 家族扩展。
背景 & 目标
当前 1skills 中央库(skills)对包的追踪键是
package_dir(文件夹名);version只是一个单调计数器(5a35643),不存历史内容、无法恢复。这带来两个根本缺陷:mutations.pypush_skill_from_path)。一旦改名或 fork,就失去溯源链。version只是计数器,推错了/想回到旧版无从下手。本 Issue 目标:给 skills 引入 ① 稳定 Skill ID(身份层)+ 血缘追踪 和 ② 整包版本历史 + 非破坏性恢复,并重构 push 流程为永不丢数据的分支模型。
核心设计决策(已讨论锁定)
.skillmeta.json(包目录内),不碰 SKILL.md<name>-<shortid>消歧history/<id>/v<N>/,全留(skill 体积很小)数据模型
sidecar:
<pkg>/.skillmeta.json随包流转(安装时写入 workspace 拷贝、push 时读取):
{ "id": "skl_01J...", // 不可变身份,追踪一律按它 "baseVersion": 5, // 这份拷贝来自中央库的哪个版本(并发冲突检测用) "forkedFrom": "skl_01H...", // fork 来源 id;非 fork 为 null "forkedFromVersion": 3, // fork 时来源的版本;非 fork 为 null "createdAt": "2026-07-03T..." }关键:
.skillmeta.json必须排除出内容指纹fingerprint_package(像.DS_Store一样 skip),否则会污染revision、每次误判「已修改」。install / status-from-path / import 扫描同样忽略它。Claude Code 只读 SKILL.md,忽略该 sidecar,安全。manifest 条目增量(
SkillStoreEntry)在现有
version基础上增加:id、forkedFrom、forkedFromVersion、isPrimary(布尔;同一血缘树内恰有一个为 main)。历史存储
~/.1agents/skill-manager/history/<id>/v<N>/— 整包快照(每次内容变化的推送落一版)。~/.1agents/skill-manager/history/<id>/index.json—[{version, revision, createdAt, source: push|import|fork|restore|promote, note?}]。血缘(lineage)
按
forkedFrom连成树;每个连通分量恰有一个isPrimary=true(主分支,决定安装/展示默认拿哪条)。Push 决策树(重构后:只剩一个交互点)
push 时读工作区 sidecar 的
id+baseVersion,对比中央库:并发编辑 → 永不覆盖,只选「谁是 main」
推上来的内容总是先落成一个 fork(新 id C,
forkedFrom = A @ baseVersion,其历史 v1 = 本次推送),然后只问:「你这次推的是 main 还是 fork?」isPrimary;原 A(含 v6/v7/v8)自动降为 fork 分支;两侧历史都原样保留、只追加、永不重写;"main" 只是
isPrimary标志位。零数据丢失。内容哈希去重(MD5 / revision)
无 id 的包在 push/导入前算
revision跟库内所有包比对:内容完全相同 → 提示「已存在(内容一致),无需导入」,不新建,并把工作区 sidecar 链到那个已有 id(以后编辑可追踪)。解决「同一 code-review 从多处第三方下载、内容一样、都无 id」的重复上传。同名共存
包名不再唯一;推一个无 id 的
code-review、库里已有无关的code-review→ 不弹窗、直接共存,靠 ID 血缘区分,dir 用<name>-<shortid>消歧、展示名可完全相同。API
Go 网关需放行
/api/skills/...新子路由(已有/api/skills前缀代理,子路径自然覆盖)。路由主键从{skill_ref}迁到{id}(或双轨兼容一段)。恢复语义:
restore(vK)= 把 vK 内容写回 live 包并 bump 成新版本(source=restore),历史只追加。Fork 的新 dir:弹窗里由用户填新展示名,
id自动生成,dir 用<新名>-<shortid>。前端
conflict:弹「main / fork(填新名)」对话框(替代原「覆盖/fork」)。source徽标)+ 每版 diff 预览 + 恢复按钮。迁移(一次性、幂等)
对所有现有 manifest 条目:分配
id→ 写入包内 sidecar.skillmeta.json→ 把当前内容作为v(当前 version)快照进history/<id>/。可重复执行(已存在则跳过)。验收标准
id(sidecar + manifest),.skillmeta.json不进内容指纹(改名/编辑不误判)。id+baseVersion);改名后 push 仍按id命中同一包。version每次内容变化的推送 +1,并落一份整包快照到history/<id>/v<N>/。<name>-<shortid>消歧。GET versions/restore/{v}(恢复为新版本、非破坏)/lineage/promote全通。非目标(本期不做)
关联:skill 弱同步 / 中央库(#360 系列)、版本计数器
5a35643、agents 家族扩展。