diff --git a/docs/00-product-direction.md b/docs/00-product-direction.md new file mode 100644 index 00000000..a6e28ec7 --- /dev/null +++ b/docs/00-product-direction.md @@ -0,0 +1,118 @@ +# Astron Skills 产品定位与 MVP 范围 + +## 1. 定位 + +单实例共享技能注册中心(Skills Hub / Registry),不是多租户平台。 + +- 平台只有一个共享注册中心实例 +- 隔离边界是 namespace,不是租户 +- `@global` 是平台级公共空间,由平台管理员管理 +- `@team-*` 是协作与治理边界(部门/团队),不是租户边界 +- 公共技能(visibility=PUBLIC)匿名可浏览和下载 + +以 ClawHub 为产品蓝本(继承产品模型,不照搬技术实现),以 OpenSkills 借鉴 SKILL.md 格式和目录结构约定(不兼容其客户端运行时行为)。 + +同时,一期必须提供 ClawHub CLI 协议兼容层:服务端需要暴露一组与 ClawHub CLI 兼容的 registry API,使现有 ClawHub CLI 在不修改或仅最小配置修改的前提下可完成 registry 侧查询、解析、下载、发布、校验等核心操作。 + +## 2. 参考项目取舍 + +### 2.1 继承 ClawHub 的部分 + +- Skill Registry 的整体产品边界 +- 技能版本、标签、下载的业务模型 +- 发布后治理机制(报告、标记、隐藏、撤回) +- Web 浏览、详情页、上传发布、管理后台的功能切分 +- 公共查询 API 与 CLI API 的双通道设计 +- ClawHub CLI 所依赖的 registry API 协议面 +- Skill 元数据提取与服务端校验思路 +- 审计、收藏、评分、统计、运营标签等扩展位 + +不直接继承: +- Convex 数据模型与运行时 +- 向量检索的一期实现方式 + +### 2.2 借鉴 OpenSkills 的部分 + +- `SKILL.md` 格式兼容(frontmatter + markdown body) +- 技能包目录结构约定(SKILL.md + references/ + scripts/ + assets/) +- 四级目录优先级(`.agent/skills` → `~/.agent/skills` → `.claude/skills` → `~/.claude/skills`) +- 目录名作为 lookup key(安装后目录名 = skill slug) +- AGENTS.md `` 描述块格式兼容 +- 目标:Astron CLI 安装的技能可被 OpenSkills/Claude 兼容客户端发现和使用 + +不直接继承: +- 以 CLI 为中心的产品定位 +- "无服务端"的前提 + +## 3. 产品原则 + +- Hub 优先:服务端是核心,CLI 和 Agent 集成是入口能力 +- 兼容优先:兼容 `SKILL.md` 及常见目录约定 +- CLI 兼容优先:除 Astron CLI 外,一期明确要求实现 ClawHub CLI 协议兼容层 +- 分层优先:搜索、对象存储都必须有可替换边界 +- 开放认证:基于标准 OAuth2 协议,一期 GitHub 登录,架构支持后续扩展多 Provider +- 审计优先:企业内部分发平台必须保留发布、下载、删除、授权等审计链路 + +## 4. 一期 MVP 功能 + +核心能力: +- 技能发布(提交 → 审核 → 上线,每版本审核) +- 技能版本管理(semver + 标签) +- 技能浏览、详情、下载(公共技能匿名可访问) +- 标签管理(`latest` 系统保留只读 + 自定义标签人工维护) +- 技能包文件校验与 SKILL.md 元数据抽取 +- 基于 MySQL 全文索引的搜索 + +命名空间与组织: +- 单一全局命名空间(`@global/skill-name`),由平台管理员管理,不支持多个平台级 namespace +- 团队/部门命名空间(`@team-slug/skill-name`) +- 命名空间成员管理 +- 创建技能时选择归属空间 + +审核流程: +- 每版本审核策略 +- 分级审核:团队空间由团队管理员审核,全局空间由平台管理员审核 +- 团队技能提升到全局需平台管理员二次审核 +- 平台管理员拥有全局审核权 +- 一期纯人工审核,架构预留自动预检扩展点(`PrePublishValidator`) + +认证与权限: +- OAuth2 标准登录(一期 GitHub OAuth) +- API Token(CLI / agent 使用) +- ClawHub CLI 协议兼容层(registry API 兼容查询、解析、下载、发布、校验等核心接口) +- RBAC 角色权限体系(平台角色:SUPER_ADMIN / SKILL_ADMIN / USER_ADMIN / AUDITOR + 命名空间角色) +- 管理后台:用户角色管理、发布审核 + +社交功能: +- 收藏(star) +- 评分(1-5 分) + +审计: +- 发布、审核、下载、删除等关键操作审计 + +## 5. 一期明确不做(含后续规划) + +- 评论 → Phase 5 上线,含举报机制 +- 自动安全扫描 → Phase 5 上线,接入 `PrePublishValidator` 扩展点 +- 举报/标记机制 → Phase 5 上线,配合评论和治理闭环 +- 向量搜索 → Phase 3(搜索演进路线) +- 在线编辑器 → 暂不规划 +- Webhook/事件通知 → Phase 5(预留扩展点) +- 技能依赖/兼容性声明 → 暂不规划(预留 `parsed_metadata_json` 字段) + +### latest 语义说明 + +这是有意的产品决策,不是继承 ClawHub 的回滚模型: + +- `latest` 自动跟随最新已发布版本,只读,不可手动移动 +- 回滚/稳定通道管理通过自定义标签实现(如 `stable`、`beta`、`stable-2026q1`) +- ClawHub 的"通过移动 latest 做回滚"能力被替换为"通过自定义标签做通道管理" + +## 6. 一期核心约束 + +- Skill 包视为"文本资源包",不接受二进制大文件 +- 技能包主入口文件固定为 `SKILL.md` +- 元数据以 `SKILL.md` frontmatter 为主,数据库持久化解析结果 +- 文件内容原文存对象存储,检索面向数据库中的派生字段与可索引文本 +- Web 认证与 API Token 认证分离,但统一汇聚到平台用户体系 +- 公共技能(visibility=PUBLIC)匿名可浏览和下载,无需登录 diff --git a/docs/01-system-architecture.md b/docs/01-system-architecture.md new file mode 100644 index 00000000..e09cfe7e --- /dev/null +++ b/docs/01-system-architecture.md @@ -0,0 +1,146 @@ +# Astron Skills 系统架构设计 + +## 1. 技术基线 + +- JDK: 21 +- Framework: Spring Boot 3.x(最新稳定版) +- Security: Spring Security + spring-boot-starter-oauth2-client +- Database: MySQL 8.x +- Cache/Session: Redis 7.x(一期必须依赖,用于 Session 存储 + 分布式锁 + 幂等去重) +- Object Storage: S3 协议兼容对象存储 +- Search: MySQL Full-Text Search(一期) +- Future Search: Elasticsearch / OpenSearch / Vector Search + +## 2. 总体架构 + +采用单体优先、模块化单体设计。业务域清晰,一期规模不需要拆分微服务。 + +## 3. 后端模块结构 + +``` +server/ +├── astron-skills-app # 启动、配置装配、Controller 聚合 +├── astron-skills-domain # 领域模型 + 领域服务 + 应用服务 +├── astron-skills-auth # OAuth2 认证 + RBAC + 授权判定 +├── astron-skills-search # 搜索 SPI + MySQL 全文实现 +├── astron-skills-storage # 对象存储抽象 + S3 实现 +└── astron-skills-infra # MyBatis、通用工具、配置基础 +``` + +## 4. 模块依赖方向(依赖倒置,禁止领域层依赖基础设施) + +``` +app → domain, auth, search, storage, infra +infra → domain # infra 实现 domain 定义的 Repository 接口 +auth → domain # auth 引用 UserAccount 等领域实体 +search → domain # search 引用 SkillSearchDocument 等领域模型 +storage → (独立抽象) # 纯 SPI,不依赖 domain +``` + +核心原则: +- domain 是最内层,不依赖任何其他模块,只定义接口和实体 +- infra 实现 domain 中定义的 Repository 接口(MyBatis Mapper) +- app 负责装配所有模块,通过 Spring 依赖注入将 infra 实现注入 domain 接口 +- 禁止 domain → infra 方向的依赖,避免领域层与 MyBatis、事件实现绑死 + +## 5. 各模块职责 + +### astron-skills-app +- Spring Boot 启动类 +- Controller 分包:`controller.portal`(公开查询)、`controller.cli`(CLI API)、`controller.admin`(管理后台) +- 全局异常处理、请求日志、OpenAPI 配置 +- 配置文件与环境 profile + +### astron-skills-domain +- 核心实体:Skill, SkillVersion, SkillFile, SkillTag, Namespace, NamespaceMember, ReviewTask, AuditLog, SkillStar, SkillRating +- 领域服务:发布流程编排、审核状态机、命名空间管理、标签管理 +- 应用服务:面向 Controller 的用例编排 +- Repository 接口定义(实现在 infra) + +### astron-skills-auth +- Spring Security OAuth2 Client 配置(一期 GitHub,可扩展多 Provider) +- `CustomOAuth2UserService`:OAuth2 用户 → 平台用户映射 +- `IdentityBindingService`:外部身份 → 平台用户绑定 +- Spring Session (Redis) 管理 +- API Token 签发、校验、吊销 +- RBAC:角色定义、权限点、资源级授权判定 +- 用户实体:UserAccount, IdentityBinding, ApiToken, Role, Permission, UserRoleBinding + +### astron-skills-search +- SPI 接口:`SearchIndexService`, `SearchQueryService`, `SearchRebuildService` +- 一期实现:`MysqlFullTextIndexService`, `MysqlFullTextQueryService` +- 独立搜索文档表 `skill_search_document` +- 未来扩展点:ES / 向量检索实现 + +### astron-skills-storage +- SPI 接口:`ObjectStorageService` +- 一期实现:S3 兼容实现(MinIO / AWS S3) +- 文件哈希校验、打包下载 +- 对象 key 规则(使用不可变 ID,避免命名空间变更导致 key 失效): + - 正式路径:`skills/{skillId}/{versionId}/{filePath}` + - 打包路径:`packages/{skillId}/{versionId}/bundle.zip` + - 临时上传:`tmp/{uploadId}/{filePath}`(24h GC 清理) + +### astron-skills-infra +- MyBatis-Plus Mapper 实现 +- Repository 实现 +- 通用工具(ID 生成、时间、JSON 等) +- Spring Events 异步事件基础设施 + +## 6. 前端工程结构 + +``` +web/ +├── src/ +│ ├── app/ # 路由、全局 Provider、布局 +│ ├── pages/ # 页面入口 +│ ├── features/ # 搜索、上传、版本管理、审核等业务功能 +│ ├── entities/ # skill、user、namespace 等领域展示逻辑 +│ ├── shared/ # 通用组件、hooks、工具 +│ └── api/ # openapi-typescript 生成的类型 + openapi-fetch 客户端 +├── package.json +└── vite.config.ts +``` + +技术栈:React 19 + TypeScript + Vite + shadcn/ui + Tailwind CSS + TanStack Query + TanStack Router + openapi-fetch + +## 7. Monorepo 顶层结构 + +``` +astron-skills/ +├── server/ # Maven 多模块 Java 后端 +├── web/ # React 前端 +├── Makefile # 顶层构建编排(dev / build / docker) +├── docs/ # 设计文档 +└── README.md +``` + +简单分目录,各自独立构建,Makefile 串联。 + +## 8. 部署架构 + +同域部署,统一入口: +- `https://skills.example.com/` → 前端静态资源 +- `https://skills.example.com/api/*` → 反向代理到 Spring Boot +- 生产环境通过 Nginx 或网关统一接入 + +## 9. 分布式环境要求 + +本服务在 K8s 中部署多个 Pod,所有组件必须无状态设计。 + +| 组件 | 一期要求 | 职责 | +|------|---------|------| +| MySQL 8.x | 主从 | 主存储 | +| Redis 7.x | Sentinel 或 Cluster | Session 存储 + 分布式锁 + 幂等去重 | +| S3 兼容存储 | MinIO 或云厂商 S3 | 技能包文件 + 预打包 zip | +| Ingress | Nginx Ingress Controller | 路由分发 + TLS 终止 | + +## 10. 推荐的一期技术决策 + +- ORM:MyBatis-Plus +- API 文档:Springdoc OpenAPI +- 对象存储:MinIO / AWS S3 兼容接口 +- 异步任务:Spring Events + 异步线程池,后续视复杂度引入 MQ +- 缓存/Session:Spring Session + Redis +- 数据库迁移:Flyway +- 认证:Spring Security OAuth2 Client(一期 GitHub) diff --git a/docs/02-domain-model.md b/docs/02-domain-model.md new file mode 100644 index 00000000..215b588e --- /dev/null +++ b/docs/02-domain-model.md @@ -0,0 +1,395 @@ +# Astron Skills 领域模型与数据模型 + +## 3.1 核心实体 + +### namespace + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | bigint | 主键 | +| slug | varchar(64) | URL 友好标识 | +| display_name | varchar(128) | 展示名 | +| type | enum | `GLOBAL` / `TEAM` | +| description | text | 描述 | +| avatar_url | varchar(512) | 头像 | +| status | enum | `ACTIVE` / `FROZEN` / `ARCHIVED` | +| created_by | bigint | 创建人 | +| created_at | datetime | | +| updated_at | datetime | | + +- `GLOBAL` 类型全局唯一(只有一个 `@global`),由平台管理员管理 +- `TEAM` 类型对应部门/团队,可创建多个 +- 技能完整寻址:`@{namespace_slug}/{skill_slug}` +- slug 唯一约束:`slug` +- slug 格式校验:`[a-z0-9]([a-z0-9-]*[a-z0-9])?`,长度 2-64 +- slug 保留词列表(用户创建 namespace 时不可使用):`admin`, `api`, `dashboard`, `search`, `auth`, `me`, `global`, `system`, `static`, `assets`, `health` +- 系统内置 namespace(`@global`)在数据库初始化时由 Flyway 脚本预置,绕过 slug 校验规则。保留词校验仅作用于用户创建 namespace 的接口 +- 状态语义: + - `ACTIVE`:正常使用 + - `FROZEN`:冻结,只读不可发布新版本,已有技能仍可浏览/下载 + - `ARCHIVED`:归档,对外不可见 + +### namespace_member + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | bigint | | +| namespace_id | bigint | | +| user_id | bigint | | +| role | enum | `OWNER` / `ADMIN` / `MEMBER` | +| created_at | datetime | | +| updated_at | datetime | | + +- `OWNER`:命名空间创建者,可转让 +- `ADMIN`:可审核该空间内的技能发布、管理成员 +- `MEMBER`:可在该空间内发布技能(提交审核) +- 唯一约束:`(namespace_id, user_id)`,一个用户在一个空间只有一个角色 + +### skill + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | bigint | | +| namespace_id | bigint | 所属命名空间 | +| slug | varchar(128) | URL 友好标识 | +| display_name | varchar(256) | | +| summary | varchar(512) | | +| owner_id | bigint | 主要维护人(可转让) | +| source_skill_id | bigint | 派生来源(团队技能提升到全局时记录原 skill ID),nullable | +| visibility | enum | `PUBLIC` / `NAMESPACE_ONLY` / `PRIVATE` | +| status | enum | `ACTIVE` / `HIDDEN` / `ARCHIVED` | +| latest_version_id | bigint | 最新已发布版本(自动跟随,每次发布自动更新) | +| download_count | bigint | | +| star_count | int | | +| rating_avg | decimal(3,2) | 平均评分 | +| rating_count | int | 评分人数 | +| created_by | bigint | | +| created_at | datetime | | +| updated_by | bigint | | +| updated_at | datetime | | + +- 唯一约束:`(namespace_id, slug)` +- `owner_id` 语义为"主要维护人",可转让。权限主轴是 namespace role,不是 owner: + - namespace ADMIN 对空间内所有 skill 有完整管理权(归档、版本管理、提升到全局),不受 owner 限制 + - owner 作为 MEMBER 时可管理自己创建的 skill(提交审核、编辑草稿) + - owner 离职/换组后,namespace ADMIN 仍能完整管理所有技能 +- `rating_avg` / `rating_count` 冗余字段,避免每次查询聚合 +- `slug`:面向用户的 URL 标识,来自 SKILL.md 的 `name` 字段,首次发布后不可变更。slug 格式校验规则与 namespace slug 相同:`[a-z0-9]([a-z0-9-]*[a-z0-9])?`,同样适用保留词限制 +- `source_skill_id`:仅在"团队技能提升到全局"场景下填充,记录原始团队空间的 skill ID,用于追溯来源 +- 提升关系的唯一事实来源是 `promotion_request` 表,UI 查询"是否已提升"通过 `SELECT ... FROM promotion_request WHERE source_skill_id=? AND status='APPROVED'` 判定 + +### skill_version + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | bigint | | +| skill_id | bigint | | +| version | varchar(32) | semver | +| version_sort | bigint | 排序用数值 | +| changelog | text | | +| manifest_json | json | 文件清单 | +| parsed_metadata_json | json | SKILL.md frontmatter 解析结果 | +| status | enum | `DRAFT` / `PENDING_REVIEW` / `PUBLISHED` / `REJECTED` / `YANKED` | +| file_transfer_status | enum | `PENDING` / `COMPLETED` / `FAILED`,异步文件转正状态 | +| reject_reason | varchar(512) | 拒绝原因 | +| published_by | bigint | | +| published_at | datetime | | +| created_at | datetime | | + +- `status` 覆盖完整审核生命周期 +- 状态机:`DRAFT → PENDING_REVIEW → PUBLISHED / REJECTED`,`PUBLISHED → YANKED` +- `DRAFT → PENDING_REVIEW` 前置条件:`file_transfer_status = COMPLETED` +- 唯一约束:`(skill_id, version)` 防止重复发布 +- `YANKED` 状态:已发布后撤回 + +版本号不可变性规则: + +| 版本状态 | 版本号处理 | +|---------|-----------| +| DRAFT | 可删除该版本记录,重新使用同版本号 | +| PENDING_REVIEW | 可撤回到 DRAFT,然后删除 | +| REJECTED | 可删除该版本记录,重新使用同版本号 | +| PUBLISHED | 版本号永久占用,不可复用 | +| YANKED | 版本号永久占用,不可复用,版本列表中显示但标记为不可下载 | + +### skill_file + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | bigint | | +| skill_version_id | bigint | | +| file_path | varchar(512) | | +| content_type | varchar(128) | | +| size_bytes | bigint | | +| sha256 | varchar(64) | | +| object_key | varchar(512) | | +| is_entry_file | boolean | | +| created_at | datetime | | + +### skill_tag + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | bigint | | +| skill_id | bigint | | +| tag_name | varchar(64) | | +| target_version_id | bigint | | +| created_by | bigint | | +| created_at | datetime | | +| updated_by | bigint | | +| updated_at | datetime | | + +- `latest` 是系统保留标签,只读,自动跟随 `skill.latest_version_id`,不允许 API 手动移动 +- 自定义标签(如 `beta`、`stable-2026q1`)允许人工创建和移动 +- 唯一约束:`(skill_id, tag_name)` + +### review_task + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | bigint | | +| skill_version_id | bigint | 关联的版本 | +| namespace_id | bigint | 所属空间(决定谁能审核) | +| status | enum | `PENDING` / `APPROVED` / `REJECTED` | +| version | int | 乐观锁版本号,默认 1 | +| submitted_by | bigint | 提交人 | +| reviewed_by | bigint | 审核人 | +| review_comment | text | 审核意见 | +| submitted_at | datetime | | +| reviewed_at | datetime | | + +- 仅用于普通发布审核,"提升到全局"使用独立的 `promotion_request` 表 +- `version` 字段用于乐观锁,防止多 Pod 并发审核 +- 业务约束:同一 `skill_version_id` 在 `status=PENDING` 时只能存在一条记录,重复提交返回 409 Conflict。撤回(PENDING → 删除 review_task + skill_version 回退到 DRAFT)后才能再次提交 + +### promotion_request + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | bigint | | +| source_skill_id | bigint | 来源团队 skill | +| source_version_id | bigint | 申请提升的版本 | +| target_namespace_id | bigint | 目标全局 namespace | +| target_skill_id | bigint | 审批通过后生成的全局 skill ID,nullable | +| status | enum | `PENDING` / `APPROVED` / `REJECTED` | +| version | int | 乐观锁版本号,默认 1 | +| submitted_by | bigint | 提交人 | +| reviewed_by | bigint | 审核人 | +| review_comment | text | 审核意见 | +| submitted_at | datetime | | +| reviewed_at | datetime | | + +- 完整表达"哪个团队 skill 的哪一版被申请提升到哪个全局空间" +- 审批通过后填充 `target_skill_id`,指向全局空间新创建的 skill +- `promotion_request` 是提升关系的唯一事实来源,skill 表不再冗余 `promoted_to_skill_id` +- 业务约束:同一 `source_version_id` 在 `status=PENDING` 时只能存在一条记录,重复提交返回 409 Conflict + +### skill_star + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | bigint | | +| skill_id | bigint | | +| user_id | bigint | | +| created_at | datetime | | + +唯一约束:`(skill_id, user_id)` + +### skill_rating + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | bigint | | +| skill_id | bigint | | +| user_id | bigint | | +| score | tinyint | 1-5 | +| created_at | datetime | | +| updated_at | datetime | | + +唯一约束:`(skill_id, user_id)`,每人每技能一条,可修改 + +### user_account + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | bigint | | +| display_name | varchar(128) | | +| email | varchar(256) | | +| avatar_url | varchar(512) | | +| status | enum | `ACTIVE` / `PENDING` / `DISABLED` / `MERGED` | +| merged_to_user_id | bigint | 合并目标用户 ID,仅 MERGED 状态有值 | +| created_at | datetime | | +| updated_at | datetime | | + +- 状态语义: + - `ACTIVE`:正常使用 + - `PENDING`:等待管理员审批(AccessPolicy 返回 PENDING_APPROVAL 时创建) + - `DISABLED`:管理员封禁,登录后拒绝所有操作,返回 403 + - `MERGED`:已合并到其他账号,保留记录不物理删除,登录时自动跳转到合并目标账号 +- 授权层在每次请求时检查用户状态,非 `ACTIVE` 用户拒绝所有写操作 + +### identity_binding + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | bigint | | +| user_id | bigint | | +| provider_code | varchar(64) | 如 `github` | +| subject | varchar(256) | OAuth Provider 返回的唯一用户标识 | +| login_name | varchar(128) | 如 GitHub login | +| extra_json | json | 原始扩展字段 | +| created_at | datetime | | +| updated_at | datetime | | + +- 唯一约束:`(provider_code, subject)` +- 一期只接入 GitHub OAuth,但表结构支持后续扩展多个 OAuth Provider + +### api_token + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | bigint | | +| subject_type | varchar(32) | `USER`(一期)/ `SERVICE_ACCOUNT`(预留) | +| subject_id | bigint | 关联主体 ID(一期等同于 user_id) | +| user_id | bigint | 兼容字段,一期与 subject_id 相同 | +| name | varchar(128) | Token 名称(必填),如"CI/CD"、"本地开发" | +| token_prefix | varchar(16) | | +| token_hash | varchar(64) | | +| scope_json | json | | +| expires_at | datetime | | +| last_used_at | datetime | | +| revoked_at | datetime | | +| created_at | datetime | | + +### audit_log + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | bigint | | +| actor_user_id | bigint | | +| action | varchar(64) | | +| target_type | varchar(64) | | +| target_id | bigint | | +| request_id | varchar(64) | | +| client_ip | varchar(64) | | +| user_agent | varchar(512) | | +| detail_json | json | | +| created_at | datetime | | + +## 3.2 RBAC 实体 + +一期即上线完整 RBAC,平台角色按最小权限拆分,避免所有治理能力压在单一超管角色上。 + +平台角色(一期内置,Flyway 预置): + +| 角色 code | 说明 | 典型权限 | +|-----------|------|---------| +| `SUPER_ADMIN` | 平台超管,拥有所有权限 | 全部 | +| `SKILL_ADMIN` | 技能治理:全局空间审核、提升审核、隐藏/撤回 | `review:approve`, `skill:manage`, `promotion:approve` | +| `USER_ADMIN` | 用户治理:准入审批、封禁/解封、角色分配(不可分配 SUPER_ADMIN) | `user:manage`, `user:approve` | +| `AUDITOR` | 审计只读:查看审计日志 | `audit:read` | + +- 命名空间权限仍由 `namespace_member.role`(OWNER / ADMIN / MEMBER)决定,不走 RBAC 表 +- 一个用户可持有多个平台角色(多条 `user_role_binding`) +- `SUPER_ADMIN` 隐含所有权限,代码中硬判定短路 + +### role + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | bigint | | +| code | varchar(64) | `SUPER_ADMIN` / `SKILL_ADMIN` / `USER_ADMIN` / `AUDITOR` | +| name | varchar(128) | 展示名 | +| description | varchar(512) | | +| is_system | boolean | 系统内置角色不可删除 | +| created_at | datetime | | + +### permission + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | bigint | | +| code | varchar(128) | 如 `skill:publish`, `review:approve`, `user:manage` | +| name | varchar(128) | | +| group_code | varchar(64) | 权限分组 | + +### role_permission + +| 字段 | 类型 | 说明 | +|------|------|------| +| role_id | bigint | | +| permission_id | bigint | | + +### user_role_binding + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | bigint | | +| user_id | bigint | | +| role_id | bigint | | +| created_at | datetime | | + +## 3.3 搜索文档表 + +### skill_search_document + +一个 skill 对应一条搜索文档,内容取 `latest_version_id` 对应版本。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | bigint | | +| skill_id | bigint | 唯一,一 skill 一条 | +| namespace_id | bigint | 用于空间过滤 | +| owner_id | bigint | 用于 PRIVATE 可见性判定 | +| title | varchar(256) | | +| summary | varchar(512) | | +| keywords | varchar(512) | | +| search_text | text | SKILL.md 正文 + frontmatter 拼接 | +| visibility | enum | 冗余,避免搜索时 join | +| status | enum | | +| updated_at | datetime | | + +MySQL Full-Text Index 建在 `(title, summary, keywords, search_text)` 上。 + +## 3.4 幂等记录表 + +### idempotency_record + +| 字段 | 类型 | 说明 | +|------|------|------| +| request_id | varchar(64) | 主键,客户端传入的 UUID v4 | +| resource_type | varchar(64) | 如 `skill_version`, `api_token` | +| resource_id | bigint | 业务操作产生的资源 ID | +| status | enum | `PROCESSING` / `COMPLETED` / `FAILED` | +| response_status_code | int | 原始响应状态码 | +| created_at | datetime | | +| expires_at | datetime | 过期时间(默认 24h) | + +- 流程:收到请求 → 插入 record(PROCESSING)→ 业务处理 → 更新为 COMPLETED + resource_id → 重复请求时查 record 返回已有结果 +- Redis 做快速去重缓存(SETNX),MySQL 做持久化兜底 +- 定时任务清理过期记录 + +## 3.5 关键索引设计 + +| 表 | 索引 | 用途 | +|------|------|------| +| namespace | `(slug)` UNIQUE | 唯一约束 | +| skill | `(namespace_id, status)` | 命名空间内技能列表 | +| skill | `(namespace_id, slug)` UNIQUE | 唯一约束 | +| skill_version | `(skill_id, status)` | 版本列表 | +| skill_version | `(skill_id, version)` UNIQUE | 唯一约束 | +| skill_tag | `(skill_id, tag_name)` UNIQUE | 标签唯一约束 | +| review_task | `(namespace_id, status)` | 审核列表 | +| review_task | `(submitted_by, status)` | 我的提交 | +| promotion_request | `(source_skill_id)` | 按来源 skill 查询 | +| promotion_request | `(status)` | 待审核列表 | +| idempotency_record | `(expires_at)` | 过期清理 | +| audit_log | `(created_at)` | 审计查询 | +| audit_log | `(actor_user_id, created_at)` | 用户操作历史 | +| skill_star | `(user_id)` | 我的收藏 | +| skill_star | `(skill_id)` | 技能收藏数 | +| skill_rating | `(skill_id)` | 评分聚合 | +| namespace_member | `(namespace_id, user_id)` UNIQUE | 成员唯一约束 | +| namespace_member | `(user_id)` | 用户所属空间 | +| identity_binding | `(provider_code, subject)` UNIQUE | 身份查找 | +| api_token | `(token_hash)` | Token 校验 | \ No newline at end of file diff --git a/docs/03-authentication-design.md b/docs/03-authentication-design.md new file mode 100644 index 00000000..567fcc84 --- /dev/null +++ b/docs/03-authentication-design.md @@ -0,0 +1,479 @@ +# Astron Skills 认证与授权设计 + +## 1. 认证架构 + +``` +请求进入 + │ + ▼ +┌─────────────────────────────┐ +│ Layer 1: OAuth2 Login │ Spring Security OAuth2 Client +│ (一期 GitHub,可扩展) │ 授权码模式 (Authorization Code) +└─────────────┬───────────────┘ + │ OAuth2User + ▼ +┌─────────────────────────────┐ +│ Layer 2: Access Policy │ 准入策略判定 +│ (认证成功 ≠ 有权使用平台) │ 白名单/邮箱域名/开放注册 +└─────────────┬───────────────┘ + │ 准入通过 + ▼ +┌─────────────────────────────┐ +│ Layer 3: Identity Mapping │ OAuth2 用户 → 平台用户 +│ (查询/创建 identity_binding) │ 自动注册 + 信息同步 +└─────────────┬───────────────┘ + │ PlatformPrincipal + ▼ +┌─────────────────────────────┐ +│ Layer 4: Session / Token │ Web: Spring Session (Redis) +│ │ CLI: API Token +└─────────────┬───────────────┘ + │ SecurityContext + ▼ +┌─────────────────────────────┐ +│ Layer 5: Authorization │ RBAC + 资源级判定 +└─────────────────────────────┘ +``` + +## 2. 准入策略(Access Policy) + +OAuth 认证成功仅代表身份可信,不代表有权使用平台。准入层在认证成功后、创建平台用户前执行。 + +```java +// 基于 claims 的准入策略,与 Provider 无关 +public interface AccessPolicy { + AccessDecision evaluate(OAuthClaims claims); +} + +public record OAuthClaims( + String provider, // github, google, wechat + String subject, // provider 唯一 ID + String email, // nullable(微信等可能无邮箱) + boolean emailVerified, // 是否已验证 + String providerLogin, // 如 GitHub login + Map extra +) {} + +public enum AccessDecision { + ALLOW, // 准入,继续创建/绑定平台用户 + DENY, // 拒绝,不建立 Session,重定向到拒绝页 + PENDING_APPROVAL // 等待管理员审批,不建立业务 Session +} +``` + +### 2.1 一期支持的策略(通过配置切换) + +```yaml +astron: + access-policy: + mode: EMAIL_DOMAIN # OPEN / PROVIDER_ALLOWLIST / EMAIL_DOMAIN / SUBJECT_WHITELIST + allowed-providers: + - github + allowed-email-domains: + - company.com + - subsidiary.com +``` + +| 策略 | 判定依据 | 说明 | +|------|---------|------| +| `OPEN` | 无限制 | 所有 OAuth 登录用户自动准入 | +| `PROVIDER_ALLOWLIST` | `claims.provider` | 仅允许指定 Provider 登录 | +| `EMAIL_DOMAIN` | `claims.email` + `claims.emailVerified` | 仅允许已验证邮箱且域名匹配(email 为空或未验证则 DENY) | +| `SUBJECT_WHITELIST` | `claims.provider` + `claims.subject` | 按 `provider:subject` 白名单,管理员预添加 | + +### 2.2 准入失败处理 + +- `DENY`:抛出 `OAuth2AccessDeniedException`,由 `failureHandler` 重定向到 `/access-denied` 页面。不创建用户,不建立 Session。 +- `PENDING_APPROVAL`:创建 `user_account`(status=`PENDING`),但不建立业务 Session。抛出 `AccountPendingException`,由 `failureHandler` 重定向到 `/pending-approval` 页面(纯静态提示页,无需登录态)。管理员在后台审批后状态变为 `ACTIVE`,用户下次 OAuth 登录才会正常建立 Session。 + +安全边界:PENDING / DISABLED 用户绝不会拥有有效的业务 Session,从根源上杜绝"待审批账号已认证"的风险。 + +### 2.3 扩展性 + +后续新增 OAuth Provider(Google、GitLab、微信)时,准入策略与 Provider 无关,统一在 AccessPolicy 层判定,不需要重做入驻逻辑。 + +## 3. Web 认证流程(OAuth2 Authorization Code) + +``` +浏览器点击"登录" + │ + ▼ +前端跳转: /oauth2/authorization/github + │ + ▼ +Spring Security 重定向到 GitHub 授权页 + │ + ▼ +用户在 GitHub 授权 + │ + ▼ +GitHub 回调: /login/oauth2/code/github?code=xxx&state=xxx + │ + ▼ +Spring Security 自动完成: + ① 用 code 换取 access_token + ② 调用 GitHub API 获取用户信息 + ③ 触发自定义 OAuth2UserService + │ + ▼ +CustomOAuth2UserService: + ① 从 OAuth2User 提取 provider + externalId → 构建 OAuthClaims + ② AccessPolicy.evaluate(claims) → 准入判定 + │ + ├── DENY → 抛出 OAuth2AccessDeniedException → failureHandler 重定向 /access-denied(不建立 Session) + ├── PENDING_APPROVAL → 创建 PENDING 用户 → 抛出 AccountPendingException → failureHandler 重定向 /pending-approval(不建立 Session) + └── ALLOW ↓ + │ + ③ 查询 identity_binding 是否已绑定 + ├── 已绑定 → 加载平台用户,检查用户状态(DISABLED → 抛异常),同步最新头像/昵称 + └── 未绑定 → 创建 user_account(ACTIVE) + identity_binding + │ + ▼ +AuthenticationSuccessHandler: + ① 创建 Spring Session (Redis) + ② 重定向到前端页面 (可配置的 redirect_uri) +``` + +### 3.1 Spring Security 配置要点 + +```java +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .oauth2Login(oauth2 -> oauth2 + .userInfoEndpoint(info -> info + .userService(customOAuth2UserService)) + .successHandler(oAuth2SuccessHandler) + .failureHandler(oAuth2FailureHandler) + ) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)) + .csrf(csrf -> csrf + .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) + .ignoringRequestMatchers("/api/v1/cli/**")) + // ... + ; + } +} +``` + +### 3.2 OAuth2 Provider 扩展设计 + +一期只实现 GitHub,但架构支持后续扩展: + +```yaml +# application.yml +spring: + security: + oauth2: + client: + registration: + github: + client-id: ${OAUTH2_GITHUB_CLIENT_ID} + client-secret: ${OAUTH2_GITHUB_CLIENT_SECRET} + scope: read:user,user:email + # 二期扩展示例: + # gitlab: + # client-id: ... + # authorization-grant-type: authorization_code + # google: + # client-id: ... +``` + +Spring Security OAuth2 Client 原生支持多 Provider 并存,新增 Provider 只需: +1. `application.yml` 添加 registration 配置 +2. `CustomOAuth2UserService` 中按 `registrationId` 分支处理用户属性映射 +3. 前端登录页增加对应按钮(通过 `/api/v1/auth/providers` 自动发现) + +## 4. 核心接口设计 + +```java +// 自定义 OAuth2 用户服务,处理准入 + 用户映射 +@Service +public class CustomOAuth2UserService extends DefaultOAuth2UserService { + + @Override + public OAuth2User loadUser(OAuth2UserRequest request) { + OAuth2User oAuth2User = super.loadUser(request); + String registrationId = request.getClientRegistration().getRegistrationId(); + + // 提取标准化 claims(传入 accessToken 用于调用 Provider API,如 GitHub /user/emails) + OAuthClaims claims = OAuthClaimsExtractor.extract(registrationId, oAuth2User, request.getAccessToken()); + + // 准入策略判定(基于 claims,与 Provider 无关) + AccessDecision decision = accessPolicy.evaluate(claims); + if (decision == AccessDecision.DENY) { + throw new OAuth2AccessDeniedException("Access denied by policy"); + } + if (decision == AccessDecision.PENDING_APPROVAL) { + // 创建 PENDING 用户但不返回有效 principal,不建立业务 Session + identityBindingService.createPendingUser(registrationId, claims); + throw new AccountPendingException("Account pending approval"); + } + + // 绑定或创建平台用户(仅 ALLOW 才走到这里) + UserAccount account = identityBindingService.bindOrCreate(registrationId, claims); + if (account.getStatus() == UserStatus.DISABLED) { + throw new AccountDisabledException("Account is disabled"); + } + + return new PlatformOAuth2User(account, oAuth2User.getAuthorities()); + } +} + +// 按 Provider 提取标准化 claims(每个 Provider 有自己的可信字段契约) +public class OAuthClaimsExtractor { + public static OAuthClaims extract(String registrationId, OAuth2User user, + OAuth2AccessToken accessToken) { + return switch (registrationId) { + case "github" -> extractGitHub(user, accessToken); + // 后续扩展其他 Provider + default -> throw new OAuth2AuthenticationException("Unsupported provider: " + registrationId); + }; + } + + // GitHub: 公开 email 可能为空,需调用 /user/emails API 获取已验证邮箱 + private static OAuthClaims extractGitHub(OAuth2User user, OAuth2AccessToken accessToken) { + String verifiedEmail = GitHubEmailFetcher.fetchVerifiedEmail(accessToken); + return new OAuthClaims( + "github", + String.valueOf(user.getAttribute("id")), + verifiedEmail, // 从 /user/emails 获取的已验证邮箱,可能为 null + verifiedEmail != null, // 只有确认 verified 才为 true + user.getAttribute("login"), + Map.of("avatar_url", user.getAttribute("avatar_url")) + ); + } + + // GitHubEmailFetcher: 调用 GitHub /user/emails API, + // 返回 primary + verified 的邮箱,无则返回 null +} +``` + +### 4.1 多 Provider 账号合并策略 + +同一个员工通过不同 OAuth Provider 登录时,可能产生多个 `user_account`。 + +一期策略:默认关闭自动合并,仅支持管理员手动合并。 + +- 一期 GitHub-only:不需要自动合并,每个 Provider 登录独立创建用户 +- 多 Provider 上线时,再引入显式绑定/合并流程(用户主动发起 + 邮箱验证确认) +- 管理员可在后台手动合并两个 user_account(合并 identity_binding、迁移 skill ownership、合并角色取并集) + +合并操作规则: +- 合并操作写入审计日志 +- 合并后原 user_account 标记为 `MERGED`,保留记录不物理删除 +- 预留扩展位:未来可配置 `astron.identity.auto-merge-on-verified-email=true` 开启基于已验证邮箱的自动合并 + +## 5. CLI 认证(API Token) + +- Token 格式:`ask_` 前缀 + 随机字符串 +- 存储:只存 SHA-256 哈希,明文只展示一次 +- 校验:从 `Authorization: Bearer ask_xxx` 提取 → 哈希比对 → 加载关联用户 → 检查用户状态 +- 作用域:`skill:read`, `skill:publish`, `skill:delete`, `token:manage` +- 天然无状态,多 Pod 安全 + +## 6. RBAC 授权判定 + +``` +权限判定 = 平台角色权限(role → permission 查询) ∪ 命名空间角色(namespace_member.role) +``` + +一期即上线完整 RBAC,平台角色按最小权限拆分: + +| 平台角色 | 职责 | +|---------|------| +| `SUPER_ADMIN` | 全部权限,硬判定短路 | +| `SKILL_ADMIN` | 全局空间审核、提升审核、隐藏/撤回技能 | +| `USER_ADMIN` | 准入审批、封禁/解封、角色分配(不可分配 SUPER_ADMIN) | +| `AUDITOR` | 审计日志只读 | + +- 命名空间权限仍由 `namespace_member.role`(OWNER / ADMIN / MEMBER)决定 +- 一个用户可持有多个平台角色 +- 普通用户无平台角色,仅通过 namespace 成员关系获得操作权限 + +判定逻辑: +1. 从 SecurityContext 获取当前用户 +2. 检查用户状态(`DISABLED` → 拒绝所有操作) +3. 查询用户的平台角色(`user_role_binding` → `role` → `role_permission`) +4. `SUPER_ADMIN` 短路:直接通过所有权限检查 +5. 如果涉及命名空间资源,查询用户在该命名空间的角色(`namespace_member.role`) +6. 检查命名空间状态(`FROZEN` → 拒绝写操作) +7. 合并平台权限 + 命名空间角色,判定是否满足 + +| 操作 | 所需权限 | 判定逻辑 | +|------|---------|---------| +| 提交发布审核 | `skill:publish` | 用户是该 namespace 的 MEMBER 以上,且 namespace 非 FROZEN | +| 管理技能(归档/版本管理) | `skill:manage` | namespace ADMIN 以上,或 owner 本人 | +| 提升到全局 | `skill:promote` | namespace ADMIN 以上,或 owner 本人 | +| 审核团队空间技能 | `review:approve` | 该 namespace 的 ADMIN,或持有 SKILL_ADMIN / SUPER_ADMIN | +| 审核全局空间技能 | `review:approve` | 持有 SKILL_ADMIN / SUPER_ADMIN | +| 审核提升申请 | `promotion:approve` | 持有 SKILL_ADMIN / SUPER_ADMIN | +| 隐藏/撤回技能 | `skill:manage` | 持有 SKILL_ADMIN / SUPER_ADMIN | +| 管理用户角色 | `user:manage` | 持有 USER_ADMIN / SUPER_ADMIN | +| 审批用户准入 | `user:approve` | 持有 USER_ADMIN / SUPER_ADMIN | +| 查看审计日志 | `audit:read` | 持有 AUDITOR / SUPER_ADMIN | + +权限主轴说明: +- namespace role 是权限主轴,namespace ADMIN 对空间内所有 skill 有完整管理权,不受 owner 限制 +- `owner_id` 语义为"主要维护人",owner 作为 MEMBER 时仅可管理自己创建的 skill +- 企业场景人员流动频繁,owner 离职后 namespace ADMIN 仍能完整管理所有技能 + +### 6.1 审核与提升 API 路径适用范围 + +| API 路径 | 适用范围 | 权限要求 | +|----------|---------|---------| +| `POST /api/v1/admin/reviews/{id}/approve` | 全局空间审核 | SKILL_ADMIN / SUPER_ADMIN | +| `POST /api/v1/admin/promotions/{id}/approve` | 提升到全局审核 | SKILL_ADMIN / SUPER_ADMIN | +| `POST /api/v1/namespaces/{slug}/reviews/{id}/approve` | 团队空间内发布审核 | 该空间 ADMIN | +| `GET /api/v1/admin/audit-logs` | 审计日志查询 | AUDITOR / SUPER_ADMIN | +| `PUT /api/v1/admin/users/{id}/roles` | 用户角色管理 | USER_ADMIN / SUPER_ADMIN | +| `POST /api/v1/admin/users/{id}/approve` | 用户准入审批 | USER_ADMIN / SUPER_ADMIN | + +SUPER_ADMIN 和持有对应角色的用户均可通过 Admin API 操作,团队管理员只能通过 Namespace API 审核本空间。 + +## 7. Session 设计 + +- 存储:Spring Session + Redis(必须,多 Pod 环境刚需) +- 序列化:JSON +- 过期:默认 8 小时,Redis TTL 自动清理 + +### 7.1 Session 内容 + +Session 中存储以下字段: +- `userId`:平台用户 ID +- `displayName`:展示名 +- `oauthProvider`:登录使用的 OAuth Provider +- `currentNamespaceId`:当前选中的命名空间(可选) +- `platformRoles`:平台角色列表(如 `["SKILL_ADMIN", "AUDITOR"]`),登录时从 `user_role_binding` → `role` 查询写入 +- `roleVersion`:角色版本号,用于缓存一致性 + +### 7.2 角色缓存一致性机制 + +平台角色变更需要即时生效(如撤销审核权限),不能等 Session 过期: + +1. 每次请求时从 Session 读取 `roleVersion` +2. 与 Redis 中的 `user:{userId}:roleVersion` 比对 +3. 版本一致 → 直接使用 Session 中的 `platformRoles` +4. 版本不一致 → 从数据库重新加载角色,更新 Session + +管理员修改用户角色时,递增 Redis 中该用户的 `roleVersion`。 + +## 8. CSRF 防护 + +采用 Cookie-to-Header 模式: +- 后端设置 `XSRF-TOKEN` Cookie(`HttpOnly=false`) +- 前端从 Cookie 读取 Token,放入请求 Header `X-XSRF-TOKEN` +- 后端校验 Header 与 Cookie 是否一致 +- CLI API(`/api/v1/cli/**`)豁免 CSRF(使用 API Token 认证,无 Cookie) + +## 9. 前端权限控制 + +### 9.1 `/api/v1/auth/me` 响应结构 + +```json +{ + "data": { + "userId": 42, + "displayName": "zhangsan", + "email": "zhangsan@company.com", + "avatarUrl": "https://...", + "oauthProvider": "github", + "platformRoles": ["SKILL_ADMIN", "AUDITOR"], + "namespaces": [ + { "slug": "ai-team", "role": "ADMIN" }, + { "slug": "global", "role": "MEMBER" } + ] + } +} +``` + +前端权限判定基于 `platformRoles` + `namespaces[].role`,后端通过 `role_permission` 表查询权限码。 + +### 9.2 usePermission() Hook + +```typescript +function usePermission() { + const { data: me } = useQuery({ queryKey: ['auth', 'me'], queryFn: fetchMe }) + + const hasRole = (role: string) => me?.platformRoles.includes(role) ?? false + const isSuperAdmin = () => hasRole('SUPER_ADMIN') + const isSkillAdmin = () => hasRole('SKILL_ADMIN') || isSuperAdmin() + const isUserAdmin = () => hasRole('USER_ADMIN') || isSuperAdmin() + const isAuditor = () => hasRole('AUDITOR') || isSuperAdmin() + + return { + isLoggedIn: !!me, + isSuperAdmin, + isSkillAdmin, + isUserAdmin, + isAuditor, + + // 命名空间角色判定 + getNamespaceRole: (slug: string) => + me?.namespaces.find(n => n.slug === slug)?.role, + isNamespaceAdmin: (slug: string) => + ['OWNER', 'ADMIN'].includes(me?.namespaces.find(n => n.slug === slug)?.role ?? ''), + isNamespaceMember: (slug: string) => + ['OWNER', 'ADMIN', 'MEMBER'].includes(me?.namespaces.find(n => n.slug === slug)?.role ?? ''), + } +} +``` + +### 9.3 路由级守卫 + +在 TanStack Router `beforeLoad` 中判定: + +| 路由 | 条件 | +|------|------| +| `/dashboard/*` | 已登录 | +| `/dashboard/namespaces/{slug}/reviews` | 已登录 + 该 namespace 的 ADMIN 以上 | +| `/admin/*` | 已登录 + 持有任一平台角色(SUPER_ADMIN / SKILL_ADMIN / USER_ADMIN / AUDITOR) | + +不满足条件时:未登录 → 重定向登录;已登录但无权限 → 显示 403 页面。 + +### 9.4 操作级控制 + +| 场景 | 判定逻辑 | UI 行为 | +|------|---------|---------| +| 技能详情页"提交发布"按钮 | `isNamespaceMember(namespace)` | 非成员不显示 | +| 审核列表"通过/拒绝"按钮 | `isNamespaceAdmin(namespace) \|\| isSkillAdmin()` | 无权限不显示 | +| 用户管理页 | `isUserAdmin()` | 无权限不显示 | +| 用户管理页"设为 SUPER_ADMIN" | `isSuperAdmin()` | 仅超管可见 | +| 审计日志页 | `isAuditor()` | 无权限不显示 | +| 技能详情页"归档"按钮 | `isNamespaceAdmin(namespace)` 或当前用户是 owner | 否则不显示 | +| 命名空间"添加成员"按钮 | `isNamespaceAdmin(namespace)` | 非管理员不显示 | +| 收藏/评分按钮 | `isLoggedIn` | 未登录时点击提示登录 | + +### 9.5 登录交互 + +``` +前端登录按钮 + │ + ▼ +window.location.href = '/oauth2/authorization/github' + │ + ▼ +(后端 OAuth2 流程,用户无感) + │ + ▼ +回调后重定向到前端 (如 /?login=success) + │ + ▼ +前端检测 URL 参数 → 调用 /api/v1/auth/me → 更新登录态 +``` + +前端无需引入额外 OAuth 库,登录流程完全由后端 Spring Security 处理。前端只需: +- 调用 `/api/v1/auth/providers` 获取可用 Provider 列表,动态渲染登录按钮 +- 处理登录后的重定向 +- 通过 `/api/v1/auth/me` 检测登录状态 + +### 9.6 安全边界原则 + +- 前端权限控制是 UX 优化,不是安全边界 +- 后端每个写操作接口独立校验权限,不信任前端判定 +- 前端隐藏按钮 ≠ 安全,用户可以直接调 API,后端必须拦截 diff --git a/docs/04-search-architecture.md b/docs/04-search-architecture.md new file mode 100644 index 00000000..7b6d72e7 --- /dev/null +++ b/docs/04-search-architecture.md @@ -0,0 +1,136 @@ +# Astron Skills 搜索架构 + +## 1 SPI 接口 + +```java +public interface SearchIndexService { + void index(SkillSearchDocument doc); + void batchIndex(List docs); + void remove(Long skillId); +} + +public interface SearchQueryService { + SearchResult search(SearchQuery query); +} + +public interface SearchRebuildService { + void rebuildAll(); + void rebuildByNamespace(Long namespaceId); + void rebuildBySkill(Long skillId); +} +``` + +## 2 SearchQuery 模型 + +```java +public record SearchQuery( + String keyword, + Long namespaceId, // 可选,指定空间搜索 + String namespaceSlug, // 可选 + SearchVisibilityScope scope, // ACL 投影,由应用服务层计算注入 + SortField sortBy, // RELEVANCE / DOWNLOADS / RATING / NEWEST + int page, + int size +) {} + +// 搜索可见范围投影,由应用服务层根据当前用户计算 +public record SearchVisibilityScope( + boolean includeAllPublic, // 是否包含所有 PUBLIC 技能 + Set memberNamespaceIds, // 用户是 MEMBER 的 namespace(可见 NAMESPACE_ONLY) + Set adminNamespaceIds, // 用户是 ADMIN 的 namespace(可见 PRIVATE) + Long userId // 当前用户 ID(可见自己的 PRIVATE skill),匿名为 null +) {} +``` + +ACL 投影计算规则: +- 匿名用户:`includeAllPublic=true`,其余为空集,`userId=null` +- 已登录用户:`includeAllPublic=true`,`memberNamespaceIds` = 用户所属空间,`adminNamespaceIds` = 用户是 ADMIN 以上的空间,`userId` = 当前用户 ID + +一期 MySQL 实现中,`SearchVisibilityScope` 转换为 WHERE 条件: +```sql +WHERE (visibility = 'PUBLIC') + OR (visibility = 'NAMESPACE_ONLY' AND namespace_id IN (:memberNamespaceIds)) + OR (visibility = 'PRIVATE' AND (namespace_id IN (:adminNamespaceIds) OR owner_id = :userId)) +``` + +迁移到 ES 时,`SearchVisibilityScope` 可直接映射为 bool query 的 should/filter 子句。 + +## 3 搜索文档表 skill_search_document + +一个 skill 对应一条搜索文档,内容取 `latest_version_id` 对应版本。版本发布时自动更新该条文档。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | bigint | | +| skill_id | bigint | 唯一,一 skill 一条 | +| namespace_id | bigint | 用于空间过滤 | +| owner_id | bigint | 用于 PRIVATE 可见性判定 | +| title | varchar(256) | | +| summary | varchar(512) | | +| keywords | varchar(512) | | +| search_text | text | SKILL.md 正文 + frontmatter 拼接 | +| visibility | enum | 冗余,避免搜索时 join | +| status | enum | | +| updated_at | datetime | | + +唯一约束:`(skill_id)` + +MySQL Full-Text Index 建在 `(title, summary, keywords, search_text)` 上。 + +## 4 索引写入时机 + +以下场景触发搜索文档更新(upsert by skill_id): +- 审核通过(`PENDING_REVIEW → PUBLISHED`):`latest_version_id` 自动更新,用新版本内容更新搜索文档 +- 技能状态变更(隐藏/归档/恢复):更新搜索文档的 status 字段 + +## 5 搜索演进路线 + +### 5.1 一期数据建模约束 + +一期"每个 skill 一条搜索文档、内容永远取 latest_version_id"是有意的简化。这个模型在以下场景下会不够用: + +- 版本级检索(搜索某个旧版本的内容) +- 自定义标签/通道检索(搜索 `@beta` 标签指向的版本内容) +- 向量 chunk 索引(一个 skill 的 SKILL.md 拆成多个 embedding chunk) + +这些场景不是简单换 provider 能解决的,需要改表结构和索引写入逻辑。 + +### 5.2 演进阶段 + +| 阶段 | 实现 | 索引粒度 | 切换方式 | +|------|------|---------|---------| +| 一期 | MySQL Full-Text | 每 skill 一条(latest_version_id) | 默认 | +| 二期 | ES / OpenSearch | 每 skill_version 一条 + skill 聚合文档 | 配置 `search.provider=elasticsearch` | +| 三期 | 向量检索 | 每 skill_version 多条(chunk 级) | 配置 `search.provider=vector` | +| 四期 | 混合排序 | 关键词 + 向量混合 | 配置 `search.provider=hybrid` | + +### 5.3 SPI 演进策略 + +一期 SPI 接口(`SearchIndexService` / `SearchQueryService`)的入参是 `SkillSearchDocument`(skill 粒度)。二期切换到 ES 时: + +1. 新增 `SkillVersionSearchDocument` 模型(version 粒度) +2. `SearchIndexService` 新增 `indexVersion()` 方法(向下兼容,一期实现空方法) +3. ES 实现同时写入 skill 聚合文档 + version 文档 +4. `SearchQueryService.search()` 的返回结果不变(仍返回 skill 级摘要),内部实现切换为 ES 查询 + +这意味着二期切换不是零成本的——需要新增模型、扩展 SPI、重建索引。但一期不为此过度设计,SPI 抽象保证了切换时不需要改业务层代码。 + +通过 `@ConditionalOnProperty` 或自定义 SPI 加载机制切换。 + +## 6 分布式安全 + +`rebuildAll()` / `rebuildByNamespace()` 执行前获取 Redis 分布式锁(key: `search:rebuild:{scope}`,TTL: 10min),获取失败则跳过。 + +## 7 MySQL 全文搜索中文支持 + +MySQL Full-Text Index 必须使用 ngram parser: + +```sql +ALTER TABLE skill_search_document +ADD FULLTEXT INDEX ft_search (title, summary, keywords, search_text) +WITH PARSER ngram; +``` + +配置 `ngram_token_size=2`(my.cnf)。 + +已知局限:ngram 分词精度不如专业搜索引擎,中文搜索体验有限。建议 Phase 2 完成后评估搜索效果,如不满足需求则在 Phase 3 提前引入 ES。 diff --git a/docs/05-business-flows.md b/docs/05-business-flows.md new file mode 100644 index 00000000..0db4c125 --- /dev/null +++ b/docs/05-business-flows.md @@ -0,0 +1,273 @@ +# Astron Skills 核心业务流 + +## 1 发布流程 + +``` +用户提交发布 + │ + ▼ +① 身份与权限校验(用户是否为该 namespace 的 MEMBER 以上) + │ + ▼ +② 技能包校验 + - SKILL.md 存在性、frontmatter 格式 + - 文件类型白名单、单文件大小限制、总包大小限制 + - 版本号 semver 合法性、不与已有版本冲突 + - [扩展点] PrePublishValidator 链(一期空实现) + │ + ▼ +③ 写入对象存储临时区(文件逐个上传到 `tmp/{uploadId}/{filePath}`,记录 SHA-256) + │ + ▼ +④ 持久化数据 + - 创建 skill_version (status=DRAFT, file_transfer_status=PENDING) + - 创建 skill_file 记录 + - 解析 SKILL.md frontmatter → parsed_metadata_json + - 生成 manifest_json + - 异步将文件从 `tmp/` 转正到 `skills/{skillId}/{versionId}/{filePath}` + - 转正成功 → file_transfer_status=COMPLETED + - 转正失败 → file_transfer_status=FAILED,记录失败原因 + │ + ▼ +⑤ 提交审核(前置检查:file_transfer_status 必须为 COMPLETED,否则拒绝提审) + - skill_version.status → PENDING_REVIEW + - 创建 review_task (status=PENDING) + │ + ▼ +⑥ 审核(人工) + ├── 通过 → skill_version.status → PUBLISHED + │ review_task.status → APPROVED(乐观锁) + │ 更新 skill.latest_version_id(自动跟随最新已发布版本) + │ 同步写入审计日志 + │ 异步触发: 搜索索引写入(取 latest_version_id 对应版本内容) + │ + └── 拒绝 → skill_version.status → REJECTED + review_task.status → REJECTED(乐观锁) + 记录 reject_reason + 同步写入审计日志 +``` + +④ 和 ⑤ 分开,给用户一个检查草稿的机会。CLI 发布走同样的流程。 + +### 对象存储临时区与 GC + +- 上传阶段文件写入 `tmp/{uploadId}/{filePath}` +- 数据库事务提交成功后,异步将文件从 `tmp/` copy 到正式路径 `skills/{skillId}/{versionId}/{filePath}`,完成后删除 `tmp/` 副本 +- 定时 GC 任务:清理超过 24h 的 `tmp/` 前缀对象(覆盖事务失败、用户取消、流程中断等场景) +- 如果数据库事务失败,`tmp/` 中的文件由 GC 自动清理,不产生孤儿对象 + +### 文件转正补偿机制 + +`skill_version` 增加 `file_transfer_status` 字段(`PENDING` / `COMPLETED` / `FAILED`),用于追踪异步转正状态。 + +安全门控: +- 提交审核(DRAFT → PENDING_REVIEW):前置检查 `file_transfer_status = COMPLETED`,否则返回 400 +- 下载接口:前置检查 `file_transfer_status = COMPLETED`,否则返回 404 + +失败重试: +- 转正失败时标记 `file_transfer_status = FAILED` +- 定时任务每 5 分钟扫描 `file_transfer_status = PENDING` 且 `created_at > 5min ago` 或 `file_transfer_status = FAILED` 的记录,重试转正 +- 最多重试 3 次,超过后标记为 FAILED 并保留,用户可在草稿页看到"文件处理失败"提示,可手动触发重试或删除该版本重新上传 +- `tmp/` 中的源文件在转正成功前不删除,确保重试有源可用 + +### CLI publish 请求规范 + +``` +POST /api/v1/cli/publish +Content-Type: multipart/form-data +Parts: + - file: zip 包(必需) + - namespace: 目标命名空间 slug(必需) + - auto_submit: boolean(可选,默认 false,为 true 时自动提交审核) +``` + +CLI 默认行为:上传 → 创建 DRAFT → 自动提交审核(`auto_submit=true`)。 +Web 端默认行为:上传 → 创建 DRAFT → 用户预览确认 → 手动提交审核。 + +### CLI publish 异步协议 + +`auto_submit=true` 时,服务端在文件转正完成后自动提交审核,CLI 不需要额外调用 submit-review。 + +``` +CLI 调用 POST /api/v1/cli/publish (auto_submit=true) + │ + ▼ +服务端返回 202 Accepted + publishId + 初始状态 + │ + ▼ +CLI 轮询 GET /api/v1/cli/publish/{publishId}/status + │ + ├── TRANSFERRING → 文件转正中,继续轮询(建议间隔 2s) + ├── SUBMITTED → 文件转正完成 + 已自动提交审核,CLI 结束 + ├── DRAFT → auto_submit=false 时,文件转正完成但未提审,CLI 结束 + └── FAILED → 文件转正失败,返回错误原因,CLI 提示用户 +``` + +`/api/v1/cli/publish/{publishId}/status` 响应: + +```json +{ + "data": { + "publishId": "uuid", + "skillVersionId": 123, + "fileTransferStatus": "COMPLETED", + "versionStatus": "PENDING_REVIEW", + "error": null + } +} +``` + +## 2 团队技能提升到全局空间(派生发布) + +不直接修改原 skill 的 `namespace_id`,而是在全局空间创建新的 skill,保留来源追溯。原团队 skill 继续存在,安装坐标 `@team/skill` 不受影响。 + +``` +团队空间技能(已发布) + │ + ▼ +① 技能 owner 或 namespace admin 发起"提升到全局"申请 + │ + ▼ +② 创建 promotion_request (source_skill_id, source_version_id, target_namespace_id, status=PENDING) + │ + ▼ +③ 平台管理员审核 + ├── 通过 → + │ ① 在全局空间创建新 skill(source_skill_id = 原 skill ID) + │ ② 复制 source_version_id 对应版本的文件和元数据到新 skill(严格使用申请时指定的版本,不取最新) + │ ③ 新 skill.visibility = PUBLIC + │ ④ promotion_request.target_skill_id = 新 skill ID,status → APPROVED + │ ⑤ 搜索索引写入新 skill,同步写入审计日志 + │ (提升关系唯一事实来源是 promotion_request,UI 查询"是否已提升"通过该表判定) + │ + └── 拒绝 → 记录原因,原技能不受影响 +``` + +后续版本更新: +- 全局空间的新 skill 由其 owner 独立管理版本 +- 原团队 skill 可继续独立迭代 +- 两者版本不自动同步,如需同步由 owner 手动操作 + +## 3 下载流程 + +``` +下载请求 + │ + ▼ +① 校验技能状态(ACTIVE)、版本状态(PUBLISHED) + │ + ▼ +② 可见性检查 + - PUBLIC: 任何人(包括匿名用户) + - NAMESPACE_ONLY: 该 namespace 的成员(需登录) + - PRIVATE: owner 本人 + 该 namespace 的 ADMIN 以上(需登录) + │ + ▼ +③ 返回预生成包或按文件清单打包 + │ + ▼ +④ 审计与统计 + - audit_log 同步写入(记录下载人/IP/版本) + - download_count 异步更新(原子 SQL: download_count = download_count + 1) + - 匿名下载:审计记录 IP + User-Agent,不关联用户 + - 已登录下载:审计记录用户 ID +``` + +### download_count 热点行优化预案 + +一期使用原子 SQL 直接更新,可接受。如出现热点行瓶颈,切换为: +1. Redis `INCR` 做实时计数(key: `skill:downloads:{skillId}`) +2. 定时任务每 5 分钟批量回写 MySQL +3. 查询时合并 MySQL 存量 + Redis 增量 + +## 4 搜索流程 + +``` +搜索请求 (keyword, namespaceSlug?, sortBy) + │ + ▼ +① 构建 SearchQuery + - 匿名用户:visibility 限定为 PUBLIC + - 已登录用户:根据命名空间成员关系计算可见范围 + │ + ▼ +② SearchQueryService.search(query) + │ + ▼ +③ 返回分页结果(技能摘要 + 命名空间信息 + 评分 + 下载量) +``` + +## 5 收藏流程 + +``` +收藏/取消收藏(需登录)→ 校验权限 → 写入/删除 skill_star +→ 异步更新 skill.star_count(原子 SQL) +``` + +## 6 评分流程 + +``` +提交评分 (score: 1-5)(需登录)→ 校验权限 → 写入/更新 skill_rating +→ 异步重算 skill.rating_avg 和 rating_count(SELECT AVG + Redis 分布式锁防重复重算) +``` + +## 7 异步事件汇总 + +| 事件 | 触发时机 | 消费方 | +|------|---------|--------| +| `SkillPublishedEvent` | 审核通过 | 搜索索引写入 | +| `SkillYankedEvent` | 版本撤回 | 搜索索引移除 | +| `SkillDownloadedEvent` | 下载完成 | 下载计数 | +| `SkillStarredEvent` | 收藏/取消 | 收藏计数 | +| `SkillRatedEvent` | 评分提交 | 评分重算 | +| `ReviewCompletedEvent` | 审核完成 | 通知提交者(一期可选) | +| `SkillPromotedEvent` | 提升到全局 | 搜索索引写入(新 skill) | + +一期用 Spring ApplicationEvent + `@Async` 实现,后续可替换为消息队列。 + +### 审计日志写入策略 + +审计日志统一同步落库,与业务操作在同一请求内同步写入,不走异步事件。审计是企业内部平台的刚性需求,不可容忍丢失。 + +异步事件仅用于搜索索引、计数器等可容忍延迟的场景。如果后续需要更强一致性,引入 outbox 模式,不依赖 ApplicationEvent + @Async 承担可靠性。 + +### 异步事件可靠性保障 + +Spring ApplicationEvent + @Async 存在 Pod 被杀时事件丢失的风险。补充以下兜底机制: + +- 搜索索引:定时任务每小时检查 `skill_version.status = PUBLISHED` 但 `skill_search_document` 中无对应记录的版本,补建索引 +- 计数器:可接受少量丢失,定时任务每天凌晨从 `skill_star` / `skill_rating` 表重算修正 +- 优雅停机:`@Async` 线程池配置 `awaitTerminationSeconds=25`,配合 30s shutdown timeout + +## 8 分布式并发安全措施 + +| 操作 | 并发控制方式 | +|------|-------------| +| 审核通过/拒绝 | 乐观锁:`UPDATE review_task SET status=? WHERE id=? AND version=?` | +| 版本发布 | 唯一约束:`(skill_id, version)` | +| 计数器更新 | 原子 SQL:`SET count = count + 1` | +| 评分重算 | 异步 + Redis 分布式锁防重复重算 | +| 写操作幂等 | Redis 存储 `X-Request-Id`,TTL 24h | + +### 幂等去重规范 + +基于 `idempotency_record` 表实现完整幂等: + +- `X-Request-Id` 由客户端生成(UUID v4 格式) +- 客户端不传时,服务端自动生成但不做幂等去重 + +去重流程: +1. Redis `SETNX` key=`idempotent:{requestId}`(快速去重缓存,TTL=24h) + - key 已存在:查询 `idempotency_record` 表返回原始结果 +2. key 不存在:插入 `idempotency_record`(status=`PROCESSING`) +3. 执行业务逻辑 +4. 成功:更新 record 为 `COMPLETED`,填充 `resource_type` + `resource_id` + `response_status_code` +5. 失败:更新 record 为 `FAILED` +6. 重复请求时:查 record,COMPLETED 返回原始资源 ID,PROCESSING 返回 `409 Conflict`,FAILED 允许重试 + +适用范围:所有 POST/PUT/DELETE 写操作(发布、提审、创建 Token 等) + +异常恢复策略: +- Redis key 存在但 `idempotency_record` 无记录(进程在两步之间崩溃):视为脏状态,删除 Redis key,允许请求正常重入 +- `idempotency_record.status = FAILED`:删除对应 Redis key,允许客户端用相同 `request_id` 重试 +- `idempotency_record.status = PROCESSING` 超过 5 分钟未更新:视为僵死,标记为 FAILED,删除 Redis key,允许重试 diff --git a/docs/06-api-design.md b/docs/06-api-design.md new file mode 100644 index 00000000..d0b5397e --- /dev/null +++ b/docs/06-api-design.md @@ -0,0 +1,212 @@ +# Astron Skills API 设计 + +## 7.1 Public API(匿名可访问) + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/v1/skills` | 搜索/列表(匿名仅返回 PUBLIC 技能) | +| GET | `/api/v1/skills/{namespace}/{slug}` | 技能详情(PUBLIC 匿名可访问) | +| GET | `/api/v1/skills/{namespace}/{slug}/versions` | 版本列表 | +| GET | `/api/v1/skills/{namespace}/{slug}/versions/{version}` | 版本详情 | +| GET | `/api/v1/skills/{namespace}/{slug}/versions/{version}/files` | 文件清单 | +| GET | `/api/v1/skills/{namespace}/{slug}/versions/{version}/file?path=...` | 读取单个文件(query param 避免路径中 / 的解析问题) | +| GET | `/api/v1/skills/{namespace}/{slug}/download` | 下载默认安装版本(latest_version_id 指向的版本) | +| GET | `/api/v1/skills/{namespace}/{slug}/versions/{version}/download` | 下载指定版本包 | +| GET | `/api/v1/namespaces` | 公开命名空间列表 | +| GET | `/api/v1/namespaces/{slug}` | 命名空间详情 | + +Public API 的可见性规则: +- `PUBLIC` 技能:匿名和已登录用户均可访问 +- `NAMESPACE_ONLY` 技能:仅该命名空间成员可访问(需登录) +- `PRIVATE` 技能:owner 本人 + 该 namespace 的 ADMIN 以上可访问(需登录) + +## 7.2 Auth API(OAuth2 登录相关) + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/oauth2/authorization/github` | 发起 GitHub OAuth 登录(Spring Security 内置) | +| GET | `/login/oauth2/code/github` | GitHub OAuth 回调(Spring Security 内置) | +| GET | `/api/v1/auth/me` | 当前用户信息(未登录返回 401) | +| POST | `/api/v1/auth/logout` | 登出(清除 Session) | +| GET | `/api/v1/auth/providers` | 可用的 OAuth Provider 列表(前端渲染登录按钮用) | + +`/api/v1/auth/providers` 响应示例: + +```json +{ + "data": [ + { "id": "github", "name": "GitHub", "authorizationUrl": "/oauth2/authorization/github" } + ] +} +``` + +前端根据此接口动态渲染登录按钮,新增 Provider 无需改前端代码。 + +## 7.3 Authenticated API(需登录) + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/api/v1/skills/{namespace}/{slug}/star` | 收藏 | +| DELETE | `/api/v1/skills/{namespace}/{slug}/star` | 取消收藏 | +| POST | `/api/v1/skills/{namespace}/{slug}/rating` | 评分 | +| GET | `/api/v1/me/stars` | 我的收藏列表 | +| GET | `/api/v1/me/skills` | 我发布的技能列表 | + +### 草稿与审核提交 + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/api/v1/skills/{namespace}/{slug}/versions/{version}/submit-review` | 将 DRAFT 版本提交审核(前置:file_transfer_status=COMPLETED) | +| POST | `/api/v1/skills/{namespace}/{slug}/versions/{version}/withdraw-review` | 撤回提审(PENDING_REVIEW → DRAFT,同时删除关联的 PENDING review_task) | +| GET | `/api/v1/skills/{namespace}/{slug}/versions/{version}/draft` | 查看草稿详情(owner 或 namespace ADMIN 以上) | + +### 标签管理 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/v1/skills/{namespace}/{slug}/tags` | 列出标签 | +| PUT | `/api/v1/skills/{namespace}/{slug}/tags/{tagName}` | 创建/移动自定义标签(`latest` 为系统保留标签,不可通过此接口操作) | +| DELETE | `/api/v1/skills/{namespace}/{slug}/tags/{tagName}` | 删除自定义标签(`latest` 不可删) | + +### 技能生命周期管理 + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/api/v1/skills/{namespace}/{slug}/archive` | 归档技能(namespace ADMIN 或 owner) | +| POST | `/api/v1/skills/{namespace}/{slug}/unarchive` | 恢复归档(namespace ADMIN 或 owner) | +| DELETE | `/api/v1/skills/{namespace}/{slug}/versions/{version}` | 删除 DRAFT/REJECTED 版本 | + +## 7.4 Token API(需登录) + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/api/v1/tokens` | 创建 API Token | +| GET | `/api/v1/tokens` | 列出我的 Token | +| DELETE | `/api/v1/tokens/{id}` | 吊销 Token | + +## 7.5 CLI API(API Token 认证) + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/v1/cli/whoami` | Token 对应的用户信息 | +| POST | `/api/v1/cli/publish` | 发布技能包(返回 202 + publishId) | +| GET | `/api/v1/cli/publish/{publishId}/status` | 查询发布状态(文件转正 + 提审进度) | +| POST | `/api/v1/cli/publish/submit-review` | 手动提交审核(auto_submit=false 时使用) | +| GET | `/api/v1/cli/resolve/{namespace}/{slug}` | 解析版本 | +| GET | `/api/v1/cli/check/{namespace}/{slug}/{version}` | 本地哈希与远端比对 | + +### ClawHub CLI 协议兼容层 + +一期不仅提供 Astron 自有 CLI API,还必须暴露一组兼容 ClawHub CLI 的 registry API。 + +- 目标:让现有 ClawHub CLI 可通过配置 registry base URL 直接对接 Astron Skills +- 范围:覆盖 ClawHub CLI 所依赖的查询、版本解析、下载、发布、校验等核心接口 +- 要求:兼容层优先保持 ClawHub CLI 既有请求/响应语义;若内部领域模型不同,通过 adapter 层完成协议转换,而不是要求客户端适配 Astron 私有协议 +- 要求:兼容层纳入 OpenAPI 或独立兼容协议文档,并作为正式对外契约维护 +- 要求:兼容层与 Astron 自有 `/api/v1/cli/**` 并存,二者共享同一套权限、审计、限流与领域服务 +- 非目标:前端页面不直接依赖兼容层;兼容层用于服务已有 ClawHub CLI 和相关自动化脚本 + +兼容层最少需要覆盖的能力类别: + +- Registry metadata:技能查询、技能详情、版本列表、标签/默认版本解析 +- Artifact resolution:按技能坐标或版本解析下载地址/下载流 +- Publish workflow:包上传、发布状态查询、提交审核 +- Integrity check:版本存在性校验、摘要/哈希比对、whoami/token 上下文确认 + +如 ClawHub CLI 的现有协议与 Astron 自有接口存在差异,文档以“兼容 ClawHub CLI 协议”为准,Astron 内部 API 可继续保持当前风格。 + +## 7.6 Admin API(需对应平台角色) + +Admin API 按最小权限拆分,不再统一要求 SUPER_ADMIN: + +### 技能治理(需 SKILL_ADMIN / SUPER_ADMIN) + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/v1/admin/reviews` | 待审核列表 | +| GET | `/api/v1/admin/reviews/{id}` | 审核详情 | +| POST | `/api/v1/admin/reviews/{id}/approve` | 通过审核 | +| POST | `/api/v1/admin/reviews/{id}/reject` | 拒绝审核 | +| GET | `/api/v1/admin/promotions` | 待审核提升申请列表 | +| GET | `/api/v1/admin/promotions/{id}` | 提升申请详情 | +| POST | `/api/v1/admin/promotions/{id}/approve` | 通过提升申请 | +| POST | `/api/v1/admin/promotions/{id}/reject` | 拒绝提升申请 | +| POST | `/api/v1/admin/skills/{id}/hide` | 隐藏技能 | +| POST | `/api/v1/admin/skills/{id}/unhide` | 恢复技能 | +| POST | `/api/v1/admin/skills/{id}/yank/{versionId}` | 撤回已发布版本 | + +### 用户治理(需 USER_ADMIN / SUPER_ADMIN) + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/v1/admin/users` | 用户列表 | +| GET | `/api/v1/admin/users/{id}` | 用户详情 | +| PUT | `/api/v1/admin/users/{id}/roles` | 修改用户角色(USER_ADMIN 不可分配 SUPER_ADMIN) | +| POST | `/api/v1/admin/users/{id}/approve` | 审批待准入用户 | +| POST | `/api/v1/admin/users/{id}/disable` | 封禁用户 | +| POST | `/api/v1/admin/users/{id}/enable` | 解封用户 | + +### 审计(需 AUDITOR / SUPER_ADMIN) + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/v1/admin/audit-logs` | 审计日志查询 | + +## 7.7 Namespace 管理 API(需命名空间 OWNER 或 ADMIN) + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/api/v1/namespaces` | 创建命名空间 | +| PUT | `/api/v1/namespaces/{slug}` | 更新命名空间信息 | +| GET | `/api/v1/namespaces/{slug}/members` | 成员列表 | +| POST | `/api/v1/namespaces/{slug}/members` | 添加成员 | +| PUT | `/api/v1/namespaces/{slug}/members/{userId}` | 修改成员角色 | +| DELETE | `/api/v1/namespaces/{slug}/members/{userId}` | 移除成员 | +| GET | `/api/v1/namespaces/{slug}/reviews` | 该空间待审核列表 | +| POST | `/api/v1/namespaces/{slug}/reviews/{id}/approve` | 空间管理员审核通过 | +| POST | `/api/v1/namespaces/{slug}/reviews/{id}/reject` | 空间管理员审核拒绝 | +| POST | `/api/v1/namespaces/{slug}/skills/{skillId}/promote` | 申请提升到全局 | + +## 7.8 `latest` 语义说明 + +`latest` 自动跟随最新已发布版本,不可手动移动。 + +- `skill.latest_version_id`:每次审核通过自动更新,始终指向最新 PUBLISHED 版本 +- `latest` 标签:系统保留,只读,自动与 `latest_version_id` 同步 +- 自定义标签(如 `beta`、`stable-2026q1`):允许人工创建和移动,用于固定安装通道 + +| 场景 | 使用字段 | 说明 | +|------|---------|------| +| 搜索索引内容 | `latest_version_id` | 搜索文档取最新已发布版本内容 | +| `/download`(不带版本号) | `latest_version_id` | 下载最新已发布版本 | +| CLI `install @team/skill` | `latest_version_id` | 等同于 `@latest` | +| CLI `install @team/skill@beta` | `skill_tag` 查询 | 自定义标签指向的版本 | + +## 7.9 Rate Limiting + +分两阶段实施: + +### Phase 1:Ingress 层基础限流 + +通过 Nginx Ingress `limit-req` 按 IP 全局限流,覆盖认证、搜索、下载等匿名可访问接口,防止基本的滥用和爬虫。 + +### Phase 2:应用层精细限流 + +基于 Redis 滑动窗口,按用户/端点分类的精细限流。 + +| 端点类别 | 限流策略 | +|---------|---------| +| 搜索 API | 已登录 60 次/分钟,匿名 20 次/分钟(按 IP) | +| 下载 API | 已登录 120 次/分钟,匿名 30 次/分钟(按 IP) | +| 发布 API | 10 次/小时(按用户) | +| 认证 API | 30 次/分钟(按 IP) | + +触发限流时返回 `429 Too Many Requests` + `Retry-After` Header。 + +## 7.10 API 设计原则 + +- 统一响应格式:`{ code, message, data, timestamp }` +- 分页格式:`{ items, total, page, size }` +- 错误码体系:业务错误码 + HTTP 状态码配合 +- 版本策略:URL path 版本 `/api/v1/` +- 幂等性:写操作通过 `X-Request-Id` + Redis 去重(TTL 24h) diff --git a/docs/07-skill-protocol.md b/docs/07-skill-protocol.md new file mode 100644 index 00000000..0bb72671 --- /dev/null +++ b/docs/07-skill-protocol.md @@ -0,0 +1,121 @@ +# Astron Skills 技能包协议 + +## 8.1 OpenSkills 互操作边界 + +Astron 的目标是客户端可互操作:Astron CLI 安装的技能可以被 Claude Code / OpenSkills 兼容客户端发现和使用,反之亦然。 + +### 互操作层(Astron CLI 必须兼容) + +- SKILL.md 格式(frontmatter + markdown body) +- 技能包目录结构约定(SKILL.md + references/ + scripts/ + assets/) +- 四级目录优先级:Astron CLI 遵循 `.agent/skills` → `~/.agent/skills` → `.claude/skills` → `~/.claude/skills` 的发现顺序,与 OpenSkills/Claude 一致 +- 目录名作为 lookup key:安装后的目录名等于 `skill.slug`(即 SKILL.md 的 `name` 字段),客户端通过目录名发现技能 +- AGENTS.md `` 描述块格式:Astron CLI 生成的 AGENTS.md 索引区块与 OpenSkills 格式兼容 + +### 服务端职责边界 + +- 服务端返回技能元数据(name, description, version),不返回 `location` +- `location` 是客户端本地安装路径,由 CLI 根据安装目录计算生成,写入 AGENTS.md +- 服务端不生成、不修改 AGENTS.md,这是客户端职责 + +### Astron 私有扩展(不影响互操作) + +- `` / `` 区块格式:Astron CLI 可自定义,但必须保证 `` 节点格式与 OpenSkills 一致 +- progressive disclosure(按需加载技能内容):Astron CLI 自行实现 +- `.astron/metadata.json`:Astron 私有元数据,其他客户端可忽略 + +## 8.2 SKILL.md 规范 + +服务端必须兼容的格式: + +```yaml +--- +name: my-skill # 必需,kebab-case +description: When to use # 必需,1-2 句话 +--- + +# Markdown 正文(技能指令内容) +``` + +解析规则: +- `name` 和 `description` 为必需字段,缺失则校验失败 +- `name` 映射为 `skill.slug`(首次发布时),后续版本不可变更 +- `description` 映射为 `skill.summary` +- frontmatter 完整解析结果存入 `skill_version.parsed_metadata_json` + +平台扩展字段(可选,`x-astron-` 前缀): + +```yaml +--- +name: my-skill +description: When to use +x-astron-category: code-review +x-astron-runtime: claude-code # 预留 +x-astron-min-version: "1.0" # 预留 +--- +``` + +## 8.3 技能包目录结构 + +``` +my-skill/ +├── SKILL.md # 主入口文件(必需) +├── references/ # 参考资料(可选) +├── scripts/ # 脚本(可选) +└── assets/ # 静态资源(可选) +``` + +校验规则: +- 根目录必须包含 `SKILL.md` +- 文件类型白名单:`.md`, `.txt`, `.json`, `.yaml`, `.yml`, `.js`, `.ts`, `.py`, `.sh`, `.png`, `.jpg`, `.svg` +- 单文件大小限制:1MB(可配置) +- 总包大小限制:10MB(可配置) +- 文件数量限制:100 个(可配置) + +## 8.4 客户端安装目录约定 + +Astron CLI 遵循以下目录优先级,与 OpenSkills/Claude 保持互操作: + +| 优先级 | 路径 | 说明 | +|--------|------|------| +| 1 | `./.agent/skills/` | 项目级,universal 模式 | +| 2 | `~/.agent/skills/` | 全局级,universal 模式 | +| 3 | `./.claude/skills/` | 项目级,Claude 默认 | +| 4 | `~/.claude/skills/` | 全局级,Claude 默认 | + +安装后目录名等于 `skill.slug`(SKILL.md 的 `name` 字段),确保其他兼容客户端可通过目录名发现。 + +## 8.5 与 AGENTS.md 的关系 + +- Astron CLI 安装技能后,通过 `sync` 命令在 AGENTS.md 中生成 `` 描述块 +- `` 块包含 `name`、`description`、`location`(本地安装路径),格式与 OpenSkills 一致 +- `location` 由 CLI 根据实际安装路径计算,不由服务端提供 +- 服务端不直接生成或修改 AGENTS.md,这是客户端职责 + +## 8.6 客户端本地元数据文件(Astron 私有实现) + +以下为 Astron CLI 的私有实现细节,不属于互操作协议的一部分。其他客户端可忽略此文件。 + +CLI 安装后在本地写入 `.astron/metadata.json`: + +```json +{ + "source": "astron-skills", + "sourceType": "registry", + "registryUrl": "https://skills.example.com", + "namespace": "@ai-platform-team", + "skillSlug": "code-review", + "version": "1.2.0", + "installedAt": "2026-03-11T10:00:00Z", + "sha256": "abc123..." +} +``` + +## 8.7 版本解析规则 + +``` +install @team/my-skill → 最新已发布版本(latest_version_id) +install @team/my-skill@1.2.0 → 精确版本 +install @team/my-skill@latest → 等同于不带版本号(系统保留标签,只读) +install @team/my-skill@beta → beta 标签(自定义标签) +``` diff --git a/docs/08-frontend-architecture.md b/docs/08-frontend-architecture.md new file mode 100644 index 00000000..32c5eed9 --- /dev/null +++ b/docs/08-frontend-architecture.md @@ -0,0 +1,146 @@ +# Astron Skills 前端架构设计 + +## 1 技术栈 + +| 类别 | 选型 | 说明 | +|------|------|------| +| 框架 | React 19 + TypeScript | | +| 构建 | Vite | | +| 路由 | TanStack Router | | +| 数据获取 | TanStack Query | 管理所有服务端数据(API 响应缓存、加载/错误状态) | +| UI 组件 | shadcn/ui + Radix UI | | +| 样式 | Tailwind CSS | | +| 本地状态 | Zustand | 仅管理纯客户端状态 | +| API 客户端 | openapi-fetch + openapi-typescript | | +| 图标 | Lucide React | | + +### 1.1 Zustand 与 TanStack Query 职责边界 + +- **TanStack Query**:管理所有服务端数据(API 响应缓存、加载/错误状态) +- **Zustand**:仅管理纯客户端状态(UI 偏好、侧边栏展开、主题、当前选中的命名空间过滤等) +- 禁止在 Zustand 中缓存服务端数据 + +## 2 页面结构 + +### 2.1 门户区(公开,匿名可访问) + +| 页面 | 路径 | 说明 | +|------|------|------| +| 首页 | `/` | 精选/热门/最新、搜索入口 | +| 搜索页 | `/search` | 关键词搜索 + 过滤 + 排序 | +| 命名空间主页 | `/@{namespace}` | 空间介绍 + 技能列表 | +| 技能详情页 | `/@{namespace}/{slug}` | README 渲染、版本、评分、收藏、下载 | +| 版本历史 | `/@{namespace}/{slug}/versions` | 版本列表 + changelog | + +门户区所有 PUBLIC 技能匿名可浏览和下载,无需登录。 + +### 2.2 个人中心(需登录) + +| 页面 | 路径 | 说明 | +|------|------|------| +| 我的技能 | `/dashboard/skills` | 我发布的技能 + 审核状态 | +| 发布技能 | `/dashboard/publish` | zip 上传 + 预览 + 提交审核 | +| 我的收藏 | `/dashboard/stars` | 收藏列表 | +| Token 管理 | `/dashboard/tokens` | 创建/查看/吊销 | +| 我的命名空间 | `/dashboard/namespaces` | 参与的命名空间 | + +### 2.3 命名空间管理(需空间 ADMIN) + +| 页面 | 路径 | 说明 | +|------|------|------| +| 成员管理 | `/dashboard/namespaces/{slug}/members` | 成员管理 | +| 空间审核 | `/dashboard/namespaces/{slug}/reviews` | 待审核列表 | + +### 2.4 平台管理(需对应平台角色) + +| 页面 | 路径 | 所需角色 | 说明 | +|------|------|---------|------| +| 审核中心 | `/admin/reviews` | SKILL_ADMIN | 全局待审核列表 | +| 提升审核 | `/admin/promotions` | SKILL_ADMIN | 提升到全局的申请列表 | +| 技能管理 | `/admin/skills` | SKILL_ADMIN | 隐藏/恢复/撤回 | +| 用户管理 | `/admin/users` | USER_ADMIN | 用户列表、角色分配、准入审批、封禁/解封 | +| 审计日志 | `/admin/audit-logs` | AUDITOR | 操作日志查询 | +| 命名空间管理 | `/admin/namespaces` | SUPER_ADMIN | 创建/归档/冻结 | + +SUPER_ADMIN 可访问所有管理页面。路由守卫检查用户是否持有对应角色。 + +## 3 布局结构 + +- 门户区:顶部导航 + 内容区,无侧边栏,突出浏览体验 +- Dashboard / Admin:顶部导航 + 左侧边栏,管理效率优先 +- 响应式:移动端侧边栏收起为抽屉 + +## 4 登录与鉴权 + +### 4.1 OAuth2 登录流程(前端视角) + +``` +用户点击"登录"按钮 + │ + ▼ +前端调用 GET /api/v1/auth/providers + │ + ▼ +渲染可用的 OAuth Provider 按钮(一期只有 GitHub) + │ + ▼ +用户点击 "Sign in with GitHub" + │ + ▼ +window.location.href = "/oauth2/authorization/github" + │ + ▼ +(浏览器跳转到 GitHub → 授权 → 回调后端 → 后端创建 Session) + │ + ▼ +后端重定向回前端页面(如 / 或用户之前访问的页面) + │ + ▼ +前端检测到 Session Cookie,调用 GET /api/v1/auth/me + │ + ▼ +获取用户信息,渲染登录态 UI +``` + +前端不需要任何 OAuth 库,登录完全由后端 Spring Security 处理。前端只负责: +1. 调用 `/api/v1/auth/providers` 获取可用 Provider 列表 +2. 跳转到对应的 `authorizationUrl` +3. 回调后通过 `/api/v1/auth/me` 检测登录态 + +### 4.2 登录态检测 + +``` +页面加载 → GET /api/v1/auth/me + │ + ┌─────────┴──────────┐ + │ 200: 已登录 │ 401: 未登录 + │ 存入全局状态 │ 门户页正常展示(匿名浏览) + │ 渲染登录态 UI │ Dashboard/Admin 重定向到登录 + └────────────────────┘ +``` + +- TanStack Router `beforeLoad` 做路由守卫 +- Admin 路由额外检查角色 +- 前端权限控制粒度详见 [03-authentication-design.md](./03-authentication-design.md) 前端权限控制粒度章节 + +## 5 API 集成工作流 + +``` +后端 Springdoc → openapi.json + → openapi-typescript 生成类型 + → openapi-fetch 创建客户端 + → TanStack Query 封装为 hooks +``` + +## 6 文件上传 + +一期 Web 端:zip 上传 → 后端解压校验 → 返回预览 → 用户确认 → 提交审核。 +支持 drag-and-drop + 进度条。 + +## 7 关键交互 + +**技能详情页**:SKILL.md Markdown 渲染、右侧信息栏(版本/下载量/评分/收藏/标签/空间)、版本切换、安装命令一键复制。匿名用户可浏览和下载,收藏/评分按钮提示登录。 + +**搜索页**:实时搜索(debounce 300ms)、技能卡片、排序(相关度/下载量/评分/最新)、命名空间过滤。匿名用户可搜索 PUBLIC 技能。 + +**审核页面**:左侧列表 + 右侧内容预览(Markdown + 文件树)、通过/拒绝 + 意见输入。 diff --git a/docs/09-deployment.md b/docs/09-deployment.md new file mode 100644 index 00000000..9913a011 --- /dev/null +++ b/docs/09-deployment.md @@ -0,0 +1,92 @@ +# Astron Skills 部署架构与运维 + +## 1 K8s 部署拓扑 + +``` + ┌─────────────┐ + │ Ingress │ + │ (Nginx) │ + └──────┬──────┘ + │ + ┌────────────┴────────────┐ + │ /api/* │ /* + ▼ ▼ + ┌──────────────────┐ ┌──────────────────┐ + │ Spring Boot │ │ Nginx / CDN │ + │ replicas: 2+ │ │ 静态资源 │ + └────────┬─────────┘ └──────────────────┘ + │ + ┌────────┴──────────────────────┐ + │ │ │ + ▼ ▼ ▼ +┌────────┐ ┌────────┐ ┌──────────────┐ +│ MySQL │ │ Redis │ │ S3 / MinIO │ +│ (主从) │ │ │ │ │ +└────────┘ └────────┘ └──────────────┘ +``` + +## 2 服务配置 + +- 无状态设计,所有状态存储在 MySQL / Redis / S3 +- 健康检查:`/actuator/health`(liveness + readiness 分离) +- 优雅停机:`spring.lifecycle.timeout-per-shutdown-phase=30s` +- JVM:`-XX:MaxRAMPercentage=75.0` + +## 3 环境 Profile + +| Profile | 用途 | 特点 | +|---------|------|------| +| `local` | 本地开发 | 本地 MySQL/MinIO,Mock OAuth(见下方说明) | +| `dev` | 开发环境 | 共享基础设施,GitHub OAuth 测试应用 | +| `staging` | 预发布 | 与生产同构 | +| `prod` | 生产 | 多 Pod,完整基础设施 | + +### 本地开发 Mock 登录 + +`local` profile 下提供两种开发登录方式: + +1. **MockAuthFilter**(默认):通过 `X-Mock-User-Id` Header 模拟登录,自动创建 Session,无需真实 OAuth 流程 +2. **GitHub OAuth 测试应用**:配置 `OAUTH2_GITHUB_CLIENT_ID` / `OAUTH2_GITHUB_CLIENT_SECRET` 后可走真实 OAuth 流程(GitHub 支持 `http://localhost` 回调) + +MockAuthFilter 仅在 `local` profile 激活,通过 `@Profile("local")` 注解保证不会泄漏到其他环境。 + +## 4 配置管理 + +- 敏感配置:K8s Secret(数据库/Redis/S3 凭证、OAuth2 Client ID/Secret) +- 非敏感配置:K8s ConfigMap(文件大小限制、Session TTL 等) + +## 5 可观测性 + +| 维度 | 方案 | +|------|------| +| 日志 | JSON 格式 stdout,包含 traceId/requestId | +| 指标 | Actuator + Micrometer → Prometheus | +| 链路追踪 | 一期 requestId 透传,后续接 Jaeger/Zipkin | +| 告警 | 基于 Prometheus(5xx 率、延迟 P99、Pod 重启) | + +requestId 透传:Ingress 注入 → Spring Filter 读取放入 MDC → 日志自动携带 → 响应 Header 回传。 + +## 6 构建与发布 + +``` +代码提交 → CI Pipeline + ├── server: mvn package → JAR + └── web: pnpm build → dist/ + │ + ▼ + Docker 多阶段构建 + ├── server → openjdk:21-jre-slim + └── web → nginx:alpine + │ + ▼ + 推送镜像 → K8s 滚动更新 +``` + +Makefile 顶层命令:`make dev-server`, `make dev-web`, `make build`, `make docker`, `make generate-api` + +## 7 数据库迁移 + +Flyway 管理 schema 变更: +- 脚本路径:`server/astron-skills-app/src/main/resources/db/migration/` +- 命名:`V{version}__{description}.sql` +- 多 Pod 安全:Flyway 自带数据库锁 diff --git a/docs/10-delivery-roadmap.md b/docs/10-delivery-roadmap.md new file mode 100644 index 00000000..6f34820c --- /dev/null +++ b/docs/10-delivery-roadmap.md @@ -0,0 +1,112 @@ +# Astron Skills 交付路线 + +## Phase 0:设计定稿(当前阶段) + +产出:架构设计文档、数据库 DDL、API OpenAPI spec 草案、前端线框图 + +## Phase 1:工程骨架 + 认证打通 + +### 后端 + +- Maven 多模块初始化(6 个模块) +- Spring Boot 启动、配置、Profile 分层 +- Flyway + 数据库初始化 +- Redis 集成(Session + 分布式锁) +- Spring Security OAuth2 Client 配置(GitHub OAuth 登录) +- CustomOAuth2UserService + IdentityBindingService(自动注册/绑定) +- Spring Session (Redis) 管理、API Token 签发校验 +- RBAC 基础(SUPER_ADMIN / SKILL_ADMIN / USER_ADMIN / AUDITOR + 命名空间角色) +- 全局异常处理、requestId 透传、日志格式 +- Springdoc OpenAPI、健康检查 +- CSRF 防护(Cookie-to-Header 模式,CLI API 豁免) +- 本地开发 MockAuthFilter(`local` profile) +- 基础限流:Nginx Ingress `limit-req` 按 IP 限流(认证/搜索/下载接口) + +### 前端 + +- Vite + React + TypeScript 初始化 +- shadcn/ui + Tailwind 配置 +- TanStack Router 路由骨架、TanStack Query 配置 +- openapi-fetch 客户端生成管线 +- 布局组件、OAuth 登录流程(调用 `/api/v1/auth/providers` → 跳转) +- 登录态检测(`/api/v1/auth/me`)+ 路由守卫 +- Makefile 顶层编排 + +### 验收 + +前后端能跑,GitHub OAuth 登录可用,AccessPolicy 准入策略生效,`/api/v1/auth/me` 可用,Token 可用,OpenAPI spec 可访问,Ingress 基础限流生效 + +## Phase 2:命名空间 + Skill 核心链路 + +### 后端 + +- 命名空间 CRUD + 成员管理 +- 对象存储集成 +- 技能发布(上传 → 校验 → 存储 → draft) +- 技能查询(详情、版本、文件)、下载(打包 + 可见性检查,PUBLIC 匿名可下载) +- 标签管理、搜索(MySQL Full-Text,匿名搜索限 PUBLIC) +- 异步事件基础设施 +- Rate Limiting 升级(应用层精细限流:按用户/端点分类,基于 Redis 滑动窗口) + +### 前端 + +- 首页、搜索页、命名空间主页(匿名可访问) +- 技能详情页、版本历史页(PUBLIC 匿名可浏览/下载) +- 发布页、我的技能列表 +- 命名空间管理页 + +### 验收 + +完整发布 → 存储 → 查询 → 下载链路,搜索可用,命名空间隔离生效,匿名用户可浏览/下载公共技能 + +## Phase 3:审核流程 + 评分收藏 + CLI API / ClawHub 兼容层 + +### 后端 + +- 审核流程(提交 → 审核 → 发布,含乐观锁) +- 团队技能提升到全局(promotion_request 流程) +- 评分 + 收藏 + 计数器(原子更新) +- CLI API(whoami、publish、resolve、check) +- ClawHub CLI 协议兼容层(registry metadata、resolve、download、publish、check 等核心接口) +- 协议适配器与兼容性测试(针对 ClawHub CLI 的真实请求/响应样例) +- 审计日志(同步落库)、幂等去重(idempotency_record + Redis) + +### 前端 + +- 审核中心、命名空间审核页、提升审核页 +- 评分组件 + 收藏按钮(匿名用户点击提示登录)、我的收藏页 +- Token 管理页 +- 管理后台(用户管理、角色分配、准入审批、封禁/解封) + +### 验收 + +发布必须经审核,分级审核权限生效,Astron CLI 全流程可用,ClawHub CLI 通过兼容层可完成核心 registry 操作,评分收藏可用 + +## Phase 4:运维增强 + 打磨 + +- 审计日志查询页面 +- 技能隐藏/恢复/版本撤回 +- Prometheus 指标暴露 +- Docker 镜像 + K8s 部署清单 +- 性能优化、安全加固 +- 文档完善 +- 后续 OAuth Provider 扩展准备(GitLab、Google 等) + +## Phase 5:治理闭环 + 社交 + +- 评论功能 +- 举报/标记机制(用户举报 → 管理员处理 → 隐藏/撤回) +- 自动安全预检(`PrePublishValidator` 实现:敏感信息扫描、恶意脚本检测) +- Webhook/事件通知(发布通知、审核结果通知) +- 多 Provider 账号显式绑定/合并流程 + +## 主要风险与应对 + +| 风险 | 应对 | +|------|------| +| GitHub OAuth 回调配置复杂 | 本地用 MockAuthFilter 解耦,OAuth 联调可并行 | +| 审核流程需求变更 | skill_version.status 已预留审核状态 | +| 搜索效果不佳 | SPI 架构允许随时切换实现 | +| 前后端接口频繁变更 | OpenAPI spec 先行,类型自动生成 | +| 新增 OAuth Provider | Spring Security OAuth2 原生多 Provider 支持,只需配置 + 属性映射 | +| ClawHub CLI 协议细节与现有模型不完全一致 | 增加兼容适配层与协议回归测试,避免把 Astron 内部模型直接暴露给兼容客户端 |