---
title: "开发文档"
description: "Kasumi 双端 WebView 框架开发文档"
---

> 项目名称：Kasumi（宿主侧标识符前缀 `KSM`）  
> 适用范围：Windows（C++/WebView2）、Android（Kotlin/WebView）  
> 读者：框架维护者、Windows/Android/前端开发者  
> 本文档描述项目约束和实现规范。文中“必须/禁止”为硬性要求，“建议/可以”为推荐或可选做法。

---

## 0. 实现约定

0. 实现前必须阅读 `https://blog.miomoe.cn/docs/agents.mdx`。该文档定义 Docs Agent 的鉴权方式、接口约束与写入规范；若任务涉及线上文档站读写，必须优先遵循该文档。本文档与 agents.mdx 冲突时，以 agents.mdx 为准。
1. 标注“必须”的条款，生成代码时必须满足，不得变通。
2. 标注“禁止”的条款，不得违反。
3. 标注“建议”的条款，如不采用，需要在代码注释或提交信息中说明理由。
4. 标注“可以”的条款为可选做法。
5. 遇到本文档未覆盖的问题，先查阅 `protocol/commands.schema.json` 和 `protocol/errors.json`；仍无法确定时，在代码中留 `// TODO(protocol):`，不要自行发明协议。
6. 生成双端代码前，先确认命令契约已在 `protocol/` 中定义。
7. 宿主侧（C++/Kotlin/TypeScript）标识符必须遵守 §10.5 的 `KSM` 前缀规范；协议字符串不得加前缀。
8. 未更新契约时，不得单方面修改 Windows 或 Android 任一端的命令行为。

---

## 1. 项目定位

### 1.1 一句话定位

**Kasumi** 是专为 **Windows + Android** 双端设计的「一套 Web 前端、双端原生独立壳」跨平台应用框架。

项目在文档、目录、应用名中一律以 `Kasumi` 指代；宿主侧（C++ / Kotlin / TypeScript）的可命名标识符一律使用 `KSM` 前缀，落地形态见 §10.5。

### 1.2 核心原则

| 编号 | 原则 | 说明 |
|---|---|---|
| P1 | 协议统一，实现分离 | 命令签名双端一致，底层各写各的 |
| P2 | 前端零平台感知 | 业务代码不得出现平台分支 |
| P3 | 原生层只提供原子能力 | 原生层不得承载业务逻辑 |
| P4 | 权限原生强制 | 权限校验必须在 C++ 和 Kotlin 两侧实现 |
| P5 | 不做多余平台 | 仅支持 Windows + Android |
| P6 | 不引入中间 runtime | 不得引入 Rust / Node / JVM 之外的运行时 |
| P7 | 内置 API 少而精 | 只覆盖高频系统能力 |
| P8 | 入口唯一 | 宿主侧能力必须全部经 `KSM` 根入口暴露（§10.5），标识符统一 `KSM` 前缀 |

### 1.3 明确不做的事

- 禁止支持 macOS / Linux / iOS
- 禁止追求「一份原生代码跑双端」
- 禁止在前端暴露真实文件路径
- 禁止使用 Android `addJavascriptInterface`
- 禁止在 JSON 消息中传输大二进制（> 64KB）

### 1.4 项目命名与标识符前缀

| 维度 | 约定 | 示例 |
|---|---|---|
| 项目名 | `Kasumi` | 文档标题、仓库名、应用名、Windows 数据目录 |
| 前端包名 | `@kasumi/*` | `@kasumi/bridge`、`@kasumi/api` |
| CLI 命令 | `ksm` | `ksm dev` / `ksm build` / `ksm run` |
| Android 包名 | 反写域名，且必须包含 `kasumi`，默认 `dev.kasumi.app` | `dev.kasumi.app` |
| 宿主类 / 结构体 / 类型 | `KSM` 前缀 + 领域名（PascalCase） | `KSMBridge`、`KSMFs`、`KSMPermission` |
| 调用形态 | `KSM::<子模块>::<动作>` | `KSM::Fs::ReadText(...)` |
| 协议命令 / 事件 / 权限名 | **不加前缀**，`<domain>.<action>` | `fs.readText`、`window.resized` |
| Windows 数据目录 | `%APPDATA%/Kasumi/...` | `%APPDATA%/Kasumi/data` |

**约束**：

- 协议字符串不得带 `KSM` 前缀。命令名是跨端契约，前缀会污染 JSON 载荷、日志检索与契约测试断言。
- 除协议字符串外的宿主侧命名必须全部走 `KSM` 前缀，形态见 §10.5。
- 前端业务层必须通过 `@kasumi/*` 与 `KSM.*` 访问能力，不得直接引用宿主注入对象。
- Android `applicationId` 必须包含 `kasumi`，默认 `dev.kasumi.app`。

---

## 2. 技术栈与版本约束

### 2.1 Windows 端

| 项 | 约束 |
|---|---|
| 语言 | C++20 |
| 编译器 | MSVC 2022 (v143) 或更高 |
| 构建 | CMake ≥ 3.25 + vcpkg |
| 渲染 | WebView2 (Evergreen Runtime) |
| UI | Win32 API（不使用 MFC/Qt） |
| 内存 | RAII + `ComPtr`，禁止裸 `new`/`delete` |

### 2.2 Android 端

| 项 | 约束 |
|---|---|
| 语言 | Kotlin 1.9+ |
| minSdk | 24 |
| targetSdk | 34+ |
| 渲染 | 系统 WebView + `androidx.webkit` |
| 桥接 | `WebViewCompat.addWebMessageListener` |
| 异步 | Kotlin Coroutines |
| 资源加载 | `WebViewAssetLoader` |

### 2.3 前端

| 项 | 约束 |
|---|---|
| 语言 | TypeScript 5.x（strict 模式） |
| 构建 | Vite |
| 包管理 | pnpm |
| 目标 | ES2020+ |

### 2.4 协议

| 项 | 约束 |
|---|---|
| 消息格式 | JSON（UTF-8） |
| 协议风格 | JSON-RPC 2.0 简化变体 |
| 契约源 | `protocol/commands.schema.json` |
| 错误源 | `protocol/errors.json` |

---

## 3. 目录结构

```
/
├── protocol/                      # 唯一事实源
│   ├── commands.schema.json       # 所有命令契约
│   ├── errors.json                # 错误码全集
│   ├── permissions.schema.json    # 权限声明
│   ├── README.md                  # 协议与权限说明
│   └── codegen/                   # 代码生成脚本
├── frontend/                      # 统一 Web 前端（包名 @kasumi/*）
│   ├── src/
│   │   ├── bridge/                # invoke / event / capabilities（KSM 根入口）
│   │   ├── api/                   # 内置 API 的 TS 封装
│   │   └── app/                   # 业务代码
│   └── package.json
├── windows/                       # C++ / WebView2 宿主
│   ├── src/
│   │   ├── main.cpp
│   │   ├── bridge/                # 协议解析 / 路由 / 回调
│   │   ├── commands/              # 内置命令实现
│   │   ├── permissions/           # 权限校验
│   │   └── platform/              # Win32 封装
│   ├── CMakeLists.txt
│   └── vcpkg.json
├── android/                       # Kotlin / WebView 宿主
│   ├── app/src/main/
│   │   ├── java/.../bridge/       # 协议解析 / 路由 / 回调
│   │   ├── java/.../commands/     # 内置命令实现
│   │   ├── java/.../permissions/  # 权限校验
│   │   └── assets/                # 前端构建产物
│   └── build.gradle.kts
├── cli/                           # 统一 dev/build/run 工具（命令名 ksm）
├── tests/
│   ├── contract/                  # 双端契约测试（JS 用例）
│   └── e2e/
└── DEVELOPMENT.md                 # 本文档
```

**约束**：

- `protocol/` 必须是唯一事实源，任何命令变更先改这里。
- `windows/` 和 `android/` 不得互相引用。
- `frontend/` 不得出现 `if (platform === 'windows')` 之类的分支。
- Android `applicationId` 必须包含 `kasumi`，默认 `dev.kasumi.app`。

---

## 4. 协议规范（核心）

### 4.1 消息类型

共四类：**请求**、**响应**、**事件**、**取消**。

#### 4.1.1 请求（前端 → 原生）

```json
{
  "type": "request",
  "id": "req-0001",
  "cmd": "fs.readText",
  "args": { "path": "app://data/config.json" },
  "timeoutMs": 5000,
  "windowId": "win-1"
}
```

- `id`：必须全局唯一，格式 `req-<递增数字>`。
- `cmd`：必须在 `commands.schema.json` 中已定义。
- `args`：必须符合该命令的 schema。
- `timeoutMs`：可以省略，默认 10000。
- `windowId`：可以省略。多窗口场景下，原生必须用它隔离请求路由表；单窗口可忽略。

#### 4.1.2 响应（原生 → 前端）

成功：

```json
{ "type": "response", "id": "req-0001", "ok": true, "data": { ... } }
```

失败：

```json
{
  "type": "response",
  "id": "req-0001",
  "ok": false,
  "error": { "code": "PERMISSION_DENIED", "message": "...", "detail": { ... } }
}
```

- 一次请求必须只返回一次响应。
- `error.code` 必须属于 `errors.json` 全集。

#### 4.1.3 事件（原生 → 前端）

```json
{ "type": "event", "event": "window.resized", "payload": { "width": 800, "height": 600 } }
```

- 事件不得携带 `id`。
- 事件名必须在 `commands.schema.json` 的 `events` 段中定义。
- 事件必须只推送给已订阅的前端上下文。前端必须通过 `event.subscribe` 显式订阅。
- 原生必须维护每个窗口/WebView 的订阅表，WebView 销毁时自动清空。

#### 4.1.4 取消（前端 → 原生）

```json
{ "type": "cancel", "id": "req-0001" }
```

- 原生收到后应尽快终止任务，并返回 `CANCELLED` 响应。
- 若任务已完成，必须忽略取消。

### 4.2 错误码全集

`protocol/errors.json` 至少包含：

| code | 含义 |
|---|---|
| `INVALID_ARGS` | 参数不符合 schema |
| `UNKNOWN_COMMAND` | 命令未注册 |
| `PERMISSION_DENIED` | 权限拒绝 |
| `NOT_SUPPORTED` | 当前平台不支持 |
| `NOT_FOUND` | 资源不存在 |
| `IO_ERROR` | IO 失败 |
| `CANCELLED` | 被取消 |
| `TIMEOUT` | 超时 |
| `BUSY` | 资源忙 |
| `INTERNAL_ERROR` | 内部错误 |

**约束**：

- 双端必须使用完全相同的 code 字符串。
- 错误 `message` 可以不同，但必须是英文，便于日志检索。
- `detail` 结构由各命令自定义，可以省略。

### 4.3 命令契约格式

`commands.schema.json` 中每个命令：

```json
{
  "fs.readText": {
    "args": {
      "type": "object",
      "properties": { "path": { "type": "string" } },
      "required": ["path"]
    },
    "result": {
      "oneOf": [
        { "type": "string" },
        { "type": "object", "properties": { "file": { "type": "string" } }, "required": ["file"] }
      ]
    },
    "permissions": ["fs.read"],
    "platforms": ["windows", "android"],
    "since": "<兼容性标记>"
  }
}
```

- `platforms` 标注支持平台，任一平台缺失时，该端必须返回 `NOT_SUPPORTED`。
- 新增命令必须更新兼容性标记字段。
- `result` 若可能返回大文件句柄，必须使用联合类型 `string` 或 `{ file: string }`。

### 4.4 二进制与大数据

- 单条消息必须 ≤ 64KB。
- 超过 64KB 的数据必须走临时文件：命令返回 `{ "file": "app://tmp/xxx" }`，前端再读取。
- 图片、音视频不得走 base64 内联。
- `fs.readText` 等命令若结果超过 64KB，必须返回 `{ "file": "app://tmp/xxx" }`，前端必须能处理联合类型。

### 4.5 超时与重试

- 原生收到请求后必须在 `timeoutMs` 内返回响应或 `TIMEOUT`。
- 前端不得自动重试非幂等命令。

### 4.6 事件订阅

前端必须通过以下命令订阅/退订事件：

```json
{ "cmd": "event.subscribe", "args": { "event": "window.resized" } }
{ "cmd": "event.unsubscribe", "args": { "event": "window.resized" } }
```

- `event.subscribe` 必须双端实现。
- 同一事件重复订阅必须幂等。
- 原生必须只向已订阅的 WebView 推送事件。
- `webview.reloaded` 为系统事件，原生必须在 WebView 加载完成后主动推送，无需前端订阅。

---

## 5. 权限模型

### 5.1 声明

权限在 `protocol/permissions.schema.json` 中定义：

```json
{
  "fs.read": {
    "description": "读取沙箱内文件",
    "params": { "path": "app://data/**" }
  },
  "dialog.openFile": { "description": "打开文件选择器" },
  "notification.show": { "description": "显示系统通知", "runtime": "android" }
}
```

- `runtime` 可以标注需要运行时授权的平台。

### 5.2 校验时机

- 必须在原生层路由到命令实现之前校验。
- 不得仅依赖前端声明的权限。
- 校验失败必须返回 `PERMISSION_DENIED`，不得返回 `NOT_SUPPORTED`。

### 5.3 校验粒度

- 权限必须绑定到「命令 + 参数」。
- 例如 `fs.read` 必须校验 `path` 是否落在允许前缀内。
- 路径校验必须先拒绝危险输入（含 `..`、`~`、绝对路径、空字节），再做规范化（resolve）与前缀校验。顺序不可颠倒。
- 若先规范化再拒绝，可能把 `app://res/../data/x` 解析为 `app://data/x` 而误放行。

### 5.4 能力查询

前端可以调用：

```json
{ "cmd": "capabilities.query", "args": {} }
```

返回：

```json
{
  "platform": "windows",
  "commands": ["fs.readText", "dialog.message"],
  "permissions": ["fs.read"]
}
```

- 该命令必须双端实现。
- 前端建议在启动时查询并缓存。

### 5.5 运行时权限请求

需要运行时授权的权限（如 Android 通知），前端必须调用：

```json
{ "cmd": "permission.request", "args": { "permission": "notification.show" } }
```

- 原生必须弹出系统授权 UI，并返回 `{ "granted": true | false }`。
- 若用户拒绝，后续调用相关命令必须返回 `PERMISSION_DENIED`。
- `permission.request` 必须双端实现；Windows 端若无需运行时授权，可直接返回 `{ "granted": true }`。

---

## 6. 路径与资源模型

### 6.1 虚拟路径

前端必须只使用虚拟路径：

| 虚拟路径 | 含义 | 可写 |
|---|---|---|
| `app://data/**` | 应用私有数据目录 | 是 |
| `app://cache/**` | 缓存目录 | 是 |
| `app://tmp/**` | 临时目录 | 是 |
| `app://res/**` | 只读资源（打包内） | 否 |
| `app://assets/**` | 前端构建产物 | 否 |

### 6.2 映射规则

| 虚拟路径 | Windows | Android |
|---|---|---|
| `app://data` | `%APPDATA%/Kasumi/data` | `filesDir/data` |
| `app://cache` | `%LOCALAPPDATA%/Kasumi/cache` | `cacheDir` |
| `app://tmp` | `%TEMP%/Kasumi` | `cacheDir/tmp` |
| `app://res` | 可执行文件同目录 `res/` | `assets/res` |
| `app://assets` | 内嵌资源目录 | `assets/web` |

**约束**：

- 双端映射必须在原生层完成，前端无感。
- 映射结果不得通过任何 API 暴露给前端。
- 真实路径与虚拟路径的转换函数必须集中在一处，便于审计。

### 6.3 资源加载

- Windows：`SetVirtualHostNameToFolderMapping("app.local", ...)`。
- Android：`WebViewAssetLoader` 映射 `https://app.local/`。
- 开发模式：加载 `http://localhost:5173`。
- 生产模式：加载 `https://app.local/index.html`。

---

## 7. 生命周期与状态恢复

### 7.1 WebView 创建

- Windows：主窗口创建后初始化 WebView2，加载入口 URL。
- Android：`Activity.onCreate` 中初始化 WebView。

### 7.2 WebView 重建

前端必须处理 `webview.reloaded` 事件：

- 原生在 WebView 完成加载后必须主动推送一次该事件。
- 前端收到后必须重新调用 `capabilities.query` 并重建状态。
- 前端必须重新发送 `event.subscribe` 订阅所需事件。

### 7.3 未完成请求

- WebView 销毁时，所有未完成请求必须被原生丢弃。
- 前端不得依赖跨重建的请求 ID。

### 7.4 Android 特有

- 旋转屏幕：必须由 `configChanges` 处理，避免重建 WebView。
- 后台：WebView 可能被回收，关键状态必须由原生持久化。
- 低内存：必须实现 `onTrimMemory` 清理缓存。

### 7.5 Windows 特有

- 窗口关闭：必须先取消所有未完成命令，再销毁 WebView2。
- 多窗口：必须为每个窗口维护独立的请求路由表；请求中的 `windowId` 用于路由。
- `KSM` 根入口必须通过 `windowId` 或上下文对象区分窗口，不得假设全局单窗口。

---

## 8. 线程与并发模型

### 8.1 通用模型

```
[WebView 线程/主线程]
        │ 接收消息
        ▼
[桥接层] ── 解析、权限校验、路由
        │
        ▼
[命令执行线程池] ── 实际执行
        │
        ▼
[WebView 线程/主线程] ── 回发响应
```

- 命令必须在后台线程执行。
- 响应必须回到 WebView 所属线程再发送。
- 单条命令不得阻塞桥接层接收下一条消息。

### 8.2 并发上限

- 同时执行的命令建议 ≤ 8。
- 超出时建议排队，队列满则返回 `BUSY`。

### 8.3 取消

- 长任务必须支持取消。
- 取消信号必须能传递到执行线程。

### 8.4 Windows 具体

- WebView2 调用必须在创建它的 UI 线程。
- 使用 `PostWebMessageAsJson` 回发。
- 线程池用 `std::thread` 或 Windows ThreadPool。

### 8.5 Android 具体

- WebView 调用必须在主线程。
- 使用 `Dispatchers.Main` 回发。
- 任务用 `Dispatchers.IO` 或 `Dispatchers.Default`。

---

## 9. 安全规范

### 9.1 通用

- 必须设置 CSP，禁止 `unsafe-eval`、`unsafe-inline`。
- 必须拦截导航：非 `app.local` / `localhost` 的 URL 必须交给系统浏览器。
- 必须拦截 `window.open`，行为同上。
- 生产环境禁止在 WebView 中启用调试。
- 外部跳转必须经过白名单校验，白名单在 `protocol/commands.schema.json` 的 `shell.openExternal` 契约中声明。

### 9.2 Windows

- 必须关闭 WebView2 不需要的宿主对象。
- 必须校验 `WebMessageReceived` 来源。
- 生产环境必须使用 `AreDevToolsEnabled=false`。

### 9.3 Android

- 禁止使用 `addJavascriptInterface`。
- 必须使用 `WebViewCompat.addWebMessageListener` 并限制 `allowedOriginRules`。
- 必须设置 `setAllowFileAccess(false)`、`setAllowContentAccess(false)`。
- 必须设置 `setMixedContentMode(MIXED_CONTENT_NEVER_ALLOW)`。
- 通知权限必须走 Android 13+ 运行时权限，并通过 `permission.request` 触发。

### 9.4 路径

- 必须先拒绝包含 `..`、`~`、绝对路径、空字节的输入。
- 必须在拒绝危险输入后，再做规范化与前缀校验。
- 必须限制单文件读写大小（默认 16MB）。

---

## 10. 双端实现规范

### 10.1 命令注册

- Windows：使用宏或注册表注册命令处理器。
- Android：使用注解或注册表注册命令处理器。
- 两端的注册表必须由 `protocol/commands.schema.json` 生成或校验。

### 10.2 命令实现模板

每个命令实现必须：

1. 校验参数（用生成的 schema 校验器）。
2. 校验权限。
3. 执行。
4. 返回标准结构。
5. 捕获所有异常并转为标准错误码。

### 10.3 平台差异

| 场景 | 处理 |
|---|---|
| 平台不支持 | 返回 `NOT_SUPPORTED` |
| 语义可映射 | 映射为统一语义（如返回虚拟句柄） |
| 无法映射 | 返回 `NOT_SUPPORTED`，并在 `capabilities.query` 中排除 |

### 10.4 命名

协议层（跨端契约，不得加 `KSM` 前缀）：

- 命令名：`<domain>.<action>`，全小写驼峰。
- 事件名：`<domain>.<event>`，全小写。
- 权限名：`<domain>.<action>`，与命令名对齐。

宿主层（C++ / Kotlin / TypeScript，必须加 `KSM` 前缀）：见 §10.5。

### 10.5 KSM 前缀落地形态

统一入口是名为 `KSM` 的根类，能力以子模块挂在其上，调用形如 `KSM::Fs::ReadText(...)`（C++）/ `KSM.fs.readText(...)`（Kotlin、TS）。命令入口不得散落在全局函数或无前缀裸类上。

#### 10.5.1 各语言形态对照

| 项 | C++（Windows） | Kotlin（Android） | TypeScript（前端） |
|---|---|---|---|
| 根入口 | `struct KSM`（静态聚合，禁止实例化） | `object KSM` | `class KSM`（静态成员） |
| 子模块 | 嵌套 `struct KSM::Fs`、`struct KSM::Dialog` | `object KSMFs`、`object KSMDialog` | `class KSMFs`、`class KSMDialog` |
| 挂载名 | PascalCase 嵌套类型 `Fs` | camelCase 属性 `fs` | camelCase 静态成员 `fs` |
| 调用形态 | `KSM::Fs::ReadText(path)` | `KSM.fs.readText(path)` | `KSM.fs.readText(path)` |
| 命令处理器 | `class KSMFsReadTextHandler` | `class KSMFsReadTextHandler` | — |
| 基础设施 | `KSMRouter`、`KSMPermission`、`KSMCommandRegistry` | `KSMRouter`、`KSMPermission`、`KSMCommandRegistry` | `KSMTransport`、`KSMBus` |
| 文件命名 | `ksm_<domain>.h` / `.cpp` | `KSM<Domain>.kt` | `ksm/<domain>.ts` |
| 常量 / 枚举 | `kKsmMaxMessageBytes`、`enum class KSMMode` | `const val KSM_MAX_MESSAGE_BYTES`、`enum class KSMMode` | `const KSM_MAX_MESSAGE_BYTES` |
| 日志 tag | `KSM` | `KSM` | `KSM` |
| 局部 / 参数 / 数据字段 | camelCase，不带前缀 | camelCase，不带前缀 | camelCase，不带前缀 |

**约束**：

- 根入口必须只有一个 `KSM`，不得出现 `KSMApp` / `KasumiApp` 之类平行入口。
- 子模块名必须为 `KSM` + 领域名：`KSMFs`、`KSMDialog`、`KSMClipboard`、`KSMNotification`、`KSMWindow`。
- 方法名不得二次重复前缀：`KSMFs::ReadText` 正确，`KSMFs::KSMReadText` 错误。
- 全局命名空间不得出现自由函数，工具方法必须收进 `KSM` 或 `KSM<Domain>` 的静态成员。
- 局部变量、参数、JSON 数据字段不得带 `KSM` 前缀。

#### 10.5.2 根入口契约

| 成员 | C++ | Kotlin / TS | 说明 |
|---|---|---|---|
| 生命周期 | `KSM::Init()` / `KSM::Shutdown()` | `KSM.init()` / `KSM.shutdown()` | 建/销桥接层与请求表 |
| 统一调用 | `KSM::Invoke(windowId, cmd, args)` | `KSM.invoke(cmd, args)` | 走 §4.1.1 请求结构 |
| 事件订阅 | `KSM::On(event, handler)` | `KSM.on(event, handler)` | 走 §4.1.3 |
| 运行时信息 | `KSM::Get()` | `KSM.get()` | 返回 `capabilities.query` 的缓存结果（§5.4） |

`KSM::Get()` 必须返回缓存值，不得每次调用都发一次 `capabilities.query`。

#### 10.5.3 C++ 示例

```cpp
// windows/src/ksm.h
#pragma once

#include <string>
#include <string_view>

// 根入口：只做聚合与生命周期，不承载业务逻辑（P3）
struct KSM final {
    KSM() = delete; // 禁止实例化

    // 子模块：文件系统
    struct Fs final {
        // 只接受虚拟路径；真实路径解析集中在此处，便于审计（§6.2）
        static std::string ReadText(std::string_view virtualPath);
        static void WriteText(std::string_view virtualPath, std::string_view content);
    };

    // 子模块：对话框
    struct Dialog final {
        static void Message(std::string_view title, std::string_view content);
        static bool Confirm(std::string_view title, std::string_view content);
    };

    static void Init();
    static void Shutdown();
    static void Invoke(std::string_view windowId, std::string_view cmd, std::string_view argsJson);
};
```

```cpp
// 调用侧
auto text = KSM::Fs::ReadText("app://data/config.json");
```

#### 10.5.4 Kotlin 示例

```kotlin
// android/.../bridge/KSM.kt
object KSM {
    val fs = KSMFs
    val dialog = KSMDialog

    fun init() { /* 注册 WebMessageListener 与请求表 */ }
    fun shutdown() { /* 丢弃未完成请求，见 §7.3 */ }

    fun invoke(windowId: String?, cmd: String, args: Map<String, Any?>): Any? {
        // 走原生路由，不经过前端 transport
        return KSMRouter.route(windowId, cmd, args)
    }
}
```

```kotlin
// android/.../commands/KSMFs.kt
// 宿主侧命令实现：直接实现文件读写，不通过 KSM.invoke 递归调用
object KSMFs {
    suspend fun readText(context: Context, virtualPath: String): String {
        KSMPermission.require(context, "fs.read", mapOf("path" to virtualPath))
        val realPath = KSMPath.resolve(context, virtualPath)
        return File(realPath).readText()
    }

    suspend fun writeText(context: Context, virtualPath: String, content: String) {
        KSMPermission.require(context, "fs.write", mapOf("path" to virtualPath))
        val realPath = KSMPath.resolve(context, virtualPath)
        File(realPath).writeText(content)
    }
}
```

#### 10.5.5 TypeScript 示例

```ts
// frontend/src/bridge/ksm.ts
import { KSMBus } from './ksm/bus';
import { KSMFs } from './ksm/fs';
import { KSMDialog } from './ksm/dialog';
import { KSMTransport } from './ksm/transport';

export class KSM {
  private static readonly transport = new KSMTransport();
  private static readonly bus = new KSMBus();

  static readonly fs = new KSMFs(KSM.transport);
  static readonly dialog = new KSMDialog(KSM.transport);

  static invoke<TResult>(cmd: string, args?: Record<string, unknown>): Promise<TResult> {
    return KSM.transport.send<TResult>(cmd, args);
  }

  static on(event: string, handler: (payload: unknown) => void): () => void {
    return KSM.bus.subscribe(event, handler);
  }
}
```

```ts
// frontend/src/bridge/ksm/fs.ts
export class KSMFs {
  constructor(private readonly transport: KSMTransport) {}

  readText(path: string): Promise<string | { file: string }> {
    return this.transport.send<string | { file: string }>('fs.readText', { path });
  }
}
```

```ts
// 调用侧
const result = await KSM.fs.readText('app://data/config.json');
if (typeof result === 'string') {
  console.log(result);
} else {
  console.log('大文件句柄:', result.file);
}
```

反例：

```ts
// 错误：绕过 KSM 根入口直连宿主注入对象
(window as unknown as { chrome: { webview: { postMessage: (v: unknown) => void } } })
  .chrome.webview.postMessage({ cmd: 'fs.readText' });

// 错误：宿主侧类型不带 KSM 前缀，跨端符号审计失效
class FsApi { /* ... */ }
```

---

## 11. 内置 API 清单

必须实现：

| 命令 | Windows | Android | 说明 |
|---|---|---|---|
| `ping` | 支持 | 支持 | 测试通路 |
| `capabilities.query` | 支持 | 支持 | 能力查询 |
| `app.getInfo` | 支持 | 支持 | 版本、平台、语言 |
| `fs.readText` | 支持 | 支持 | 沙箱内读文本，返回 `string` 或 `{ file }` |
| `fs.writeText` | 支持 | 支持 | 沙箱内写文本 |
| `fs.exists` | 支持 | 支持 | 判断存在 |
| `fs.remove` | 支持 | 支持 | 删除 |
| `dialog.message` | 支持 | 支持 | 提示框 |
| `dialog.confirm` | 支持 | 支持 | 确认框 |
| `dialog.openFile` | 支持 | 支持 | 文件选择，返回虚拟句柄 |
| `dialog.saveFile` | 支持 | 支持 | 保存选择 |
| `clipboard.readText` | 支持 | 支持 | 读剪贴板 |
| `clipboard.writeText` | 支持 | 支持 | 写剪贴板 |
| `notification.show` | 支持 | 支持 | 系统通知 |
| `permission.request` | 支持 | 支持 | 运行时权限请求 |
| `event.subscribe` | 支持 | 支持 | 订阅事件 |
| `event.unsubscribe` | 支持 | 支持 | 退订事件 |
| `window.setTitle` | 支持 | 不支持 | Android 返回 `NOT_SUPPORTED` |
| `window.minimize` | 支持 | 不支持 | 同上 |
| `window.maximize` | 支持 | 不支持 | 同上 |
| `window.close` | 支持 | 支持 | 关闭应用 |
| `app.exit` | 支持 | 支持 | 退出 |

建议实现（后续版本）：

- `fs.readDir`、`fs.mkdir`、`fs.stat`
- `http.download`
- `shell.openExternal`
- `device.getInfo`（Android）
- `window.setSize`（Windows）

---

## 12. 契约测试

### 12.1 目标

保证双端对同一命令的**行为一致**，不只是签名一致。

### 12.2 结构

```
tests/contract/
├── cases/
│   ├── ping.test.ts
│   ├── fs.readText.test.ts
│   └── dialog.message.test.ts
├── helpers.ts
└── runner.ts
```

### 12.3 用例模板

```ts
import { invoke } from '@kasumi/bridge';
import { expectError, expectOk } from './helpers';

test('fs.readText 正常读取', async () => {
  await invoke('fs.writeText', { path: 'app://data/a.txt', content: 'hi' });
  const text = await invoke('fs.readText', { path: 'app://data/a.txt' });
  expect(text).toBe('hi');
});

test('fs.readText 拒绝越权路径', async () => {
  await expectError(
    invoke('fs.readText', { path: 'app://res/../data/x' }),
    'PERMISSION_DENIED'
  );
});
```

### 12.4 执行

- CI 必须同时在 Windows runner 和 Android 模拟器上跑全部用例。
- 任一端失败即视为失败。
- 用例必须覆盖：正常路径、参数错误、权限拒绝、不支持、超时、取消。
- `runner.ts` 必须提供与 `@kasumi/bridge` 一致的 `invoke` 接口，并注入到测试环境。
- 测试环境必须通过环境变量或测试适配器区分 Windows / Android，但用例本身不得出现平台分支。

### 12.5 模糊测试

- 桥接层建议接受畸形 JSON 并返回 `INVALID_ARGS`，不得崩溃。

---

## 13. 编码规范

### 13.1 通用

- 提交前必须通过 linter 和 formatter。
- 禁止提交注释掉的代码。
- 日志必须分级（error / warn / info / debug），生产默认 info。
- 关键路径建议携带 `traceId`，便于跨端日志检索。

### 13.2 C++

- 遵循 C++ Core Guidelines。
- 必须使用 `ComPtr` 管理 COM。
- 必须使用 `std::string` / `std::wstring_view`，避免裸 `wchar_t*`。
- 不得在 UI 线程做阻塞 IO。
- 代码示例优先可读性，命名清晰，避免过度缩写。

### 13.3 Kotlin

- 遵循 Kotlin 官方风格。
- 必须使用 `suspend` 函数处理异步。
- 不得在 `onPostMessage` 中做耗时操作。
- 必须使用 `try/catch` 包裹所有 JNI/SDK 调用。
- 宿主侧 `KSMFs` 等对象必须直接实现命令逻辑，不得通过 `KSM.invoke` 递归调用自身。

### 13.4 TypeScript

- strict 模式，不得使用 `any`（除非有 `// eslint-disable`）。
- 桥接层必须做运行时参数校验。
- 宿主侧类型必须使用 `KSM` 前缀，入口为 `KSM`（§10.5）。
- 业务层不得直接调用 `window.chrome.webview` 或 `window.androidBridge`，必须走 `KSM.invoke` / `KSM.fs.*`。

---

## 14. 开发流程

### 14.1 新增内置命令

1. 在 `protocol/commands.schema.json` 增加命令。
2. 在 `protocol/permissions.schema.json` 增加权限（如需）。
3. 运行 `pnpm codegen` 生成 TS/C++/Kotlin 常量、参数校验器、命令注册表、事件常量、错误码常量、权限常量。
4. 实现 Windows 端 C++ 处理器。
5. 实现 Android 端 Kotlin 处理器。
6. 在 `frontend/src/api/` 增加 TS 封装，并挂到 `KSM` 根入口（§10.5）。
7. 在 `tests/contract/cases/` 增加用例。
8. 在 `protocol/README.md` 更新协议说明。
9. 双端跑契约测试。

不得跳过第 1~3 步。

### 14.2 修改现有命令

- 任何行为变更必须视为破坏性变更，并更新命令的兼容性标记。
- 兼容性策略：新增字段提升次要标记，删除字段提升主要标记。

### 14.3 提交规范

```
feat(protocol): add fs.readDir
feat(windows): implement fs.readDir
feat(android): implement fs.readDir
test(contract): add fs.readDir cases
```

---

## 15. MVP 路径

按顺序执行，不得跳级：

### Phase 1 · 通路
- `ping`
- `capabilities.query`
- 错误结构

### Phase 2 · 权限
- `fs.readText` / `fs.writeText`
- 权限拒绝路径
- 路径沙箱

### Phase 3 · 生命周期
- `webview.reloaded` 事件
- 请求超时
- 请求取消
- `event.subscribe` / `event.unsubscribe`

### Phase 4 · 内置 API
- `dialog.*`
- `notification.show`
- `permission.request`
- `clipboard.*`
- `window.*` / `app.*`

### Phase 5 · 工程化
- CLI
- 契约测试 CI
- 打包签名

---

## 16. 实现红线

1. 禁止在未定义契约的情况下新增命令。
2. 禁止让双端命令名、参数名、错误码不一致。
3. 禁止在前端暴露真实文件路径。
4. 禁止使用 `addJavascriptInterface`。
5. 禁止在 UI 线程执行阻塞操作。
6. 禁止让单条 JSON 消息超过 64KB。
7. 禁止依赖跨 WebView 重建的请求 ID。
8. 禁止在 `windows/` 和 `android/` 之间共享代码。
9. 禁止引入 Rust / Node 中间层。
10. 禁止跳过契约测试。
11. 禁止使用 `any` 绕过类型检查。
12. 禁止在未更新 `protocol/` 的情况下改任一端的命令行为。
13. 禁止定义非 `KSM` 前缀的宿主侧类 / 结构体 / 类型（协议字符串除外）。
14. 禁止绕过 `KSM` 根入口直连 `window.chrome.webview` / `WebViewCompat`。
15. 禁止在协议命令名、事件名、权限名上加 `KSM` 前缀。
16. 禁止在宿主侧命令实现中通过 `KSM.invoke` 递归调用自身。
17. 禁止先规范化路径再拒绝危险输入。

---

## 17. 术语表

| 术语 | 含义 |
|---|---|
| 前端层 | Web 技术栈实现的业务层 |
| 桥接层 | 协议解析、路由、权限校验、回调派发 |
| 原生层 | C++/WebView2 或 Kotlin/WebView |
| 命令 | 前端可调用的原生能力 |
| 事件 | 原生主动推送给前端的消息 |
| 虚拟路径 | `app://` 开头的路径，前端唯一可见路径 |
| 契约 | `protocol/` 下的 schema |
| 契约测试 | 双端跑同一套用例验证行为一致 |
| Kasumi | 项目名称；文档、应用名、Windows 数据目录的正式指代 |
| KSM | 宿主侧标识符前缀与统一调用入口，形如 `KSM::Fs::ReadText`（§10.5） |

---

**文档结束。**

> 实现前检查：  
> 1. 对应契约是否已定义。  
> 2. 权限是否已声明。  
> 3. 是否同时更新了双端与测试。  
> 4. 是否违反第 16 节任意一条。