# Connector 应用开发

开发 Connector 本地应用，定义清单、服务、内嵌界面、权限、调试、打包和市场发布流程。

本地应用是安装在用户电脑上的 Connector。它适合连接只能在本机访问的数据和程序，例如桌面软件、本地数据库、文件、浏览器、开发工具或硬件设备。

本地应用不进入 Bundle，也不是平台应用。它由百积木客户端安装和管理，在本机启动独立进程，通过 loopback HTTP 向 Bridge Agent 提供方法和事件；Bridge Agent 完成设备授权、远程调用控制和能力上报。

```text
智能体或工作流
  -> 百积木 Relay
  -> 用户已授权的 Bridge Agent
  -> Connector 注册的方法
  -> 127.0.0.1 上的本地服务
  -> 本机数据、程序或设备
```

## 开发流程

1. 确认能力必须在用户电脑上运行，并定义最小权限。
2. 实现仅监听 loopback 的本地 HTTP 服务。
3. 编写 `service-registration.json`，声明可远程调用的方法和事件。
4. 编写 `connector.json`，声明运行时、权限、配置、管理操作和可选内嵌界面。
5. 从本地目录安装，完成启动、健康检查、方法调用和停止测试。
6. 生成固定版本的 Git 标签或压缩包，计算 SHA-256。
7. 创建本地应用定义和不可变版本，提交市场审核。
8. 从市场或固定版本地址重新安装，完成最终验收。

## 项目结构

推荐使用下面的结构：

```text
example-connector/
├── connector.json
├── service-registration.json
├── README.md
├── ui/
│   ├── index.html
│   ├── app.js
│   └── app.css
├── bin/
│   ├── macos-arm64/
│   │   └── example-connector
│   ├── macos-x86_64/
│   │   └── example-connector
│   └── windows-x86_64/
│       └── example-connector.exe
└── tests/
```

Node.js 项目也可以在 `package.json` 的 `bin` 中声明命令；Python 项目可以在 `pyproject.toml` 的 `[project.scripts]` 中声明入口。百积木客户端安装 Connector 后会解析这些入口。原生程序优先放在 `bin/<平台>-<架构>/`，也可以放在包根目录或 `bin/`。

平台目录使用 `macos-arm64`、`macos-x86_64`、`windows-x86_64`、`linux-x86_64` 等名称。只在应用定义的 `platforms` 中声明真正构建和测试过的平台。

## 编写 Connector 清单

新应用使用 `schemaVersion: "1.2"`。下面是一份包含本地服务、内嵌界面和管理操作的示例：

```json
{
  "schemaVersion": "1.2",
  "id": "com.example.connector.notes",
  "name": "Example Notes Connector",
  "version": "0.1.0",
  "description": "Read notes stored on this computer.",
  "icon": {
    "mediaType": "image/png",
    "data": "<256x256 PNG 原始字节的标准 Base64>"
  },
  "publisher": {
    "name": "Example",
    "homepage": "https://example.com"
  },
  "source": {
    "type": "git",
    "repo": "example/example-notes-connector",
    "revision": "v0.1.0"
  },
  "hostRequirements": {
    "minimumVersion": "0.2.95",
    "capabilities": ["connector.presentation.icon.v1"]
  },
  "runtime": {
    "type": "process",
    "command": "example-connector",
    "args": ["start"],
    "startPolicy": "manual",
    "healthCheck": {
      "type": "http",
      "url": "http://127.0.0.1:18120/health",
      "timeoutSecs": 2,
      "expectStatus": 200
    }
  },
  "permissions": [
    {
      "id": "filesystem.notes.read",
      "title": "读取本机笔记",
      "description": "读取用户选择的笔记目录。",
      "platforms": ["macos", "windows", "linux"]
    }
  ],
  "management": {
    "type": "http",
    "baseUrl": "http://127.0.0.1:18120",
    "auth": {
      "type": "connector_token"
    },
    "operations": {
      "runtimeState": {
        "method": "GET",
        "path": "/management/v1/state"
      },
      "chooseNotesDirectory": {
        "method": "POST",
        "path": "/management/v1/choose-directory"
      }
    }
  },
  "ui": {
    "type": "embedded",
    "entry": "ui/index.html",
    "title": "管理",
    "defaultView": true
  },
  "configSchema": {
    "type": "object",
    "properties": {
      "port": {
        "type": "integer",
        "default": 18120,
        "minimum": 1024,
        "maximum": 65535
      }
    },
    "additionalProperties": false
  },
  "remoteCapabilities": [
    {
      "name": "notes.read",
      "risk": "medium",
      "description": "Read notes from the directory selected by the user."
    }
  ],
  "serviceRegistrationFiles": [
    "service-registration.json"
  ]
}
```

必须满足以下约束：

- `id`、`name`、`version` 不能为空，`id` 只能使用安全的路径字符。第三方应用不要使用保留的 `com.baijimu.*` 命名空间。
- 至少通过 `services` 或 `serviceRegistrationFiles` 声明一个服务。
- `runtime.startPolicy` 只能是 `automatic` 或 `manual`；权限声明和手动启动策略要求 schema 1.2。
- `icon` 是应用图标的唯一源事实：`mediaType` 必须为 `image/png`，`data` 是不带 URI 前缀和空白的标准 Base64。
  PNG 必须为 `256 × 256`，解码后不超过 `128 KiB`。声明图标时同时要求百积木客户端 `0.2.95` 和
  `connector.presentation.icon.v1`。
- `management.baseUrl` 必须是只包含 origin 的 loopback HTTP 地址，不能包含账号、路径、查询参数或片段。
- management operation 只支持 `GET`、`POST`，路径必须位于 `/management/` 下。
- 内嵌 UI 必须放在独立目录中，入口必须是包内 `.html` 文件，不能使用绝对路径或 `..` 逃逸目录。

## 注册方法和事件

`service-registration.json` 定义远端实际可见的服务。方法应使用明确的输入 JSON Schema，不要接收任意命令、任意文件路径或未约束的 URL。

```json
{
  "name": "localNotes",
  "description": "Read notes stored on this computer.",
  "transport": {
    "type": "http",
    "baseUrl": "http://127.0.0.1:18120"
  },
  "healthCheck": {
    "type": "http",
    "path": "/health",
    "httpMethod": "GET",
    "timeoutSecs": 2,
    "expectStatus": 200
  },
  "startCommand": {
    "type": "shell_command",
    "command": ["example-connector", "start"],
    "timeoutSecs": 20
  },
  "stopCommand": {
    "type": "shell_command",
    "command": ["example-connector", "stop"],
    "timeoutSecs": 20
  },
  "methods": [
    {
      "name": "listNotes",
      "description": "List notes visible to the configured local account.",
      "path": "/invoke/listNotes",
      "httpMethod": "POST",
      "timeoutSecs": 10,
      "input_schema": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "default": 20
          }
        },
        "additionalProperties": false
      }
    }
  ],
  "events": [
    {
      "name": "noteChanged",
      "description": "Emitted after a local note changes.",
      "enabled": true,
      "payload_schema": {
        "type": "object",
        "required": ["noteId", "occurredAt"],
        "properties": {
          "noteId": {
            "type": "string"
          },
          "occurredAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "additionalProperties": false
      }
    }
  ],
  "replace": true,
  "managed_by": "example-connector"
}
```

Connector 进程必须只监听 `127.0.0.1` 或 `::1`。`healthCheck` 通过后，Bridge Agent 才会把相应服务作为可用能力上报。启动命令运行时，客户端会提供：

- `BAIJIMU_CONNECTOR_DATA_DIR`：应用私有数据目录，用于配置、状态和本机管理凭证。
- `BAIJIMU_CONNECTOR_START_POLICY`：清单中的 `automatic` 或 `manual`。

不要把用户数据、密钥或可变状态写回 Connector 安装目录；同步和升级会替换安装副本。

## 本机管理接口和内嵌 UI

### 图标和应用基础信息由宿主展示

百积木客户端会从同一份 `connector.json.icon` 在应用市场、已安装应用卡片、应用详情和升级确认中展示图标。
内嵌 UI 不要再放置应用图标，也不要按应用 ID 或名称维护第二份图标。应用没有自定义图标时，客户端会显示
通用占位图形。

内嵌 UI 加载在应用详情中 `ui.title` 对应的一级标签页内，并不是独立窗口。客户端外壳已经展示应用图标、
名称、类型、版本、能力数、描述、运行状态，以及更新、启动、停止、卸载和“概览/能力/配置”导航。因此
个性化主页必须直接从应用自己的状态、内容和操作开始：

- 不重复应用图标、应用名、版本、能力数、“已安装应用”等 Hero 基础信息。
- 不复制检查更新、启动、停止和卸载操作；这些动作由客户端统一负责。
- `ui.title` 使用“管理”“设置”“工作台”等简短名词，不写“应用名 + 管理”。
- 少量管理域默认使用纵向业务分区和卡片，放在同一页面。只有每个子视图都足够复杂且需要互斥切换时才用
  二级 tab，而且视觉层级必须弱于宿主一级 tab。
- 有明确先后关系的安装或初始化流程使用步骤条或进度列表，不使用 tab；tab 只表示可自由切换的平级内容。
- 二级 tab 必须实现 `tablist`、`tab`、`tabpanel`、`aria-selected`、`aria-controls` 和左右方向键操作。
- 页面从 320px 宽度起可用，不绘制第二层全窗口应用外壳，不使用固定视口高度制造双重滚动。业务 section
  从 `h2` 开始，应用名称的 `h1` 语义由宿主承担。

确有二级 tab 时，结构至少应满足以下语义，并在脚本中补齐左右方向键、Home、End 和焦点同步：

```html
<nav role="tablist" aria-label="设置分类">
  <button id="account-tab" role="tab" aria-selected="true" aria-controls="account-panel">账号</button>
</nav>
<section id="account-panel" role="tabpanel" aria-labelledby="account-tab">
  <h2>账号连接</h2>
</section>
```

页面可以展示应用专有的初始化、连接、账号、同步、诊断和修复操作。“刷新状态”只能刷新页面拥有的业务
状态，不替代客户端生命周期刷新。

内嵌页面不能直接调用 Tauri 命令、Relay、文件系统或任意本机 HTTP 地址。页面只能调用 `connector.json` 中已经声明的 management operation：

```js
const state = await window.baijimuLocalApp.invoke("runtimeState");

await window.baijimuLocalApp.invoke("chooseNotesDirectory", {
  path: "/Users/me/Documents/Notes"
});
```

Bridge Agent 会读取应用私有目录中的 `management-token`，并以 `Authorization: Bearer ...` 调用声明的 loopback management API。Connector 应在首次启动时生成至少 32 个字符的高熵 token，以仅当前用户可读的权限保存；页面代码和 management 响应都不能返回这个 token。

management API 用于本机配置、状态、授权和诊断，不能注册为 Relay 方法。所有来自页面的 payload 仍是不可信输入，Connector 必须进行 schema、路径和权限校验。

## 本地安装和调试

最终用户优先使用百积木客户端的“应用 > 安装应用 > 自定义安装”。完整安装步骤见[安装本地应用](/local-client/local-app-installation/)。

开发期间可以从项目目录安装。CLI 安装会执行本机程序，确认已经审查源代码后再显式接受非市场来源：

```bash
bridge-agent connector install /absolute/path/to/example-connector \
  --replace \
  --accept-untrusted

bridge-agent connector show com.example.connector.notes
bridge-agent connector start com.example.connector.notes
bridge-agent connector list
```

每次安装都会复制项目内容。修改源码后需要重新安装或在百积木客户端中执行同步，原目录变化不会直接修改已安装副本。

至少完成下面的验收：

1. `connector.json` 和服务注册文件可以解析。
2. 安装后命令能从复制后的包目录启动，而不是依赖源码目录。
3. 进程只监听 loopback，健康检查从不可用变为可用。
4. 每个方法分别覆盖成功、非法参数、无权限、超时和依赖不可用。
5. 停止后进程退出，健康检查变为不可用；再次启动不产生重复进程。
6. 内嵌 UI 只能调用已声明的 management operation。
7. Connector 重启或升级后，私有数据目录中的配置仍然有效。
8. Bridge Agent 断线重连后，健康服务、方法和事件可以重新上报。

## 打包固定版本

正式版本使用语义版本号，例如 `0.1.0`。`connector.json`、Git 标签、压缩包文件名和市场版本必须一致。

发布包可以直接以 Connector 目录为根，也可以只包含一个顶层目录。包内必须只有一个可识别的 `connector.json`。不要打包：

- `.git`、构建缓存、测试数据和日志。
- 明文 token、cookie、用户配置或本地数据库。
- 只适用于开发机的绝对路径。
- 没有为目标平台构建的二进制。

生成压缩包后计算 SHA-256，并把包上传到不可变的 HTTPS 地址：

```bash
shasum -a 256 example-connector-0.1.0.zip
```

同一个版本号和下载地址不得覆盖。修复后发布新版本，并重新计算校验值。

## 发布到本地应用市场

先查看当前 CLI 的实际能力：

```bash
baijimu --version
baijimu local-app --help
baijimu local-app version create --help
baijimu auth status --verify
```

创建应用定义时，`id` 和 `connectorId` 都必须稳定且唯一；第三方 `connectorId` 不能使用 `com.baijimu.*`。平台只能是 `macos`、`windows`、`linux`，风险等级只能是 `low`、`medium`、`high`。

```bash
baijimu local-app create \
  --id example-notes \
  --connector-id com.example.connector.notes \
  --name "Example Notes Connector" \
  --description "Read notes stored on this computer." \
  --publisher "Example" \
  --risk-level medium \
  --platform macos \
  --platform windows
```

应用定义与版本是两个资源。创建不可变版本时必须提交语义版本、HTTPS 制品地址、SHA-256 和 manifest；`sourceType` 只能是 `https` 或 `archive`：

```bash
baijimu local-app version create example-notes \
  --version 0.1.0 \
  --source-type archive \
  --source https://downloads.example.com/example-connector-0.1.0.zip \
  --repo example/example-notes-connector \
  --revision v0.1.0 \
  --checksum 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef \
  --manifest @connector.json \
  --capability notes.read
```

示例 checksum 只是格式占位，发布时必须替换为实际压缩包的 SHA-256。创建版本后读取应用，确认服务端保存的版本、来源、校验值和 manifest 与发布物一致，再提交审核：

```bash
baijimu local-app get example-notes --json
baijimu local-app submit example-notes 0.1.0 --json
baijimu local-app publications --json
```

提交后状态进入 `PENDING_REVIEW`。审核通过前不要把版本描述为已发布；只有审核通过并能从市场安装，才算完成交付。

## 发布后的完整验证

1. 在一台没有源码目录和开发依赖的设备上，从市场安装目标版本。
2. 核对下载来源、SHA-256、`connectorId` 和版本。
3. 完成系统权限和应用内配置，启动 Connector。
4. 确认健康检查、方法、事件和内嵌 UI 都可用。
5. 从已授权工作区发起一次真实调用，并在 Bridge Agent 与 Connector 两侧核对日志。
6. 安装前一版本，再执行同步或升级，确认配置迁移和回滚说明有效。
7. 卸载后确认进程、服务注册和安装包已移除，用户明确保留的数据符合产品说明。

## 常见问题

### 提示没有声明服务

`connector.json` 必须包含非空 `services`，或通过 `serviceRegistrationFiles` 引用有效的服务注册文件。

### 安装后找不到启动命令

确认目标平台二进制位于 `bin/<平台>-<架构>/`，或已在 `package.json` 的 `bin`、`pyproject.toml` 的 `[project.scripts]` 中声明。还要确认压缩包里包含构建产物，并且命令具有执行权限。

### 服务启动了但平台看不到方法

依次检查健康检查、服务名和方法名、HTTP 路径、Bridge Agent 连接状态、设备与工作区授权。健康检查不通过的服务不会作为可用能力上报。

### 内嵌页面调用失败

确认 operation 名称同时存在于 `management.operations` 和页面的 `window.baijimuLocalApp.invoke()` 调用中，management 服务只监听 loopback，并且私有数据目录中存在权限安全的 `management-token`。

### 本地可用但安装包不可用

通常是启动命令依赖源码目录、开发机 PATH、未打包的动态库或绝对路径。必须在干净设备上从最终压缩包重新安装验证，不能只测试源码目录。
