- 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 (13 of 22 files). Show all
Unified · Split
| @@ -20,8 +20,8 @@ This directory contains standard dynamic library plugin examples for the CLIProx · 19 unmodified lines | |||
| 20 | - `thinking/`: thinking applier capability only. | 20 | - `thinking/`: thinking applier capability only. |
| 21 | - `usage/`: usage observer capability only. | 21 | - `usage/`: usage observer capability only. |
| 22 | - `cli/`: command-line capability only. | 22 | - `cli/`: command-line capability only. |
| 23 | - `management-api/`: Management API capability only. | 23 | - `management-api/`: Management API and resource capability only. |
| 24 | - `host-callback/`: minimal Management API route that demonstrates host callbacks. | 24 | - `host-callback/`: minimal plugin resource that demonstrates host callbacks. |
| 25 | 25 | ||
| 26 | Most standard capability examples contain `go/`, `c/`, and `rust/` subdirectories. Specialized examples may provide only the implementation language they need. | 26 | Most standard capability examples contain `go/`, `c/`, and `rust/` subdirectories. Specialized examples may provide only the implementation language they need. |
| 27 | 27 | ||
| @@ -68,4 +68,6 @@ Artifacts are written to `examples/plugin/bin`. · 40 unmodified lines | |||
| 68 | 68 | ||
| 69 | `protocol-format` uses a minimal executor because format declarations belong to executor capabilities. | 69 | `protocol-format` uses a minimal executor because format declarations belong to executor capabilities. |
| 70 | 70 | ||
| 71 | `host-callback` uses a minimal Management API route because host callbacks are invoked from plugin methods and are not standalone capabilities. | 71 | `host-callback` uses a minimal plugin resource because host callbacks are invoked from plugin methods and are not standalone capabilities. |
| 72 | |||
| 73 | Menu resources returned by `management.register` through the `resources` field are exposed by CPA under `/v0/resource/plugins/<pluginID>/...`. Authenticated plugin Management API routes remain under `/v0/management/...`. | ||
| @@ -20,8 +20,8 @@ · 19 unmodified lines | |||
| 20 | - `thinking/`:只演示 Thinking 处理能力。 | 20 | - `thinking/`:只演示 Thinking 处理能力。 |
| 21 | - `usage/`:只演示 Usage 观察能力。 | 21 | - `usage/`:只演示 Usage 观察能力。 |
| 22 | - `cli/`:只演示命令行扩展能力。 | 22 | - `cli/`:只演示命令行扩展能力。 |
| 23 | - `management-api/`:只演示 Management API 扩展能力。 | 23 | - `management-api/`:只演示 Management API 和资源扩展能力。 |
| 24 | - `host-callback/`:使用最小 Management API 路由演示宿主回调。 | 24 | - `host-callback/`:使用最小插件资源演示宿主回调。 |
| 25 | 25 | ||
| 26 | 多数标准能力示例都包含 `go/`、`c/` 和 `rust/` 三个子目录。专用示例可能只提供所需的实现语言。 | 26 | 多数标准能力示例都包含 `go/`、`c/` 和 `rust/` 三个子目录。专用示例可能只提供所需的实现语言。 |
| 27 | 27 | ||
| @@ -68,4 +68,6 @@ make -C examples/plugin build · 40 unmodified lines | |||
| 68 | 68 | ||
| 69 | `protocol-format` 使用最小执行器承载,因为格式声明属于执行器能力。 | 69 | `protocol-format` 使用最小执行器承载,因为格式声明属于执行器能力。 |
| 70 | 70 | ||
| 71 | `host-callback` 使用最小 Management API 路由承载,因为宿主回调只能从插件方法内部发起,不是独立能力。 | 71 | `host-callback` 使用最小插件资源承载,因为宿主回调只能从插件方法内部发起,不是独立能力。 |
| 72 | |||
| 73 | `management.register` 通过 `resources` 字段返回的菜单资源会由 CPA 暴露在 `/v0/resource/plugins/<pluginID>/...` 下。需要认证的插件自有 Management API 路由仍保留在 `/v0/management/...` 下。 | ||
| @@ -84,14 +84,14 @@ static int plugin_call(const char* method, const uint8_t* request, size_t reques · 83 unmodified lines | |||
| 84 | return 0; | 84 | return 0; |
| 85 | } | 85 | } |
| 86 | if (strcmp(method, "management.register") == 0) { | 86 | if (strcmp(method, "management.register") == 0) { |
| 87 | write_response(response, "{\"ok\":true,\"result\":{\"routes\":[{\"Method\":\"GET\",\"Path\":\"/plugins/example-host-callback-c/status\",\"Menu\":\"Host Callback\",\"Description\":\"Host callback example carried by a minimal Management API route.\"}]}}"); | 87 | write_response(response, "{\"ok\":true,\"result\":{\"resources\":[{\"Path\":\"/status\",\"Menu\":\"Host Callback\",\"Description\":\"CPA exposes this menu resource under /v0/resource/plugins/example-host-callback-c/status.\"}]}}"); |
| 88 | return 0; | 88 | return 0; |
| 89 | } | 89 | } |
| 90 | if (strcmp(method, "management.handle") == 0) { | 90 | if (strcmp(method, "management.handle") == 0) { |
| 91 | call_host("host.log", "{\"level\":\"info\",\"message\":\"example-host-callback-c host callback log\",\"fields\":{\"plugin\":\"example-host-callback-c\"}}"); | 91 | call_host("host.log", "{\"level\":\"info\",\"message\":\"example-host-callback-c host callback log\",\"fields\":{\"plugin\":\"example-host-callback-c\"}}"); |
| 92 | call_host("host.http.do", "{\"method\":\"GET\",\"url\":\"https://example.com\",\"headers\":{\"user-agent\":[\"example-host-callback-c\"]}}"); | 92 | call_host("host.http.do", "{\"method\":\"GET\",\"url\":\"https://example.com\",\"headers\":{\"user-agent\":[\"example-host-callback-c\"]}}"); |
| 93 | 93 | ||
| 94 | write_response(response, "{\"ok\":true,\"result\":{\"StatusCode\":200,\"Headers\":{\"content-type\":[\"application/json\"]},\"Body\":\"eyJwbHVnaW4iOiJleGFtcGxlLWhvc3QtY2FsbGJhY2stYyJ9\"}}"); | 94 | write_response(response, "{\"ok\":true,\"result\":{\"StatusCode\":200,\"Headers\":{\"content-type\":[\"text/html; charset=utf-8\"]},\"Body\":\"PCFkb2N0eXBlIGh0bWw+PHRpdGxlPkhvc3QgQ2FsbGJhY2s8L3RpdGxlPjxtYWluPkhvc3QgQ2FsbGJhY2sgcmVzb3VyY2U8L21haW4+\"}}"); |
| 95 | return 0; | 95 | return 0; |
| 96 | } | 96 | } |
| 97 | write_response(response, "{\"ok\":false,\"error\":{\"code\":\"unknown_method\",\"message\":\"unknown method\"}}"); | 97 | write_response(response, "{\"ok\":false,\"error\":{\"code\":\"unknown_method\",\"message\":\"unknown method\"}}"); |
| @@ -131,11 +131,11 @@ func handleMethod(method string) ([]byte, error) { · 130 unmodified lines | |||
| 131 | case "plugin.reconfigure": | 131 | case "plugin.reconfigure": |
| 132 | return okEnvelopeJSON("{\"schema_version\":1,\"metadata\":{\"Name\":\"example-host-callback-go\",\"Version\":\"0.1.0\",\"Author\":\"router-for-me\",\"GitHubRepository\":\"https://github.com/router-for-me/CLIProxyAPI\",\"Logo\":\"https://example.invalid/example-host-callback-go.png\",\"ConfigFields\":[]},\"capabilities\":{\"management_api\":true}}") | 132 | return okEnvelopeJSON("{\"schema_version\":1,\"metadata\":{\"Name\":\"example-host-callback-go\",\"Version\":\"0.1.0\",\"Author\":\"router-for-me\",\"GitHubRepository\":\"https://github.com/router-for-me/CLIProxyAPI\",\"Logo\":\"https://example.invalid/example-host-callback-go.png\",\"ConfigFields\":[]},\"capabilities\":{\"management_api\":true}}") |
| 133 | case "management.register": | 133 | case "management.register": |
| 134 | return okEnvelopeJSON("{\"routes\":[{\"Method\":\"GET\",\"Path\":\"/plugins/example-host-callback-go/status\",\"Menu\":\"Host Callback\",\"Description\":\"Host callback example carried by a minimal Management API route.\"}]}") | 134 | return okEnvelopeJSON("{\"resources\":[{\"Path\":\"/status\",\"Menu\":\"Host Callback\",\"Description\":\"CPA exposes this menu resource under /v0/resource/plugins/example-host-callback-go/status.\"}]}") |
| 135 | case "management.handle": | 135 | case "management.handle": |
| 136 | callHost("host.log", []byte(`{"level":"info","message":"example-host-callback-go host callback log","fields":{"plugin":"example-host-callback-go"}}`)) | 136 | callHost("host.log", []byte(`{"level":"info","message":"example-host-callback-go host callback log","fields":{"plugin":"example-host-callback-go"}}`)) |
| 137 | callHost("host.http.do", []byte(`{"method":"GET","url":"https://example.com","headers":{"user-agent":["example-host-callback-go"]}}`)) | 137 | callHost("host.http.do", []byte(`{"method":"GET","url":"https://example.com","headers":{"user-agent":["example-host-callback-go"]}}`)) |
| 138 | return okEnvelopeJSON("{\"StatusCode\":200,\"Headers\":{\"content-type\":[\"application/json\"]},\"Body\":\"eyJwbHVnaW4iOiJleGFtcGxlLWhvc3QtY2FsbGJhY2stZ28ifQ==\"}") | 138 | return okEnvelopeJSON("{\"StatusCode\":200,\"Headers\":{\"content-type\":[\"text/html; charset=utf-8\"]},\"Body\":\"PCFkb2N0eXBlIGh0bWw+PHRpdGxlPkhvc3QgQ2FsbGJhY2s8L3RpdGxlPjxtYWluPkhvc3QgQ2FsbGJhY2sgcmVzb3VyY2U8L21haW4+\"}") |
| 139 | default: | 139 | default: |
| 140 | return errorEnvelope("unknown_method", "unknown method: "+method), nil | 140 | return errorEnvelope("unknown_method", "unknown method: "+method), nil |
| 141 | } | 141 | } |
| @@ -68,10 +68,10 @@ unsafe extern "C" fn plugin_call(method: *const c_char, request: *const u8, requ · 67 unmodified lines | |||
| 68 | let _ = request; | 68 | let _ = request; |
| 69 | let _ = request_len; | 69 | let _ = request_len; |
| 70 | match method { | 70 | match method { |
| 71 | "plugin.register" => { write_response(response, "{\"ok\":true,\"result\":{\"schema_version\":1,\"metadata\":{\"Name\":\"example-host-callback-rust\",\"Version\":\"0.1.0\",\"Author\":\"router-for-me\",\"GitHubRepository\":\"https://github.com/router-for-me/CLIProxyAPI\",\"Logo\":\"https://example.invalid/example-host-callback-rust.png\",\"ConfigFields\":[]},\"capabilities\":{\"management_api\":true}}}"); 0 },"plugin.reconfigure" => { write_response(response, "{\"ok\":true,\"result\":{\"schema_version\":1,\"metadata\":{\"Name\":\"example-host-callback-rust\",\"Version\":\"0.1.0\",\"Author\":\"router-for-me\",\"GitHubRepository\":\"https://github.com/router-for-me/CLIProxyAPI\",\"Logo\":\"https://example.invalid/example-host-callback-rust.png\",\"ConfigFields\":[]},\"capabilities\":{\"management_api\":true}}}"); 0 },"management.register" => { write_response(response, "{\"ok\":true,\"result\":{\"routes\":[{\"Method\":\"GET\",\"Path\":\"/plugins/example-host-callback-rust/status\",\"Menu\":\"Host Callback\",\"Description\":\"Host callback example carried by a minimal Management API route.\"}]}}"); 0 },"management.handle" => { | 71 | "plugin.register" => { write_response(response, "{\"ok\":true,\"result\":{\"schema_version\":1,\"metadata\":{\"Name\":\"example-host-callback-rust\",\"Version\":\"0.1.0\",\"Author\":\"router-for-me\",\"GitHubRepository\":\"https://github.com/router-for-me/CLIProxyAPI\",\"Logo\":\"https://example.invalid/example-host-callback-rust.png\",\"ConfigFields\":[]},\"capabilities\":{\"management_api\":true}}}"); 0 },"plugin.reconfigure" => { write_response(response, "{\"ok\":true,\"result\":{\"schema_version\":1,\"metadata\":{\"Name\":\"example-host-callback-rust\",\"Version\":\"0.1.0\",\"Author\":\"router-for-me\",\"GitHubRepository\":\"https://github.com/router-for-me/CLIProxyAPI\",\"Logo\":\"https://example.invalid/example-host-callback-rust.png\",\"ConfigFields\":[]},\"capabilities\":{\"management_api\":true}}}"); 0 },"management.register" => { write_response(response, "{\"ok\":true,\"result\":{\"resources\":[{\"Path\":\"/status\",\"Menu\":\"Host Callback\",\"Description\":\"CPA exposes this menu resource under /v0/resource/plugins/example-host-callback-rust/status.\"}]}}"); 0 },"management.handle" => { |
| 72 | call_host("host.log", r#"{"level":"info","message":"example-host-callback-rust host callback log","fields":{"plugin":"example-host-callback-rust"}}"#); | 72 | call_host("host.log", r#"{"level":"info","message":"example-host-callback-rust host callback log","fields":{"plugin":"example-host-callback-rust"}}"#); |
| 73 | call_host("host.http.do", r#"{"method":"GET","url":"https://example.com","headers":{"user-agent":["example-host-callback-rust"]}}"#); | 73 | call_host("host.http.do", r#"{"method":"GET","url":"https://example.com","headers":{"user-agent":["example-host-callback-rust"]}}"#); |
| 74 | write_response(response, "{\"ok\":true,\"result\":{\"StatusCode\":200,\"Headers\":{\"content-type\":[\"application/json\"]},\"Body\":\"eyJwbHVnaW4iOiJleGFtcGxlLWhvc3QtY2FsbGJhY2stcnVzdCJ9\"}}"); 0 }, | 74 | write_response(response, "{\"ok\":true,\"result\":{\"StatusCode\":200,\"Headers\":{\"content-type\":[\"text/html; charset=utf-8\"]},\"Body\":\"PCFkb2N0eXBlIGh0bWw+PHRpdGxlPkhvc3QgQ2FsbGJhY2s8L3RpdGxlPjxtYWluPkhvc3QgQ2FsbGJhY2sgcmVzb3VyY2U8L21haW4+\"}}"); 0 }, |
| 75 | _ => { | 75 | _ => { |
| 76 | write_response(response, r#"{"ok":false,"error":{"code":"unknown_method","message":"unknown method"}}"#); | 76 | write_response(response, r#"{"ok":false,"error":{"code":"unknown_method","message":"unknown method"}}"#); |
| 77 | 0 | 77 | 0 |
| @@ -84,11 +84,11 @@ static int plugin_call(const char* method, const uint8_t* request, size_t reques · 83 unmodified lines | |||
| 84 | return 0; | 84 | return 0; |
| 85 | } | 85 | } |
| 86 | if (strcmp(method, "management.register") == 0) { | 86 | if (strcmp(method, "management.register") == 0) { |
| 87 | write_response(response, "{\"ok\":true,\"result\":{\"routes\":[{\"Method\":\"GET\",\"Path\":\"/plugins/example-management-api-c/status\",\"Menu\":\"Management API\",\"Description\":\"Management API capability example.\"}]}}"); | 87 | write_response(response, "{\"ok\":true,\"result\":{\"resources\":[{\"Path\":\"/status\",\"Menu\":\"Management API\",\"Description\":\"CPA exposes this menu resource under /v0/resource/plugins/example-management-api-c/status.\"}]}}"); |
| 88 | return 0; | 88 | return 0; |
| 89 | } | 89 | } |
| 90 | if (strcmp(method, "management.handle") == 0) { | 90 | if (strcmp(method, "management.handle") == 0) { |
| 91 | write_response(response, "{\"ok\":true,\"result\":{\"StatusCode\":200,\"Headers\":{\"content-type\":[\"application/json\"]},\"Body\":\"eyJwbHVnaW4iOiJleGFtcGxlLW1hbmFnZW1lbnQtYXBpLWMifQ==\"}}"); | 91 | write_response(response, "{\"ok\":true,\"result\":{\"StatusCode\":200,\"Headers\":{\"content-type\":[\"text/html; charset=utf-8\"]},\"Body\":\"PCFkb2N0eXBlIGh0bWw+PHRpdGxlPk1hbmFnZW1lbnQgQVBJPC90aXRsZT48bWFpbj5NYW5hZ2VtZW50IEFQSSByZXNvdXJjZTwvbWFpbj4=\"}}"); |
| 92 | return 0; | 92 | return 0; |
| 93 | } | 93 | } |
| 94 | write_response(response, "{\"ok\":false,\"error\":{\"code\":\"unknown_method\",\"message\":\"unknown method\"}}"); | 94 | write_response(response, "{\"ok\":false,\"error\":{\"code\":\"unknown_method\",\"message\":\"unknown method\"}}"); |
| @@ -131,9 +131,9 @@ func handleMethod(method string) ([]byte, error) { · 130 unmodified lines | |||
| 131 | case "plugin.reconfigure": | 131 | case "plugin.reconfigure": |
| 132 | return okEnvelopeJSON("{\"schema_version\":1,\"metadata\":{\"Name\":\"example-management-api-go\",\"Version\":\"0.1.0\",\"Author\":\"router-for-me\",\"GitHubRepository\":\"https://github.com/router-for-me/CLIProxyAPI\",\"Logo\":\"https://example.invalid/example-management-api-go.png\",\"ConfigFields\":[]},\"capabilities\":{\"management_api\":true}}") | 132 | return okEnvelopeJSON("{\"schema_version\":1,\"metadata\":{\"Name\":\"example-management-api-go\",\"Version\":\"0.1.0\",\"Author\":\"router-for-me\",\"GitHubRepository\":\"https://github.com/router-for-me/CLIProxyAPI\",\"Logo\":\"https://example.invalid/example-management-api-go.png\",\"ConfigFields\":[]},\"capabilities\":{\"management_api\":true}}") |
| 133 | case "management.register": | 133 | case "management.register": |
| 134 | return okEnvelopeJSON("{\"routes\":[{\"Method\":\"GET\",\"Path\":\"/plugins/example-management-api-go/status\",\"Menu\":\"Management API\",\"Description\":\"Management API capability example.\"}]}") | 134 | return okEnvelopeJSON("{\"resources\":[{\"Path\":\"/status\",\"Menu\":\"Management API\",\"Description\":\"CPA exposes this menu resource under /v0/resource/plugins/example-management-api-go/status.\"}]}") |
| 135 | case "management.handle": | 135 | case "management.handle": |
| 136 | return okEnvelopeJSON("{\"StatusCode\":200,\"Headers\":{\"content-type\":[\"application/json\"]},\"Body\":\"eyJwbHVnaW4iOiJleGFtcGxlLW1hbmFnZW1lbnQtYXBpLWdvIn0=\"}") | 136 | return okEnvelopeJSON("{\"StatusCode\":200,\"Headers\":{\"content-type\":[\"text/html; charset=utf-8\"]},\"Body\":\"PCFkb2N0eXBlIGh0bWw+PHRpdGxlPk1hbmFnZW1lbnQgQVBJPC90aXRsZT48bWFpbj5NYW5hZ2VtZW50IEFQSSByZXNvdXJjZTwvbWFpbj4=\"}") |
| 137 | default: | 137 | default: |
| 138 | return errorEnvelope("unknown_method", "unknown method: "+method), nil | 138 | return errorEnvelope("unknown_method", "unknown method: "+method), nil |
| 139 | } | 139 | } |
| @@ -68,7 +68,7 @@ unsafe extern "C" fn plugin_call(method: *const c_char, request: *const u8, requ · 67 unmodified lines | |||
| 68 | let _ = request; | 68 | let _ = request; |
| 69 | let _ = request_len; | 69 | let _ = request_len; |
| 70 | match method { | 70 | match method { |
| 71 | "plugin.register" => { write_response(response, "{\"ok\":true,\"result\":{\"schema_version\":1,\"metadata\":{\"Name\":\"example-management-api-rust\",\"Version\":\"0.1.0\",\"Author\":\"router-for-me\",\"GitHubRepository\":\"https://github.com/router-for-me/CLIProxyAPI\",\"Logo\":\"https://example.invalid/example-management-api-rust.png\",\"ConfigFields\":[]},\"capabilities\":{\"management_api\":true}}}"); 0 },"plugin.reconfigure" => { write_response(response, "{\"ok\":true,\"result\":{\"schema_version\":1,\"metadata\":{\"Name\":\"example-management-api-rust\",\"Version\":\"0.1.0\",\"Author\":\"router-for-me\",\"GitHubRepository\":\"https://github.com/router-for-me/CLIProxyAPI\",\"Logo\":\"https://example.invalid/example-management-api-rust.png\",\"ConfigFields\":[]},\"capabilities\":{\"management_api\":true}}}"); 0 },"management.register" => { write_response(response, "{\"ok\":true,\"result\":{\"routes\":[{\"Method\":\"GET\",\"Path\":\"/plugins/example-management-api-rust/status\",\"Menu\":\"Management API\",\"Description\":\"Management API capability example.\"}]}}"); 0 },"management.handle" => { write_response(response, "{\"ok\":true,\"result\":{\"StatusCode\":200,\"Headers\":{\"content-type\":[\"application/json\"]},\"Body\":\"eyJwbHVnaW4iOiJleGFtcGxlLW1hbmFnZW1lbnQtYXBpLXJ1c3QifQ==\"}}"); 0 }, | 71 | "plugin.register" => { write_response(response, "{\"ok\":true,\"result\":{\"schema_version\":1,\"metadata\":{\"Name\":\"example-management-api-rust\",\"Version\":\"0.1.0\",\"Author\":\"router-for-me\",\"GitHubRepository\":\"https://github.com/router-for-me/CLIProxyAPI\",\"Logo\":\"https://example.invalid/example-management-api-rust.png\",\"ConfigFields\":[]},\"capabilities\":{\"management_api\":true}}}"); 0 },"plugin.reconfigure" => { write_response(response, "{\"ok\":true,\"result\":{\"schema_version\":1,\"metadata\":{\"Name\":\"example-management-api-rust\",\"Version\":\"0.1.0\",\"Author\":\"router-for-me\",\"GitHubRepository\":\"https://github.com/router-for-me/CLIProxyAPI\",\"Logo\":\"https://example.invalid/example-management-api-rust.png\",\"ConfigFields\":[]},\"capabilities\":{\"management_api\":true}}}"); 0 },"management.register" => { write_response(response, "{\"ok\":true,\"result\":{\"resources\":[{\"Path\":\"/status\",\"Menu\":\"Management API\",\"Description\":\"CPA exposes this menu resource under /v0/resource/plugins/example-management-api-rust/status.\"}]}}"); 0 },"management.handle" => { write_response(response, "{\"ok\":true,\"result\":{\"StatusCode\":200,\"Headers\":{\"content-type\":[\"text/html; charset=utf-8\"]},\"Body\":\"PCFkb2N0eXBlIGh0bWw+PHRpdGxlPk1hbmFnZW1lbnQgQVBJPC90aXRsZT48bWFpbj5NYW5hZ2VtZW50IEFQSSByZXNvdXJjZTwvbWFpbj4=\"}}"); 0 }, |
| 72 | _ => { | 72 | _ => { |
| 73 | write_response(response, r#"{"ok":false,"error":{"code":"unknown_method","message":"unknown method"}}"#); | 73 | write_response(response, r#"{"ok":false,"error":{"code":"unknown_method","message":"unknown method"}}"#); |
| 74 | 0 | 74 | 0 |
| @@ -187,7 +187,9 @@ PUT /v0/management/plugins/{pluginID}/config · 186 unmodified lines | |||
| 187 | PATCH /v0/management/plugins/{pluginID}/config | 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 | ## Trust Boundary | 194 | ## Trust Boundary |
| 193 | 195 | ||
| @@ -185,7 +185,9 @@ PUT /v0/management/plugins/{pluginID}/config · 184 unmodified lines | |||
| 185 | PATCH /v0/management/plugins/{pluginID}/config | 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 | static const char* CLI_EXECUTE_RESPONSE = | 84 | static const char* CLI_EXECUTE_RESPONSE = |
| 85 | "{\"ok\":true,\"result\":{\"Stdout\":\"cGx1Z2luIGV4YW1wbGUgYyBjb21tYW5kCg==\",\"ExitCode\":0}}"; | 85 | "{\"ok\":true,\"result\":{\"Stdout\":\"cGx1Z2luIGV4YW1wbGUgYyBjb21tYW5kCg==\",\"ExitCode\":0}}"; |
| 86 | static const char* MANAGEMENT_REGISTER_RESPONSE = | 86 | static const char* MANAGEMENT_REGISTER_RESPONSE = |
| 87 | "{\"ok\":true,\"result\":{\"Routes\":[{\"Method\":\"GET\",\"Path\":\"/plugins/example-c/status\"," | 87 | "{\"ok\":true,\"result\":{\"Resources\":[{\"Path\":\"/status\"," |
| 88 | "\"Menu\":\"Example C Plugin\",\"Description\":\"Shows example C plugin status.\"}]}}"; | 88 | "\"Menu\":\"Example C Plugin\",\"Description\":\"CPA exposes this menu resource under /v0/resource/plugins/example-c/status.\"}]}}"; |
| 89 | static const char* UNKNOWN_METHOD_RESPONSE = | 89 | static const char* UNKNOWN_METHOD_RESPONSE = |
| 90 | "{\"ok\":false,\"error\":{\"code\":\"unknown_method\",\"message\":\"unknown method\"}}"; | 90 | "{\"ok\":false,\"error\":{\"code\":\"unknown_method\",\"message\":\"unknown method\"}}"; |
| 91 | static const char* INVALID_METHOD_RESPONSE = | 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 | char* url = extract_json_string(json, "URL"); | 421 | char* url = extract_json_string(json, "URL"); |
| 422 | char* path = extract_json_string(json, "Path"); | 422 | char* path = extract_json_string(json, "Path"); |
| 423 | char* method_escaped = json_escape(method == NULL ? "GET" : method); | 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 | char* body_json = format_string( | 425 | char* body_json = format_string( |
| 426 | "{\"plugin\":\"example-c\",\"method\":\"%s\",\"target\":\"%s\"}", | 426 | "{\"plugin\":\"example-c\",\"method\":\"%s\",\"target\":\"%s\"}", |
| 427 | method_escaped == NULL ? "" : method_escaped, | 427 | method_escaped == NULL ? "" : method_escaped, |
| @@ -100,7 +100,8 @@ type streamResponse struct { · 99 unmodified lines | |||
| 100 | } | 100 | } |
| 101 | 101 | ||
| 102 | type managementRegistrationResponse struct { | 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 | func main() {} | 107 | func main() {} |
| @@ -207,14 +208,18 @@ func handleMethod(method string, request []byte) ([]byte, error) { · 100 unmodified lines | |||
| 207 | case pluginabi.MethodCommandLineExecute: | 208 | case pluginabi.MethodCommandLineExecute: |
| 208 | return okEnvelope(pluginapi.CommandLineExecutionResponse{Stdout: []byte("plugin example command\n")}) | 209 | return okEnvelope(pluginapi.CommandLineExecutionResponse{Stdout: []byte("plugin example command\n")}) |
| 209 | case pluginabi.MethodManagementRegister: | 210 | case pluginabi.MethodManagementRegister: |
| 210 | return okEnvelope(managementRegistrationResponse{Routes: []pluginapi.ManagementRoute{{ | 211 | // CPA exposes menu resources under /v0/resource/plugins/<plugin-id>/. |
| 211 | Method: http.MethodGet, | 212 | return okEnvelope(managementRegistrationResponse{Resources: []pluginapi.ResourceRoute{{ |
| 212 | Path: "/plugins/example/status", | 213 | Path: "/status", |
| 213 | Menu: "Example Plugin", | 214 | Menu: "Example Plugin", |
| 214 | Description: "Shows example plugin status.", | 215 | Description: "Shows example plugin status as a browser-navigable resource.", |
| 215 | }}}) | 216 | }}}) |
| 216 | case pluginabi.MethodManagementHandle: | 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 | default: | 223 | default: |
| 219 | return errorEnvelope("unknown_method", "unknown method: "+method), nil | 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 | const STREAM_RESPONSE: &str = r#"{"ok":true,"result":{"headers":{"content-type":["text/event-stream"]},"chunks":[{"Payload":"cGx1Z2luLWV4YW1wbGUtcnVzdAo="}]}}"#; | 18 | const STREAM_RESPONSE: &str = r#"{"ok":true,"result":{"headers":{"content-type":["text/event-stream"]},"chunks":[{"Payload":"cGx1Z2luLWV4YW1wbGUtcnVzdAo="}]}}"#; |
| 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"}]}}"#; | 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 | const CLI_EXECUTE_RESPONSE: &str = r#"{"ok":true,"result":{"Stdout":"cGx1Z2luIGV4YW1wbGUgcnVzdCBjb21tYW5kCg==","ExitCode":0}}"#; | 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 | const UNKNOWN_METHOD_RESPONSE: &str = r#"{"ok":false,"error":{"code":"unknown_method","message":"unknown method"}}"#; | 22 | const UNKNOWN_METHOD_RESPONSE: &str = r#"{"ok":false,"error":{"code":"unknown_method","message":"unknown method"}}"#; |
| 23 | const INVALID_METHOD_RESPONSE: &str = r#"{"ok":false,"error":{"code":"invalid_method","message":"method is required"}}"#; | 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 | let method = extract_json_string(&json, "Method").unwrap_or_else(|| "GET".to_string()); | 170 | let method = extract_json_string(&json, "Method").unwrap_or_else(|| "GET".to_string()); |
| 171 | let target = extract_json_string(&json, "URL") | 171 | let target = extract_json_string(&json, "URL") |
| 172 | .or_else(|| extract_json_string(&json, "Path")) | 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 | let body = format!( | 174 | let body = format!( |
| 175 | r#"{{"plugin":"example-rust","method":"{}","target":"{}"}}"#, | 175 | r#"{{"plugin":"example-rust","method":"{}","target":"{}"}}"#, |
| 176 | json_escape(&method), | 176 | json_escape(&method), |