wx2tg-puppet 6.0.17 → 6.0.18-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -2
- package/VERSION +1 -1
- package/dist/core/Contact.d.ts +3 -0
- package/dist/core/Contact.js +12 -2
- package/dist/database/data-source.js +4 -0
- package/dist/database/migrations/1760000000000-OfficialAccountMetadata.d.ts +6 -0
- package/dist/database/migrations/1760000000000-OfficialAccountMetadata.js +34 -0
- package/dist/entities/ContactEntity.d.ts +2 -0
- package/dist/entities/ContactEntity.js +8 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +92 -26
- package/dist/utils/OfficialAccountMetadata.d.ts +17 -0
- package/dist/utils/OfficialAccountMetadata.js +168 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
微信机器人核心库,供 `wechat2tg` 调用 `wx2tg-server`。
|
|
4
4
|
|
|
5
|
-
## v6
|
|
5
|
+
## v6 当前稳定配套版本
|
|
6
6
|
|
|
7
|
-
| 仓库 | 维护分支 |
|
|
7
|
+
| 仓库 | 维护分支 | 当前稳定 Git 标签 | npm / Docker 版本 |
|
|
8
8
|
| --- | --- | --- | --- |
|
|
9
9
|
| wechat2tg | `v6` | `v6.0.39` | 包版本 `6.0.39`;镜像 `finalpi/wechat2tg:v6.0.39` |
|
|
10
10
|
| wx2tg-puppet | `v6` | `v6.0.17` | npm 格式包 `wx2tg-puppet@6.0.17` |
|
|
@@ -25,6 +25,16 @@ npm pack
|
|
|
25
25
|
|
|
26
26
|
`wechat2tg` 可继续锁定同版本本地制品以保证构建可重现;若切换 registry 来源,必须重新生成 lockfile 并校验制品,不要手工伪造 integrity。
|
|
27
27
|
|
|
28
|
+
## v6.0.18-dev.1:登录不再等待全量联系人(开发预览)
|
|
29
|
+
|
|
30
|
+
- 修复 `loginSuccess()` 等待所有联系人详情后才启动消息接收、发出登录事件的时序问题。恢复登录和扫码登录先启动接收与心跳,目录任务在后台处理;启动 Sync 取得的消息继续分发,不丢弃。
|
|
31
|
+
- `start()` / `loginTwice()` 完成不再代表全量目录同步结束。依赖初次完整目录的调用方可显式 `await bot.waitForContactSync()`;这是等待当前轮次,不保证失败项已成功,也不启动额外同步。
|
|
32
|
+
- `contacts-updated` 事件通知当前目录轮次结束(也包括后续分页失败后的部分更新),供调用方重新读取目录;不重新触发登录事件。
|
|
33
|
+
- 保留每页最多 4 个详情请求、逐项失败隔离及分页等待。跨登录任务串行衔接,旧任务不再启动剩余详情 / 下一页或发出完成通知。
|
|
34
|
+
- 联系人接口失败不主动退出账号;会话恢复仍由消息 Sync 的原有单次恢复逻辑负责。清缓存和切换扫码账号前等待旧在途写入结束,避免清空后被旧任务重新写入。
|
|
35
|
+
- 208 项离线测试通过,包括安全菜单元数据、联系人保持挂起时接收已就绪、积压消息不丢、退出 / 重新登录、失败处理和缓存清理顺序。未访问真实账号,不代表线上延迟已验收。
|
|
36
|
+
- 此开发包供 Bot `v6.0.40-dev.4` 使用;dev tag 只发布 npm `v6-dev` 通道,以发布工作流成功为准。不替换已发布的 `6.0.17` 制品,也不代表线上完整验收。
|
|
37
|
+
|
|
28
38
|
## v6.0.17:联系人初始化修复
|
|
29
39
|
|
|
30
40
|
- 修复初始化时异步详情查询未等待、未捕获,导致 `Contact detail response did not contain the requested contact` 等异常刷屏。
|
|
@@ -64,6 +74,18 @@ HTTP 200 仅表示 puppet 处理并同步发出了事件,不代表异步业务
|
|
|
64
74
|
|
|
65
75
|
此 SDK 管理 `puppet.db` 的联系人 schema。其他读取连接不要再次用不完整的镜像实体执行 schema synchronize,否则会删除新增字段。服务端目录接口的 `profile.verify_flag` 可用于批量补齐,但必须验证账号范围,不覆盖更新的已知值。
|
|
66
76
|
|
|
77
|
+
## 公众号类型与只读菜单元数据(6.0.18-dev.1 开发预览)
|
|
78
|
+
|
|
79
|
+
- `Contact.serviceType: number | null` 保留 `ContactList[0].CustomizedInfo.ExternalInfo` JSON 字符串中**顶层** `ServiceType` 的 uint32 原值(也接受规范十进制字符串)。仅详情本次明确带品牌位 8 的 `VerifyFlag` 时解析;不根据名称、ID、`BrandFlag` 或任意嵌套 `Type` 猜测。缺失/非法表示未知或保留旧值,绝不推断为订阅号/type 0。
|
|
80
|
+
- `Contact.officialMenu: OfficialMenu | null` 返回经过重新校验的菜单。根入口导出 `OfficialMenu`、`OfficialMenuButton` 类型及纯函数 `parseStoredOfficialMenu(value: unknown): OfficialMenu | null`,供其他只读 DB 连接解析 TEXT 缓存;每次返回新的安全对象。无法解析是 `null`;明确空菜单为 `{buttons: []}`,两者不可混同。
|
|
81
|
+
- `OfficialMenu` 为 `{buttons: OfficialMenuButton[]}`,按钮仅含 `name`、`kind: group | url | unsupported`,以及相应的 `children`、`url` 或 `reason: event | miniapp | wechat | unknown`。菜单组来自 type 0;普通网址只读取 type 2 的 **value**,不把 opaque key 当网址。type 1 事件和 type 5 小程序显示为不可执行项,不提供点击、授权或小程序执行 API。
|
|
82
|
+
- 仅允许无凭据/控制字符的 HTTP(S) 地址;微信协议、脚本、data 及 OAuth/授权地址均变成 `unsupported/wechat`,缓存中的 URL 同样重新验证。不保存原始 key、uin、appid、native_url、版本或其他任意字段。
|
|
83
|
+
- JSON/规范化结果最多 64 KiB、菜单最多 3 层/100 个节点、名称最多 256 字符、URL 最多 4096 字符。原始按钮 ID 可能全部相同,因此不作为身份或执行依据且不保存;超限、相同同级按钮、歧义子树、非法结构均拒绝整个菜单,不截断后冒充完整数据。缺失或非法菜单不覆盖已知缓存;显式空 `button_list` 才清除旧菜单。
|
|
84
|
+
|
|
85
|
+
Schema 仍由 SDK 管理:`contact.serviceType INTEGER NULL` 与 `contact.officialMenu TEXT NULL` 都无默认值。迁移顺序为已有 `contact-verify-flag.sql` schema → `OfficialAccountMetadata1760000000000`(`src/database/migrations/1760000000000-OfficialAccountMetadata.ts`)→ 原有 TypeORM synchronize。新迁移检查现有列并直接执行 additive `ALTER TABLE ADD COLUMN`,避免 SQLite 的 addColumn helper 重建旧联系人表;fresh DB 无 contact 表时跳过 ALTER,由后续 synchronize 创建完整 schema。已执行迁移记入 TypeORM migrations 表,重启不重复;不提供会丢失元数据的自动 down。`migrations/contact-official-metadata.sql` 仅为对应增量的版本控制参考,不需手工修改远程 DB。Bot 镜像连接必须保持 `synchronize=false`。
|
|
86
|
+
|
|
87
|
+
离线测试 `test/official-account-metadata.test.cjs` 使用合成数据覆盖真实详情入库路径、未知字段保留、菜单/URL 安全、隐私字段剔除、边界,以及旧/新 SQLite 初始化和旧行无重建保留。
|
|
88
|
+
|
|
67
89
|
## 接龙消息
|
|
68
90
|
|
|
69
91
|
`Message.Type.Solitaire` 对应外层 `49`、应用子类型 `53`。`Message.text()` 返回接龙完整可见标题、名单和尾注;原始 `Content`、`_text`、`_xml` 保持不变,不把内部 `solitaire_info` 当正文输出。CDATA、实体转义、前导零、Unicode 和换行保留;无法安全解析时返回明确的接龙占位提示。此功能只负责识别/读取,不自动参与、发送或重放历史接龙。
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
6.0.
|
|
1
|
+
6.0.18-dev.1
|
package/dist/core/Contact.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ContactEntity } from "../entities/ContactEntity";
|
|
2
2
|
import { ResponseMsg } from "./Message";
|
|
3
|
+
import { OfficialMenu } from '../utils/OfficialAccountMetadata';
|
|
3
4
|
export type Message = any;
|
|
4
5
|
export type FileBox = any;
|
|
5
6
|
export type ContactType = any;
|
|
@@ -12,6 +13,8 @@ export declare class Contact {
|
|
|
12
13
|
get nickName(): string;
|
|
13
14
|
get verifyFlag(): number | null;
|
|
14
15
|
isOfficialAccount(): boolean | undefined;
|
|
16
|
+
get serviceType(): number | null;
|
|
17
|
+
get officialMenu(): OfficialMenu | null;
|
|
15
18
|
/**
|
|
16
19
|
* Contact id (readonly)
|
|
17
20
|
*/
|
package/dist/core/Contact.js
CHANGED
|
@@ -15,6 +15,7 @@ const FriendApi_1 = require("../api/FriendApi");
|
|
|
15
15
|
const config_service_1 = require("../services/config.service");
|
|
16
16
|
const common_1 = require("../action/common");
|
|
17
17
|
const ContactClassification_1 = require("../utils/ContactClassification");
|
|
18
|
+
const OfficialAccountMetadata_1 = require("../utils/OfficialAccountMetadata");
|
|
18
19
|
const detailFetches = new Map();
|
|
19
20
|
const unknownRefreshAttempts = new Map();
|
|
20
21
|
const UNKNOWN_REFRESH_INTERVAL_MS = 30000;
|
|
@@ -36,6 +37,14 @@ class Contact {
|
|
|
36
37
|
isOfficialAccount() {
|
|
37
38
|
return (0, ContactClassification_1.isOfficialAccountVerifyFlag)(this.verifyFlag);
|
|
38
39
|
}
|
|
40
|
+
get serviceType() {
|
|
41
|
+
var _a;
|
|
42
|
+
return this.isOfficialAccount() === true ? (0, ContactClassification_1.normalizeContactVerifyFlag)((_a = this.contact) === null || _a === void 0 ? void 0 : _a.serviceType) : null;
|
|
43
|
+
}
|
|
44
|
+
get officialMenu() {
|
|
45
|
+
var _a;
|
|
46
|
+
return this.isOfficialAccount() === true ? (0, OfficialAccountMetadata_1.parseStoredOfficialMenu)((_a = this.contact) === null || _a === void 0 ? void 0 : _a.officialMenu) : null;
|
|
47
|
+
}
|
|
39
48
|
constructor(contact) {
|
|
40
49
|
this.contact = null;
|
|
41
50
|
this.contact = contact;
|
|
@@ -228,7 +237,7 @@ class Contact {
|
|
|
228
237
|
}
|
|
229
238
|
static loadContactDetail(Wxid, Towxid) {
|
|
230
239
|
return __awaiter(this, void 0, void 0, function* () {
|
|
231
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1;
|
|
240
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2;
|
|
232
241
|
const detail = yield FriendApi_1.friendApi.GetContractDetail({
|
|
233
242
|
"ChatRoom": "",
|
|
234
243
|
"Towxids": Towxid,
|
|
@@ -243,7 +252,8 @@ class Contact {
|
|
|
243
252
|
throw new Error('Contact detail response did not contain the requested contact');
|
|
244
253
|
}
|
|
245
254
|
const verifyFlag = (0, ContactClassification_1.normalizeContactVerifyFlag)(detailDataSave.VerifyFlag);
|
|
246
|
-
const
|
|
255
|
+
const metadata = (0, OfficialAccountMetadata_1.decodeOfficialAccountMetadata)((_e = detailDataSave.CustomizedInfo) === null || _e === void 0 ? void 0 : _e.ExternalInfo, verifyFlag);
|
|
256
|
+
const contact = Object.assign(Object.assign(Object.assign(Object.assign({ userName }, (verifyFlag === null ? {} : { verifyFlag })), (metadata.serviceType === undefined ? {} : { serviceType: metadata.serviceType })), (metadata.officialMenu === undefined ? {} : { officialMenu: JSON.stringify(metadata.officialMenu) })), { nickName: (_g = (_f = detailDataSave.NickName) === null || _f === void 0 ? void 0 : _f.string) !== null && _g !== void 0 ? _g : 'no name', pyInitial: (_j = (_h = detailDataSave.Pyinitial) === null || _h === void 0 ? void 0 : _h.string) !== null && _j !== void 0 ? _j : '', quanPin: (_l = (_k = detailDataSave.QuanPin) === null || _k === void 0 ? void 0 : _k.string) !== null && _l !== void 0 ? _l : '', sex: (_m = detailDataSave.Sex) !== null && _m !== void 0 ? _m : null, remark: (_p = (_o = detailDataSave.Remark) === null || _o === void 0 ? void 0 : _o.string) !== null && _p !== void 0 ? _p : '', remarkPyInitial: (_r = (_q = detailDataSave.RemarkPyinitial) === null || _q === void 0 ? void 0 : _q.string) !== null && _r !== void 0 ? _r : '', remarkQuanPin: (_t = (_s = detailDataSave.RemarkQuanPin) === null || _s === void 0 ? void 0 : _s.string) !== null && _t !== void 0 ? _t : '', signature: (_u = detailDataSave.Signature) !== null && _u !== void 0 ? _u : '', alias: (_v = detailDataSave.Alias) !== null && _v !== void 0 ? _v : '', snsBgImg: (_x = (_w = detailDataSave.SnsUserInfo) === null || _w === void 0 ? void 0 : _w.SnsBgimgId) !== null && _x !== void 0 ? _x : '', country: (_y = detailDataSave.Country) !== null && _y !== void 0 ? _y : '', bigHeadImgUrl: (_z = detailDataSave.BigHeadImgUrl) !== null && _z !== void 0 ? _z : '', smallHeadImgUrl: (_0 = detailDataSave.SmallHeadImgUrl) !== null && _0 !== void 0 ? _0 : '', province: (_1 = detailDataSave.Province) !== null && _1 !== void 0 ? _1 : '', city: (_2 = detailDataSave.City) !== null && _2 !== void 0 ? _2 : '' });
|
|
247
257
|
return contact_service_1.contactService.upsertByUsername(userName, contact);
|
|
248
258
|
});
|
|
249
259
|
}
|
|
@@ -8,11 +8,15 @@ const config_entity_1 = require("../entities/config.entity");
|
|
|
8
8
|
const ContactEntity_1 = require("../entities/ContactEntity");
|
|
9
9
|
const RoomEntity_1 = require("../entities/RoomEntity");
|
|
10
10
|
const MessageEntity_1 = require("../entities/MessageEntity");
|
|
11
|
+
const _1760000000000_OfficialAccountMetadata_1 = require("./migrations/1760000000000-OfficialAccountMetadata");
|
|
11
12
|
function createAppDataSource(databasePath) {
|
|
12
13
|
exports.AppDataSource = new typeorm_1.DataSource({
|
|
13
14
|
type: 'sqlite',
|
|
14
15
|
database: databasePath,
|
|
15
16
|
synchronize: true,
|
|
17
|
+
// TypeORM runs migrations before synchronize: upgrade old caches in place.
|
|
18
|
+
migrationsRun: true,
|
|
19
|
+
migrations: [_1760000000000_OfficialAccountMetadata_1.OfficialAccountMetadata1760000000000],
|
|
16
20
|
logging: false,
|
|
17
21
|
entities: [config_entity_1.Config, ContactEntity_1.ContactEntity, RoomEntity_1.RoomEntity, MessageEntity_1.MessageEntity],
|
|
18
22
|
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from 'typeorm';
|
|
2
|
+
/** Add columns in place: TypeORM's SQLite addColumn helper rebuilds tables. */
|
|
3
|
+
export declare class OfficialAccountMetadata1760000000000 implements MigrationInterface {
|
|
4
|
+
up(queryRunner: QueryRunner): Promise<void>;
|
|
5
|
+
down(): Promise<void>;
|
|
6
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.OfficialAccountMetadata1760000000000 = void 0;
|
|
13
|
+
/** Add columns in place: TypeORM's SQLite addColumn helper rebuilds tables. */
|
|
14
|
+
class OfficialAccountMetadata1760000000000 {
|
|
15
|
+
up(queryRunner) {
|
|
16
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
17
|
+
if (!(yield queryRunner.hasTable('contact')))
|
|
18
|
+
return; // Fresh SDK schema is created by synchronize afterwards.
|
|
19
|
+
const columns = yield queryRunner.query('PRAGMA table_info("contact")');
|
|
20
|
+
if (!columns.some(column => column.name === 'serviceType')) {
|
|
21
|
+
yield queryRunner.query('ALTER TABLE "contact" ADD COLUMN "serviceType" integer');
|
|
22
|
+
}
|
|
23
|
+
if (!columns.some(column => column.name === 'officialMenu')) {
|
|
24
|
+
yield queryRunner.query('ALTER TABLE "contact" ADD COLUMN "officialMenu" text');
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
down() {
|
|
29
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
30
|
+
throw new Error('Additive contact metadata migration cannot be reverted without losing cached metadata');
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
exports.OfficialAccountMetadata1760000000000 = OfficialAccountMetadata1760000000000;
|
|
@@ -39,6 +39,14 @@ __decorate([
|
|
|
39
39
|
(0, typeorm_1.Column)('integer', { nullable: true }),
|
|
40
40
|
__metadata("design:type", Object)
|
|
41
41
|
], ContactEntity.prototype, "verifyFlag", void 0);
|
|
42
|
+
__decorate([
|
|
43
|
+
(0, typeorm_1.Column)('integer', { nullable: true }),
|
|
44
|
+
__metadata("design:type", Object)
|
|
45
|
+
], ContactEntity.prototype, "serviceType", void 0);
|
|
46
|
+
__decorate([
|
|
47
|
+
(0, typeorm_1.Column)('text', { nullable: true }),
|
|
48
|
+
__metadata("design:type", Object)
|
|
49
|
+
], ContactEntity.prototype, "officialMenu", void 0);
|
|
42
50
|
__decorate([
|
|
43
51
|
(0, typeorm_1.Column)('text', { nullable: true }),
|
|
44
52
|
__metadata("design:type", String)
|
package/dist/index.d.ts
CHANGED
|
@@ -38,6 +38,8 @@ export declare class WxBot extends EventEmitter {
|
|
|
38
38
|
private syncTask;
|
|
39
39
|
private syncRequested;
|
|
40
40
|
private loginRecoveryInProgress;
|
|
41
|
+
private contactSyncTask;
|
|
42
|
+
private contactSyncGeneration;
|
|
41
43
|
private syncFailureCount;
|
|
42
44
|
private httpServer;
|
|
43
45
|
private loggingOut;
|
|
@@ -58,6 +60,9 @@ export declare class WxBot extends EventEmitter {
|
|
|
58
60
|
* @param currentChatroomSeq 当前群聊联系人序列号
|
|
59
61
|
*/
|
|
60
62
|
private fetchAllContacts;
|
|
63
|
+
private startContactSync;
|
|
64
|
+
/** Explicit directory barrier for callers that need the current background pass. */
|
|
65
|
+
waitForContactSync(): Promise<void>;
|
|
61
66
|
private loginSuccess;
|
|
62
67
|
private startCallbackServer;
|
|
63
68
|
private handleCallbackMessage;
|
|
@@ -86,4 +91,6 @@ export declare class WxBot extends EventEmitter {
|
|
|
86
91
|
}
|
|
87
92
|
export { Contact, Room, Message };
|
|
88
93
|
export { normalizeContactVerifyFlag, isOfficialAccountVerifyFlag } from './utils/ContactClassification';
|
|
94
|
+
export { parseStoredOfficialMenu } from './utils/OfficialAccountMetadata';
|
|
95
|
+
export type { OfficialMenu, OfficialMenuButton } from './utils/OfficialAccountMetadata';
|
|
89
96
|
export { Emoji, Voice, WeVideo, UrlLink };
|
package/dist/index.js
CHANGED
|
@@ -42,7 +42,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
42
42
|
});
|
|
43
43
|
};
|
|
44
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
-
exports.UrlLink = exports.WeVideo = exports.Voice = exports.Emoji = exports.isOfficialAccountVerifyFlag = exports.normalizeContactVerifyFlag = exports.Message = exports.Room = exports.Contact = exports.WxBot = exports.AppDataSource = void 0;
|
|
45
|
+
exports.UrlLink = exports.WeVideo = exports.Voice = exports.Emoji = exports.parseStoredOfficialMenu = exports.isOfficialAccountVerifyFlag = exports.normalizeContactVerifyFlag = exports.Message = exports.Room = exports.Contact = exports.WxBot = exports.AppDataSource = void 0;
|
|
46
46
|
require("reflect-metadata");
|
|
47
47
|
const axiosInstance_1 = require("./utils/axiosInstance");
|
|
48
48
|
const data_source_1 = require("./database/data-source");
|
|
@@ -87,6 +87,8 @@ class WxBot extends events_1.EventEmitter {
|
|
|
87
87
|
this.syncTask = null;
|
|
88
88
|
this.syncRequested = false;
|
|
89
89
|
this.loginRecoveryInProgress = false;
|
|
90
|
+
this.contactSyncTask = null;
|
|
91
|
+
this.contactSyncGeneration = 0;
|
|
90
92
|
this.syncFailureCount = 0; // Sync failure counter
|
|
91
93
|
this.httpServer = null; // HTTP回调服务器
|
|
92
94
|
this.loggingOut = false; // Flag to prevent concurrent logOut calls
|
|
@@ -256,13 +258,18 @@ class WxBot extends events_1.EventEmitter {
|
|
|
256
258
|
const statusData = statusResp.data;
|
|
257
259
|
if (statusData.Success) {
|
|
258
260
|
if (statusData.Message === '登录成功') {
|
|
261
|
+
// Drain only old in-flight detail requests before changing accounts.
|
|
262
|
+
this.contactSyncGeneration++;
|
|
263
|
+
yield this.waitForContactSync();
|
|
259
264
|
yield config_service_1.configService.save({
|
|
260
265
|
uuid: this.uuid,
|
|
261
266
|
wxid: statusData.Data.acctSectResp.userName,
|
|
262
267
|
});
|
|
263
|
-
this.
|
|
264
|
-
if (this.interval)
|
|
268
|
+
if (this.interval) {
|
|
265
269
|
clearInterval(this.interval);
|
|
270
|
+
this.interval = null;
|
|
271
|
+
}
|
|
272
|
+
yield this.loginSuccess(statusData.Data.acctSectResp.userName);
|
|
266
273
|
}
|
|
267
274
|
}
|
|
268
275
|
else if (statusData.Message.includes('数据不存在') || statusData.Message.includes('未知的错误')) {
|
|
@@ -294,6 +301,8 @@ class WxBot extends events_1.EventEmitter {
|
|
|
294
301
|
});
|
|
295
302
|
}
|
|
296
303
|
clearAllIntervals() {
|
|
304
|
+
// In-flight requests may settle, but old sessions cannot schedule more work.
|
|
305
|
+
this.contactSyncGeneration++;
|
|
297
306
|
if (this.interval) {
|
|
298
307
|
clearInterval(this.interval);
|
|
299
308
|
this.interval = null;
|
|
@@ -322,22 +331,27 @@ class WxBot extends events_1.EventEmitter {
|
|
|
322
331
|
* @param currentChatroomSeq 当前群聊联系人序列号
|
|
323
332
|
*/
|
|
324
333
|
fetchAllContacts(userName_1) {
|
|
325
|
-
return __awaiter(this, arguments, void 0, function* (userName, currentWxSeq = 0, currentChatroomSeq = 0) {
|
|
334
|
+
return __awaiter(this, arguments, void 0, function* (userName, currentWxSeq = 0, currentChatroomSeq = 0, generation = this.contactSyncGeneration) {
|
|
335
|
+
if (generation !== this.contactSyncGeneration)
|
|
336
|
+
return false;
|
|
326
337
|
try {
|
|
327
338
|
const res = yield FriendApi_1.friendApi.GetContractList({
|
|
328
339
|
"Wxid": userName,
|
|
329
340
|
"CurrentChatRoomContactSeq": currentChatroomSeq,
|
|
330
341
|
"CurrentWxcontactSeq": currentWxSeq,
|
|
331
342
|
});
|
|
343
|
+
if (generation !== this.contactSyncGeneration)
|
|
344
|
+
return false;
|
|
332
345
|
if (res.data.Message === '用户可能退出') {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
346
|
+
// Optional directory work is not the session authority. Message Sync
|
|
347
|
+
// owns recovery; starting it here could wait on this very task on reset.
|
|
348
|
+
console.error('联系人接口提示会话可能失效,交由消息同步检查');
|
|
349
|
+
return false;
|
|
336
350
|
}
|
|
337
351
|
const data = res.data;
|
|
338
352
|
if (!data.Success) {
|
|
339
|
-
console.error('
|
|
340
|
-
return;
|
|
353
|
+
console.error('获取联系人列表失败,保留已有目录');
|
|
354
|
+
return false;
|
|
341
355
|
}
|
|
342
356
|
// 处理当前批次的联系人
|
|
343
357
|
if (data.Data && data.Data.ContactUsernameList) {
|
|
@@ -348,7 +362,7 @@ class WxBot extends events_1.EventEmitter {
|
|
|
348
362
|
let failed = 0;
|
|
349
363
|
// Bound detail requests and finish this page before requesting the next.
|
|
350
364
|
const worker = () => __awaiter(this, void 0, void 0, function* () {
|
|
351
|
-
while (nextIndex < contacts.length) {
|
|
365
|
+
while (generation === this.contactSyncGeneration && nextIndex < contacts.length) {
|
|
352
366
|
const contact = contacts[nextIndex++];
|
|
353
367
|
try {
|
|
354
368
|
if (!contact.endsWith('@chatroom')) {
|
|
@@ -366,6 +380,8 @@ class WxBot extends events_1.EventEmitter {
|
|
|
366
380
|
}
|
|
367
381
|
});
|
|
368
382
|
yield Promise.all(Array.from({ length: Math.min(4, contacts.length) }, () => worker()));
|
|
383
|
+
if (generation !== this.contactSyncGeneration)
|
|
384
|
+
return false;
|
|
369
385
|
console.log(`联系人批次处理完成: 成功 ${succeeded},失败 ${failed}`);
|
|
370
386
|
}
|
|
371
387
|
// 检查是否还有更多联系人需要获取
|
|
@@ -375,27 +391,60 @@ class WxBot extends events_1.EventEmitter {
|
|
|
375
391
|
const nextChatroomSeq = data.Data.CurrentChatRoomContactSeq || 0;
|
|
376
392
|
console.log(`还有更多联系人,继续获取... (WxSeq: ${nextWxSeq}, ChatroomSeq: ${nextChatroomSeq})`);
|
|
377
393
|
// 递归获取下一批联系人
|
|
378
|
-
yield this.fetchAllContacts(userName, nextWxSeq, nextChatroomSeq);
|
|
394
|
+
return yield this.fetchAllContacts(userName, nextWxSeq, nextChatroomSeq, generation);
|
|
379
395
|
}
|
|
380
396
|
else {
|
|
381
397
|
console.log('联系人列表获取完成');
|
|
398
|
+
return true;
|
|
382
399
|
}
|
|
383
400
|
}
|
|
384
|
-
catch (
|
|
385
|
-
|
|
401
|
+
catch (_a) {
|
|
402
|
+
if (generation === this.contactSyncGeneration)
|
|
403
|
+
console.error('获取联系人列表时发生错误,保留已有目录');
|
|
404
|
+
return false;
|
|
386
405
|
}
|
|
387
406
|
});
|
|
388
407
|
}
|
|
408
|
+
startContactSync(userName) {
|
|
409
|
+
const generation = this.contactSyncGeneration;
|
|
410
|
+
const previous = this.contactSyncTask;
|
|
411
|
+
const task = Promise.resolve(previous).then(() => __awaiter(this, void 0, void 0, function* () {
|
|
412
|
+
if (generation !== this.contactSyncGeneration)
|
|
413
|
+
return;
|
|
414
|
+
console.log('开始后台同步联系人,不阻塞消息接收');
|
|
415
|
+
const complete = yield this.fetchAllContacts(userName, 0, 0, generation);
|
|
416
|
+
if (generation !== this.contactSyncGeneration)
|
|
417
|
+
return;
|
|
418
|
+
console.log(complete ? '联系人后台同步批次处理完成' : '联系人后台同步未完成,已有目录仍可使用');
|
|
419
|
+
// Also invalidate a partially updated directory after a later page fails.
|
|
420
|
+
this.emit('contacts-updated');
|
|
421
|
+
})).catch(() => {
|
|
422
|
+
if (generation === this.contactSyncGeneration)
|
|
423
|
+
console.error('联系人后台同步失败');
|
|
424
|
+
}).finally(() => {
|
|
425
|
+
if (this.contactSyncTask === task)
|
|
426
|
+
this.contactSyncTask = null;
|
|
427
|
+
});
|
|
428
|
+
this.contactSyncTask = task;
|
|
429
|
+
}
|
|
430
|
+
/** Explicit directory barrier for callers that need the current background pass. */
|
|
431
|
+
waitForContactSync() {
|
|
432
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
433
|
+
yield this.contactSyncTask;
|
|
434
|
+
});
|
|
435
|
+
}
|
|
389
436
|
loginSuccess(userName) {
|
|
390
437
|
return __awaiter(this, void 0, void 0, function* () {
|
|
391
|
-
|
|
392
|
-
this.
|
|
438
|
+
this.clearAllIntervals(); // 确保清理所有定时器,取消旧目录任务
|
|
439
|
+
const generation = this.contactSyncGeneration;
|
|
440
|
+
yield contact_service_1.contactService.clearRoom();
|
|
441
|
+
if (generation !== this.contactSyncGeneration)
|
|
442
|
+
return false;
|
|
393
443
|
this.syncFailureCount = 0; // 重置失败计数器
|
|
394
|
-
// 分页获取所有联系人
|
|
395
|
-
yield this.fetchAllContacts(userName, 0, 0);
|
|
396
|
-
console.log('所有联系人获取完成');
|
|
397
444
|
// 设置设备信息
|
|
398
445
|
const cacheInfo = yield LoginApi_1.loginApi.GetCacheInfo(userName);
|
|
446
|
+
if (generation !== this.contactSyncGeneration)
|
|
447
|
+
return false;
|
|
399
448
|
if (cacheInfo.data.Message === '成功') {
|
|
400
449
|
yield config_service_1.configService.save({
|
|
401
450
|
deviceId: cacheInfo.data.Data.Deviceid_str,
|
|
@@ -403,10 +452,16 @@ class WxBot extends events_1.EventEmitter {
|
|
|
403
452
|
DeviceType: this.deviceType,
|
|
404
453
|
});
|
|
405
454
|
}
|
|
455
|
+
if (generation !== this.contactSyncGeneration)
|
|
456
|
+
return false;
|
|
406
457
|
// 先监听再启动心跳;不再通过 Sync 清空并丢弃登录期间的消息。
|
|
407
458
|
if (this.mode === 'callback') {
|
|
408
459
|
yield this.startCallbackServer(userName);
|
|
460
|
+
if (generation !== this.contactSyncGeneration)
|
|
461
|
+
return false;
|
|
409
462
|
this.emit('login', userName);
|
|
463
|
+
if (generation !== this.contactSyncGeneration)
|
|
464
|
+
return false;
|
|
410
465
|
this.callbackInterval = setInterval(() => {
|
|
411
466
|
void this.handleMsg(userName).catch(() => console.error('回调兜底同步失败'));
|
|
412
467
|
}, 1000 * 60 * 5);
|
|
@@ -414,7 +469,13 @@ class WxBot extends events_1.EventEmitter {
|
|
|
414
469
|
else {
|
|
415
470
|
this.startPollingMode(userName);
|
|
416
471
|
}
|
|
472
|
+
if (generation !== this.contactSyncGeneration)
|
|
473
|
+
return false;
|
|
417
474
|
yield this.setupHeartbeat(userName);
|
|
475
|
+
if (generation !== this.contactSyncGeneration)
|
|
476
|
+
return false;
|
|
477
|
+
this.startContactSync(userName);
|
|
478
|
+
return true;
|
|
418
479
|
});
|
|
419
480
|
}
|
|
420
481
|
startCallbackServer(userName) {
|
|
@@ -728,15 +789,17 @@ class WxBot extends events_1.EventEmitter {
|
|
|
728
789
|
*/
|
|
729
790
|
clearCache() {
|
|
730
791
|
return __awaiter(this, void 0, void 0, function* () {
|
|
731
|
-
this.logOut();
|
|
792
|
+
yield this.logOut();
|
|
793
|
+
// Never let an old request repopulate the cache after an account reset.
|
|
794
|
+
yield this.waitForContactSync();
|
|
732
795
|
const config = yield config_service_1.configService.get();
|
|
733
796
|
if (config) {
|
|
734
797
|
const res = yield LoginApi_1.loginApi.LogOut(config.wxid);
|
|
735
798
|
}
|
|
736
|
-
config_service_1.configService.clear();
|
|
737
|
-
room_service_1.roomService.clear();
|
|
738
|
-
contact_service_1.contactService.clear();
|
|
739
|
-
message_service_1.messageService.clear();
|
|
799
|
+
yield config_service_1.configService.clear();
|
|
800
|
+
yield room_service_1.roomService.clear();
|
|
801
|
+
yield contact_service_1.contactService.clear();
|
|
802
|
+
yield message_service_1.messageService.clear();
|
|
740
803
|
});
|
|
741
804
|
}
|
|
742
805
|
login() {
|
|
@@ -774,8 +837,8 @@ class WxBot extends events_1.EventEmitter {
|
|
|
774
837
|
"Synckey": ""
|
|
775
838
|
});
|
|
776
839
|
if (resp.data.Code === 0) {
|
|
777
|
-
yield this.loginSuccess(config.wxid)
|
|
778
|
-
|
|
840
|
+
if (yield this.loginSuccess(config.wxid))
|
|
841
|
+
yield this.enqueueMessages(config.wxid, resp.data);
|
|
779
842
|
}
|
|
780
843
|
else {
|
|
781
844
|
yield this.login();
|
|
@@ -804,7 +867,8 @@ class WxBot extends events_1.EventEmitter {
|
|
|
804
867
|
});
|
|
805
868
|
yield LoginApi_1.loginApi.LoginTwiceAutoAuth(config === null || config === void 0 ? void 0 : config.wxid);
|
|
806
869
|
if (yield this.checkSyncLoginStatus(resp.data, config === null || config === void 0 ? void 0 : config.wxid)) {
|
|
807
|
-
yield this.loginSuccess(config.wxid)
|
|
870
|
+
if (!(yield this.loginSuccess(config.wxid)))
|
|
871
|
+
return false;
|
|
808
872
|
yield this.enqueueMessages(config.wxid, resp.data);
|
|
809
873
|
return true;
|
|
810
874
|
}
|
|
@@ -836,3 +900,5 @@ exports.WxBot = WxBot;
|
|
|
836
900
|
var ContactClassification_1 = require("./utils/ContactClassification");
|
|
837
901
|
Object.defineProperty(exports, "normalizeContactVerifyFlag", { enumerable: true, get: function () { return ContactClassification_1.normalizeContactVerifyFlag; } });
|
|
838
902
|
Object.defineProperty(exports, "isOfficialAccountVerifyFlag", { enumerable: true, get: function () { return ContactClassification_1.isOfficialAccountVerifyFlag; } });
|
|
903
|
+
var OfficialAccountMetadata_1 = require("./utils/OfficialAccountMetadata");
|
|
904
|
+
Object.defineProperty(exports, "parseStoredOfficialMenu", { enumerable: true, get: function () { return OfficialAccountMetadata_1.parseStoredOfficialMenu; } });
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface OfficialMenu {
|
|
2
|
+
buttons: OfficialMenuButton[];
|
|
3
|
+
}
|
|
4
|
+
export interface OfficialMenuButton {
|
|
5
|
+
name: string;
|
|
6
|
+
kind: 'group' | 'url' | 'unsupported';
|
|
7
|
+
url?: string;
|
|
8
|
+
reason?: 'event' | 'miniapp' | 'wechat' | 'unknown';
|
|
9
|
+
children?: OfficialMenuButton[];
|
|
10
|
+
}
|
|
11
|
+
/** Revalidate untrusted DB JSON (or an object) and return a fresh, allowlisted tree. */
|
|
12
|
+
export declare function parseStoredOfficialMenu(value: unknown): OfficialMenu | null;
|
|
13
|
+
/** Missing/invalid fields are omitted so an atomic partial upsert preserves known metadata. */
|
|
14
|
+
export declare function decodeOfficialAccountMetadata(externalInfo: unknown, verifyFlag: unknown): {
|
|
15
|
+
serviceType?: number;
|
|
16
|
+
officialMenu?: OfficialMenu;
|
|
17
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseStoredOfficialMenu = parseStoredOfficialMenu;
|
|
4
|
+
exports.decodeOfficialAccountMetadata = decodeOfficialAccountMetadata;
|
|
5
|
+
const ContactClassification_1 = require("./ContactClassification");
|
|
6
|
+
// Reject the entire tree at a limit, rather than displaying a truncated menu as complete.
|
|
7
|
+
const MAX_JSON_BYTES = 64 * 1024;
|
|
8
|
+
const MAX_DEPTH = 3;
|
|
9
|
+
const MAX_NODES = 100;
|
|
10
|
+
const MAX_NAME_LENGTH = 256;
|
|
11
|
+
const MAX_URL_LENGTH = 4096;
|
|
12
|
+
const CONTROL = /[\u0000-\u001f\u007f-\u009f]/;
|
|
13
|
+
function record(value) {
|
|
14
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
15
|
+
return false;
|
|
16
|
+
const prototype = Object.getPrototypeOf(value);
|
|
17
|
+
return prototype === Object.prototype || prototype === null;
|
|
18
|
+
}
|
|
19
|
+
// Never use inherited values or invoke getters on caller-supplied cached objects.
|
|
20
|
+
function own(value, key) {
|
|
21
|
+
var _a;
|
|
22
|
+
return (_a = Object.getOwnPropertyDescriptor(value, key)) === null || _a === void 0 ? void 0 : _a.value;
|
|
23
|
+
}
|
|
24
|
+
function json(value) {
|
|
25
|
+
if (typeof value !== 'string' || value.length > MAX_JSON_BYTES
|
|
26
|
+
|| Buffer.byteLength(value, 'utf8') > MAX_JSON_BYTES)
|
|
27
|
+
return null;
|
|
28
|
+
return JSON.parse(value);
|
|
29
|
+
}
|
|
30
|
+
function safeUrl(value) {
|
|
31
|
+
if (typeof value !== 'string' || !value || value.length > MAX_URL_LENGTH
|
|
32
|
+
|| /[\s\\]/u.test(value) || CONTROL.test(value) || !/^https?:\/\//i.test(value))
|
|
33
|
+
return null;
|
|
34
|
+
const url = new URL(value);
|
|
35
|
+
if (!url.hostname || url.username || url.password)
|
|
36
|
+
return null;
|
|
37
|
+
// Encoded authorization links and control characters must not bypass the check.
|
|
38
|
+
let decoded = value;
|
|
39
|
+
for (let i = 0; i < 4; i++) {
|
|
40
|
+
if (CONTROL.test(decoded) || /oauth|authori[sz]|snsapi_|connect\/qrconnect|(?:[?&#])(?:response_type|redirect_uri)=/i.test(decoded)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const next = decodeURIComponent(decoded);
|
|
44
|
+
if (next === decoded)
|
|
45
|
+
return value;
|
|
46
|
+
decoded = next;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
function normalizeMenu(value, raw) {
|
|
51
|
+
if (!record(value))
|
|
52
|
+
return null;
|
|
53
|
+
let nodes = 0;
|
|
54
|
+
let textSize = 0;
|
|
55
|
+
const seen = new WeakSet();
|
|
56
|
+
function buttons(value, depth) {
|
|
57
|
+
if (!Array.isArray(value) || depth > MAX_DEPTH || value.length > MAX_NODES)
|
|
58
|
+
return null;
|
|
59
|
+
if (seen.has(value))
|
|
60
|
+
return null;
|
|
61
|
+
seen.add(value);
|
|
62
|
+
const result = [];
|
|
63
|
+
const siblings = new Set();
|
|
64
|
+
for (let i = 0; i < value.length; i++) {
|
|
65
|
+
const item = own(value, String(i));
|
|
66
|
+
if (!record(item) || seen.has(item) || ++nodes > MAX_NODES)
|
|
67
|
+
return null;
|
|
68
|
+
seen.add(item);
|
|
69
|
+
const name = own(item, 'name');
|
|
70
|
+
if (typeof name !== 'string' || !name.trim() || name.length > MAX_NAME_LENGTH || CONTROL.test(name))
|
|
71
|
+
return null;
|
|
72
|
+
textSize += Buffer.byteLength(name, 'utf8');
|
|
73
|
+
// Live menus may reuse the same placeholder id for every button. IDs and
|
|
74
|
+
// event keys are neither exposed nor used to execute actions here.
|
|
75
|
+
const type = own(item, raw ? 'type' : 'kind');
|
|
76
|
+
if (raw && (typeof type !== 'number' || !Number.isInteger(type) || type < 0 || type > 0xffffffff))
|
|
77
|
+
return null;
|
|
78
|
+
const children = own(item, raw ? 'sub_button_list' : 'children');
|
|
79
|
+
let button;
|
|
80
|
+
if (type === (raw ? 0 : 'group')) {
|
|
81
|
+
const nested = buttons(children, depth + 1);
|
|
82
|
+
if (!nested || !nested.length)
|
|
83
|
+
return null;
|
|
84
|
+
button = { name, kind: 'group', children: nested };
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
// A leaf with children is ambiguous; do not silently discard a subtree.
|
|
88
|
+
if (children !== undefined && !(raw && children === null)
|
|
89
|
+
&& (!Array.isArray(children) || children.length !== 0))
|
|
90
|
+
return null;
|
|
91
|
+
if (type === (raw ? 2 : 'url')) {
|
|
92
|
+
const candidate = own(item, raw ? 'value' : 'url');
|
|
93
|
+
if (typeof candidate !== 'string')
|
|
94
|
+
return null;
|
|
95
|
+
let url;
|
|
96
|
+
try {
|
|
97
|
+
url = safeUrl(candidate);
|
|
98
|
+
}
|
|
99
|
+
catch (_a) {
|
|
100
|
+
url = null;
|
|
101
|
+
}
|
|
102
|
+
if (url) {
|
|
103
|
+
textSize += Buffer.byteLength(url, 'utf8');
|
|
104
|
+
button = { name, kind: 'url', url };
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
button = { name, kind: 'unsupported', reason: 'wechat' };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else if (raw) {
|
|
111
|
+
button = { name, kind: 'unsupported', reason: type === 1 ? 'event' : type === 5 ? 'miniapp' : 'unknown' };
|
|
112
|
+
}
|
|
113
|
+
else if (type === 'unsupported') {
|
|
114
|
+
const reason = own(item, 'reason');
|
|
115
|
+
if (reason !== 'event' && reason !== 'miniapp' && reason !== 'wechat' && reason !== 'unknown')
|
|
116
|
+
return null;
|
|
117
|
+
button = { name, kind: 'unsupported', reason };
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (textSize > MAX_JSON_BYTES)
|
|
124
|
+
return null;
|
|
125
|
+
const identity = JSON.stringify(button);
|
|
126
|
+
if (siblings.has(identity))
|
|
127
|
+
return null;
|
|
128
|
+
siblings.add(identity);
|
|
129
|
+
result.push(button);
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
const normalized = buttons(own(value, raw ? 'button_list' : 'buttons'), 1);
|
|
134
|
+
if (!normalized)
|
|
135
|
+
return null;
|
|
136
|
+
const menu = { buttons: normalized };
|
|
137
|
+
return Buffer.byteLength(JSON.stringify(menu), 'utf8') <= MAX_JSON_BYTES ? menu : null;
|
|
138
|
+
}
|
|
139
|
+
/** Revalidate untrusted DB JSON (or an object) and return a fresh, allowlisted tree. */
|
|
140
|
+
function parseStoredOfficialMenu(value) {
|
|
141
|
+
try {
|
|
142
|
+
return normalizeMenu(typeof value === 'string' ? json(value) : value, false);
|
|
143
|
+
}
|
|
144
|
+
catch (_a) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** Missing/invalid fields are omitted so an atomic partial upsert preserves known metadata. */
|
|
149
|
+
function decodeOfficialAccountMetadata(externalInfo, verifyFlag) {
|
|
150
|
+
const result = {};
|
|
151
|
+
if ((0, ContactClassification_1.isOfficialAccountVerifyFlag)(verifyFlag) !== true)
|
|
152
|
+
return result;
|
|
153
|
+
try {
|
|
154
|
+
const external = json(externalInfo);
|
|
155
|
+
if (!record(external))
|
|
156
|
+
return result;
|
|
157
|
+
const serviceType = (0, ContactClassification_1.normalizeContactVerifyFlag)(own(external, 'ServiceType'));
|
|
158
|
+
if (serviceType !== null)
|
|
159
|
+
result.serviceType = serviceType;
|
|
160
|
+
const officialMenu = normalizeMenu(own(external, 'MMBizMenu'), true);
|
|
161
|
+
if (officialMenu)
|
|
162
|
+
result.officialMenu = officialMenu;
|
|
163
|
+
}
|
|
164
|
+
catch (_a) {
|
|
165
|
+
// Malformed ExternalInfo is not evidence that the account has no menu.
|
|
166
|
+
}
|
|
167
|
+
return result;
|
|
168
|
+
}
|