@syllm/brickly-sdk
v0.1.1
Published
Brickly Brick Node runtime 官方 SDK
Readme
@syllm/brickly-sdk
Brickly Brick Node runtime 官方 SDK。用 1 行代码替代 ~150 行手写的 BPP(Brickly Plugin Protocol)样板。
零运行时依赖,只用 Node 内置 readline / process / events。
快速上手
const { BricklyRuntime } = require('@syllm/brickly-sdk')
const brick = new BricklyRuntime({ brickId: 'com.example.foo' })
brick.onCommand('spawn-pet', async (ctx, input) => {
const win = await ctx.ui.createBrowserWindow('pet.html', {
width: 200,
height: 200,
frame: false,
transparent: true,
alwaysOnTop: true
})
win.on('closed', () => ctx.events.publish('pet.closed', { id: win.id }))
return { windowId: win.id }
})
brick.start()SDK 自动完成:
host.hello→runtime.ready握手runtime.ping→runtime.pong心跳host.*请求 id 分配与host.result/host.error路由command.invoke分发与command.result/command.error序列化runtime.shutdown→onShutdown钩子 →runtime.bye→ 退出
核心 API
BricklyRuntime
| 方法 | 作用 |
| -------------------------------------- | ----------------------------------------------------------- |
| onCommand(id, handler) | 注册命令处理器 |
| onReady(fn) | runtime.ready 之后立即触发(适合 host-start service 或实例初始化) |
| onShutdown(fn) | runtime.shutdown 时触发,返回后 SDK 自动发 runtime.bye 退出 |
| ui.createBrowserWindow(url, options) | 创建子窗口,返回 WindowHandle |
| ui.listWindows() | 列出本 Brick 持有的窗口 |
| events.on(event, fn) | 订阅事件总线(含 window.* 系列) |
| events.publish(event, payload) | 发布事件 |
| invoke(brickId, commandId, input?, options?) | 在 command 作用域内发起 child 跨 Brick 调用 |
| invokeRoot(brickId, commandId, input?, options?) | command 外发起 root 跨 Brick 调用 |
| openSession(brickId, options?) | 打开跨 Brick 会话;后续 session.invoke 需在 command 作用域内调用 |
| log(...parts) | 写 stderr 日志;宿主日志中心自动关联 Brick 身份 |
| start() | 启动 stdin 循环 |
CommandContext(handler 第一个参数)
| 字段 | 作用 |
| --------------------------- | ------------------------------------- |
| requestId / commandId | 当前请求与命令 id |
| invocation | 宿主注入的调用来源;热键触发时 source === 'hotkey',可携带热键 Profile 选择 |
| progress(value, message?) | 进度(0~1) |
| chunk(chunk, name?) | 向具名输出追加片段 |
| output(name, value) | 一次性覆盖具名输出 |
| onCancel(fn) | 注册取消回调 |
| isCancelled() | 协作式取消轮询 |
| invoke(brickId, commandId, input?, options?) | 跨 Brick 调用命令,自动携带当前 parentRequestId,支持 options.profileId |
| invokeStream(brickId, commandId, input?, options?) | 流式跨 Brick 调用命令,自动携带当前 parentRequestId |
| openSession(brickId, options?) | 打开跨 Brick 会话,支持 options.profileId;session.invoke 自动携带当前 parentRequestId |
| ui / events / platform | 与 brick.ui / brick.events / brick.platform 同源 |
日志约定
brick.log(...) / transport.log(...) 只写入 stderr,stdout 永远只写 BPP 协议消息。SDK 不会把 brickId 拼进日志正文;宿主日志中心会在采集 stderr 时用结构化字段记录来源、Brick id、stream 与作用域名称。插件代码不要手动输出 [brickId] 前缀,避免日志中心和 SQLite 中出现重复信息。
跨 Brick 调用
普通 Brick 在 command handler 内调用其它 Brick 命令时只需要 ctx.invoke。SDK 会自动携带当前请求的 parentRequestId,宿主会把子调用挂到同一 invocation graph 下,并自动启动、复用和回收目标 Brick 实例。目标 Brick 需要配置时,可传入目标 Brick 的 Profile ID;不传则使用目标 Brick 默认 Profile。
const result = await ctx.invoke(
'com.brickly.openai',
'chat',
{ prompt: 'hello' },
{ profileId: 'work' }
)调用方 manifest 必须在 dependencies 中声明目标 Brick 和允许调用的命令:
"dependencies": {
"com.brickly.openai": {
"commands": ["chat"]
}
}commands: ["*"] 表示允许调用目标 Brick 的全部可见命令;隐藏命令必须显式写命令 id。
热键触发 command 时,用户可以在热键管理页为依赖 Brick 选择 Profile。SDK 会在 ctx.invoke(targetBrickId, ...) 未显式传 options.profileId 时自动使用这份选择;代码里显式传入 profileId 时仍以代码为准。
如果需要在 command 外主动创建顶级调用,使用显式 root API:
const result = await brick.invokeRoot(
'com.brickly.openai',
'chat',
{ prompt: 'hello' },
{ profileId: 'work' }
)平台 System API
brick.platform.system.* 与 handler 内的 ctx.platform.system.* 通过 BPP host.platform.system.* 调用宿主系统能力:
brick.onCommand('show-app-info', async (ctx) => {
return {
appName: await ctx.platform.system.getAppName(),
appVersion: await ctx.platform.system.getAppVersion(),
userData: await ctx.platform.system.getPath('userData'),
isMacOS: await ctx.platform.system.isMacOS()
}
})当前方法包括 showNotification、shellOpenPath、shellTrashItem、shellShowItemInFolder、shellOpenExternal、shellBeep、getNativeId、getAppName、getAppVersion、getPath、getFileIcon、readCurrentFolderPath、readCurrentBrowserUrl、isDev、isMacOS、isWindows、isLinux。
runtime 侧仍按 manifest 权限校验:通知需要 os.notification;Shell 类能力需要 os.exec;应用信息、路径、设备 ID、平台判断和当前文件夹路径读取需要 os.env;文件图标需要 fs.read。readCurrentFolderPath() 在 macOS Finder 与 Windows Explorer 前台窗口可用;当前没有可读取的前台文件管理器文件夹时会抛 CURRENT_FOLDER_UNAVAILABLE。readCurrentBrowserUrl() 当前预留,会返回 UNSUPPORTED_PLATFORM。
跨 Brick 会话
当目标 Brick 实例内部有状态,需要在多次命令调用之间保留上下文时,在 command handler 内使用 ctx.openSession。同一个 session 的后续 invoke 会落到同一个目标 Brick 实例;每次 session.invoke 都会自动携带当前 parentRequestId。调用 close()、调用方 Brick 实例退出或宿主回收调用方时,会话会结束。
const session = await ctx.openSession('com.brickly.openai', { profileId: 'work' })
try {
await session.invoke('start-thread', { title: 'Draft' })
const reply = await session.invoke('chat', { prompt: '继续刚才的话题' })
return reply
} finally {
await session.close()
}profileId 仍然是目标 Brick 的 Profile ID;不传则使用目标 Brick 默认 Profile,或使用热键调用上下文中的依赖 Profile 选择。session.invoke 也会按调用方 manifest 的 dependencies[target].commands 重新校验命令。
依赖调用类型生成
如果当前 Brick 在 manifest.dependencies 声明了依赖 Brick,可以生成一组开发期 JS 包装函数和 .d.ts,让调用依赖命令时获得命令名、入参、返回值和说明提示。
{
"dependencies": {
"com.brickly.text-toolkit": {
"commands": ["case", "stats"]
}
}
}在当前 Brick 根目录执行:
brickly-typegen默认输出到 runtime/node/_generated/deps/。生成物包含:
index.js/index.d.ts:统一导出所有依赖命名空间,并扩展 SDKCommandMap。<brick-id>.js/<brick-id>.d.ts:每个依赖 Brick 一个文件,函数注释来自目标 manifest 的commands[].description与 socket 描述。
普通 JS runtime 可以直接使用生成的 wrapper:
const { BricklyRuntime } = require('@syllm/brickly-sdk')
const { textToolkit } = require('./_generated/deps')
const brick = new BricklyRuntime({ brickId: 'com.example.composite' })
brick.onCommand('run', async (ctx, input) => {
return textToolkit.caseCommand(ctx, {
text: String(input?.text || ''),
mode: 'upper'
})
})也可以绑定一次上下文,减少重复传参:
const text = textToolkit.bind(ctx)
const changed = await text.caseCommand({ text: 'hello', mode: 'upper' })TypeScript 项目只要把生成的 index.d.ts 纳入 tsconfig.include,裸写 ctx.invoke('com.brickly.text-toolkit', 'case', ...) 也会按生成的 CommandMap 自动推导输入和返回类型。
WindowHandle(107 个方法)
ui.createBrowserWindow 返回的 WindowHandle 完整封装了宿主白名单的 107 个方法。详细签名/参数/返回值见 @d:\ai-bricks\specs\window-api.md。
1. 几何 / 位置(17)
win.setBounds({ x, y, width, height }) // 全部字段可选
win.getBounds() //=> { x, y, width, height }
win.setContentBounds({ ... }) win.getContentBounds()
win.getNormalBounds() // 非最大化/最小化时的"常态"
win.setPosition(x, y) win.getPosition() //=> [x, y]
win.setSize(w, h) win.getSize()
win.setContentSize(w, h) win.getContentSize()
win.setMinimumSize(w, h) win.getMinimumSize()
win.setMaximumSize(w, h) win.getMaximumSize()
win.setAspectRatio(16/9) win.setAspectRatio(16/9, { width: 40, height: 50 })
win.center()2. 状态切换(11)
win.minimize() win.maximize() win.unmaximize() win.restore()
win.hide() win.show() win.showInactive()
win.focus() win.blur()
win.setFullScreen(true|false)
win.destroy() // 强制销毁,不触发 close 事件3. 状态查询(18,全部返回 boolean)
// 可见性 / 焦点 / 状态
win.isVisible() isFocused() isMinimized() isMaximized()
isFullScreen() isNormal() isModal() isDestroyed()
// 能力开关
win.isResizable() isMovable() isFocusable()
isMinimizable() isMaximizable() isClosable() isFullScreenable()
isEnabled() isKiosk() hasShadow()4. 视觉属性(10)
win.setOpacity(0.85) win.getOpacity()
win.setBackgroundColor('#1e293b')
win.setTitle('My Window') win.getTitle()
win.setHasShadow(true) win.invalidateShadow() // macOS
win.flashFrame(true)
win.setProgressBar(0.4) win.setProgressBar(0.8, { mode: 'indeterminate' })
win.moveTop()5. 层叠 / 鼠标 / 任务栏(7)
win.setAlwaysOnTop(true) win.isAlwaysOnTop()
win.setAlwaysOnTop(true, 'screen-saver') // 带 level
win.setIgnoreMouseEvents(true) win.setIgnoreMouseEvents(true, { forward: true })
win.setSkipTaskbar(true)
win.setVisibleOnAllWorkspaces(true) win.isVisibleOnAllWorkspaces()
win.moveAbove(mediaSourceId)6. 能力开关 setter(9)
win.setResizable(false) win.setMovable(false) win.setFocusable(false)
win.setMinimizable(false) win.setMaximizable(false) win.setClosable(false)
win.setFullScreenable(false) win.setEnabled(false) win.setKiosk(true)7. 菜单栏(5)
win.setMenuBarVisibility(false) win.isMenuBarVisible()
win.setAutoHideMenuBar(true) win.isMenuBarAutoHide()
win.removeMenu()8. macOS 文档窗口(4)
win.setRepresentedFilename('/path/to/doc.txt') win.getRepresentedFilename()
win.setDocumentEdited(true) win.isDocumentEdited()9. 内容加载(3)
win.loadURL('https://example.com') win.loadURL(url, { httpReferrer, userAgent })
win.loadFile('relative/path.html') win.loadFile(p, { query, hash })
win.reload()10. WebContents 子对象(22)
仿 Electron 原生写法:
// DevTools
win.webContents.openDevTools({ mode: 'detach' })
win.webContents.closeDevTools()
win.webContents.toggleDevTools()
win.webContents.isDevToolsOpened()
// 跨进程消息(宿主 → 子窗口 ipcRenderer 'channel')
win.webContents.send('lab:result', { ok: true, value: 42 })
// 远程执行 JS
const title = await win.webContents.executeJavaScript('document.title')
// 导航
win.webContents.goBack() goForward() canGoBack() canGoForward()
win.webContents.getURL() getTitle()
// 缩放
win.webContents.setZoomFactor(1.25) getZoomFactor()
win.webContents.setZoomLevel(1) getZoomLevel()
// 编辑命令(作用于聚焦元素)
win.webContents.copy() paste() cut() selectAll() undo() redo()关闭与事件
win.close() // 走 host.ui.closeWindow,可被 beforeunload 拦截
win.id // BrowserWindow.webContents id
win.on('closed', () => ...)
win.on('message', ({ channel, args }) => ...) // 子窗口 brickly.sendToParent 推上来
win.on('focus' | 'blur' | 'minimize' | 'maximize' | 'resize' | 'move', ...)网络检查
createBrowserWindow 可通过 network 选项启用平台侧 CDP Network 监听。runtime 仍是普通 Node 进程,不需要直接依赖 Electron:
const win = await ctx.ui.createBrowserWindow('ui/browser.html', {
webPreferences: { webviewTag: true },
network: {
enabled: true,
captureBody: true,
maxBodyChars: 200000,
targets: 'all'
}
})
win.onNetwork((event) => {
if (event.stage === 'response-body') {
ctx.chunk(event, 'requests')
}
})targets 可选 window、webview、all。captureBody 会尝试读取响应体;下载流、特殊协议、浏览器内部拦截或 CDP 未缓存的大资源可能产生 response-body-error。
协议与版本对齐
- 白名单真相源:
@d:\ai-bricks\specs\bpp.schema.json的BrickWindowMethodenum。 - 跨语言协议规范:
@d:\ai-bricks\specs\window-api.md(写 Go / Python SDK 时照此抄)。 - 当前协议版本:
0.1.0,窗口 API 子集版本v0.2(41 → 107 个方法,向后兼容)。 - Go SDK 对照实现:
@d:\ai-bricks\Brickly\packages\brickly-sdk-go,API 表面与本包一一对应。 src/protocol.ts是 schema 的 TS 镜像,由@d:\ai-bricks\Brickly\scripts\check-bpp-schema.mjs强制保持同步。
构建与同步到 Brick
# 在 Brickly/ 目录下
npm run sdk:buildBrick runtime 通过 require('@syllm/brickly-sdk') 加载,并在 runtime/node/package.json
声明依赖。SDK 协议变更后发布新版 npm 包,Brick 升级依赖版本即可。
测试
npm run sdk:test # tsx --test tests/runtime.test.ts tests/api.test.ts参考实现:
- 窗口 API 实验室(覆盖 80+ 个方法的可视化测试):
@d:\ai-bricks\bricks\com.brickly.demo-window-lab\
