History

feat(pluginhost): introduce browser-navigable plugin resources in Management API

- 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.

Luis Pater committed 5 months agov7.1.59

Commit 44ea9ab1 parent 2aeb41c22 files · +341 −69Browse files

Showing the changes to examples/plugin (13 of 22 files). Show all

Unified · Split

13 files
  1. examples/plugin/README.md +5 −3
  2. examples/plugin/README_CN.md +5 −3
  3. examples/plugin/host-callback/c/src/plugin.c +2 −2
  4. examples/plugin/host-callback/go/main.go +2 −2
  5. examples/plugin/host-callback/rust/src/lib.rs +2 −2
  6. examples/plugin/management-api/c/src/plugin.c +2 −2
  7. examples/plugin/management-api/go/main.go +2 −2
  8. examples/plugin/management-api/rust/src/lib.rs +1 −1
  9. examples/plugin/simple/README.md +3 −1
  10. examples/plugin/simple/README_CN.md +3 −1
  11. examples/plugin/simple/c/src/plugin.c +3 −3
  12. examples/plugin/simple/go/main.go +11 −6
  13. examples/plugin/simple/rust/src/lib.rs +2 −2
examples/plugin/README.md+5−3
@@ -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
26Most standard capability examples contain `go/`, `c/`, and `rust/` subdirectories. Specialized examples may provide only the implementation language they need.26Most 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
73Menu 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/...`.
examples/plugin/README_CN.md+5−3
@@ -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/...` 下。
examples/plugin/host-callback/c/src/plugin.c+2−2
@@ -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\"}}");
examples/plugin/host-callback/go/main.go+2−2
@@ -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), nil140 return errorEnvelope("unknown_method", "unknown method: "+method), nil
141 }141 }
examples/plugin/host-callback/rust/src/lib.rs+2−2
@@ -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 077 0
examples/plugin/management-api/c/src/plugin.c+2−2
@@ -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\"}}");
examples/plugin/management-api/go/main.go+2−2
@@ -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), nil138 return errorEnvelope("unknown_method", "unknown method: "+method), nil
139 }139 }
examples/plugin/management-api/rust/src/lib.rs+1−1
@@ -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 074 0
examples/plugin/simple/README.md+3−1
@@ -187,7 +187,9 @@ PUT /v0/management/plugins/{pluginID}/config · 186 unmodified lines
187PATCH /v0/management/plugins/{pluginID}/config187PATCH /v0/management/plugins/{pluginID}/config
188```188```
189 189
190Plugin-owned Management API routes are registered through `management.register` and handled through `management.handle`.190Plugin-owned Management API routes are registered through the `routes` field of `management.register` and handled through `management.handle`.
191
192Browser-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 Boundary194## Trust Boundary
193 195
examples/plugin/simple/README_CN.md+3−1
@@ -185,7 +185,9 @@ PUT /v0/management/plugins/{pluginID}/config · 184 unmodified lines
185PATCH /v0/management/plugins/{pluginID}/config185PATCH /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
examples/plugin/simple/c/src/plugin.c+3−3
@@ -84,8 +84,8 @@ static const char* CLI_REGISTER_RESPONSE = · 83 unmodified lines
84static const char* CLI_EXECUTE_RESPONSE =84static const char* CLI_EXECUTE_RESPONSE =
85 "{\"ok\":true,\"result\":{\"Stdout\":\"cGx1Z2luIGV4YW1wbGUgYyBjb21tYW5kCg==\",\"ExitCode\":0}}";85 "{\"ok\":true,\"result\":{\"Stdout\":\"cGx1Z2luIGV4YW1wbGUgYyBjb21tYW5kCg==\",\"ExitCode\":0}}";
86static const char* MANAGEMENT_REGISTER_RESPONSE =86static 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.\"}]}}";
89static const char* UNKNOWN_METHOD_RESPONSE =89static 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\"}}";
91static const char* INVALID_METHOD_RESPONSE =91static 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,
examples/plugin/simple/go/main.go+11−6
@@ -100,7 +100,8 @@ type streamResponse struct { · 99 unmodified lines
100}100}
101 101
102type managementRegistrationResponse struct {102type 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
106func main() {}107func 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), nil224 return errorEnvelope("unknown_method", "unknown method: "+method), nil
220 }225 }
examples/plugin/simple/rust/src/lib.rs+2−2
@@ -18,7 +18,7 @@ const FRONTEND_AUTH_RESPONSE: &str = r#"{"ok":true,"result":{"Authenticated":tru · 17 unmodified lines
18const STREAM_RESPONSE: &str = r#"{"ok":true,"result":{"headers":{"content-type":["text/event-stream"]},"chunks":[{"Payload":"cGx1Z2luLWV4YW1wbGUtcnVzdAo="}]}}"#;18const STREAM_RESPONSE: &str = r#"{"ok":true,"result":{"headers":{"content-type":["text/event-stream"]},"chunks":[{"Payload":"cGx1Z2luLWV4YW1wbGUtcnVzdAo="}]}}"#;
19const CLI_REGISTER_RESPONSE: &str = r#"{"ok":true,"result":{"Flags":[{"Name":"plugin-example-rust-command","Usage":"Run the example Rust ABI plugin command","Type":"bool"}]}}"#;19const CLI_REGISTER_RESPONSE: &str = r#"{"ok":true,"result":{"Flags":[{"Name":"plugin-example-rust-command","Usage":"Run the example Rust ABI plugin command","Type":"bool"}]}}"#;
20const CLI_EXECUTE_RESPONSE: &str = r#"{"ok":true,"result":{"Stdout":"cGx1Z2luIGV4YW1wbGUgcnVzdCBjb21tYW5kCg==","ExitCode":0}}"#;20const CLI_EXECUTE_RESPONSE: &str = r#"{"ok":true,"result":{"Stdout":"cGx1Z2luIGV4YW1wbGUgcnVzdCBjb21tYW5kCg==","ExitCode":0}}"#;
21const 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."}]}}"#;21const 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."}]}}"#;
22const UNKNOWN_METHOD_RESPONSE: &str = r#"{"ok":false,"error":{"code":"unknown_method","message":"unknown method"}}"#;22const UNKNOWN_METHOD_RESPONSE: &str = r#"{"ok":false,"error":{"code":"unknown_method","message":"unknown method"}}"#;
23const INVALID_METHOD_RESPONSE: &str = r#"{"ok":false,"error":{"code":"invalid_method","message":"method is required"}}"#;23const 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),