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

# 一、功能介绍

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

QQ游戏大厅

QQ游戏-跳一跳

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

# 二、准备工作

  • 申请开通「PC 小程序插件」能力:
    开发者需前往微信开发者平台绑定网站应用后,前往「网站应用 - 开放能力 - PC 小程序插件」申请开通此能力,详细操作步骤可查看申请 PC 小程序插件

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

# 三、名词解释

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

# 四、初始化

点此下载 PC 小程序插件

解压下载到的压缩包。其中仅包含「PC 小程序插件」,不包含网站应用 OpenSDK。
宿主应用开发者负责部署分发压缩包内的文件。
开发阶段,宿主应用开发者主要关注如下文件:

  • 动态库:wmpf_host_export_x64.dll
  • protobuf 定义:wmpf_host.proto
  • 头文件:wmpf_host_interface.h

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

  1. 调用LoadLibrary()加载wmpf_host_export_x64.dll
  2. 调用GetProcAddress(),获取GetBrowsingServiceIsFeatureSupport函数指针。
  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
  1. 调用IBrowsingService::QueryInterface("IAppletManagerV3"),获得IAppletManagerV3类型实例的指针。该实例用于打开/关闭小程序。
  2. 调用IBrowsingService::QueryInterface("IILinkAuthManager"),获得IILinkAuthManager类型实例的指针。该实例用于登录态管理。

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

# 五、鉴权

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

授权时序

# 1. OpenSDK 授权

通常,你的应用接入网站应用 OpenSDK以便实现「微信授权登录」,所需的权限(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 为空。

# 返回示例

{
  "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. 检查登录态》。

流程图

# 六、打开小程序(小游戏)

打开小程序流程

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

前置条件:开通安全鉴权模式 ,配置加解密用的 AES256_GCM 密钥。

# 1. 加密启动参数

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

# Step1. 开通安全鉴权模式并配置了 AES256_GCM 密钥,你应该会得到如下两个字段:

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

# Step 2. 准备原始待加密数据(即小程序启动参数),构造如下格式的 JSON 对象:

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

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

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

示例代码(Node.js 环境测试有效):

const crypto = require("crypto");
// 要打开「小程序示例」(wxe5f52902cxxxe896)的「接口」页面。
const req = {
  app_id: "wxe5f52902cf4de896",
  entry_url: "page/API/index",
  _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 环境测试有效):

const _appid = "wxe5f5xxxxe896"; // OpenSDK使用的appid
const _timestamp = Math.floor(Date.now() / 1000);
const sn = "fa05fe1e5bcc79b81ad5ad4b58acf787"; // Step 1中的AES密钥的sn
const aad = `jm_76zerw3f0fxs|${_appid}|${timestamp}|${sn}`;

# Step 5. 生成 iv 参数

iv: 初始向量,为 16 字节 base64 字符串(解码后为 12 字节随机字符串)

示例代码(Node.js 环境测试有效):

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

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

完整 Demo 如下(Node.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 = "wxe5f5290xxxe896";
// 参照文档登记的AES key。需保密,不应下发给客户端。
// https://developers.weixin.qq.com/doc/oplatform/Mobile_App/Access_Guide/signature_verify.html
const aes_key = {
  Sn: "fa05fe1e5bcc79b81ad5ad4b58acf787",
  Key: "otUpngOjU+nVQaWJIC3D/yMLV17RKaP6t4Ot9tbnzLY=",
};
// 你要打开的微信小程序的appid和页面路径。通常由客户端提供。
const mini_program = {
  app_id: "wxe5f52902xxxde896",
  entry_url: "page/API/index",
};
const res = encode(mini_program, _appid, aes_key);
console.log(res);
// 应将res内容下发给客户端,以便客户端进一步调用PC小程序插件接口并打开小程序。

# 2. 启动小程序(小游戏)

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

# 接口参数说明

参数 类型 说明
appid const char * 要打开的小程序的 appid,需与加密信息里的内容一致。
launch_config_pb const char * protobuf message LaunchConfig 对象序列化后 buffer
launch_config_pb_length size_t protobuf message LaunchConfig 对象序列化后 buffer 的长度
callback LaunchCallback* 结果回调。result_code: 0 表示打开成功。

其中,LaunchConfig 定义如下:

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 个参数,就是上一步后台获得的加密参数。

# 七、问题与反馈

如有接入或开发相关问题,可前往PC 小程序社区专区咨询。