# Web 接入与运行态调用

使用百积木标准浏览器 SDK 完成平台应用启动、PKCE、会话和 Runtime 方法调用。

环境托管应用必须使用公共 npm 包 `@baijimu/platform-application-sdk` 接入平台。不要在项目中复制
`sdk.js`，也不要由每个平台应用分别实现授权跳转、PKCE、token 存储、CModel 解析和 Runtime URL
拼接。

## 安装标准 SDK

```bash
pnpm add --save-exact @baijimu/platform-application-sdk@7.0.0
```

提交项目锁文件，保证构建和回滚使用已解析的精确依赖版本。SDK 是应用的构建时依赖，不会在目标环境
部署新服务，也不会改变 Bundle 安装内容。

通过 `PLATFORM_APPLICATION` 项目类型新建的项目应已经包含 SDK、`bootstrap()` 入口、部署配置文件和
平台应用声明。若项目中仍存在自实现的 PKCE、token 缓存或 Gateway client，不能只在旁边新增 SDK；应删除
重复实现，让授权生命周期和逐方法调用统一经过 SDK。

## 读取同源环境配置

环境托管应用使用 SDK 的专用初始化入口：

```ts
import { createPlatformApplicationClientFromHostedConfiguration } from "@baijimu/platform-application-sdk";

const client = await createPlatformApplicationClientFromHostedConfiguration(applicationDefinition.id);
```

`applicationDefinition.id` 是当前应用稳定的 `platformAppId`。SDK 只从当前 HTTPS origin 的
`/.well-known/baijimu-platform-application.json` 读取配置，并校验应用身份和精确 origin。
该文件由平台在 Bundle 安装或升级时的托管流程自动提供，开发者无需创建静态站点或手工部署配置文件，也不能让应用构建归档覆盖它。
历史 `STATIC_SPA` 站点需要完成 [环境托管迁移](/development/bundle-development/platform-application-development/create-and-install/#迁移历史外部应用)，只升级 npm 依赖不会生成同源配置。

新应用在独立 origin 的根路径运行，配置不含 `releasePath`。已有 SDK 6 部署继续使用原配置；历史 SDK 6 归档首次安装到新环境需要升级 SDK、重新构建并创建新应用版本。应用版本与 Bundle 安装、升级和回滚不变。

配置字段如下：

| 字段                  | 来源与含义                     |
| ------------------- | ------------------------- |
| `contractVersion`   | 同源配置合同版本，SDK 7 使用 `2.0.0` |
| `platformAppId`     | 当前平台应用的稳定身份               |
| `applicationOrigin` | 该应用在当前环境的 HTTPS origin    |
| `appGatewayApiBase` | 当前环境的 App Gateway API 地址  |
| `authorizationUrl`  | 当前环境的网页授权地址               |
| `managementUrl`     | 可选的当前环境管理入口               |

SDK 拒绝配置重定向、缺失字段、未知字段和身份不匹配，不缓存配置，也不从 URL 参数或公共 SaaS 地址回退。
配置不包含 `installId`、工作区、用户、token 或密钥；真实安装选择器由启动 URL 提供，服务端负责权限校验。
只升级 SDK 不会让外部站点取得平台授权，应用还必须按环境托管方式创建新版本并升级 Bundle。

旧项目应删除 `window.__APP_CONFIG__` 手工地址注入、签名启动初始化和自实现授权逻辑。
`createPlatformApplicationClient(config)` 仅用于显式受控集成或本机测试，不能用它绕过平台托管要求；
外部自托管应用使用自己的后台授权。

## 在业务页面之前启动

```ts
const session = await client.bootstrap();
```

`bootstrap()` 统一执行以下状态机：

```text
读取 installId
  -> 有 code：校验 state 并兑换本次授权 token
  -> 当前 client 已有未过期内存会话：进入应用
  -> 都没有：创建 PKCE 请求并跳转网页授权页
```

SDK 会校验返回 token 的 `workspaceId`、`runtimeAppId`、`installId`、`platformAppId` 和过期时间，
token 只保存在当前 client 实例的内存中。每个页面使用一个 client，同一实例的重复或并发
`bootstrap()` 不重复发起授权。新开页面或刷新必须重新经过平台授权页，以当前平台登录身份兑换
新 token，不能恢复上次打开时的应用身份。SDK 会删除当前应用和安装的旧 localStorage token。

不要把 token 写入 localStorage、sessionStorage、Cookie 或 URL，也不要通过自有缓存跳过 SDK 初始化。
PKCE verifier 和预期 state 仍需在 sessionStorage 中跨授权跳转保存；它们不是应用登录 token。
授权成功后删除 PKCE 临时数据，并从地址栏移除授权参数。
它只移除一次性的 `code` 和 `state`，会保留平台入口要求的 `installId`。业务代码不要再次清空整个查询串。

## 迁移 SDK 6

SDK 6.0.0 取消持久化应用会话。更新依赖和锁文件后，必须重新构建并创建平台应用版本、更新 Bundle 引用，
已打开的旧页面需要重新加载。发布 npm 包本身不会改变已部署的 JavaScript。

旧应用除了更新依赖，还需删除自实现的 token 缓存。SDK 2.x/3.x 的初始化应迁移到同源托管配置入口；
SDK 4.x 的调用需检查 `/invoke` 及结构化 target 的兼容性。目标网关须支持 SDK 5 起采用的领域调用协议。

新开或刷新会重新核验平台当前身份。已打开应用不会因另一个域名的平台账号切换自动收到通知；
即时退出和账号切换联动需要平台的可信会话失效机制，不得信任 URL 或任意跨域消息中传来的 userId。
`clearSession()` 只清客户端状态，服务端撤销使用 `revoke()`。

验收至少覆盖：浏览器已有账号 A 的旧应用缓存、平台切换为账号 B、重新打开/刷新应用后接口身份为 B；
同时验证 PKCE 回跳、并发初始化、token 过期和授权途中退出。SDK 不自动重放失败的业务请求。

## 调用模块方法

业务组件只传平台应用版本已声明的结构化 target、method 和方法参数：

```ts
const projects = await client.call(
  {
    type: "module",
    bundleId: "project-directory",
    moduleId: "project-directory",
  },
  "listProjects",
  { offset: 0, limit: 50 },
);
```

SDK 使用规范调用路由，并生成包含 target、method 和 params 的请求体：

```text
POST {appGatewayApiBase}/workspace-runtime/apps/{installId}/invoke
```

新应用不得提交 Runtime `service`；`/services/{service}/methods/{method}` 只服务历史 SDK 2.x 应用。
App Gateway 会校验 token、安装、默认 Runtime 绑定、平台应用版本的
方法声明和工作区权限；前端不选择后端实例、不读取具体 `businessId`，也不持有模块服务凭证。完整协议见
[调用模块方法](/development/bundle-development/platform-application-development/module-method-calls/)。

## 错误与会话处理

```ts
import {
  CModelFailureError,
  PlatformApplicationSdkError,
} from "@baijimu/platform-application-sdk";

try {
  await client.call(
    { type: "module", bundleId: "project-directory", moduleId: "project-directory" },
    "listProjects",
    {},
  );
} catch (error) {
  // 由 SDK 判定会话失效；不要仅凭业务 errorCode 或 HTTP 状态重登。
  if (!client.getSession()) {
    client.restartAuthorization();
    throw error;
  }
  if (error instanceof CModelFailureError) {
    console.error(error.errorCode);
  } else if (error instanceof PlatformApplicationSdkError) {
    console.error(error.code);
  }
}
```

SDK 6.0.0 在 App Gateway 返回 `APP_GATEWAY_UNAUTHORIZED` 时清除当前安装会话，也会处理网关返回的非结构化 HTTP 401。
后端业务返回合法 CModel `UNAUTHORIZED` 时，即使 HTTP 状态是 401，SDK 仍保留会话；应用应展示业务错误，不能重新授权。
参数错误、能力未声明、目标服务异常和网络失败同样不能触发登录循环。以 `client.getSession()` 是否已由 SDK 清空作为重新授权的依据，
不要维护另一份会话状态，也不要在捕获到业务 `UNAUTHORIZED` 时自行清理 SDK 会话或自动重放业务请求。
需要主动注销时调用 `await client.revoke()`；需要重新授权时调用 `client.restartAuthorization()`。

平台应用没有另一套 `PLATFORM_APPLICATION` token，也不通过通用 HTTP 代理访问平台内部 API。需要新增
能力时，应先把能力定义为 Runtime 模块方法并创建不可变模块版本，再把该方法加入平台应用版本的
`methodDefinitions`。

## 分享与管理入口

面向其他用户分享通用功能时，使用 Manager 的 `/bundle-app/{bundleId}/{platformAppId}` 路由。
分享当前工作区的具体页面时，使用 `/workspace/{workspaceId}/bundle-app/{bundleId}/{platformAppId}`，
并通过 URL 编码后的 `target` 参数携带原生应用内地址，例如 `/orders/123?tab=items` 或
`/#/orders/123?tab=items`。这是普通 href，可以本地拼接，无需链接生成请求，详见
[用普通 href 打开应用内页面](/development/bundle-development/platform-application-development/entry-and-manifest/#用普通-href-打开应用内页面)。
不要分享包含当前工作区 `installId` 的应用启动地址。应用配置了 `managementUrl` 后，可以使用
`client.openManagementRoute(route)` 打开管理页面；未配置时 SDK 会明确失败，不会猜测 Manager 地址。

## 会话失效

以下情况应终止当前应用会话并重新授权：

- token 已到期或被撤销。
- Bundle 安装已经停用、卸载或升级后使旧 token 失效。
- 返回的 token 与当前 `workspaceId`、`runtimeAppId`、`installId` 不一致。
- App Gateway 明确拒绝当前会话，SDK 已将其清空。

不要在所有业务错误上盲目跳转授权页。参数错误、目标服务异常和能力未声明应分别展示真实错误，避免形成授权循环。

## 本地开发

本地回跳地址必须与实际托管入口同源。若入口是已部署域名，本地 `localhost` 回跳不会通过同源校验。托管初始化要求真实 HTTPS 同源配置；本地页面展示可使用明确的测试配置，但完整授权联调应把新版本随 Bundle 安装到目标测试环境，并从该环境的安装入口启动，不能用外部 URL 替代环境托管。验证时保证：

- 页面可以从入口参数取得真实 `installId`。
- HTTPS、端口和域名与登记入口一致。
- Hash Router 和普通查询参数都能正确读取回跳字段。
- 浏览器刷新后不会丢失 PKCE 会话。
- 本地配置明确指向本次开发环境，不回退到公共 SaaS。
