@sliverp/qqbot 1.3.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 (78) hide show
  1. package/README.md +231 -0
  2. package/clawdbot.plugin.json +16 -0
  3. package/dist/index.d.ts +17 -0
  4. package/dist/index.js +22 -0
  5. package/dist/src/api.d.ts +194 -0
  6. package/dist/src/api.js +555 -0
  7. package/dist/src/channel.d.ts +3 -0
  8. package/dist/src/channel.js +146 -0
  9. package/dist/src/config.d.ts +25 -0
  10. package/dist/src/config.js +148 -0
  11. package/dist/src/gateway.d.ts +17 -0
  12. package/dist/src/gateway.js +722 -0
  13. package/dist/src/image-server.d.ts +62 -0
  14. package/dist/src/image-server.js +401 -0
  15. package/dist/src/known-users.d.ts +100 -0
  16. package/dist/src/known-users.js +264 -0
  17. package/dist/src/onboarding.d.ts +10 -0
  18. package/dist/src/onboarding.js +190 -0
  19. package/dist/src/outbound.d.ts +149 -0
  20. package/dist/src/outbound.js +476 -0
  21. package/dist/src/proactive.d.ts +170 -0
  22. package/dist/src/proactive.js +398 -0
  23. package/dist/src/runtime.d.ts +3 -0
  24. package/dist/src/runtime.js +10 -0
  25. package/dist/src/session-store.d.ts +49 -0
  26. package/dist/src/session-store.js +242 -0
  27. package/dist/src/types.d.ts +116 -0
  28. package/dist/src/types.js +1 -0
  29. package/dist/src/utils/image-size.d.ts +51 -0
  30. package/dist/src/utils/image-size.js +234 -0
  31. package/dist/src/utils/payload.d.ts +112 -0
  32. package/dist/src/utils/payload.js +186 -0
  33. package/index.ts +27 -0
  34. package/moltbot.plugin.json +16 -0
  35. package/node_modules/ws/LICENSE +20 -0
  36. package/node_modules/ws/README.md +548 -0
  37. package/node_modules/ws/browser.js +8 -0
  38. package/node_modules/ws/index.js +13 -0
  39. package/node_modules/ws/lib/buffer-util.js +131 -0
  40. package/node_modules/ws/lib/constants.js +19 -0
  41. package/node_modules/ws/lib/event-target.js +292 -0
  42. package/node_modules/ws/lib/extension.js +203 -0
  43. package/node_modules/ws/lib/limiter.js +55 -0
  44. package/node_modules/ws/lib/permessage-deflate.js +528 -0
  45. package/node_modules/ws/lib/receiver.js +706 -0
  46. package/node_modules/ws/lib/sender.js +602 -0
  47. package/node_modules/ws/lib/stream.js +161 -0
  48. package/node_modules/ws/lib/subprotocol.js +62 -0
  49. package/node_modules/ws/lib/validation.js +152 -0
  50. package/node_modules/ws/lib/websocket-server.js +554 -0
  51. package/node_modules/ws/lib/websocket.js +1393 -0
  52. package/node_modules/ws/package.json +69 -0
  53. package/node_modules/ws/wrapper.mjs +8 -0
  54. package/openclaw.plugin.json +16 -0
  55. package/package.json +38 -0
  56. package/qqbot-1.3.0.tgz +0 -0
  57. package/scripts/proactive-api-server.ts +346 -0
  58. package/scripts/send-proactive.ts +273 -0
  59. package/scripts/upgrade.sh +106 -0
  60. package/skills/qqbot-cron/SKILL.md +490 -0
  61. package/skills/qqbot-media/SKILL.md +138 -0
  62. package/src/api.ts +752 -0
  63. package/src/channel.ts +303 -0
  64. package/src/config.ts +172 -0
  65. package/src/gateway.ts +1588 -0
  66. package/src/image-server.ts +474 -0
  67. package/src/known-users.ts +358 -0
  68. package/src/onboarding.ts +254 -0
  69. package/src/openclaw-plugin-sdk.d.ts +483 -0
  70. package/src/outbound.ts +571 -0
  71. package/src/proactive.ts +528 -0
  72. package/src/runtime.ts +14 -0
  73. package/src/session-store.ts +292 -0
  74. package/src/types.ts +123 -0
  75. package/src/utils/image-size.ts +266 -0
  76. package/src/utils/payload.ts +265 -0
  77. package/tsconfig.json +16 -0
  78. package/upgrade-and-run.sh +89 -0
package/src/channel.ts ADDED
@@ -0,0 +1,303 @@
1
+ import {
2
+ type ChannelPlugin,
3
+ type OpenClawConfig,
4
+ applyAccountNameToChannelSection,
5
+ deleteAccountFromConfigSection,
6
+ setAccountEnabledInConfigSection,
7
+ } from "openclaw/plugin-sdk";
8
+
9
+ import type { ResolvedQQBotAccount } from "./types.js";
10
+ import { DEFAULT_ACCOUNT_ID, listQQBotAccountIds, resolveQQBotAccount, applyQQBotAccountConfig, resolveDefaultQQBotAccountId } from "./config.js";
11
+ import { sendText, sendMedia } from "./outbound.js";
12
+ import { startGateway } from "./gateway.js";
13
+ import { qqbotOnboardingAdapter } from "./onboarding.js";
14
+ import { getQQBotRuntime } from "./runtime.js";
15
+
16
+ /**
17
+ * 简单的文本分块函数
18
+ * 用于预先分块长文本
19
+ */
20
+ function chunkText(text: string, limit: number): string[] {
21
+ if (text.length <= limit) return [text];
22
+
23
+ const chunks: string[] = [];
24
+ let remaining = text;
25
+
26
+ while (remaining.length > 0) {
27
+ if (remaining.length <= limit) {
28
+ chunks.push(remaining);
29
+ break;
30
+ }
31
+
32
+ // 尝试在换行处分割
33
+ let splitAt = remaining.lastIndexOf("\n", limit);
34
+ if (splitAt <= 0 || splitAt < limit * 0.5) {
35
+ // 没找到合适的换行,尝试在空格处分割
36
+ splitAt = remaining.lastIndexOf(" ", limit);
37
+ }
38
+ if (splitAt <= 0 || splitAt < limit * 0.5) {
39
+ // 还是没找到,强制在 limit 处分割
40
+ splitAt = limit;
41
+ }
42
+
43
+ chunks.push(remaining.slice(0, splitAt));
44
+ remaining = remaining.slice(splitAt).trimStart();
45
+ }
46
+
47
+ return chunks;
48
+ }
49
+
50
+ export const qqbotPlugin: ChannelPlugin<ResolvedQQBotAccount> = {
51
+ id: "qqbot",
52
+ meta: {
53
+ id: "qqbot",
54
+ label: "QQ Bot",
55
+ selectionLabel: "QQ Bot",
56
+ docsPath: "/docs/channels/qqbot",
57
+ blurb: "Connect to QQ via official QQ Bot API",
58
+ order: 50,
59
+ },
60
+ capabilities: {
61
+ chatTypes: ["direct", "group"],
62
+ media: true,
63
+ reactions: false,
64
+ threads: false,
65
+ /**
66
+ * blockStreaming: true 表示该 Channel 支持块流式
67
+ * 框架会收集流式响应,然后通过 deliver 回调发送
68
+ */
69
+ blockStreaming: false,
70
+ },
71
+ reload: { configPrefixes: ["channels.qqbot"] },
72
+ // CLI onboarding wizard
73
+ onboarding: qqbotOnboardingAdapter,
74
+ // 消息目标解析
75
+ messaging: {
76
+ normalizeTarget: (target) => {
77
+ // 支持格式: qqbot:c2c:xxx, qqbot:group:xxx, c2c:xxx, group:xxx, openid
78
+ const normalized = target.replace(/^qqbot:/i, "");
79
+ return { ok: true, to: normalized };
80
+ },
81
+ targetResolver: {
82
+ looksLikeId: (id) => {
83
+ // 先去掉 qqbot: 前缀
84
+ const normalized = id.replace(/^qqbot:/i, "");
85
+ // 支持 c2c:xxx, group:xxx, channel:xxx 格式
86
+ if (normalized.startsWith("c2c:") || normalized.startsWith("group:") || normalized.startsWith("channel:")) return true;
87
+ // 支持纯 openid(32位十六进制)
88
+ if (/^[A-F0-9]{32}$/i.test(normalized)) return true;
89
+ return false;
90
+ },
91
+ hint: "c2c:<openid> or group:<groupOpenid>",
92
+ },
93
+ },
94
+ config: {
95
+ listAccountIds: (cfg) => listQQBotAccountIds(cfg),
96
+ resolveAccount: (cfg, accountId) => resolveQQBotAccount(cfg, accountId),
97
+ defaultAccountId: (cfg) => resolveDefaultQQBotAccountId(cfg),
98
+ // 新增:设置账户启用状态
99
+ setAccountEnabled: ({ cfg, accountId, enabled }) =>
100
+ setAccountEnabledInConfigSection({
101
+ cfg,
102
+ sectionKey: "qqbot",
103
+ accountId,
104
+ enabled,
105
+ allowTopLevel: true,
106
+ }),
107
+ // 新增:删除账户
108
+ deleteAccount: ({ cfg, accountId }) =>
109
+ deleteAccountFromConfigSection({
110
+ cfg,
111
+ sectionKey: "qqbot",
112
+ accountId,
113
+ clearBaseFields: ["appId", "clientSecret", "clientSecretFile", "name"],
114
+ }),
115
+ isConfigured: (account) => Boolean(account?.appId && account?.clientSecret),
116
+ describeAccount: (account) => ({
117
+ accountId: account?.accountId ?? DEFAULT_ACCOUNT_ID,
118
+ name: account?.name,
119
+ enabled: account?.enabled ?? false,
120
+ configured: Boolean(account?.appId && account?.clientSecret),
121
+ tokenSource: account?.secretSource,
122
+ }),
123
+ },
124
+ setup: {
125
+ // 新增:规范化账户 ID
126
+ resolveAccountId: ({ accountId }) => accountId?.trim().toLowerCase() || DEFAULT_ACCOUNT_ID,
127
+ // 新增:应用账户名称
128
+ applyAccountName: ({ cfg, accountId, name }) =>
129
+ applyAccountNameToChannelSection({
130
+ cfg,
131
+ channelKey: "qqbot",
132
+ accountId,
133
+ name,
134
+ }),
135
+ validateInput: ({ input }) => {
136
+ if (!input.token && !input.tokenFile && !input.useEnv) {
137
+ return "QQBot requires --token (format: appId:clientSecret) or --use-env";
138
+ }
139
+ return null;
140
+ },
141
+ applyAccountConfig: ({ cfg, accountId, input }) => {
142
+ let appId = "";
143
+ let clientSecret = "";
144
+
145
+ if (input.token) {
146
+ const parts = input.token.split(":");
147
+ if (parts.length === 2) {
148
+ appId = parts[0];
149
+ clientSecret = parts[1];
150
+ }
151
+ }
152
+
153
+ return applyQQBotAccountConfig(cfg, accountId, {
154
+ appId,
155
+ clientSecret,
156
+ clientSecretFile: input.tokenFile,
157
+ name: input.name,
158
+ imageServerBaseUrl: input.imageServerBaseUrl,
159
+ });
160
+ },
161
+ },
162
+ // 新增:消息目标解析
163
+ messaging: {
164
+ normalizeTarget: (target) => {
165
+ // 支持格式: qqbot:openid, qqbot:group:xxx, openid, group:xxx
166
+ const normalized = target.replace(/^qqbot:/i, "");
167
+ return { ok: true, to: normalized };
168
+ },
169
+ targetResolver: {
170
+ looksLikeId: (id) => /^[A-F0-9]{32}$/i.test(id) || id.startsWith("group:") || id.startsWith("channel:"),
171
+ hint: "<openid> or group:<groupOpenid>",
172
+ },
173
+ },
174
+ outbound: {
175
+ deliveryMode: "direct",
176
+ chunker: chunkText,
177
+ chunkerMode: "markdown",
178
+ textChunkLimit: 2000,
179
+ sendText: async ({ to, text, accountId, replyToId, cfg }) => {
180
+ const account = resolveQQBotAccount(cfg, accountId);
181
+ const result = await sendText({ to, text, accountId, replyToId, account });
182
+ return {
183
+ channel: "qqbot",
184
+ messageId: result.messageId,
185
+ error: result.error ? new Error(result.error) : undefined,
186
+ };
187
+ },
188
+ sendMedia: async ({ to, text, mediaUrl, accountId, replyToId, cfg }) => {
189
+ const account = resolveQQBotAccount(cfg, accountId);
190
+ const result = await sendMedia({ to, text: text ?? "", mediaUrl: mediaUrl ?? "", accountId, replyToId, account });
191
+ return {
192
+ channel: "qqbot",
193
+ messageId: result.messageId,
194
+ error: result.error ? new Error(result.error) : undefined,
195
+ };
196
+ },
197
+ },
198
+ gateway: {
199
+ startAccount: async (ctx) => {
200
+ const { account, abortSignal, log, cfg } = ctx;
201
+
202
+ log?.info(`[qqbot:${account.accountId}] Starting gateway`);
203
+
204
+ await startGateway({
205
+ account,
206
+ abortSignal,
207
+ cfg,
208
+ log,
209
+ onReady: () => {
210
+ log?.info(`[qqbot:${account.accountId}] Gateway ready`);
211
+ ctx.setStatus({
212
+ ...ctx.getStatus(),
213
+ running: true,
214
+ connected: true,
215
+ lastConnectedAt: Date.now(),
216
+ });
217
+ },
218
+ onError: (error) => {
219
+ log?.error(`[qqbot:${account.accountId}] Gateway error: ${error.message}`);
220
+ ctx.setStatus({
221
+ ...ctx.getStatus(),
222
+ lastError: error.message,
223
+ });
224
+ },
225
+ });
226
+ },
227
+ // 新增:登出账户(清除配置中的凭证)
228
+ logoutAccount: async ({ accountId, cfg }) => {
229
+ const nextCfg = { ...cfg } as OpenClawConfig;
230
+ const nextQQBot = cfg.channels?.qqbot ? { ...cfg.channels.qqbot } : undefined;
231
+ let cleared = false;
232
+ let changed = false;
233
+
234
+ if (nextQQBot) {
235
+ const qqbot = nextQQBot as Record<string, unknown>;
236
+ if (accountId === DEFAULT_ACCOUNT_ID && qqbot.clientSecret) {
237
+ delete qqbot.clientSecret;
238
+ cleared = true;
239
+ changed = true;
240
+ }
241
+ const accounts = qqbot.accounts as Record<string, Record<string, unknown>> | undefined;
242
+ if (accounts && accountId in accounts) {
243
+ const entry = accounts[accountId] as Record<string, unknown> | undefined;
244
+ if (entry && "clientSecret" in entry) {
245
+ delete entry.clientSecret;
246
+ cleared = true;
247
+ changed = true;
248
+ }
249
+ if (entry && Object.keys(entry).length === 0) {
250
+ delete accounts[accountId];
251
+ changed = true;
252
+ }
253
+ }
254
+ }
255
+
256
+ if (changed && nextQQBot) {
257
+ nextCfg.channels = { ...nextCfg.channels, qqbot: nextQQBot };
258
+ const runtime = getQQBotRuntime();
259
+ const configApi = runtime.config as { writeConfigFile: (cfg: OpenClawConfig) => Promise<void> };
260
+ await configApi.writeConfigFile(nextCfg);
261
+ }
262
+
263
+ const resolved = resolveQQBotAccount(changed ? nextCfg : cfg, accountId);
264
+ const loggedOut = resolved.secretSource === "none";
265
+ const envToken = Boolean(process.env.QQBOT_CLIENT_SECRET);
266
+
267
+ return { ok: true, cleared, envToken, loggedOut };
268
+ },
269
+ },
270
+ status: {
271
+ defaultRuntime: {
272
+ accountId: DEFAULT_ACCOUNT_ID,
273
+ running: false,
274
+ connected: false,
275
+ lastConnectedAt: null,
276
+ lastError: null,
277
+ lastInboundAt: null,
278
+ lastOutboundAt: null,
279
+ },
280
+ // 新增:构建通道摘要
281
+ buildChannelSummary: ({ snapshot }: { snapshot: Record<string, unknown> }) => ({
282
+ configured: snapshot.configured ?? false,
283
+ tokenSource: snapshot.tokenSource ?? "none",
284
+ running: snapshot.running ?? false,
285
+ connected: snapshot.connected ?? false,
286
+ lastConnectedAt: snapshot.lastConnectedAt ?? null,
287
+ lastError: snapshot.lastError ?? null,
288
+ }),
289
+ buildAccountSnapshot: ({ account, runtime }: { account?: ResolvedQQBotAccount; runtime?: Record<string, unknown> }) => ({
290
+ accountId: account?.accountId ?? DEFAULT_ACCOUNT_ID,
291
+ name: account?.name,
292
+ enabled: account?.enabled ?? false,
293
+ configured: Boolean(account?.appId && account?.clientSecret),
294
+ tokenSource: account?.secretSource,
295
+ running: runtime?.running ?? false,
296
+ connected: runtime?.connected ?? false,
297
+ lastConnectedAt: runtime?.lastConnectedAt ?? null,
298
+ lastError: runtime?.lastError ?? null,
299
+ lastInboundAt: runtime?.lastInboundAt ?? null,
300
+ lastOutboundAt: runtime?.lastOutboundAt ?? null,
301
+ }),
302
+ },
303
+ };
package/src/config.ts ADDED
@@ -0,0 +1,172 @@
1
+ import type { ResolvedQQBotAccount, QQBotAccountConfig } from "./types.js";
2
+ import type { OpenClawConfig } from "openclaw/plugin-sdk";
3
+
4
+ export const DEFAULT_ACCOUNT_ID = "default";
5
+
6
+ interface QQBotChannelConfig extends QQBotAccountConfig {
7
+ accounts?: Record<string, QQBotAccountConfig>;
8
+ }
9
+
10
+ /**
11
+ * 列出所有 QQBot 账户 ID
12
+ */
13
+ export function listQQBotAccountIds(cfg: OpenClawConfig): string[] {
14
+ const ids = new Set<string>();
15
+ const qqbot = cfg.channels?.qqbot as QQBotChannelConfig | undefined;
16
+
17
+ if (qqbot?.appId) {
18
+ ids.add(DEFAULT_ACCOUNT_ID);
19
+ }
20
+
21
+ if (qqbot?.accounts) {
22
+ for (const accountId of Object.keys(qqbot.accounts)) {
23
+ if (qqbot.accounts[accountId]?.appId) {
24
+ ids.add(accountId);
25
+ }
26
+ }
27
+ }
28
+
29
+ return Array.from(ids);
30
+ }
31
+
32
+ /**
33
+ * 获取默认账户 ID
34
+ */
35
+ export function resolveDefaultQQBotAccountId(cfg: OpenClawConfig): string {
36
+ const qqbot = cfg.channels?.qqbot as QQBotChannelConfig | undefined;
37
+ // 如果有默认账户配置,返回 default
38
+ if (qqbot?.appId) {
39
+ return DEFAULT_ACCOUNT_ID;
40
+ }
41
+ // 否则返回第一个配置的账户
42
+ if (qqbot?.accounts) {
43
+ const ids = Object.keys(qqbot.accounts);
44
+ if (ids.length > 0) {
45
+ return ids[0];
46
+ }
47
+ }
48
+ return DEFAULT_ACCOUNT_ID;
49
+ }
50
+
51
+ /**
52
+ * 解析 QQBot 账户配置
53
+ */
54
+ export function resolveQQBotAccount(
55
+ cfg: OpenClawConfig,
56
+ accountId?: string | null
57
+ ): ResolvedQQBotAccount {
58
+ const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
59
+ const qqbot = cfg.channels?.qqbot as QQBotChannelConfig | undefined;
60
+
61
+ // 基础配置
62
+ let accountConfig: QQBotAccountConfig = {};
63
+ let appId = "";
64
+ let clientSecret = "";
65
+ let secretSource: "config" | "file" | "env" | "none" = "none";
66
+
67
+ if (resolvedAccountId === DEFAULT_ACCOUNT_ID) {
68
+ // 默认账户从顶层读取
69
+ accountConfig = {
70
+ enabled: qqbot?.enabled,
71
+ name: qqbot?.name,
72
+ appId: qqbot?.appId,
73
+ clientSecret: qqbot?.clientSecret,
74
+ clientSecretFile: qqbot?.clientSecretFile,
75
+ dmPolicy: qqbot?.dmPolicy,
76
+ allowFrom: qqbot?.allowFrom,
77
+ systemPrompt: qqbot?.systemPrompt,
78
+ imageServerBaseUrl: qqbot?.imageServerBaseUrl,
79
+ markdownSupport: qqbot?.markdownSupport,
80
+ };
81
+ appId = qqbot?.appId ?? "";
82
+ } else {
83
+ // 命名账户从 accounts 读取
84
+ const account = qqbot?.accounts?.[resolvedAccountId];
85
+ accountConfig = account ?? {};
86
+ appId = account?.appId ?? "";
87
+ }
88
+
89
+ // 解析 clientSecret
90
+ if (accountConfig.clientSecret) {
91
+ clientSecret = accountConfig.clientSecret;
92
+ secretSource = "config";
93
+ } else if (accountConfig.clientSecretFile) {
94
+ // 从文件读取(运行时处理)
95
+ secretSource = "file";
96
+ } else if (process.env.QQBOT_CLIENT_SECRET && resolvedAccountId === DEFAULT_ACCOUNT_ID) {
97
+ clientSecret = process.env.QQBOT_CLIENT_SECRET;
98
+ secretSource = "env";
99
+ }
100
+
101
+ // AppId 也可以从环境变量读取
102
+ if (!appId && process.env.QQBOT_APP_ID && resolvedAccountId === DEFAULT_ACCOUNT_ID) {
103
+ appId = process.env.QQBOT_APP_ID;
104
+ }
105
+
106
+ return {
107
+ accountId: resolvedAccountId,
108
+ name: accountConfig.name,
109
+ enabled: accountConfig.enabled !== false,
110
+ appId,
111
+ clientSecret,
112
+ secretSource,
113
+ systemPrompt: accountConfig.systemPrompt,
114
+ imageServerBaseUrl: accountConfig.imageServerBaseUrl || process.env.QQBOT_IMAGE_SERVER_BASE_URL,
115
+ markdownSupport: accountConfig.markdownSupport,
116
+ config: accountConfig,
117
+ };
118
+ }
119
+
120
+ /**
121
+ * 应用账户配置
122
+ */
123
+ export function applyQQBotAccountConfig(
124
+ cfg: OpenClawConfig,
125
+ accountId: string,
126
+ input: { appId?: string; clientSecret?: string; clientSecretFile?: string; name?: string; imageServerBaseUrl?: string }
127
+ ): OpenClawConfig {
128
+ const next = { ...cfg };
129
+
130
+ if (accountId === DEFAULT_ACCOUNT_ID) {
131
+ next.channels = {
132
+ ...next.channels,
133
+ qqbot: {
134
+ ...(next.channels?.qqbot as Record<string, unknown> || {}),
135
+ enabled: true,
136
+ ...(input.appId ? { appId: input.appId } : {}),
137
+ ...(input.clientSecret
138
+ ? { clientSecret: input.clientSecret }
139
+ : input.clientSecretFile
140
+ ? { clientSecretFile: input.clientSecretFile }
141
+ : {}),
142
+ ...(input.name ? { name: input.name } : {}),
143
+ ...(input.imageServerBaseUrl ? { imageServerBaseUrl: input.imageServerBaseUrl } : {}),
144
+ },
145
+ };
146
+ } else {
147
+ next.channels = {
148
+ ...next.channels,
149
+ qqbot: {
150
+ ...(next.channels?.qqbot as Record<string, unknown> || {}),
151
+ enabled: true,
152
+ accounts: {
153
+ ...((next.channels?.qqbot as QQBotChannelConfig)?.accounts || {}),
154
+ [accountId]: {
155
+ ...((next.channels?.qqbot as QQBotChannelConfig)?.accounts?.[accountId] || {}),
156
+ enabled: true,
157
+ ...(input.appId ? { appId: input.appId } : {}),
158
+ ...(input.clientSecret
159
+ ? { clientSecret: input.clientSecret }
160
+ : input.clientSecretFile
161
+ ? { clientSecretFile: input.clientSecretFile }
162
+ : {}),
163
+ ...(input.name ? { name: input.name } : {}),
164
+ ...(input.imageServerBaseUrl ? { imageServerBaseUrl: input.imageServerBaseUrl } : {}),
165
+ },
166
+ },
167
+ },
168
+ };
169
+ }
170
+
171
+ return next;
172
+ }