# SDK

TypeScript、Python、Go 和 Rust 客户端，通过同一套测试。用路径代替 id，流式上传和按范围下载，安全写入、局部修改、附加信息、搜索、变更、Webhook 和动作。

| 语言 | 包名 | 目录 | 运行环境 |
|---|---|---|---|
| TypeScript / JavaScript | `@sidespace/sdk` | `sdks/typescript` | Node 18+、Deno、Bun、浏览器（有 `fetch` 就行） |
| Python | `sidespace` | `sdks/python` | Python 3.9+，只用标准库 |
| Go | `github.com/blendduck/sidespace/sdks/go` | `sdks/go` | Go 1.22+，只用标准库 |
| Rust | `sidespace` | `sdks/rust` | 异步，基于 `reqwest`（rustls）和 `tokio` |

四个 SDK 都是 Apache-2.0 许可，文件相关的功能一致。它们还没有发布到 npm、PyPI、Go 模块代理和 crates.io：从仓库的检出目录使用，或者把文件夹拷进你的项目。

```sh tab=TypeScript
npm install ./sdks/typescript
```

```sh tab=Python
pip install ./sdks/python
```

```sh tab=Go
# 在你的模块里，把导入路径指向检出目录
go mod edit -replace github.com/blendduck/sidespace/sdks/go=../sidespace/sdks/go
go get github.com/blendduck/sidespace/sdks/go
```

```toml tab=Rust
# Cargo.toml
[dependencies]
sidespace = { path = "../sidespace/sdks/rust" }
```

## 初始化

```ts tab=TypeScript
import { SideSpace } from "@sidespace/sdk";

const side = new SideSpace({ baseUrl: "https://side.example.com", token: process.env.SIDESPACE_TOKEN! });
```

```python tab=Python
from sidespace import SideSpace

side = SideSpace("https://side.example.com", token="ssp_…")   # 或者设好 SIDESPACE_URL 和 SIDESPACE_TOKEN 后直接 SideSpace()
```

```go tab=Go
import sidespace "github.com/blendduck/sidespace/sdks/go"

side := sidespace.New("https://side.example.com", os.Getenv("SIDESPACE_TOKEN"))
hits, err := side.Search(ctx, "harbor night", nil)
```

```rust tab=Rust
use sidespace::SideSpace;

let side = SideSpace::new("https://side.example.com", &std::env::var("SIDESPACE_TOKEN")?);
let hits = side.search("harbor night", &Default::default()).await?;
```

空间可以用名字或 id，文件用路径。错误里带服务器返回的 `status`、`code` 和 `message`：TypeScript 和 Python 抛出 `SideSpaceError`；Go 和 Rust 的错误可以用 `IsPreconditionFailed` / `is_precondition_failed()` 判断安全写入是否冲突，并拿到当前版本。完整方法列表见各自目录的 README。

## 覆盖范围

| 功能 | TypeScript | Python |
|---|---|---|
| 空间和文件夹 | `spaces()`、`space()`、`node()`、`list()`、`mkdir()` | `spaces()`、`space()`、`node()`、`list()`、`mkdir()` |
| 上传和下载 | `upload()`、`download()`、带 `range` 的 `open()` | `upload()`（文件对象流式上传）、`download()`、带 `range` 的 `open()` |
| 版本 | `versions()`、`restoreVersion()` | `versions()`、`restore_version()` |
| 只改一部分 | `edit()`、`appendText()` | `edit()`、`append_text()` |
| 过去的样子 | `node()`、`list()`、`open()`、`download()` 带 `asOf`；`rewind()` | `node()`、`list()`、`open()` 带 `as_of=`；`rewind()` |
| 附加信息 | `getSidecar()`、`putSidecar()` | `get_sidecar()`、`put_sidecar()` |
| 搜索 | `search(q, { space, type, mode })` | `search(q, space=, type=, mode=)` |
| 变更 | `journal()`、`changes()`（异步迭代器） | `journal()`、`changes()`（生成器） |
| Webhook | `createWebhook()`，`@sidespace/sdk/webhooks` 里的 `verifyWebhookSignature()` | `create_webhook()`、`verify_webhook_signature()` |
| 动作和流水线 | `actions()`、`run()`、`startRun()`、`waitRun()`、`runs()`、`createPipeline()` | `actions()`、`run()`、`start_run()`、`wait_run()`、`runs()`、`create_pipeline()` |
| 评论 | `comments()`、`addComment()`、`editComment()`、`resolveComment()` | `comments()`、`add_comment()`、`edit_comment()`、`resolve_comment()` |
| 分支 | `fork()`、`forks()`、`forkChanges()`、`applyFork()` | `fork()`、`forks()`、`fork_changes()`、`apply_fork()` |
| 插件 | `installPlugin()`、`PluginRun` | `install_plugin()`、`PluginRun` |

Go 和 Rust 覆盖同样的文件操作（空间、路径、流式上传和按范围下载、安全写入、按字节修改、版本、附加信息、搜索、评论、变更、动作和 Webhook），方法名按各自语言的习惯（`UploadText`、`upload_text`）。

[API 参考](/docs/api)里的其他接口，用 `request()` 一行就能调（Go 里是 `Do`）：

```ts tab=TypeScript
const film = await side.space("Film A");
const presence = await side.request("GET", `/v1/spaces/${film.id}/presence`);
```

```python tab=Python
film = side.space("Film A")
presence = side.request("GET", f"/v1/spaces/{film['id']}/presence")
```

## 四个 SDK 一套测试

`sdks/conformance/cases.json` 是一份 34 步的清单（保存、按范围读、安全写入冲突、局部修改、恢复、文件夹、附加信息、评论、搜索和错误），每个 SDK 用自己的执行器对着真实服务器跑一遍，`check.py` 对比结果，所以四个 SDK 不会各走各的。四个都全部通过。想自己对着开发服务器检查一个：

```sh
SIDESPACE_SERVER=… SIDE_TOKEN=… SPACE=… python3 sdks/conformance/runner.py | python3 sdks/conformance/check.py --sdk python
```

任何语言的新 SDK，跑通全部步骤就算完成。

## 完整示例

给 `/dailies` 里还没打分的每个视频，用你自己的模型打分（只读每个文件的前 8 MB），结果存进附加信息：

```ts tab=TypeScript
import { SideSpace, SideSpaceError } from "@sidespace/sdk";

const side = new SideSpace({ baseUrl: process.env.SIDESPACE_URL!, token: process.env.SIDESPACE_TOKEN! });

for (const file of await side.list("Film A", "/dailies")) {
  if (!file.mime?.startsWith("video/")) continue;
  const path = `/dailies/${file.name}`;
  const done = await side.getSidecar("Film A", path, "x-mymodel").catch((e) => {
    if (e instanceof SideSpaceError && e.status === 404) return null;
    throw e;
  });
  if (done) continue;
  const head = await side.open("Film A", path, { range: [0, 8 * 1024 * 1024 - 1] });
  const score = await myModel(await head.arrayBuffer());
  await side.putSidecar("Film A", path, "x-mymodel", { score, model: "mymodel-2" });
}
```

```python tab=Python
from sidespace import SideSpace, SideSpaceError

side = SideSpace()
for file in side.list("Film A", "/dailies"):
    if not (file.get("mime") or "").startswith("video/"):
        continue
    path = f"/dailies/{file['name']}"
    try:
        side.get_sidecar("Film A", path, "x-mymodel")
        continue  # 已经打过分
    except SideSpaceError as e:
        if e.status != 404:
            raise
    with side.open("Film A", path, range=(0, 8 * 1024 * 1024 - 1)) as r:
        score = my_model(r.read())
    side.put_sidecar("Film A", path, "x-mymodel", {"score": score, "model": "mymodel-2"})
```

要随着新素材到来持续处理，把同样的代码放进 [Webhook](/docs/changes#webhook) 处理函数，或者打包成[插件](/docs/plugins)再配一条流水线：两者都会记住进度并自动重试。
