Metadata-Version: 2.5
Name: brickly-sdk
Version: 0.6.0
Summary: Brickly Brick Python runtime 官方 SDK。
Author: Brickly
License: MIT
Requires-Python: >=3.10
Requires-Dist: grpcio>=1.75.1
Requires-Dist: protobuf>=6.32.1
Requires-Dist: typing-extensions>=4.1; python_version < '3.11'
Description-Content-Type: text/markdown

# brickly-sdk

Brickly Brick **Python runtime** 官方 SDK。通过 loopback gRPC 接入 Host Runtime
（`invoke` / `interact`），让 Python Brick 专注写命令逻辑。缺少 Host endpoint 时拒绝 BPP fallback。

业务日志请使用 `brick.log(...)` 写入 stderr，不要手写旧 stdin/stdout 协议帧。

## 安装

```bash
pip install brickly-sdk==0.6.0
```

国内环境可以使用 PyPI 镜像：

```bash
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple brickly-sdk==0.6.0
```

## 快速开始

```python
from brickly import BricklyRuntime

brick = BricklyRuntime("com.example.python")


@brick.on_command("hello")
def hello(ctx, input_value):
    ctx.send({"type": "hello", "text": "处理中..."})
    return {"ok": True, "input": input_value}


brick.run()
```

SDK 会自动处理：

- 连接 `BRICKLY_HOST_ENDPOINT` 并注册 gRPC Runtime
- `invoke` / `interact` 命令分发
- 取消信号、`ctx.is_cancelled()` 和 `ctx.on_cancel(...)`
- `invoke` 一次一结果；`interact` 用 `ctx.send` / `on_event` / `closed`
- Host 平台 / UI / Resource 客户端
- 可选 shutdown hook
- 子窗口创建、窗口方法调用、`window.*` 事件路由
- 事件总线 `events.publish(...)` / `events.on(...)`
- alias-first 跨 Brick 调用与会话：`dependencies.require(alias)`
- 平台能力 `platform.screenshot`、`platform.screen`、`platform.input`、`platform.clipboard`、`platform.system`

跨 Brick 的 `invoke` / `interact` 不设置 SDK 本地固定超时，由 Host
调用生命周期统一终止，避免大资源或长任务被误判超时。其他底层 Host API
仍保留默认超时，显式 `timeout` 的调用方式不变。

## 与 Node SDK 的同步关系

Python SDK 的协议语义与 `@syllm/brickly-sdk` 保持一致：

- Python 使用 `snake_case` 方法名，例如 `create_browser_window()`、`set_full_screen()`。
- Node 使用 `camelCase` 方法名，例如 `createBrowserWindow()`、`setFullScreen()`。
- 两者底层走同一套 gRPC Runtime / Host 服务，窗口方法名一致。
- 当前 SDK 包版本为 `0.6.0`；生产协议是 `brickly.runtime.v1`。
- Python 不提供 Node 的 TypeScript `CommandMap` 类型生成能力；Python 侧依赖类型标注和中文 docstring 提供 IDE 补全。

## 命令上下文

命令处理函数会收到 `CommandContext`：

```python
@brick.on_command("process")
def process(ctx, input_value):
    if ctx.is_cancelled():
        return {"cancelled": True}

    ctx.send({"type": "status", "step": "start"})
    return {"ok": True}
```

长期占用使用 `ToolSdk.start()` / `ToolHandle`。异步上下文管理器是 `dispose()` 语法糖。一次性 invoke 不会 pin 进程。

常用属性和方法：

- `ctx.request_id`：当前请求 ID
- `ctx.command_id`：当前命令 ID
- `ctx.invocation`：宿主传入的可信调用来源和依赖 Profile 映射
- `ctx.config`：当前 Profile 配置快照
- `ctx.ui`：子窗口 API
- `ctx.events`：事件总线 API
- `ctx.platform`：平台能力 API
- `ctx.system`：`ctx.platform.system` 的快捷别名
- `ctx.send(event)`：推给调用方（仅 interact）
- `ctx.on_event(handler)`：收调用方 send（仅 interact）
- `ctx.closed.wait()`：等到调用方 closeInput / 断开
- `ctx.dependencies.require(alias)`：获取绑定当前 command parent、trace 与 Profile 的依赖客户端

## 子窗口

```python
from brickly import BricklyRuntime

brick = BricklyRuntime("com.example.window")


@brick.on_command("open")
def open_window(ctx, _input):
    win = ctx.ui.create_browser_window("ui/index.html", {"width": 640, "height": 480})
    win.on("closed", lambda payload: brick.log("窗口已关闭", payload))
    win.set_title("Hello from Python")
    win.web_contents.send("app:hello", {"requestId": ctx.request_id, "text": "你好"})
    return {"windowId": win.id, "windowKey": win.window_key}


brick.run()
```

`WindowHandle` 提供与 Node SDK 对齐的常用窗口方法，例如：

- 几何尺寸：`set_bounds()`、`get_bounds()`、`set_position()`、`set_size()`
- 内容区域：`set_content_bounds()`、`get_content_bounds()`、`set_content_size()`
- 状态切换：`minimize()`、`maximize()`、`restore()`、`show()`、`hide()`、`focus()`
- 状态查询：`is_visible()`、`is_focused()`、`is_minimized()`、`is_full_screen()`
- 外观能力：`set_title()`、`set_opacity()`、`set_background_color()`、`set_has_shadow()`
- webContents：`send()`、`execute_javascript()`、`open_dev_tools()`、`go_back()`、`set_zoom_factor()`、`copy()`、`paste()`、`undo()`

关闭是显式生命周期操作：

```python
result = win.close()
if result["status"] in ("pending", "prevented"):
    win.focus()  # 句柄仍可用

termination = win.force_close()
# win.destroy() 是 force_close() 的旧便利名，不走反射 destroy。
```

`close()` 返回 `closed/prevented/pending/not-found`。只有 `closed/not-found` 和 `window.closed` 终态事件会把句柄标记为 closed、从 Runtime Map 删除并清空 listener。终态 `eventId` 有界去重，transport 结束时也会释放全部窗口句柄。

## 跨 Brick 调用

命令处理函数内部只使用 manifest alias：

```python
result = ctx.dependencies.require("openai").invoke(
    "chat",
    {"prompt": "hello"},
    profile_id="work",
)

session = await ctx.dependencies.require("openai").interact(
    "complete",
    {"prompt": "写一首诗"},
)
await session.close_input()
poem = await session.result
```

精确来源和版本由 Host 握手绑定。热键依赖 Profile 会按绑定的精确 `BrickKey` 自动使用，显式
`profile_id` 始终优先。

命令处理函数外部需要使用显式 root 调用：

```python
result = brick.dependencies.require("openai").invoke_root(
    "chat",
    {"prompt": "hello"},
    profile_id="work",
)
```

### 大载荷与资源

普通 `invoke` / `invoke_root` 始终返回直接值，逻辑 JSON 输入和结果上限为 10 MiB，一次传完；
超限抛出 `PAYLOAD_TOO_LARGE`，不会静默改成资源类型。大结果由作者 `create` 后
`return` Handle；`invoke` 交回 `ResourceRef`，调用方再 `open`：

```python
ref = ctx.dependencies.require("report").invoke("export", input_value)
resource = brick.resources.open(ref)

if resource.ref["sizeBytes"] <= 200 * 1024 * 1024:
    report = resource.json()
else:
    resource.save_to(output_path)

ctx.dependencies.require("consumer").invoke(
    "analyse",
    {"source": resource},
)
```

Brick 可主动创建资源：

```python
input_resource = brick.resources.create(data, name="input.bin")
```

`str` 默认 `text/plain; charset=utf-8`，`bytes` 默认 `application/octet-stream`；只有下游需要
具体类型时才传 `mime_type`。该能力无需声明 manifest 权限，但仍受 Host 配额与生命周期治理。小内容走一次性快速路径，大内容
自动切换到 Writer，调用方式和返回类型不变。

大内容使用 `create_from`：

```python
with open("large.bin", "rb") as source:
    resource = brick.resources.create_from(source, name="large.bin")
```

它也接受 `Iterable[str | bytes]`，自动聚合后按最大 1 MiB 的 wire 分块顺序写入 Host，finish 后返回
`ResourceHandle`。finish 前资源不可读取；发布后下游独立读取，不会向上传端施加背压。资源总大小
不受普通 invoke 的 10 MiB 上限约束。Host 限制并发上传并在生产环境保留 1 GiB 磁盘安全
余量；部署还可配置全局和 Brick 维度的 pending bytes 配额。

`ResourceHandle` 支持迭代字节、`text()`、`json()`、`save_to()`、`close()` 和
`revoke()`；再次作为 input 时只传 `ResourceRef`。事件总线回调统一收到外层
`ResourceHandle`，需要先 `json()` 取得业务对象。资源内容按普通 JSON 解析，内嵌的
`ResourceRef` 不会自动水合，需要读取时应显式转换。不要记录 capability token，也不要
长期持久化 Ref。无论事件大小，回调都不会收到内联对象或内部
`{"resource": ..., "encoding": "json"}` 包装；消费完成后应调用 `close()`。

普通 invoke、interact、命令输入和资源 JSON 中的嵌套引用保持 `ResourceRef`。发送 invoke、
command 结果或事件时，SDK 自动把嵌套 `ResourceHandle` 转为完整 Ref。接收方通过
`brick.resources.open(ref)` 显式创建惰性 Handle；`open()` 不会立即访问 Host：

```python
resource = brick.resources.open(payload["attachment"])
try:
    resource.save_to(output_path)
finally:
    resource.close()
```

流式调用用 `interact`，收事件后再 `result()`：

```python
session = await ctx.dependencies.require("openai").interact(
    "chat",
    {"prompt": "hello"},
)
async for event in session.events:
    ctx.send(event)
await session.close_input()
return await session.result
```

跨 Brick 调用需要在调用方 manifest 的 `dependencies` 中声明目标 Brick 和命令：

```json
"dependencies": {
  "openai": {
    "target": {
      "brickId": "com.brickly.openai",
      "origin": "installed",
      "version": "2.1.0"
    },
    "commands": ["chat"]
  }
}
```

## 跨 Brick 会话

目标 Brick 有状态时，在 command handler 里 `interact`，不要另做 `open()`：

```python
session = await ctx.dependencies.require("openai").interact(
    "chat",
    {"prompt": "继续这个话题"},
)
async for event in session.events:
    ctx.send(event)
await session.close_input()
return await session.result
```

## 平台能力

系统 API：

```python
@brick.on_command("show-app-info")
def show_app_info(ctx, _input):
    return {
        "appName": ctx.system.get_app_name(),
        "appVersion": ctx.system.get_app_version(),
        "userData": ctx.system.get_path("userData"),
        "isWindows": ctx.system.is_windows(),
    }
```

剪贴板 API：

```python
@brick.on_command("replace-clipboard")
def replace_clipboard(ctx, _input):
    previous = ctx.platform.clipboard.read_content()
    updated = ctx.platform.clipboard.set_content({"kind": "text", "text": "来自 Python"})
    return {"previous": previous, "updated": updated}
```

输入和屏幕 API：

```python
@brick.on_command("screen-info")
def screen_info(ctx, _input):
    point = ctx.platform.screen.get_cursor_screen_point()
    display = ctx.platform.screen.get_primary_display()
    return {"point": point, "display": display}


@brick.on_command("click")
def click(ctx, _input):
    ctx.platform.input.mouse_click(100, 100)
    ctx.platform.input.keyboard_tap("A", "control")
    return {"ok": True}
```

这些能力由宿主按 manifest 权限校验。缺少权限时，宿主会返回 `host.error`，SDK 会抛出 `BppError`。

## 错误处理

抛出 `BppError` 可以保留明确错误码：

```python
from brickly import BppError

raise BppError("INVALID_INPUT", "url 不能为空")
```

普通异常会被 SDK 转换为 `INTERNAL_ERROR` 并返回给宿主。

## 日志

```python
brick.log("开始处理", {"id": 1})
```

`brick.log(...)` 会发送 `runtime.log`（info）到宿主。不要手动给日志加 `[brickId]` 前缀，宿主日志中心会自动附加 Brick ID、来源和作用域信息。

## 源码结构

实现按领域拆分为 `transport` / `scope` / `command` / `session` / `window` / `events` / `platform` / `runtime`，与 Node SDK 对齐。

详情见 [`brickly/README.md`](brickly/README.md)。

## AI 对齐框架

本 SDK 是 **Follower** 实现。用 AI 跟进 Node 时请走：

- `specs/sdk/AGENT.md`
- `specs/sdk/capability-matrix.yaml`
- `specs/sdk/api-mapping.yaml`
- `specs/sdk/prompts/follower-agent.md`（`target=python`）
