@clipbus/plugin-sdk 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +641 -0
- package/LICENSE +21 -0
- package/README.md +466 -0
- package/SPECIFICATION.md +355 -0
- package/dist/dom/autoFit.d.ts +15 -0
- package/dist/dom/consolePatch.d.ts +1 -0
- package/dist/dom/index.cjs +211 -0
- package/dist/dom/index.d.cts +6 -0
- package/dist/dom/index.d.ts +6 -0
- package/dist/dom/index.js +188 -0
- package/dist/dom/textInputState.d.ts +1 -0
- package/dist/dom/topicAdapter.d.ts +30 -0
- package/dist/generated/INDEX.runtime.generated.d.ts +6 -0
- package/dist/generated/INDEX.ui.generated.d.ts +4 -0
- package/dist/generated/capabilityClients.generated.d.ts +199 -0
- package/dist/generated/data.generated.d.ts +193 -0
- package/dist/generated/hostClients.generated.d.ts +38 -0
- package/dist/generated/runtime.actionResult.generated.d.ts +28 -0
- package/dist/generated/runtime.definePlugin.generated.d.ts +16 -0
- package/dist/generated/runtime.handlers.generated.d.ts +20 -0
- package/dist/generated/runtime.host.generated.d.ts +34 -0
- package/dist/generated/topicSubscribers.generated.d.ts +32 -0
- package/dist/generated/ui.bootstrap.generated.d.ts +15 -0
- package/dist/generated/ui.clipbus.generated.d.ts +79 -0
- package/dist/generated/wireConstants.generated.d.ts +3 -0
- package/dist/internal/capabilities.d.ts +31 -0
- package/dist/internal/index.cjs +68 -0
- package/dist/internal/index.d.ts +1 -0
- package/dist/internal/internalConsole.d.ts +7 -0
- package/dist/internal/ipcBus.d.ts +48 -0
- package/dist/internal/runtimeInvokeClient.d.ts +3 -0
- package/dist/internal/topic.d.ts +20 -0
- package/dist/runtime/defineMessage.d.ts +6 -0
- package/dist/runtime/index.cjs +163 -0
- package/dist/runtime/index.d.cts +4 -0
- package/dist/runtime/index.d.ts +4 -0
- package/dist/runtime/index.js +132 -0
- package/dist/shared/defineMessage.d.ts +7 -0
- package/dist/ui/defineMessage.d.ts +7 -0
- package/dist/ui/index.cjs +362 -0
- package/dist/ui/index.d.cts +4 -0
- package/dist/ui/index.d.ts +4 -0
- package/dist/ui/index.js +339 -0
- package/docs/README.md +34 -0
- package/docs/authoring.md +288 -0
- package/docs/capability-detection.md +105 -0
- package/docs/concepts.md +80 -0
- package/docs/entry.md +137 -0
- package/docs/faq.md +65 -0
- package/docs/item-context.md +186 -0
- package/docs/manifest.md +149 -0
- package/docs/permissions.md +32 -0
- package/docs/rpc.md +84 -0
- package/package.json +76 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# 能力检测 / Capability detection
|
|
2
|
+
|
|
3
|
+
> 本文档属于 @clipbus/plugin-sdk 开发文档 · 返回[文档地图](./README.md) · capability 真相源见 [API.md](../API.md)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 什么时候做能力检测
|
|
8
|
+
|
|
9
|
+
**只对「可能晚于宿主出现的新能力」做门控**(如 `infoPanel.open`)。`clipbus.action.complete`、`clipbus.clipboard.copyText` 等 baseline 老能力直接调即可,无需门控。
|
|
10
|
+
|
|
11
|
+
加门控的信号:能力在较新版宿主才引入,插件需在老宿主上也能降级运行。
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## UI 侧(WebView)
|
|
16
|
+
|
|
17
|
+
### 调用前门控
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { clipbus } from '@clipbus/plugin-sdk/ui';
|
|
21
|
+
|
|
22
|
+
// has() 接受类型化的 capability 名称(CapabilityMethodName)
|
|
23
|
+
if (clipbus.capabilities.has('infoPanel.open')) {
|
|
24
|
+
await clipbus.infoPanel.open({ document: { … } });
|
|
25
|
+
} else {
|
|
26
|
+
// 降级:隐藏按钮或显示提示
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
老宿主未注入能力清单时,`has()` 恒返回 `false`,安全降级。
|
|
31
|
+
|
|
32
|
+
### 调用兜底(catch)
|
|
33
|
+
|
|
34
|
+
若需要在调用处做二次防御(例如按钮在渲染后宿主降级),UI 侧可用 `instanceof`——错误在插件 bundle 内由 `parseReplyError` 构造(未跨 realm),且每个 UI bundle 只打包一份 SDK,故 `instanceof` 可靠:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { clipbus, CapabilityUnsupportedError } from '@clipbus/plugin-sdk/ui';
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
await clipbus.infoPanel.open({ document: { … } });
|
|
41
|
+
} catch (err) {
|
|
42
|
+
if (err instanceof CapabilityUnsupportedError) {
|
|
43
|
+
// err.capability 包含不支持的 capability 名称
|
|
44
|
+
console.warn('infoPanel not supported:', err.capability);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Node runtime 侧
|
|
54
|
+
|
|
55
|
+
### 调用前门控
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import type { PluginActionHandler } from '@clipbus/plugin-sdk/runtime';
|
|
59
|
+
|
|
60
|
+
const action: PluginActionHandler = {
|
|
61
|
+
async runAutoAction(input, ctx) {
|
|
62
|
+
if (ctx.host.capabilities.has('item.setTags')) {
|
|
63
|
+
await ctx.host.item.setTags({ itemID: input.item.id, tags: ['processed'] });
|
|
64
|
+
}
|
|
65
|
+
// 不支持时跳过,不报错
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 调用兜底(catch)
|
|
71
|
+
|
|
72
|
+
Node 侧错误来自**跨进程 IPC**,抛出的是反序列化的普通对象,**不是 SDK 的类实例**——`instanceof CapabilityUnsupportedError` 在这里不可靠。改用结构化判断:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
try {
|
|
76
|
+
await ctx.host.item.setTags({ itemID: input.item.id, tags: ['processed'] });
|
|
77
|
+
} catch (err: unknown) {
|
|
78
|
+
const e = err as { name?: string; data?: { capability?: string } };
|
|
79
|
+
if (e.name === 'PluginCapabilityUnsupported') {
|
|
80
|
+
// e.data?.capability 包含不支持的能力名
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
> **UI / Node 差异总结:**
|
|
88
|
+
> - UI 侧:用 `instanceof CapabilityUnsupportedError`(SDK 保证同一类实例)
|
|
89
|
+
> - Node 侧:用 `err.name === 'PluginCapabilityUnsupported'` + `err.data?.capability`(跨进程,不能用 instanceof)
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 老宿主行为
|
|
94
|
+
|
|
95
|
+
宿主未注入能力清单(老版本)→ `has()` 恒返回 `false` → 门控分支跳过 → 安全降级,无需额外处理。
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 完整示例
|
|
100
|
+
|
|
101
|
+
模板插件的 `capability-gallery` feature 演示了完整的门控写法:
|
|
102
|
+
|
|
103
|
+
- **门控渲染**:`plugins/template-plugin/src/features/capability-gallery/draft-action-ui/app.vue`
|
|
104
|
+
— `infoPanelSupported = clipbus.capabilities.has('infoPanel.open')`,控制 infoPanel 按钮的 v-if
|
|
105
|
+
- **调用兜底**:同文件 `openInfoPanel()` 函数中的 `catch (err instanceof CapabilityUnsupportedError)` 分支
|
package/docs/concepts.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# 架构
|
|
2
|
+
|
|
3
|
+
> 本文档属于 @clipbus/plugin-sdk 开发文档 · 返回[文档地图](./README.md) · capability 真相源见 [API.md](../API.md)
|
|
4
|
+
|
|
5
|
+
## 两个执行上下文
|
|
6
|
+
|
|
7
|
+
一个 v2 插件运行在两个完全隔离的环境:
|
|
8
|
+
|
|
9
|
+
| 环境 | 入口 | 负责 |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| **Node runtime** | `manifest.runtime.nodeEntry` | detector、`resolveAttachment`、`resolveSession`、`runAutoAction`、`messageHandlers` |
|
|
12
|
+
| **WebView UI** | `manifest.<thing>.uiEntry` HTML | 卡片/表单渲染、用户交互、最终结果提交 |
|
|
13
|
+
|
|
14
|
+
两边通过 SDK 暴露的高阶 API 通信:
|
|
15
|
+
|
|
16
|
+
- Runtime 侧用 `host.*`(来自 `@clipbus/plugin-sdk/runtime`)调宿主能力
|
|
17
|
+
- UI 侧用 `clipbus.*`(来自 `@clipbus/plugin-sdk/ui`)调宿主能力
|
|
18
|
+
- UI ↔ Runtime 之间用 `clipbus.runtime.invoke` ↔ `messageHandlers` 桥接
|
|
19
|
+
|
|
20
|
+
宿主与插件之间的传输层(postMessage / Node IPC / 宿主事件 envelope)由 SDK 完全封装,**插件作者不需要关心**底层 wire 形状。
|
|
21
|
+
|
|
22
|
+
## 三类产物
|
|
23
|
+
|
|
24
|
+
| 产物 | 在哪运行 | runtime 入口 | UI 入口 |
|
|
25
|
+
|---|---|---|---|
|
|
26
|
+
| **detector** | 仅 Node runtime | `detect(input, ctx)` | — |
|
|
27
|
+
| **attachment renderer** | Node runtime + WebView | `resolveAttachment(input, ctx)` | `manifest.attachmentRenderers[].uiEntry` |
|
|
28
|
+
| **action(auto-run)** | 仅 Node runtime | `runAutoAction(input, ctx)` | — |
|
|
29
|
+
| **action(draft)** | Node runtime + WebView | `resolveSession(input, ctx)`(可选) | `manifest.actions[].uiEntry` |
|
|
30
|
+
|
|
31
|
+
> 历史上的 `invokeOperation` 入口已被**完全移除**。`definePlugin` 在 setup 阶段就会拦截带 `invokeOperation` 的 handler 并抛错。所有按钮副作用与 draft 提交都通过 UI verb 完成。
|
|
32
|
+
|
|
33
|
+
## 数据流总图
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
┌─────────────────────────────────────┐
|
|
37
|
+
│ Clipbus Host │
|
|
38
|
+
└───────────┬─────────────┬───────────┘
|
|
39
|
+
│ │
|
|
40
|
+
┌────────▼─────────┐ ┌▼─────────────────────────┐
|
|
41
|
+
│ Node Runtime │ │ WebView UI │
|
|
42
|
+
│ │ │ │
|
|
43
|
+
│ definePlugin() │ │ import { clipbus } │
|
|
44
|
+
│ detect │ │ from '@clipbus/plugin- │
|
|
45
|
+
│ resolveAttach… │ │ sdk/ui' │
|
|
46
|
+
│ resolveSession │ │ │
|
|
47
|
+
│ runAutoAction │ │ clipbus.item.current() │
|
|
48
|
+
│ messageHandlers│ │ clipbus.action.* │
|
|
49
|
+
│ │ │ clipbus.attachmentRenderer.│
|
|
50
|
+
│ host.item.* │ │ clipbus.runtime.invoke() │
|
|
51
|
+
│ host.clipboard. │ │ … │
|
|
52
|
+
│ host.action.* │ │ │
|
|
53
|
+
│ … │ │ │
|
|
54
|
+
└────────▲─────────┘ └───────────────────────────┘
|
|
55
|
+
│ │
|
|
56
|
+
└──────────────────────────┘
|
|
57
|
+
clipbus.runtime.invoke
|
|
58
|
+
↔ messageHandlers
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Host → Plugin(推送):**
|
|
62
|
+
- 宿主调 Node runtime 的 handler:`detect` / `resolveAttachment` / `resolveSession` / `runAutoAction`
|
|
63
|
+
- 宿主向 WebView 推送 topic 状态:UI 用 `clipbus.<domain>.on(fn)` 订阅
|
|
64
|
+
- 宿主派发按钮点击:UI 用 `clipbus.action.onHostInvoke(fn)` / `clipbus.attachmentRenderer.onHostInvoke(fn)` 订阅
|
|
65
|
+
|
|
66
|
+
**Plugin → Host(请求):**
|
|
67
|
+
- Runtime 用 `host.*` 调宿主(Node IPC,真 RPC,返回 `Promise<Result>`)
|
|
68
|
+
- UI 用 `clipbus.*` verb 调宿主(WebView postMessage,同样是 `Promise<Result>`)
|
|
69
|
+
|
|
70
|
+
## Host → WebView 状态形状
|
|
71
|
+
|
|
72
|
+
UI 侧拿到的所有宿主状态遵守三种形状之一(capability 完整签名见 [API.md](../API.md)):
|
|
73
|
+
|
|
74
|
+
| 形状 | 接口 | 适用 | 举例 |
|
|
75
|
+
|---|---|---|---|
|
|
76
|
+
| **Topic\<T\>** | `.current(): T \| undefined` + `.on(fn)` | 有快照的持续状态 | `clipbus.item`、`clipbus.theme` |
|
|
77
|
+
| **OptionalTopic\<T\>** | 同上,但 `.current()` 可能 `undefined` | 上下文相关的状态 | `clipbus.item.attachment`、`clipbus.action.draft` |
|
|
78
|
+
| **Stream\<T\>** | `.on(fn)` only | 离散事件流 | `clipbus.attachmentRenderer.onHostInvoke`、`clipbus.action.onHostInvoke` |
|
|
79
|
+
|
|
80
|
+
> **重要:SDK 没有 `clipbus.ready()`**。监听器可以在模块加载时立即注册——宿主会在 bootstrap 推送时唤起它们。`.current()` 在 bootstrap 之前可能返回 `undefined`,用 `?.` / `??` / 早返回防御即可。
|
package/docs/entry.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# SDK 入口与初始化
|
|
2
|
+
|
|
3
|
+
> 本文档属于 @clipbus/plugin-sdk 开发文档 · 返回[文档地图](./README.md) · capability 真相源见 [API.md](../API.md)
|
|
4
|
+
|
|
5
|
+
SDK 暴露两个 subpath。每个入口导出的符号**完全由 codegen 生成**,精确签名见 [API.md](../API.md)。
|
|
6
|
+
|
|
7
|
+
## Runtime 入口
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// CommonJS(推荐与现行 bundler 配合)
|
|
11
|
+
const { definePlugin, host, actionResult, defineMessage } = require('@clipbus/plugin-sdk/runtime');
|
|
12
|
+
|
|
13
|
+
// ESM(types)
|
|
14
|
+
import type { PluginAttachmentRendererHandler, PluginAutoRunActionHandler, PluginDetectorHandler } from '@clipbus/plugin-sdk/runtime';
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
入口导出:
|
|
18
|
+
|
|
19
|
+
- `definePlugin` —— 注册 handler;本地校验 registry 形状
|
|
20
|
+
- `host` —— 直接调宿主能力的单例(`host.<domain>.<verb>(payload)`)。**推荐写法**是从 handler 收到的 `ctx.host` 调用(同一个对象,但写在 handler 内更显式)。
|
|
21
|
+
- `actionResult` —— `runAutoAction` 的返回值构造器
|
|
22
|
+
- `defineMessage` —— UI ↔ Runtime RPC 的类型契约
|
|
23
|
+
|
|
24
|
+
## UI 入口
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { clipbus } from '@clipbus/plugin-sdk/ui';
|
|
28
|
+
import { defineMessage } from '@clipbus/plugin-sdk/ui';
|
|
29
|
+
import { PluginContextError } from '@clipbus/plugin-sdk/ui';
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`clipbus` 的命名空间一览:
|
|
33
|
+
|
|
34
|
+
| Namespace | 形态 | 说明 |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| `clipbus.item` | Topic | `current()`, `on()`, `readAttachment()`,子 OptionalTopic `clipbus.item.attachment` |
|
|
37
|
+
| `clipbus.asset` | Verb | `currentItemImageUrl()`、`pathReferenceImageUrl({index})` —— 按身份取本地图片的 `clipbus-asset://` URL 供 WebView `<img>` 渲染(真实路径不出宿主);Node 产图经 `host.asset.registerImage({path})` |
|
|
38
|
+
| `clipbus.theme` | Topic | `current()`, `on()` |
|
|
39
|
+
| `clipbus.pluginContext` | Topic | `current()`, `on()`,返回 `{mode, pluginID}` |
|
|
40
|
+
| `clipbus.action` | Verb + OptionalTopic | action 上下文专属;`setButtons`、`complete`、`draft`、`onHostInvoke` |
|
|
41
|
+
| `clipbus.attachmentRenderer` | Verb + Stream | attachment 上下文专属;`setButtons`、`onHostInvoke` |
|
|
42
|
+
| `clipbus.window` | Verb | `setHeight`, `autoFit` |
|
|
43
|
+
| `clipbus.clipboard` | Verb | `copyText` |
|
|
44
|
+
| `clipbus.navigation` | Verb | `openUrl`, `revealInFinder`, `openFilePath` |
|
|
45
|
+
| `clipbus.settings` | Verb | `get`, `getAll` |
|
|
46
|
+
| `clipbus.console` | Verb | `log({level, message})` |
|
|
47
|
+
| `clipbus.textInput` | Verb | `stateChanged({isFocused, isComposing})` |
|
|
48
|
+
| `clipbus.runtime` | Verb | `invoke<TResp>({key, payload, timeoutMs?})` |
|
|
49
|
+
|
|
50
|
+
每个方法的精确 payload / response 形状以 [API.md](../API.md) 为准。
|
|
51
|
+
|
|
52
|
+
## 没有 `clipbus.ready()`
|
|
53
|
+
|
|
54
|
+
SDK **不**导出 `clipbus.ready()`。订阅者(`clipbus.<domain>.on(fn)`)可在模块加载时立即注册——监听是 context-neutral 的,宿主推送 bootstrap 时会自动触发回调。
|
|
55
|
+
|
|
56
|
+
如果需要在初始数据到达后做同步动作,常见模式是:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { clipbus } from '@clipbus/plugin-sdk/ui';
|
|
60
|
+
|
|
61
|
+
const item = clipbus.item.current(); // bootstrap 已到时直接拿到
|
|
62
|
+
if (item) {
|
|
63
|
+
init(item);
|
|
64
|
+
} else {
|
|
65
|
+
const unsub = clipbus.item.on(initialItem => {
|
|
66
|
+
init(initialItem);
|
|
67
|
+
unsub();
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Context guards
|
|
73
|
+
|
|
74
|
+
部分 verb 只在特定 WebView 上下文有意义,在错误上下文调用会以 `PluginContextError` reject:
|
|
75
|
+
|
|
76
|
+
| Verb | 必须的上下文 |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `clipbus.action.complete(...)` | action |
|
|
79
|
+
| `clipbus.action.setButtons(...)` | action |
|
|
80
|
+
| `clipbus.attachmentRenderer.setButtons(...)` | attachmentRenderer |
|
|
81
|
+
|
|
82
|
+
捕获方式:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { PluginContextError } from '@clipbus/plugin-sdk/ui';
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
await clipbus.action.complete({ result: { resultKind: 'none' } });
|
|
89
|
+
} catch (e) {
|
|
90
|
+
if ((e as Error).name === 'PluginContextError') {
|
|
91
|
+
// 当前 WebView 不是 action 上下文
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
订阅类 verb(`clipbus.action.draft.on`、`clipbus.attachmentRenderer.onHostInvoke.on` 等)是 context-neutral 的:在错误上下文订阅不会抛错,监听器只是永远不会被触发。
|
|
97
|
+
|
|
98
|
+
## `definePlugin`
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const { definePlugin } = require('@clipbus/plugin-sdk/runtime');
|
|
102
|
+
|
|
103
|
+
module.exports = definePlugin({
|
|
104
|
+
attachmentRenderers: {
|
|
105
|
+
'sample-card': createSampleRenderer(),
|
|
106
|
+
},
|
|
107
|
+
detectors: {
|
|
108
|
+
'sample-detector': createSampleDetector(),
|
|
109
|
+
},
|
|
110
|
+
actions: {
|
|
111
|
+
'sample-apply': createSampleAction(),
|
|
112
|
+
},
|
|
113
|
+
messageHandlers: {
|
|
114
|
+
'sample.applyMetadata': async (req, ctx) => {
|
|
115
|
+
await ctx.host.item.setTags({ tags: req.tags });
|
|
116
|
+
return { ok: true };
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
注册项的 key **必须**与 `manifest.json` 中对应 `id` 完全一致,否则宿主无法寻址。manifest 结构详见 [manifest.md](./manifest.md)。
|
|
123
|
+
|
|
124
|
+
`definePlugin` 同时接受 setup 函数形式:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
module.exports = definePlugin({
|
|
128
|
+
setup() {
|
|
129
|
+
return { attachmentRenderers, detectors, actions, messageHandlers };
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
校验规则(在 setup 阶段 throw):
|
|
135
|
+
|
|
136
|
+
- renderer / action 注册项若包含 `invokeOperation` 立刻抛错(该入口已删除)
|
|
137
|
+
- 其他 registry 形状由 TypeScript 在编译期约束
|
package/docs/faq.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# 常见坑点 Q&A
|
|
2
|
+
|
|
3
|
+
> 本文档属于 @clipbus/plugin-sdk 开发文档 · 返回[文档地图](./README.md) · capability 真相源见 [API.md](../API.md)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
**Q:监听器要等 `clipbus.ready()` 才能注册吗?**
|
|
8
|
+
|
|
9
|
+
A:不要。**SDK 没有 `clipbus.ready()`**。监听器在模块加载时就注册——宿主 bootstrap 推送到达时会自动触发。同步读快照用 `.current()`,可能返回 `undefined`,用 `?.` / `??` 防御即可。
|
|
10
|
+
|
|
11
|
+
**Q:`clipbus.action.draft.update` 在哪?**
|
|
12
|
+
|
|
13
|
+
A:**不存在**。Draft 是只读 OptionalTopic:UI 通过 `current()` / `on()` 读 `initialDraft`,之后自管表单本地状态,最终通过 `clipbus.action.complete(...)` 提交。这是 plugin-api-shrink 的主动设计——宿主不需要知道每次按键。
|
|
14
|
+
|
|
15
|
+
**Q:`clipbus.theme.refresh()` / `clipbus.theme.getThemeSnapshot()` 在哪?**
|
|
16
|
+
|
|
17
|
+
A:**不存在**。`clipbus.theme` 只有 `current()` 和 `on(fn)`。宿主在 WebView 启动时通过 `__CLIPBUS_PLUGIN_THEME__` window global 注入快照,并通过 `clipbus-plugin-theme` host event 推送更新,SDK 自动维护 Topic。
|
|
18
|
+
|
|
19
|
+
**Q:旧版的 `clipbus.action.allocateImageTempPath` 还在吗?**
|
|
20
|
+
|
|
21
|
+
A:**已从 UI 端移除**。临时路径只能由 runtime 端通过 `host.action.allocateImageTempPath({formatHint?})` 申请,再由 runtime 写文件。如果 UI 需要触发,按 [authoring.md](./authoring.md) 中的 draft action 模式:UI 调 `clipbus.runtime.invoke(...)` → 自己的 `messageHandlers` → 在 runtime 里申请路径 + 写文件 + 把路径返回给 UI → UI 用 `clipbus.action.complete({result: {resultKind: 'image', imageTempPath}})` 提交。
|
|
22
|
+
|
|
23
|
+
**Q:旧版的 `ctx.host.capabilities.setTags` 检查在哪?**
|
|
24
|
+
|
|
25
|
+
A:**已移除**。如果调用未授权的 verb,宿主侧 reject,错误会通过 `host.item.setTags(...)` 的 Promise 抛回来。在调用点 `try/catch` 即可。如果需要在执行前做条件分支,直接看 `manifest.json` 的 `permissions` 数组——它就是真相源。详见 [permissions.md](./permissions.md)。
|
|
26
|
+
|
|
27
|
+
**Q:`actionResult.image` 的参数形状到底是什么?**
|
|
28
|
+
|
|
29
|
+
A:第一个参数是 **`imageTempPath: string`**(不是对象)。选项里用 `imageFormatHint`:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
actionResult.image(tempPath, { imageFormatHint: 'png', userMessage: 'Saved' });
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Q:`actionResult.none()` 返回值还有 `text: null` 吗?**
|
|
36
|
+
|
|
37
|
+
A:没有。当前实现:`{ result: { resultKind: 'none' }, userMessage }`。
|
|
38
|
+
|
|
39
|
+
**Q:detector 返回值是数组还是对象?**
|
|
40
|
+
|
|
41
|
+
A:**数组**:`Promise<PluginDetectorArtifact[]>`。直接 `return [artifact1, artifact2]`,不要 `return { artifacts: [...] }`。未命中返回 `[]`。
|
|
42
|
+
|
|
43
|
+
**Q:detector 也能调 `materializeImagePath` 吗?**
|
|
44
|
+
|
|
45
|
+
A:可以。但 detector 的超时较短(3 秒级),大图拷贝可能超时,请酌情使用。
|
|
46
|
+
|
|
47
|
+
**Q:`input.content.payload.text` 还存在吗?**
|
|
48
|
+
|
|
49
|
+
A:不存在。`content` 的字段**平铺**在顶层:`content.text`(text)/`content.{width,height,format,bytes}`(image)/`content.entries`(path_reference)。
|
|
50
|
+
|
|
51
|
+
**Q:`PluginPathEntry` 还存在吗?**
|
|
52
|
+
|
|
53
|
+
A:是。`{kind: 'file' | 'folder', path: string, displayName: string}`,导出自生成的 `data.generated.ts`。被废弃的是无前缀的便利别名 `PathEntry`。
|
|
54
|
+
|
|
55
|
+
**Q:临时目录什么时候清理?**
|
|
56
|
+
|
|
57
|
+
A:invocation 结束后宿主同步删除。宿主启动时还会扫描 1 小时以上的遗留目录兜底清理。
|
|
58
|
+
|
|
59
|
+
**Q:handler key 拼错了会怎样?**
|
|
60
|
+
|
|
61
|
+
A:宿主无法寻址该 capability,调用静默失败或宿主侧报"未注册"。务必保证 `manifest.json` 的 `id` 与 `definePlugin({...})` 中 registry 的 key **完全一致**。详见 [manifest.md](./manifest.md)。
|
|
62
|
+
|
|
63
|
+
**Q:怎么给 capability/host event 加新能力?**
|
|
64
|
+
|
|
65
|
+
A:见 SDK 包内 [SPECIFICATION.md](../SPECIFICATION.md) 第 3 章。简而言之:改 `protocol/plugin/src/catalog.ts` → 跑 `npm run codegen` → 在宿主端实现新方法 → 添测试。SDK 表面由 codegen 自动暴露。
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# 入参形状(ItemContext envelope)
|
|
2
|
+
|
|
3
|
+
> 本文档属于 @clipbus/plugin-sdk 开发文档 · 返回[文档地图](./README.md) · capability 真相源见 [API.md](../API.md)
|
|
4
|
+
|
|
5
|
+
每个 handler 的 `input` 都遵守 **ItemContext envelope**:
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
interface ItemContext {
|
|
9
|
+
item: PluginClipboardItem;
|
|
10
|
+
content: PluginContentEnvelope;
|
|
11
|
+
attachments: PluginAttachmentRef[];
|
|
12
|
+
}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
各 handler 在此之上附加专属字段,详见 [entry.md](./entry.md)。
|
|
16
|
+
|
|
17
|
+
## `PluginContentEnvelope`:三种 kind,字段平铺在顶层
|
|
18
|
+
|
|
19
|
+
### 字段结构
|
|
20
|
+
|
|
21
|
+
**关键:字段直接挂在 `content` 上,没有 `.payload.` 间接层。**
|
|
22
|
+
|
|
23
|
+
| kind | 字段 |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `'text'` | `content.text: string` |
|
|
26
|
+
| `'image'` | `content.width: number`, `content.height: number`, `content.format: string`, `content.bytes: number` |
|
|
27
|
+
| `'path_reference'` | `content.entries: PluginPathEntry[]` |
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
switch (input.content.kind) {
|
|
31
|
+
case 'text':
|
|
32
|
+
console.log(input.content.text);
|
|
33
|
+
break;
|
|
34
|
+
case 'image':
|
|
35
|
+
console.log(`${input.content.width}x${input.content.height} ${input.content.format}`);
|
|
36
|
+
// input.content 不含像素数据,需要时调 host.item.materializeImagePath(见下方「图片懒副本机制」)
|
|
37
|
+
break;
|
|
38
|
+
case 'path_reference':
|
|
39
|
+
for (const entry of input.content.entries) {
|
|
40
|
+
console.log(entry.kind, entry.path, entry.displayName);
|
|
41
|
+
}
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`PluginPathEntry` 形状:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
interface PluginPathEntry {
|
|
50
|
+
kind: 'file' | 'folder';
|
|
51
|
+
path: string;
|
|
52
|
+
displayName: string;
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## `PluginClipboardItem`
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
interface PluginClipboardItem {
|
|
60
|
+
id: string;
|
|
61
|
+
type: string; // 'text' | 'image' | 'path_reference'
|
|
62
|
+
tags: string[];
|
|
63
|
+
sourceAppID: string;
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
> **历史变更:** 旧版 SDK 在 `item` 上有 `text`。新版**已移除**——文本走 `input.content.text`(且仅在 `kind === 'text'` 时存在)。
|
|
68
|
+
|
|
69
|
+
## 图片懒副本机制
|
|
70
|
+
|
|
71
|
+
`input.content`(image 情形)只携带元信息,不带像素。当 detector / action 需要读字节时:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
const { path } = await ctx.host.item.materializeImagePath();
|
|
75
|
+
// path 是宿主复制到临时目录的副本,invocation 结束后宿主自动清理。
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
同一 invocation 多次调用幂等返回同一路径。
|
|
79
|
+
|
|
80
|
+
## Attachment 按需读
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
const { payloadJson } = await ctx.host.item.readAttachment({
|
|
84
|
+
attachmentType: 'plugin.example.sample.card',
|
|
85
|
+
attachmentKey: 'card-1',
|
|
86
|
+
});
|
|
87
|
+
if (payloadJson) {
|
|
88
|
+
const payload = JSON.parse(payloadJson);
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`input.attachments` 给出当前 item 已有的附件引用列表(`{attachmentType, attachmentKey}`)。`readAttachment` 按需拉取真正内容,避免把所有附件塞进入参。
|
|
93
|
+
|
|
94
|
+
## Action 返回图片
|
|
95
|
+
|
|
96
|
+
字节写入由 **runtime 端**完成,**不要**让 UI 写文件。两种推荐写法:
|
|
97
|
+
|
|
98
|
+
**A. Auto-run action — runtime 一气呵成:**
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const { definePlugin, actionResult } = require('@clipbus/plugin-sdk/runtime');
|
|
102
|
+
const fs = require('node:fs/promises');
|
|
103
|
+
|
|
104
|
+
module.exports = definePlugin({
|
|
105
|
+
actions: {
|
|
106
|
+
'generate-image': {
|
|
107
|
+
async runAutoAction(input, ctx) {
|
|
108
|
+
const { path } = await ctx.host.action.allocateImageTempPath({ formatHint: 'png' });
|
|
109
|
+
await fs.writeFile(path, await generateImageBytes(input));
|
|
110
|
+
return actionResult.image(path, { imageFormatHint: 'png' });
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**B. Draft action — UI 触发 runtime 产文件,再由 UI 调 `complete`:** 见 [entry.md](./entry.md) 中的 draft action 示例。
|
|
118
|
+
|
|
119
|
+
## 在 WebView 中展示本地图片(`clipbus-asset://`)
|
|
120
|
+
|
|
121
|
+
上方「图片懒副本机制」和「Action 返回图片」讲的是 **runtime 侧**读写图片字节、产出图片**结果**。如果你要在 renderer 卡片或 draft action 表单里**把一张本地图片显示出来**(`<img>`),用 `clipbus.asset.*`。
|
|
122
|
+
|
|
123
|
+
WebView 沙箱禁止 `file://`,且 SDK **从不**把真实文件路径暴露给 JS。`clipbus.asset.*` 返回一个 `clipbus-asset://` URL——一个会话级、不透明的 token,直接塞进 `<img src>` 即可渲染,宿主在内部把 token 解析回真实文件。
|
|
124
|
+
|
|
125
|
+
| 能力 | 侧 | 入参 | 返回 | 用途 |
|
|
126
|
+
|---|---|---|---|---|
|
|
127
|
+
| `clipbus.asset.currentItemImageUrl()` | UI | `{}` | `{ url?: string }` | 当前 item **自身**的图(item 非 image kind → `url` 缺失) |
|
|
128
|
+
| `clipbus.asset.pathReferenceImageUrl({ index })` | UI | `{ index }` | `{ url?: string }` | 当前 item 第 `index` 个 `path_reference` 条目的图(越界 / 非图片文件 → `url` 缺失) |
|
|
129
|
+
| `host.asset.registerImage({ path })` | runtime | `{ path }` | `{ url: string }` | 把 **Node 产出 / 已有**的本地图注册进当前 session,签发 URL |
|
|
130
|
+
|
|
131
|
+
capability 完整签名见 [API.md](../API.md)。
|
|
132
|
+
|
|
133
|
+
> `currentItemImageUrl` / `pathReferenceImageUrl` 按**身份**取图(当前 item、`path_reference` 下标),UI 不需要、也拿不到真实路径。任意路径 / 动态生成的图**只能**在 runtime 侧经 `host.asset.registerImage` 签发——这样即使 WebView 被 XSS,也无法把任意文件变成可渲染 URL。
|
|
134
|
+
|
|
135
|
+
**A. 展示当前 item 的图(纯 UI):**
|
|
136
|
+
|
|
137
|
+
```vue
|
|
138
|
+
<template>
|
|
139
|
+
<img v-if="url" :src="url" alt="current image" />
|
|
140
|
+
<p v-else>not an image item</p>
|
|
141
|
+
</template>
|
|
142
|
+
|
|
143
|
+
<script setup lang="ts">
|
|
144
|
+
import { onMounted, ref } from 'vue';
|
|
145
|
+
import { clipbus } from '@clipbus/plugin-sdk/ui';
|
|
146
|
+
|
|
147
|
+
const url = ref<string | null>(null);
|
|
148
|
+
onMounted(async () => {
|
|
149
|
+
const { url: u } = await clipbus.asset.currentItemImageUrl();
|
|
150
|
+
url.value = u ?? null; // 非图片 item 时 u 为 undefined
|
|
151
|
+
});
|
|
152
|
+
</script>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`path_reference` 条目同理,换成 `clipbus.asset.pathReferenceImageUrl({ index })`。
|
|
156
|
+
|
|
157
|
+
**B. 展示 Node 产出的图(runtime 产图 → UI 渲染):**
|
|
158
|
+
|
|
159
|
+
图片字节由 **runtime 侧**生成或读取(不要把字节经 postMessage 回传),注册后只把 URL 经 RPC 交给 UI。RPC 消息定义模式详见 [rpc.md](./rpc.md):
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
// shared/contracts.ts
|
|
163
|
+
import { defineMessage } from '@clipbus/plugin-sdk/runtime'; // 或 /ui,类型相同
|
|
164
|
+
export const GenerateImage = defineMessage<Record<string, never>, { url: string }>('generate-image');
|
|
165
|
+
|
|
166
|
+
// runtime —— 生成 / 拿到一个本地文件后注册,拿回 clipbus-asset:// URL
|
|
167
|
+
const { definePlugin } = require('@clipbus/plugin-sdk/runtime');
|
|
168
|
+
const { GenerateImage } = require('../shared/contracts');
|
|
169
|
+
module.exports = definePlugin({
|
|
170
|
+
messageHandlers: Object.fromEntries([
|
|
171
|
+
GenerateImage.handle(async (_req, ctx) => {
|
|
172
|
+
const tmpPath = await writeSomePng(); // 你的产图逻辑,写到临时文件
|
|
173
|
+
const { url } = await ctx.host.asset.registerImage({ path: tmpPath });
|
|
174
|
+
return { url }; // 只回传 URL,不回传字节
|
|
175
|
+
}),
|
|
176
|
+
]),
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// UI —— 调 runtime 拿 URL,直接渲染
|
|
180
|
+
import { GenerateImage } from '../shared/contracts';
|
|
181
|
+
const { url } = await GenerateImage.invoke({});
|
|
182
|
+
imageUrl.value = url; // <img :src="imageUrl">
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
> **token 生命周期:** URL 绑定当前 session owner,invocation / WebView 释放时一并失效,且跨 owner 隔离——不要持久化或跨会话复用。
|
|
186
|
+
> **何时用哪个:** 显示「当前剪贴板项的图」用 `currentItemImageUrl`;显示「`path_reference` 里的某张图」用 `pathReferenceImageUrl`;显示「插件自己算出来 / 从别处读来的图」用 runtime `registerImage`。
|