@zhin.js/adapter-qq 5.0.2 → 6.0.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.
Files changed (130) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +75 -409
  3. package/adapters/qq.ts +51 -0
  4. package/agent/tools/add_role.ts +2 -2
  5. package/agent/tools/channel_info.ts +2 -2
  6. package/agent/tools/create_role.ts +2 -2
  7. package/agent/tools/list_channels.ts +2 -2
  8. package/agent/tools/list_guilds.ts +2 -2
  9. package/agent/tools/list_roles.ts +2 -2
  10. package/agent/tools/member_detail.ts +2 -2
  11. package/agent/tools/remove_role.ts +2 -2
  12. package/commands/endpoint/add/[name:string=].ts +21 -0
  13. package/commands/endpoint/cancel.ts +15 -0
  14. package/commands/endpoint/list.ts +10 -0
  15. package/commands/endpoint/remove/[name:string].ts +15 -0
  16. package/lib/endpoint.d.ts +75 -0
  17. package/lib/endpoint.js +358 -0
  18. package/lib/index.d.ts +6 -0
  19. package/lib/index.js +6 -0
  20. package/lib/platform-permit.d.ts +16 -0
  21. package/lib/{src/platform-permit.js → platform-permit.js} +1 -2
  22. package/lib/protocol.d.ts +96 -0
  23. package/lib/protocol.js +129 -0
  24. package/lib/qq-agent-deps.d.ts +21 -0
  25. package/lib/qq-agent-deps.js +30 -0
  26. package/lib/qq-bind-api.d.ts +36 -0
  27. package/lib/{src/qq-bind-api.js → qq-bind-api.js} +0 -1
  28. package/lib/qq-bind-flow.d.ts +19 -0
  29. package/lib/{src/qq-bind-flow.js → qq-bind-flow.js} +0 -1
  30. package/lib/qq-bind-persist.d.ts +12 -0
  31. package/lib/{src/qq-bind-persist.js → qq-bind-persist.js} +4 -5
  32. package/lib/qq-endpoint-commands.d.ts +25 -0
  33. package/lib/qq-endpoint-commands.js +147 -0
  34. package/lib/qq-endpoint-config.d.ts +17 -0
  35. package/lib/qq-endpoint-config.js +110 -0
  36. package/lib/qq-runtime-state.d.ts +17 -0
  37. package/lib/qq-runtime-state.js +13 -0
  38. package/lib/webhook.d.ts +18 -0
  39. package/lib/webhook.js +100 -0
  40. package/lib/ws.d.ts +24 -0
  41. package/lib/ws.js +118 -0
  42. package/package.json +51 -40
  43. package/plugin.ts +16 -0
  44. package/schema.json +85 -0
  45. package/src/endpoint.ts +362 -596
  46. package/src/index.ts +56 -186
  47. package/src/platform-permit.ts +1 -1
  48. package/src/protocol.ts +229 -0
  49. package/src/qq-agent-deps.ts +49 -12
  50. package/src/qq-bind-persist.ts +4 -4
  51. package/src/qq-endpoint-commands.ts +174 -0
  52. package/src/qq-endpoint-config.ts +135 -0
  53. package/src/qq-runtime-state.ts +36 -0
  54. package/src/webhook.ts +127 -0
  55. package/src/ws.ts +172 -0
  56. package/client/Dashboard.tsx +0 -230
  57. package/client/index.tsx +0 -11
  58. package/client/tsconfig.json +0 -7
  59. package/client/utils/api.ts +0 -30
  60. package/dist/index.js +0 -30
  61. package/lib/agent/tools/add_role.js +0 -23
  62. package/lib/agent/tools/add_role.js.map +0 -1
  63. package/lib/agent/tools/channel_info.js +0 -19
  64. package/lib/agent/tools/channel_info.js.map +0 -1
  65. package/lib/agent/tools/create_role.js +0 -22
  66. package/lib/agent/tools/create_role.js.map +0 -1
  67. package/lib/agent/tools/list_channels.js +0 -20
  68. package/lib/agent/tools/list_channels.js.map +0 -1
  69. package/lib/agent/tools/list_guilds.js +0 -17
  70. package/lib/agent/tools/list_guilds.js.map +0 -1
  71. package/lib/agent/tools/list_roles.js +0 -20
  72. package/lib/agent/tools/list_roles.js.map +0 -1
  73. package/lib/agent/tools/member_detail.js +0 -20
  74. package/lib/agent/tools/member_detail.js.map +0 -1
  75. package/lib/agent/tools/remove_role.js +0 -23
  76. package/lib/agent/tools/remove_role.js.map +0 -1
  77. package/lib/package.json +0 -105
  78. package/lib/src/adapter.js +0 -81
  79. package/lib/src/adapter.js.map +0 -1
  80. package/lib/src/endpoint.js +0 -600
  81. package/lib/src/endpoint.js.map +0 -1
  82. package/lib/src/gateway-config.js +0 -32
  83. package/lib/src/gateway-config.js.map +0 -1
  84. package/lib/src/group-at-normalize.js +0 -68
  85. package/lib/src/group-at-normalize.js.map +0 -1
  86. package/lib/src/inbound-action.js +0 -57
  87. package/lib/src/inbound-action.js.map +0 -1
  88. package/lib/src/inbound-normalize.js +0 -47
  89. package/lib/src/inbound-normalize.js.map +0 -1
  90. package/lib/src/index.js +0 -194
  91. package/lib/src/index.js.map +0 -1
  92. package/lib/src/mention-enrich.js +0 -52
  93. package/lib/src/mention-enrich.js.map +0 -1
  94. package/lib/src/outbound-keyboard.js +0 -100
  95. package/lib/src/outbound-keyboard.js.map +0 -1
  96. package/lib/src/outbound-markdown.js +0 -189
  97. package/lib/src/outbound-markdown.js.map +0 -1
  98. package/lib/src/outbound-media.js +0 -51
  99. package/lib/src/outbound-media.js.map +0 -1
  100. package/lib/src/outbound-mixed-media.js +0 -78
  101. package/lib/src/outbound-mixed-media.js.map +0 -1
  102. package/lib/src/platform-permit.js.map +0 -1
  103. package/lib/src/qq-agent-deps.js +0 -10
  104. package/lib/src/qq-agent-deps.js.map +0 -1
  105. package/lib/src/qq-bind-api.js.map +0 -1
  106. package/lib/src/qq-bind-flow.js.map +0 -1
  107. package/lib/src/qq-bind-persist.js.map +0 -1
  108. package/lib/src/qq-endpoint-manager.js +0 -159
  109. package/lib/src/qq-endpoint-manager.js.map +0 -1
  110. package/lib/src/sdk-version.js +0 -19
  111. package/lib/src/sdk-version.js.map +0 -1
  112. package/lib/src/segment-mapper.js +0 -2
  113. package/lib/src/segment-mapper.js.map +0 -1
  114. package/lib/src/types.js +0 -2
  115. package/lib/src/types.js.map +0 -1
  116. package/plugin.yml +0 -3
  117. package/src/adapter.ts +0 -97
  118. package/src/gateway-config.ts +0 -48
  119. package/src/group-at-normalize.ts +0 -75
  120. package/src/inbound-action.ts +0 -87
  121. package/src/inbound-normalize.ts +0 -57
  122. package/src/mention-enrich.ts +0 -71
  123. package/src/outbound-keyboard.ts +0 -117
  124. package/src/outbound-markdown.ts +0 -212
  125. package/src/outbound-media.ts +0 -58
  126. package/src/outbound-mixed-media.ts +0 -101
  127. package/src/qq-endpoint-manager.ts +0 -199
  128. package/src/sdk-version.ts +0 -21
  129. package/src/segment-mapper.ts +0 -1
  130. package/src/types.ts +0 -52
package/src/index.ts CHANGED
@@ -1,188 +1,58 @@
1
- /**
2
- * QQ 官方适配器入口:类型扩展、导出、注册
3
- */
4
- import path from "node:path";
5
- import { usePlugin, type Plugin, type ISceneManagement, createSceneManagementTools, type ToolFeature } from "zhin.js";
6
- import type { Router } from "@zhin.js/host-router";
7
- import { QQAdapter } from "./adapter.js";
8
- import { PageManager } from "@zhin.js/host-api";
9
- import {
1
+ export {
2
+ formatInboundContent,
3
+ formatInboundTarget,
4
+ formatOutboundText,
5
+ parseSendTarget,
6
+ resolveOutboundMessageId,
7
+ resolveQqConfig,
8
+ senderDisplayName,
9
+ type ParsedSendTarget,
10
+ type QqAdapterConfig,
11
+ type QqChannelKind,
12
+ type QqInboundMessage,
13
+ type QqWireSegment,
14
+ type ResolvedQqConfig,
15
+ type ResolvedQqHttpConfig,
16
+ type ResolvedQqWebsocketConfig,
17
+ } from './protocol.js';
18
+
19
+ export {
20
+ QqHttpEndpoint,
21
+ QqWebsocketEndpoint,
22
+ type CreateQqBot,
23
+ type CreateQqHttpBot,
24
+ type QqBotTransport,
25
+ type QqEndpointOptions,
26
+ type QqHttpBotTransport,
27
+ type QqHttpEndpointOptions,
28
+ } from './endpoint.js';
29
+
30
+ export {
31
+ registerQqWebhookRoutes,
32
+ handleQqWebhookRequest,
33
+ readRequestBodyText,
34
+ defaultCreateHttpBot,
35
+ type QqWebhookHandler,
36
+ } from './webhook.js';
37
+
38
+ export {
39
+ bindQqBotInboundEvents,
40
+ defaultCreateBot,
41
+ normalizeQqMessage,
42
+ } from './ws.js';
43
+
44
+ export {
45
+ getQqAgentDeps,
46
+ registerQqAgentEndpoint,
47
+ setQqAgentDeps,
48
+ type QqAgentDeps,
49
+ type QqAgentEndpoint,
50
+ } from './qq-agent-deps.js';
51
+
52
+ export {
53
+ checkQqPlatformPermit,
54
+ normalizeQqGuildSenderForPermit,
55
+ platformPermit,
10
56
  qqGuildPermitResolver,
11
57
  registerQqPlatformPermitChecker,
12
- } from "./platform-permit.js";
13
- import { disposeQqEndpointProvision } from "./qq-endpoint-manager.js";
14
- import { setQqAgentDeps } from "./qq-agent-deps.js";
15
-
16
- declare module "zhin.js" {
17
- namespace Plugin {
18
- interface Contexts {
19
- web: PageManager;
20
- router: Router;
21
- }
22
- }
23
- }
24
-
25
- declare module "zhin.js" {
26
- interface Adapters {
27
- qq: QQAdapter;
28
- }
29
- }
30
-
31
- export * from "./types.js";
32
- export { QQEndpoint } from "./endpoint.js";
33
- export { QQAdapter } from "./adapter.js";
34
- export { startQqBindFlow } from "./qq-bind-flow.js";
35
- export * from "./qq-bind-api.js";
36
- export { QqEndpointManager, disposeQqEndpointProvision } from "./qq-endpoint-manager.js";
37
-
38
- const plugin = usePlugin();
39
- const { provide, useContext, onDispose } = plugin;
40
-
41
- useContext("router", (router: Router) => {
42
- provide({
43
- name: "qq",
44
- description: "QQ Official Endpoint Adapter",
45
- mounted: async (p: Plugin) => {
46
- const adapter = new QQAdapter(p, router);
47
- await adapter.start();
48
- return adapter;
49
- },
50
- dispose: async (adapter: QQAdapter) => {
51
- await adapter.stop();
52
- },
53
- });
54
- });
55
-
56
- useContext('tool', 'qq', (toolService: ToolFeature, qq: QQAdapter) => {
57
- const disposers: (() => void)[] = [];
58
- disposers.push(registerQqPlatformPermitChecker());
59
- setQqAgentDeps({
60
- getEndpoint: (endpointId) => {
61
- const endpoint = qq.endpoints.get(endpointId);
62
- if (!endpoint) throw new Error(`Endpoint ${endpointId} 不存在`);
63
- return endpoint;
64
- },
65
- getAdapter: () => qq,
66
- });
67
- const sceneTools = createSceneManagementTools(
68
- qq as unknown as ISceneManagement,
69
- 'qq',
70
- { permitResolver: qqGuildPermitResolver, registerChecker: false },
71
- );
72
- disposers.push(...sceneTools.map(t => toolService.addTool(t, plugin.name)));
73
-
74
- return () => disposers.forEach(d => d());
75
- });
76
-
77
- // ── Web 控制台 ─────────────────────────────────────────────────────────
78
- useContext("web", (pageManager) => {
79
- pageManager.addEntry({
80
- id: "qq",
81
- development: path.resolve(import.meta.dirname, "../client/index.tsx"),
82
- production: path.resolve(import.meta.dirname, "../dist/index.js"),
83
- meta: { name: "QQ" },
84
- });
85
- });
86
-
87
- useContext("router", "qq", (router: any, qq: QQAdapter) => {
88
- router.get("/api/qq/endpoints", async (ctx: any) => {
89
- try {
90
- const endpoints = Array.from(qq.endpoints.values());
91
- const result = await Promise.all(endpoints.map(async (endpoint) => {
92
- try {
93
- let guildCount = 0;
94
- try { const guilds = await endpoint.getGuilds(); guildCount = guilds?.length || 0; } catch {}
95
- return {
96
- name: endpoint.$config.name,
97
- connected: endpoint.$connected || false,
98
- guildCount,
99
- status: endpoint.$connected ? "online" : "offline",
100
- };
101
- } catch {
102
- return { name: endpoint.$config.name, connected: false, guildCount: 0, status: "error" };
103
- }
104
- }));
105
- ctx.body = { success: true, data: result };
106
- } catch {
107
- ctx.status = 500;
108
- ctx.body = { success: false, error: "获取 Endpoint 数据失败" };
109
- }
110
- });
111
-
112
- // Endpoint 连接/断开
113
- router.post("/api/qq/endpoints/:name/connect", async (ctx: any) => {
114
- try {
115
- const endpoint = qq.endpoints.get(ctx.params.name);
116
- if (!endpoint) { ctx.status = 404; ctx.body = { success: false, error: "Endpoint 不存在" }; return; }
117
- if (endpoint.$connected) { ctx.body = { success: true, message: "已经在线" }; return; }
118
- await endpoint.$connect();
119
- ctx.body = { success: true, message: "连接成功" };
120
- } catch (e: unknown) {
121
- ctx.status = 500;
122
- ctx.body = { success: false, error: e instanceof Error ? e.message : "连接失败" };
123
- }
124
- });
125
-
126
- router.post("/api/qq/endpoints/:name/disconnect", async (ctx: any) => {
127
- try {
128
- const endpoint = qq.endpoints.get(ctx.params.name);
129
- if (!endpoint) { ctx.status = 404; ctx.body = { success: false, error: "Endpoint 不存在" }; return; }
130
- if (!endpoint.$connected) { ctx.body = { success: true, message: "已经离线" }; return; }
131
- await endpoint.$disconnect();
132
- ctx.body = { success: true, message: "已断开" };
133
- } catch (e: unknown) {
134
- ctx.status = 500;
135
- ctx.body = { success: false, error: e instanceof Error ? e.message : "断开失败" };
136
- }
137
- });
138
-
139
- // 频道列表(QQ 官方 API 返回 guild_id/guild_name,归一化为控制台用的 id/name)
140
- router.get("/api/qq/endpoints/:name/guilds", async (ctx: any) => {
141
- try {
142
- const endpoint = qq.endpoints.get(ctx.params.name);
143
- if (!endpoint) { ctx.status = 404; ctx.body = { success: false, error: "Endpoint 不存在" }; return; }
144
- if (!endpoint.$connected) { ctx.status = 400; ctx.body = { success: false, error: "Endpoint 未连接" }; return; }
145
- const raw = await endpoint.getGuilds();
146
- const guilds = (raw || []).map((g: any) => ({
147
- id: g.guild_id ?? g.id,
148
- name: g.guild_name ?? g.name,
149
- icon: g.icon,
150
- description: g.description,
151
- memberCount: g.member_count,
152
- ownerId: g.owner_id,
153
- owner: g.owner,
154
- joinTime: g.join_time,
155
- }));
156
- ctx.body = { success: true, data: guilds };
157
- } catch (e: unknown) {
158
- ctx.status = 500;
159
- ctx.body = { success: false, error: e instanceof Error ? e.message : "获取频道失败" };
160
- }
161
- });
162
-
163
- // 子频道列表(QQ 官方 API 返回 channel_id/channel_name,归一化为 id/name)
164
- router.get("/api/qq/endpoints/:name/guilds/:guildId/channels", async (ctx: any) => {
165
- try {
166
- const endpoint = qq.endpoints.get(ctx.params.name);
167
- if (!endpoint) { ctx.status = 404; ctx.body = { success: false, error: "Endpoint 不存在" }; return; }
168
- if (!endpoint.$connected) { ctx.status = 400; ctx.body = { success: false, error: "Endpoint 未连接" }; return; }
169
- const raw = await endpoint.getChannels(ctx.params.guildId);
170
- const channels = (raw || []).map((ch: any) => ({
171
- id: ch.channel_id ?? ch.id,
172
- name: ch.channel_name ?? ch.name,
173
- type: ch.type,
174
- subType: ch.sub_type,
175
- position: ch.position,
176
- parentId: ch.parent_id,
177
- }));
178
- ctx.body = { success: true, data: channels };
179
- } catch (e: unknown) {
180
- ctx.status = 500;
181
- ctx.body = { success: false, error: e instanceof Error ? e.message : "获取子频道失败" };
182
- }
183
- });
184
- });
185
-
186
- onDispose(() => {
187
- disposeQqEndpointProvision();
188
- });
58
+ } from './platform-permit.js';
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * QQ 官方频道 platform permit(guild/channel,非 QQ 群 group_*)
3
3
  */
4
- import { registerPlatformPermitChecker, type Message } from 'zhin.js';
4
+ import { registerPlatformPermitChecker, type Message } from '@zhin.js/core';
5
5
 
6
6
  const ADAPTER = 'qq';
7
7
 
@@ -0,0 +1,229 @@
1
+ /**
2
+ * QQ Official Bot protocol helpers — no legacy Adapter/Endpoint / segment-mapper.
3
+ * Canonicalization is owned by gateway/core before endpoint.send.
4
+ */
5
+
6
+ import { pickCredential } from '@zhin.js/adapter';
7
+
8
+ /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
9
+ export interface QqAdapterConfig {
10
+ readonly name?: string;
11
+ readonly appid?: string;
12
+ readonly secret?: string;
13
+ /** Default `websocket`. `webhook` / `middleware` use httpHostToken POST. */
14
+ readonly mode?: 'websocket' | 'webhook' | 'middleware';
15
+ readonly sandbox?: boolean;
16
+ readonly intents?: readonly string[];
17
+ readonly accessTokenUrl?: string;
18
+ readonly gatewayUrl?: string;
19
+ readonly webhookPath?: string;
20
+ readonly port?: number;
21
+ readonly path?: string;
22
+ /** Transitional: legacy root `endpoints[]` with `context: qq`. */
23
+ readonly endpoints?: ReadonlyArray<{
24
+ readonly context?: string;
25
+ readonly name?: string;
26
+ readonly appid?: string;
27
+ readonly secret?: string;
28
+ readonly mode?: 'websocket' | 'webhook' | 'middleware';
29
+ readonly sandbox?: boolean;
30
+ readonly intents?: readonly string[];
31
+ readonly accessTokenUrl?: string;
32
+ readonly gatewayUrl?: string;
33
+ readonly webhookPath?: string;
34
+ readonly port?: number;
35
+ readonly path?: string;
36
+ }>;
37
+ }
38
+
39
+ export interface ResolvedQqWebsocketConfig {
40
+ readonly context: 'qq';
41
+ readonly mode: 'websocket';
42
+ readonly name: string;
43
+ readonly appid: string;
44
+ readonly secret: string;
45
+ readonly sandbox: boolean;
46
+ readonly intents?: readonly string[];
47
+ readonly accessTokenUrl?: string;
48
+ readonly gatewayUrl?: string;
49
+ }
50
+
51
+ export interface ResolvedQqHttpConfig {
52
+ readonly context: 'qq';
53
+ readonly mode: 'webhook' | 'middleware';
54
+ readonly name: string;
55
+ readonly appid: string;
56
+ readonly secret: string;
57
+ readonly webhookPath: string;
58
+ readonly sandbox: boolean;
59
+ }
60
+
61
+ export type ResolvedQqConfig = ResolvedQqWebsocketConfig | ResolvedQqHttpConfig;
62
+
63
+ export type QqChannelKind = 'private' | 'group' | 'channel' | 'direct';
64
+
65
+ export interface QqInboundMessage {
66
+ readonly id: string;
67
+ readonly content: string;
68
+ readonly channelKind: QqChannelKind;
69
+ readonly channelId: string;
70
+ readonly authorId: string;
71
+ readonly authorName: string;
72
+ readonly authorRoles?: string[];
73
+ readonly timestamp: number;
74
+ readonly guildId?: string;
75
+ readonly rawMessage?: string;
76
+ /**
77
+ * 入站即 @ 机器人(AT_MESSAGE_CREATE 的 mentions[].bot,或群 GROUP_AT_MESSAGE_CREATE
78
+ * 事件本身仅 @ 时下发)。新 Runtime 纯文本 content 需经 metadata 传递。
79
+ */
80
+ readonly mentioned?: boolean;
81
+ }
82
+
83
+ export interface QqWireSegment {
84
+ readonly type: string;
85
+ readonly data?: Record<string, unknown>;
86
+ }
87
+
88
+ export interface ParsedSendTarget {
89
+ readonly kind: QqChannelKind;
90
+ readonly id: string;
91
+ }
92
+
93
+ export function resolveQqConfig(config: QqAdapterConfig = {}): ResolvedQqConfig {
94
+ const entry = config.endpoints?.find((item) => item.context === 'qq' || !item.context);
95
+ const appid = pickCredential(config.appid, entry?.appid, process.env.QQ_APPID, process.env.QQ_BOT_APPID);
96
+ const secret = pickCredential(config.secret, entry?.secret, process.env.QQ_SECRET, process.env.QQ_BOT_SECRET);
97
+ if (!appid || !secret) {
98
+ throw new TypeError(
99
+ 'QQ adapter requires appid + secret (plugins.<key>.appid/secret or endpoints with context: qq)',
100
+ );
101
+ }
102
+ const name = (typeof config.name === 'string' && config.name)
103
+ || (typeof entry?.name === 'string' && entry.name)
104
+ || process.env.QQ_BOT_NAME
105
+ || 'qq-bot';
106
+ const mode = config.mode ?? entry?.mode ?? 'websocket';
107
+
108
+ if (mode === 'webhook' || mode === 'middleware') {
109
+ const webhookPathRaw = config.webhookPath ?? entry?.webhookPath ?? '/qq/webhook';
110
+ const webhookPath = webhookPathRaw.startsWith('/') ? webhookPathRaw : `/${webhookPathRaw}`;
111
+ return {
112
+ context: 'qq',
113
+ mode,
114
+ name,
115
+ appid,
116
+ secret,
117
+ webhookPath,
118
+ sandbox: config.sandbox === true || entry?.sandbox === true,
119
+ };
120
+ }
121
+
122
+ return {
123
+ context: 'qq',
124
+ mode: 'websocket',
125
+ name,
126
+ appid,
127
+ secret,
128
+ sandbox: config.sandbox === true || entry?.sandbox === true,
129
+ intents: config.intents ?? entry?.intents,
130
+ accessTokenUrl: config.accessTokenUrl ?? entry?.accessTokenUrl,
131
+ gatewayUrl: config.gatewayUrl ?? entry?.gatewayUrl,
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Gateway reply target:`private:uid` / `group:gid` / `channel:cid` / `direct:guildId`.
137
+ */
138
+ export function formatInboundTarget(msg: QqInboundMessage): string {
139
+ return `${msg.channelKind}:${msg.channelId}`;
140
+ }
141
+
142
+ export function parseSendTarget(target: string): ParsedSendTarget {
143
+ const sep = target.indexOf(':');
144
+ if (sep <= 0) {
145
+ return { kind: 'private', id: target };
146
+ }
147
+ const head = target.slice(0, sep);
148
+ const rest = target.slice(sep + 1);
149
+ if (head === 'private' || head === 'group' || head === 'channel' || head === 'direct') {
150
+ return { kind: head, id: rest };
151
+ }
152
+ return { kind: 'private', id: target };
153
+ }
154
+
155
+ export function senderDisplayName(msg: QqInboundMessage): string {
156
+ return msg.authorName || msg.authorId;
157
+ }
158
+
159
+ export function formatInboundContent(msg: QqInboundMessage): string {
160
+ const text = (msg.content || msg.rawMessage || '').trim();
161
+ return text || '(Empty message)';
162
+ }
163
+
164
+ /**
165
+ * Wire-encode an already-rendered outbound payload into a plain text / QQ Sendable string.
166
+ * Segment canonicalization is intentionally not done here.
167
+ */
168
+ export function formatOutboundText(payload: unknown): string {
169
+ if (typeof payload === 'string') return payload;
170
+
171
+ const segments: Array<string | QqWireSegment> = Array.isArray(payload)
172
+ ? payload as Array<string | QqWireSegment>
173
+ : payload && typeof payload === 'object' && 'type' in (payload as object)
174
+ ? [payload as QqWireSegment]
175
+ : [];
176
+
177
+ if (segments.length === 0) {
178
+ return payload == null
179
+ ? ''
180
+ : typeof payload === 'object'
181
+ ? JSON.stringify(payload)
182
+ : String(payload);
183
+ }
184
+
185
+ return segments
186
+ .map((item) => {
187
+ if (typeof item === 'string') return item;
188
+ const data = item.data ?? {};
189
+ switch (item.type) {
190
+ case 'text':
191
+ return String(data.text ?? data.content ?? '');
192
+ case 'at': {
193
+ const id = String(data.user_id ?? data.qq ?? data.id ?? '');
194
+ return id ? `<@!${id}>` : '';
195
+ }
196
+ case 'image':
197
+ return String(data.url || data.file || '');
198
+ case 'reply':
199
+ return '';
200
+ default:
201
+ return data.text != null ? String(data.text) : '';
202
+ }
203
+ })
204
+ .filter(Boolean)
205
+ .join('');
206
+ }
207
+
208
+ /** 从 QQ API SendResult / 审核回包中解析出站消息 ID */
209
+ export function resolveOutboundMessageId(result: unknown): string {
210
+ if (!result || typeof result !== 'object') {
211
+ throw new Error('QQ 发送消息失败:响应为空');
212
+ }
213
+ const row = result as Record<string, unknown>;
214
+ const nested = row.data && typeof row.data === 'object'
215
+ ? row.data as Record<string, unknown>
216
+ : undefined;
217
+ const audit = (row.message_audit ?? nested?.message_audit) as Record<string, unknown> | undefined;
218
+ const id = row.id ?? row.message_id ?? audit?.audit_id;
219
+ if (id == null || id === '') {
220
+ const code = row.code;
221
+ const msg = row.message;
222
+ throw new Error(
223
+ code != null
224
+ ? `QQ 发送消息失败(${String(code)})${msg ? `: ${String(msg)}` : ''}`
225
+ : 'QQ 发送消息失败:响应缺少消息 ID',
226
+ );
227
+ }
228
+ return String(id);
229
+ }
@@ -1,23 +1,60 @@
1
1
  /**
2
- * Shared runtime deps for qq agent/ authoring tools.
3
- * Set once from plugin index during init — never call getPlugin() from tool execute.
2
+ * Agent tool deps for qq.
3
+ * Endpoints register themselves on start; tools look up by config name / endpoint id.
4
4
  */
5
- import type { QQAdapter } from './adapter.js';
6
- import type { QQEndpoint } from './endpoint.js';
7
- import type { ReceiverMode } from './types.js';
5
+
6
+ export interface QqAgentEndpoint {
7
+ getGuilds(): Promise<unknown[]>;
8
+ getChannels(guildId: string): Promise<unknown[]>;
9
+ getChannelInfo(channelId: string): Promise<unknown>;
10
+ getGuildMember(guildId: string, userId: string): Promise<unknown>;
11
+ getGuildRoles(guildId: string): Promise<unknown[]>;
12
+ createGuildRole(guildId: string, name: string, color?: number): Promise<unknown>;
13
+ addMemberRole(
14
+ guildId: string,
15
+ channelId: string,
16
+ userId: string,
17
+ roleId: string,
18
+ ): Promise<boolean>;
19
+ removeMemberRole(
20
+ guildId: string,
21
+ channelId: string,
22
+ userId: string,
23
+ roleId: string,
24
+ ): Promise<boolean>;
25
+ }
8
26
 
9
27
  export interface QqAgentDeps {
10
- getEndpoint: (endpointId: string) => QQEndpoint<ReceiverMode>;
11
- getAdapter: () => QQAdapter;
28
+ getEndpoint: (endpointId: string) => QqAgentEndpoint;
12
29
  }
13
30
 
14
- let _deps: QqAgentDeps | null = null;
31
+ const endpoints = new Map<string, QqAgentEndpoint>();
32
+ let override: QqAgentDeps | null = null;
33
+
34
+ export function registerQqAgentEndpoint(
35
+ endpointId: string,
36
+ endpoint: QqAgentEndpoint,
37
+ ): () => void {
38
+ endpoints.set(endpointId, endpoint);
39
+ return () => {
40
+ if (endpoints.get(endpointId) === endpoint) {
41
+ endpoints.delete(endpointId);
42
+ }
43
+ };
44
+ }
15
45
 
16
- export function setQqAgentDeps(deps: QqAgentDeps): void {
17
- _deps = deps;
46
+ /** Optional override used by tests / transitional callers. Pass `null` to clear. */
47
+ export function setQqAgentDeps(deps: QqAgentDeps | null): void {
48
+ override = deps;
18
49
  }
19
50
 
20
51
  export function getQqAgentDeps(): QqAgentDeps {
21
- if (!_deps) throw new Error('qq agent deps not initialized');
22
- return _deps;
52
+ if (override) return override;
53
+ return {
54
+ getEndpoint(endpointId: string): QqAgentEndpoint {
55
+ const registered = endpoints.get(endpointId);
56
+ if (!registered) throw new Error(`Endpoint ${endpointId} 不存在`);
57
+ return registered;
58
+ },
59
+ };
23
60
  }
@@ -1,6 +1,5 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { runtimeCwd } from 'zhin.js';
4
3
 
5
4
  export interface QqCredentialEnvKeys {
6
5
  appidKey: string;
@@ -9,7 +8,7 @@ export interface QqCredentialEnvKeys {
9
8
  secretRef: string;
10
9
  }
11
10
 
12
- /** 按 endpoint 名称生成唯一 env 键(如 `QQ_MOCK_QQ_BOT_APPID`) */
11
+ /** 按 endpoint 名称生成唯一 env 键(如 `QQ_MY_BOT_APPID`) */
13
12
  export function buildQqCredentialEnvKeys(endpointName: string): QqCredentialEnvKeys {
14
13
  const slug = endpointName.replace(/[^a-zA-Z0-9_]/g, '_').toUpperCase();
15
14
  const appidKey = `QQ_${slug}_APPID`;
@@ -22,9 +21,10 @@ export function buildQqCredentialEnvKeys(endpointName: string): QqCredentialEnvK
22
21
  };
23
22
  }
24
23
 
25
- function resolveProjectRoot(): string {
24
+ /** 项目根:ZHIN_PROJECT_ROOT 优先,缺省 process.cwd()(替代 legacy runtimeCwd) */
25
+ export function resolveProjectRoot(): string {
26
26
  const envRoot = process.env.ZHIN_PROJECT_ROOT?.trim();
27
- return path.resolve(envRoot || runtimeCwd());
27
+ return path.resolve(envRoot || process.cwd());
28
28
  }
29
29
 
30
30
  function escapeRegExp(text: string): string {