- Added `resources` field in `management.register` for defining browser-accessible resources. - Updated examples and documentation to reflect resource-based paths under `/v0/resource/plugins/<pluginID>/...`. - Replaced legacy `GET` menu routes with resource-based implementations for consistent plugin behavior. - Enhanced request handling for resource paths, including proper response headers and streamlined test coverage.
Showing the changes to examples/plugin/simple (5 of 22 files). Show all
Unified · Split
| @@ -187,7 +187,9 @@ PUT /v0/management/plugins/{pluginID}/config · 186 unmodified lines | ||
| 187 | 187 | PATCH /v0/management/plugins/{pluginID}/config |
| 188 | 188 | ``` |
| 189 | 189 | |
| 190 | −Plugin-owned Management API routes are registered through `management.register` and handled through `management.handle`. | |
| 190 | +Plugin-owned Management API routes are registered through the `routes` field of `management.register` and handled through `management.handle`. | |
| 191 | + | |
| 192 | +Browser-navigable menu resources are registered through the `resources` field of `management.register`. CPA exposes those resources under `/v0/resource/plugins/<pluginID>/...`; for example, a plugin with ID `example` and resource path `/status` is served as `/v0/resource/plugins/example/status`. | |
| 191 | 193 | |
| 192 | 194 | ## Trust Boundary |
| 193 | 195 | |
| @@ -185,7 +185,9 @@ PUT /v0/management/plugins/{pluginID}/config · 184 unmodified lines | ||
| 185 | 185 | PATCH /v0/management/plugins/{pluginID}/config |
| 186 | 186 | ``` |
| 187 | 187 | |
| 188 | −插件自有 Management API 路由通过 `management.register` 注册,通过 `management.handle` 处理。 | |
| 188 | +插件自有 Management API 路由通过 `management.register` 的 `routes` 字段注册,并通过 `management.handle` 处理。 | |
| 189 | + | |
| 190 | +可由浏览器直接访问的菜单资源通过 `management.register` 的 `resources` 字段注册。CPA 会将这些资源暴露在 `/v0/resource/plugins/<pluginID>/...` 下;例如插件 ID 为 `example` 且资源路径为 `/status` 时,最终路径是 `/v0/resource/plugins/example/status`。 | |
| 189 | 191 | |
| 190 | 192 | ## 信任边界 |
| 191 | 193 | |
| @@ -84,8 +84,8 @@ static const char* CLI_REGISTER_RESPONSE = · 83 unmodified lines | ||
| 84 | 84 | static const char* CLI_EXECUTE_RESPONSE = |
| 85 | 85 | "{\"ok\":true,\"result\":{\"Stdout\":\"cGx1Z2luIGV4YW1wbGUgYyBjb21tYW5kCg==\",\"ExitCode\":0}}"; |
| 86 | 86 | static const char* MANAGEMENT_REGISTER_RESPONSE = |
| 87 | − "{\"ok\":true,\"result\":{\"Routes\":[{\"Method\":\"GET\",\"Path\":\"/plugins/example-c/status\"," | |
| 88 | − "\"Menu\":\"Example C Plugin\",\"Description\":\"Shows example C plugin status.\"}]}}"; | |
| 87 | + "{\"ok\":true,\"result\":{\"Resources\":[{\"Path\":\"/status\"," | |
| 88 | + "\"Menu\":\"Example C Plugin\",\"Description\":\"CPA exposes this menu resource under /v0/resource/plugins/example-c/status.\"}]}}"; | |
| 89 | 89 | static const char* UNKNOWN_METHOD_RESPONSE = |
| 90 | 90 | "{\"ok\":false,\"error\":{\"code\":\"unknown_method\",\"message\":\"unknown method\"}}"; |
| 91 | 91 | static const char* INVALID_METHOD_RESPONSE = |
| @@ -421,7 +421,7 @@ static char* make_http_response(const uint8_t* request, size_t request_len) { · 329 unmodified lines | ||
| 421 | 421 | char* url = extract_json_string(json, "URL"); |
| 422 | 422 | char* path = extract_json_string(json, "Path"); |
| 423 | 423 | char* method_escaped = json_escape(method == NULL ? "GET" : method); |
| 424 | − char* target_escaped = json_escape(url != NULL ? url : (path == NULL ? "/plugins/example-c/status" : path)); | |
| 424 | + char* target_escaped = json_escape(url != NULL ? url : (path == NULL ? "/v0/resource/plugins/example-c/status" : path)); | |
| 425 | 425 | char* body_json = format_string( |
| 426 | 426 | "{\"plugin\":\"example-c\",\"method\":\"%s\",\"target\":\"%s\"}", |
| 427 | 427 | method_escaped == NULL ? "" : method_escaped, |
| @@ -100,7 +100,8 @@ type streamResponse struct { · 99 unmodified lines | ||
| 100 | 100 | } |
| 101 | 101 | |
| 102 | 102 | type managementRegistrationResponse struct { |
| 103 | − Routes []pluginapi.ManagementRoute `json:"routes,omitempty"` | |
| 103 | + Routes []pluginapi.ManagementRoute `json:"routes,omitempty"` | |
| 104 | + Resources []pluginapi.ResourceRoute `json:"resources,omitempty"` | |
| 104 | 105 | } |
| 105 | 106 | |
| 106 | 107 | func main() {} |
| @@ -207,14 +208,18 @@ func handleMethod(method string, request []byte) ([]byte, error) { · 100 unmodified lines | ||
| 207 | 208 | case pluginabi.MethodCommandLineExecute: |
| 208 | 209 | return okEnvelope(pluginapi.CommandLineExecutionResponse{Stdout: []byte("plugin example command\n")}) |
| 209 | 210 | case pluginabi.MethodManagementRegister: |
| 210 | − return okEnvelope(managementRegistrationResponse{Routes: []pluginapi.ManagementRoute{{ | |
| 211 | − Method: http.MethodGet, | |
| 212 | − Path: "/plugins/example/status", | |
| 211 | + // CPA exposes menu resources under /v0/resource/plugins/<plugin-id>/. | |
| 212 | + return okEnvelope(managementRegistrationResponse{Resources: []pluginapi.ResourceRoute{{ | |
| 213 | + Path: "/status", | |
| 213 | 214 | Menu: "Example Plugin", |
| 214 | − Description: "Shows example plugin status.", | |
| 215 | + Description: "Shows example plugin status as a browser-navigable resource.", | |
| 215 | 216 | }}}) |
| 216 | 217 | case pluginabi.MethodManagementHandle: |
| 217 | − return okEnvelope(pluginapi.ManagementResponse{StatusCode: http.StatusOK, Body: []byte(`{"plugin":"example"}`)}) | |
| 218 | + return okEnvelope(pluginapi.ManagementResponse{ | |
| 219 | + StatusCode: http.StatusOK, | |
| 220 | + Headers: http.Header{"Content-Type": []string{"text/html; charset=utf-8"}}, | |
| 221 | + Body: []byte(`<!doctype html><title>Example Plugin</title><main>Example Plugin</main>`), | |
| 222 | + }) | |
| 218 | 223 | default: |
| 219 | 224 | return errorEnvelope("unknown_method", "unknown method: "+method), nil |
| 220 | 225 | } |
| @@ -18,7 +18,7 @@ const FRONTEND_AUTH_RESPONSE: &str = r#"{"ok":true,"result":{"Authenticated":tru · 17 unmodified lines | ||
| 18 | 18 | const STREAM_RESPONSE: &str = r#"{"ok":true,"result":{"headers":{"content-type":["text/event-stream"]},"chunks":[{"Payload":"cGx1Z2luLWV4YW1wbGUtcnVzdAo="}]}}"#; |
| 19 | 19 | const CLI_REGISTER_RESPONSE: &str = r#"{"ok":true,"result":{"Flags":[{"Name":"plugin-example-rust-command","Usage":"Run the example Rust ABI plugin command","Type":"bool"}]}}"#; |
| 20 | 20 | const CLI_EXECUTE_RESPONSE: &str = r#"{"ok":true,"result":{"Stdout":"cGx1Z2luIGV4YW1wbGUgcnVzdCBjb21tYW5kCg==","ExitCode":0}}"#; |
| 21 | −const MANAGEMENT_REGISTER_RESPONSE: &str = r#"{"ok":true,"result":{"Routes":[{"Method":"GET","Path":"/plugins/example-rust/status","Menu":"Example Rust Plugin","Description":"Shows example Rust plugin status."}]}}"#; | |
| 21 | +const MANAGEMENT_REGISTER_RESPONSE: &str = r#"{"ok":true,"result":{"Resources":[{"Path":"/status","Menu":"Example Rust Plugin","Description":"CPA exposes this menu resource under /v0/resource/plugins/example-rust/status."}]}}"#; | |
| 22 | 22 | const UNKNOWN_METHOD_RESPONSE: &str = r#"{"ok":false,"error":{"code":"unknown_method","message":"unknown method"}}"#; |
| 23 | 23 | const INVALID_METHOD_RESPONSE: &str = r#"{"ok":false,"error":{"code":"invalid_method","message":"method is required"}}"#; |
| 24 | 24 | |
| @@ -170,7 +170,7 @@ fn make_http_response(request: &[u8]) -> String { · 145 unmodified lines | ||
| 170 | 170 | let method = extract_json_string(&json, "Method").unwrap_or_else(|| "GET".to_string()); |
| 171 | 171 | let target = extract_json_string(&json, "URL") |
| 172 | 172 | .or_else(|| extract_json_string(&json, "Path")) |
| 173 | − .unwrap_or_else(|| "/plugins/example-rust/status".to_string()); | |
| 173 | + .unwrap_or_else(|| "/v0/resource/plugins/example-rust/status".to_string()); | |
| 174 | 174 | let body = format!( |
| 175 | 175 | r#"{{"plugin":"example-rust","method":"{}","target":"{}"}}"#, |
| 176 | 176 | json_escape(&method), |