From 70136d2f3a91ee5aeaa8e146f805ebc74c4a87e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=80=AA=E7=A8=8B=E4=BC=9F?= Date: Wed, 24 Jun 2026 00:21:27 +0800 Subject: [PATCH] =?UTF-8?q?docs(api):=20=E5=AE=8C=E5=96=84=20WebAPI=20?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E4=B8=8E=20OpenAPI=20/=20Swagger=20=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #406 - 新增 OpenApiConfig 全局配置 Bean:标题/描述/服务器 + bearerAuth 安全方案 (覆盖 JWT 与 mc_ PAT,对齐 JwtAuthFilter 前缀分发) - application.yml 增 springdoc default-flat-param-object + mateclaw.openapi.* 外置项 - api.md 中英双语补全「通用约定」(R 信封、ResultCode、错误模型、IPage 分页、 ID 约定、三态认证、X-Workspace-Id 机制)+ 9 个旗舰端点完整参考 - 新增 openapi.md 中英双语 Swagger 使用指南 --- .../java/vip/mate/config/OpenApiConfig.java | 98 ++++++ .../src/main/resources/application.yml | 14 + .../src/main/resources/docs/en/api.md | 326 ++++++++++++++++++ .../src/main/resources/docs/en/openapi.md | 72 ++++ .../src/main/resources/docs/zh/api.md | 326 ++++++++++++++++++ .../src/main/resources/docs/zh/openapi.md | 72 ++++ 6 files changed, 908 insertions(+) create mode 100644 mateclaw-server/src/main/java/vip/mate/config/OpenApiConfig.java create mode 100644 mateclaw-server/src/main/resources/docs/en/openapi.md create mode 100644 mateclaw-server/src/main/resources/docs/zh/openapi.md diff --git a/mateclaw-server/src/main/java/vip/mate/config/OpenApiConfig.java b/mateclaw-server/src/main/java/vip/mate/config/OpenApiConfig.java new file mode 100644 index 000000000..ccc8b1962 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/config/OpenApiConfig.java @@ -0,0 +1,98 @@ +package vip.mate.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +/** + * SpringDoc OpenAPI 全局配置。 + *

+ * 项目已依赖 springdoc-openapi-starter-webmvc-ui(见根 pom 的 + * {@code springdoc.version}),但此前没有任何 OpenAPI 配置 Bean,导致 + * Swagger UI 缺少标题/描述、缺少安全方案(Authorize 按钮不可用)。 + * 本类补齐这些全局元信息,让现有 Controller 上已有的 + * {@code @Tag} / {@code @Operation} 注解直接可用。 + * + *

安全方案

+ * 两种 token 都通过标准 {@code Authorization: Bearer } 头传入, + * {@link JwtAuthFilter} 按 token 前缀分发:JWT 以 {@code eyJ} 开头, + * Personal Access Token 以 {@code mc_} 开头(PAT_PREFIX)。因此 Swagger + * UI 的 Authorize 按钮只需填入任意一种 token 即可。 + * + *

注意:Swagger 当前公开可访问

+ * {@link SecurityConfig#filterChain} 中 {@code /api/**} 要求认证,但 + * {@code /swagger-ui*}、{@code /v3/api-docs*}、{@code /webjars/**} 落到 + * {@code .anyRequest().permitAll()},即 Swagger UI 当前是公开的。如生产环境 + * 需要收口,应在 SecurityConfig 显式加规则,而不是改本类。 + * + *

未做的事(与「全局配置 + 安全方案」范围一致)

+ * 不逐个 Controller 补 {@code @Parameter} / {@code @ApiResponse} / + * {@code @Schema} / 公开端点的 {@code @SecurityRequirements({})} opt-out。 + * 这些属于「关键端点注解」增强档,留作后续。 + * + * @author MateClaw Team + */ +@Configuration +public class OpenApiConfig { + + /** HTTP Bearer 安全方案的引用键,与 {@link Components#getSecuritySchemes()} 中的登记名一致。 */ + public static final String BEARER_AUTH = "bearerAuth"; + + @Bean + public OpenAPI mateclawOpenAPI( + @Value("${mateclaw.openapi.title:MateClaw REST API}") String title, + @Value("${mateclaw.openapi.description:#{null}}") String description, + @Value("${mateclaw.openapi.version:1.0}") String version, + @Value("${mateclaw.openapi.server-url:}") String serverUrl) { + + Info info = new Info() + .title(title) + .version(version) + .description(defaultIfBlank(description, "" + + "MateClaw 多用户 AI Agent 平台的 REST API。" + + "所有业务端点使用 /api/v1 前缀,绝大多数 JSON 响应走 {code,msg,data} 统一信封。" + + "点击右上角 Authorize 并粘贴 JWT 或 Personal Access Token (mc_) 即可调试受保护端点。" + + "完整人读文档见部署地址的 /docs 页面。")); + + Components components = new Components() + .addSecuritySchemes(BEARER_AUTH, bearerScheme( + "JWT 或 Personal Access Token。两种都通过 Authorization: Bearer 头传入," + + "服务端按前缀分发:JWT 以 eyJ 开头,PAT 以 mc_ 开头。" + + "EventSource 不支持自定义请求头,SSE 流式端点可用 ?token= 查询参数替代。")); + + OpenAPI openAPI = new OpenAPI() + .info(info) + .components(components) + // 默认所有端点需要鉴权;公开端点(登录、SSE 等)在 SecurityConfig 中放行, + // Swagger 上会仍标注锁图标,但不影响实际调用。 + .addSecurityItem(new SecurityRequirement().addList(BEARER_AUTH)); + + if (serverUrl != null && !serverUrl.isBlank()) { + openAPI.servers(List.of(new Server().url(serverUrl))); + } + // serverUrl 为空时不显式配置 —— SpringDoc 默认从请求 host 推导, + // 避免「Try it out」打到错误地址。 + + return openAPI; + } + + private SecurityScheme bearerScheme(String description) { + return new SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + .description(description); + } + + private static String defaultIfBlank(String value, String fallback) { + return (value == null || value.isBlank()) ? fallback : value; + } +} diff --git a/mateclaw-server/src/main/resources/application.yml b/mateclaw-server/src/main/resources/application.yml index 005d440b3..32a479bed 100644 --- a/mateclaw-server/src/main/resources/application.yml +++ b/mateclaw-server/src/main/resources/application.yml @@ -116,11 +116,16 @@ mybatis-plus: log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl # SpringDoc OpenAPI +# 注意:/swagger-ui*、/v3/api-docs*、/webjars/** 落在 SecurityConfig 的 +# .anyRequest().permitAll(),即 Swagger UI 当前公开可访问。如生产需要收口, +# 在 SecurityConfig 加显式规则(不要在此处配)。 springdoc: api-docs: path: /v3/api-docs swagger-ui: path: /swagger-ui.html + # 把单个嵌套 query 参数对象(如分页 wrapper)拍平成独立字段,减少 schema 噪音 + default-flat-param-object: true # MateClaw 自定义配置 mateclaw: @@ -131,6 +136,15 @@ mateclaw: # Set this when agents deliver download links to channels/clients that cannot # resolve a relative URL (IM messages, copied links, external downloads). public-base-url: ${MATECLAW_PUBLIC_BASE_URL:} + openapi: + # SpringDoc OpenAPI 元信息(驱动 /swagger-ui.html 与 /v3/api-docs)。 + # server-url 留空时由 SpringDoc 从请求 host 推导,避免 Try it out 打到错误地址。 + # 生产若需固定,通过 MATECLAW_OPENAPI_SERVER_URL 覆盖(如 https://mate.example.com)。 + title: ${MATECLAW_OPENAPI_TITLE:MateClaw REST API} + version: ${MATECLAW_OPENAPI_VERSION:1.0} + server-url: ${MATECLAW_OPENAPI_SERVER_URL:} + # description 留空则使用 OpenApiConfig 中的内置默认描述 + description: ${MATECLAW_OPENAPI_DESCRIPTION:} jwt: secret: ${JWT_SECRET:MateClaw-JWT-Secret-Key-2024-Please-Change-In-Production} expiration: 86400000 diff --git a/mateclaw-server/src/main/resources/docs/en/api.md b/mateclaw-server/src/main/resources/docs/en/api.md index 7879cf700..70746b09b 100644 --- a/mateclaw-server/src/main/resources/docs/en/api.md +++ b/mateclaw-server/src/main/resources/docs/en/api.md @@ -2,6 +2,8 @@ This page is source-aligned with the Spring MVC controllers under `mateclaw-server/src/main/java`. The route inventory below was rebuilt from controller annotations; when it conflicts with an older feature page, this page and the source code are the contract. +> Want a machine-readable OpenAPI doc (import into Postman / Apifox, online debugging)? See the [OpenAPI / Swagger guide](./openapi.md) — visit `/swagger-ui.html` on your deployment. + ## Contract All application REST endpoints use the `/api/v1` prefix unless explicitly noted. Most JSON responses use the project envelope: @@ -34,6 +36,119 @@ Public routes from `SecurityConfig` include login, first-run setup, webhook/webc Workspace-scoped APIs usually accept `X-Workspace-Id`. If omitted, many handlers fall back to workspace `1` for desktop/local compatibility. +## Conventions + +Structural contracts shared by every endpoint. Read this section first, then the flagship endpoint examples and the full route inventory below will line up. + +### Response envelope `R` + +Source: `vip.mate.common.result.R` (`R.java`). Three fields: + +| Field | Type | Meaning | +|---|---|---| +| `code` | `int` | Status code. `200` = success; see "Status codes" below | +| `msg` | `string` | Message. **Note: `msg`, not `message`** | +| `data` | `T` | Payload; `null` on failure | + +Success example: + +```json +{ "code": 200, "msg": "success", "data": { "id": "1", "name": "Agent A" } } +``` + +Failure example: + +```json +{ "code": 401, "msg": "Token expired or invalid", "data": null } +``` + +**HTTP status mirrors `code`**: `RHttpStatusAdvice` (`ResponseBodyAdvice`) sets the HTTP status to `HttpStatus.resolve(code)` whenever `code != 200`. So business code `401` → HTTP 401, `404` → HTTP 404. Business codes that are not valid HTTP statuses (e.g. `1001`, `2001`) fall back to HTTP `500`. + +### Status codes + +Source: `vip.mate.common.result.ResultCode`. Two categories: + +| Code | Meaning | Maps to HTTP status directly? | +|---|---|---| +| `200` | Success | Yes | +| `400` | Param error | Yes | +| `401` | Unauthorized | Yes | +| `403` | Forbidden | Yes | +| `404` | Not found | Yes | +| `500` | System error | Yes | +| `1001` | Agent not found | No (HTTP 500) | +| `1002` | Agent busy | No (HTTP 500) | +| `2001` | LLM error | No (HTTP 500) | +| `3001` | Tool not found | No (HTTP 500) | +| `4001` | Channel error | No (HTTP 500) | + +### Error model + +Errors are handled centrally by `GlobalExceptionHandler` (`@RestControllerAdvice`): + +| HTTP | Trigger | Body | +|---|---|---| +| 400 | `@Valid` / `BindException` validation failure | `{code:400, msg:"field: defaultMessage"}` — only the **first** field error is returned | +| 400 | `MethodArgumentTypeMismatchException` (e.g. non-numeric `/{id}`) | `{code:400, msg:"Invalid value for parameter 'X': expected Long"}` | +| 401 | Unauthenticated / invalid token (`SecurityConfig` `authenticationEntryPoint`) | `{code:401, msg:"Token expired or invalid"}` | +| 403 | Insufficient workspace role (`WorkspaceAccessInterceptor` writes the response directly) | `{code:403, msg:"...", data:null}` | +| 404 | No route match (`NoResourceFoundException`) | `{code:404, msg:"Resource not found"}` | +| 405 | Method not supported (`HttpRequestMethodNotSupportedException`) | `{code:405, msg:"Method not allowed"}` | +| 409 | Confirmation required (`ConfirmRequiredException`) | **Breaks the envelope**: `{code, message, boundAgents}` (field is `message`, **not** `msg`) — the only non-`R` response in the API | +| 500 | Catch-all (`Exception`) | `{code:500, msg:"Internal server error"}` — stack trace is not leaked | +| 503 | Async timeout (non-SSE) | `{code:503, msg:"Request timeout, please try again"}` | + +> SSE endpoints (`/chat/stream`, etc.) do **not** emit a JSON envelope on error; they send an SSE `error` event instead: `event: error` / `data: {"message":"..."}`. See the [WebChat guide](./webchat.md#sse-event-protocol). + +### Pagination + +Paginated endpoints return `R>` directly — the MyBatis Plus `Page` serialization: + +```json +{ + "code": 200, + "data": { + "records": [ /* current page rows */ ], + "total": 128, + "size": 20, + "current": 1, + "pages": 7 + } +} +``` + +| Field | Meaning | +|---|---| +| `records` | Current page rows (**field name is `records`**, not `list`/`items`) | +| `total` | Total record count | +| `size` | Page size | +| `current` | Current page number, 1-based | +| `pages` | Total page count | + +Common query params: `page` (default 1), `size` (default 20). Examples: `GET /api/v1/audit/events`, `GET /api/v1/conversations/page`. + +### ID & type conventions + +- **Snowflake `Long` serialized as JSON string**: all backend PKs are `Long`, but Jackson serializes them as strings. Clients (especially JS) should **treat IDs as strings end-to-end** to avoid `Number.MAX_SAFE_INTEGER` precision loss. +- **Password is write-only**: `UserEntity.password` is annotated `@JsonProperty(access = WRITE_ONLY)` — accepted on login/create, never present in any response. + +### Auth model + +`JwtAuthFilter` supports three token forms, all via the `Authorization` header: + +1. **JWT**: `Authorization: Bearer `. Starts with `eyJ` (base64 header). The `token` field returned by login is exactly this. +2. **Personal Access Token (PAT)**: `Authorization: Bearer `. Prefixed with `mc_`, for headless / CI / SDK use. The plaintext is returned **only once** at `POST /api/v1/auth/tokens` creation; only the hash is stored afterwards. The filter dispatches by the `mc_` prefix to the PAT verification path. +3. **SSE `?token=` query param**: the native browser `EventSource` cannot set custom headers, so SSE streaming endpoints additionally accept `?token=` (JWT or PAT). + +**Sliding renewal**: when a JWT is near expiry (default < 2h remaining), the response header returns a fresh token — `X-New-Token: ` (with `Access-Control-Expose-Headers: X-New-Token`). Clients should watch for and replace the locally stored token. JWT TTL defaults to 24h (`mateclaw.jwt.expiration=86400000`). + +### How `X-Workspace-Id` works + +- **No ThreadLocal / request-context holder.** The workspace id is consumed two ways: + 1. **RBAC enforcement**: `WorkspaceAccessInterceptor` reads `X-Workspace-Id` for methods annotated `@RequireWorkspaceRole` (roles owner > admin > member > viewer) or `@RequireGlobalAdmin`, falling back to workspace `1` when absent/unparseable. Insufficient permission writes a 403 JSON response directly. + 2. **Business reads**: many controllers take it via `@RequestHeader(value="X-Workspace-Id", required=false) Long workspaceId` for query scoping, also defaulting to `1`. +- So workspace isolation is enforced by "interceptor auth + controller self-read" together; clients should pass `X-Workspace-Id` explicitly for workspace-scoped calls. + ## Frequently Used APIs ### Login @@ -72,6 +187,217 @@ Image, video, music, and 3D generation are agent tools (`image_generate`, `video `/api/v1/talk/ws` is registered by `WebSocketConfig` for Talk Mode. It is intentionally listed in `SecurityConfig` as a public WebSocket route, but it is not counted in the controller route inventory below. +## Flagship Endpoint Reference + +Full request/response reference for the most-used endpoints. Every field maps 1:1 to the source DTO. The 406-row route inventory further down is the complete index; this section is the human-readable walkthrough for high-traffic endpoints. + +### Login: `POST /api/v1/auth/login` + +Public endpoint (no auth required). Exchanges credentials for a JWT. + +**Request body** `LoginRequest` (`AuthController.java`): + +| Field | Type | Description | +|---|---|---| +| `username` | string | Username | +| `password` | string | Password | + +**Response** `R`: + +```json +{ + "code": 200, + "data": { + "id": "1", + "token": "eyJhbGciOi...", + "username": "admin", + "nickname": "Admin", + "role": "admin" + } +} +``` + +| Field | Type | Description | +|---|---|---| +| `id` | string | User ID (Snowflake, as a string) | +| `token` | string | **JWT** — send as `Authorization: Bearer ` on subsequent requests. There is no separate expiry field; expiry lives in the JWT's `exp` claim | +| `username` | string | Username | +| `nickname` | string | Display name | +| `role` | string | `admin` or `user` | + +**Errors**: wrong username/password → HTTP 401, `{code:401, msg:"..."}`. + +```bash +curl -X POST http://localhost:18088/api/v1/auth/login \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"admin123"}' +``` + +### Streaming chat: `POST /api/v1/chat/stream` + +> Public endpoint (`SecurityConfig` permits `/api/v1/chat/stream`), but in practice you still need a token to resolve the user and permissions — pass `?token=` or the `Authorization` header. + +Returns `text/event-stream`; **not** the JSON envelope. The SSE event protocol (`meta` / `content_delta` / `done` / `error`, etc.) is documented in the [WebChat guide](./webchat.md#sse-event-protocol). + +**Request body** `ChatController.ChatStreamRequest` (`ChatController.java:1211`): + +| Field | Type | Default | Description | +|---|---|---|---| +| `agentId` | string | — | Required, target Agent ID | +| `message` | string | — | This turn's user message (mutually exclusive with `contentParts`) | +| `contentParts` | array | — | Multimodal message parts (text + image); mutually exclusive with `message` | +| `conversationId` | string | `"default"` | Conversation ID; for a new conversation use a client-generated unique string | +| `reconnect` | boolean | — | `true` = reconnect to an in-flight stream, send no new message | +| `lastEventId` | string | — | Only meaningful with `reconnect=true`: skip events with id ≤ this value to avoid duplicate replay | +| `thinkingLevel` | string | null | Reasoning depth: `off` / `low` / `medium` / `high` / `max`; null follows the Agent default | +| `modelProvider` | string | null | Per-conversation provider override (paired with `modelName`) | +| `modelName` | string | null | Per-conversation model-name override | +| `endUserId` | string | null | Third-party end-user ID, isolates memory when one MateClaw account fronts many end-users | + +The native browser `EventSource` cannot send a POST body — use `fetch()` with a streaming reader. + +```bash +curl -N -X POST "http://localhost:18088/api/v1/chat/stream?token=$TOKEN" \ + -H "Content-Type: application/json" \ + -H "Accept: text/event-stream" \ + -d '{"agentId":"1","message":"Hello","conversationId":"conv-abc123"}' +``` + +Related endpoints: `POST /api/v1/chat/{conversationId}/stop` (stop generation), `POST /api/v1/chat/{conversationId}/interrupt` (queue a follow-up without interrupting the current stream). + +### Agent management + +Mounted at `/api/v1/agents`, `@Tag("Agent管理")`. Every method requires `@RequireWorkspaceRole` (at least `viewer`; writes need `member`). + +**List** `GET /api/v1/agents?enabled=true` — request header `X-Workspace-Id`; returns `R>`. + +**Create** `POST /api/v1/agents` — request body is an `AgentEntity` (key fields below); the backend force-injects `workspaceId` and `creatorUserId`. Returns the full created entity. + +Key `AgentEntity` fields (`AgentEntity.java`): + +| Field | Type | Description | +|---|---|---| +| `id` | string | Agent ID (ignored on create, assigned by the backend) | +| `name` | string | Name | +| `description` | string | Description | +| `agentType` | string | `react` or `plan_execute` | +| `systemPrompt` | string | System prompt | +| `modelName` | string | Per-Agent model override (model name); empty = global default | +| `maxIterations` | int | Max iterations | +| `enabled` | boolean | Enabled flag | +| `icon` | string | Icon (emoji or URL) | +| `tags` | string | Tags (comma-separated) | +| `defaultThinkingLevel` | string | Default reasoning depth | +| `primaryKbId` | string | Primary knowledge base ID | +| `skillsDisabled` | boolean | Explicitly disable all skills | +| `toolsDisabled` | boolean | Explicitly disable all non-system tools | + +**Delete** `DELETE /api/v1/agents/{id}` — three-way auth: system admin / workspace admin+ / the creator. Otherwise 403. + +```bash +# List +curl http://localhost:18088/api/v1/agents?enabled=true \ + -H "Authorization: Bearer $TOKEN" -H "X-Workspace-Id: 1" + +# Create +curl -X POST http://localhost:18088/api/v1/agents \ + -H "Authorization: Bearer $TOKEN" -H "X-Workspace-Id: 1" \ + -H "Content-Type: application/json" \ + -d '{"name":"Support Agent","agentType":"react","systemPrompt":"You are a support agent","enabled":true}' +``` + +> The same tree also has `GET /api/v1/agents/{id}/chat/stream` (a GET form of SSE, coexisting with the POST `/chat/stream` above), `POST /api/v1/agents/{id}/chat` (synchronous chat), and `POST /api/v1/agents/{id}/execute` (Plan-Execute). + +### Conversation management + +Mounted at `/api/v1/conversations`, `@Tag("会话管理")`. Isolated per logged-in user (JWT principal). + +**List** `GET /api/v1/conversations` — returns `R>`. `ConversationVO` adds display fields on top of the conversation entity: + +| Field | Description | +|---|---| +| `conversationId` | Conversation ID (a string, not a Snowflake) | +| `title` | Title | +| `agentId` / `agentName` / `agentIcon` | Associated Agent | +| `username` | Owning user | +| `messageCount` | Message count | +| `lastMessage` / `lastActiveTime` | Last message and time | +| `pinned` / `archived` | Pinned / archived (0/1) | +| `modelProvider` / `modelName` | Per-conversation model override | +| `status` | `active` (active within 24h) / `closed` | +| `streamStatus` | `idle` / `running` | +| `source` | Source channel: `web` / `feishu` / `dingtalk` / `telegram` / `discord` / `wecom` / `qq` / `weixin` / `cron` | + +**Paginated** `GET /api/v1/conversations/page?page=1&size=20&keyword=xxx` — returns `R>` (pagination shape in "Conventions"). + +**Message history** `GET /api/v1/conversations/{conversationId}/messages` — supports three modes: +- No `limit`: returns all messages (`R>`, backward compatible). +- With `limit`: returns the latest `limit` messages + a `hasMore` flag: `R<{messages: MessageVO[], hasMore: boolean}>`. +- With `beforeId` + `limit`: pull up to load earlier messages. + +Key `MessageVO` fields: `id`, `role`, `content`, `toolName`, `status`, `metadata` (object, contains toolCalls etc.), `promptTokens` / `completionTokens`, `runtimeModel` / `runtimeProvider`, `contentParts`, `createTime`. + +**Per-conversation ops**: `PUT .../title` (rename), `PUT .../pin` (`{pinned:bool}`), `PUT .../model` (switch model `{modelProvider, modelName}`), `DELETE .../messages` (clear messages, keep the conversation), `DELETE .../{conversationId}` (delete conversation), `POST /batch-delete` (`{conversationIds: [...]}`), `GET .../status` (stream status `{streamStatus}`). + +> Every op first checks `isConversationOwner(conversationId, username)`; non-owners get 403. + +### Model configuration + +Mounted at `/api/v1/models`, `@Tag("模型配置管理")`. `GET /` and `GET /catalog` require `@RequireGlobalAdmin` (they include sensitive info like API keys); `/enabled`, `/default`, `/active` only need `viewer`. + +- `GET /api/v1/models` — enabled provider list (`R>`, includes keys, admin only). +- `GET /api/v1/models/enabled` — enabled model list (`R>`, no keys). +- `GET /api/v1/models/default` — global default model (`R`). +- `GET /api/v1/models/active` — current active model `{activeLlm: {provider, modelName}}`. +- `PUT /api/v1/models/active` — set the active model. + +### Audit events (pagination example) + +`GET /api/v1/audit/events` — `@RequireWorkspaceRole("admin")`, returns `R>`. The canonical example of "pagination + workspace header". + +| Query param | Default | Description | +|---|---|---| +| `action` | — | Action filter (e.g. `CREATE` / `UPDATE` / `DELETE`) | +| `resourceType` | — | Resource type filter (e.g. `AGENT`) | +| `startTime` | — | ISO 8601 start time | +| `endTime` | — | ISO 8601 end time | +| `page` | 1 | Page number | +| `size` | 20 | Page size | + +```bash +curl "http://localhost:18088/api/v1/audit/events?page=1&size=20&resourceType=AGENT" \ + -H "Authorization: Bearer $TOKEN" -H "X-Workspace-Id: 1" +``` + +### Change password: `PUT /api/v1/auth/users/{id}/password` + +Three things to note (they differ from intuition): + +1. Params go via **`@RequestParam`, not a request body**: both `oldPassword` and `newPassword` are query params. +2. The `{id}` in the path is **informational only**: the user actually operated on is resolved from the JWT principal (`auth.getName()`); a user can only change their own password. +3. Requires login (not `@RequireGlobalAdmin`). + +```bash +curl -X PUT "http://localhost:18088/api/v1/auth/users/1/password?oldPassword=admin123&newPassword=newPass456" \ + -H "Authorization: Bearer $TOKEN" +``` + +### Personal Access Token + +Mounted at `/api/v1/auth/tokens`, `@Tag("Personal Access Tokens")`. For headless / CI / SDK use. + +- `GET /api/v1/auth/tokens` — list my PATs (metadata only; **plaintext is never returned**). +- `POST /api/v1/auth/tokens` — create a PAT: **the plaintext appears only in this response**, afterwards only the SHA-256 hash is stored and cannot be recovered. Save it immediately. +- `DELETE /api/v1/auth/tokens/{id}` — soft-delete revoke; subsequent auth with this token fails. + +A created PAT (`mc_` prefix) goes straight into `Authorization: Bearer mc_...`; `JwtAuthFilter` dispatches by prefix to the PAT verification path, behaving identically to a JWT. + +### Tool approval (important clarification) + +There is **no** standalone approval REST endpoint like `POST /api/v1/approvals/{id}/resolve`. Web-side approve / deny happens by sending `/approve` or `/deny` in the waiting conversation, going through the chat-stream replay flow. The read-only "hydration" endpoint after a page refresh is `GET /api/v1/chat/{conversationId}/pending-approvals`. Auto-approval policies are managed under `/api/v1/approval/grants`. + +> Dangerous operations requiring a second confirmation raise `ConfirmRequiredException` — returning **HTTP 409** and **breaking the `R` envelope**: `{code, message, boundAgents}` (the field is `message`, not `msg`). Clients should branch on the 409 status and render a confirmation dialog. + ## Source-Aligned Route Inventory Total routes extracted: 406. diff --git a/mateclaw-server/src/main/resources/docs/en/openapi.md b/mateclaw-server/src/main/resources/docs/en/openapi.md new file mode 100644 index 000000000..21422f999 --- /dev/null +++ b/mateclaw-server/src/main/resources/docs/en/openapi.md @@ -0,0 +1,72 @@ +# OpenAPI / Swagger Guide + +The MateClaw backend integrates [SpringDoc OpenAPI](https://springdoc.org/) (`springdoc-openapi-starter-webmvc-ui`), which auto-generates an OpenAPI 3 document from every `@RestController` and serves an interactive debugging UI. This page covers how to access and use it. + +> This is the **machine-readable** doc entry point. For the human-readable endpoint details, conventions, and the full route inventory, see the [API Reference](./api.md). Relationship: Swagger = the auto-generated, machine-readable contract from source annotations; `api.md` = flagship endpoint walkthroughs + shared conventions. + +## URLs + +After deploying the backend, relative to the server address (default local port `18088`): + +| URL | Purpose | +|---|---| +| `/swagger-ui.html` | Swagger UI — browse + debug (Authorize, Try it out) | +| `/v3/api-docs` | OpenAPI 3 JSON (import into Postman / Apifox / Insomnia) | +| `/v3/api-docs.yaml` | OpenAPI 3 YAML (download, commit, or import) | + +Local examples: + +```bash +# Open in a browser +open http://localhost:18088/swagger-ui.html + +# Download the YAML +curl http://localhost:18088/v3/api-docs.yaml -o mateclaw-openapi.yaml +``` + +## Authentication (Authorize) + +Use the **Authorize** button at the top right. In the `bearerAuth` field, paste a token **without** the `Bearer ` prefix (the UI adds it automatically): + +- **JWT**: the `token` field returned by `POST /api/v1/auth/login` (starts with `eyJ...`). +- **Personal Access Token**: a `mc_...` token created via `POST /api/v1/auth/tokens`. + +Both go through the standard `Authorization: Bearer ` header; the backend `JwtAuthFilter` dispatches by prefix (JWT → JWT verification, `mc_` → PAT verification). Once authorized, protected `@RequireWorkspaceRole` / `@RequireGlobalAdmin` endpoints can be called directly via Try it out. + +> Debugging SSE streaming endpoints (`/chat/stream`, etc.) in Swagger UI is limited — the UI buffers `text/event-stream` responses. For real SSE integration, follow the [API Reference](./api.md) and use `curl -N` or a `fetch()` streaming reader. + +## Endpoint coverage + +SpringDoc auto-scans every `@RestController`. About 85% of controllers already carry `@Tag` (grouping) and `@Operation(summary)` (method summary), so the UI's grouping and endpoint descriptions are largely complete. + +**Annotation enhancements not yet done** (out of scope for this pass, left for later): + +- No `@Parameter` descriptions, `@ApiResponse` error codes, or request-body `@Schema` — for field-level docs, defer to the human-readable `api.md` walkthroughs. +- Public endpoints (login, SSE, etc.) are not individually opted out with `@SecurityRequirements({})`, so they show a lock icon in Swagger even though `SecurityConfig` already permits them — actual calls are unaffected. + +## Configuration + +Global OpenAPI metadata (title, description, version, server URL) is driven by the `OpenApiConfig` bean and overridable via `mateclaw.openapi.*` in `application.yml`: + +```yaml +mateclaw: + openapi: + title: ${MATECLAW_OPENAPI_TITLE:MateClaw REST API} + version: ${MATECLAW_OPENAPI_VERSION:1.0} + server-url: ${MATECLAW_OPENAPI_SERVER_URL:} # empty → derived from request host + description: ${MATECLAW_OPENAPI_DESCRIPTION:} # empty → built-in default +``` + +When `server-url` is empty, SpringDoc derives it from the request host so "Try it out" hits the right address; for fixed production URLs (e.g. behind a reverse proxy), set `MATECLAW_OPENAPI_SERVER_URL=https://mate.example.com`. + +## ⚠️ Security note: Swagger is currently public + +In `SecurityConfig.filterChain`, `/api/**` requires authentication, but `/swagger-ui*`, `/v3/api-docs*`, and `/webjars/**` fall through to `.anyRequest().permitAll()` — meaning **Swagger UI is publicly accessible** without login; anyone can browse the full endpoint surface (including request/response schemas). + +- Local dev / intranet deployments: usually acceptable. +- Public production deployments: consider adding an explicit auth rule for the Swagger paths in `SecurityConfig` (e.g. require `@RequireGlobalAdmin`). Don't rely on the network layer alone. When locking it down, edit `SecurityConfig`, not `OpenApiConfig`. + +## See also + +- [API Reference (human-readable)](./api.md) — flagship endpoint walkthroughs + conventions + full route inventory +- [WebChat integration guide](./webchat.md) — external-site HTTP / SSE integration (incl. the SSE event protocol) diff --git a/mateclaw-server/src/main/resources/docs/zh/api.md b/mateclaw-server/src/main/resources/docs/zh/api.md index e295942ec..1c81926c7 100644 --- a/mateclaw-server/src/main/resources/docs/zh/api.md +++ b/mateclaw-server/src/main/resources/docs/zh/api.md @@ -2,6 +2,8 @@ 本页以 `mateclaw-server/src/main/java` 下的 Spring MVC Controller 注解为准。下面的路由索引由源码注解重建;如果它和旧功能页冲突,以本页和源码为接口契约。 +> 想要机读的 OpenAPI 文档(导入 Postman / Apifox、在线调试)?见 [OpenAPI / Swagger 指南](./openapi.md) —— 部署后访问 `/swagger-ui.html`。 + ## 全局契约 应用 REST 端点默认使用 `/api/v1` 前缀。大多数 JSON 响应使用项目统一信封: @@ -34,6 +36,119 @@ Authorization: Bearer 工作空间接口通常接受 `X-Workspace-Id`。省略时,很多 handler 会为了桌面/本地兼容回退到 workspace `1`。 +## 通用约定 + +下面是所有端点共用的结构约定。读完这一节,再看后面的旗舰端点示例和完整路由表就能对上。 + +### 统一响应信封 `R` + +源码:`vip.mate.common.result.R`(`R.java`)。三个字段: + +| 字段 | 类型 | 含义 | +|---|---|---| +| `code` | `int` | 状态码。`200` 为成功;其余见下方「状态码」 | +| `msg` | `string` | 提示信息。**注意是 `msg` 不是 `message`** | +| `data` | `T` | 业务数据;失败时为 `null` | + +成功示例: + +```json +{ "code": 200, "msg": "success", "data": { "id": "1", "name": "助手A" } } +``` + +失败示例: + +```json +{ "code": 401, "msg": "Token expired or invalid", "data": null } +``` + +**HTTP 状态码与 `code` 对齐**:`RHttpStatusAdvice`(`ResponseBodyAdvice`)在 `code != 200` 时把 HTTP 状态设成 `HttpStatus.resolve(code)`。即业务码 `401` → HTTP 401,业务码 `404` → HTTP 404。业务码若不是合法 HTTP 状态(如 `1001`、`2001`),则 HTTP 回落到 `500`。 + +### 状态码 + +源码:`vip.mate.common.result.ResultCode`。注意区分两类: + +| 码 | 含义 | 是否直接作 HTTP 状态 | +|---|---|---| +| `200` | 成功 | 是 | +| `400` | 参数错误 | 是 | +| `401` | 未认证 | 是 | +| `403` | 无权限 | 是 | +| `404` | 资源不存在 | 是 | +| `500` | 系统错误 | 是 | +| `1001` | Agent 不存在 | 否(HTTP 500) | +| `1002` | Agent 忙碌 | 否(HTTP 500) | +| `2001` | LLM 调用错误 | 否(HTTP 500) | +| `3001` | 工具不存在 | 否(HTTP 500) | +| `4001` | 渠道错误 | 否(HTTP 500) | + +### 错误模型 + +错误统一由 `GlobalExceptionHandler`(`@RestControllerAdvice`)处理。下表覆盖常见情况: + +| HTTP | 触发 | 响应体 | +|---|---|---| +| 400 | `@Valid` / `BindException` 校验失败 | `{code:400, msg:"字段名: 默认信息"}` — 只返回**首个**字段错误 | +| 400 | `MethodArgumentTypeMismatchException`(如 `/{id}` 传了非数字) | `{code:400, msg:"Invalid value for parameter 'X': expected Long"}` | +| 401 | 未认证 / token 无效(`SecurityConfig` 的 `authenticationEntryPoint`) | `{code:401, msg:"Token expired or invalid"}` | +| 403 | 工作区角色不足(`WorkspaceAccessInterceptor` 直接写响应) | `{code:403, msg:"...", data:null}` | +| 404 | 路由不匹配(`NoResourceFoundException`) | `{code:404, msg:"Resource not found"}` | +| 405 | 方法不允许(`HttpRequestMethodNotSupportedException`) | `{code:405, msg:"Method not allowed"}` | +| 409 | 需二次确认(`ConfirmRequiredException`) | **打破信封**:`{code, message, boundAgents}`(字段是 `message`,**不是** `msg`)—— 全 API 唯一非 `R` 响应 | +| 500 | 兜底(`Exception`) | `{code:500, msg:"Internal server error"}` — 栈不外泄 | +| 503 | 异步超时(非 SSE 请求) | `{code:503, msg:"Request timeout, please try again"}` | + +> SSE 端点(`/chat/stream` 等)出错时**不发 JSON 信封**,而是发送 SSE `error` 事件:`event: error` / `data: {"message":"..."}`。详见 [WebChat 指南](./webchat.md#sse-事件协议)。 + +### 分页结构 + +分页端点直接返回 `R>`,`data` 是 MyBatis Plus 的 `Page` 序列化结构: + +```json +{ + "code": 200, + "data": { + "records": [ /* 当前页数据 */ ], + "total": 128, + "size": 20, + "current": 1, + "pages": 7 + } +} +``` + +| 字段 | 含义 | +|---|---| +| `records` | 当前页数据数组(**字段名是 `records`**,不是 `list`/`items`) | +| `total` | 总记录数 | +| `size` | 每页条数 | +| `current` | 当前页码,从 `1` 开始 | +| `pages` | 总页数 | + +常见分页查询参数:`page`(默认 1)、`size`(默认 20)。示例端点见 `GET /api/v1/audit/events`、`GET /api/v1/conversations/page`。 + +### ID 与类型约定 + +- **Snowflake `Long` 序列化为 JSON 字符串**:后端所有主键是 `Long`,但 Jackson 序列化成字符串。客户端(尤其 JS)应**全程按 `string` 处理**,避免 `Number.MAX_SAFE_INTEGER` 精度丢失。 +- **密码字段只写不读**:`UserEntity.password` 标注 `@JsonProperty(access = WRITE_ONLY)`,登录/建用户时接受写入,但任何响应里都不会出现。 + +### 认证模型 + +`JwtAuthFilter` 支持三种 token 形态,全部走 `Authorization` 头: + +1. **JWT**:`Authorization: Bearer `。以 `eyJ` 开头(base64 头)。登录接口返回的 `token` 字段即此。 +2. **Personal Access Token (PAT)**:`Authorization: Bearer `。以 `mc_` 前缀开头,用于 headless / CI / SDK 场景。PAT 在 `POST /api/v1/auth/tokens` 创建时**明文只返回一次**,之后只存哈希。filter 按 `mc_` 前缀分发到 PAT 校验路径。 +3. **SSE 的 `?token=` 查询参数**:浏览器原生 `EventSource` 不能设置自定义请求头,SSE 流式端点额外接受 `?token=`(JWT 或 PAT 均可)。 + +**滑动续期**:JWT 接近过期(默认剩余 < 2 小时)时,响应头回传新 token —— `X-New-Token: `(同时设 `Access-Control-Expose-Headers: X-New-Token`)。客户端应监听并替换本地存储的 token。JWT TTL 默认 24 小时(`mateclaw.jwt.expiration=86400000`)。 + +### `X-Workspace-Id` 的工作机制 + +- **无 ThreadLocal / 请求上下文持有**。工作区 ID 通过两种途径消费: + 1. **RBAC 拦截**:`WorkspaceAccessInterceptor` 对标注了 `@RequireWorkspaceRole`(角色 owner > admin > member > viewer)或 `@RequireGlobalAdmin` 的方法,读取 `X-Workspace-Id` 做权限校验。缺失或无法解析时回落 workspace `1`。权限不足直接写 403 JSON 响应。 + 2. **业务读取**:许多 Controller 用 `@RequestHeader(value="X-Workspace-Id", required=false) Long workspaceId` 直接取值用于数据查询范围;缺失时同样回落 `1`。 +- 因此工作区隔离由「拦截器鉴权 + Controller 自取」两段配合实现,客户端调用工作区相关接口时应显式传 `X-Workspace-Id`。 + ## 常用接口 ### 登录 @@ -72,6 +187,217 @@ curl -N -X POST http://localhost:18088/api/v1/chat/stream \ `/api/v1/talk/ws` 由 `WebSocketConfig` 注册,用于 Talk Mode。它会出现在 `SecurityConfig` 的公共 WebSocket 路由里,但不计入下面的 controller 路由索引。 +## 旗舰端点参考 + +下面是接入最频繁的端点的完整请求/响应说明。字段均与源码 DTO 一一对齐。下面的 406 行路由表是完整索引,本节是高频端点的「人读」详解。 + +### 登录:`POST /api/v1/auth/login` + +公开端点(无需认证)。换取 JWT。 + +**请求体** `LoginRequest`(`AuthController.java`): + +| 字段 | 类型 | 说明 | +|---|---|---| +| `username` | string | 用户名 | +| `password` | string | 密码 | + +**响应** `R`: + +```json +{ + "code": 200, + "data": { + "id": "1", + "token": "eyJhbGciOi...", + "username": "admin", + "nickname": "管理员", + "role": "admin" + } +} +``` + +| 字段 | 类型 | 说明 | +|---|---|---| +| `id` | string | 用户 ID(Snowflake,字符串) | +| `token` | string | **JWT**,后续请求放 `Authorization: Bearer `。响应里没有独立 expiry 字段,过期时间在 JWT 的 `exp` claim 内 | +| `username` | string | 用户名 | +| `nickname` | string | 昵称 | +| `role` | string | `admin` 或 `user` | + +**错误**:用户名/密码错误 → HTTP 401,`{code:401, msg:"用户名或密码错误"}`。 + +```bash +curl -X POST http://localhost:18088/api/v1/auth/login \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"admin123"}' +``` + +### 流式对话:`POST /api/v1/chat/stream` + +> 公开端点(`SecurityConfig` 放行 `/api/v1/chat/stream`),但实际使用时仍需 token 才能定位用户与权限——传 `?token=` 或 `Authorization` 头。 + +返回 `text/event-stream`,**不走 JSON 信封**。SSE 事件协议(`meta` / `content_delta` / `done` / `error` 等)见 [WebChat 指南](./webchat.md#sse-事件协议)。 + +**请求体** `ChatController.ChatStreamRequest`(`ChatController.java:1211`): + +| 字段 | 类型 | 默认 | 说明 | +|---|---|---|---| +| `agentId` | string | — | 必填,目标 Agent ID | +| `message` | string | — | 本轮用户消息(与 `contentParts` 二选一) | +| `contentParts` | array | — | 多模态消息分片(图文混合),与 `message` 二选一 | +| `conversationId` | string | `"default"` | 会话 ID;新会话用客户端生成的唯一串 | +| `reconnect` | boolean | — | `true` = 断线重连,不发新消息,只附着到已有流 | +| `lastEventId` | string | — | 仅 `reconnect=true` 有意义:跳过 id ≤ 此值的事件,避免重放重复 | +| `thinkingLevel` | string | null | 思考深度:`off` / `low` / `medium` / `high` / `max`;null 跟随 Agent 默认 | +| `modelProvider` | string | null | 本会话模型 provider 覆盖(与 `modelName` 配对) | +| `modelName` | string | null | 本会话模型名覆盖 | +| `endUserId` | string | null | 第三方终端用户 ID,用于一个 MateClaw 账号下的记忆隔离 | + +浏览器原生 `EventSource` 不支持 POST body,必须用 `fetch()` + 流式 reader。 + +```bash +curl -N -X POST "http://localhost:18088/api/v1/chat/stream?token=$TOKEN" \ + -H "Content-Type: application/json" \ + -H "Accept: text/event-stream" \ + -d '{"agentId":"1","message":"你好","conversationId":"conv-abc123"}' +``` + +相关端点:`POST /api/v1/chat/{conversationId}/stop`(停止生成)、`POST /api/v1/chat/{conversationId}/interrupt`(排队后续消息,不打断当前流)。 + +### Agent 管理 + +挂在 `/api/v1/agents`,`@Tag("Agent管理")`。所有方法需 `@RequireWorkspaceRole`(至少 `viewer`,写入需 `member`)。 + +**列表** `GET /api/v1/agents?enabled=true` — 请求头 `X-Workspace-Id`;返回 `R>`。 + +**创建** `POST /api/v1/agents` — 请求体是 `AgentEntity`(关键字段见下),后端强制注入 `workspaceId` 与 `creatorUserId`。返回创建后的完整实体。 + +`AgentEntity` 关键字段(`AgentEntity.java`): + +| 字段 | 类型 | 说明 | +|---|---|---| +| `id` | string | Agent ID(创建时忽略,后端分配) | +| `name` | string | 名称 | +| `description` | string | 描述 | +| `agentType` | string | `react` 或 `plan_execute` | +| `systemPrompt` | string | 系统提示词 | +| `modelName` | string | Per-Agent 模型覆盖(模型名);空则用全局默认 | +| `maxIterations` | int | 最大迭代次数 | +| `enabled` | boolean | 是否启用 | +| `icon` | string | 图标(emoji 或 URL) | +| `tags` | string | 标签(逗号分隔) | +| `defaultThinkingLevel` | string | 默认思考深度 | +| `primaryKbId` | string | 主知识库 ID | +| `skillsDisabled` | boolean | 显式禁用所有 Skill | +| `toolsDisabled` | boolean | 显式禁用所有非系统工具 | + +**删除** `DELETE /api/v1/agents/{id}` — 三选一鉴权:系统 admin / 工作区 admin+ / 创建者本人。否则 403。 + +```bash +# 列表 +curl http://localhost:18088/api/v1/agents?enabled=true \ + -H "Authorization: Bearer $TOKEN" -H "X-Workspace-Id: 1" + +# 创建 +curl -X POST http://localhost:18088/api/v1/agents \ + -H "Authorization: Bearer $TOKEN" -H "X-Workspace-Id: 1" \ + -H "Content-Type: application/json" \ + -d '{"name":"客服助手","agentType":"react","systemPrompt":"你是一名客服","enabled":true}' +``` + +> 同目录下还有 `GET /api/v1/agents/{id}/chat/stream`(GET 形式的 SSE,与上面 POST `/chat/stream` 并存)、`POST /api/v1/agents/{id}/chat`(同步对话)、`POST /api/v1/agents/{id}/execute`(Plan-Execute)。 + +### 会话管理 + +挂在 `/api/v1/conversations`,`@Tag("会话管理")`。按当前登录用户(JWT principal)隔离。 + +**列表** `GET /api/v1/conversations` — 返回 `R>`。`ConversationVO` 在会话实体基础上补充展示字段: + +| 字段 | 说明 | +|---|---| +| `conversationId` | 会话 ID(字符串,非 Snowflake) | +| `title` | 标题 | +| `agentId` / `agentName` / `agentIcon` | 关联 Agent | +| `username` | 会话归属用户 | +| `messageCount` | 消息数 | +| `lastMessage` / `lastActiveTime` | 最近消息与时间 | +| `pinned` / `archived` | 置顶 / 归档(0/1) | +| `modelProvider` / `modelName` | 会话级模型覆盖 | +| `status` | `active`(24h 内活跃)/ `closed` | +| `streamStatus` | `idle` / `running` | +| `source` | 来源渠道:`web` / `feishu` / `dingtalk` / `telegram` / `discord` / `wecom` / `qq` / `weixin` / `cron` | + +**分页** `GET /api/v1/conversations/page?page=1&size=20&keyword=xxx` — 返回 `R>`(分页结构见「通用约定」)。 + +**消息历史** `GET /api/v1/conversations/{conversationId}/messages` — 支持三种模式: +- 不传 `limit`:返回全部消息(`R>`,向后兼容)。 +- 传 `limit`:返回最新 `limit` 条 + `hasMore` 标志:`R<{messages: MessageVO[], hasMore: boolean}>`。 +- 传 `beforeId` + `limit`:上拉加载更早消息。 + +`MessageVO` 关键字段:`id`、`role`、`content`、`toolName`、`status`、`metadata`(对象,含 toolCalls 等)、`promptTokens` / `completionTokens`、`runtimeModel` / `runtimeProvider`、`contentParts`、`createTime`。 + +**会话级操作**:`PUT .../title`(重命名)、`PUT .../pin`(置顶 `{pinned:bool}`)、`PUT .../model`(切换模型 `{modelProvider, modelName}`)、`DELETE .../messages`(清空消息保留会话)、`DELETE .../{conversationId}`(删除会话)、`POST /batch-delete`(`{conversationIds: [...]}`)、`GET .../status`(查流状态 `{streamStatus}`)。 + +> 所有操作都先校验 `isConversationOwner(conversationId, username)`,非归属者返回 403。 + +### 模型配置 + +挂在 `/api/v1/models`,`@Tag("模型配置管理")`。`GET /` 与 `GET /catalog` 需 `@RequireGlobalAdmin`(含 API Key 等敏感信息);`/enabled`、`/default`、`/active` 仅需 `viewer`。 + +- `GET /api/v1/models` — 已启用 Provider 列表(`R>`,含密钥,admin only)。 +- `GET /api/v1/models/enabled` — 已启用模型列表(`R>`,无密钥)。 +- `GET /api/v1/models/default` — 全局默认模型(`R`)。 +- `GET /api/v1/models/active` — 当前激活模型 `{activeLlm: {provider, modelName}}`。 +- `PUT /api/v1/models/active` — 设置激活模型。 + +### 审计事件(分页示范) + +`GET /api/v1/audit/events` — `@RequireWorkspaceRole("admin")`,返回 `R>`,是「分页 + 工作区头」的标准示范。 + +| 查询参数 | 默认 | 说明 | +|---|---|---| +| `action` | — | 动作过滤(如 `CREATE` / `UPDATE` / `DELETE`) | +| `resourceType` | — | 资源类型过滤(如 `AGENT`) | +| `startTime` | — | ISO 8601 起始时间 | +| `endTime` | — | ISO 8601 结束时间 | +| `page` | 1 | 页码 | +| `size` | 20 | 每页条数 | + +```bash +curl "http://localhost:18088/api/v1/audit/events?page=1&size=20&resourceType=AGENT" \ + -H "Authorization: Bearer $TOKEN" -H "X-Workspace-Id: 1" +``` + +### 修改密码:`PUT /api/v1/auth/users/{id}/password` + +注意三点(与直觉不同): + +1. 参数走 **`@RequestParam` 而非请求体**:`oldPassword` 和 `newPassword` 都是 query 参数。 +2. 路径里的 `{id}` **仅信息性**:实际操作的用户从 JWT principal 解析(`auth.getName()`),用户只能改自己的密码。 +3. 需登录(非 `@RequireGlobalAdmin`)。 + +```bash +curl -X PUT "http://localhost:18088/api/v1/auth/users/1/password?oldPassword=admin123&newPassword=newPass456" \ + -H "Authorization: Bearer $TOKEN" +``` + +### Personal Access Token + +挂在 `/api/v1/auth/tokens`,`@Tag("Personal Access Tokens")`。用于 headless / CI / SDK。 + +- `GET /api/v1/auth/tokens` — 列出我的 PAT(仅元数据,**明文永不返回**)。 +- `POST /api/v1/auth/tokens` — 创建 PAT:**明文只在此响应里出现一次**,之后只存 SHA-256 哈希,无法找回。创建后立即保存。 +- `DELETE /api/v1/auth/tokens/{id}` — 软删除吊销,此后用该 token 鉴权会失败。 + +创建出的 PAT(`mc_` 前缀)可直接放 `Authorization: Bearer mc_...`,`JwtAuthFilter` 按前缀分发到 PAT 校验路径,与 JWT 行为一致。 + +### 工具审批(重要澄清) + +**没有** `POST /api/v1/approvals/{id}/resolve` 这样的独立审批 REST 端点。Web 端的批准 / 拒绝通过在等待中的会话里发送 `/approve` 或 `/deny` 走 chat stream replay 流程。刷新页面后的只读「补水」接口是 `GET /api/v1/chat/{conversationId}/pending-approvals`。自动批准策略在 `/api/v1/approval/grants` 下管理。 + +> 需二次确认的危险操作会触发 `ConfirmRequiredException` —— 返回 **HTTP 409** 且**打破 `R` 信封**:`{code, message, boundAgents}`(字段是 `message` 不是 `msg`),客户端应按 409 状态码分支渲染确认弹窗。 + ## 源码对齐路由索引 抽取到的路由总数:406。 diff --git a/mateclaw-server/src/main/resources/docs/zh/openapi.md b/mateclaw-server/src/main/resources/docs/zh/openapi.md new file mode 100644 index 000000000..b076811de --- /dev/null +++ b/mateclaw-server/src/main/resources/docs/zh/openapi.md @@ -0,0 +1,72 @@ +# OpenAPI / Swagger 指南 + +MateClaw 后端集成了 [SpringDoc OpenAPI](https://springdoc.org/)(`springdoc-openapi-starter-webmvc-ui`),自动把所有 `@RestController` 的端点生成为 OpenAPI 3 文档,并提供在线调试 UI。本页说明如何访问和使用。 + +> 本文是**机读文档**入口。人读的端点详解、通用约定与完整路由索引见 [API 参考](./api.md)。两者关系:Swagger = 由源码注解自动生成的机读契约;`api.md` = 旗舰端点人读详解 + 通用约定。 + +## 访问地址 + +部署后端后,相对服务地址(本地默认端口 `18088`): + +| 地址 | 用途 | +|---|---| +| `/swagger-ui.html` | Swagger UI 在线浏览 + 调试(Authorize、Try it out) | +| `/v3/api-docs` | OpenAPI 3 JSON(可导入 Postman / Apifox / Insomnia) | +| `/v3/api-docs.yaml` | OpenAPI 3 YAML(可下载、纳入版本库或导入工具) | + +本地示例: + +```bash +# 浏览器打开 +open http://localhost:18088/swagger-ui.html + +# 下载 YAML +curl http://localhost:18088/v3/api-docs.yaml -o mateclaw-openapi.yaml +``` + +## 鉴权(Authorize) + +页面右上角 **Authorize** 按钮。在 `bearerAuth` 输入框填入 token(不带 `Bearer ` 前缀,UI 会自动加): + +- **JWT**:登录 `POST /api/v1/auth/login` 拿到的 `token` 字段(`eyJ...` 开头)。 +- **Personal Access Token**:在 `POST /api/v1/auth/tokens` 创建的 `mc_...` token。 + +两种 token 都走标准 `Authorization: Bearer ` 头,后端 `JwtAuthFilter` 按前缀自动分发(JWT → JWT 校验,`mc_` → PAT 校验)。授权后,受保护的 `@RequireWorkspaceRole` / `@RequireGlobalAdmin` 端点即可在 UI 内直接 Try it out。 + +> SSE 流式端点(`/chat/stream` 等)在 Swagger UI 里调试体验有限 —— UI 对 `text/event-stream` 的渲染是缓冲式的。正式集成 SSE 请按 [API 参考](./api.md#流式对话-post-apiv1chatstream) 用 `curl -N` 或 `fetch()` 流式 reader。 + +## 端点覆盖范围 + +SpringDoc 自动扫描所有 `@RestController`,约 85% 的 Controller 已标注 `@Tag`(分组)与 `@Operation(summary)`(方法摘要),所以 Swagger UI 的分组与端点说明基本齐全。 + +**当前未做的注解增强**(不在本次范围,留作后续): + +- 没有 `@Parameter` 描述、`@ApiResponse` 错误码、请求体 `@Schema` —— 这些字段文档以 `api.md` 人读详解为准。 +- 公开端点(登录、SSE 等)没有逐个加 `@SecurityRequirements({})` opt-out,所以 Swagger 上会显示锁图标,但实际调用不受影响(`SecurityConfig` 已放行)。 + +## 配置项 + +全局 OpenAPI 元信息(标题、描述、版本、服务器地址)由 `OpenApiConfig` Bean 驱动,可通过 `application.yml` 的 `mateclaw.openapi.*` 覆盖: + +```yaml +mateclaw: + openapi: + title: ${MATECLAW_OPENAPI_TITLE:MateClaw REST API} + version: ${MATECLAW_OPENAPI_VERSION:1.0} + server-url: ${MATECLAW_OPENAPI_SERVER_URL:} # 留空则从请求 host 推导 + description: ${MATECLAW_OPENAPI_DESCRIPTION:} # 留空则用内置默认描述 +``` + +`server-url` 留空时由 SpringDoc 从请求 host 推导,避免 "Try it out" 打到错误地址;生产若需固定(如反代后),设 `MATECLAW_OPENAPI_SERVER_URL=https://mate.example.com`。 + +## ⚠️ 安全提示:Swagger 当前公开可访问 + +`SecurityConfig.filterChain` 中 `/api/**` 要求认证,但 `/swagger-ui*`、`/v3/api-docs*`、`/webjars/**` 落到 `.anyRequest().permitAll()` —— 即 **Swagger UI 当前是公开可访问的**,无需登录即可浏览全部端点结构(含请求/响应 schema)。 + +- 本地开发 / 内网部署:通常可接受。 +- 公网生产部署:建议在 `SecurityConfig` 显式给 Swagger 路径加鉴权规则(如要求 `@RequireGlobalAdmin`),**不要**仅靠网络层防护。收口时应改 `SecurityConfig`,而非 `OpenApiConfig`。 + +## 关联 + +- [API 参考(人读)](./api.md) —— 旗舰端点详解 + 通用约定 + 完整路由索引 +- [WebChat 接入指南](./webchat.md) —— 外部网站 HTTP / SSE 集成(含 SSE 事件协议)