Metadata-Version: 2.5
Name: brickly-sdk
Version: 0.8.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.info` / `brick.warn` / `brick.error`（或兼容旧名 `brick.log`），经 Host `diagnostics.log` 进入日志中心。不要手写旧 stdin/stdout 协议帧。平台未连接时这些方法是 no-op，不会抛错。

## 安装

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

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

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

## 快速开始

```python
from brickly import BricklyRuntime

brick = BricklyRuntime()


@brick.on_command("hello")
def hello(ctx, input_value):
    data = input_value if isinstance(input_value, dict) else {}
    name = str(data.get("name") or "Brickly")
    brick.info("hello", {"name": name})
    return {"message": f"Hello, {name}"}


brick.run()
```

SDK 会自动处理：

- 连接 `BRICKLY_HOST_ENDPOINT` 并注册 gRPC Runtime
- `invoke` / `interact` 命令分发
- 取消信号、`ctx.is_cancelled()` 和 `ctx.on_cancel(...)`
- `invoke` 一次一结果；`interact` 用 `ctx.send` / `on_event` / `closed`
- 再跑自己的命令：`invoke` / `interact` / `call`（已有占用则不 dispose；没有占用则拒绝）
- Host 平台 / UI / Resource 客户端
- 可选 shutdown hook
- 子窗口创建、窗口方法调用、`window.*` 事件路由
- 事件总线 `events.publish(...)` / `events.on(...)`（公共事件 `命名空间:主题`；窗口寿命用 `win.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` 的调用方式不变。

### 进阶：双向 `live`

`ctx.send` 只在 `interact` 里有意义，不要写进 `hello`。页面用 `interact`，不要用 `call`。

```python
@brick.on_command("live")
def live(ctx, input_value):
    n = {"value": 0}

    def on_event(event):
        data = event if isinstance(event, dict) else {}
        ctx.send({"type": "reply", "text": "收到" + str(data.get("text") or "")})

    ctx.on_event(on_event)
    ctx.closed.wait()
    return {"n": n["value"]}
```

### 进阶：两种子窗

`attached`（默认）随这次调用 / runtime 消失。`standalone` 窗在则 runtime 在：须声明 `command.window: standalone`，并在命令执行期间创建。后台定时弹窗请 `invoke` 一条 `window: standalone` 的命令，不要直接 `create_browser_window`。建窗写 `lifetime="standalone"`。不要把「附加」理解成常驻。

## 与 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.8.0`（`__version__`）；生产协议是 `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`。Runtime 里占用依赖用 `require(alias).start()`，必须先进入自己的命令（`brick.invoke` 中转）；占用跟这次 Call，return 自动放手。异步上下文管理器是 `dispose()` 语法糖。一次性 invoke 不会 pin 进程。

常用属性和方法：

- `ctx.request_id`：当前请求 ID
- `ctx.command_id`：当前命令 ID
- `ctx.invocation`：宿主传入的可信调用来源和依赖 Profile 映射
- `ctx.config`：当前 Profile 配置快照
- `ctx.storage`：本机持久 KV / collection / secrets；与体验窗共库。看不见路径或 `_rev`
- `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()


@brick.on_command("open")
def open_window(ctx, _input):
    win = ctx.ui.create_browser_window("ui/index.html", {"width": 640, "height": 480})
    win.expose({
        "pause": lambda _payload, _session=None: None,
    })
    win.send("tick", {"remaining": 60})
    win.on("closed", lambda payload: brick.info("窗口已关闭", {"payload": payload}))
    win.set_title("Hello from Python")
    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()
```

`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",
)

from brickly.internal.grpc.client import call

poem = await call(
    ctx.dependencies.require("openai"),
    "complete",
    {"prompt": "写一首诗"},
    on_event=lambda event: None,
)
```

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

没有当前命令时，同一套 `invoke` / `call` / `interact` 就是 root：

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

### 大载荷与资源

普通 `invoke` 始终返回直接值，逻辑 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`。资源创建仍受 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`。若业务对象里本身带 `ResourceRef`，需要
读内容时再 `resources.open`。不要记录 capability token，也不要长期持久化 Ref。

普通 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()
```

过程调用用 `call`，必须传入 `on_event`：

```python
from brickly.internal.grpc.client import call

return await call(
    ctx.dependencies.require("openai"),
    "chat",
    {"prompt": "hello"},
    on_event=lambda event: ctx.send(event),
)
```

跨 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()`。收过程只走 `on_event`，说完用 `end`：

```python
session = await ctx.dependencies.require("openai").interact(
    "chat",
    {"prompt": "继续这个话题"},
)
return await session.end()
```

## 平台能力

系统 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}
```

宿主错误会以 `BppError` 原样抛出。`shell_open_external` 仅允许 `http` / `https` / `mailto`。

## 错误处理

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

```python
from brickly import BppError

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

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

## 日志

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

`debug` / `info` / `warn` / `error` 经 Host `diagnostics.log` 进入日志中心。命令 handler 内会带上当前 `invocationId` 挂到该 command 节点；`on_ready` 等无当前 command 时走顶级 diagnostic。handler 返回后的异步日志仍能靠 ContextVar 挂回（与 Node 一致）。不要手写 stdin/stdout 协议帧。

## 源码结构

实现按领域拆分为 `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`）
