@syllm/brickly-sdk 0.1.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/README.md ADDED
@@ -0,0 +1,402 @@
1
+ # @syllm/brickly-sdk
2
+
3
+ Brickly Brick Node runtime 官方 SDK。用 1 行代码替代 ~150 行手写的 BPP(Brickly Plugin Protocol)样板。
4
+
5
+ 零运行时依赖,只用 Node 内置 `readline` / `process` / `events`。
6
+
7
+ ---
8
+
9
+ ## 快速上手
10
+
11
+ ```js
12
+ const { BricklyRuntime } = require('@syllm/brickly-sdk')
13
+
14
+ const brick = new BricklyRuntime({ brickId: 'com.example.foo' })
15
+
16
+ brick.onCommand('spawn-pet', async (ctx, input) => {
17
+ const win = await ctx.ui.createBrowserWindow('pet.html', {
18
+ width: 200,
19
+ height: 200,
20
+ frame: false,
21
+ transparent: true,
22
+ alwaysOnTop: true
23
+ })
24
+ win.on('closed', () => ctx.events.publish('pet.closed', { id: win.id }))
25
+ return { windowId: win.id }
26
+ })
27
+
28
+ brick.start()
29
+ ```
30
+
31
+ SDK 自动完成:
32
+
33
+ - `host.hello` → `runtime.ready` 握手
34
+ - `runtime.ping` → `runtime.pong` 心跳
35
+ - `host.*` 请求 id 分配与 `host.result` / `host.error` 路由
36
+ - `command.invoke` 分发与 `command.result` / `command.error` 序列化
37
+ - `runtime.shutdown` → `onShutdown` 钩子 → `runtime.bye` → 退出
38
+
39
+ ---
40
+
41
+ ## 核心 API
42
+
43
+ ### `BricklyRuntime`
44
+
45
+ | 方法 | 作用 |
46
+ | -------------------------------------- | ----------------------------------------------------------- |
47
+ | `onCommand(id, handler)` | 注册命令处理器 |
48
+ | `onReady(fn)` | runtime.ready 之后立即触发(适合 host-start service 或实例初始化) |
49
+ | `onShutdown(fn)` | runtime.shutdown 时触发,返回后 SDK 自动发 runtime.bye 退出 |
50
+ | `ui.createBrowserWindow(url, options)` | 创建子窗口,返回 `WindowHandle` |
51
+ | `ui.listWindows()` | 列出本 Brick 持有的窗口 |
52
+ | `events.on(event, fn)` | 订阅事件总线(含 `window.*` 系列) |
53
+ | `events.publish(event, payload)` | 发布事件 |
54
+ | `invoke(brickId, commandId, input?, options?)` | 在 command 作用域内发起 child 跨 Brick 调用 |
55
+ | `invokeRoot(brickId, commandId, input?, options?)` | command 外发起 root 跨 Brick 调用 |
56
+ | `openSession(brickId, options?)` | 打开跨 Brick 会话;后续 `session.invoke` 需在 command 作用域内调用 |
57
+ | `log(...parts)` | 写 stderr 日志;宿主日志中心自动关联 Brick 身份 |
58
+ | `start()` | 启动 stdin 循环 |
59
+
60
+ ### `CommandContext`(handler 第一个参数)
61
+
62
+ | 字段 | 作用 |
63
+ | --------------------------- | ------------------------------------- |
64
+ | `requestId` / `commandId` | 当前请求与命令 id |
65
+ | `invocation` | 宿主注入的调用来源;热键触发时 `source === 'hotkey'`,可携带热键 Profile 选择 |
66
+ | `progress(value, message?)` | 进度(0~1) |
67
+ | `chunk(chunk, name?)` | 向具名输出追加片段 |
68
+ | `output(name, value)` | 一次性覆盖具名输出 |
69
+ | `onCancel(fn)` | 注册取消回调 |
70
+ | `isCancelled()` | 协作式取消轮询 |
71
+ | `invoke(brickId, commandId, input?, options?)` | 跨 Brick 调用命令,自动携带当前 `parentRequestId`,支持 `options.profileId` |
72
+ | `invokeStream(brickId, commandId, input?, options?)` | 流式跨 Brick 调用命令,自动携带当前 `parentRequestId` |
73
+ | `openSession(brickId, options?)` | 打开跨 Brick 会话,支持 `options.profileId`;`session.invoke` 自动携带当前 `parentRequestId` |
74
+ | `ui` / `events` / `platform` | 与 `brick.ui` / `brick.events` / `brick.platform` 同源 |
75
+
76
+ ---
77
+
78
+ ## 日志约定
79
+
80
+ `brick.log(...)` / `transport.log(...)` 只写入 stderr,stdout 永远只写 BPP 协议消息。SDK 不会把 `brickId` 拼进日志正文;宿主日志中心会在采集 stderr 时用结构化字段记录来源、Brick id、stream 与作用域名称。插件代码不要手动输出 `[brickId]` 前缀,避免日志中心和 SQLite 中出现重复信息。
81
+
82
+ ---
83
+
84
+ ## 跨 Brick 调用
85
+
86
+ 普通 Brick 在 command handler 内调用其它 Brick 命令时只需要 `ctx.invoke`。SDK 会自动携带当前请求的 `parentRequestId`,宿主会把子调用挂到同一 invocation graph 下,并自动启动、复用和回收目标 Brick 实例。目标 Brick 需要配置时,可传入目标 Brick 的 Profile ID;不传则使用目标 Brick 默认 Profile。
87
+
88
+ ```ts
89
+ const result = await ctx.invoke(
90
+ 'com.brickly.openai',
91
+ 'chat',
92
+ { prompt: 'hello' },
93
+ { profileId: 'work' }
94
+ )
95
+ ```
96
+
97
+ 调用方 manifest 必须在 `dependencies` 中声明目标 Brick 和允许调用的命令:
98
+
99
+ ```json
100
+ "dependencies": {
101
+ "com.brickly.openai": {
102
+ "commands": ["chat"]
103
+ }
104
+ }
105
+ ```
106
+
107
+ `commands: ["*"]` 表示允许调用目标 Brick 的全部可见命令;隐藏命令必须显式写命令 id。
108
+
109
+ 热键触发 command 时,用户可以在热键管理页为依赖 Brick 选择 Profile。SDK 会在 `ctx.invoke(targetBrickId, ...)` 未显式传 `options.profileId` 时自动使用这份选择;代码里显式传入 `profileId` 时仍以代码为准。
110
+
111
+ 如果需要在 command 外主动创建顶级调用,使用显式 root API:
112
+
113
+ ```ts
114
+ const result = await brick.invokeRoot(
115
+ 'com.brickly.openai',
116
+ 'chat',
117
+ { prompt: 'hello' },
118
+ { profileId: 'work' }
119
+ )
120
+ ```
121
+
122
+ ## 平台 System API
123
+
124
+ `brick.platform.system.*` 与 handler 内的 `ctx.platform.system.*` 通过 BPP `host.platform.system.*` 调用宿主系统能力:
125
+
126
+ ```ts
127
+ brick.onCommand('show-app-info', async (ctx) => {
128
+ return {
129
+ appName: await ctx.platform.system.getAppName(),
130
+ appVersion: await ctx.platform.system.getAppVersion(),
131
+ userData: await ctx.platform.system.getPath('userData'),
132
+ isMacOS: await ctx.platform.system.isMacOS()
133
+ }
134
+ })
135
+ ```
136
+
137
+ 当前方法包括 `showNotification`、`shellOpenPath`、`shellTrashItem`、`shellShowItemInFolder`、`shellOpenExternal`、`shellBeep`、`getNativeId`、`getAppName`、`getAppVersion`、`getPath`、`getFileIcon`、`readCurrentFolderPath`、`readCurrentBrowserUrl`、`isDev`、`isMacOS`、`isWindows`、`isLinux`。
138
+
139
+ runtime 侧仍按 manifest 权限校验:通知需要 `os.notification`;Shell 类能力需要 `os.exec`;应用信息、路径、设备 ID、平台判断和当前文件夹路径读取需要 `os.env`;文件图标需要 `fs.read`。`readCurrentFolderPath()` 在 macOS Finder 与 Windows Explorer 前台窗口可用;当前没有可读取的前台文件管理器文件夹时会抛 `CURRENT_FOLDER_UNAVAILABLE`。`readCurrentBrowserUrl()` 当前预留,会返回 `UNSUPPORTED_PLATFORM`。
140
+
141
+ ## 跨 Brick 会话
142
+
143
+ 当目标 Brick 实例内部有状态,需要在多次命令调用之间保留上下文时,在 command handler 内使用 `ctx.openSession`。同一个 session 的后续 `invoke` 会落到同一个目标 Brick 实例;每次 `session.invoke` 都会自动携带当前 `parentRequestId`。调用 `close()`、调用方 Brick 实例退出或宿主回收调用方时,会话会结束。
144
+
145
+ ```ts
146
+ const session = await ctx.openSession('com.brickly.openai', { profileId: 'work' })
147
+
148
+ try {
149
+ await session.invoke('start-thread', { title: 'Draft' })
150
+ const reply = await session.invoke('chat', { prompt: '继续刚才的话题' })
151
+ return reply
152
+ } finally {
153
+ await session.close()
154
+ }
155
+ ```
156
+
157
+ `profileId` 仍然是目标 Brick 的 Profile ID;不传则使用目标 Brick 默认 Profile,或使用热键调用上下文中的依赖 Profile 选择。`session.invoke` 也会按调用方 manifest 的 `dependencies[target].commands` 重新校验命令。
158
+
159
+ ## 依赖调用类型生成
160
+
161
+ 如果当前 Brick 在 `manifest.dependencies` 声明了依赖 Brick,可以生成一组开发期 JS 包装函数和 `.d.ts`,让调用依赖命令时获得命令名、入参、返回值和说明提示。
162
+
163
+ ```json
164
+ {
165
+ "dependencies": {
166
+ "com.brickly.text-toolkit": {
167
+ "commands": ["case", "stats"]
168
+ }
169
+ }
170
+ }
171
+ ```
172
+
173
+ 在当前 Brick 根目录执行:
174
+
175
+ ```bash
176
+ brickly-typegen
177
+ ```
178
+
179
+ 默认输出到 `runtime/node/_generated/deps/`。生成物包含:
180
+
181
+ - `index.js` / `index.d.ts`:统一导出所有依赖命名空间,并扩展 SDK `CommandMap`。
182
+ - `<brick-id>.js` / `<brick-id>.d.ts`:每个依赖 Brick 一个文件,函数注释来自目标 manifest 的 `commands[].description` 与 socket 描述。
183
+
184
+ 普通 JS runtime 可以直接使用生成的 wrapper:
185
+
186
+ ```js
187
+ const { BricklyRuntime } = require('./_sdk')
188
+ const { textToolkit } = require('./_generated/deps')
189
+
190
+ const brick = new BricklyRuntime({ brickId: 'com.example.composite' })
191
+
192
+ brick.onCommand('run', async (ctx, input) => {
193
+ return textToolkit.caseCommand(ctx, {
194
+ text: String(input?.text || ''),
195
+ mode: 'upper'
196
+ })
197
+ })
198
+ ```
199
+
200
+ 也可以绑定一次上下文,减少重复传参:
201
+
202
+ ```js
203
+ const text = textToolkit.bind(ctx)
204
+ const changed = await text.caseCommand({ text: 'hello', mode: 'upper' })
205
+ ```
206
+
207
+ TypeScript 项目只要把生成的 `index.d.ts` 纳入 `tsconfig.include`,裸写 `ctx.invoke('com.brickly.text-toolkit', 'case', ...)` 也会按生成的 `CommandMap` 自动推导输入和返回类型。
208
+
209
+ ---
210
+
211
+ ## WindowHandle(107 个方法)
212
+
213
+ `ui.createBrowserWindow` 返回的 `WindowHandle` 完整封装了宿主白名单的 107 个方法。详细签名/参数/返回值见 `@d:\ai-bricks\specs\window-api.md`。
214
+
215
+ ### 1. 几何 / 位置(17)
216
+
217
+ ```ts
218
+ win.setBounds({ x, y, width, height }) // 全部字段可选
219
+ win.getBounds() //=> { x, y, width, height }
220
+ win.setContentBounds({ ... }) win.getContentBounds()
221
+ win.getNormalBounds() // 非最大化/最小化时的"常态"
222
+ win.setPosition(x, y) win.getPosition() //=> [x, y]
223
+ win.setSize(w, h) win.getSize()
224
+ win.setContentSize(w, h) win.getContentSize()
225
+ win.setMinimumSize(w, h) win.getMinimumSize()
226
+ win.setMaximumSize(w, h) win.getMaximumSize()
227
+ win.setAspectRatio(16/9) win.setAspectRatio(16/9, { width: 40, height: 50 })
228
+ win.center()
229
+ ```
230
+
231
+ ### 2. 状态切换(11)
232
+
233
+ ```ts
234
+ win.minimize() win.maximize() win.unmaximize() win.restore()
235
+ win.hide() win.show() win.showInactive()
236
+ win.focus() win.blur()
237
+ win.setFullScreen(true|false)
238
+ win.destroy() // 强制销毁,不触发 close 事件
239
+ ```
240
+
241
+ ### 3. 状态查询(18,全部返回 boolean)
242
+
243
+ ```ts
244
+ // 可见性 / 焦点 / 状态
245
+ win.isVisible() isFocused() isMinimized() isMaximized()
246
+ isFullScreen() isNormal() isModal() isDestroyed()
247
+ // 能力开关
248
+ win.isResizable() isMovable() isFocusable()
249
+ isMinimizable() isMaximizable() isClosable() isFullScreenable()
250
+ isEnabled() isKiosk() hasShadow()
251
+ ```
252
+
253
+ ### 4. 视觉属性(10)
254
+
255
+ ```ts
256
+ win.setOpacity(0.85) win.getOpacity()
257
+ win.setBackgroundColor('#1e293b')
258
+ win.setTitle('My Window') win.getTitle()
259
+ win.setHasShadow(true) win.invalidateShadow() // macOS
260
+ win.flashFrame(true)
261
+ win.setProgressBar(0.4) win.setProgressBar(0.8, { mode: 'indeterminate' })
262
+ win.moveTop()
263
+ ```
264
+
265
+ ### 5. 层叠 / 鼠标 / 任务栏(7)
266
+
267
+ ```ts
268
+ win.setAlwaysOnTop(true) win.isAlwaysOnTop()
269
+ win.setAlwaysOnTop(true, 'screen-saver') // 带 level
270
+ win.setIgnoreMouseEvents(true) win.setIgnoreMouseEvents(true, { forward: true })
271
+ win.setSkipTaskbar(true)
272
+ win.setVisibleOnAllWorkspaces(true) win.isVisibleOnAllWorkspaces()
273
+ win.moveAbove(mediaSourceId)
274
+ ```
275
+
276
+ ### 6. 能力开关 setter(9)
277
+
278
+ ```ts
279
+ win.setResizable(false) win.setMovable(false) win.setFocusable(false)
280
+ win.setMinimizable(false) win.setMaximizable(false) win.setClosable(false)
281
+ win.setFullScreenable(false) win.setEnabled(false) win.setKiosk(true)
282
+ ```
283
+
284
+ ### 7. 菜单栏(5)
285
+
286
+ ```ts
287
+ win.setMenuBarVisibility(false) win.isMenuBarVisible()
288
+ win.setAutoHideMenuBar(true) win.isMenuBarAutoHide()
289
+ win.removeMenu()
290
+ ```
291
+
292
+ ### 8. macOS 文档窗口(4)
293
+
294
+ ```ts
295
+ win.setRepresentedFilename('/path/to/doc.txt') win.getRepresentedFilename()
296
+ win.setDocumentEdited(true) win.isDocumentEdited()
297
+ ```
298
+
299
+ ### 9. 内容加载(3)
300
+
301
+ ```ts
302
+ win.loadURL('https://example.com') win.loadURL(url, { httpReferrer, userAgent })
303
+ win.loadFile('relative/path.html') win.loadFile(p, { query, hash })
304
+ win.reload()
305
+ ```
306
+
307
+ ### 10. WebContents 子对象(22)
308
+
309
+ 仿 Electron 原生写法:
310
+
311
+ ```ts
312
+ // DevTools
313
+ win.webContents.openDevTools({ mode: 'detach' })
314
+ win.webContents.closeDevTools()
315
+ win.webContents.toggleDevTools()
316
+ win.webContents.isDevToolsOpened()
317
+
318
+ // 跨进程消息(宿主 → 子窗口 ipcRenderer 'channel')
319
+ win.webContents.send('lab:result', { ok: true, value: 42 })
320
+
321
+ // 远程执行 JS
322
+ const title = await win.webContents.executeJavaScript('document.title')
323
+
324
+ // 导航
325
+ win.webContents.goBack() goForward() canGoBack() canGoForward()
326
+ win.webContents.getURL() getTitle()
327
+
328
+ // 缩放
329
+ win.webContents.setZoomFactor(1.25) getZoomFactor()
330
+ win.webContents.setZoomLevel(1) getZoomLevel()
331
+
332
+ // 编辑命令(作用于聚焦元素)
333
+ win.webContents.copy() paste() cut() selectAll() undo() redo()
334
+ ```
335
+
336
+ ### 关闭与事件
337
+
338
+ ```ts
339
+ win.close() // 走 host.ui.closeWindow,可被 beforeunload 拦截
340
+ win.id // BrowserWindow.webContents id
341
+ win.on('closed', () => ...)
342
+ win.on('message', ({ channel, args }) => ...) // 子窗口 brickly.sendToParent 推上来
343
+ win.on('focus' | 'blur' | 'minimize' | 'maximize' | 'resize' | 'move', ...)
344
+ ```
345
+
346
+ ### 网络检查
347
+
348
+ `createBrowserWindow` 可通过 `network` 选项启用平台侧 CDP Network 监听。runtime 仍是普通 Node 进程,不需要直接依赖 Electron:
349
+
350
+ ```js
351
+ const win = await ctx.ui.createBrowserWindow('ui/browser.html', {
352
+ webPreferences: { webviewTag: true },
353
+ network: {
354
+ enabled: true,
355
+ captureBody: true,
356
+ maxBodyChars: 200000,
357
+ targets: 'all'
358
+ }
359
+ })
360
+
361
+ win.onNetwork((event) => {
362
+ if (event.stage === 'response-body') {
363
+ ctx.chunk(event, 'requests')
364
+ }
365
+ })
366
+ ```
367
+
368
+ `targets` 可选 `window`、`webview`、`all`。`captureBody` 会尝试读取响应体;下载流、特殊协议、浏览器内部拦截或 CDP 未缓存的大资源可能产生 `response-body-error`。
369
+
370
+ ---
371
+
372
+ ## 协议与版本对齐
373
+
374
+ - **白名单真相源**:`@d:\ai-bricks\specs\bpp.schema.json` 的 `BrickWindowMethod` enum。
375
+ - **跨语言协议规范**:`@d:\ai-bricks\specs\window-api.md`(写 Go / Python SDK 时照此抄)。
376
+ - 当前协议版本:`0.1.0`,窗口 API 子集版本 `v0.2`(41 → 107 个方法,向后兼容)。
377
+ - **Go SDK 对照实现**:[`@d:\ai-bricks\Brickly\packages\brickly-sdk-go`](../brickly-sdk-go),API 表面与本包一一对应。
378
+ - `src/protocol.ts` 是 schema 的 TS 镜像,由 `@d:\ai-bricks\Brickly\scripts\check-bpp-schema.mjs` 强制保持同步。
379
+
380
+ ---
381
+
382
+ ## 构建与同步到 Brick
383
+
384
+ ```bash
385
+ # 在 Brickly/ 目录下
386
+ npm run sdk:build
387
+ npm run sdk:sync:demo-window-lab # Window API 实验室
388
+ ```
389
+
390
+ Brick runtime 通过 `require('./_sdk')` 加载,保持 Brick 自包含、可分发。
391
+
392
+ ---
393
+
394
+ ## 测试
395
+
396
+ ```bash
397
+ npm run sdk:test # tsx --test tests/runtime.test.ts tests/api.test.ts
398
+ ```
399
+
400
+ 参考实现:
401
+
402
+ - 窗口 API 实验室(覆盖 80+ 个方法的可视化测试):`@d:\ai-bricks\bricks\com.brickly.demo-window-lab\`