koishi-plugin-ets2-tools-tmp 3.4.5 → 3.5.0
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/lib/command/ets-app/changePoint/changePointV1.js +28 -7
- package/lib/command/ets-app/pointRanking/pointRanking.js +12 -5
- package/lib/command/ets-app/queryPoint/queryPoint.js +15 -1
- package/lib/command/ets-app/queryPoint/queryPointV1.js +21 -5
- package/lib/command/ets-app/resetPassword/resetPasswordV1.js +32 -3
- package/lib/command/tmpBind.js +41 -4
- package/lib/command/tmpDlcMap.js +1 -1
- package/lib/command/tmpFootprint.js +3 -2
- package/lib/command/tmpMileageRanking.js +74 -23
- package/lib/command/tmpPosition.js +3 -2
- package/lib/command/tmpQuery/tmpQuery.js +70 -1
- package/lib/command/tmpQuery/tmpQueryImg.js +4 -3
- package/lib/command/tmpQuery/tmpQueryText.js +4 -3
- package/lib/command/tmpServer/tmpServer.js +47 -3
- package/lib/command/tmpServer/tmpServerImg.js +1 -1
- package/lib/command/tmpTraffic/tmpTraffic.js +64 -3
- package/lib/command/tmpTraffic/tmpTrafficMap.js +2 -2
- package/lib/command/tmpVersion.js +81 -9
- package/lib/command/tmpVersionCheck.js +53 -10
- package/lib/command/tmpVtcMileageRanking.js +75 -22
- package/lib/command/tmpVtcOnline/tmpVtcOnlineImg.js +1 -1
- package/lib/database/guildBind.js +150 -1
- package/lib/database/model.js +12 -0
- package/lib/index.js +139 -15
- package/lib/resource/command-suggest.html +222 -0
- package/lib/resource/tmp-version.html +292 -0
- package/lib/util/commandSuggest.js +140 -0
- package/lib/util/commandSuggestMiddleware.js +210 -0
- package/lib/util/localImageHost.js +219 -0
- package/lib/util/messageRender.js +234 -0
- package/lib/util/qqMarkdown.js +171 -0
- package/lib/util/qqMediaUpload.js +165 -0
- package/lib/util/qqOfficial.js +233 -0
- package/package.json +1 -1
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
const qqOfficial = require('./qqOfficial');
|
|
2
|
+
const crypto = require('crypto');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* QQ 官方机器人:上传图片素材,换取可在 markdown 里引用的 qqbot-media:// 链接。
|
|
6
|
+
*
|
|
7
|
+
* 为什么不能直接用 <img> 元素:
|
|
8
|
+
* 适配器遇到 <img> 会先 flush(),图片会单独成为一条消息,
|
|
9
|
+
* 无法和按钮合成一条。要合并只能把图片写进 markdown 正文。
|
|
10
|
+
*
|
|
11
|
+
* 官方文档(素材管理)规定的是三阶段上传,**不是** /v2/groups/{id}/files:
|
|
12
|
+
* 1. POST /v2/media-images/prepare 申请上传 → presigned_url + upload_id
|
|
13
|
+
* 2. PUT <presigned_url> 直传二进制(Content-Type 必须与声明一致)
|
|
14
|
+
* 3. POST /v2/media-images/complete 完成 → url: qqbot-media://image/v1/<token>
|
|
15
|
+
*
|
|
16
|
+
* 限制:单张 ≤10MB,格式 png/jpeg/gif/webp,单机器人上限 100 张。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** 单张图片上限(官方 10MB,这里留些余量) */
|
|
20
|
+
const MAX_UPLOAD_BYTES = 8 * 1024 * 1024;
|
|
21
|
+
|
|
22
|
+
/** 支持的图片格式 */
|
|
23
|
+
const SUPPORTED_MIME = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 从 segment.image 元素里解析出上传所需信息。
|
|
27
|
+
* @returns {{base64: string|null, mime: string|null, bytes: number, url: string|null}}
|
|
28
|
+
*/
|
|
29
|
+
function extractImageSource(imageSegment) {
|
|
30
|
+
const empty = { base64: null, mime: null, bytes: 0, url: null };
|
|
31
|
+
if (!imageSegment) return empty;
|
|
32
|
+
|
|
33
|
+
const attrs = imageSegment.attrs || {};
|
|
34
|
+
const src = attrs.src || attrs.url || '';
|
|
35
|
+
if (!src) return empty;
|
|
36
|
+
|
|
37
|
+
// 公网 URL:markdown 可直接引用,无需上传
|
|
38
|
+
if (/^https?:\/\//i.test(src)) {
|
|
39
|
+
return { base64: null, mime: null, bytes: 0, url: src };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const m = /^data:([^;]+);base64,(.*)$/s.exec(src);
|
|
43
|
+
if (!m) return empty;
|
|
44
|
+
|
|
45
|
+
const mime = String(m[1] || '').toLowerCase();
|
|
46
|
+
const base64 = m[2];
|
|
47
|
+
const bytes = Math.floor(base64.length * 3 / 4);
|
|
48
|
+
return { base64, mime, bytes, url: null };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** mime → 扩展名 */
|
|
52
|
+
function extOf(mime) {
|
|
53
|
+
switch (mime) {
|
|
54
|
+
case 'image/png': return 'png';
|
|
55
|
+
case 'image/jpeg':
|
|
56
|
+
case 'image/jpg': return 'jpg';
|
|
57
|
+
case 'image/gif': return 'gif';
|
|
58
|
+
case 'image/webp': return 'webp';
|
|
59
|
+
default: return 'png';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getLogger(session) {
|
|
64
|
+
return (session && session.app && session.app.logger) ||
|
|
65
|
+
(session && session.bot && session.bot.logger) ||
|
|
66
|
+
null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 取 API 基础地址。
|
|
71
|
+
*
|
|
72
|
+
* 适配器的 bot.http 只设置了 `endpoint` 选项(不是 baseURL),
|
|
73
|
+
* 相对路径不一定能正确拼接,所以这里显式用 bot.config.endpoint 构造绝对地址。
|
|
74
|
+
*/
|
|
75
|
+
function apiBase(bot) {
|
|
76
|
+
const configured = bot && bot.config && bot.config.endpoint;
|
|
77
|
+
const base = configured || 'https://api.bot.qq.com/';
|
|
78
|
+
return String(base).replace(/\/+$/, '');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 通过官方三阶段接口上传图片,返回 markdown 引用。
|
|
83
|
+
*
|
|
84
|
+
* @param {object} session koishi session
|
|
85
|
+
* @param {object} imageSegment 图片元素
|
|
86
|
+
* @returns {Promise<string|null>} 成功返回 qqbot-media://... ;失败返回 null(调用方退化为普通图片发送)
|
|
87
|
+
*/
|
|
88
|
+
async function uploadImageForMarkdown(session, imageSegment) {
|
|
89
|
+
if (!qqOfficial.isQQOfficial(session)) return null;
|
|
90
|
+
|
|
91
|
+
const logger = getLogger(session);
|
|
92
|
+
const warn = (msg) => { try { logger && logger.warn(`[QQ图片上传] ${msg}`); } catch { } };
|
|
93
|
+
const debug = (msg) => { try { logger && logger.debug(`[QQ图片上传] ${msg}`); } catch { } };
|
|
94
|
+
|
|
95
|
+
const bot = session.bot;
|
|
96
|
+
// bot.http 是带 Authorization 的实例(适配器内部用它发请求)
|
|
97
|
+
if (!bot || !bot.http) { warn('session.bot 或 bot.http 不可用,退化为普通图片发送'); return null; }
|
|
98
|
+
|
|
99
|
+
const { base64, mime, bytes, url } = extractImageSource(imageSegment);
|
|
100
|
+
if (url) { debug('图片已是公网 URL,直接引用'); return url; }
|
|
101
|
+
if (!base64) { warn('无法从元素中解析出图片数据'); return null; }
|
|
102
|
+
|
|
103
|
+
const contentType = SUPPORTED_MIME.includes(mime) ? mime : 'image/png';
|
|
104
|
+
if (!SUPPORTED_MIME.includes(mime)) {
|
|
105
|
+
warn(`图片格式 ${mime} 不受支持,按 ${contentType} 尝试`);
|
|
106
|
+
}
|
|
107
|
+
if (bytes > MAX_UPLOAD_BYTES) {
|
|
108
|
+
warn(`图片过大(${Math.round(bytes / 1024)}KB),退化为普通图片发送`);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const buffer = Buffer.from(base64, 'base64');
|
|
113
|
+
const md5 = crypto.createHash('md5').update(buffer).digest('hex');
|
|
114
|
+
const sha1 = crypto.createHash('sha1').update(buffer).digest('hex');
|
|
115
|
+
const fileName = `image_${Date.now()}.${extOf(contentType)}`;
|
|
116
|
+
const base = apiBase(bot);
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
// ---- 1. prepare ----
|
|
120
|
+
const prepared = await bot.http.post(`${base}/v2/media-images/prepare`, {
|
|
121
|
+
file_name: fileName,
|
|
122
|
+
file_size: buffer.length,
|
|
123
|
+
content_type: contentType,
|
|
124
|
+
md5,
|
|
125
|
+
sha1,
|
|
126
|
+
}, { timeout: 15000 });
|
|
127
|
+
const uploadId = prepared && prepared.upload_id;
|
|
128
|
+
const presignedUrl = prepared && prepared.presigned_url;
|
|
129
|
+
if (!uploadId || !presignedUrl) {
|
|
130
|
+
warn(`prepare 未返回有效数据: ${JSON.stringify(prepared)}`);
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ---- 2. PUT 直传(Content-Type 必须与 prepare 声明一致,否则 COS 403)----
|
|
135
|
+
await bot.http.put(presignedUrl, buffer, {
|
|
136
|
+
headers: { 'Content-Type': contentType },
|
|
137
|
+
timeout: 30000,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ---- 3. complete ----
|
|
141
|
+
const completed = await bot.http.post(`${base}/v2/media-images/complete`, {
|
|
142
|
+
upload_id: uploadId,
|
|
143
|
+
md5,
|
|
144
|
+
}, { timeout: 15000 });
|
|
145
|
+
if (completed && completed.url) {
|
|
146
|
+
debug(`上传成功: ${completed.url}`);
|
|
147
|
+
return completed.url;
|
|
148
|
+
}
|
|
149
|
+
warn(`complete 未返回引用: ${JSON.stringify(completed)}`);
|
|
150
|
+
return null;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
const detail = error && error.response && error.response.data
|
|
153
|
+
? JSON.stringify(error.response.data)
|
|
154
|
+
: (error && error.message) || String(error);
|
|
155
|
+
warn(`上传失败: ${detail}`);
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = {
|
|
161
|
+
MAX_UPLOAD_BYTES,
|
|
162
|
+
SUPPORTED_MIME,
|
|
163
|
+
extractImageSource,
|
|
164
|
+
uploadImageForMarkdown,
|
|
165
|
+
};
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QQ 官方机器人(@koishijs/plugin-adapter-qq)适配层。
|
|
3
|
+
*
|
|
4
|
+
* 目的:在不影响 onebot 的前提下,为 QQ 官方机器人提供 markdown + 按钮消息。
|
|
5
|
+
*
|
|
6
|
+
* 判定依据:官方适配器的 bot.platform 为 'qq'(群聊)或 'qqguild'(频道)。
|
|
7
|
+
* onebot 适配器的 platform 是 'onebot',所以两者天然区分,互不干扰。
|
|
8
|
+
*
|
|
9
|
+
* 官方适配器原生支持的元素:
|
|
10
|
+
* - <button-group> 包裹若干 <button>,适配器会自动切换为 markdown 消息并生成键盘
|
|
11
|
+
* - <button type="input" text="指令"> 点击后把指令填进输入框
|
|
12
|
+
* - <button type="link" href="..."> 跳转链接
|
|
13
|
+
* - <button type="callback" id="..."> 回调(需要后端配合)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { h } = require('koishi');
|
|
17
|
+
|
|
18
|
+
const QQ_OFFICIAL_PLATFORMS = ['qq', 'qqguild'];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 车队平台 /api/user/info/list 的固定查询参数(顺序与后端约定一致)。
|
|
22
|
+
* 后端新增了 unionid / isExecutive / isManager / adminUserId 四个参数,
|
|
23
|
+
* 这里集中定义,避免各处手拼漏参数。
|
|
24
|
+
*/
|
|
25
|
+
const PLATFORM_QUERY_PARAMS = {
|
|
26
|
+
fixed: {
|
|
27
|
+
tmpId: '',
|
|
28
|
+
tmpName: '',
|
|
29
|
+
teamId: '',
|
|
30
|
+
state: 0,
|
|
31
|
+
teamRole: '',
|
|
32
|
+
isExecutive: '',
|
|
33
|
+
isManager: '',
|
|
34
|
+
adminUserId: '',
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 是否来自 QQ 官方机器人适配器。
|
|
40
|
+
* 注意:不能只看 platform 前缀,'qq' 与 'onebot' 是两个不同适配器。
|
|
41
|
+
*/
|
|
42
|
+
function isQQOfficial(session) {
|
|
43
|
+
if (!session) return false;
|
|
44
|
+
return QQ_OFFICIAL_PLATFORMS.includes(String(session.platform || ''));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 判断会话所在的 bot 是否为官方适配器(比 session.platform 更可靠,
|
|
49
|
+
* 因为部分事件里 platform 可能被改写)。
|
|
50
|
+
*/
|
|
51
|
+
function isQQOfficialBot(session) {
|
|
52
|
+
if (!session) return false;
|
|
53
|
+
if (isQQOfficial(session)) return true;
|
|
54
|
+
const bot = session.bot;
|
|
55
|
+
if (!bot) return false;
|
|
56
|
+
return QQ_OFFICIAL_PLATFORMS.includes(String(bot.platform || '')) ||
|
|
57
|
+
QQ_OFFICIAL_PLATFORMS.includes(String(bot.selfId && bot.platform || ''));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 构造一个「指令按钮」:点击后把 text 填进输入框并发送。
|
|
62
|
+
* 注意必须用 h() 构造真正的 Element 实例,普通对象会被
|
|
63
|
+
* Element.normalize 拒绝(Invalid content)。
|
|
64
|
+
* @param {string} label 按钮显示文字
|
|
65
|
+
* @param {string} text 点击后实际发送的指令
|
|
66
|
+
* @param {boolean} primary 是否主按钮样式
|
|
67
|
+
*/
|
|
68
|
+
function inputButton(label, text, primary) {
|
|
69
|
+
return h('button', { type: 'input', text, class: primary ? 'primary' : '' }, label);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 构造一个「链接按钮」。
|
|
74
|
+
*/
|
|
75
|
+
function linkButton(label, href) {
|
|
76
|
+
return h('button', { type: 'link', href }, label);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 构造一个「回调按钮」(需要后端配合)。
|
|
81
|
+
*/
|
|
82
|
+
function callbackButton(label, id) {
|
|
83
|
+
return h('button', { type: 'callback', id }, label);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 把按钮分行包裹成 button-group 元素(官方适配器据此生成键盘)。
|
|
88
|
+
* @param {Array<Array<object>>} rows 每行若干按钮
|
|
89
|
+
*/
|
|
90
|
+
function buttonGroup(rows) {
|
|
91
|
+
const children = [];
|
|
92
|
+
for (const row of rows) {
|
|
93
|
+
if (!row || row.length === 0) continue;
|
|
94
|
+
for (const btn of row) children.push(btn);
|
|
95
|
+
children.push(h('br'));
|
|
96
|
+
}
|
|
97
|
+
return h('button-group', {}, children);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 组装 markdown 正文 + 按钮。
|
|
102
|
+
*
|
|
103
|
+
* 官方适配器把 <button-group> 之外的文本作为 markdown content 发送,
|
|
104
|
+
* 所以这里直接把 markdown 字符串作为 children 传入。
|
|
105
|
+
*
|
|
106
|
+
* @param {string} markdown markdown 正文
|
|
107
|
+
* @param {Array<Array<object>>} buttonRows 按钮行
|
|
108
|
+
* @returns {Array<object>} 可直接交给 session.send 的元素数组
|
|
109
|
+
*/
|
|
110
|
+
function markdownWithButtons(markdown, buttonRows) {
|
|
111
|
+
const children = [markdownBlock(markdown)];
|
|
112
|
+
if (buttonRows && buttonRows.length > 0) {
|
|
113
|
+
children.push(buttonGroup(buttonRows));
|
|
114
|
+
}
|
|
115
|
+
return children;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 用 <qq:markdown> 包裹正文。
|
|
120
|
+
* 新版适配器在此元素内部不转义文本,markdown 语法才能真正生效;
|
|
121
|
+
* 放在外面会被 escapeMarkdown 转义成普通字符。
|
|
122
|
+
*/
|
|
123
|
+
function markdownBlock(text) {
|
|
124
|
+
return h('qq:markdown', {}, [h.text(String(text == null ? '' : text))]);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 「猜你想查」按钮组:一组常用查询指令,方便官方机器人用户点选。
|
|
129
|
+
*/
|
|
130
|
+
function guessButtons() {
|
|
131
|
+
return [
|
|
132
|
+
[inputButton('活动查询', '活动查询', true), inputButton('我的绑定', '我的绑定')],
|
|
133
|
+
[inputButton('里程排行榜', '里程排行榜'), inputButton('VTC里程榜', 'vtc里程排行榜')],
|
|
134
|
+
[inputButton('今日里程榜', '今日里程排行榜'), inputButton('VTC今日榜', 'vtc今日里程排行榜')],
|
|
135
|
+
[inputButton('欧卡服务器', '欧卡服务器'), inputButton('路况', '路况 s1')],
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 从 session 里提取 QQ 官方机器人的身份信息。
|
|
141
|
+
*
|
|
142
|
+
* 适配器把原始事件同时挂在两处(见 @satorijs/core 的 Session.setInternal):
|
|
143
|
+
* session.event._data = 原始 payload
|
|
144
|
+
* session.qq = Object.assign(bot.internal, 原始 payload) ← 原始字段直接可读
|
|
145
|
+
*
|
|
146
|
+
* 其中:
|
|
147
|
+
* - openid = payload.author.id(== session.userId,群内身份,跨群可能不同)
|
|
148
|
+
* - union_openid = payload.author.union_openid(跨群统一,需申请 union 权限才会下发)
|
|
149
|
+
*
|
|
150
|
+
* @returns {{openid: string|null, unionId: string|null, raw: object|null}}
|
|
151
|
+
*/
|
|
152
|
+
function getQQIdentity(session) {
|
|
153
|
+
const empty = { openid: null, unionId: null, raw: null };
|
|
154
|
+
if (!session || !isQQOfficial(session)) return empty;
|
|
155
|
+
|
|
156
|
+
// 原始 payload:优先 session.qq,其次 event._data
|
|
157
|
+
const raw = session.qq || (session.event && session.event._data) || null;
|
|
158
|
+
const author = raw && raw.author ? raw.author : null;
|
|
159
|
+
|
|
160
|
+
const openid = (author && (author.id || author.member_openid)) ||
|
|
161
|
+
session.userId || null;
|
|
162
|
+
|
|
163
|
+
const unionId = (author && (author.union_openid || author.union_id)) || null;
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
openid: openid ? String(openid) : null,
|
|
167
|
+
unionId: unionId ? String(unionId) : null,
|
|
168
|
+
raw,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 组装一个 identity 描述对象(用于写入数据库 / 传给后端)。
|
|
174
|
+
* onebot 下 openid=null、unionId=null,qq 下按实际下发填充。
|
|
175
|
+
*/
|
|
176
|
+
function buildIdentity(session) {
|
|
177
|
+
const id = getQQIdentity(session);
|
|
178
|
+
const platform = String(session && session.platform || '');
|
|
179
|
+
return {
|
|
180
|
+
platform,
|
|
181
|
+
userId: session && session.userId ? String(session.userId) : null,
|
|
182
|
+
openid: id.openid,
|
|
183
|
+
unionId: id.unionId,
|
|
184
|
+
isQQOfficial: isQQOfficial(session),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 构造「用哪个身份去查车队平台用户」的查询参数。
|
|
190
|
+
*
|
|
191
|
+
* 平台接口 /api/user/info/list 同时支持 `qq` 和 `unionid` 两种入参:
|
|
192
|
+
* - onebot:平台能拿到 QQ 号,用 qq 查(行为与以前完全一致)
|
|
193
|
+
* - QQ 官方机器人:只能拿到 openid/unionid,用 unionid 查
|
|
194
|
+
*
|
|
195
|
+
* 返回值用于拼 URL,未命中的那个参数给空字符串(与既有调用风格一致)。
|
|
196
|
+
*
|
|
197
|
+
* @param {object} session koishi session
|
|
198
|
+
* @returns {{qq: string, unionid: string, isQQOfficial: boolean, hasIdentity: boolean}}
|
|
199
|
+
*/
|
|
200
|
+
function buildQueryIdentity(session) {
|
|
201
|
+
const isOfficial = isQQOfficial(session);
|
|
202
|
+
if (!isOfficial) {
|
|
203
|
+
// onebot 及任何非官方平台:沿用 QQ 号
|
|
204
|
+
const qq = session && session.userId ? String(session.userId) : '';
|
|
205
|
+
return { qq, unionid: '', isQQOfficial: false, hasIdentity: !!qq };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const id = getQQIdentity(session);
|
|
209
|
+
const unionId = id.unionId || '';
|
|
210
|
+
return {
|
|
211
|
+
qq: '',
|
|
212
|
+
unionid: unionId,
|
|
213
|
+
isQQOfficial: true,
|
|
214
|
+
hasIdentity: !!unionId,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
module.exports = {
|
|
219
|
+
QQ_OFFICIAL_PLATFORMS,
|
|
220
|
+
isQQOfficial,
|
|
221
|
+
isQQOfficialBot,
|
|
222
|
+
getQQIdentity,
|
|
223
|
+
buildIdentity,
|
|
224
|
+
buildQueryIdentity,
|
|
225
|
+
inputButton,
|
|
226
|
+
linkButton,
|
|
227
|
+
callbackButton,
|
|
228
|
+
buttonGroup,
|
|
229
|
+
markdownBlock,
|
|
230
|
+
markdownWithButtons,
|
|
231
|
+
guessButtons,
|
|
232
|
+
PLATFORM_QUERY_PARAMS,
|
|
233
|
+
};
|