@zhin.js/adapter-qq 1.1.17 → 1.1.19
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/CHANGELOG.md +82 -0
- package/README.md +75 -21
- package/adapters/qq/index.js +4 -33
- package/adapters/qq/index.ts +3 -41
- package/agents/qq/boundaries.md +2 -0
- package/agents/qq/conversation.js +92 -0
- package/agents/qq/conversation.ts +99 -0
- package/agents/qq/messages.js +60 -0
- package/agents/qq/messages.ts +60 -0
- package/agents/qq/skills/qq/SKILL.md +8 -2
- package/agents/qq/skills/qq/tools/add_role/index.js +2 -1
- package/agents/qq/skills/qq/tools/add_role/index.ts +2 -1
- package/agents/qq/skills/qq/tools/channel_info/index.js +1 -1
- package/agents/qq/skills/qq/tools/channel_info/index.ts +1 -1
- package/agents/qq/skills/qq/tools/create_role/index.js +1 -1
- package/agents/qq/skills/qq/tools/create_role/index.ts +1 -1
- package/agents/qq/skills/qq/tools/guild_kick_member/index.js +30 -0
- package/agents/qq/skills/qq/tools/guild_kick_member/index.ts +29 -0
- package/agents/qq/skills/qq/tools/guild_mute_member/index.js +31 -0
- package/agents/qq/skills/qq/tools/guild_mute_member/index.ts +30 -0
- package/agents/qq/skills/qq/tools/list_channels/index.js +1 -1
- package/agents/qq/skills/qq/tools/list_channels/index.ts +1 -1
- package/agents/qq/skills/qq/tools/list_roles/index.js +1 -1
- package/agents/qq/skills/qq/tools/list_roles/index.ts +1 -1
- package/agents/qq/skills/qq/tools/member_detail/index.js +1 -1
- package/agents/qq/skills/qq/tools/member_detail/index.ts +1 -1
- package/agents/qq/skills/qq/tools/remove_role/index.js +2 -1
- package/agents/qq/skills/qq/tools/remove_role/index.ts +2 -1
- package/agents/qq/skills/qq-conversation/SKILL.md +24 -0
- package/agents/qq/skills/qq-conversation/tools/conversation_info/index.js +78 -0
- package/agents/qq/skills/qq-conversation/tools/conversation_info/index.ts +76 -0
- package/agents/qq/skills/qq-directory/tools/list_guilds/index.js +1 -1
- package/agents/qq/skills/qq-directory/tools/list_guilds/index.ts +1 -1
- package/agents/qq/skills/qq-group-admin/SKILL.md +34 -0
- package/agents/qq/skills/qq-group-admin/tools/group_kick/index.js +39 -0
- package/agents/qq/skills/qq-group-admin/tools/group_kick/index.ts +38 -0
- package/agents/qq/skills/qq-group-admin/tools/handle_join_request/index.js +33 -0
- package/agents/qq/skills/qq-group-admin/tools/handle_join_request/index.ts +32 -0
- package/agents/qq/skills/qq-group-admin/tools/recall_member_message/index.js +25 -0
- package/agents/qq/skills/qq-group-admin/tools/recall_member_message/index.ts +25 -0
- package/agents/qq/skills/qq-group-admin/tools/set_mute/index.js +37 -0
- package/agents/qq/skills/qq-group-admin/tools/set_mute/index.ts +36 -0
- package/agents/qq/skills/qq-group-directory/SKILL.md +24 -0
- package/agents/qq/skills/qq-group-directory/tools/group_info/index.js +16 -0
- package/agents/qq/skills/qq-group-directory/tools/group_info/index.ts +16 -0
- package/agents/qq/skills/qq-group-directory/tools/group_member_info/index.js +19 -0
- package/agents/qq/skills/qq-group-directory/tools/group_member_info/index.ts +19 -0
- package/agents/qq/skills/qq-group-directory/tools/group_members/index.js +19 -0
- package/agents/qq/skills/qq-group-directory/tools/group_members/index.ts +19 -0
- package/agents/qq/skills/qq-group-state/SKILL.md +21 -0
- package/agents/qq/skills/qq-group-state/tools/group_blacklist/index.js +19 -0
- package/agents/qq/skills/qq-group-state/tools/group_blacklist/index.ts +19 -0
- package/agents/qq/skills/qq-group-state/tools/list_join_requests/index.js +19 -0
- package/agents/qq/skills/qq-group-state/tools/list_join_requests/index.ts +19 -0
- package/agents/qq/skills/qq-group-state/tools/list_muted/index.js +21 -0
- package/agents/qq/skills/qq-group-state/tools/list_muted/index.ts +21 -0
- package/agents/qq/skills/qq-messages/SKILL.md +27 -0
- package/agents/qq/skills/qq-messages/tools/quote_reply/index.js +21 -0
- package/agents/qq/skills/qq-messages/tools/quote_reply/index.ts +21 -0
- package/agents/qq/skills/qq-messages/tools/recall_own_message/index.js +18 -0
- package/agents/qq/skills/qq-messages/tools/recall_own_message/index.ts +17 -0
- package/commands/qq/endpoint/definition.js +73 -55
- package/commands/qq/endpoint/definition.ts +75 -86
- package/lib/adapter.d.ts +13 -0
- package/lib/adapter.js +27 -0
- package/lib/client.d.ts +7 -9
- package/lib/client.js +1 -2
- package/lib/console.d.ts +3 -0
- package/lib/console.js +93 -0
- package/lib/endpoint.d.ts +17 -27
- package/lib/endpoint.js +325 -146
- package/lib/group-admin.d.ts +41 -0
- package/lib/group-admin.js +114 -0
- package/lib/i18n.js +4 -4
- package/lib/image-size.d.ts +6 -1
- package/lib/image-size.js +2 -3
- package/lib/index.d.ts +5 -4
- package/lib/index.js +3 -3
- package/lib/interaction.js +1 -1
- package/lib/outbound.d.ts +4 -1
- package/lib/outbound.js +3 -2
- package/lib/protocol.d.ts +7 -0
- package/lib/protocol.js +4 -0
- package/lib/qq-console-spec.d.ts +17 -0
- package/lib/qq-console-spec.js +21 -0
- package/lib/qq-console.d.ts +10 -0
- package/lib/qq-console.js +126 -0
- package/lib/qq-endpoint-write.d.ts +18 -0
- package/lib/qq-endpoint-write.js +7 -0
- package/lib/qq-runtime-state.d.ts +29 -9
- package/lib/qq-runtime-state.js +5 -5
- package/lib/qq-send.d.ts +7 -14
- package/lib/qq-send.js +20 -6
- package/lib/webhook.d.ts +3 -6
- package/lib/webhook.js +6 -49
- package/lib/ws.d.ts +10 -33
- package/lib/ws.js +21 -70
- package/middlewares/bot-kind-reply/index.js +1 -2
- package/middlewares/bot-kind-reply/index.ts +0 -2
- package/package.json +26 -18
- package/pages/index/index.js +15 -0
- package/pages/index/index.ts +16 -0
- package/plugin.js +9 -6
- package/schema.json +174 -0
- package/src/adapter.ts +23 -0
- package/src/client.ts +4 -12
- package/src/console.ts +103 -0
- package/src/endpoint.ts +328 -141
- package/src/group-admin.ts +155 -0
- package/src/i18n.ts +4 -4
- package/src/image-size.ts +7 -3
- package/src/index.ts +5 -5
- package/src/interaction.ts +1 -1
- package/src/outbound.ts +3 -2
- package/src/protocol.ts +10 -0
- package/src/qq-console-spec.ts +29 -0
- package/src/qq-console.ts +130 -0
- package/src/qq-endpoint-write.ts +24 -0
- package/src/qq-runtime-state.ts +31 -15
- package/src/qq-send.ts +28 -12
- package/src/webhook.ts +7 -57
- package/src/ws.ts +27 -105
|
@@ -3,16 +3,16 @@ import { resolveLocale } from 'zhin.js/i18n';
|
|
|
3
3
|
import { messages as t } from "../../../lib/i18n.js";
|
|
4
4
|
/**
|
|
5
5
|
* `qq endpoint` 命令的业务逻辑(与命令定义文件分离,便于测试)。
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* 扫码绑定 add 流程(经套件 bindFlow 钩子接入)与 cancel,并导出
|
|
6
|
+
* list / remove 与权限共用 @zhin.js/adapter 的账号管理入口;
|
|
7
|
+
* 本文件保留 QQ 扫码绑定 add 与 cancel,保存委派捕获的所属 Adapter CAS,并导出
|
|
9
8
|
* qqEndpointCommands 供 commands/qq/endpoint/ 下的命令文件默认导出。
|
|
10
9
|
*/
|
|
11
|
-
import {
|
|
10
|
+
import { createEndpointCommands, endpointCommandForbidden, createDurableEndpointCommandReply, isEndpointOperator, extractEndpointCommandReply, adapterFeatureId, isAdapterIndex } from 'zhin.js/adapter';
|
|
12
11
|
import { defineCommand } from 'zhin.js/command';
|
|
13
12
|
import { qqBotKindPrompt, qqCommandSessionKey } from "../../../lib/qq-bot-kind-prompt.js";
|
|
14
13
|
import { startQqBindFlow } from "../../../lib/qq-bind-flow.js";
|
|
15
|
-
import {
|
|
14
|
+
import { writeQqEndpointWithBotKind } from "../../../lib/qq-endpoint-write.js";
|
|
15
|
+
import { cancelQqConsoleBind } from "../../../lib/qq-console.js";
|
|
16
16
|
import { qqRuntimeStateToken, } from "../../../lib/qq-runtime-state.js";
|
|
17
17
|
export { qqRuntimeStateToken };
|
|
18
18
|
/**
|
|
@@ -21,7 +21,17 @@ export { qqRuntimeStateToken };
|
|
|
21
21
|
* legacy applyBindOwnership 会把 operator 写为新端点的 master)。
|
|
22
22
|
*/
|
|
23
23
|
export function isQqEndpointOperator(config, input) {
|
|
24
|
-
|
|
24
|
+
if (isEndpointOperator(config, input))
|
|
25
|
+
return true;
|
|
26
|
+
// The outage-notify target is the operator's conversation on another adapter.
|
|
27
|
+
const cfg = (config ?? {});
|
|
28
|
+
const notifyIds = [
|
|
29
|
+
cfg.notify?.id,
|
|
30
|
+
...(Array.isArray(cfg.endpoints) ? cfg.endpoints.map((endpoint) => endpoint.notify?.id) : []),
|
|
31
|
+
];
|
|
32
|
+
const senderRaw = input?.sender;
|
|
33
|
+
const senderId = (typeof senderRaw === 'object' && senderRaw?.id) ? String(senderRaw.id).trim() : '';
|
|
34
|
+
return senderId !== '' && notifyIds.some((id) => id != null && String(id) === senderId);
|
|
25
35
|
}
|
|
26
36
|
export const QQ_ENDPOINT_FORBIDDEN = endpointCommandForbidden('QQ');
|
|
27
37
|
/**
|
|
@@ -36,59 +46,47 @@ function busyFooter(state, locale) {
|
|
|
36
46
|
if (state.pendingBotKind) {
|
|
37
47
|
return t(locale, 'kindBusy', { id: state.pendingBotKind.endpointId });
|
|
38
48
|
}
|
|
49
|
+
if (state.consoleBind?.state === 'scanned') {
|
|
50
|
+
return t(locale, 'kindBusy', { id: state.consoleBind.pending?.endpointId ?? 'QQ' });
|
|
51
|
+
}
|
|
39
52
|
return undefined;
|
|
40
53
|
}
|
|
41
|
-
/** `qq endpoint list`:运行中的 endpoints(本 generation adapter create 注册)+ 配置里的 endpoints */
|
|
42
|
-
export async function runQqEndpointList(state, store, locale) {
|
|
43
|
-
return formatEndpointList(qqEndpointListSpec, {
|
|
44
|
-
running: state.endpoints.values(),
|
|
45
|
-
configured: await store.list('qq'),
|
|
46
|
-
footer: busyFooter(state, locale), locale,
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
54
|
const qqEndpointListSpec = {
|
|
50
55
|
adapterKey: 'qq',
|
|
51
56
|
adapterDisplayName: 'QQ',
|
|
52
57
|
describeEntry: (entry) => `appid: ${String(entry.appid)}`,
|
|
53
58
|
};
|
|
54
|
-
async function writeEndpointWithBotKind(pending, botKind,
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
botKind: intentFields.botKind,
|
|
66
|
-
intents: [...intentFields.intents],
|
|
67
|
-
},
|
|
68
|
-
environment: {
|
|
69
|
-
[appidKey]: pending.appId,
|
|
70
|
-
[secretKey]: pending.appSecret,
|
|
71
|
-
},
|
|
59
|
+
async function writeEndpointWithBotKind(pending, botKind, save) {
|
|
60
|
+
const result = await writeQqEndpointWithBotKind({
|
|
61
|
+
endpointId: pending.endpointId,
|
|
62
|
+
appId: pending.appId,
|
|
63
|
+
appSecret: pending.appSecret,
|
|
64
|
+
botKind,
|
|
65
|
+
}, save);
|
|
66
|
+
return t(pending.locale, 'endpointSaved', {
|
|
67
|
+
kind: result.botKind,
|
|
68
|
+
id: result.endpointId,
|
|
69
|
+
intents: result.intents.join(', '),
|
|
72
70
|
});
|
|
73
|
-
return t(locale, 'endpointSaved', { kind: intentFields.botKind, id: pending.endpointId, file: filePath, intents: intentFields.intents.join(', ') });
|
|
74
71
|
}
|
|
75
72
|
/**
|
|
76
|
-
* 完成 pending
|
|
73
|
+
* 完成 pending 的公域/私域选择:通过已捕获的所属 Adapter 保存一次账号配置。
|
|
77
74
|
* @returns 成功文案;调用方负责清 pending。
|
|
78
75
|
*/
|
|
79
|
-
export async function completeQqPendingBotKind(pending, botKind
|
|
80
|
-
return await writeEndpointWithBotKind(pending, botKind,
|
|
76
|
+
export async function completeQqPendingBotKind(pending, botKind) {
|
|
77
|
+
return await writeEndpointWithBotKind(pending, botKind, pending.save);
|
|
81
78
|
}
|
|
82
79
|
/**
|
|
83
80
|
* `qq endpoint add [id]`:启动扫码绑定流程。
|
|
84
81
|
* 返回的 Promise 在二维码链接就绪(或前置失败)时 resolve 为首条回复;
|
|
85
82
|
* 后续状态(已扫码 / 成功 / 失败 / 过期刷新)通过 reply 推回当前会话。
|
|
86
|
-
*
|
|
83
|
+
* 扫码成功后凭据暂存内存并询问公域/私域;确认后保存到账号库,等待显式启动。
|
|
87
84
|
*/
|
|
88
|
-
export function runQqEndpointAdd(state, id, reply,
|
|
89
|
-
if (state.bindFlow || state.pendingBotKind) {
|
|
85
|
+
export function runQqEndpointAdd(state, id, reply, save, input, locale = 'zh-CN', cancelCommand, commandHeads) {
|
|
86
|
+
if (state.bindFlow || state.pendingBotKind || state.consoleBind?.pending || state.consoleBind?.state === 'waiting') {
|
|
90
87
|
return Promise.resolve(t(locale, 'existingFlow'));
|
|
91
88
|
}
|
|
89
|
+
state.consoleBind = null;
|
|
92
90
|
const endpointId = id?.trim() || undefined;
|
|
93
91
|
const sessionKey = qqCommandSessionKey(input);
|
|
94
92
|
return new Promise((resolve) => {
|
|
@@ -128,8 +126,8 @@ export function runQqEndpointAdd(state, id, reply, store, input, locale = 'zh-CN
|
|
|
128
126
|
endpointId: finalName,
|
|
129
127
|
appId,
|
|
130
128
|
appSecret,
|
|
131
|
-
sessionKey: '', locale, cancelCommand,
|
|
132
|
-
}, 'public',
|
|
129
|
+
sessionKey: '', locale, cancelCommand, save,
|
|
130
|
+
}, 'public', save) +
|
|
133
131
|
t(locale, 'noInteractiveKind'));
|
|
134
132
|
return;
|
|
135
133
|
}
|
|
@@ -137,7 +135,7 @@ export function runQqEndpointAdd(state, id, reply, store, input, locale = 'zh-CN
|
|
|
137
135
|
endpointId: finalName,
|
|
138
136
|
appId,
|
|
139
137
|
appSecret,
|
|
140
|
-
sessionKey, locale, cancelCommand, commandHeads,
|
|
138
|
+
sessionKey, locale, cancelCommand, commandHeads, save,
|
|
141
139
|
};
|
|
142
140
|
settle(t(locale, 'scanned', { id: finalName, prompt: qqBotKindPrompt(locale, cancelCommand) }));
|
|
143
141
|
}
|
|
@@ -159,6 +157,7 @@ export function runQqEndpointCancel(state, preference) {
|
|
|
159
157
|
if (state.bindFlow) {
|
|
160
158
|
state.bindFlow.stop();
|
|
161
159
|
state.bindFlow = null;
|
|
160
|
+
cancelQqConsoleBind(state);
|
|
162
161
|
return t(locale, 'cancelled');
|
|
163
162
|
}
|
|
164
163
|
if (state.pendingBotKind) {
|
|
@@ -166,25 +165,44 @@ export function runQqEndpointCancel(state, preference) {
|
|
|
166
165
|
state.pendingBotKind = null;
|
|
167
166
|
return t(locale, 'pendingCancelled', { name });
|
|
168
167
|
}
|
|
168
|
+
if (state.consoleBind) {
|
|
169
|
+
cancelQqConsoleBind(state);
|
|
170
|
+
return t(locale, 'cancelled');
|
|
171
|
+
}
|
|
169
172
|
return t(locale, 'noFlow');
|
|
170
173
|
}
|
|
171
|
-
/** `qq endpoint remove <id>`:从 zhin.config.yml 移除对应 endpoints 项 */
|
|
172
|
-
export async function runQqEndpointRemove(_state, id, store) {
|
|
173
|
-
return await removeEndpointById(qqEndpointListSpec, id, store);
|
|
174
|
-
}
|
|
175
174
|
/**
|
|
176
175
|
* 通用套件生成的 QQ endpoint 命令(add 经 bindFlow 钩子走扫码绑定)。
|
|
177
176
|
* commands/qq/endpoint/ 下的 list / add / remove 直接默认导出这三项;cancel 为 QQ 特化,单独定义。
|
|
178
177
|
*/
|
|
179
|
-
|
|
178
|
+
const standard = createEndpointCommands({
|
|
180
179
|
...qqEndpointListSpec,
|
|
181
|
-
fields: [
|
|
182
|
-
{ key: 'appid', required: true, env: true, description: 'QQ 机器人 appid', descriptionI18n: { en: "QQ bot app ID" } },
|
|
183
|
-
{ key: 'secret', required: true, env: true, description: 'QQ 机器人 secret', descriptionI18n: { en: "QQ bot secret" } },
|
|
184
|
-
],
|
|
185
|
-
running: (use) => use(qqRuntimeStateToken).endpoints.values(),
|
|
180
|
+
fields: [{ key: 'appid', required: true }, { key: 'secret', required: true, secret: true }],
|
|
186
181
|
listFooter: (use, locale) => busyFooter(use(qqRuntimeStateToken), locale),
|
|
187
|
-
addDescriptionI18n: { en: 'Bind a bot through mobile QQ, choose public/private, then save configuration; restart to apply' },
|
|
188
|
-
addDescription: '手机 QQ 扫码绑定机器人:确认公域/私域后一次性写入 .env 与 zhin.config.yml(重启生效)',
|
|
189
|
-
bindFlow: ({ id, reply, input, use, locale, formatCommand }) => runQqEndpointAdd(use(qqRuntimeStateToken), id, reply, use(endpointConfigurationStoreToken), input, locale, formatCommand?.('qq endpoint cancel'), ['qq endpoint list', 'qq endpoint add', 'qq endpoint remove', 'qq endpoint cancel'].flatMap(path => { const head = formatCommand?.(path); return head ? [head] : []; })),
|
|
190
182
|
}, defineCommand);
|
|
183
|
+
export const qqEndpointCommands = {
|
|
184
|
+
...standard,
|
|
185
|
+
add: defineCommand({
|
|
186
|
+
description: '手机 QQ 扫码绑定;确认公域/私域后保存账号,手动启动',
|
|
187
|
+
descriptionI18n: { en: 'Scan with mobile QQ, choose public/private, save the account and start explicitly' },
|
|
188
|
+
params: { id: { type: 'string', description: 'endpoint ID' } },
|
|
189
|
+
async execute(context) {
|
|
190
|
+
if (!isEndpointOperator(context.config, context.input))
|
|
191
|
+
return endpointCommandForbidden('QQ', context.locale);
|
|
192
|
+
const index = context.project(adapterFeatureId);
|
|
193
|
+
if (!isAdapterIndex(index))
|
|
194
|
+
throw new Error('QQ Adapter projection is unavailable');
|
|
195
|
+
const own = index.forOwner(context.owner.id).adapters().filter(adapter => adapter.descriptor.name === 'qq');
|
|
196
|
+
if (own.length !== 1)
|
|
197
|
+
throw new Error('QQ Adapter ownership is ambiguous');
|
|
198
|
+
const adapter = own[0];
|
|
199
|
+
if (adapter.needsMigration)
|
|
200
|
+
return '首次修改账号前,请由管理员在 Console 确认迁移账号清单';
|
|
201
|
+
if (!adapter.canManage)
|
|
202
|
+
return '此环境未提供持久化账号管理';
|
|
203
|
+
const revision = adapter.revision;
|
|
204
|
+
const save = config => adapter.add(revision, { endpointId: config.id, config: { ...config } }, { waitForExecution: false });
|
|
205
|
+
return runQqEndpointAdd(context.use(qqRuntimeStateToken), context.params.id, createDurableEndpointCommandReply(context.input, context.use), save, context.input, context.locale, context.formatCommand?.('qq endpoint cancel'), ['qq endpoint list', 'qq endpoint add', 'qq endpoint remove', 'qq endpoint cancel'].flatMap(path => { const head = context.formatCommand?.(path); return head ? [head] : []; }));
|
|
206
|
+
},
|
|
207
|
+
}),
|
|
208
|
+
};
|
|
@@ -2,28 +2,18 @@ import { resolveLocale, type Locale } from 'zhin.js/i18n';
|
|
|
2
2
|
import { messages as t } from '../../../src/i18n.js';
|
|
3
3
|
/**
|
|
4
4
|
* `qq endpoint` 命令的业务逻辑(与命令定义文件分离,便于测试)。
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* 扫码绑定 add 流程(经套件 bindFlow 钩子接入)与 cancel,并导出
|
|
5
|
+
* list / remove 与权限共用 @zhin.js/adapter 的账号管理入口;
|
|
6
|
+
* 本文件保留 QQ 扫码绑定 add 与 cancel,保存委派捕获的所属 Adapter CAS,并导出
|
|
8
7
|
* qqEndpointCommands 供 commands/qq/endpoint/ 下的命令文件默认导出。
|
|
9
8
|
*/
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
createEndpointCommands,
|
|
13
|
-
endpointCommandForbidden,
|
|
14
|
-
endpointConfigurationStoreToken,
|
|
15
|
-
extractEndpointCommandReply,
|
|
16
|
-
formatEndpointList,
|
|
17
|
-
isEndpointOperator,
|
|
18
|
-
removeEndpointById,
|
|
19
|
-
type ConfiguredEndpointEntry,
|
|
20
|
-
type EndpointCommandReply,
|
|
21
|
-
type EndpointConfigurationStore,
|
|
22
|
-
} from 'zhin.js/adapter';
|
|
9
|
+
import { createEndpointCommands, endpointCommandForbidden, createDurableEndpointCommandReply, isEndpointOperator, extractEndpointCommandReply,
|
|
10
|
+
adapterFeatureId, isAdapterIndex, type ConfiguredEndpointEntry, type EndpointCommandReply } from 'zhin.js/adapter';
|
|
23
11
|
import { defineCommand } from 'zhin.js/command';
|
|
24
12
|
import { qqBotKindPrompt, qqCommandSessionKey } from '../../../src/qq-bot-kind-prompt.js';
|
|
25
13
|
import { startQqBindFlow } from '../../../src/qq-bind-flow.js';
|
|
26
|
-
import {
|
|
14
|
+
import { writeQqEndpointWithBotKind, type QqEndpointSavePort } from '../../../src/qq-endpoint-write.js';
|
|
15
|
+
import type { QqBotKind } from '../../../src/qq-intents.js';
|
|
16
|
+
import { cancelQqConsoleBind } from '../../../src/qq-console.js';
|
|
27
17
|
import {
|
|
28
18
|
qqRuntimeStateToken,
|
|
29
19
|
type QqPendingBotKind,
|
|
@@ -40,7 +30,19 @@ export type QqCommandReply = EndpointCommandReply;
|
|
|
40
30
|
* legacy applyBindOwnership 会把 operator 写为新端点的 master)。
|
|
41
31
|
*/
|
|
42
32
|
export function isQqEndpointOperator(config: unknown, input: unknown): boolean {
|
|
43
|
-
|
|
33
|
+
if (isEndpointOperator(config, input)) return true;
|
|
34
|
+
// The outage-notify target is the operator's conversation on another adapter.
|
|
35
|
+
const cfg = (config ?? {}) as {
|
|
36
|
+
notify?: { id?: unknown };
|
|
37
|
+
endpoints?: readonly { notify?: { id?: unknown } }[];
|
|
38
|
+
};
|
|
39
|
+
const notifyIds = [
|
|
40
|
+
cfg.notify?.id,
|
|
41
|
+
...(Array.isArray(cfg.endpoints) ? cfg.endpoints.map((endpoint) => endpoint.notify?.id) : []),
|
|
42
|
+
];
|
|
43
|
+
const senderRaw = (input as { sender?: { id?: unknown } } | null | undefined)?.sender;
|
|
44
|
+
const senderId = (typeof senderRaw === 'object' && senderRaw?.id) ? String(senderRaw.id).trim() : '';
|
|
45
|
+
return senderId !== '' && notifyIds.some((id) => id != null && String(id) === senderId);
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
export const QQ_ENDPOINT_FORBIDDEN = endpointCommandForbidden('QQ');
|
|
@@ -57,22 +59,12 @@ function busyFooter(state: QqRuntimeState, locale?: Locale): string | undefined
|
|
|
57
59
|
if (state.pendingBotKind) {
|
|
58
60
|
return t(locale, 'kindBusy', { id: state.pendingBotKind.endpointId });
|
|
59
61
|
}
|
|
62
|
+
if (state.consoleBind?.state === 'scanned') {
|
|
63
|
+
return t(locale, 'kindBusy', { id: state.consoleBind.pending?.endpointId ?? 'QQ' });
|
|
64
|
+
}
|
|
60
65
|
return undefined;
|
|
61
66
|
}
|
|
62
67
|
|
|
63
|
-
/** `qq endpoint list`:运行中的 endpoints(本 generation adapter create 注册)+ 配置里的 endpoints */
|
|
64
|
-
export async function runQqEndpointList(
|
|
65
|
-
state: QqRuntimeState,
|
|
66
|
-
store: EndpointConfigurationStore,
|
|
67
|
-
locale?: Locale,
|
|
68
|
-
): Promise<string> {
|
|
69
|
-
return formatEndpointList(qqEndpointListSpec, {
|
|
70
|
-
running: state.endpoints.values(),
|
|
71
|
-
configured: await store.list('qq'),
|
|
72
|
-
footer: busyFooter(state, locale), locale,
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
68
|
const qqEndpointListSpec = {
|
|
77
69
|
adapterKey: 'qq',
|
|
78
70
|
adapterDisplayName: 'QQ',
|
|
@@ -82,62 +74,54 @@ const qqEndpointListSpec = {
|
|
|
82
74
|
async function writeEndpointWithBotKind(
|
|
83
75
|
pending: QqPendingBotKind,
|
|
84
76
|
botKind: QqBotKind,
|
|
85
|
-
|
|
77
|
+
save: QqEndpointSavePort,
|
|
86
78
|
): Promise<string> {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
botKind: intentFields.botKind,
|
|
98
|
-
intents: [...intentFields.intents],
|
|
99
|
-
},
|
|
100
|
-
environment: {
|
|
101
|
-
[appidKey]: pending.appId,
|
|
102
|
-
[secretKey]: pending.appSecret,
|
|
103
|
-
},
|
|
79
|
+
const result = await writeQqEndpointWithBotKind({
|
|
80
|
+
endpointId: pending.endpointId,
|
|
81
|
+
appId: pending.appId,
|
|
82
|
+
appSecret: pending.appSecret,
|
|
83
|
+
botKind,
|
|
84
|
+
}, save);
|
|
85
|
+
return t(pending.locale, 'endpointSaved', {
|
|
86
|
+
kind: result.botKind,
|
|
87
|
+
id: result.endpointId,
|
|
88
|
+
intents: result.intents.join(', '),
|
|
104
89
|
});
|
|
105
|
-
return t(locale, 'endpointSaved', { kind: intentFields.botKind, id: pending.endpointId, file: filePath, intents: intentFields.intents.join(', ') });
|
|
106
90
|
}
|
|
107
91
|
|
|
108
92
|
/**
|
|
109
|
-
* 完成 pending
|
|
93
|
+
* 完成 pending 的公域/私域选择:通过已捕获的所属 Adapter 保存一次账号配置。
|
|
110
94
|
* @returns 成功文案;调用方负责清 pending。
|
|
111
95
|
*/
|
|
112
96
|
export async function completeQqPendingBotKind(
|
|
113
97
|
pending: QqPendingBotKind,
|
|
114
98
|
botKind: QqBotKind,
|
|
115
|
-
store: EndpointConfigurationStore,
|
|
116
99
|
): Promise<string> {
|
|
117
|
-
return await writeEndpointWithBotKind(pending, botKind,
|
|
100
|
+
return await writeEndpointWithBotKind(pending, botKind, pending.save);
|
|
118
101
|
}
|
|
119
102
|
|
|
120
103
|
/**
|
|
121
104
|
* `qq endpoint add [id]`:启动扫码绑定流程。
|
|
122
105
|
* 返回的 Promise 在二维码链接就绪(或前置失败)时 resolve 为首条回复;
|
|
123
106
|
* 后续状态(已扫码 / 成功 / 失败 / 过期刷新)通过 reply 推回当前会话。
|
|
124
|
-
*
|
|
107
|
+
* 扫码成功后凭据暂存内存并询问公域/私域;确认后保存到账号库,等待显式启动。
|
|
125
108
|
*/
|
|
126
109
|
export function runQqEndpointAdd(
|
|
127
110
|
state: QqRuntimeState,
|
|
128
111
|
id: string | undefined,
|
|
129
112
|
reply: QqCommandReply,
|
|
130
|
-
|
|
113
|
+
save: QqEndpointSavePort,
|
|
131
114
|
input?: unknown,
|
|
132
115
|
locale: Locale = 'zh-CN',
|
|
133
116
|
cancelCommand?: string,
|
|
134
117
|
commandHeads?: readonly string[],
|
|
135
118
|
): Promise<string> {
|
|
136
|
-
if (state.bindFlow || state.pendingBotKind) {
|
|
119
|
+
if (state.bindFlow || state.pendingBotKind || state.consoleBind?.pending || state.consoleBind?.state === 'waiting') {
|
|
137
120
|
return Promise.resolve(
|
|
138
121
|
t(locale, 'existingFlow'),
|
|
139
122
|
);
|
|
140
123
|
}
|
|
124
|
+
state.consoleBind = null;
|
|
141
125
|
const endpointId = id?.trim() || undefined;
|
|
142
126
|
const sessionKey = qqCommandSessionKey(input);
|
|
143
127
|
return new Promise<string>((resolve) => {
|
|
@@ -182,10 +166,10 @@ export function runQqEndpointAdd(
|
|
|
182
166
|
endpointId: finalName,
|
|
183
167
|
appId,
|
|
184
168
|
appSecret,
|
|
185
|
-
sessionKey: '', locale, cancelCommand,
|
|
169
|
+
sessionKey: '', locale, cancelCommand, save,
|
|
186
170
|
},
|
|
187
171
|
'public',
|
|
188
|
-
|
|
172
|
+
save,
|
|
189
173
|
) +
|
|
190
174
|
t(locale, 'noInteractiveKind'),
|
|
191
175
|
);
|
|
@@ -196,7 +180,7 @@ export function runQqEndpointAdd(
|
|
|
196
180
|
endpointId: finalName,
|
|
197
181
|
appId,
|
|
198
182
|
appSecret,
|
|
199
|
-
sessionKey, locale, cancelCommand, commandHeads,
|
|
183
|
+
sessionKey, locale, cancelCommand, commandHeads, save,
|
|
200
184
|
};
|
|
201
185
|
settle(
|
|
202
186
|
t(locale, 'scanned', { id: finalName, prompt: qqBotKindPrompt(locale, cancelCommand) }),
|
|
@@ -222,6 +206,7 @@ export function runQqEndpointCancel(state: QqRuntimeState, preference?: Locale):
|
|
|
222
206
|
if (state.bindFlow) {
|
|
223
207
|
state.bindFlow.stop();
|
|
224
208
|
state.bindFlow = null;
|
|
209
|
+
cancelQqConsoleBind(state);
|
|
225
210
|
return t(locale, 'cancelled');
|
|
226
211
|
}
|
|
227
212
|
if (state.pendingBotKind) {
|
|
@@ -229,40 +214,44 @@ export function runQqEndpointCancel(state: QqRuntimeState, preference?: Locale):
|
|
|
229
214
|
state.pendingBotKind = null;
|
|
230
215
|
return t(locale, 'pendingCancelled', { name });
|
|
231
216
|
}
|
|
217
|
+
if (state.consoleBind) {
|
|
218
|
+
cancelQqConsoleBind(state);
|
|
219
|
+
return t(locale, 'cancelled');
|
|
220
|
+
}
|
|
232
221
|
return t(locale, 'noFlow');
|
|
233
222
|
}
|
|
234
223
|
|
|
235
|
-
/** `qq endpoint remove <id>`:从 zhin.config.yml 移除对应 endpoints 项 */
|
|
236
|
-
export async function runQqEndpointRemove(
|
|
237
|
-
_state: QqRuntimeState,
|
|
238
|
-
id: string,
|
|
239
|
-
store: EndpointConfigurationStore,
|
|
240
|
-
): Promise<string> {
|
|
241
|
-
return await removeEndpointById(qqEndpointListSpec, id, store);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
224
|
/**
|
|
245
225
|
* 通用套件生成的 QQ endpoint 命令(add 经 bindFlow 钩子走扫码绑定)。
|
|
246
226
|
* commands/qq/endpoint/ 下的 list / add / remove 直接默认导出这三项;cancel 为 QQ 特化,单独定义。
|
|
247
227
|
*/
|
|
248
|
-
|
|
228
|
+
const standard = createEndpointCommands({
|
|
249
229
|
...qqEndpointListSpec,
|
|
250
|
-
fields: [
|
|
251
|
-
{ key: 'appid', required: true, env: true, description: 'QQ 机器人 appid', descriptionI18n: { en: "QQ bot app ID" } },
|
|
252
|
-
{ key: 'secret', required: true, env: true, description: 'QQ 机器人 secret', descriptionI18n: { en: "QQ bot secret" } },
|
|
253
|
-
],
|
|
254
|
-
running: (use) => use(qqRuntimeStateToken).endpoints.values(),
|
|
230
|
+
fields: [{ key: 'appid', required: true }, { key: 'secret', required: true, secret: true }],
|
|
255
231
|
listFooter: (use, locale) => busyFooter(use(qqRuntimeStateToken), locale),
|
|
256
|
-
addDescriptionI18n: { en: 'Bind a bot through mobile QQ, choose public/private, then save configuration; restart to apply' },
|
|
257
|
-
addDescription:
|
|
258
|
-
'手机 QQ 扫码绑定机器人:确认公域/私域后一次性写入 .env 与 zhin.config.yml(重启生效)',
|
|
259
|
-
bindFlow: ({ id, reply, input, use, locale, formatCommand }) =>
|
|
260
|
-
runQqEndpointAdd(
|
|
261
|
-
use(qqRuntimeStateToken),
|
|
262
|
-
id,
|
|
263
|
-
reply,
|
|
264
|
-
use(endpointConfigurationStoreToken),
|
|
265
|
-
input, locale, formatCommand?.('qq endpoint cancel'),
|
|
266
|
-
['qq endpoint list', 'qq endpoint add', 'qq endpoint remove', 'qq endpoint cancel'].flatMap(path => { const head = formatCommand?.(path); return head ? [head] : []; }),
|
|
267
|
-
),
|
|
268
232
|
}, defineCommand);
|
|
233
|
+
|
|
234
|
+
export const qqEndpointCommands = {
|
|
235
|
+
...standard,
|
|
236
|
+
add: defineCommand({
|
|
237
|
+
description: '手机 QQ 扫码绑定;确认公域/私域后保存账号,手动启动',
|
|
238
|
+
descriptionI18n: { en: 'Scan with mobile QQ, choose public/private, save the account and start explicitly' },
|
|
239
|
+
params: { id: { type: 'string', description: 'endpoint ID' } },
|
|
240
|
+
async execute(context) {
|
|
241
|
+
if (!isEndpointOperator(context.config, context.input)) return endpointCommandForbidden('QQ', context.locale);
|
|
242
|
+
const index = context.project<unknown>(adapterFeatureId);
|
|
243
|
+
if (!isAdapterIndex(index)) throw new Error('QQ Adapter projection is unavailable');
|
|
244
|
+
const own = index.forOwner(context.owner.id).adapters().filter(adapter => adapter.descriptor.name === 'qq');
|
|
245
|
+
if (own.length !== 1) throw new Error('QQ Adapter ownership is ambiguous');
|
|
246
|
+
const adapter = own[0];
|
|
247
|
+
if (adapter.needsMigration) return '首次修改账号前,请由管理员在 Console 确认迁移账号清单';
|
|
248
|
+
if (!adapter.canManage) return '此环境未提供持久化账号管理';
|
|
249
|
+
const revision = adapter.revision;
|
|
250
|
+
const save: QqEndpointSavePort = config => adapter.add(revision, { endpointId: config.id, config: { ...config } }, { waitForExecution: false });
|
|
251
|
+
return runQqEndpointAdd(context.use(qqRuntimeStateToken), context.params.id,
|
|
252
|
+
createDurableEndpointCommandReply(context.input, context.use), save, context.input, context.locale,
|
|
253
|
+
context.formatCommand?.('qq endpoint cancel'),
|
|
254
|
+
['qq endpoint list', 'qq endpoint add', 'qq endpoint remove', 'qq endpoint cancel'].flatMap(path => { const head = context.formatCommand?.(path); return head ? [head] : []; }));
|
|
255
|
+
},
|
|
256
|
+
}),
|
|
257
|
+
};
|
package/lib/adapter.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Adapter, type AdapterContext, type EndpointCreationContext } from 'zhin.js/adapter';
|
|
2
|
+
import { QqHttpEndpoint, QqWebsocketEndpoint } from './endpoint.js';
|
|
3
|
+
import { type QqEndpointConfig } from './protocol.js';
|
|
4
|
+
import type { QqClientEventMap } from './client.js';
|
|
5
|
+
export declare class QqAdapter extends Adapter<QqHttpEndpoint | QqWebsocketEndpoint, QqEndpointConfig, QqClientEventMap> {
|
|
6
|
+
private readonly context;
|
|
7
|
+
constructor(context: AdapterContext<QqEndpointConfig>);
|
|
8
|
+
get configurationIdentityFields(): readonly string[];
|
|
9
|
+
get configurationIdentityProbeFields(): readonly string[];
|
|
10
|
+
get configurationUpdateFields(): readonly string[];
|
|
11
|
+
protected validateConfiguration(input: QqEndpointConfig): void;
|
|
12
|
+
protected createEndpoint(input: QqEndpointConfig, execution: EndpointCreationContext): QqHttpEndpoint | QqWebsocketEndpoint;
|
|
13
|
+
}
|
package/lib/adapter.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Adapter, createAdapterOutageNotifier } from 'zhin.js/adapter';
|
|
2
|
+
import { outboundHostToken } from 'zhin.js';
|
|
3
|
+
import { httpHostToken } from '@zhin.js/host-http';
|
|
4
|
+
import { QqHttpEndpoint, QqWebsocketEndpoint } from './endpoint.js';
|
|
5
|
+
import { resolveQqConfig } from './protocol.js';
|
|
6
|
+
export class QqAdapter extends Adapter {
|
|
7
|
+
context;
|
|
8
|
+
constructor(context) {
|
|
9
|
+
super();
|
|
10
|
+
this.context = context;
|
|
11
|
+
}
|
|
12
|
+
get configurationIdentityFields() { return ['id', 'appid']; }
|
|
13
|
+
get configurationIdentityProbeFields() { return ['accessTokenUrl', 'gatewayUrl']; }
|
|
14
|
+
get configurationUpdateFields() {
|
|
15
|
+
return ['secret', 'mode', 'sandbox', 'intents', 'botKind',
|
|
16
|
+
'accessTokenUrl', 'gatewayUrl', 'streamProxy', 'streamProxy.port', 'streamProxy.serverName', 'webhookPath'];
|
|
17
|
+
}
|
|
18
|
+
validateConfiguration(input) { resolveQqConfig(input); }
|
|
19
|
+
createEndpoint(input, execution) {
|
|
20
|
+
const config = resolveQqConfig(input);
|
|
21
|
+
const outbound = this.context.has?.(outboundHostToken) ? this.context.use(outboundHostToken) : undefined;
|
|
22
|
+
const notifier = createAdapterOutageNotifier(outbound, config.notify);
|
|
23
|
+
return config.mode === 'websocket'
|
|
24
|
+
? new QqWebsocketEndpoint({ id: execution.canonicalId, config, notifier })
|
|
25
|
+
: new QqHttpEndpoint({ id: execution.canonicalId, config, notifier, http: this.context.use(httpHostToken) });
|
|
26
|
+
}
|
|
27
|
+
}
|
package/lib/client.d.ts
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
3
|
-
export type QqClientEventMap =
|
|
1
|
+
import type { EventMap } from 'qq-official-bot';
|
|
2
|
+
import type { QqAdapter } from './adapter.js';
|
|
3
|
+
export type QqClientEventMap = {
|
|
4
|
+
readonly [K in keyof EventMap]: EventMap[K] extends (event: infer E, ...args: never[]) => unknown ? E : never;
|
|
5
|
+
};
|
|
4
6
|
declare module '@zhin.js/feature-kit' {
|
|
5
|
-
interface
|
|
6
|
-
readonly qq:
|
|
7
|
-
readonly client: QqBotTransport;
|
|
8
|
-
readonly events: QqClientEventMap;
|
|
9
|
-
};
|
|
7
|
+
interface AdapterRegistry {
|
|
8
|
+
readonly qq: QqAdapter;
|
|
10
9
|
}
|
|
11
10
|
}
|
|
12
|
-
export declare const qqClient: import("@zhin.js/adapter").EndpointClientToken<QqBotTransport, QqClientEventMap>;
|
package/lib/client.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export const qqClient = defineEndpointClient('qq');
|
|
1
|
+
export {};
|
package/lib/console.d.ts
ADDED
package/lib/console.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QQ Console RPC:除通用 endpoint 管理外,把 `qq endpoint add` 的扫码绑定
|
|
3
|
+
* (以及公域 / 私域选择、取消)搬到 Console 页面。
|
|
4
|
+
*/
|
|
5
|
+
import { prepareConsoleAdapterMutation, ownedConsoleAdapter, createAdapterConsoleRpc, } from 'zhin.js/adapter';
|
|
6
|
+
import { PluginConsoleRpcError } from 'zhin.js';
|
|
7
|
+
import { qqConsoleSpec } from './qq-console-spec.js';
|
|
8
|
+
import { cancelQqConsoleBind, completeQqConsoleBind, startQqConsoleBind, } from './qq-console.js';
|
|
9
|
+
export function createQqConsoleRpc(state, getConfig) {
|
|
10
|
+
const spec = {
|
|
11
|
+
...qqConsoleSpec,
|
|
12
|
+
getConfig,
|
|
13
|
+
extraStatus: () => {
|
|
14
|
+
const bind = state.consoleBind;
|
|
15
|
+
if (!bind) {
|
|
16
|
+
return { bindState: state.bindFlow ? 'binding' : 'idle' };
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
bindState: bind.state,
|
|
20
|
+
...(bind.url ? { scanUrl: bind.url } : {}),
|
|
21
|
+
...(bind.message ? { bindMessage: bind.message } : {}),
|
|
22
|
+
...(bind.pending ? { bindEndpoint: bind.pending.endpointId } : {}),
|
|
23
|
+
...(bind.error ? { bindError: bind.error } : {}),
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
availableActions: () => {
|
|
27
|
+
if (state.consoleBind?.pending)
|
|
28
|
+
return ['qq-bind-kind', 'qq-bind-cancel'];
|
|
29
|
+
if (state.bindFlow || state.consoleBind?.state === 'waiting')
|
|
30
|
+
return ['qq-bind-cancel'];
|
|
31
|
+
return ['qq-bind-start'];
|
|
32
|
+
},
|
|
33
|
+
actions: {
|
|
34
|
+
'qq-bind-start': {
|
|
35
|
+
label: '扫码绑定新机器人',
|
|
36
|
+
labelI18n: { en: 'Bind a bot with QR' },
|
|
37
|
+
description: '用手机 QQ 扫码,选择公域 / 私域后保存到账号库;保存后需手动启动。',
|
|
38
|
+
descriptionI18n: { en: 'Scan with mobile QQ, choose public/private and save the account; start it explicitly afterwards.' },
|
|
39
|
+
handler: async (input, call) => {
|
|
40
|
+
if (!call.runtime)
|
|
41
|
+
throw new PluginConsoleRpcError('unsupported', '账号运行目录不可用');
|
|
42
|
+
const adapter = ownedConsoleAdapter(spec, call);
|
|
43
|
+
if (!adapter.canManage)
|
|
44
|
+
throw new PluginConsoleRpcError('unsupported', '此环境未提供持久化账号管理');
|
|
45
|
+
if (input.expectedGeneration !== call.runtime.generation || input.expectedRevision !== adapter.revision) {
|
|
46
|
+
throw new PluginConsoleRpcError('conflict', '账号清单已变化,请刷新核实');
|
|
47
|
+
}
|
|
48
|
+
const starting = startQqConsoleBind(state);
|
|
49
|
+
state.consoleAdmission = { adapterId: adapter.descriptor.id, generation: call.runtime.generation, revision: adapter.revision };
|
|
50
|
+
const { url } = await starting;
|
|
51
|
+
return { message: '请用手机 QQ 扫码;扫码成功后选择公域或私域', url };
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
'qq-bind-kind': {
|
|
55
|
+
accountMutation: true,
|
|
56
|
+
label: '选择公域 / 私域',
|
|
57
|
+
labelI18n: { en: 'Choose public/private' },
|
|
58
|
+
description: '公域订阅 PUBLIC_GUILD_MESSAGES;私域订阅 GUILD_MESSAGES。',
|
|
59
|
+
descriptionI18n: { en: 'Public subscribes PUBLIC_GUILD_MESSAGES; private subscribes GUILD_MESSAGES.' },
|
|
60
|
+
options: [
|
|
61
|
+
{ value: 'public', label: '公域', labelI18n: { en: 'Public' } },
|
|
62
|
+
{ value: 'private', label: '私域', labelI18n: { en: 'Private' } },
|
|
63
|
+
],
|
|
64
|
+
handler: async (input, call) => {
|
|
65
|
+
const captured = state.consoleAdmission;
|
|
66
|
+
if (!captured || captured.generation !== input.expectedGeneration || captured.revision !== input.expectedRevision) {
|
|
67
|
+
throw new PluginConsoleRpcError('conflict', '扫码期间账号清单已变化,请刷新核实');
|
|
68
|
+
}
|
|
69
|
+
const admission = prepareConsoleAdapterMutation(spec, call, input);
|
|
70
|
+
if (admission.adapter.descriptor.id !== captured.adapterId)
|
|
71
|
+
throw new PluginConsoleRpcError('conflict', '扫码所属 Adapter 已变化');
|
|
72
|
+
const result = await completeQqConsoleBind(state, config => admission.adapter.add(admission.revision, { endpointId: config.id, config: { ...config } }, admission.options), input.value);
|
|
73
|
+
return {
|
|
74
|
+
message: `已保存账号「${result.endpointId}」(${result.botKind}),尚未启动`,
|
|
75
|
+
saved: true, restartRequired: false, execution: { ...result.mutation.execution },
|
|
76
|
+
details: { endpoint: result.endpointId, botKind: result.botKind, intents: result.intents.join(', ') },
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
'qq-bind-cancel': {
|
|
81
|
+
label: '取消扫码绑定',
|
|
82
|
+
labelI18n: { en: 'Cancel QR binding' },
|
|
83
|
+
description: '中止正在进行的扫码绑定;已写入的配置不会删除。',
|
|
84
|
+
descriptionI18n: { en: 'Stop the current QR binding flow; already-saved configuration is untouched.' },
|
|
85
|
+
handler: async () => {
|
|
86
|
+
const cancelled = cancelQqConsoleBind(state);
|
|
87
|
+
return { message: cancelled ? '已取消绑定' : '当前没有进行中的绑定流程' };
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
return createAdapterConsoleRpc(spec);
|
|
93
|
+
}
|