fix: change api/v1/cli to api/v1

This commit is contained in:
yun-zhi-ztl 2026-03-14 20:35:50 +08:00
parent 57c4f77d25
commit bf62b340ab
14 changed files with 42 additions and 42 deletions

View file

@ -233,7 +233,7 @@ public class SecurityConfig {
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED))
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.ignoringRequestMatchers("/api/v1/cli/**"))
.ignoringRequestMatchers("/api/v1/**"))
// ...
;
}
@ -458,7 +458,7 @@ Session 中存储以下字段:
- 后端设置 `XSRF-TOKEN` Cookie`HttpOnly=false`
- 前端从 Cookie 读取 Token放入请求 Header `X-XSRF-TOKEN`
- 后端校验 Header 与 Cookie 是否一致
- CLI API`/api/v1/cli/**`)与兼容层(`/api/compat/v1/**`)豁免 CSRF使用 Bearer Token无 Cookie
- CLI API`/api/v1/**`)与兼容层(`/api/compat/v1/**`)豁免 CSRF使用 Bearer Token无 Cookie
## 9. 前端权限控制
@ -609,8 +609,8 @@ window.location.href = '/oauth2/authorization/github'
| 接口 | 所需凭证 | 额外判定 |
|------|---------|---------|
| `GET /api/v1/cli/whoami` | 任意有效 Bearer Token | 无 |
| `POST /api/v1/cli/publish` | Bearer Token + `skill:publish` | 用户是目标 namespace 的 MEMBER 以上 |
| `GET /api/v1/whoami` | 任意有效 Bearer Token | 无 |
| `POST /api/v1/publish` | Bearer Token + `skill:publish` | 用户是目标 namespace 的 MEMBER 以上 |
### 10.4 Admin API

View file

@ -54,7 +54,7 @@ Phase 3 在不改变发布入口的前提下,把后半段切换为“创建 DR
- 审核通过后才转为 `PUBLISHED`
- 审核拒绝后转为 `REJECTED`
- 例外:提交人持有 `SUPER_ADMIN` 平台角色时,发布入口直接创建 `skill_version(status=PUBLISHED)`,跳过 `review_task` 创建,同时不再要求其必须是目标 namespace 成员
- 上述例外必须对 Web、`/api/v1/cli/publish`、`/api/compat/v1/publish` 保持一致
- 上述例外必须对 Web、`/api/v1/publish`、`/api/compat/v1/publish` 保持一致
### 对象存储写入策略
@ -67,7 +67,7 @@ Phase 3 在不改变发布入口的前提下,把后半段切换为“创建 DR
### CLI publish 请求规范
```
POST /api/v1/cli/publish
POST /api/v1/publish
Content-Type: multipart/form-data
Parts:
- file: zip 包(必需)
@ -81,7 +81,7 @@ Phase 3 CLI 默认行为:上传 → 创建 DRAFT → 自动提交审核。
如果调用方持有 `SUPER_ADMIN`,则直接发布为 `PUBLISHED`
Web 端可保留“发布后再提交审核”的两段式体验,但这属于 Phase 3 能力。
`/api/v1/cli/publish` 响应:
`/api/v1/publish` 响应:
```json
{

View file

@ -239,7 +239,7 @@ Public API 的可见性规则:
发布状态约束:
- 普通用户发布成功后,`status``PENDING_REVIEW`
- 持有 `SUPER_ADMIN` 的用户通过 Web、`/api/v1/cli/publish`、`/api/compat/v1/publish` 发布时,`status``PUBLISHED`,且不要求其必须是目标 namespace 成员
- 持有 `SUPER_ADMIN` 的用户通过 Web、`/api/v1/publish`、`/api/compat/v1/publish` 发布时,`status``PUBLISHED`,且不要求其必须是目标 namespace 成员
## 7.4 Token API需登录
@ -253,10 +253,10 @@ Public API 的可见性规则:
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/v1/cli/whoami` | 当前 Bearer Token 对应的用户信息 |
| POST | `/api/v1/cli/publish` | 发布技能包Phase 2 直接返回 `PUBLISHED`Phase 3 恢复审核流;`SUPER_ADMIN` 始终直发) |
| GET | `/api/v1/cli/resolve/{namespace}/{slug}` | 解析版本 |
| GET | `/api/v1/cli/check/{namespace}/{slug}/{version}` | 本地哈希与远端比对 |
| GET | `/api/v1/whoami` | 当前 Bearer Token 对应的用户信息 |
| POST | `/api/v1/publish` | 发布技能包Phase 2 直接返回 `PUBLISHED`Phase 3 恢复审核流;`SUPER_ADMIN` 始终直发) |
| GET | `/api/v1/resolve/{namespace}/{slug}` | 解析版本 |
| GET | `/api/v1/check/{namespace}/{slug}/{version}` | 本地哈希与远端比对 |
### ClawHub CLI 协议兼容层
@ -266,7 +266,7 @@ Public API 的可见性规则:
- 范围:一期聚焦覆盖 ClawHub CLI 所依赖的核心接口查询、版本解析、下载、发布、whoami
- 要求:兼容层优先保持 ClawHub CLI 既有请求/响应语义;若内部领域模型不同,通过 adapter 层完成协议转换,而不是要求客户端适配 skillhub 私有协议
- 要求:兼容层纳入 OpenAPI 或独立兼容协议文档,并作为正式对外契约维护
- 要求:兼容层与 skillhub 自有 `/api/v1/cli/**` 并存,二者共享同一套权限、审计、限流与领域服务
- 要求:兼容层与 skillhub 自有 `/api/v1/**` 并存,二者共享同一套权限、审计、限流与领域服务
- 非目标:前端页面不直接依赖兼容层;兼容层用于服务已有 ClawHub CLI 和相关自动化脚本
兼容层最少需要覆盖的能力类别:

View file

@ -22,7 +22,7 @@ import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/v1/cli")
@RequestMapping("/api/v1")
public class CliController extends BaseApiController {
private final SkillPackageValidator skillPackageValidator;

View file

@ -11,7 +11,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/cli/auth/device")
@RequestMapping("/api/v1/auth/device")
public class DeviceAuthController extends BaseApiController {
private final DeviceAuthService deviceAuthService;

View file

@ -23,7 +23,7 @@ import java.io.IOException;
import java.util.List;
@RestController
@RequestMapping("/api/v1/cli")
@RequestMapping("/api/v1")
public class CliPublishController extends BaseApiController {
private final SkillPublishService skillPublishService;

View file

@ -43,7 +43,7 @@ class CliControllerTest {
@Test
void whoamiShouldReturnUnauthorizedForAnonymousRequest() throws Exception {
mockMvc.perform(get("/api/v1/cli/whoami"))
mockMvc.perform(get("/api/v1/whoami"))
.andExpect(status().isUnauthorized());
}
@ -66,7 +66,7 @@ class CliControllerTest {
List.of(new SimpleGrantedAuthority("ROLE_SKILL_ADMIN"))
);
mockMvc.perform(get("/api/v1/cli/whoami").with(authentication(auth)))
mockMvc.perform(get("/api/v1/whoami").with(authentication(auth)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.msg").isNotEmpty())
@ -88,7 +88,7 @@ class CliControllerTest {
zipBytes
);
mockMvc.perform(multipart("/api/v1/cli/check").file(file))
mockMvc.perform(multipart("/api/v1/check").file(file))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.valid").value(true))
@ -107,7 +107,7 @@ class CliControllerTest {
zipBytes
);
mockMvc.perform(multipart("/api/v1/cli/check").file(file))
mockMvc.perform(multipart("/api/v1/check").file(file))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.valid").value(false))
@ -125,7 +125,7 @@ class CliControllerTest {
zipBytes
);
mockMvc.perform(multipart("/api/v1/cli/check").file(file))
mockMvc.perform(multipart("/api/v1/check").file(file))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.valid").value(false))
@ -142,7 +142,7 @@ class CliControllerTest {
zipBytes
);
mockMvc.perform(multipart("/api/v1/cli/check").file(file))
mockMvc.perform(multipart("/api/v1/check").file(file))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.valid").value(false))

View file

@ -44,7 +44,7 @@ class DeviceAuthControllerTest {
given(deviceAuthService.generateDeviceCode()).willReturn(response);
mockMvc.perform(post("/api/v1/cli/auth/device/code")
mockMvc.perform(post("/api/v1/auth/device/code")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
@ -61,7 +61,7 @@ class DeviceAuthControllerTest {
given(deviceAuthService.pollToken("device_abc123")).willReturn(response);
mockMvc.perform(post("/api/v1/cli/auth/device/token")
mockMvc.perform(post("/api/v1/auth/device/token")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"deviceCode\": \"device_abc123\"}"))
.andExpect(status().isOk())
@ -77,7 +77,7 @@ class DeviceAuthControllerTest {
given(deviceAuthService.pollToken("device_abc123")).willReturn(response);
mockMvc.perform(post("/api/v1/cli/auth/device/token")
mockMvc.perform(post("/api/v1/auth/device/token")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"deviceCode\": \"device_abc123\"}"))
.andExpect(status().isOk())

View file

@ -70,7 +70,7 @@ public class SecurityConfig {
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(csrfHandler)
.ignoringRequestMatchers("/api/v1/cli/**", "/api/compat/**")
.ignoringRequestMatchers("/api/v1/**", "/api/compat/**")
)
.authorizeHttpRequests(auth -> auth
.requestMatchers(
@ -81,8 +81,8 @@ public class SecurityConfig {
"/api/v1/auth/session/bootstrap",
"/api/v1/auth/direct/login",
"/api/v1/auth/local/**",
"/api/v1/cli/auth/device/**",
"/api/v1/cli/check",
"/api/v1/auth/device/**",
"/api/v1/check",
"/actuator/health",
"/actuator/prometheus",
"/v3/api-docs/**",

View file

@ -19,9 +19,9 @@ public class ApiTokenScopeService {
ScopeRule.allow(null, "/api/v1/health"),
ScopeRule.allow(null, "/api/v1/auth/providers"),
ScopeRule.allow(null, "/api/v1/auth/me"),
ScopeRule.allow(null, "/api/v1/cli/auth/device/**"),
ScopeRule.allow(null, "/api/v1/cli/check"),
ScopeRule.allow("GET", "/api/v1/cli/whoami"),
ScopeRule.allow(null, "/api/v1/auth/device/**"),
ScopeRule.allow(null, "/api/v1/check"),
ScopeRule.allow("GET", "/api/v1/whoami"),
ScopeRule.allow("GET", "/api/v1/skills"),
ScopeRule.allow("GET", "/api/v1/skills/**"),
ScopeRule.allow("GET", "/api/v1/namespaces"),
@ -39,7 +39,7 @@ public class ApiTokenScopeService {
ScopeRule.require(null, "/api/v1/tokens", "token:manage"),
ScopeRule.require(null, "/api/v1/tokens/**", "token:manage"),
ScopeRule.require("POST", "/api/v1/skills/*/publish", "skill:publish"),
ScopeRule.require("POST", "/api/v1/cli/publish", "skill:publish"),
ScopeRule.require("POST", "/api/v1/publish", "skill:publish"),
ScopeRule.require("POST", "/api/compat/v1/publish", "skill:publish")
);

View file

@ -58,7 +58,7 @@ class ApiTokenAuthenticationFilterTest {
when(role.getCode()).thenReturn("SKILL_ADMIN");
MockHttpServletRequest request = new MockHttpServletRequest();
request.setRequestURI("/api/v1/cli/whoami");
request.setRequestURI("/api/v1/whoami");
request.addHeader("Authorization", "Bearer raw-token");
filter.doFilter(request, new MockHttpServletResponse(), new MockFilterChain());
@ -84,7 +84,7 @@ class ApiTokenAuthenticationFilterTest {
when(userAccountRepository.findById("user-2")).thenReturn(Optional.of(user));
MockHttpServletRequest request = new MockHttpServletRequest();
request.setRequestURI("/api/v1/cli/publish");
request.setRequestURI("/api/v1/publish");
request.addHeader("Authorization", "Bearer raw-token");
filter.doFilter(request, new MockHttpServletResponse(), new MockFilterChain());

View file

@ -59,7 +59,7 @@ class ApiTokenScopeFilterTest {
);
SecurityContextHolder.getContext().setAuthentication(authentication);
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/v1/cli/publish");
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/v1/publish");
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain chain = mock(FilterChain.class);

View file

@ -24,7 +24,7 @@ class ApiTokenScopeServiceTest {
void authorizeShouldAllowCliWhoamiWithoutScope() {
ApiTokenScopeService.AuthorizationDecision decision = scopeService.authorize(
"GET",
"/api/v1/cli/whoami",
"/api/v1/whoami",
Set.of()
);

View file

@ -308,7 +308,7 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/v1/cli/publish": {
"/api/v1/publish": {
parameters: {
query?: never;
header?: never;
@ -324,7 +324,7 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/v1/cli/check": {
"/api/v1/check": {
parameters: {
query?: never;
header?: never;
@ -340,7 +340,7 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/v1/cli/auth/device/token": {
"/api/v1/auth/device/token": {
parameters: {
query?: never;
header?: never;
@ -356,7 +356,7 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/v1/cli/auth/device/code": {
"/api/v1/auth/device/code": {
parameters: {
query?: never;
header?: never;
@ -916,7 +916,7 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/v1/cli/whoami": {
"/api/v1/whoami": {
parameters: {
query?: never;
header?: never;
@ -932,7 +932,7 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/v1/cli/resolve/{namespace}/{slug}": {
"/api/v1/resolve/{namespace}/{slug}": {
parameters: {
query?: never;
header?: never;