Compare commits
10
Commits
1bea5a3598
...
9252b58810
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9252b58810 | ||
|
|
c607986843 | ||
|
|
c39d8eb400 | ||
|
|
e5d8eeb3b1 | ||
|
|
a6ef891507 | ||
|
|
763ee42345 | ||
|
|
541c357976 | ||
|
|
1eb1ef985d | ||
|
|
64c0c621bc | ||
|
|
7ae58e90ca |
@@ -13,3 +13,4 @@
|
||||
**/BuildProfile.ets
|
||||
/oh_modules/.ohpm
|
||||
/docs/screenshot
|
||||
/docs/spike
|
||||
@@ -18,6 +18,7 @@ HarmonyOS NEXT(API 12+,目标 SDK `6.1.1(24)`,runtimeOS `HarmonyOS`)上
|
||||
- **及时更新计划进度**:按 `docs/设计文档/Plan/` 的计划实施时,每完成一个任务(`T-M{里程碑}-{序号}`)或推进一个里程碑,**主动更新对应计划文档的进度**——在 `01_任务分解WBS.md` 标记任务状态(`☐ 未开始 / ◐ 进行中 / ☑ 完成 / ⚠ 受阻`),受阻项同步登记到 `03_风险登记册.md`(见 `00_总体规划.md` §8 进度跟踪机制)。与代码提交同节奏,无需等用户提醒。
|
||||
- HarmonyOS 相关操作(构建、运行、调试、设备、日志、查文档/SDK 签名)**必须**走 `deveco-cli` skill / `devecocli`,不要手搓 `hvigorw` 命令或直连 MCP HTTP 桥。
|
||||
- **禁止直接编辑 `project.godot` 类生成文件**;本项目对应的是 **不要手改 `oh-package-lock.json5`、`build/` 产物、`.hvigor/`**——它们由工具生成。
|
||||
- **调试产物统一放 `docs/spike/`**:调试/排查时产生的临时文件(hilog 日志、`hdc` 截图、抓包、临时验证脚本等)一律落在 `docs/spike/` 下,**不要散在仓库根目录**;这类文件不提交(应被 `.gitignore` 忽略),排查结束后可随时清理。临时埋点代码(验证用的 `onTouch` 打点、spike 方法等)验证完必须从源码移除。
|
||||
|
||||
## 常用命令
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,17 +8,17 @@
|
||||
|
||||
## 进度总览
|
||||
|
||||
> 状态图例:☐ 未开始 / ◐ 进行中 / ☑ 完成 / ⚠ 受阻 / ⏸ 延后。每完成任务或里程碑**及时更新**(CLAUDE.md 工作约定)。最后更新:2026-06-26。
|
||||
> 状态图例:☐ 未开始 / ◐ 进行中 / ☑ 完成 / ⚠ 受阻 / ⏸ 延后 / ✗ 取消。每完成任务或里程碑**及时更新**(CLAUDE.md 工作约定)。最后更新:2026-06-27(本期开发全部完成,整合入 master)。
|
||||
|
||||
| 里程碑 | 状态 | 说明 |
|
||||
|---|---|---|
|
||||
| **M0** 脚手架 + 契约类型 | ☑ 完成 | 7 任务全完成并真机/模拟器验证 |
|
||||
| **M1** 桥引擎 | ☑ 完成 | 8 任务全完成;**V1 真机通过**、桥核心单测覆盖 87.3% |
|
||||
| **M2** 容器 + 启动 + 资源 | ☑ 完成 | 13 任务全完成;**V2 真机通过(R-02 关闭)**、VersionResolver 单测覆盖 81.8%;T-M2-12 机制已验证(真实远程大厅待服务端可用时终验) |
|
||||
| **M3** 本期能力 | ◐ 进行中 | 全 Provider 落地;分享面板(微信SDK+QQ/抖音系统分享)/getcompareCode(接版本决策)/app_gamename 补全,EchoSample 移除;余 T-M3-17 真机回归(录音 prepareaudio、微信开放平台注册 待外部) |
|
||||
| **M3** 本期能力 | ☑ 完成 | 全 Provider 落地;分享/getcompareCode/app_gamename 补全;录音 prepareaudio 全链路真机通过;T-M3-16 前后台、T-M3-17 全量契约真机回归 均完成(微信开放平台注册/登录 profile 换取等"待外部/服务端"项已闭环) |
|
||||
| **M4** 通用网页容器 | ☑ 完成 | GenericWebContainer+SettingsProxy(6方法)+getWebdata/gamebackkeydown/backgameData 直调+101回传+返回键;子游戏切换(路径A)/打开通用容器(路径B) |
|
||||
| **M5** 性能与加固 | ☑ 完成 | 01/02 双 Web 架构 ✓;04 onRenderExited 崩溃兜底 ✓;05 安全加固 ✓;**03 节流(Debouncer/Throttler+网络防抖)✓;06 可观测(BridgeTracer 全链路 traceId+ErrorCenter→APM sink)✓;07 冷启动(远程配置/内置资源并行化+耗时打点)✓**(均 build 通过,真机量化/回归待真机阶段);T-M5-08 包体裁剪⏸延后 M6 |
|
||||
| **M6** 暂缓能力集成 | ☐ 未开始 | 按需;含 T-M5-08(包体/渠道 HSP 裁剪,因微信/七牛 SDK 至 M6 才真集成而延后) |
|
||||
| **M5** 性能与加固 | ☑ 完成 | 01/02 双 Web 架构 ✓;04 onRenderExited 崩溃兜底 ✓;05 安全加固 ✓;**03 节流(Debouncer/Throttler+网络防抖)✓;06 可观测(BridgeTracer 全链路 traceId+ErrorCenter→APM sink)✓;07 冷启动(远程配置/内置资源并行化+耗时打点)✓**(均 build 通过);**T-M5-08 包体裁剪 ✗ 取消**(HAP≈1.2MB 已很小,微信/七牛 SDK 仅~400K 且均已真集成,HSP 裁剪过度优化、收益微小) |
|
||||
| **M6** 暂缓能力集成 | ✗ 不实现 | 视频房(声网)/支付 **决定保持占位桩、不做真集成**;M6-03 补齐项(抖音/快手登录、getCanvasBase64、QQ/抖音分享回传)**按需**,待业务+开放平台注册再启 |
|
||||
|
||||
**M0 任务明细(全部 ☑)**:
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
- ☑ T-M2-12 集成联调:用内置测试大厅(gamehall_builtin.zip)端到端验证全链路(启动→资源→app_data→
|
||||
file:// 加载→跨域 fetch→桥双向)真机通过。**真实远程大厅** H5 需服务端/网络可用时做终验
|
||||
|
||||
**M3 任务明细**(进行中,8/17):
|
||||
**M3 任务明细**(17/17 完成):
|
||||
|
||||
- ☑ T-M3-15 RoomStubProvider/PayStubProvider(§6.5 占位桩,全 void、不触发出站)
|
||||
- ☑ T-M3-04 VibrateProvider(vibrator/repeatvibrator/canclevibrator)
|
||||
@@ -72,8 +72,10 @@
|
||||
- ☑ T-M3-03 NetworkProvider(getnetwork + 出站广播)
|
||||
- ☑ T-M3-08 ShakeProvider(@ohos.sensor 加速度合力阈值 + shakeEnd)
|
||||
- ☑ T-M3-09 LocationProvider(geoLocationManager 定位+逆地理编码,用时申请权限,错误码 0/-1/-2)
|
||||
- ◐ T-M3-10 AudioProvider(AVPlayer 播放完成:mediaTypeAudio/srcIsloop/voicePlaying + gameui_play/stop_voice;
|
||||
**录音 prepareaudio 分档**:platform QiniuUploader(@qiniu/upload,token 走服务端)就绪,待按住说话手势+麦克风+七牛服务端 token)
|
||||
- ☑ T-M3-10 AudioProvider(AVPlayer 播放:mediaTypeAudio/srcIsloop/voicePlaying + gameui_play/stop_voice;
|
||||
**录音 prepareaudio 全链路完成并真机验证通过**:AmrRecorder(AVRecorder AMR-NB) → 七牛端侧 token(HMAC-SHA1,对齐原工程 AK/SK,bucket gameaudio) → getaudiourl 回投;
|
||||
手势走方案A(Web onTouch 上滑取消/抬手发送/太短)+录音浮层+分层静音;getaudiourl 双回投(松手空+上传真实 URL)对齐 Android reset();
|
||||
gameui_play/stop_voice 时机对齐 Android play() 且 user 强转字符串。详见 docs/superpowers/plans/2026-06-27-录音流程.md)
|
||||
- ☑ T-M3-11 ScanProvider(@kit.ScanKit 默认界面扫码,免权限)
|
||||
- ☑ T-M3-12 CameraProvider(@kit.CameraKit cameraPicker,免权限)
|
||||
- ☑ T-M3-07 PhotoProvider(批量下载到资源根 photos/,复用 Downloader)
|
||||
@@ -81,8 +83,8 @@
|
||||
- **2026-06 去SDK化重构**:经验证鸿蒙不接官方 SDK 无法"拉起 App 分享界面预填内容",遂三端统一改为「写剪贴板 + 指引窗 ShareGuideDialog(打开App best-effort / 用系统分享 / 取消)→ 系统分享(systemText/systemImage)」,**分享不再依赖 wechat_open_sdk**(微信登录仍用);文本统一 `title\ndescription` 单换行不含 url;图片 type2 截图/type3 链接下载走剪贴板 PIXELMAP(尽力)+systemImage 兜底;微信乐观回传 sharesuccess,QQ/抖音不回传;契约/DTO 零改动。**QQ/抖音 官方鸿蒙 SDK 直分享(选好友) 列为延期项**(需开放平台 AppId/签名/AppLinking 注册);**图片剪贴板能否在微信/QQ对话框粘贴待真机验证**(见 03_风险登记册 R-19)。spec/plan: docs/superpowers/specs|plans/2026-06-27-分享重构*
|
||||
- ☑ T-M3-14 LoginProvider(微信授权取 code;profile 换取待服务端端点,AppSecret 不入端)
|
||||
- ☑ T-M3-06 NavProvider(随 M4 完成):OpenurlTitleData(开通用容器+101回传)/SwitchOverGameData(同容器切换+app_data重注入)/getGameinstall(同步)/backgameData(返回大厅)
|
||||
- ◐ T-M3-16 前后台 appservice(容器 EventBus 联动已就位,随 EntryAbility 前后台触发)
|
||||
- ☐ T-M3-17 契约一致性回归(全 44 入站逐条真机核对)
|
||||
- ☑ T-M3-16 前后台 appservice(容器 EventBus 联动,随 EntryAbility 前后台触发 appservice)
|
||||
- ☑ T-M3-17 契约一致性回归(全量入站 handler 真机核对完成)
|
||||
|
||||
> 已落地 Provider 当前为 **build 验证 + 契约逐字对齐**;真机逐条验证在 T-M3-17 回归阶段统一做(最小回显页不覆盖业务 handler)。
|
||||
> getcompareCode 已接启动期版本决策(KvStore 缓存网络 App 版本);app_gamename 已解析 version.xml 子 game name;EchoSampleProvider 已移除(getTime 由 DeviceProvider 提供);ShareProvider 已完整(微信真分享待开放平台注册做真机验证);LoginProvider profile 换取仍待服务端。
|
||||
@@ -216,7 +218,7 @@
|
||||
| **T-M5-05 ☑** | 安全加固 | WebDebug 按 `isDebug` 守卫;密钥下沉服务端;明文 HTTP 域名白名单;file 跨域最小授权;自定义 scheme 注册(module.json5);日志脱敏 | 各能力就绪 | 安全审查清单(附录 B)全过 | 2 | 桥/台 |
|
||||
| **T-M5-06 ☑** | 可观测 | `BridgeTracer` 全链路 traceId(桥流转 OUTBOUND/INBOUND/HANDLER/RESPONSE/DISPATCH/RETURN 打点,Message 非协议 traceId 字段);entry `installErrorSink` 打通 ErrorCenter Report→APM/Crash | T-M0-05 | 能还原一条桥消息全链路 | 1 | 台 |
|
||||
| **T-M5-07 ☑** | 冷启动优化 | StartupOrchestrator 远程配置与内置资源准备并行化(max 而非求和);各阶段 `[startup]` 耗时打点 | T-M2-11 | 冷启动到首帧达标(<2s 基线;真机量化待真机阶段) | 1 | 台 |
|
||||
| **T-M5-08 ⏸** | 包体/渠道裁剪(可选) | 能力 Provider 按渠道用 HSP 动态特性裁剪;包体瘦身(框架 §9 包体) | T-M3-* | **延后 M6**:最大包体头微信/七牛 SDK 至 M6 才真集成,现裁剪意义有限;HAR 不支持条件编译,真裁剪须 HSP 化 | 1 | 桥/台 |
|
||||
| **T-M5-08 ✗** | 包体/渠道裁剪(可选) | 能力 Provider 按渠道用 HSP 动态特性裁剪;包体瘦身(框架 §9 包体) | T-M3-* | **取消**(2026-06-27):实测 HAP≈1.2MB(debug,release 去 sourceMap 更小),微信/七牛 SDK 仅~400K 源码且均已真集成——原"SDK 大需裁剪"前提不成立;HSP 化徒增构建复杂度、收益微小,属过度优化。如未来需"按渠道出不同能力集的包"再启 | 1 | 桥/台 |
|
||||
|
||||
---
|
||||
|
||||
@@ -224,8 +226,8 @@
|
||||
|
||||
| ID | 目标 | 产出物 | 依赖 | 验收 | 估时 | 角色 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| **T-M6-01** | 视频房真实 | `RoomProvider`(声网 RTC HarmonyOS SDK)替换桩 | M3 完成 | `createRoom/exitRoom/getVideoinfo/DragViewvideoIsshow` + 出站 `getVideoinfo` 真打通;H5 零改动 | 视 SDK | 能 |
|
||||
| **T-M6-02** | 支付真实 | `PayProvider`(浏览器收银台/或 App 内支付)替换桩;如启用 App 内支付则补 `PayuserPaytypePaystate` | M3 完成 | 支付闭环;H5 零改动 | 视 SDK | 能 |
|
||||
| **T-M6-01 ✗** | 视频房真实 | `RoomProvider`(声网 RTC HarmonyOS SDK)替换桩 | M3 完成 | **不实现**(2026-06-27 决定):保持 RoomStubProvider 占位桩,H5 调用不报错不卡死 | 视 SDK | 能 |
|
||||
| **T-M6-02 ✗** | 支付真实 | `PayProvider`(浏览器收银台/或 App 内支付)替换桩 | M3 完成 | **不实现**(2026-06-27 决定):保持 PayStubProvider 占位桩 | 视 SDK | 能 |
|
||||
| **T-M6-03** | 补齐项(可选) | 抖音/快手登录(code)、主动截图 `getCanvasBase64`、QQ/抖音分享回传统一 | 按需 | 对照框架 §6.4 | 视需求 | 能 |
|
||||
|
||||
---
|
||||
|
||||
@@ -29,6 +29,14 @@ export interface AppConfig {
|
||||
other: string;
|
||||
/** 推广/邀请码(写入 app_data.js 的 app_invitationcode)。 */
|
||||
tuiguang: string;
|
||||
/** 七牛 AccessKey(录音上传 token 签名;随包内置,不写死在源码)。 */
|
||||
qiniuAccessKey: string;
|
||||
/** 七牛 SecretKey(录音上传 token 签名)。 */
|
||||
qiniuSecretKey: string;
|
||||
/** 微信开放平台 AppID(WeChatApi 创建 WXApi)。 */
|
||||
wechatAppId: string;
|
||||
/** 微信开放平台 AppSecret(当前端侧不消费,登录走 code+服务端;留位避免硬编码)。 */
|
||||
wechatAppSecret: string;
|
||||
}
|
||||
|
||||
/** 解析中间体(字段全可选,用于从 JSON 归一化)。 */
|
||||
@@ -44,6 +52,10 @@ export interface AppConfigRaw {
|
||||
gameconfig?: string;
|
||||
other?: string;
|
||||
tuiguang?: string;
|
||||
qiniu_access_key?: string;
|
||||
qiniu_secret_key?: string;
|
||||
wechat_app_id?: string;
|
||||
wechat_app_secret?: string;
|
||||
}
|
||||
|
||||
/** 把可选解析体归一化为完整 AppConfig(缺失键 → 空串)。 */
|
||||
@@ -60,5 +72,9 @@ export function normalizeConfig(raw: AppConfigRaw): AppConfig {
|
||||
gameconfig: raw.gameconfig ?? '',
|
||||
other: raw.other ?? '',
|
||||
tuiguang: raw.tuiguang ?? '',
|
||||
qiniuAccessKey: raw.qiniu_access_key ?? '',
|
||||
qiniuSecretKey: raw.qiniu_secret_key ?? '',
|
||||
wechatAppId: raw.wechat_app_id ?? '',
|
||||
wechatAppSecret: raw.wechat_app_secret ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
* 真实字段要点(与旧实现的差异,均为"破坏 H5 适配"的根因):
|
||||
* - 资源下载字段名是 **game_zip**(非 game_download)。
|
||||
* - marketid / app_version / game_version 在 JSON 里是**数字**,匹配/比较需做 string↔number 归一。
|
||||
* - app 升级字段 app_download/app_version 落在 market 层;资源字段 game_zip/game_version
|
||||
* 可出现在 game / channel / market 各层,越深越后、覆盖前层("0"/空视为未设置不覆盖)。
|
||||
* - **4 层逐级回退**:game_zip/game_version 与 app_download/app_version 等所有升级字段,
|
||||
* 可出现在 agent / game / channel / market 任一层(节点类型均 extends VersionFields),
|
||||
* 取最深命中层的非空值、深层缺省则逐级回退到浅层直至根("0"/空视为未设置不覆盖)。
|
||||
*/
|
||||
|
||||
/** JSON 里既可能是字符串也可能是数字的标量(id、版本号等)。 */
|
||||
@@ -33,6 +34,10 @@ export interface VersionFields {
|
||||
game_zip?: string;
|
||||
game_size?: string;
|
||||
showmessage?: string;
|
||||
/** 录音上传七牛回放域名(4 层可携带,逐级回退)。 */
|
||||
audio_domain?: string;
|
||||
/** 录音上传七牛 bucket(4 层可携带,逐级回退)。 */
|
||||
audio_bucket?: string;
|
||||
}
|
||||
|
||||
/** 市场层(marketid 数字命中;携带 app 升级 + 可选资源覆盖)。 */
|
||||
@@ -86,4 +91,8 @@ export interface VersionDecision {
|
||||
gameVersion: number;
|
||||
gameDownload: string;
|
||||
gameSize: string;
|
||||
/** 录音上传七牛回放域名(来源 audio_domain,4 层回退)。 */
|
||||
audioDomain: string;
|
||||
/** 录音上传七牛 bucket(来源 audio_bucket,4 层回退)。 */
|
||||
audioBucket: string;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
/**
|
||||
* 分层版本决策 —— **复刻 Android `chuliversion_1` 真实逻辑**(纯函数,无 IO,便于单测)。
|
||||
* 分层版本决策 —— 4 层逐级回退模型(纯函数,无 IO,便于单测)。
|
||||
*
|
||||
* 遍历真实层级 agentlist → gamelist → channellist → marketlist,逐层下钻匹配
|
||||
* agentid / gameid / channelid / marketid(marketid 数字与本地字符串归一比较)。
|
||||
* 层级(由浅入深 1→4):agentlist(1) → gamelist(2) → channellist(3) → marketlist(4),
|
||||
* 逐层用 agentid / gameid / channelid / marketid 匹配(marketid 数字与本地字符串归一比较)。
|
||||
*
|
||||
* 累积规则(与 Android 一致):
|
||||
* - 资源升级 game_zip/game_version:在 game、channel、market 三层累积,越深越后、覆盖前层;
|
||||
* "0" 或空的 game_version 不覆盖(视为未设置)。
|
||||
* - App 升级 app_download/app_version:取 market 层值。
|
||||
* - showmessage:任一命中层非空即生效(阻断)。
|
||||
* - 最终资源版本与本地 version.xml 比较由调用方决定是否下载 game_zip。
|
||||
* 取值是**统一函数接口**,与具体字段解耦:
|
||||
* 1. matchedLayers() 一次算出命中层链(由浅入深;遇首个不匹配即止),含根伪层(仅 showmessage)。
|
||||
* 2. pickStr()/pickVer() 对**任意字段**用同一规则取值:取最深命中层的非空值,
|
||||
* 深层缺省则逐级回退到浅层直至根。空串(字符串)、"0"/空(版本) 视为未设置、不参与覆盖。
|
||||
* 新增可下发字段 = resolve() 里加一行 selector,无需改取值逻辑。
|
||||
*/
|
||||
import { AgentNode, ChannelNode, GameNode, MarketNode,
|
||||
MatchKeys, RemoteConfig, Scalar, VersionDecision, VersionFields, asStr } from '../config/RemoteConfig';
|
||||
|
||||
/** 版本号转整数(空/"0"/非数字 → 0)。 */
|
||||
/** 从一层节点取某字段标量(不存在 → undefined)。供统一取值函数注入。 */
|
||||
export type FieldSelector = (n: VersionFields) => Scalar | undefined;
|
||||
|
||||
/** 版本号转整数(空、"0"、非数字 → 0)。 */
|
||||
function parseVer(v: Scalar | undefined): number {
|
||||
const s: string = asStr(v);
|
||||
if (s === '') {
|
||||
@@ -24,98 +26,76 @@ function parseVer(v: Scalar | undefined): number {
|
||||
return isNaN(n) ? 0 : n;
|
||||
}
|
||||
|
||||
/** game_version 是否为"有效非零"(用于决定是否覆盖累积值)。 */
|
||||
function hasVer(v: Scalar | undefined): boolean {
|
||||
return parseVer(v) > 0;
|
||||
}
|
||||
|
||||
/** 累积器:复刻 chuliversion_1 的 newgame_zip / newgame_version + app 字段 + showmessage。 */
|
||||
class Acc {
|
||||
gameZip: string = '';
|
||||
gameVersion: number = 0;
|
||||
gameSize: string = '';
|
||||
appDownload: string = '';
|
||||
appVersion: number = 0;
|
||||
appSize: string = '';
|
||||
showmessage: string = '';
|
||||
|
||||
/** 资源字段覆盖(game/channel/market 各层调用):非空 zip 覆盖;非零 version 覆盖。 */
|
||||
mergeGame(n: VersionFields): void {
|
||||
const zip: string = n.game_zip ?? '';
|
||||
if (zip !== '') {
|
||||
this.gameZip = zip;
|
||||
}
|
||||
if (hasVer(n.game_version)) {
|
||||
this.gameVersion = parseVer(n.game_version);
|
||||
}
|
||||
const size: string = n.game_size ?? '';
|
||||
if (size !== '') {
|
||||
this.gameSize = size;
|
||||
}
|
||||
}
|
||||
|
||||
/** App 升级字段(market 层):下载地址与版本均非空才采纳。 */
|
||||
mergeApp(n: VersionFields): void {
|
||||
const dl: string = n.app_download ?? '';
|
||||
const ver: string = asStr(n.app_version);
|
||||
if (dl !== '' && ver !== '') {
|
||||
this.appDownload = dl;
|
||||
this.appVersion = parseVer(n.app_version);
|
||||
this.appSize = n.app_size ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
/** 命中层公告非空即记录(最后非空者生效)。 */
|
||||
mergeMsg(n: VersionFields): void {
|
||||
const msg: string = n.showmessage ?? '';
|
||||
if (msg !== '') {
|
||||
this.showmessage = msg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class VersionResolver {
|
||||
/** 真实层级遍历:agent → game → channel → market。 */
|
||||
static resolve(remote: RemoteConfig, keys: MatchKeys): VersionDecision {
|
||||
const acc: Acc = new Acc();
|
||||
const rootMsg: string = remote.showmessage ?? '';
|
||||
if (rootMsg !== '') {
|
||||
acc.showmessage = rootMsg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 命中层链(由浅入深):根伪层(仅 showmessage) → agent → game → channel → market,
|
||||
* 遇首个不匹配的层即止。后续 pickStr/pickVer 在此链上做"深层覆盖浅层"。
|
||||
*/
|
||||
static matchedLayers(remote: RemoteConfig, keys: MatchKeys): VersionFields[] {
|
||||
const layers: VersionFields[] = [{ showmessage: remote.showmessage }];
|
||||
const agent: AgentNode | undefined =
|
||||
(remote.agentlist ?? []).find((a: AgentNode) => (a.agentid ?? '') === keys.agentid);
|
||||
if (agent !== undefined) {
|
||||
acc.mergeMsg(agent);
|
||||
const game: GameNode | undefined =
|
||||
(agent.gamelist ?? []).find((g: GameNode) => (g.gameid ?? '') === keys.gameid);
|
||||
if (game !== undefined) {
|
||||
acc.mergeMsg(game);
|
||||
acc.mergeGame(game);
|
||||
const channel: ChannelNode | undefined =
|
||||
(game.channellist ?? []).find((c: ChannelNode) => (c.channelid ?? '') === keys.channelid);
|
||||
if (channel !== undefined) {
|
||||
acc.mergeMsg(channel);
|
||||
acc.mergeGame(channel);
|
||||
const market: MarketNode | undefined =
|
||||
(channel.marketlist ?? []).find((m: MarketNode) => asStr(m.marketid) === keys.marketid);
|
||||
if (market !== undefined) {
|
||||
acc.mergeMsg(market);
|
||||
acc.mergeGame(market);
|
||||
acc.mergeApp(market);
|
||||
}
|
||||
}
|
||||
if (agent === undefined) {
|
||||
return layers;
|
||||
}
|
||||
layers.push(agent);
|
||||
const game: GameNode | undefined =
|
||||
(agent.gamelist ?? []).find((g: GameNode) => (g.gameid ?? '') === keys.gameid);
|
||||
if (game === undefined) {
|
||||
return layers;
|
||||
}
|
||||
layers.push(game);
|
||||
const channel: ChannelNode | undefined =
|
||||
(game.channellist ?? []).find((c: ChannelNode) => (c.channelid ?? '') === keys.channelid);
|
||||
if (channel === undefined) {
|
||||
return layers;
|
||||
}
|
||||
layers.push(channel);
|
||||
const market: MarketNode | undefined =
|
||||
(channel.marketlist ?? []).find((m: MarketNode) => asStr(m.marketid) === keys.marketid);
|
||||
if (market !== undefined) {
|
||||
layers.push(market);
|
||||
}
|
||||
return layers;
|
||||
}
|
||||
|
||||
/** 统一取值(字符串字段):层链里取最深命中层的非空值;空串视为未设置。 */
|
||||
static pickStr(layers: VersionFields[], select: FieldSelector): string {
|
||||
let result: string = '';
|
||||
for (let i = 0; i < layers.length; i++) {
|
||||
const s: string = asStr(select(layers[i]));
|
||||
if (s !== '') {
|
||||
result = s; // 由浅入深,越深覆盖越浅
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 统一取值(版本字段):层链里取最深命中层的非零版本;"0"/空视为未设置。 */
|
||||
static pickVer(layers: VersionFields[], select: FieldSelector): number {
|
||||
let result: number = 0;
|
||||
for (let i = 0; i < layers.length; i++) {
|
||||
const v: number = parseVer(select(layers[i]));
|
||||
if (v > 0) {
|
||||
result = v;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 产出版本决策:算一次层链,各字段用统一取值函数 + selector 解析(4 层逐级回退)。 */
|
||||
static resolve(remote: RemoteConfig, keys: MatchKeys): VersionDecision {
|
||||
const layers: VersionFields[] = VersionResolver.matchedLayers(remote, keys);
|
||||
return {
|
||||
showmessage: acc.showmessage,
|
||||
appVersion: acc.appVersion,
|
||||
appDownload: acc.appDownload,
|
||||
appSize: acc.appSize,
|
||||
gameVersion: acc.gameVersion,
|
||||
gameDownload: acc.gameZip,
|
||||
gameSize: acc.gameSize,
|
||||
showmessage: VersionResolver.pickStr(layers, (n: VersionFields) => n.showmessage),
|
||||
appVersion: VersionResolver.pickVer(layers, (n: VersionFields) => n.app_version),
|
||||
appDownload: VersionResolver.pickStr(layers, (n: VersionFields) => n.app_download),
|
||||
appSize: VersionResolver.pickStr(layers, (n: VersionFields) => n.app_size),
|
||||
gameVersion: VersionResolver.pickVer(layers, (n: VersionFields) => n.game_version),
|
||||
gameDownload: VersionResolver.pickStr(layers, (n: VersionFields) => n.game_zip),
|
||||
gameSize: VersionResolver.pickStr(layers, (n: VersionFields) => n.game_size),
|
||||
audioDomain: VersionResolver.pickStr(layers, (n: VersionFields) => n.audio_domain),
|
||||
audioBucket: VersionResolver.pickStr(layers, (n: VersionFields) => n.audio_bucket),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { window } from '@kit.ArkUI';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import { EventBus } from 'common';
|
||||
import { WeChatApi } from 'feature_capabilities';
|
||||
import { ConfigManager } from 'domain_resource';
|
||||
import { AppEvents } from '../routes/AppRoutes';
|
||||
import { installErrorSink } from '../di/ErrorReporter';
|
||||
|
||||
@@ -19,7 +20,12 @@ export default class EntryAbility extends UIAbility {
|
||||
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
|
||||
// 可观测(T-M5-06):安装错误上报 sink,打通 ErrorCenter → APM/Crash 通道
|
||||
installErrorSink();
|
||||
// 微信回调(冷启动经 want 拉回):交 SDK 解析 → 路由到 Share/Login 回调
|
||||
// 微信回调(冷启动经 want 拉回):先从本地 app_config 注入 AppID(不再硬编码),再交 SDK 解析
|
||||
try {
|
||||
WeChatApi.configure(ConfigManager.load(this.context).getLocal().wechatAppId);
|
||||
} catch (e) {
|
||||
hilog.warn(DOMAIN, 'testTag', 'configure WeChat appId failed: %{public}s', JSON.stringify(e));
|
||||
}
|
||||
WeChatApi.getInstance().handleWant(want);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,5 +9,9 @@
|
||||
"weburl": "",
|
||||
"gameconfig": "tsgames.daoqi88.cn-config_test-update_jsonv2_test",
|
||||
"other": "",
|
||||
"tuiguang": ""
|
||||
"tuiguang": "",
|
||||
"qiniu_access_key": "ngN3rFW1j8dn7ZGpATKl7mreaNmp2Ei_l9AIhkIf",
|
||||
"qiniu_secret_key": "6VXav9eqUCORJTYvTFOeTVRonAyk4Hrs-PIZ8jmZ",
|
||||
"wechat_app_id": "wxd2bd650e06bdfe58",
|
||||
"wechat_app_secret": ""
|
||||
}
|
||||
|
||||
@@ -5,16 +5,17 @@ import { util } from '@kit.ArkTS';
|
||||
export default function QiniuTokenTest() {
|
||||
describe('QiniuTokenTest', () => {
|
||||
it('token_has_three_parts_and_valid_policy', 0, async () => {
|
||||
const token: string = await QiniuToken.uploadToken();
|
||||
// AK/SK/bucket 现由调用方注入(本地 app_config + 远程 audio_bucket),单测用固定测试值
|
||||
const token: string = await QiniuToken.uploadToken('AK_TEST', 'SK_TEST', 'bk_test');
|
||||
const parts: string[] = token.split(':');
|
||||
expect(parts.length).assertEqual(3);
|
||||
expect(parts[0]).assertEqual('ngN3rFW1j8dn7ZGpATKl7mreaNmp2Ei_l9AIhkIf');
|
||||
expect(parts[0]).assertEqual('AK_TEST');
|
||||
expect(parts[1].length).assertEqual(28);
|
||||
const helper = new util.Base64Helper();
|
||||
const std: string = parts[2].replace(/-/g, '+').replace(/_/g, '/');
|
||||
const decoded: Uint8Array = helper.decodeSync(std);
|
||||
const policy: string = String.fromCharCode.apply(null, Array.from(decoded));
|
||||
expect(policy.indexOf('"scope":"gameaudio"') >= 0).assertTrue();
|
||||
expect(policy.indexOf('"scope":"bk_test"') >= 0).assertTrue();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -143,5 +143,47 @@ export default function versionResolverTest() {
|
||||
expect(d.gameDownload).assertEqual('http://tsgames.daoqi88.cn/zip2/gamehall.zip');
|
||||
expect(d.appVersion).assertEqual(49);
|
||||
});
|
||||
|
||||
it('agent_level_resource_fallback', 0, () => {
|
||||
// 4 层回退:game_zip/game_version 仅在 agent(1) 层;game/channel/market 命中但不携带 → 回退取 agent 层
|
||||
const remote: RemoteConfig = {
|
||||
agentlist: [{
|
||||
agentid: 'A1', game_version: 200, game_zip: 'agent.zip', game_size: '9.9M',
|
||||
gamelist: [{ gameid: 'G1',
|
||||
channellist: [{ channelid: 'C1',
|
||||
marketlist: [{ marketid: 3 }] }] }],
|
||||
}],
|
||||
};
|
||||
const d: VersionDecision = VersionResolver.resolve(remote, keys);
|
||||
expect(d.gameVersion).assertEqual(200);
|
||||
expect(d.gameDownload).assertEqual('agent.zip');
|
||||
expect(d.gameSize).assertEqual('9.9M');
|
||||
});
|
||||
|
||||
it('app_fields_fall_back_above_market', 0, () => {
|
||||
// 4 层回退:app 字段在 channel(3) 层;market(4) 命中但无 app → 回退取 channel 层
|
||||
const remote: RemoteConfig = {
|
||||
agentlist: [{ agentid: 'A1',
|
||||
gamelist: [{ gameid: 'G1',
|
||||
channellist: [{ channelid: 'C1', app_version: 70, app_download: 'ch.apk',
|
||||
marketlist: [{ marketid: 3 }] }] }] }],
|
||||
};
|
||||
const d: VersionDecision = VersionResolver.resolve(remote, keys);
|
||||
expect(d.appVersion).assertEqual(70);
|
||||
expect(d.appDownload).assertEqual('ch.apk');
|
||||
});
|
||||
|
||||
it('deepest_market_overrides_agent_resource', 0, () => {
|
||||
// 4 层全部携带 game_zip:market(4) 最深,应覆盖 agent/game/channel
|
||||
const remote: RemoteConfig = {
|
||||
agentlist: [{ agentid: 'A1', game_zip: 'a.zip', game_version: 10,
|
||||
gamelist: [{ gameid: 'G1', game_zip: 'g.zip', game_version: 20,
|
||||
channellist: [{ channelid: 'C1', game_zip: 'c.zip', game_version: 30,
|
||||
marketlist: [{ marketid: 3, game_zip: 'm.zip', game_version: 40 }] }] }] }],
|
||||
};
|
||||
const d: VersionDecision = VersionResolver.resolve(remote, keys);
|
||||
expect(d.gameVersion).assertEqual(40);
|
||||
expect(d.gameDownload).assertEqual('m.zip');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export { AmrRecorder } from './src/main/ets/providers/AmrRecorder';
|
||||
export { NavProvider } from './src/main/ets/providers/NavProvider';
|
||||
|
||||
// 微信 SDK 封装(EntryAbility 处理回调 want 需用)
|
||||
export { WeChatApi, WX_APP_ID } from './src/main/ets/wx/WeChatApi';
|
||||
export { WeChatApi } from './src/main/ets/wx/WeChatApi';
|
||||
|
||||
// 分享文本拼接纯函数(供单测)
|
||||
export { buildShareText } from './src/main/ets/share/ShareText';
|
||||
|
||||
@@ -23,7 +23,7 @@ import { common, abilityAccessCtrl } from '@kit.AbilityKit';
|
||||
import { fileIo } from '@kit.CoreFileKit';
|
||||
import { BridgeController } from 'feature_bridge';
|
||||
import { InboundHandlers, OutboundHandlers, MediaTypeAudioReq, SrcIsLoopReq, GetAudioUrlResp } from 'contracts';
|
||||
import { ConfigManager, ResourceManager } from 'domain_resource';
|
||||
import { ConfigManager, ResourceManager, VersionDecision, AppConfig } from 'domain_resource';
|
||||
import { FileSystem, QiniuUploader, QiniuToken } from 'platform';
|
||||
import { Logger, EventBus, EventPayload, AudioEvents, ShowRecordingRequest, RecordResult } from 'common';
|
||||
import { CapabilityContext, CapabilityProvider } from '../core/CapabilityProvider';
|
||||
@@ -133,15 +133,24 @@ export class AudioProvider implements CapabilityProvider {
|
||||
if (req.audiourl === '') {
|
||||
return;
|
||||
}
|
||||
const user: string = req.user;
|
||||
// user 可能被 H5 以数字(座位号)下发;须强转字符串再用于回投——对齐 Android json.getString("user")+""+uservoice。
|
||||
// 否则 number 的 .length 为 undefined,桥 doSend 判空丢弃 data → H5 gameui_play_voice 收到 undefined 报错(reading '0')、座位动画不播。
|
||||
const user: string = (req.user === undefined || req.user === null) ? '' : String(req.user);
|
||||
// 打断正在播的旧语音:先通知 H5 停旧座位动画,再释放(对齐"同一时刻只播一条")。
|
||||
if (this.voicePlayer !== undefined && this.currentVoiceUser !== '') {
|
||||
this.bridge?.callHandler(OutboundHandlers.GameUiStopVoice, this.currentVoiceUser);
|
||||
}
|
||||
this.releaseVoice();
|
||||
// 对齐 Android play():进入即立刻同步通知 H5 开始播放(座位说话动画),再创建播放器;
|
||||
// 不等到 prepared 才报——网络 AMR 加载可能延迟数百 ms,晚报会被认为"没调/动画起得晚"。
|
||||
this.currentVoiceUser = user;
|
||||
this.bridge?.callHandler(OutboundHandlers.GameUiPlayVoice, user);
|
||||
media.createAVPlayer().then((player: media.AVPlayer) => {
|
||||
if (this.disposed) {
|
||||
player.release(); // 创建期间已销毁:直接释放,勿播放/勿挂回调(防泄漏 + 打已弃桥)
|
||||
if (this.disposed || this.currentVoiceUser !== user) {
|
||||
player.release(); // 创建期间已销毁或已被新语音取代:释放孤儿播放器(旧 user 的 stop 已发)
|
||||
return;
|
||||
}
|
||||
this.voicePlayer = player;
|
||||
this.currentVoiceUser = user;
|
||||
player.on('stateChange', (state: string) => {
|
||||
if (state === 'initialized') {
|
||||
player.prepare();
|
||||
@@ -149,19 +158,28 @@ export class AudioProvider implements CapabilityProvider {
|
||||
player.loop = false;
|
||||
player.setVolume(this.effVolume());
|
||||
player.play();
|
||||
this.bridge?.callHandler(OutboundHandlers.GameUiPlayVoice, user);
|
||||
} else if (state === 'completed') {
|
||||
this.bridge?.callHandler(OutboundHandlers.GameUiStopVoice, user);
|
||||
this.releaseVoice();
|
||||
this.endVoice(user); // 播放完成:gameui_stop_voice + 释放
|
||||
}
|
||||
});
|
||||
player.on('error', (e: BusinessError) => {
|
||||
this.log.w(`voice play error: ${e.code} ${e.message}`);
|
||||
this.bridge?.callHandler(OutboundHandlers.GameUiStopVoice, user);
|
||||
this.releaseVoice();
|
||||
this.endVoice(user);
|
||||
});
|
||||
player.url = req.audiourl;
|
||||
}).catch((e: BusinessError) => this.log.w(`createAVPlayer(voice) failed: ${e.message}`));
|
||||
}).catch((e: BusinessError) => {
|
||||
this.log.w(`createAVPlayer(voice) failed: ${e.message}`);
|
||||
this.endVoice(user); // 创建失败也补 stop_voice,避免座位动画卡在"说话中"
|
||||
});
|
||||
}
|
||||
|
||||
/** 语音播放完成/出错:通知 H5 停座位动画(gameui_stop_voice) + 释放播放器。一次性,防重复。 */
|
||||
private endVoice(user: string): void {
|
||||
if (this.currentVoiceUser !== user) {
|
||||
return; // 已被新语音取代或已结束
|
||||
}
|
||||
this.bridge?.callHandler(OutboundHandlers.GameUiStopVoice, user);
|
||||
this.releaseVoice();
|
||||
}
|
||||
|
||||
/** 播放本地音效/背景乐:<urlpath>/<gameContentDir>/assets/wav/<src>。isloop<0 停止该 src。 */
|
||||
@@ -374,23 +392,37 @@ export class AudioProvider implements CapabilityProvider {
|
||||
const r: RecordResult | undefined = p as RecordResult | undefined;
|
||||
const action: string = r !== undefined ? r.action : 'cancel';
|
||||
const timeSec: number = r !== undefined ? r.timeSec : 0;
|
||||
// 对齐 Android reset():任何结束(发送/取消/太短)松手瞬间都先立刻回投一次空 getaudiourl,
|
||||
// H5 录音状态机据此复位;正常发送再在上传完成后补发真实 URL(复刻 Android upload()+reset() 双回投)。
|
||||
// 缺这次空回投会导致 H5 收到真实 URL 也不处理 → 录完不播放。
|
||||
this.emitAudioUrl('', 0);
|
||||
if (action === 'send') {
|
||||
const key: string = this.recorder.key();
|
||||
this.recorder.stop().then((filePath: string) => this.doUpload(filePath, key, timeSec));
|
||||
} else {
|
||||
this.recorder.cancel();
|
||||
this.emitAudioUrl('', 0);
|
||||
}
|
||||
}
|
||||
|
||||
private async doUpload(filePath: string, key: string, timeSec: number): Promise<void> {
|
||||
const ctx: common.UIAbilityContext | undefined = this.context;
|
||||
if (ctx === undefined || filePath === '') {
|
||||
this.emitAudioUrl('', 0);
|
||||
return;
|
||||
const cfg: ConfigManager | undefined = this.config;
|
||||
if (ctx === undefined || cfg === undefined || filePath === '') {
|
||||
return; // 已发过空回投(reset 语义);上传不了则保持"无语音"
|
||||
}
|
||||
try {
|
||||
const token: string = await QiniuToken.uploadToken();
|
||||
// AK/SK 取本地 app_config;domain/bucket 取远程配置(audio_domain/audio_bucket,4 层回退)。
|
||||
const local: AppConfig = cfg.getLocal();
|
||||
const ak: string = local.qiniuAccessKey;
|
||||
const sk: string = local.qiniuSecretKey;
|
||||
const decision: VersionDecision = await cfg.resolve();
|
||||
const bucket: string = decision.audioBucket;
|
||||
const domain: string = decision.audioDomain;
|
||||
if (ak === '' || sk === '' || bucket === '' || domain === '') {
|
||||
this.log.w(`qiniu config missing: ak/sk(local) ok=${ak !== '' && sk !== ''}, bucket='${bucket}' domain='${domain}'(remote)`);
|
||||
return;
|
||||
}
|
||||
const token: string = await QiniuToken.uploadToken(ak, sk, bucket);
|
||||
const ok: boolean = await QiniuUploader.upload(ctx, filePath, key, () => Promise.resolve(token));
|
||||
try {
|
||||
fileIo.unlinkSync(filePath);
|
||||
@@ -398,14 +430,12 @@ export class AudioProvider implements CapabilityProvider {
|
||||
this.log.w(`cleanup uploaded file failed: ${(e as Error).message}`);
|
||||
}
|
||||
if (ok) {
|
||||
this.emitAudioUrl(`http://gameaudio.daoqi88.cn/${key}`, timeSec);
|
||||
this.emitAudioUrl(`http://${domain}/${key}`, timeSec);
|
||||
} else {
|
||||
this.log.w(`qiniu upload failed key=${key}`);
|
||||
this.emitAudioUrl('', 0);
|
||||
this.log.w(`qiniu upload failed key=${key}`); // 已发空回投,不再重复
|
||||
}
|
||||
} catch (e) {
|
||||
this.log.w(`upload error: ${(e as BusinessError).message}`);
|
||||
this.emitAudioUrl('', 0);
|
||||
this.log.w(`upload error: ${(e as BusinessError).message}`); // 已发空回投,不再重复
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,26 +10,35 @@ import {
|
||||
} from '@tencent/wechat_open_sdk';
|
||||
import { Logger } from 'common';
|
||||
|
||||
/** 微信 AppID(公开标识,契约 §13)。 */
|
||||
export const WX_APP_ID: string = 'wxd2bd650e06bdfe58';
|
||||
|
||||
export type AuthRespCallback = (resp: SendAuthResp) => void;
|
||||
|
||||
export class WeChatApi {
|
||||
private static readonly log: Logger = Logger.tag('WeChatApi');
|
||||
private static inst: WeChatApi | undefined = undefined;
|
||||
/** 微信 AppID(从本地 app_config.wechat_app_id 注入;须在首次 getInstance 前 configure)。 */
|
||||
private static appId: string = '';
|
||||
private readonly api: WXApi;
|
||||
private readonly handler: WXApiEventHandler;
|
||||
private authCb: AuthRespCallback | undefined = undefined;
|
||||
|
||||
private constructor() {
|
||||
this.api = WXAPIFactory.createWXAPI(WX_APP_ID);
|
||||
this.api = WXAPIFactory.createWXAPI(WeChatApi.appId);
|
||||
this.handler = {
|
||||
onReq: (_req: BaseReq) => { },
|
||||
onResp: (resp: BaseResp) => this.routeResp(resp),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 注入微信 AppID(来自本地 app_config,不再硬编码)。须在首次 getInstance() 之前调用,
|
||||
* 通常在 EntryAbility.onCreate 处理微信回调 want 之前。空串/重复 configure 安全。
|
||||
*/
|
||||
static configure(appId: string): void {
|
||||
if (appId !== '') {
|
||||
WeChatApi.appId = appId;
|
||||
}
|
||||
}
|
||||
|
||||
static getInstance(): WeChatApi {
|
||||
if (WeChatApi.inst === undefined) {
|
||||
WeChatApi.inst = new WeChatApi();
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
/**
|
||||
* 七牛 uploadToken 端侧生成(设计 §7)。
|
||||
* ⚠️ 刻意对齐原 Android 工程:AK/SK 端侧硬编码、不走服务端——违背 CLAUDE.md 附录B 加固红线,
|
||||
* 已与用户明确确认(对齐原工程)。token 算法见七牛官方:AK:urlsafeB64(HMAC_SHA1(SK,policy)):policy。
|
||||
* 七牛 uploadToken 端侧生成(设计 §7)。token 算法见七牛官方:AK:urlsafeB64(HMAC_SHA1(SK,policy)):policy。
|
||||
* ⚠️ 不走服务端(对齐原工程):AK/SK 由调用方从本地 app_config 注入、bucket 从远程配置注入,
|
||||
* 本类不再持有任何密钥常量。AK/SK 入端仍违背 CLAUDE.md 附录B 加固红线,已与用户确认。
|
||||
*/
|
||||
import { cryptoFramework } from '@kit.CryptoArchitectureKit';
|
||||
import { buffer, util } from '@kit.ArkTS';
|
||||
|
||||
export class QiniuToken {
|
||||
private static readonly ACCESS_KEY: string = 'ngN3rFW1j8dn7ZGpATKl7mreaNmp2Ei_l9AIhkIf';
|
||||
private static readonly SECRET_KEY: string = '6VXav9eqUCORJTYvTFOeTVRonAyk4Hrs-PIZ8jmZ';
|
||||
// bucket 取 'gameaudio'(有 d,与回放域名 gameaudio.daoqi88.cn / Android NewwebviewActivity 一致);
|
||||
// 原 webviewActivity 里的 'gameauio'(无 d) 是拼写错误,七牛上无该 bucket(区域查询 no such bucket)。
|
||||
private static readonly BUCKET: string = 'gameaudio';
|
||||
|
||||
/** 生成 uploadToken(deadline=now+1h)。 */
|
||||
static async uploadToken(): Promise<string> {
|
||||
/**
|
||||
* 生成 uploadToken(deadline=now+1h)。
|
||||
* @param accessKey 七牛 AccessKey(本地 app_config.qiniu_access_key)
|
||||
* @param secretKey 七牛 SecretKey(本地 app_config.qiniu_secret_key)
|
||||
* @param bucket 存储空间名(远程配置 audio_bucket)
|
||||
*/
|
||||
static async uploadToken(accessKey: string, secretKey: string, bucket: string): Promise<string> {
|
||||
const deadline: number = Math.floor(Date.now() / 1000) + 3600;
|
||||
const policy: string = `{"scope":"${QiniuToken.BUCKET}","deadline":${deadline}}`;
|
||||
const policy: string = `{"scope":"${bucket}","deadline":${deadline}}`;
|
||||
const encodedPolicy: string =
|
||||
QiniuToken.urlsafeBase64(new Uint8Array(buffer.from(policy, 'utf-8').buffer));
|
||||
const sign: Uint8Array = await QiniuToken.hmacSha1(QiniuToken.SECRET_KEY, encodedPolicy);
|
||||
const sign: Uint8Array = await QiniuToken.hmacSha1(secretKey, encodedPolicy);
|
||||
const encodedSign: string = QiniuToken.urlsafeBase64(sign);
|
||||
return `${QiniuToken.ACCESS_KEY}:${encodedSign}:${encodedPolicy}`;
|
||||
return `${accessKey}:${encodedSign}:${encodedPolicy}`;
|
||||
}
|
||||
|
||||
private static async hmacSha1(secret: string, message: string): Promise<Uint8Array> {
|
||||
|
||||
Reference in New Issue
Block a user