@syllm/brickly-sdk
v0.9.0
Published
Brickly Brick Node runtime 官方 SDK 0.9.0(gRPC Runtime)
Readme
@syllm/brickly-sdk
Brickly Brick Node runtime 官方 SDK。用 1 行代码接入 Host gRPC Runtime(invoke / interact),不要手写旧 BPP 或 session API。
体验窗 / 页面 TypeScript 请安装 @syllm/brickly-ui,不要用本包给 window.brickly 做类型。
生产协议是 loopback gRPC;缺少 Host endpoint 时会拒绝 fallback 到 stdin/stdout。
快速上手
const { BricklyRuntime } = require('@syllm/brickly-sdk')
const brick = new BricklyRuntime()
brick.onCommand('hello', async (_ctx, input) => {
const name = String((input && input.name) || 'Brickly')
brick.log.info('hello', { name })
return { message: 'Hello, ' + name }
})
brick.start()SDK 自动完成:
- 连接
BRICKLY_HOST_ENDPOINT并注册 gRPC Runtime invoke/interact命令分发- Host 平台 / UI / Resource 客户端路由
- 取消信号与 shutdown 钩子
进阶:双向 live
ctx.send 只在 interact 里有意义,不要写进 hello。页面用 interact,不要用 call(call 会立刻关掉输入)。
brick.onCommand('live', async (ctx) => {
let n = 0
const timer = setInterval(() => {
n += 1
ctx.send({ type: 'tick', n }).catch(() => {})
}, 1000)
ctx.onEvent(async (event) => {
await ctx.send({ type: 'reply', text: '收到' + String(event?.text || '') })
})
await ctx.closed
clearInterval(timer)
return { n }
})进阶:两种子窗
attached(默认)随这次调用 / runtime 消失。standalone 窗在则 runtime 在:须声明 command.window: standalone,并在命令执行期间创建。后台定时弹窗请 invoke 一条 window: standalone 的命令,不要直接 createWindow。建窗写 lifetime: 'standalone'。不要把「附加」理解成常驻。
核心 API
长期占用请使用 ToolSdk.start() / ToolHandle。Runtime 里占用依赖用 require(alias).start(),必须先进入自己的命令(brick.invoke 中转);占用跟这次 Call,return 自动放手。一次性 invoke / interact 不会 pin 进程。
dispose() 关闭 attached 窗口并释放 caller retainer;stop() 取消该 Lifetime 的调用并关闭全部窗口。
子窗口默认 attached;standalone 须声明 command.window: standalone,并在当前命令执行期间创建。后台定时弹窗请 invoke。建窗标 lifetime: 'standalone'。per-call 不能 start(),因此不能开 standalone。有当前命令且 command.window === none 时禁止 createWindow。
BricklyRuntime
| 方法 | 作用 |
| ------------------------------------------------------ | ---------------------------------------------------------------------- |
| onCommand(id, handler) | 注册命令处理器 |
| invoke(commandId, input?) | 再跑自己的一条命令;已有占用则不 dispose。没有占用则拒绝 |
| interact(commandId, input?, { onEvent }) | 已有占用上再开会话,不 dispose;必须传入 onEvent |
| call(commandId, input, { onEvent }) | interact + 半关闭的糖;必须与命令 mode=call 对齐 |
| onReady(fn) | gRPC Runtime 就绪后立即触发(适合 service 预热后初始化 / 实例启动逻辑) |
| onShutdown(fn) | Host 关闭 Runtime 时触发 |
| ui.createBrowserWindow(url, options) | 创建子窗口,返回 WindowHandle |
| ui.listWindows() | 列出本 Brick 持有的窗口 |
| events.on(event, fn) | 订阅公共事件(命名空间:主题);窗口寿命用 win.on |
| events.publish(event, payload) | 发布事件 |
| resources.open(ref) | 惰性绑定已有 ResourceRef,不立即访问 Host |
| dependencies.require(alias) | 获取 Host 握手绑定到精确 BrickRef 的依赖客户端 |
| log.debug/info/warn/error(...) | 经 Host diagnostics.log 进日志中心;带着这次 command 的 invocationId 则挂该节点(含返回后的异步/定时器),否则顶级 |
| start() | 连接 Host gRPC endpoint 并阻塞运行;同时读取 BRICKLY_PROFILE_CONFIG 填 ctx.config |
CommandContext(handler 第一个参数)
| 字段 | 作用 |
| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| requestId / commandId | 当前请求与命令 id |
| invocation | 宿主注入的调用来源;热键触发时 source === 'hotkey',可携带热键 Profile 选择 |
| send(event) | 推给调用方(仅 interact) |
| onEvent(handler) | 收调用方 send(仅 interact) |
| closed | 等到调用方 end / 断开 |
| onCancel(fn) | 注册取消回调 |
| isCancelled() | 协作式取消轮询 |
| dependencies.require(alias) | 获取当前 command 的依赖客户端,自动携带 parent、trace 与依赖 Profile |
| ui / events / platform | 与 brick.ui / brick.events / brick.platform 同源 |
| config | Host 在 spawn 时固化的当前 Profile 字段快照(inject=env 的字段不在这里) |
| storage | 本机持久 KV / collection / secrets;与体验窗 window.brickly.storage 共库。看不见路径或 _rev |
日志约定
业务日志只允许带 level 的 API:
brick.log.info('ready')
brick.log.warn('retry', { n: 2 })
brick.log.error('failed', err)
// command 内自动挂当前 Trace:
// (若使用 CommandContext 扩展)ctx 侧同样应走 structured log对应宿主结构化日志。禁止 console.* / 裸 stderr / 无 level 的 log(...)(已移除)。
不要往 stdout 打业务日志;应改用 plugin.log / brick.log。
跨 Brick 调用
业务代码只使用 manifest alias。精确来源和版本由 Host 注入依赖绑定,SDK 不做回退或猜测。
const openai = ctx.dependencies.require('openai')
const result = await openai.invoke('chat', { prompt: 'hello' }, { profileId: 'work' })
const poem = await openai.call('complete', { prompt: '写一首诗' }, {
onEvent(event) {
void event
}
})调用方 manifest 必须在 dependencies 中声明目标 Brick 和允许调用的命令:
"dependencies": {
"openai": {
"target": {
"brickId": "com.brickly.openai",
"origin": "installed",
"version": "2.1.0"
},
"commands": ["chat"]
}
}commands: ["*"] 表示允许调用目标 Brick 的全部可见命令;隐藏命令必须显式写命令 id。
热键触发 command 时,SDK 按 alias 绑定的精确 BrickKey 自动使用依赖 Profile;显式 profileId 优先。
没有当前命令时,同一套 invoke / call / interact 就是 root,不必另写 invokeRoot:
const result = await brick.dependencies
.require('openai')
.invoke('chat', { prompt: 'hello' }, { profileId: 'work' })大载荷与资源
普通 invoke 始终返回直接值,输入和结果的逻辑 JSON 上限为 10 MiB,一次传完;超过上限会抛
PAYLOAD_TOO_LARGE,不会自动改成联合返回类型,也不会自动重试可能有副作用的命令。
大结果由作者 resources.create 后直接 return Handle。invoke 原样交回 ResourceRef
或普通 JSON,调用方要读时再 open:
const ref = await ctx.dependencies.require('report').invoke('export', input)
const resource = brick.resources.open(ref)
if (resource.ref.sizeBytes <= 200 * 1024 * 1024) {
const report = await resource.json()
} else {
await resource.saveTo(outputPath)
}
await ctx.dependencies.require('consumer').invoke('analyse', { source: resource })Brick 也可以主动创建资源后再传给下游:
const inputResource = await brick.resources.create(bytes, {
mimeType: 'application/octet-stream',
name: 'input.bin'
})string 默认 text/plain; charset=utf-8,Uint8Array 默认
application/octet-stream,通常无需填写 mimeType。资源创建仍受 Host
配额与生命周期治理。小内容使用一次性快速路径;超过内部阈值时 SDK 自动切换到 Writer,调用方式
和返回类型不变。
大内容或未知长度数据使用 store-and-forward 流式创建:
const resource = await brick.resources.createFrom(createReadStream('large.bin'), {
name: 'large.bin'
})SDK 自动聚合来源小块并按最大 1 MiB 的 wire 分块逐块等待 Host 落盘确认,最后调用 finish 返回
ResourceHandle。资源在 finish 前不可读取;finish 后是不可变快照,下游读取速度不会影响上传。
流式资源总大小不受普通 invoke 的 10 MiB 上限约束。Host 限制并发上传并在生产环境保留
1 GiB 磁盘安全余量;部署还可配置全局和 Brick 维度的 pending bytes 配额。
ResourceHandle 提供 stream()、bytes()、text()、json()、saveTo()、close() 和
revoke();再次作为 input 时 SDK 只发送 ResourceRef,不会复制资源字节。
bytes() / text() / json() 仅允许整体物化不超过 200 MiB 的资源。
普通 invoke、interact、命令输入和资源 JSON 中的嵌套引用保持 ResourceRef。发送 invoke、
command 结果或事件时,SDK 会自动把嵌套 ResourceHandle 转为完整 Ref;接收方读取
嵌套资源时显式打开,open() 本身不会发送 Host 请求:
const handle = brick.resources.open(payload.attachment)
try {
await handle.saveTo(outputPath)
} finally {
await handle.close()
}events.on() 回调收到的就是发布时的业务对象,不会再包一层资源,也不会水合成
ResourceHandle。若业务对象里本身带 ResourceRef,需要读内容时再 resources.open。
平台 System API
brick.platform.system.* 与 handler 内的 ctx.platform.system.* 通过 Host PlatformService 调用宿主系统能力:
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。
readCurrentFolderPath() 在 macOS Finder 与 Windows Explorer 前台窗口可用;当前没有可读取的前台文件管理器文件夹时会抛 CURRENT_FOLDER_UNAVAILABLE。readCurrentBrowserUrl() 当前预留,会返回 UNSUPPORTED_PLATFORM。shellOpenExternal 仅允许 http / https / mailto。
跨 Brick 会话
目标 Brick 有状态时,在 command handler 里 interact,不要另做 open()。
const session = await ctx.dependencies.require('openai').interact(
'chat',
{ prompt: '继续刚才的话题' },
{
onEvent(event) {
void ctx.send(event)
}
}
)
return await session.end()profileId 仍然是目标 Brick 的 Profile ID;不传则使用目标 Brick 默认 Profile,或使用热键调用上下文中的依赖 Profile 选择。invoke / interact 都会按调用方 manifest 的 dependencies[target].commands 重新校验命令。
依赖调用类型生成
如果当前 Brick 在 manifest.dependencies 声明了依赖 Brick,可以生成一组开发期 JS 包装函数和 .d.ts,让调用依赖命令时获得命令名、入参、返回值和说明提示。
{
"dependencies": {
"text": {
"target": {
"brickId": "com.brickly.text-toolkit",
"origin": "installed",
"version": "1.0.0"
},
"commands": ["case", "stats"]
}
}
}在当前 Brick 根目录执行:
brickly-typegen默认输出到 runtime/node/_generated/deps/。生成物包含:
index.js/index.d.ts:统一导出所有依赖命名空间,并扩展 SDKCommandMap。<alias>.js/<alias>.d.ts:每个依赖 alias 一个文件,函数注释来自目标 manifest。
普通 JS runtime 可以直接使用生成的 wrapper:
const { BricklyRuntime } = require('@syllm/brickly-sdk')
const { textToolkit } = require('./_generated/deps')
const brick = new BricklyRuntime()
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,wrapper 就会按 alias 扩展 CommandMap;业务代码不传 BrickRef。
WindowHandle(105 个反射方法)
ui.createBrowserWindow 返回的 WindowHandle 封装宿主的 105 个反射方法,并提供独立的普通关闭和强制终止操作。详细契约见 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. 状态切换(10)
win.minimize() win.maximize() win.unmaximize() win.restore()
win.hide() win.show() win.showInactive()
win.focus() win.blur()
win.setFullScreen(true|false)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()关闭与事件
const result = await win.close() // closed | prevented | pending | not-found
await win.forceClose() // 跳过页面关闭协商
win.id // BrowserWindow id
win.windowKey // 生命周期主键
win.webContentsId // Electron webContents id
win.isClosed // 仅 closed/not-found/终态事件后为 true
win.expose({
pause: () => timer.pause(),
import: async (payload, { emit, signal }) => {
emit({ progress: 1 })
return { ok: true, payload, aborted: signal.aborted }
}
})
await win.send('tick', { remaining: 60 })
win.on('closed', ({ eventId, cause, forced }) => ...)
win.on('focus' | 'blur' | 'show' | 'hide' | 'resize' | 'move', ...)pending/prevented 后句柄保持可用。终态会从 Runtime 的窗口 Map 删除句柄并清空它的全部 listener;重复 window.closed.eventId 只处理一次,transport 结束也会释放所有句柄。
协议与版本对齐
- 白名单真相源:
specs/window-protocol.schema.json的BrickWindowMethodenum。 - 跨语言协议规范:
specs/window-api.md(写 Go / Python SDK 时以此为准)。 - 当前 SDK 包版本为
0.9.0(SDK_VERSION);生产协议是brickly.runtime.v1。 - 实验性的窗口网络抓包 API 已于 2026-07-15 删除,不保留
network选项或onNetwork()。 - Go SDK 对照实现:brickly-sdk-go,API 表面与本包一一对应。
- 窗口 DTO 与方法类型由 Schema 生成到
src/generated/window-protocol.ts。
构建与同步到 Brick
# 在 Brickly/ 目录下
npm run sdk:buildBrick runtime 通过 require('@syllm/brickly-sdk') 加载,并在 runtime/node/package.json
声明依赖。SDK 协议变更后发布新版 npm 包,Brick 升级依赖版本即可。
测试
npm test # 包目录:tsc + 官方 tsx --test 清单
npm run sdk:test # 工作区等价:npm -w @syllm/brickly-sdk test源码结构
源码在 src/,按协议、invoke 类型、window/platform 能力层与 runtime 编排层拆分;api.ts / index.ts 为公共入口。
| 目录 | 职责摘要 |
| --------------------------- | --------------------------------- |
| internal/grpc/ | Host gRPC 客户端与 Runtime 接入 |
| scope/ | command / event ALS |
| invoke/ | CommandMap 与 invoke 类型 |
| window/ | WindowHandle、UiApi、结果校验 |
| platform/ | PlatformApi 工厂 |
| command/ | CommandContext 等类型 |
| storage/ | StorageApi、Host 存储客户端 |
| runtime/ | BricklyRuntime 编排 |
| protocol.ts / errors.ts | 协议类型与错误 |
| generated/ | schema 生成物(禁止手改) |
模块职责表、允许依赖与改动规则见 src/README.md(给后续改代码的 AI / 维护者用)。
