# PC 小程序插件接入指南 Beta

## 一、功能介绍

PC 小程序插件，是微信团队开发的支持外部桌面应用运行微信小程序的能力。用户不需安装或登录 PC 微信客户端，即可在开发者的桌面应用内通过微信身份登录直接唤起小程序。当前仅支持 64 位 Windows 版本。

![](https://res.wx.qq.com/op_res/GVzhB322ObswpNmZZRLuZp5DXxaWtjRclP9CV39xcGnsbYc-iCcEPcrVzXZwjpF-5no1p64_0AB24uS0DvBOlQ)

![](https://res.wx.qq.com/op_res/GVzhB322ObswpNmZZRLuZu27RQ75-I37lPGU9LC0HEOl1afe5hBr5JJycfgRY7njyHQbocOuIGCHo_RxU7BJ0w)

QQ桌面端运行PC小程序的体验

## 二、准备工作

- 申请开通「PC 小程序插件」能力：  
  开发者需前往[微信开发者平台](https://developers.weixin.qq.com/platform)绑定网站应用后，前往「网站应用 - 开放能力 - PC 小程序插件」申请开通此能力，详细操作步骤可查看[申请 PC 小程序插件](/doc/oplatform/Website_App/guide/apply)。

- 开发环境要求：  
  Windows 操作系统，且已安装 PC 小程序插件版，拥有有效的网站应用 AppID 和 AppSecret。

## 三、名词解释

1. 宿主应用：要接入 OpenSDK 和 PC 小程序插件的微信外桌面应用。本文的读者通常应该是宿主应用的开发者。
2. OpenSDK：本文用到的 OpenSDK 特指“[网站应用 OpenSDK](guideline)”。传统上 Windows 应用通过接入此 SDK 来获取微信用户授权登录能力。使用 PC 小程序插件同样依赖此 OpenSDK 向用户申请授权。
3. PC 小程序插件：为用户提供在宿主应用中使用 PC 小程序的能力。

## 四、初始化

首先通过文末的链接下载最新版 PC 小程序插件压缩包。先校验 md5，再对其解压。  
注意：此压缩包仅包含「PC 小程序插件」，不包含「网站应用 OpenSDK」。请按照 OpenSDK 的文档自行将其接入。  
开发阶段，宿主应用开发者主要关注`include`、`proto`、`host`目录中的如下文件：

- 动态库：`wmpf_host_export_x64.dll`
- protobuf 定义：`wmpf_host.proto`
- 头文件：`wmpf_host_interface.h`

发布时，宿主应用的开发者需负责将 PC 小程序插件的`host`和`runtime`目录需要部署到用户系统。

初始化 PC 小程序插件，主要包括以下步骤

1. 调用`LoadLibrary()`加载`wmpf_host_export_x64.dll`
2. 调用`GetProcAddress()`，获取`GetBrowsingService`、`IsFeatureSupport`函数指针。
3. 调用`GetBrowsingService`函数指针，获得`IBrowsingService`类型的实例。
4. 调用`IBrowsingService::InitilizeBrowsingCore`，初始化一个`XWebService`类型的实例。初始化时需传入一个 Config，格式是一个 JSON String，JSON 对象需包含如下字段：

| Key              | Value 类型 | 必填 | 说明                                                                                                                        |
| ---------------- | ---------- | ---- | --------------------------------------------------------------------------------------------------------------------------- |
| product-id       | number     | 是   | 必须设置为`3000`                                                                                                            |
| log-level        | number     | 否   | 0（默认）：打印全部日志。2：打印重要日志。                                                                                  |
| no-preload       | boolean    | 否   | true：禁用小程序预加载；false(默认)：不禁用小程序预加载。小程序预加载：提前准备好一个小程序运行时，以便加速小程序启动速度。 |
| max-retry-count  | number     | 否   | 主进程挂了之后的尝试重启次数。默认为 5。                                                                                    |
| wmpf_root_dir    | string     | 是   | 数据目录。如宿主应用需要基于自身的用户体系隔离小程序用户数据，通过此字段控制。                                              |
| enable-applet-v3 | boolean    | 是   | 必须设置为`true`                                                                                                            |

5. 调用`IBrowsingService::QueryInterface("IAppletManagerV3")`，获得`IAppletManagerV3`类型实例的指针。该实例用于打开/关闭小程序。
6. 调用`IBrowsingService::QueryInterface("IILinkAuthManager")`，获得`IILinkAuthManager`类型实例的指针。该实例用于登录态管理。

初始化完毕，你将可以调用 PC 小程序插件提供的各种能力。通常，你需要从获取微信登录态开始。

## 五、鉴权

> 你的应用需要接入[网站应用 OpenSDK](guideline)，以便能够向用户申请“使用小程序”的授权。小程序插件不内置 OpenSDK。

![](https://res.wx.qq.com/op_res/RXX5iNP-v8YsaUehNPw-G6NlZbehs0pmMw-8Gb0AET_vE2HIhT1Tinf7CRn5vt9snT15ByPi9tyeGC2wmEview)

### 1. OpenSDK 授权

通常，你的应用接入[网站应用 OpenSDK](guideline)以便实现「微信授权登录」，所需的权限（scope）是`snsapi_login`。  
为了使用小程序插件，你需要向用户申请权限（scope）：`snsapi_runtime_pcsdk`。  
用户同意授权，你将获得`code`，并需要进一步调用`/sns/oauth2/access`接口得到`access_token`。请留意`/sns/oauth2/access`接口响应中的**scope**字段，该字段体现用户实际授予的权限集。  
要注意，多次授权流程后`access_token`具备的权限不会叠加。例如你第一次向用户申请了`snsapi_login`权限，有效期内又重新向用户申请了`snsapi_runtime_pcsdk`权限，然而最终你得到的`access_token`只有`snsapi_runtime_pcsdk`一个权限。如果你想同时拥有两个权限，需要在一次调用 OpenSDK 时同时申请。  
请按需申请权限。如同时申请多个权限，在调用 OpenSDK 时以英文半角逗号分割对应的 scopes，例如：`snsapi_login,snsapi_runtime_pcsdk`。  
具体用法参见网站应用 OpenSDK 官方文档，此处不再展开。

### 2. 后台调用微信后台 API：`/sns/pcsdk/login`

申请并获得具备`snsapi_runtime_pcsdk`权限的`access_token`后，你将获准调用微信后台接口`/sns/pcsdk/login`，以便得到登录小程序插件所需的凭据`login_buffer`。

```
POST https://api.weixin.qq.com/sns/pcsdk/login?access_token=ACCESS_TOKEN
```

请求的 Body 为空。

#### 返回示例

```json
{
  "base_resp": {
    "errcode": 0,
    "errmsg": "ok"
  },
  "login_buffer": "LOGIN_BUFFER"
}
```

### 3. 客户端调用小程序插件接口：`IILinkAuthManager::ThirdAppLogin`

你的后台成功获取`login_buffer`临时票据后，将其下发给客户端。在端上调用小程序插件的`IILinkAuthManager::ThirdAppLogin`接口完成授权，在收到登录成功回调后即可正常打开小程序。

### 4. 检查登录态

用户授权登录后，PC 小程序插件会对登录态进行持久化。随着时间的推移，登录态可能失效。因此，每次进程冷启动、完成《四、初始化
》」所述的 PC 小程序插件初始化流程后，你都应首先调用`IILinkAuthManager::GetUserInfo()`检查登录态。
`GetUserInfo`是异步方法，其回调函数中的`login_status`表示当前用户的登录态，是如下枚举值之一：

| IlinkLoginStatus         | 说明                                                                                                                                                                                                                                         |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| kIlinkLoginSuccess       | 登录成功，可直接打开小程序                                                                                                                                                                                                                   |
| kIlinkRequireManualLogin | 小程序插件没有登录态，需要用 OpenSDK 的`access_token`换取`login_buffer`（见《五、2.》），以登录小程序插件。如果你没有持有有效的 OpenSDK access_token，需要先向用户申请。                                                                     |
| kIlinkRequireAutoLogin   | 小程序插件之前登录过，登录态有可能有效。你需进一步调用`IILinkAuthManager::AutoLogin()`确认登录状态，并根据 `AutoLogin()`的回调结果决定后续操作。如回调返回成功，则可直接打开小程序。如回调返回失败，按`kIlinkRequireManualLogin`的说明处理。 |

### 5. 正确处理小程序插件的事件回调：`IILinkAuthManager::SetSessionTimeoutCallback`

当小程序插件登录成功后，登录态可能会在未来某个时间过期。你的应用应该提前监听`SetSessionTimeoutCallback`回调，理解和处理登录态过期的情况。  
当收到此回调时，你的应用应该调用`IILinkAuthManager::GetUserInfo()`重新检查登录态，参考《五、4. 检查登录态》。

![](https://res.wx.qq.com/op_res/B__61HaSO_JxFVa99eXeVLuTgEXBRz4dzmMJEBUJLqIjBBwjOUbpyVjXLK16H69_Q8s9m_TU2QiCQ7TpxGPqqg)

## 六、打开小程序（小游戏）

![](https://res.wx.qq.com/op_res/LomoX0s9niu2XECawWbGlvsxxjHHV4sw-eElSVuHvemufTqFL5IgIcSoSGQ4VfNJFS5gg9sD_vfW8FJEeokaVw)

安全起见，小程序插件不支持在客户端明文传递启动小程序的参数，需要走后台加密流程完成参数传递。

**前置条件**：开通[安全鉴权模式](/doc/oplatform/Website_App/guide/signature_verify) ，配置加解密用的 AES256_GCM 密钥。

### 1. 加密启动参数

**为避免密钥泄漏，此流程需在后台进行**

#### Step1. 开通[安全鉴权模式](/doc/oplatform/Website_App/guide/signature_verify)并配置了 AES256_GCM 密钥，你应该会得到如下两个字段:

```js
const sn = "fa05fe1e5bcc79b81ad5ad4b58acf787"; // 密钥的sn
const key = "otUpngOjU+nVQaWJIC3D/yMLV17RKaP6t4Ot9tbnzLY="; // 密钥的Key
```

#### Step 2. 准备原始待加密数据（即小程序启动参数），构造如下格式的 JSON 对象：

```json
{
  "app_id": "<你要打开的小程序/小游戏的AppID>",
  "entry_url": "<要打开的小程序页面。留空表示打开默认首页>"
}
```

如果待加密数据是一个小程序短链（以“#小程序://”开头的字符串），则构造如下格式的 JSON 对象：

```json
{
  "short_link": "<你要打开的小程序/小游戏短链>"
}
```

#### Step 3. 在待加密 JSON 中添加额外 3 个字段：

```json
// 以"app_id" + "entry_url"打开小程序/游戏：
{
  "app_id": "<你要打开的小程序/小游戏的AppID>",
  "entry_url": "<要打开的小程序页面。留空表示打开默认首页>",
  "_n": "<随机字符串，推荐使用16-32字节非固定长度随机base64字符串>",
  "_appid": "<你当前在使用OpenSDK的AppID>",
  "_timestamp": "<当前的时间戳>"
}
// "以short_link"打开小程序/游戏：
{
  "short_link": "<你要打开的小程序/小游戏短链>",
  "_n": "<随机字符串，推荐使用16-32字节非固定长度随机base64字符串>",
  "_appid": "<你当前在使用OpenSDK的AppID>",
  "_timestamp": "<当前的时间戳>"
}
```

示例代码（Node.js 环境测试有效）：

```js
const crypto = require("crypto");
// 要打开小程序页面：
const mini_program = {
  app_id: "wxe5f52902cf4de896",
  entry_url: "page/API/index",
};
// 拼接3个额外字段：
const req = {
  ...mini_program,
  _n: crypto.randomBytes(16).toString("base64").replace(/=/g, ""),
  _timestamp: Math.floor(Date.now() / 1000),
  _appid: "wx12345xxxbcdef",
};
```

#### Step 4. 生成 aad 参数

aad 是以下格式的字符串：

```
jm_76zerw3f0fxs|<你使用OpenSDK的appid>|<前面生成的时间戳>|<使用的对称密钥编号，在Open平台申请AES密钥时获得>
```

示例代码（Node.js 环境测试有效）：

```js
const _appid = "wx12345xxxbcdef"; // OpenSDK使用的appid，而不是要打开的小程序的appid
const _timestamp = Math.floor(Date.now() / 1000); // 需与Step 3中用到的_timestamp保持一致
const sn = "fa05fe1e5bcc79b81ad5ad4b58acf787"; // Step 1中的AES密钥的sn
const aad = `jm_76zerw3f0fxs|${_appid}|${timestamp}|${sn}`; // 固定前缀需完整保留
```

#### Step 5. 生成 iv 参数

iv: 初始向量，为 16 字节 base64 字符串（解码后为 12 字节随机字符串）

示例代码（Node.js 环境测试有效）：

```js
const real_iv = crypto.randomBytes(12);
const iv = real_iv.toString("base64");
```

#### Step 6. 基于如上参数进行加密

完整 Demo 如下（Node.js 环境测试有效）：

```js
// 本代码应运行于后台环境，以避免AES密钥泄露
const crypto = require("crypto");

function encode(req, _appid, aes_key) {
  //加密签名使用的统一时间戳，一次加密过程不要重复生成时间戳。
  const timestamp = Math.floor(Date.now() / 1000);
  const nonce = crypto.randomBytes(16).toString("base64").replace(/=/g, "");
  const reqex = {
    _n: nonce,
    _appid,
    _timestamp: timestamp,
  };
  // 生成并添加安全校验字段
  const real_req = Object.assign({}, reqex, req);
  const plaintext = JSON.stringify(real_req);
  const aad = `jm_76zerw3f0fxs|${_appid}|${timestamp}|${aes_key.Sn}`;
  const real_key = Buffer.from(aes_key.Key, "base64");
  const real_iv = crypto.randomBytes(12);
  const real_aad = Buffer.from(aad, "utf-8");
  const real_plaintext = Buffer.from(plaintext, "utf-8");
  const cipher = crypto.createCipheriv("aes-256-gcm", real_key, real_iv);
  cipher.setAAD(real_aad);
  let cipher_update = cipher.update(real_plaintext);
  let cipher_final = cipher.final();
  const real_ciphertext = Buffer.concat([cipher_update, cipher_final]);
  const real_authTag = cipher.getAuthTag();
  const iv = real_iv.toString("base64");
  const data = real_ciphertext.toString("base64");
  const authtag = real_authTag.toString("base64");
  return {
    iv,
    data,
    authtag,
    timestamp,
  };
}
// 在open.weixin.qq.com 注册的网站应用appid。即你调用OpenSDK用到的那个appid。
const _appid = "wx12345xxxbcdef";
// 参照文档登记的AES key。需保密，不应下发给客户端。
// /doc/oplatform/Mobile_App/Access_Guide/signature_verify.md
const aes_key = {
  Sn: "fa05fe1e5bcc79b81ad5ad4b58acf787",
  Key: "otUpngOjU+nVQaWJIC3D/yMLV17RKaP6t4Ot9tbnzLY=",
};
// 你要打开的微信小程序的appid和页面路径。通常由客户端提供。
const mini_program = {
  app_id: "wxe5f52902cf4de896",
  entry_url: "page/API/index",
};
// 如打开的是短链，则：
// const mini_program = {
//   "short_link": "#小程序://xxxxxxxx"
// }
const res = encode(mini_program, _appid, aes_key);
console.log(res);
// 应将res内容下发给客户端，以便客户端进一步调用PC小程序插件接口并打开小程序。
```

### 2. 启动小程序（小游戏）

你的客户端软件从后台收到启动小程序所需的加密参数，然后调用小程序插件的`IAppletManagerV3::LaunchApplet()`接口，以打开小程序（小游戏）。

#### 接口参数说明

| 参数                    | 类型             | 说明                                                                                                                                                |
| ----------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| appid                   | const char \*    | 当使用"app_id + entry_path"打开小程序的，传"app_id"。当使用"entry_path"打开小程序，传空值或自定义内容，如打开失败，`OnLaunchApplet`回调会使用此值。 |
| launch_config_pb        | const char \*    | protobuf message LaunchConfig 对象序列化后 buffer                                                                                                   |
| launch_config_pb_length | size_t           | protobuf message LaunchConfig 对象序列化后 buffer 的长度                                                                                            |
| callback                | LaunchCallback\* | 结果回调。result_code: 0 表示打开成功。                                                                                                             |

其中，Protobuf Message LaunchConfig 定义如下：

```proto
message LaunchConfig {
  message TranslateLink {
    // 你的OpenSDK使用的appid
    optional string appid = 1;
    // 初始向量，为16字节base64字符串（解码后为12字节随机字符串）
    optional string iv = 2;
    // 加密后的密文，使用base64编码
    optional string data = 3;
    // 加密时的时间戳，需要与明文data中的_timestamp一致
    optional string timestamp = 4;
    //  GCM模式输出的认证信息，使用base64编码
    optional string authtag = 5;
  }
  optional TranslateLink translate_link = 27;
}
```

可以看到`LaunchConfig::TranslateLink`中编号 2-5 的 4 个参数，就是上一步后台获得的加密参数。  
完成一切准备后，请在 C++代码中接入 Google Protobuf 库，构造`LaunchConfig`对象，序列化为二进制 Buffer 后调用小程序插件的 LaunchApplet 接口，实现启动小程序。

## 七、问题与反馈

如有接入或开发相关问题，可前往[PC 小程序社区专区](https://developers.weixin.qq.com/community/minihome/mixflow/3690550954962780162)咨询。

## 八、下载

- [2.4.4.49_x64_universal_V20127(md5: 76e64da57c51794f7bedc532c62573cc)](https://dldir1.qq.com/WechatWebDev/wmpf/RadiumWMPF_2.4.4.49_x64_universal_V20127.zip)
- [2.2.3.520_x64_V14673(md5: c7ba25d6518443b75958781fb97de2c1)](https://dldir1.qq.com/WechatWebDev/WechatWebDev/wmpf/RadiumWMPF_2.2.3.520_x64_V14673.zip)
- [2.2.3.465_x64_V14299(md5: af146442c2da9f7d9ba4438b0c6468ca)](https://dldir1.qq.com/WechatWebDev/WechatWebDev/wmpf/RadiumWMPF_2.2.3.465_x64_V14299.zip)
- [2.2.3.382_x64_V14087(md5: 56bd6451e3845e997c36f1333bd7602b)](https://dldir1.qq.com/WechatWebDev/WechatWebDev/wmpf/RadiumWMPF_2.2.3.382_x64_V14087.zip)
