@zhin.js/adapter-qq 1.1.18 → 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 +39 -0
- package/README.md +21 -18
- package/adapters/qq/index.js +4 -38
- package/adapters/qq/index.ts +3 -46
- package/agents/qq/conversation.js +1 -1
- package/agents/qq/conversation.ts +1 -1
- 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 +1 -1
- package/agents/qq/skills/qq/tools/guild_kick_member/index.ts +1 -1
- package/agents/qq/skills/qq/tools/guild_mute_member/index.js +1 -1
- package/agents/qq/skills/qq/tools/guild_mute_member/index.ts +1 -1
- 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/tools/conversation_info/index.js +1 -1
- package/agents/qq/skills/qq-conversation/tools/conversation_info/index.ts +1 -1
- 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/tools/group_kick/index.js +2 -2
- package/agents/qq/skills/qq-group-admin/tools/group_kick/index.ts +2 -2
- package/agents/qq/skills/qq-group-admin/tools/handle_join_request/index.js +1 -1
- package/agents/qq/skills/qq-group-admin/tools/handle_join_request/index.ts +1 -1
- package/agents/qq/skills/qq-group-admin/tools/set_mute/index.js +2 -2
- package/agents/qq/skills/qq-group-admin/tools/set_mute/index.ts +2 -2
- package/agents/qq/skills/qq-group-directory/tools/group_info/index.js +1 -1
- package/agents/qq/skills/qq-group-directory/tools/group_info/index.ts +1 -1
- package/agents/qq/skills/qq-group-directory/tools/group_member_info/index.js +1 -1
- package/agents/qq/skills/qq-group-directory/tools/group_member_info/index.ts +1 -1
- package/agents/qq/skills/qq-group-directory/tools/group_members/index.js +1 -1
- package/agents/qq/skills/qq-group-directory/tools/group_members/index.ts +1 -1
- package/agents/qq/skills/qq-group-state/tools/group_blacklist/index.js +1 -1
- package/agents/qq/skills/qq-group-state/tools/group_blacklist/index.ts +1 -1
- package/agents/qq/skills/qq-group-state/tools/list_join_requests/index.js +1 -1
- package/agents/qq/skills/qq-group-state/tools/list_join_requests/index.ts +1 -1
- package/agents/qq/skills/qq-group-state/tools/list_muted/index.js +1 -1
- package/agents/qq/skills/qq-group-state/tools/list_muted/index.ts +1 -1
- package/commands/qq/endpoint/definition.js +41 -36
- package/commands/qq/endpoint/definition.ts +41 -67
- 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 +1 -6
- package/lib/console.js +29 -20
- package/lib/endpoint.d.ts +8 -27
- package/lib/endpoint.js +218 -165
- package/lib/group-admin.d.ts +14 -9
- package/lib/group-admin.js +21 -20
- 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 +2 -0
- package/lib/qq-console-spec.js +12 -1
- package/lib/qq-console.d.ts +3 -4
- package/lib/qq-console.js +16 -6
- package/lib/qq-endpoint-write.d.ts +6 -10
- package/lib/qq-endpoint-write.js +5 -32
- package/lib/qq-runtime-state.d.ts +11 -10
- package/lib/qq-runtime-state.js +4 -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 +19 -19
- package/plugin.js +5 -5
- package/src/adapter.ts +23 -0
- package/src/client.ts +4 -12
- package/src/console.ts +27 -20
- package/src/endpoint.ts +218 -160
- package/src/group-admin.ts +28 -26
- 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 +2 -0
- package/src/qq-console-spec.ts +12 -1
- package/src/qq-console.ts +14 -8
- package/src/qq-endpoint-write.ts +10 -38
- package/src/qq-runtime-state.ts +11 -16
- package/src/qq-send.ts +28 -12
- package/src/webhook.ts +7 -57
- package/src/ws.ts +27 -105
|
@@ -10,7 +10,7 @@ export default defineAgentTool({
|
|
|
10
10
|
readOnly: true,
|
|
11
11
|
inputSchema: z.object({}),
|
|
12
12
|
async execute(_input, context) {
|
|
13
|
-
const info = await context.$client.
|
|
13
|
+
const info = await context.$client.groupService.getInfo(currentGroup(context));
|
|
14
14
|
return { success: true, group: info };
|
|
15
15
|
},
|
|
16
16
|
});
|
|
@@ -13,7 +13,7 @@ export default defineAgentTool({
|
|
|
13
13
|
user_id: z.string().min(1).describe('目标成员的 member_openid(来自消息发送者或成员列表,不要猜测)'),
|
|
14
14
|
}),
|
|
15
15
|
async execute({ user_id }, context) {
|
|
16
|
-
const member = await context.$client.
|
|
16
|
+
const member = await context.$client.groupService.getMemberInfo(currentGroup(context), user_id);
|
|
17
17
|
return { success: true, member };
|
|
18
18
|
},
|
|
19
19
|
});
|
|
@@ -13,7 +13,7 @@ export default defineAgentTool({
|
|
|
13
13
|
user_id: z.string().min(1).describe('目标成员的 member_openid(来自消息发送者或成员列表,不要猜测)'),
|
|
14
14
|
}),
|
|
15
15
|
async execute({ user_id }, context) {
|
|
16
|
-
const member = await context.$client.
|
|
16
|
+
const member = await context.$client.groupService.getMemberInfo(currentGroup(context), user_id);
|
|
17
17
|
return { success: true, member };
|
|
18
18
|
},
|
|
19
19
|
});
|
|
@@ -13,7 +13,7 @@ export default defineAgentTool({
|
|
|
13
13
|
cursor: z.string().optional().describe('上一页返回的 next_cursor;首次不传'),
|
|
14
14
|
}),
|
|
15
15
|
async execute({ cursor }, context) {
|
|
16
|
-
const page = await context.$client.
|
|
16
|
+
const page = await context.$client.groupService.getMembersPage(currentGroup(context), cursor ? { cursor } : {});
|
|
17
17
|
return { success: true, members: page.members ?? [], count: page.members?.length ?? 0, next_cursor: page.next_cursor || undefined };
|
|
18
18
|
},
|
|
19
19
|
});
|
|
@@ -13,7 +13,7 @@ export default defineAgentTool({
|
|
|
13
13
|
cursor: z.string().optional().describe('上一页返回的 next_cursor;首次不传'),
|
|
14
14
|
}),
|
|
15
15
|
async execute({ cursor }, context) {
|
|
16
|
-
const page = await context.$client.
|
|
16
|
+
const page = await context.$client.groupService.getMembersPage(currentGroup(context), cursor ? { cursor } : {});
|
|
17
17
|
return { success: true, members: page.members ?? [], count: page.members?.length ?? 0, next_cursor: page.next_cursor || undefined };
|
|
18
18
|
},
|
|
19
19
|
});
|
|
@@ -13,7 +13,7 @@ export default defineAgentTool({
|
|
|
13
13
|
cursor: z.string().optional().describe('上一页返回的 next_cursor;首次不传'),
|
|
14
14
|
}),
|
|
15
15
|
async execute({ cursor }, context) {
|
|
16
|
-
const page = await context.$client.
|
|
16
|
+
const page = await context.$client.groupService.getMemberBlacklist(currentGroup(context), cursor ? { cursor } : {});
|
|
17
17
|
return { users: page.users ?? [], count: page.users?.length ?? 0, next_cursor: page.next_cursor || undefined };
|
|
18
18
|
},
|
|
19
19
|
});
|
|
@@ -13,7 +13,7 @@ export default defineAgentTool({
|
|
|
13
13
|
cursor: z.string().optional().describe('上一页返回的 next_cursor;首次不传'),
|
|
14
14
|
}),
|
|
15
15
|
async execute({ cursor }, context) {
|
|
16
|
-
const page = await context.$client.
|
|
16
|
+
const page = await context.$client.groupService.getMemberBlacklist(currentGroup(context), cursor ? { cursor } : {});
|
|
17
17
|
return { users: page.users ?? [], count: page.users?.length ?? 0, next_cursor: page.next_cursor || undefined };
|
|
18
18
|
},
|
|
19
19
|
});
|
|
@@ -13,7 +13,7 @@ export default defineAgentTool({
|
|
|
13
13
|
cursor: z.string().optional().describe('上一页返回的 next_cursor;首次不传'),
|
|
14
14
|
}),
|
|
15
15
|
async execute({ cursor }, context) {
|
|
16
|
-
const page = await context.$client.
|
|
16
|
+
const page = await context.$client.groupService.getJoinRequests(currentGroup(context), cursor ? { cursor } : {});
|
|
17
17
|
return { requests: page.list ?? [], count: page.list?.length ?? 0, next_cursor: page.next_cursor || undefined };
|
|
18
18
|
},
|
|
19
19
|
});
|
|
@@ -13,7 +13,7 @@ export default defineAgentTool({
|
|
|
13
13
|
cursor: z.string().optional().describe('上一页返回的 next_cursor;首次不传'),
|
|
14
14
|
}),
|
|
15
15
|
async execute({ cursor }, context) {
|
|
16
|
-
const page = await context.$client.
|
|
16
|
+
const page = await context.$client.groupService.getJoinRequests(currentGroup(context), cursor ? { cursor } : {});
|
|
17
17
|
return { requests: page.list ?? [], count: page.list?.length ?? 0, next_cursor: page.next_cursor || undefined };
|
|
18
18
|
},
|
|
19
19
|
});
|
|
@@ -10,7 +10,7 @@ export default defineAgentTool({
|
|
|
10
10
|
readOnly: true,
|
|
11
11
|
inputSchema: z.object({}),
|
|
12
12
|
async execute(_input, context) {
|
|
13
|
-
const setting = await context.$client.
|
|
13
|
+
const setting = await context.$client.groupService.getMuteSetting(currentGroup(context));
|
|
14
14
|
const now = Date.now();
|
|
15
15
|
const muted = (setting.members ?? []).filter((m) => {
|
|
16
16
|
const until = Date.parse(m.mute_expire_at);
|
|
@@ -10,7 +10,7 @@ export default defineAgentTool({
|
|
|
10
10
|
readOnly: true,
|
|
11
11
|
inputSchema: z.object({}),
|
|
12
12
|
async execute(_input, context) {
|
|
13
|
-
const setting = await context.$client.
|
|
13
|
+
const setting = await context.$client.groupService.getMuteSetting(currentGroup(context));
|
|
14
14
|
const now = Date.now();
|
|
15
15
|
const muted = (setting.members ?? []).filter((m) => {
|
|
16
16
|
const until = Date.parse(m.mute_expire_at);
|
|
@@ -3,12 +3,11 @@ 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 { createEndpointCommands, endpointCommandForbidden,
|
|
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";
|
|
@@ -52,47 +51,38 @@ function busyFooter(state, locale) {
|
|
|
52
51
|
}
|
|
53
52
|
return undefined;
|
|
54
53
|
}
|
|
55
|
-
/** `qq endpoint list`:运行中的 endpoints(本 generation adapter create 注册)+ 配置里的 endpoints */
|
|
56
|
-
export async function runQqEndpointList(state, store, locale) {
|
|
57
|
-
return formatEndpointList(qqEndpointListSpec, {
|
|
58
|
-
running: state.endpoints.values(),
|
|
59
|
-
configured: await store.list('qq'),
|
|
60
|
-
footer: busyFooter(state, locale), locale,
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
54
|
const qqEndpointListSpec = {
|
|
64
55
|
adapterKey: 'qq',
|
|
65
56
|
adapterDisplayName: 'QQ',
|
|
66
57
|
describeEntry: (entry) => `appid: ${String(entry.appid)}`,
|
|
67
58
|
};
|
|
68
|
-
async function writeEndpointWithBotKind(pending, botKind,
|
|
59
|
+
async function writeEndpointWithBotKind(pending, botKind, save) {
|
|
69
60
|
const result = await writeQqEndpointWithBotKind({
|
|
70
61
|
endpointId: pending.endpointId,
|
|
71
62
|
appId: pending.appId,
|
|
72
63
|
appSecret: pending.appSecret,
|
|
73
64
|
botKind,
|
|
74
|
-
},
|
|
65
|
+
}, save);
|
|
75
66
|
return t(pending.locale, 'endpointSaved', {
|
|
76
67
|
kind: result.botKind,
|
|
77
68
|
id: result.endpointId,
|
|
78
|
-
file: result.filePath,
|
|
79
69
|
intents: result.intents.join(', '),
|
|
80
70
|
});
|
|
81
71
|
}
|
|
82
72
|
/**
|
|
83
|
-
* 完成 pending
|
|
73
|
+
* 完成 pending 的公域/私域选择:通过已捕获的所属 Adapter 保存一次账号配置。
|
|
84
74
|
* @returns 成功文案;调用方负责清 pending。
|
|
85
75
|
*/
|
|
86
|
-
export async function completeQqPendingBotKind(pending, botKind
|
|
87
|
-
return await writeEndpointWithBotKind(pending, botKind,
|
|
76
|
+
export async function completeQqPendingBotKind(pending, botKind) {
|
|
77
|
+
return await writeEndpointWithBotKind(pending, botKind, pending.save);
|
|
88
78
|
}
|
|
89
79
|
/**
|
|
90
80
|
* `qq endpoint add [id]`:启动扫码绑定流程。
|
|
91
81
|
* 返回的 Promise 在二维码链接就绪(或前置失败)时 resolve 为首条回复;
|
|
92
82
|
* 后续状态(已扫码 / 成功 / 失败 / 过期刷新)通过 reply 推回当前会话。
|
|
93
|
-
*
|
|
83
|
+
* 扫码成功后凭据暂存内存并询问公域/私域;确认后保存到账号库,等待显式启动。
|
|
94
84
|
*/
|
|
95
|
-
export function runQqEndpointAdd(state, id, reply,
|
|
85
|
+
export function runQqEndpointAdd(state, id, reply, save, input, locale = 'zh-CN', cancelCommand, commandHeads) {
|
|
96
86
|
if (state.bindFlow || state.pendingBotKind || state.consoleBind?.pending || state.consoleBind?.state === 'waiting') {
|
|
97
87
|
return Promise.resolve(t(locale, 'existingFlow'));
|
|
98
88
|
}
|
|
@@ -136,8 +126,8 @@ export function runQqEndpointAdd(state, id, reply, store, input, locale = 'zh-CN
|
|
|
136
126
|
endpointId: finalName,
|
|
137
127
|
appId,
|
|
138
128
|
appSecret,
|
|
139
|
-
sessionKey: '', locale, cancelCommand,
|
|
140
|
-
}, 'public',
|
|
129
|
+
sessionKey: '', locale, cancelCommand, save,
|
|
130
|
+
}, 'public', save) +
|
|
141
131
|
t(locale, 'noInteractiveKind'));
|
|
142
132
|
return;
|
|
143
133
|
}
|
|
@@ -145,7 +135,7 @@ export function runQqEndpointAdd(state, id, reply, store, input, locale = 'zh-CN
|
|
|
145
135
|
endpointId: finalName,
|
|
146
136
|
appId,
|
|
147
137
|
appSecret,
|
|
148
|
-
sessionKey, locale, cancelCommand, commandHeads,
|
|
138
|
+
sessionKey, locale, cancelCommand, commandHeads, save,
|
|
149
139
|
};
|
|
150
140
|
settle(t(locale, 'scanned', { id: finalName, prompt: qqBotKindPrompt(locale, cancelCommand) }));
|
|
151
141
|
}
|
|
@@ -181,23 +171,38 @@ export function runQqEndpointCancel(state, preference) {
|
|
|
181
171
|
}
|
|
182
172
|
return t(locale, 'noFlow');
|
|
183
173
|
}
|
|
184
|
-
/** `qq endpoint remove <id>`:从 zhin.config.yml 移除对应 endpoints 项 */
|
|
185
|
-
export async function runQqEndpointRemove(_state, id, store) {
|
|
186
|
-
return await removeEndpointById(qqEndpointListSpec, id, store);
|
|
187
|
-
}
|
|
188
174
|
/**
|
|
189
175
|
* 通用套件生成的 QQ endpoint 命令(add 经 bindFlow 钩子走扫码绑定)。
|
|
190
176
|
* commands/qq/endpoint/ 下的 list / add / remove 直接默认导出这三项;cancel 为 QQ 特化,单独定义。
|
|
191
177
|
*/
|
|
192
|
-
|
|
178
|
+
const standard = createEndpointCommands({
|
|
193
179
|
...qqEndpointListSpec,
|
|
194
|
-
fields: [
|
|
195
|
-
{ key: 'appid', required: true, env: true, description: 'QQ 机器人 appid', descriptionI18n: { en: "QQ bot app ID" } },
|
|
196
|
-
{ key: 'secret', required: true, env: true, description: 'QQ 机器人 secret', descriptionI18n: { en: "QQ bot secret" } },
|
|
197
|
-
],
|
|
198
|
-
running: (use) => use(qqRuntimeStateToken).endpoints.values(),
|
|
180
|
+
fields: [{ key: 'appid', required: true }, { key: 'secret', required: true, secret: true }],
|
|
199
181
|
listFooter: (use, locale) => busyFooter(use(qqRuntimeStateToken), locale),
|
|
200
|
-
addDescriptionI18n: { en: 'Bind a bot through mobile QQ, choose public/private, then save configuration; restart to apply' },
|
|
201
|
-
addDescription: '手机 QQ 扫码绑定机器人:确认公域/私域后一次性写入 .env 与 zhin.config.yml(重启生效)',
|
|
202
|
-
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] : []; })),
|
|
203
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,27 +2,16 @@ 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
|
-
endpointCommandForbidden,
|
|
13
|
-
endpointConfigurationStoreToken,
|
|
14
|
-
extractEndpointCommandReply,
|
|
15
|
-
formatEndpointList,
|
|
16
|
-
isEndpointOperator,
|
|
17
|
-
removeEndpointById,
|
|
18
|
-
type ConfiguredEndpointEntry,
|
|
19
|
-
type EndpointCommandReply,
|
|
20
|
-
type EndpointConfigurationStore,
|
|
21
|
-
} from 'zhin.js/adapter';
|
|
9
|
+
import { createEndpointCommands, endpointCommandForbidden, createDurableEndpointCommandReply, isEndpointOperator, extractEndpointCommandReply,
|
|
10
|
+
adapterFeatureId, isAdapterIndex, type ConfiguredEndpointEntry, type EndpointCommandReply } from 'zhin.js/adapter';
|
|
22
11
|
import { defineCommand } from 'zhin.js/command';
|
|
23
12
|
import { qqBotKindPrompt, qqCommandSessionKey } from '../../../src/qq-bot-kind-prompt.js';
|
|
24
13
|
import { startQqBindFlow } from '../../../src/qq-bind-flow.js';
|
|
25
|
-
import { writeQqEndpointWithBotKind } from '../../../src/qq-endpoint-write.js';
|
|
14
|
+
import { writeQqEndpointWithBotKind, type QqEndpointSavePort } from '../../../src/qq-endpoint-write.js';
|
|
26
15
|
import type { QqBotKind } from '../../../src/qq-intents.js';
|
|
27
16
|
import { cancelQqConsoleBind } from '../../../src/qq-console.js';
|
|
28
17
|
import {
|
|
@@ -76,19 +65,6 @@ function busyFooter(state: QqRuntimeState, locale?: Locale): string | undefined
|
|
|
76
65
|
return undefined;
|
|
77
66
|
}
|
|
78
67
|
|
|
79
|
-
/** `qq endpoint list`:运行中的 endpoints(本 generation adapter create 注册)+ 配置里的 endpoints */
|
|
80
|
-
export async function runQqEndpointList(
|
|
81
|
-
state: QqRuntimeState,
|
|
82
|
-
store: EndpointConfigurationStore,
|
|
83
|
-
locale?: Locale,
|
|
84
|
-
): Promise<string> {
|
|
85
|
-
return formatEndpointList(qqEndpointListSpec, {
|
|
86
|
-
running: state.endpoints.values(),
|
|
87
|
-
configured: await store.list('qq'),
|
|
88
|
-
footer: busyFooter(state, locale), locale,
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
|
|
92
68
|
const qqEndpointListSpec = {
|
|
93
69
|
adapterKey: 'qq',
|
|
94
70
|
adapterDisplayName: 'QQ',
|
|
@@ -98,45 +74,43 @@ const qqEndpointListSpec = {
|
|
|
98
74
|
async function writeEndpointWithBotKind(
|
|
99
75
|
pending: QqPendingBotKind,
|
|
100
76
|
botKind: QqBotKind,
|
|
101
|
-
|
|
77
|
+
save: QqEndpointSavePort,
|
|
102
78
|
): Promise<string> {
|
|
103
79
|
const result = await writeQqEndpointWithBotKind({
|
|
104
80
|
endpointId: pending.endpointId,
|
|
105
81
|
appId: pending.appId,
|
|
106
82
|
appSecret: pending.appSecret,
|
|
107
83
|
botKind,
|
|
108
|
-
},
|
|
84
|
+
}, save);
|
|
109
85
|
return t(pending.locale, 'endpointSaved', {
|
|
110
86
|
kind: result.botKind,
|
|
111
87
|
id: result.endpointId,
|
|
112
|
-
file: result.filePath,
|
|
113
88
|
intents: result.intents.join(', '),
|
|
114
89
|
});
|
|
115
90
|
}
|
|
116
91
|
|
|
117
92
|
/**
|
|
118
|
-
* 完成 pending
|
|
93
|
+
* 完成 pending 的公域/私域选择:通过已捕获的所属 Adapter 保存一次账号配置。
|
|
119
94
|
* @returns 成功文案;调用方负责清 pending。
|
|
120
95
|
*/
|
|
121
96
|
export async function completeQqPendingBotKind(
|
|
122
97
|
pending: QqPendingBotKind,
|
|
123
98
|
botKind: QqBotKind,
|
|
124
|
-
store: EndpointConfigurationStore,
|
|
125
99
|
): Promise<string> {
|
|
126
|
-
return await writeEndpointWithBotKind(pending, botKind,
|
|
100
|
+
return await writeEndpointWithBotKind(pending, botKind, pending.save);
|
|
127
101
|
}
|
|
128
102
|
|
|
129
103
|
/**
|
|
130
104
|
* `qq endpoint add [id]`:启动扫码绑定流程。
|
|
131
105
|
* 返回的 Promise 在二维码链接就绪(或前置失败)时 resolve 为首条回复;
|
|
132
106
|
* 后续状态(已扫码 / 成功 / 失败 / 过期刷新)通过 reply 推回当前会话。
|
|
133
|
-
*
|
|
107
|
+
* 扫码成功后凭据暂存内存并询问公域/私域;确认后保存到账号库,等待显式启动。
|
|
134
108
|
*/
|
|
135
109
|
export function runQqEndpointAdd(
|
|
136
110
|
state: QqRuntimeState,
|
|
137
111
|
id: string | undefined,
|
|
138
112
|
reply: QqCommandReply,
|
|
139
|
-
|
|
113
|
+
save: QqEndpointSavePort,
|
|
140
114
|
input?: unknown,
|
|
141
115
|
locale: Locale = 'zh-CN',
|
|
142
116
|
cancelCommand?: string,
|
|
@@ -192,10 +166,10 @@ export function runQqEndpointAdd(
|
|
|
192
166
|
endpointId: finalName,
|
|
193
167
|
appId,
|
|
194
168
|
appSecret,
|
|
195
|
-
sessionKey: '', locale, cancelCommand,
|
|
169
|
+
sessionKey: '', locale, cancelCommand, save,
|
|
196
170
|
},
|
|
197
171
|
'public',
|
|
198
|
-
|
|
172
|
+
save,
|
|
199
173
|
) +
|
|
200
174
|
t(locale, 'noInteractiveKind'),
|
|
201
175
|
);
|
|
@@ -206,7 +180,7 @@ export function runQqEndpointAdd(
|
|
|
206
180
|
endpointId: finalName,
|
|
207
181
|
appId,
|
|
208
182
|
appSecret,
|
|
209
|
-
sessionKey, locale, cancelCommand, commandHeads,
|
|
183
|
+
sessionKey, locale, cancelCommand, commandHeads, save,
|
|
210
184
|
};
|
|
211
185
|
settle(
|
|
212
186
|
t(locale, 'scanned', { id: finalName, prompt: qqBotKindPrompt(locale, cancelCommand) }),
|
|
@@ -247,37 +221,37 @@ export function runQqEndpointCancel(state: QqRuntimeState, preference?: Locale):
|
|
|
247
221
|
return t(locale, 'noFlow');
|
|
248
222
|
}
|
|
249
223
|
|
|
250
|
-
/** `qq endpoint remove <id>`:从 zhin.config.yml 移除对应 endpoints 项 */
|
|
251
|
-
export async function runQqEndpointRemove(
|
|
252
|
-
_state: QqRuntimeState,
|
|
253
|
-
id: string,
|
|
254
|
-
store: EndpointConfigurationStore,
|
|
255
|
-
): Promise<string> {
|
|
256
|
-
return await removeEndpointById(qqEndpointListSpec, id, store);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
224
|
/**
|
|
260
225
|
* 通用套件生成的 QQ endpoint 命令(add 经 bindFlow 钩子走扫码绑定)。
|
|
261
226
|
* commands/qq/endpoint/ 下的 list / add / remove 直接默认导出这三项;cancel 为 QQ 特化,单独定义。
|
|
262
227
|
*/
|
|
263
|
-
|
|
228
|
+
const standard = createEndpointCommands({
|
|
264
229
|
...qqEndpointListSpec,
|
|
265
|
-
fields: [
|
|
266
|
-
{ key: 'appid', required: true, env: true, description: 'QQ 机器人 appid', descriptionI18n: { en: "QQ bot app ID" } },
|
|
267
|
-
{ key: 'secret', required: true, env: true, description: 'QQ 机器人 secret', descriptionI18n: { en: "QQ bot secret" } },
|
|
268
|
-
],
|
|
269
|
-
running: (use) => use(qqRuntimeStateToken).endpoints.values(),
|
|
230
|
+
fields: [{ key: 'appid', required: true }, { key: 'secret', required: true, secret: true }],
|
|
270
231
|
listFooter: (use, locale) => busyFooter(use(qqRuntimeStateToken), locale),
|
|
271
|
-
addDescriptionI18n: { en: 'Bind a bot through mobile QQ, choose public/private, then save configuration; restart to apply' },
|
|
272
|
-
addDescription:
|
|
273
|
-
'手机 QQ 扫码绑定机器人:确认公域/私域后一次性写入 .env 与 zhin.config.yml(重启生效)',
|
|
274
|
-
bindFlow: ({ id, reply, input, use, locale, formatCommand }) =>
|
|
275
|
-
runQqEndpointAdd(
|
|
276
|
-
use(qqRuntimeStateToken),
|
|
277
|
-
id,
|
|
278
|
-
reply,
|
|
279
|
-
use(endpointConfigurationStoreToken),
|
|
280
|
-
input, locale, formatCommand?.('qq endpoint cancel'),
|
|
281
|
-
['qq endpoint list', 'qq endpoint add', 'qq endpoint remove', 'qq endpoint cancel'].flatMap(path => { const head = formatCommand?.(path); return head ? [head] : []; }),
|
|
282
|
-
),
|
|
283
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
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* QQ Console RPC:除通用 endpoint 管理外,把 `qq endpoint add` 的扫码绑定
|
|
3
|
-
* (以及公域 / 私域选择、取消)搬到 Console 页面。
|
|
4
|
-
*/
|
|
5
|
-
import { type AdapterConsoleResources } from 'zhin.js/adapter';
|
|
6
1
|
import { type PluginConsoleRpc } from 'zhin.js';
|
|
7
2
|
import type { QqRuntimeState } from './qq-runtime-state.js';
|
|
8
|
-
export declare function createQqConsoleRpc(state: QqRuntimeState,
|
|
3
|
+
export declare function createQqConsoleRpc(state: QqRuntimeState, getConfig: () => unknown): PluginConsoleRpc;
|
package/lib/console.js
CHANGED
|
@@ -2,17 +2,13 @@
|
|
|
2
2
|
* QQ Console RPC:除通用 endpoint 管理外,把 `qq endpoint add` 的扫码绑定
|
|
3
3
|
* (以及公域 / 私域选择、取消)搬到 Console 页面。
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { prepareConsoleAdapterMutation, ownedConsoleAdapter, createAdapterConsoleRpc, } from 'zhin.js/adapter';
|
|
6
6
|
import { PluginConsoleRpcError } from 'zhin.js';
|
|
7
7
|
import { qqConsoleSpec } from './qq-console-spec.js';
|
|
8
8
|
import { cancelQqConsoleBind, completeQqConsoleBind, startQqConsoleBind, } from './qq-console.js';
|
|
9
|
-
export function createQqConsoleRpc(state,
|
|
10
|
-
const store = resources.has(endpointConfigurationStoreToken)
|
|
11
|
-
? resources.use(endpointConfigurationStoreToken)
|
|
12
|
-
: undefined;
|
|
9
|
+
export function createQqConsoleRpc(state, getConfig) {
|
|
13
10
|
const spec = {
|
|
14
11
|
...qqConsoleSpec,
|
|
15
|
-
running: () => state.endpoints.values(),
|
|
16
12
|
getConfig,
|
|
17
13
|
extraStatus: () => {
|
|
18
14
|
const bind = state.consoleBind;
|
|
@@ -28,8 +24,6 @@ export function createQqConsoleRpc(state, resources, getConfig) {
|
|
|
28
24
|
};
|
|
29
25
|
},
|
|
30
26
|
availableActions: () => {
|
|
31
|
-
if (!store)
|
|
32
|
-
return [];
|
|
33
27
|
if (state.consoleBind?.pending)
|
|
34
28
|
return ['qq-bind-kind', 'qq-bind-cancel'];
|
|
35
29
|
if (state.bindFlow || state.consoleBind?.state === 'waiting')
|
|
@@ -40,16 +34,25 @@ export function createQqConsoleRpc(state, resources, getConfig) {
|
|
|
40
34
|
'qq-bind-start': {
|
|
41
35
|
label: '扫码绑定新机器人',
|
|
42
36
|
labelI18n: { en: 'Bind a bot with QR' },
|
|
43
|
-
description: '用手机 QQ
|
|
44
|
-
descriptionI18n: { en: 'Scan with mobile QQ, choose public/private
|
|
45
|
-
handler: async () => {
|
|
46
|
-
if (!
|
|
47
|
-
throw new PluginConsoleRpcError('unsupported', '
|
|
48
|
-
const
|
|
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;
|
|
49
51
|
return { message: '请用手机 QQ 扫码;扫码成功后选择公域或私域', url };
|
|
50
52
|
},
|
|
51
53
|
},
|
|
52
54
|
'qq-bind-kind': {
|
|
55
|
+
accountMutation: true,
|
|
53
56
|
label: '选择公域 / 私域',
|
|
54
57
|
labelI18n: { en: 'Choose public/private' },
|
|
55
58
|
description: '公域订阅 PUBLIC_GUILD_MESSAGES;私域订阅 GUILD_MESSAGES。',
|
|
@@ -58,12 +61,18 @@ export function createQqConsoleRpc(state, resources, getConfig) {
|
|
|
58
61
|
{ value: 'public', label: '公域', labelI18n: { en: 'Public' } },
|
|
59
62
|
{ value: 'private', label: '私域', labelI18n: { en: 'Private' } },
|
|
60
63
|
],
|
|
61
|
-
handler: async (input) => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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);
|
|
65
73
|
return {
|
|
66
|
-
message:
|
|
74
|
+
message: `已保存账号「${result.endpointId}」(${result.botKind}),尚未启动`,
|
|
75
|
+
saved: true, restartRequired: false, execution: { ...result.mutation.execution },
|
|
67
76
|
details: { endpoint: result.endpointId, botKind: result.botKind, intents: result.intents.join(', ') },
|
|
68
77
|
};
|
|
69
78
|
},
|
|
@@ -80,5 +89,5 @@ export function createQqConsoleRpc(state, resources, getConfig) {
|
|
|
80
89
|
},
|
|
81
90
|
},
|
|
82
91
|
};
|
|
83
|
-
return createAdapterConsoleRpc(spec
|
|
92
|
+
return createAdapterConsoleRpc(spec);
|
|
84
93
|
}
|