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

# brickly-sdk

Brickly Brick **Python runtime** 官方 SDK。它通过 stdin/stdout 与宿主通信，封装 BPP
（Brickly Plugin Protocol），让 Python Brick 可以专注写命令逻辑，而不是手写协议消息。

stdout 会被 SDK 保留给 BPP 协议消息；业务日志请使用 `brick.log(...)` 写入 stderr。

## 安装

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

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

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

## 快速开始

```python
from brickly import BricklyRuntime

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


@brick.on_command("hello")
def hello(ctx, input_value):
    ctx.progress(0.5, "处理中...")
    ctx.chunk("hello\n")
    return {"ok": True, "input": input_value}


brick.run()
```

SDK 会自动处理：

- `host.hello` -> `runtime.ready`
- `runtime.ping` -> `runtime.pong`
- `command.invoke` 命令分发
- `command.cancel` 取消信号、`ctx.is_cancelled()` 和 `ctx.on_cancel(...)`
- `command.progress`、`command.chunk` 和 `command.output`
- `host.*` 请求 ID 分配，以及 `host.result` / `host.error` 路由
- `runtime.shutdown` -> 可选 shutdown hook -> `runtime.bye`
- 子窗口创建、窗口方法调用、`window.*` 事件路由
- 事件总线 `events.publish(...)` / `events.on(...)`
- 跨 Brick 调用 `invoke(...)`、`invoke_root(...)`、`invoke_stream(...)`
- 跨 Brick 会话 `open_session(...)`
- 平台能力 `platform.screenshot`、`platform.screen`、`platform.input`、`platform.clipboard`、`platform.system`

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

## 与 Node SDK 的同步关系

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

- Python 使用 `snake_case` 方法名，例如 `create_browser_window()`、`set_full_screen()`。
- Node 使用 `camelCase` 方法名，例如 `createBrowserWindow()`、`setFullScreen()`。
- 两者底层发送的 BPP 消息类型和窗口方法名一致。
- 当前 SDK 包版本为 `0.3.0`；BPP 协议版本为 `0.2.0`；窗口 DTO 和方法 Literal 由 `specs/bpp.schema.json` 生成。
- Python 不提供 Node 的 TypeScript `CommandMap` 类型生成能力；Python 侧依赖类型标注和中文 docstring 提供 IDE 补全。

## 命令上下文

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

```python
@brick.on_command("process")
def process(ctx, input_value):
    ctx.progress(0.2, "准备中")

    if ctx.is_cancelled():
        return {"cancelled": True}

    ctx.output("debug", {"step": "start"})
    ctx.chunk("第一段输出\n")
    return {"ok": True}
```

常用属性和方法：

- `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.progress(value, message)`：上报进度
- `ctx.chunk(chunk, name=None)`：追加流式输出
- `ctx.output(name, value)`：设置具名输出
- `ctx.invoke(...)`：在命令作用域内调用另一个 Brick
- `ctx.invoke_resource(...)`：调用另一个 Brick 并固定返回 `ResourceHandle`
- `ctx.invoke_root(...)`：发起独立 root 调用，并携带当前请求 ID 供宿主审计
- `ctx.invoke_root_resource(...)`：发起独立资源结果调用
- `ctx.invoke_stream(...)`：流式调用另一个 Brick
- `ctx.open_session(...)`：打开跨 Brick 会话，后续可用 `session.invoke(...)` / `session.invoke_stream(...)`

## 子窗口

```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 调用

命令处理函数内部使用 `ctx.invoke(...)`：

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

如果宿主在 `ctx.invocation.dependencyProfiles` 中给目标 Brick 指定了 Profile，且调用时没有显式传
`profile_id`，`ctx.invoke(...)`、`ctx.invoke_root(...)`、`ctx.invoke_stream(...)` 和
`ctx.open_session(...)` 都会自动沿用该 Profile；显式 `profile_id` 始终优先。这一点与 Node SDK 行为一致。

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

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

### 大载荷与资源

普通 `invoke` / `invoke_root` 始终返回直接值，逻辑 JSON 输入和结果上限为 200 MiB；
超限抛出 `PAYLOAD_TOO_LARGE`，不会静默改成资源类型。预期结果需要流式读取时使用
`ctx.invoke_resource(...)`、`brick.invoke_root_resource(...)` 或
`session.invoke_resource(...)`，它们固定返回 `ResourceHandle`：

```python
resource = ctx.invoke_resource("com.example.report", "export", input_value)

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

ctx.invoke("com.example.consumer", "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`。该 API 要求 `resource.register`。小内容走一次性快速路径，大内容
自动切换到 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 的 200 MiB 上限约束。Host 限制并发上传并在生产环境保留 1 GiB 磁盘安全
余量；部署还可配置全局和 Brick 维度的 pending bytes 配额。

需要主动分多次写入时使用 `create_writer`：

```python
writer = brick.resources.create_writer(name="result.bin")
writer.write(header)
writer.write_from(download_stream)
resource = writer.finish()
```

同一 Writer 的 `write`、`write_from`、`finish`、`abort` 按调用顺序串行执行；`abort` 不会越过
已经开始的数据源操作。大文本在 SDK 内分段编码，不会额外创建一份完整 UTF-8 副本。

`ResourceHandle` 支持迭代字节、`text()`、`json()`、`save_to()`、`close()` 和
`revoke()`；再次作为 input 时只传 `ResourceRef`。事件总线回调统一收到外层
`ResourceHandle`，需要先 `json()` 取得业务对象。资源内容按普通 JSON 解析，内嵌的
`ResourceRef` 不会自动水合，需要读取时应显式转换。不要记录 capability token，也不要
长期持久化 Ref。

流式调用：

```python
for event in ctx.invoke_stream("com.brickly.openai", "chat", {"prompt": "hello"}):
    if event["type"] == "progress":
        ctx.progress(event.get("progress", 0), event.get("message"))
    elif event["type"] == "chunk":
        ctx.chunk(event.get("chunk"))
    elif event["type"] == "output":
        ctx.output(event["name"], event.get("value"))
    elif event["type"] == "result":
        return event["result"]
```

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

```json
"dependencies": {
  "com.brickly.openai": {
    "commands": ["chat"]
  }
}
```

## 跨 Brick 会话

当目标 Brick 需要保留内存状态时，使用 `ctx.open_session(...)`：

```python
session = ctx.open_session("com.brickly.openai", profile_id="work")

try:
    session.invoke("start-thread", {"title": "草稿"})
    reply = None
    for event in session.invoke_stream("chat", {"prompt": "继续这个话题"}):
        if event["type"] == "chunk":
            ctx.chunk(event.get("chunk"))
        elif event["type"] == "result":
            reply = event["result"]
    return reply
finally:
    session.close()
```

## 平台能力

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