Skip to content

Repository files navigation

Toooony 小程序 API 类型定义

Note

要求 Toooony Head 版本 >= 1.8.0。

Toooony 小程序运行时 API 的 TypeScript 类型定义,为页面提供全局 t4ony 对象及相关类型。

安装

使用 pnpm:

pnpm add -D @ziztechnology/miniprogram-api-typings

也可以使用 npm:

npm install --save-dev @ziztechnology/miniprogram-api-typings

配置 TypeScript

安装后,需要在项目实际使用的 tsconfig.json 中,通过 compilerOptions.types 引入本包:

{
  "compilerOptions": {
    "types": ["@ziztechnology/miniprogram-api-typings"]
  }
}

如果项目已经配置了 types,请保留原有内容并追加本包,不要直接覆盖。例如:

{
  "compilerOptions": {
    "types": ["node", "vite/client", "@ziztechnology/miniprogram-api-typings"]
  }
}

types 是一个显式的类型包白名单。配置该字段后,只有列表中的类型包会被加入 全局作用域。在 monorepo 或存在多个 tsconfig.json 的项目中,请确认修改的是当前 应用实际加载的配置文件。

配置完成后,不需要手动 import,即可直接使用全局 t4ony

const runtimeInfo = await t4ony.getRuntimeInfo();
const windowInfo = await t4ony.getWindowInfo();

console.log(runtimeInfo.apiVersion);
console.log(windowInfo.windowWidth, windowInfo.windowHeight);

如果编辑器没有立即识别到类型,请重启 TypeScript 服务或开发服务器。

导入类型

除全局对象外,也可以按需导入公开类型:

import type {
  RuntimeInfo,
  T4ony,
  WindowInfo,
} from '@ziztechnology/miniprogram-api-typings';

运行环境与能力边界

以下示例假定已完成上述安装与 TypeScript 配置。公开 API 仅注入到受支持的 PACKAGED_H5 表盘页面中;普通浏览器、Node.js、服务端渲染环境和 EXTERNAL_URL_H5 页面不保证存在该对象。可选功能应先判断宿主环境:

const hasT4ony =
  typeof window !== 'undefined' && typeof window.t4ony === 'object';

if (hasT4ony) {
  renderWatchFace();
} else {
  renderRuntimeUnavailable();
}

应用只能直接调用 T4ony 接口中声明的方法。驾驶状态、驾驶表情、贴纸播放、 车载多媒体和兼容层能力应通过 @ziztechnology/dial-library 使用,不要直接访问 window.unifiedSensorInfowindow.AndroidCarBridgewindow.__carBridge 等运行时 内部对象。

检查 API 能力

getRuntimeInfo() 会返回 API 版本、运行时版本、平台、配置档案,以及每个公开 API 的当前能力状态:

const { capabilities } = await t4ony.getRuntimeInfo();
const sensorCapability = capabilities.getSensorSnapshot;

switch (sensorCapability.state) {
  case 'available':
    renderSensorUI();
    break;
  case 'inactive':
  case 'denied':
  case 'unavailable':
    renderSensorUnavailable(sensorCapability.reason);
    break;
}

能力状态只是调用前的即时快照,实际调用仍可能失败,因此还需要捕获异常。公开 API 没有 canIUse() 方法。

处理调用错误

公开方法失败时会拒绝 Promise,并返回可能带有 apicodedetails 字段的 T4onyError。业务分支应依据稳定的 code,不要依赖错误文案:

import type { T4onyError } from '@ziztechnology/miniprogram-api-typings';

const isT4onyError = (error: unknown): error is T4onyError =>
  error instanceof Error &&
  typeof (error as Partial<T4onyError>).api === 'string' &&
  typeof (error as Partial<T4onyError>).code === 'string';

try {
  await t4ony.accessFile({ directory: 'data', path: 'profile.json' });
} catch (error) {
  if (isT4onyError(error) && error.code === 'NOT_FOUND') {
    renderFirstRunState();
  } else {
    throw error;
  }
}

权限、能力不可用、页面失效、参数错误、配额、I/O、忙碌、中止、超时、不支持及 内部错误,均应按照声明中的 T4onyErrorCode 处理。

基础信息与显示控制

窗口、运行时、设备和电池信息可以独立读取,也可以并行读取:

const [windowState, runtimeState, deviceState, batteryState] =
  await Promise.all([
    t4ony.getWindowInfo(),
    t4ony.getRuntimeInfo(),
    t4ony.getDeviceInfo(),
    t4ony.getBatteryInfo(),
  ]);

console.log(windowState.windowWidth, windowState.windowHeight);
console.log(runtimeState.apiVersion, runtimeState.runtimeVersion);
console.log(deviceState.brand, deviceState.model, deviceState.androidApiLevel);
console.log(batteryState.level, batteryState.isCharging);

getWindowInfo() 的尺寸单位为 CSS 像素。设备和电池结果中的可选字段可能不存在, getDeviceInfo() 也不会提供序列号或厂商私有的系统版本。

屏幕常亮和亮度设置只对当前页面会话生效:

await t4ony.setKeepScreenOn({ keepScreenOn: true });
await t4ony.setScreenBrightness({ mode: 'manual', value: 0.65 });

const brightness = await t4ony.getScreenBrightness();
console.log(brightness.mode, brightness.value);

await t4ony.setScreenBrightness({ mode: 'system' });
await t4ony.setKeepScreenOn({ keepScreenOn: false });

手动亮度 value 必须是 01 之间的有限数值;使用系统亮度时不要传 value。新页面会话开始后,需要重新设置期望值。

文件系统

文件 API 提供三个虚拟目录:

  • data:持久化应用文件,可读写。
  • cache:保存可重新生成的临时文件,可读写。
  • package:读取表盘安装包中的文件,只读。

一般条目必须使用相对路径,不能包含反斜杠、NUL、空路径段、... 或末尾 斜杠。只有 accessFile()statFile()readDirectory() 可用空字符串路径 表示虚拟目录根节点。

查询容量与限制

在确定分块大小或执行配额敏感的写入前,先读取文件系统信息:

const fileSystemInfo = await t4ony.getFileSystemInfo();
const dataVolume = fileSystemInfo.volumes.find(
  (volume) => volume.directory === 'data',
);

if (!dataVolume?.writable) {
  throw new Error('数据目录不可用');
}

console.log(dataVolume.remainingBytes);
console.log(fileSystemInfo.limits.maxWriteChunkBytes);

写入和读取文本

文本读写都应显式指定 encoding: "utf8"

await t4ony.createDirectory({
  directory: 'data',
  path: 'settings',
  recursive: true,
});

const writeResult = await t4ony.writeFile({
  directory: 'data',
  path: 'settings/events.log',
  data: '表盘已启动\n',
  encoding: 'utf8',
  overwrite: true,
  createParents: true,
});

await t4ony.appendFile({
  directory: 'data',
  path: 'settings/events.log',
  data: '表盘已恢复\n',
  encoding: 'utf8',
});

const textFile = await t4ony.readFile({
  directory: 'data',
  path: 'settings/events.log',
  encoding: 'utf8',
});

console.log(writeResult.bytesWritten, textFile.data, textFile.sizeBytes);

分块读取二进制文件

二进制写入必须传入真正的 ArrayBuffer。读取大文件时,应按运行时给出的上限 分块,并根据 bytesRead 推进偏移量,直到 endOfFiletrue

const { limits } = await t4ony.getFileSystemInfo();
const chunks: Uint8Array[] = [];
let offset = 0;
let endOfFile = false;

while (!endOfFile) {
  const result = await t4ony.readFile({
    directory: 'package',
    path: 'assets/model.bin',
    encoding: 'binary',
    offset,
    length: limits.maxReadChunkBytes,
  });

  chunks.push(new Uint8Array(result.data));
  offset += result.bytesRead;
  endOfFile = result.endOfFile;

  if (!endOfFile && result.bytesRead === 0) {
    throw new Error('二进制读取没有取得进展');
  }
}

每次二进制写入或追加都不能超过 maxWriteChunkBytes,生成的文件不能超过 maxFileBytes

检查和管理条目

其余文件方法可用于检查文件、读取元数据、分页列目录、复制、移动和删除:

await t4ony.accessFile({ directory: 'data', path: 'settings/events.log' });

const stat = await t4ony.statFile({
  directory: 'data',
  path: 'settings/events.log',
});

const firstPage = await t4ony.readDirectory({
  directory: 'data',
  path: 'settings',
  limit: 50,
});

const secondPage = firstPage.nextCursor
  ? await t4ony.readDirectory({
      directory: 'data',
      path: 'settings',
      limit: 50,
      cursor: firstPage.nextCursor,
    })
  : null;

await t4ony.copyFile({
  source: { directory: 'data', path: 'settings/events.log' },
  destination: { directory: 'cache', path: 'events-copy.log' },
  overwrite: true,
});

await t4ony.moveFile({
  source: { directory: 'cache', path: 'events-copy.log' },
  destination: { directory: 'cache', path: 'events-archive.log' },
  overwrite: true,
});

await t4ony.removeFile({
  directory: 'cache',
  path: 'events-archive.log',
  ignoreIfNotExists: true,
});

await t4ony.removeDirectory({
  directory: 'cache',
  path: 'temporary',
  recursive: true,
  ignoreIfNotExists: true,
});

console.log(stat.type, firstPage.entries, secondPage?.entries);

readDirectory() 只返回直接子项。需要完整列表时,应持续使用 nextCursor 翻页,直到结果不再返回游标。

JSON 存储

需要跨表盘更新或资源缓存清理保留的数据,应写入 t4ony Storage。它以 miniProgramId(打包表盘对应 faceId)为作用域,与资源版本无关。只允许写入 JSON 值,不能包含函数、undefinedsymbolBigInt、类实例或循环引用。

await t4ony.setStorage({
  key: 'preferences',
  data: {
    theme: 'dark',
    complications: ['battery', 'weather'],
  },
});

const preferences = await t4ony.getStorage({ key: 'preferences' });
const storageInfo = await t4ony.getStorageInfo();

console.log(
  preferences.data,
  storageInfo.usedBytes,
  storageInfo.remainingBytes,
);

await t4ony.removeStorage({ key: 'preferences' });

当前运行时用 code: "INVALID_ARGUMENT"details.reason: "KEY_NOT_FOUND" 的组合表示键不存在,用同一错误码与 details.reason: "QUOTA_EXCEEDED" 表示配额不足。配额敏感的写入前可先调用 getStorageInfo(),同时仍需处理写入期间空间变化导致的异常。

clearStorage() 会删除当前应用命名空间内的全部键,仅应在产品行为明确要求清空 所有数据时调用:

await t4ony.clearStorage();

表盘实例被删除、全部实例解除绑定或应用主动清空时,该存储命名空间会被移除。 如果数据只需保留在当前资源版本中,并允许在更新后丢弃,可使用 localStorage

Head 生命周期

注册和移除监听器时必须传入同一个函数。监听器注册后不会补发已经发生的 initial 事件,因此当前 UI 状态需要单独初始化:

import type {
  T4onyHeadHideListener,
  T4onyHeadShowListener,
} from '@ziztechnology/miniprogram-api-typings';

const handleHeadShow: T4onyHeadShowListener = (event) => {
  resumeAnimations(event.reason);
};

const handleHeadHide: T4onyHeadHideListener = (event) => {
  pauseAnimations(event.reason);
};

await t4ony.onT4onyHeadShow(handleHeadShow);
await t4ony.onT4onyHeadHide(handleHeadHide);

const disposeHeadLifecycle = async (): Promise<void> => {
  await Promise.all([
    t4ony.offT4onyHeadShow(handleHeadShow),
    t4ony.offT4onyHeadHide(handleHeadHide),
  ]);
};

occurredAtElapsedRealtimeMs 适合计算经过时间,不能与 Unix 时间戳比较。

网络状态

先用 getNetworkType() 取得初始状态,需要实时更新时再注册一个稳定的监听函数:

import type { NetworkStatusListener } from '@ziztechnology/miniprogram-api-typings';

const initialNetworkStatus = await t4ony.getNetworkType();
renderNetworkStatus(initialNetworkStatus);

const handleNetworkStatusChange: NetworkStatusListener = (status) => {
  renderNetworkStatus(status);
};

await t4ony.onNetworkStatusChange(handleNetworkStatusChange);

const disposeNetworkStatus = async (): Promise<void> => {
  await t4ony.offNetworkStatusChange(handleNetworkStatusChange);
};

可直接读取 isConnectednetworkTypehasSystemProxyweakNetsignalStrengthDbm 是可选字段。

基础传感器

getSensorSnapshot() 返回一次性的基础传感器和设备指标。每个指标都要先检查 available 判别字段,再读取 value

const snapshot = await t4ony.getSensorSnapshot();

if (snapshot.accelerometer.available) {
  const { x, y, z } = snapshot.accelerometer.value;
  renderAcceleration({ x, y, z });
} else {
  renderAccelerationUnavailable(snapshot.accelerometer.unavailableReason);
}

if (snapshot.wifiSsid.available) {
  renderWifiName(snapshot.wifiSsid.value);
}

连续采样使用 startAccelerometer()startGyroscope()。页面暂时隐藏时应保留 返回的订阅;运行时会在文档冻结时释放底层传感器,并在恢复后自动还原逻辑订阅。 只有组件卸载或永久销毁等不再需要采样的场景才停止订阅:

const accelerometerSubscription = await t4ony.startAccelerometer({
  rate: 'ui',
  listener(sample) {
    renderAcceleration(sample);
  },
});

const gyroscopeSubscription = await t4ony.startGyroscope({
  rate: 'normal',
  listener(sample) {
    renderAngularVelocity(sample);
  },
});

const stopSensors = async (): Promise<void> => {
  await Promise.all([
    accelerometerSubscription.stop(),
    gyroscopeSubscription.stop(),
  ]);
};

getSensorSnapshot() 不提供驾驶模型使用的原子 drivingMotionFramedrivingSensorContext。驾驶状态功能应使用 @ziztechnology/dial-libraryDrivingStatusControllercalibrateDrivingSensors() 等公开导出。

蓝牙正在播放

getBluetoothNowPlaying() 返回一份原始播放快照:

const nowPlaying = await t4ony.getBluetoothNowPlaying();

if (!nowPlaying.available) {
  renderBluetoothUnavailable(nowPlaying.status, nowPlaying.message);
} else if (nowPlaying.status !== 'OK' && nowPlaying.status !== 'IDLE') {
  renderBluetoothReadFailure(nowPlaying.status, nowPlaying.message);
} else if (!nowPlaying.active) {
  renderNoActiveBluetoothSession();
} else {
  renderTrack({
    title: nowPlaying.title,
    artist: nowPlaying.artist,
    positionMs: nowPlaying.positionMs,
    durationMs: nowPlaying.durationMs,
    canPause: nowPlaying.canPause,
  });
}

直接调用返回 availablestatusactive,不包含 SDK 归一化结果中的 successdisplayTitledisplaySubtitledisplayDescription。需要归一化的 BluetoothNowPlayingSnapshot 时,请使用 @ziztechnology/dial-library

此 API 只能读取元数据和播放能力,不能控制播放、订阅变化、获取封面或估算进度。 如果应用自行轮询,页面隐藏时应停止轮询,并避免请求重叠。

SSO 令牌

已有部署绑定时可无参数调用,也可传入公开字段 clientIdscope

const tokenResult = await t4ony.requestSSOToken({
  clientId: 'finance-watch-face',
  scope: 'finance.read',
});

const response = await fetch(
  new URL('/api/v1/finance/a-stock-app', tokenResult.issuer),
  {
    headers: {
      'X-Toooony-SSO-Token': tokenResult.ssoToken,
    },
  },
);

直接结果字段为 ssoTokenexpiresInSecondsexpiresAtEpochSecondsdeviceIduserIdissuerauthorizeUrl。它们不同于 SDK 归一化结果中的 tokenexpiresAtMsdeviceIDuserIDauthorizeURL

每次访问受保护接口前都应获取新的单次令牌,并且只通过指定的请求头发送。令牌不应 进入 URL、日志、分析事件、持久化缓存、浏览器存储或组件状态;请求重试时也不能复用 原令牌。公开调用不依赖旧版 SDK 兼容方案中的 requestSSOToken customFields Handler。

测试应用代码

本包只提供类型声明,不提供浏览器模拟器;SDK 的测试入口也不会在真实 window 上 安装完整的公开对象。需要单元测试的直接调用,应封装到应用自有适配器中,再注入 实现相同接口的 mock:

type BatteryAPI = Pick<typeof t4ony, 'getBatteryInfo'>;

export const runtimeBatteryAPI: BatteryAPI = {
  getBatteryInfo: () => t4ony.getBatteryInfo(),
};

测试中使用实现 BatteryAPI 的普通对象,不要替换生产环境的全局对象。最终验证应在 受支持的 Toooony Runtime 中,以打包表盘方式完成。

开发检查

pnpm check

License

MIT

About

Toooony 小程序类型定义

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages