diff --git a/README.md b/README.md index 5d61d2e..71a7023 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ SDK 的源码都在本仓库的 `sdk/` 下,发布在[本仓库的 Releases](ht | [原生构建工作流(开发中)](guides/native-build-workflows.md) | Node/Java SDK 的构建执行、精确测试、运行准备,以及未提交源码的真实 Core 验证 | | [构建、运行与测试接入](guides/build-tool-integration.md) | 可用的执行提供者示例、工程后端边界,以及开发中的 API 3 状态 | | [编写调试适配器插件](guides/debug-adapter-plugin.md) | API 4:从最小清单到启动方式、先编译再调试的 `executable`、调试控制台补全,以及预发布与转正 | -| [插件后端:工作区上下文与宿主服务(开发中)](guides/plugin-backend-workspace.md) | `backend` 2 与协议 3:免页面启动、工作区上下文、宿主通知、失败码,以及协议 3 只增不改时后端要遵守的规则 | +| [插件后端:工作区上下文与宿主服务(开发中)](guides/plugin-backend-workspace.md) | `backend` 2 与协议 3:免页面启动、工作区上下文、纯后端命令(`ui` 4)、文档/文件/窗口宿主方法、宿主通知、失败码,以及协议 3 只增不改时后端要遵守的规则 | 收录和上架的规则由 Lithe Store 维护:[发布社区插件](https://github.com/Lithe-IDEA/Lithe-store/blob/main/docs/publishing.md)讲怎么打包、提交收录和发布新版本,[版本追踪与更新](https://github.com/Lithe-IDEA/Lithe-store/blob/main/docs/versioning-and-updates.md)讲 Lithe 怎么选择可安装的版本。 diff --git a/contract.lock.json b/contract.lock.json index 10c669e..acfc13c 100644 --- a/contract.lock.json +++ b/contract.lock.json @@ -3,12 +3,12 @@ "source": { "repository": "https://github.com/1lck/Lithe-IDEA", "directory": "shared/contracts/plugins/v1", - "revision": "f20601ee6194f32d597031556ea628de586ac2e2" + "revision": "9175bd73431aec5086f1fe4c532be54fc49ba4e8" }, "files": [ { "path": "README.md", - "sha256": "1ac89012a6e620ddeec63b0fc82460c56f05623a1d2a29dcf45e87fa123a4806" + "sha256": "b2864095465511324cfde17968184fb87ac9191711c18a68fd48c5db91149f35" }, { "path": "fixtures/backend-workspace.plugin.json", @@ -94,6 +94,10 @@ "path": "fixtures/build-tool-path.plugin.json", "sha256": "685144e417bbf01b013144dec109d51b56922f98811e9ea6eda34da26749165c" }, + { + "path": "fixtures/command-backend.plugin.json", + "sha256": "791f11f3b9d59d5909a9ca94788bddb7f7493b7610361553d5733d7aed18df22" + }, { "path": "fixtures/command-plans.json", "sha256": "a188fd65f6cdb47f9e9ad4b0de3a7ddcd17e4e577b34f6e25b96d2c1f2bdca7c" @@ -116,7 +120,7 @@ }, { "path": "fixtures/function-protocol.json", - "sha256": "d8f11ab3287d24210e9369ec6b5fcdb65316d6afe82c60583455c84065ae583b" + "sha256": "c00b99c31f4c2b5d2cb7644d123cdec69d8bc439a5f043e5b655db6351545cff" }, { "path": "fixtures/hello-lithe.plugin.json", @@ -228,7 +232,7 @@ }, { "path": "plugin-manifest.schema.json", - "sha256": "789a340f03a598f9e1505ff887a90ee5a0b8a004071ef331d6382a97ea1b9f11" + "sha256": "3065b75d2a2e3913e6ac002acfe50f9da179e703603b06753c9bfd171ffe8905" } ] } diff --git a/contract/v1/README.md b/contract/v1/README.md index cd3d02e..bd94d74 100644 --- a/contract/v1/README.md +++ b/contract/v1/README.md @@ -405,7 +405,7 @@ hosts. Level 3 versions each section on its own. A level 3 manifest lists in | `serverProcess` | The launch fields of language servers, of build providers with a process of their own, of VCS providers, and of the plugin backend: `launch`, `arguments`, `validationArguments`, `runtimeDependencies`, `initializationOptions` | `1` | | `executionProviders` | `executionProviders[]` | `1` | | `settings` | `settings[]` and `settingActions[]` | `1`, `2` (adds [placement, scope, effect, page buttons](#settings-v2-placement-scope-effect-and-page-buttons)) | -| `ui` | `ui` | `1`, `2` (adds [web content](#web-content-ui-2)) | +| `ui` | `ui` | `1`, `2` (web content), `3` (native controls), `4` ([backend-only commands](#backend-only-commands-ui-4)) | | `buildSystems` | Declarative build detection, models, and command plans | `1` | | `buildProviders` | `buildSystems[].provider`, including own-process and shared language-server providers; v2 tasks, v3 tests, v4 run, v5 native debug preparation, v6 read-only model inputs | `1`, `2`, `3`, `4`, `5`, `6` | | `buildModelInputs` | Optional public model sharing between enabled workspace plugins | `1` | @@ -1876,7 +1876,7 @@ granted services as `lsp.startServer.hostServices` (from the launch plan's - A session holds at most 16 unanswered host requests; a further one is refused with `plugin.host.failed`. Unanswered requests end with the session. - `hostServices` needs `pluginId` and names only services this Core defines - (`credentials`, `workspace`); otherwise `lsp.startServer` is + (`credentials`, `workspace`, or registered method-grant names); otherwise `lsp.startServer` is `invalid_request`. ### Workspace contexts and capability discovery (protocol version 3) @@ -1902,7 +1902,7 @@ notifications. grants only the services it has handlers for, so a listed method never fails for want of one. `backendAuthority` is `"local"`: the backend runs on the machine that runs Lithe. `limits` gives `pendingHostRequests`, - `functionResultBytes`, `errorMessageBytes` (the cut of a host failure's + `functionResultBytes`, `hostRequestBytes`, `hostResultBytes`, `errorMessageBytes` (the cut of a host failure's message), `errorDetailsBytes`, and `notificationBytes`. A backend must check `services` instead of assuming a method from the version. - A context is one open workspace in one window: @@ -2071,6 +2071,103 @@ So that a version 3 backend works with every host of version 3: `fixtures/function-protocol.json` (`hostRequests`) holds the unsupported-option cases. +### Public document, file and window methods (protocol version 3) + +These methods are implemented by the macOS host. Windows does not advertise +`backend` support. `getCapabilities` lists only the intersection of the plugin's +permission-derived grants and this host's registered method handlers. + +New grants in `lsp.startServer.hostServices` use **full method names**, not service +names. The legacy `workspace` grant still admits only `listContexts`, `getContext` +and `getFolders`; it never grants file reads. The legacy `credentials` grant is +unchanged. Registering another handler cannot replace a method already registered. + +All methods below require `context: {contextID, generation}`. Resource arguments +use `{rootID, path}` from that plugin's `getFolders` response; paths use `/` and +are relative to the root. A root ID is not a lifetime or an authorization token. +Opening the same folder in two windows creates independent context-owned leases; +closing one does not invalidate the other, and reopening works with generation 1 +in its new context. Remote contexts are explicitly refused by this first set of +methods. Unknown options fail before side effects with `unsupportedOption`. + +| Methods (prefix `lithe/host/`) | Permission | Params in addition to `context` | Result | +| --- | --- | --- | --- | +| `documents/list` | `documents.read` | none | `{documents:[]}` | +| `documents/getSnapshot` | `documents.read` | `documentID` | `{document:, text}` from the dirty live buffer | +| `editor/getActive` | `documents.read` | none | summary of the foreground text tab, or `null` | +| `editor/getSelections` | `documents.read` | `documentID` | `{documentID,version,selections:[{anchor,active}],primaryIndex}` | +| `editor/applyEdits` | `editor.edit` | `documentID,expectedVersion,edits:[{range,text}]` | `{documentID,version,applied}` | +| `editor/replaceSelections` | `editor.edit` | `documentID,expectedVersion,selections:[{anchor,active}],replacements:[string]` | same; one replacement for all selections or one per selection | +| `editor/insertText` | `editor.edit` | `documentID,expectedVersion,at,text` | same | +| `workspace/readDirectory` | `workspace.files.read` | `resource,cursor?,limit?` | `{entries:[{name,resource,kind}],nextCursor}` | +| `workspace/stat` | `workspace.files.read` | `resource` | `{resource,kind,size,modifiedMilliseconds}` | +| `workspace/readFile` | `workspace.files.read` | `resource,maximumBytes?` | `{resource,source:"disk",byteCount,isTruncated,identity,text?,encoding?,base64?}` | +| `workspace/watch` | `workspace.files.watch` | `resource,recursive?` (default false) | `{subscriptionID}`; events contain `{rootID,changedPaths,requiresRescan}` | +| `workspace/findFiles` | `workspace.search` | `resource` naming the root (`path:""`), `query,fileMask?,cursor?,limit?` | `{matches:[],nextCursor,isTruncated,source:"disk"}` | +| `workspace/findText` | `workspace.search` | root `resource,pattern,fileMask?,caseSensitive?,wholeWords?,regularExpression?,cursor?,limit?` | same envelope, matches `{resource,line,preview,range:null}`; the upstream search owner currently reports lines, not exact match spans | +| `editor/open`, `editor/reveal` | `editor.open` | `resource,activation?` (`select` or `background`), `range?,tab?` (`regular` only) | document identity/version and state, `wasAlreadyOpen,windowIsForeground,appliedSelection` | +| `preview/openHTML` | `preview.open` | HTML `resource,presentation?` (`editorAndPreview` or `previewOnly`), `range?` | open result plus `usesLiveBuffer:true,presentation`; explicitly changes the existing tab's presentation | +| `browser/openExternal` | `browser.open` | exactly one of HTTP(S) `url` or HTML `resource`; `unsaved?` (`openSavedFile` or `requireSaved`) for a resource | `{opened:true,source:"web"|"disk",hasUnsavedChanges?}` | +| `window/showInformation`, `window/showWarning`, `window/showError` | `window.messages` | `text,actions?` | `{selectedIndex}`; zero-based, or `null` on dismissal | + +A document summary has `documentID,version,path,languageID,isReadOnly,isDirty, +encoding,lineEnding`. `path` is workspace-relative or null for a document outside +the root; `languageID` may be null. Positions `{line,column}` are one-based UTF-16; +ranges `{start,end}` are half-open and cannot split a surrogate pair. Selections +keep anchor/active direction. Batches are atomic and one Monaco undo step. Stale +versions, read-only buffers, pending editor synchronization and IME composition +are refused. Background documents without a mounted editor can be listed/read, +but edits/selections may return `busy`; opening a foreground tab does not imply +that its web editor is already ready. Retry reads after readiness, not edits +blindly. `editor.open/reveal` currently applies a range's start as a caret and +reports `{kind:"caret",line,column}`, not a fabricated full selection. + +Limits: snapshots and file reads expose at most 128 KiB of source text/bytes per +call; oversized snapshots return `limitExceeded`, file reads report truncation. +This leaves room for worst-case JSON escaping under the 1 MiB serialized host +request/result budget. Batches contain at most 1,000 edits/selections. Directory +pages default to 200 and cap at 1,000; search pages default to 100 and cap at 200, +with the search owner's overall 200-result cap. Explicit file reads use +component-wise `O_NOFOLLOW`; listings omit symlinks. Hidden-file rules affect +listing/search/watch, not authorization for explicit reads. Open/browser paths +retain the existing canonical containment checks and their documented +check-to-open race; native backends are not OS-sandboxed. + +Machine host requests have a 30-second host deadline. User-choice messages wait +until the user answers, the request is cancelled, or the context/session closes. +Messages retain the per-plugin/window rate and queue limits (5/10 seconds, +3 pending per plugin, 10 per window, 2,000 text characters, 3 actions of 40 +characters). Session exit revokes grants and cancels requests/subscriptions; +old session callbacks cannot affect a replacement. Closing a context cancels its +requests with `contextExpired`. Notifications have a 128-entry per-session queue; +context revisions coalesce to the latest, and overflow ends the session instead +of allowing unbounded memory or silently losing commands. Notification/response +writes share the process write watchdog and stop cleanup with function requests. + +Cancellation or timeout **does not promise rollback**: an edit or external open +may already have committed. The SDK must not replay a mutation on these failures. +On `versionConflict`, reread and recompute; on `contextExpired`, resolve the +original target again, never substitute the focused window. `busy`/`rateLimited` +may be retried with bounds; `permissionDenied`/`readOnly` need user intervention. + +### Backend-only commands (`ui` 4) + +`requires.ui:4` permits `ui.commands` without `toolWindowID` and allows an empty +or omitted `toolWindows` array. Such a command requires `backend:2`, the existing +`ui.contribute` and `backend.launch` permissions, and its `onCommand:` event. +Versions 1-3 retain their window-required behavior. An old host rejects ui 4 at +installation, rather than accepting a command it cannot execute. See +`fixtures/command-backend.plugin.json`. + +The command appears through the ordinary workbench command contributions and +sends `commands/didRun` without opening a dummy tool window. Commands arriving +while initialize is pending wait until ready; a crashed backend can start again +on its declared command event. Background workspace activation has two concurrent +start slots; explicit command activation does not wait for unrelated background +plugins. `didRun` remains an event, not an acknowledgement that the command's +business action succeeded; a future result-bearing execute API must use the +existing request lifecycle rather than infer success from notification delivery. + ### Compatibility - Packages of every API level are unchanged: functions add no manifest field. diff --git a/contract/v1/fixtures/command-backend.plugin.json b/contract/v1/fixtures/command-backend.plugin.json new file mode 100644 index 0000000..31fdec6 --- /dev/null +++ b/contract/v1/fixtures/command-backend.plugin.json @@ -0,0 +1,50 @@ +{ + "schemaVersion": 2, + "id": "dev.lithe.fixture.workspacebackend", + "displayName": "Workspace Backend", + "version": "1.0.0", + "pluginAPIVersion": 3, + "requires": { + "backend": 2, + "serverProcess": 1, + "ui": 4 + }, + "vendor": { + "id": "org.example", + "displayName": "Example", + "signatureRequirement": "publisherPackage" + }, + "permissions": [ + "backend.launch", + "workspace.metadata", + "ui.contribute", + "documents.read", + "editor.edit" + ], + "activationEvents": [ + "onCommand:dev.lithe.fixture.workspacebackend.transform" + ], + "languages": [], + "backend": { + "launch": { + "kind": "packaged", + "path": "bin/backend" + }, + "arguments": [ + "--state", + "${stateDir}" + ] + }, + "ui": { + "toolWindows": [], + "commands": [ + { + "id": "dev.lithe.fixture.workspacebackend.transform", + "title": "Transform selection", + "placements": [ + "commandPalette" + ] + } + ] + } +} diff --git a/contract/v1/fixtures/function-protocol.json b/contract/v1/fixtures/function-protocol.json index 5ae2cc5..b79fdcb 100644 --- a/contract/v1/fixtures/function-protocol.json +++ b/contract/v1/fixtures/function-protocol.json @@ -278,6 +278,9 @@ } ], "hostRequests": [ + {"name": "workspace metadata never authorizes file reads", "version": 3, "hostServices": ["workspace"], "method": "lithe/host/workspace/readFile", "params": {"context": {"contextID": "w1", "generation": 1}}, "expected": {"rpcCode": -32010, "code": "plugin.host.permissionDenied"}}, + {"name": "editor read method grant never authorizes edits", "version": 3, "hostServices": ["lithe/host/editor/getSelections"], "method": "lithe/host/editor/applyEdits", "params": {}, "expected": {"rpcCode": -32010, "code": "plugin.host.permissionDenied"}}, + {"name": "only the explicit file method is forwarded", "version": 3, "hostServices": ["lithe/host/workspace/readFile"], "method": "lithe/host/workspace/readFile", "params": {"context": {"contextID": "w1", "generation": 1}, "resource": {"rootID": "r1", "path": "hello.txt"}}, "expected": {"forward": "workspace"}}, { "name": "version 1 sessions get no host service even when granted", "version": 1, @@ -410,6 +413,8 @@ "limits": { "pendingHostRequests": 16, "functionResultBytes": 1048576, + "hostRequestBytes": 1048576, + "hostResultBytes": 1048576, "errorMessageBytes": 1024, "errorDetailsBytes": 4096, "notificationBytes": 1048576 diff --git a/contract/v1/plugin-manifest.schema.json b/contract/v1/plugin-manifest.schema.json index 9612693..1d2b09a 100644 --- a/contract/v1/plugin-manifest.schema.json +++ b/contract/v1/plugin-manifest.schema.json @@ -40,7 +40,7 @@ "projectGenerators": { "description": "Entries of the New Project window: fields, requirements, and steps.", "enum": [1] }, "serverProcess": { "description": "The launch fields of language servers, of build providers that start their own process, of VCS providers, of debug adapters, and of the plugin backend. Version 2 adds runSelectedRuntime and is required exactly when a debug adapter uses it (pluginAPIVersion 4).", "enum": [1, 2] }, "settings": { "enum": [1] }, - "ui": { "description": "Version 2 adds web content; version 3 adds native controls.", "enum": [1, 2, 3] }, + "ui": { "description": "Version 2 adds web content; version 3 adds native controls; version 4 adds backend-only commands.", "enum": [1, 2, 3, 4] }, "vcsProviders": { "description": "Required exactly when vcsProviders is non-empty. The provider protocol itself is negotiated in initialize (shared/contracts/vcs-provider-v1.md).", "enum": [1] } } }, @@ -60,7 +60,16 @@ }, "permissions": { "type": "array", - "items": { "enum": ["backend.launch", "credentials.store", "debug.attach", "debug.launch", "languageServer.launch", "process.build", "process.run", "process.test", "process.vcs", "project.generate", "ui.contribute", "ui.web", "workspace.metadata"] } + "items": { "enum": ["backend.launch", "credentials.store", "debug.attach", "debug.launch", "languageServer.launch", "process.build", "process.run", "process.test", "process.vcs", "project.generate", "ui.contribute", "ui.web", "workspace.metadata", + "workspace.files.read", + "workspace.files.watch", + "workspace.search", + "documents.read", + "editor.edit", + "editor.open", + "preview.open", + "browser.open", + "window.messages"] } }, "activationEvents": { "type": "array", @@ -114,13 +123,11 @@ "ui": { "type": "object", "additionalProperties": false, - "required": [ - "toolWindows" - ], + "description": "ui 4 permits backend commands without toolWindowID or tool windows; older ui versions require a tool window.", "properties": { "toolWindows": { "type": "array", - "minItems": 1, + "minItems": 0, "maxItems": 64, "items": { "type": "object", @@ -189,7 +196,6 @@ "required": [ "id", "title", - "toolWindowID", "placements" ], "properties": { diff --git a/guides/plugin-backend-workspace.md b/guides/plugin-backend-workspace.md index ec4112a..93aba20 100644 --- a/guides/plugin-backend-workspace.md +++ b/guides/plugin-backend-workspace.md @@ -42,6 +42,8 @@ - `backend` 2 至少要有一种启动方式:网页工具窗口(首次页面调用,同 `backend` 1)、`ui.commands[]` 里的命令 (`onCommand:` 也会启动后端),或 `onWorkspaceOpen`。一种都没有是 `plugin.manifest.invalid`。 +- 只想要命令、不要工具窗口时,声明 `requires.ui: 4`:命令可以不写 `toolWindowID`,`toolWindows` 可以为空或省略, + 见第 7 节。`ui` 1~3 的命令仍必须挂在工具窗口上。 - `workspace.metadata` 只让后端知道打开的工作区名字、根目录、ID、来源(本地或远程)和每个工作区的私有状态目录, 不读任何文件内容。不需要这些信息就不要声明,安装审查会把它列给用户。 - 仍然拒绝 `${workspaceStateDir}`:一个插件只有一个后端进程,被所有窗口共享,工作区不是进程状态。 @@ -87,6 +89,8 @@ "limits": { "pendingHostRequests": 16, "functionResultBytes": 1048576, + "hostRequestBytes": 1048576, + "hostResultBytes": 1048576, "errorMessageBytes": 1024, "errorDetailsBytes": 4096, "notificationBytes": 1048576 @@ -95,7 +99,7 @@ ``` 上例是声明了 `workspace.metadata` 的会话。`host`、`subscriptions`、`commands` 每个插件会话都有;`services` 只列本次会话可用的方法, -没声明的权限、宿主没有实现的服务都不会出现。数值以实际返回为准。**按 `services` 判断能不能调用,不要按协议版本号推断**,见第 8 节。 +没声明的权限、宿主没有实现的服务都不会出现。数值以实际返回为准。**按 `services` 判断能不能调用,不要按协议版本号推断**,见第 10 节。 ## 6. 工作区上下文 @@ -143,7 +147,56 @@ 后端晚启动时不会补发之前的 `didChangeContexts`,启动后自己调一次 `listContexts`。 -## 8. 宿主失败与错误码 +`didRun` 只是事件,不代表命令要做的事已经成功;宿主也不会为了送达它而打开一个空工具窗口。 + +### 纯后端命令(`ui` 4) + +```json +"requires": { "backend": 2, "serverProcess": 1, "ui": 4 }, +"permissions": ["backend.launch", "ui.contribute"], +"activationEvents": ["onCommand:io.github.<你的用户名>.orders.refresh"], +"ui": { + "commands": [ + { "id": "io.github.<你的用户名>.orders.refresh", "title": "Refresh Orders", "placements": ["commandPalette"] } + ] +} +``` + +这样的命令要求 `backend` 2、`ui.contribute` 与 `backend.launch` 权限,以及对应的 `onCommand:`。 +不认识 `ui` 4 的旧宿主会在安装时拒绝插件,不会装上一个执行不了的命令。完整示例见 +[`fixtures/command-backend.plugin.json`](../contract/v1/fixtures/command-backend.plugin.json)。 + +## 8. 文档、文件与窗口方法 + +协议 3 还开放了一组公共宿主方法,每组对应一个新权限。目前只有 macOS 宿主实现;Windows 宿主不提供 `backend`。 +`getCapabilities.services` 只列出“清单权限推出的授权”与“本宿主已实现的方法”的交集,所以照第 10 节第 1 条先查再调。 + +| 方法(前缀 `lithe/host/`) | 权限 | 用途 | +| --- | --- | --- | +| `documents/list`、`documents/getSnapshot`、`editor/getActive`、`editor/getSelections` | `documents.read` | 列出打开的文档,读编辑器里未保存的内容、当前标签页和选区 | +| `editor/applyEdits`、`editor/replaceSelections`、`editor/insertText` | `editor.edit` | 带 `expectedVersion` 的编辑;一批编辑是一次原子操作、一步撤销 | +| `workspace/readDirectory`、`workspace/stat`、`workspace/readFile` | `workspace.files.read` | 读磁盘上的目录和文件 | +| `workspace/watch` | `workspace.files.watch` | 订阅文件变化,事件经 `subscriptions/notify` 送达 | +| `workspace/findFiles`、`workspace/findText` | `workspace.search` | 按文件名或内容搜索,分页返回 | +| `editor/open`、`editor/reveal` | `editor.open` | 打开文件并定位到某处 | +| `preview/openHTML` | `preview.open` | 用内置预览打开 HTML 文件 | +| `browser/openExternal` | `browser.open` | 用系统浏览器打开 HTTP(S) 链接或 HTML 文件 | +| `window/showInformation`、`window/showWarning`、`window/showError` | `window.messages` | 显示消息,可带最多 3 个按钮,返回用户选了哪个 | + +参数、结果和上限的权威定义在[契约](../contract/v1/README.md#public-document-file-and-window-methods-protocol-version-3)。写后端时要注意: + +- 每个方法都要带 `context: { contextID, generation }`。文件用 `{ rootID, path }` 指明,`path` 用 `/`、相对于 + `getFolders` 返回的根目录。这组方法明确拒绝远程工作区(`authorityUnsupported`)。 +- `workspace.metadata` 只给工作区信息,**不授权读文件**;读文件要另外声明 `workspace.files.read`。 +- 位置 `{ line, column }` 从 1 开始、按 UTF-16 计;范围是半开区间。 +- 单次读取最多返回 128 KiB:快照超出时得到 `limitExceeded`,读文件则用 `isTruncated` 标明截断。一批编辑或选区最多 1,000 项。 +- 取消或超时**不代表已回滚**:编辑或打开可能已经生效,不要自动重放修改。遇到 `versionConflict` 先重新读取再计算; + 遇到 `contextExpired` 要重新找到原来的目标,不要改用当前聚焦的窗口;`busy`、`rateLimited` 可以有限次重试; + `permissionDenied`、`readOnly` 需要用户处理。 +- 后台没有挂载编辑器的文档可以列出和读取,但编辑与选区可能返回 `busy`。 +- 消息有频率和排队上限(每插件 10 秒 5 条、最多 3 条待答),用户回答前请求一直挂起。 + +## 9. 宿主失败与错误码 协议 3 的宿主失败带类型:JSON-RPC 号码与 `error.data.code` 一一对应,号码永不改义,部分失败还带 `error.data.details`。 @@ -168,7 +221,7 @@ `-32000` 到 `-32009` 留给后端自己的失败,宿主号码从 `-32010` 起。 -## 9. 协议 3 只增不改:后端要遵守的四条 +## 10. 协议 3 只增不改:后端要遵守的四条 从协议 3 起,宿主再开放新能力(新服务及其方法和通知、可选参数、结果字段、错误码)都在协议 3 内新增, 不再升协议版本;只有会让已有后端读错的改动(删除或改名、改字段含义或类型、把可选参数改成必填、改错误码号码) @@ -184,10 +237,11 @@ 新服务总是配一个新的清单权限。旧宿主不认识这个权限时,会在安装时以 `plugin.permission.unknown` 拒绝整个插件包, 权限不会被悄悄授予或忽略。 -## 10. 验证 +## 11. 验证 - 用契约里的 [`fixtures/function-protocol.json`](../contract/v1/fixtures/function-protocol.json) 核对你的实现: `hostRequests` 有不认识的选项等请求用例,`invocationContexts` 有调用上下文,`hostFailures` 有失败码映射, `hostNotifications` 有通知用例。 - 清单示例是 [`fixtures/backend-workspace.plugin.json`](../contract/v1/fixtures/backend-workspace.plugin.json); + 纯后端命令的示例是 [`fixtures/command-backend.plugin.json`](../contract/v1/fixtures/command-backend.plugin.json); `fixtures/invalid-manifests.json` 里的 backend 用例是会被拒绝的写法。