docs: project's documents

This commit is contained in:
vsxd 2026-03-11 20:24:01 +08:00
parent b36b375ba0
commit db64bef098
11 changed files with 2230 additions and 0 deletions

View file

@ -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 `<skill>` 描述块格式兼容
- 目标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 TokenCLI / 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匿名可浏览和下载无需登录

View file

@ -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. 推荐的一期技术决策
- ORMMyBatis-Plus
- API 文档Springdoc OpenAPI
- 对象存储MinIO / AWS S3 兼容接口
- 异步任务Spring Events + 异步线程池,后续视复杂度引入 MQ
- 缓存/SessionSpring Session + Redis
- 数据库迁移Flyway
- 认证Spring Security OAuth2 Client一期 GitHub

395
docs/02-domain-model.md Normal file
View file

@ -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 IDnullable |
| 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 IDnullable |
| 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 |
- 流程:收到请求 → 插入 recordPROCESSING→ 业务处理 → 更新为 COMPLETED + resource_id → 重复请求时查 record 返回已有结果
- Redis 做快速去重缓存SETNXMySQL 做持久化兜底
- 定时任务清理过期记录
## 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 校验 |

View file

@ -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<String, Object> 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 ProviderGoogle、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后端必须拦截

View file

@ -0,0 +1,136 @@
# Astron Skills 搜索架构
## 1 SPI 接口
```java
public interface SearchIndexService {
void index(SkillSearchDocument doc);
void batchIndex(List<SkillSearchDocument> 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<Long> memberNamespaceIds, // 用户是 MEMBER 的 namespace可见 NAMESPACE_ONLY
Set<Long> 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。

273
docs/05-business-flows.md Normal file
View file

@ -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)
③ 平台管理员审核
├── 通过 →
│ ① 在全局空间创建新 skillsource_skill_id = 原 skill ID
│ ② 复制 source_version_id 对应版本的文件和元数据到新 skill严格使用申请时指定的版本不取最新
│ ③ 新 skill.visibility = PUBLIC
│ ④ promotion_request.target_skill_id = 新 skill IDstatus → APPROVED
│ ⑤ 搜索索引写入新 skill同步写入审计日志
│ (提升关系唯一事实来源是 promotion_requestUI 查询"是否已提升"通过该表判定)
└── 拒绝 → 记录原因,原技能不受影响
```
后续版本更新:
- 全局空间的新 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_countSELECT 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. 重复请求时:查 recordCOMPLETED 返回原始资源 IDPROCESSING 返回 `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允许重试

212
docs/06-api-design.md Normal file
View file

@ -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 APIOAuth2 登录相关)
| 方法 | 路径 | 说明 |
|------|------|------|
| 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 APIAPI 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 1Ingress 层基础限流
通过 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

121
docs/07-skill-protocol.md Normal file
View file

@ -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 `<skill>` 描述块格式Astron CLI 生成的 AGENTS.md 索引区块与 OpenSkills 格式兼容
### 服务端职责边界
- 服务端返回技能元数据name, description, version不返回 `location`
- `location` 是客户端本地安装路径,由 CLI 根据安装目录计算生成,写入 AGENTS.md
- 服务端不生成、不修改 AGENTS.md这是客户端职责
### Astron 私有扩展(不影响互操作)
- `<skills_system>` / `<available_skills>` 区块格式Astron CLI 可自定义,但必须保证 `<skill>` 节点格式与 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 中生成 `<skill>` 描述块
- `<skill>` 块包含 `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 标签(自定义标签)
```

View file

@ -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 + 文件树)、通过/拒绝 + 意见输入。

92
docs/09-deployment.md Normal file
View file

@ -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/MinIOMock 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 |
| 告警 | 基于 Prometheus5xx 率、延迟 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 自带数据库锁

112
docs/10-delivery-roadmap.md Normal file
View file

@ -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 APIwhoami、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 内部模型直接暴露给兼容客户端 |