foliko 1.1.13 → 1.1.15

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 (102) hide show
  1. package/.agent/data/plugins-state.json +1 -1
  2. package/.agent/data/weixin/images/file_1776188148383jpg +0 -0
  3. package/.agent/data/weixin/images/file_1776188458326.jpg +0 -0
  4. package/.agent/data/weixin/images/file_1776188689423.jpg +0 -0
  5. package/.agent/data/weixin/images/file_1776188813604.jpg +0 -0
  6. package/.agent/data/weixin/images/file_1776189097450.jpg +0 -0
  7. package/.agent/data/weixin/videos/file_1776188318431.mp4 +0 -0
  8. package/.agent/mcp_config.json +7 -0
  9. package/.agent/memory/feedback/mnxe0cxc-14l6q5.md +17 -0
  10. package/.agent/memory/feedback/mnxe11pa-nxf577.md +9 -0
  11. package/.agent/memory/feedback/mnxe1an2-84faff.md +9 -0
  12. package/.agent/memory/feedback/mnxgcfj0-qg3wjc.md +9 -0
  13. package/.agent/memory/feedback/mnxgcn3y-40mqss.md +9 -0
  14. package/.agent/memory/feedback/mnxgcxq9-jm7ydl.md +9 -0
  15. package/.agent/memory/feedback/mnxgdyfj-pzjvkb.md +9 -0
  16. package/.agent/memory/feedback/mnxge3z1-7vyit1.md +9 -0
  17. package/.agent/memory/feedback/mnxhrg28-41hhjr.md +9 -0
  18. package/.agent/memory/feedback/mnxhrx0e-yth94k.md +9 -0
  19. package/.agent/memory/feedback/mnxhs3jd-rvx8aq.md +9 -0
  20. package/.agent/memory/feedback/mnxhs7p7-g5rtn9.md +9 -0
  21. package/.agent/memory/feedback/mnxhslx5-oqwuhr.md +9 -0
  22. package/.agent/memory/feedback/mnxhsvd6-nuyvvc.md +9 -0
  23. package/.agent/memory/project/mnxegq6z-5fc64w.md +22 -0
  24. package/.agent/memory/project/mnxh2w4r-le9hur.md +17 -0
  25. package/.agent/memory/project/mnxhq2yv-9qa8ay.md +31 -0
  26. package/.agent/memory/project/mnxhql11-iaun2o.md +34 -0
  27. package/.agent/memory/project/mnxhr78p-jpg7eq.md +23 -0
  28. package/.agent/memory/reference/mnxe0oa9-p6wzk6.md +27 -0
  29. package/.agent/memory/reference/mnxehcll-kcrmpf.md +29 -0
  30. package/.agent/memory/reference/mnxei0ts-jw091y.md +18 -0
  31. package/.agent/memory/reference/mnxfnrr4-rski36.md +40 -0
  32. package/.agent/memory/reference/mnxfo6n5-af9zls.md +18 -0
  33. package/.agent/memory/reference/mnxh2ady-u6cmvk.md +61 -0
  34. package/.agent/memory/reference/mnxhqdqh-ucsbsk.md +31 -0
  35. package/.agent/memory/reference/mnxiixyp-rz2gvw.md +34 -0
  36. package/.agent/memory/user/mnxhqxk3-vjjhlf.md +23 -0
  37. package/.agent/sessions/cli_default.json +11 -639
  38. package/.agent/sessions/weixin_o9cq80zgZqKPA2-s59PN43GdDy1w@im.wechat.json +25 -0
  39. package/.claude/settings.local.json +23 -1
  40. package/cli/src/commands/chat.js +9 -15
  41. package/cli/src/ui/chat-ui.js +40 -71
  42. package/package.json +4 -2
  43. package/plugins/default-plugins.js +5 -5
  44. package/plugins/file-system-plugin.js +1 -1
  45. package/plugins/memory-plugin.js +12 -12
  46. package/plugins/plugin-manager-plugin.js +1 -0
  47. package/plugins/subagent-plugin.js +55 -1
  48. package/plugins/telegram-plugin.js +9 -6
  49. package/plugins/weixin-plugin.js +75 -78
  50. package/src/core/agent-chat.js +468 -1612
  51. package/src/core/agent.js +53 -134
  52. package/src/core/chat-session.js +423 -0
  53. package/src/core/context-compressor.js +473 -0
  54. package/src/core/context-manager.js +0 -48
  55. package/src/core/framework.js +95 -68
  56. package/src/core/index.js +11 -0
  57. package/src/core/notification-manager.js +125 -0
  58. package/src/core/subagent.js +295 -0
  59. package/src/core/token-counter.js +190 -0
  60. package/src/core/tool-executor.js +270 -0
  61. package/src/executors/mcp-executor.js +14 -1
  62. package/src/utils/download.js +596 -0
  63. package/system.md +312 -2373
  64. package/.agent/agents/code-assistant.json +0 -17
  65. package/.agent/agents/email-assistant.json +0 -14
  66. package/.agent/agents/file-assistant.json +0 -18
  67. package/.agent/agents/orchestrator-demo.md +0 -53
  68. package/.agent/agents/orchestrator.json +0 -7
  69. package/.agent/agents/poster-expert.md +0 -228
  70. package/.agent/agents/system-assistant.json +0 -15
  71. package/.agent/agents/web-assistant.json +0 -12
  72. package/.agent/memory/feedback/mnv3nu27-3o15pf.md +0 -9
  73. package/.agent/memory/feedback/mnv3o078-b959yj.md +0 -9
  74. package/.agent/memory/feedback/mnv3o6ej-u0fif5.md +0 -9
  75. package/.agent/memory/feedback/mnv3obgl-bkkjoj.md +0 -9
  76. package/.agent/memory/feedback/mnv4a3js-dv6onx.md +0 -9
  77. package/.agent/memory/feedback/mnv4aacm-sxxowp.md +0 -9
  78. package/.agent/memory/feedback/mnv4ahto-w40ffm.md +0 -9
  79. package/.agent/memory/feedback/mnv4anvp-3cs06y.md +0 -9
  80. package/.agent/memory/feedback/mnvzgvtd-0o2900.md +0 -9
  81. package/.agent/memory/feedback/mnvzhajn-swbx61.md +0 -15
  82. package/.agent/memory/feedback/mnvzhgsp-p5vog3.md +0 -9
  83. package/.agent/memory/feedback/mnvzho0c-fgql7q.md +0 -14
  84. package/.agent/memory/feedback/mnvzhtzq-ufr5at.md +0 -9
  85. package/.agent/memory/feedback/mnvzhyb3-9byq2z.md +0 -9
  86. package/.agent/memory/feedback/mnvzi7hp-hyeafp.md +0 -9
  87. package/.agent/memory/feedback/mnvzibph-z7rwp5.md +0 -9
  88. package/.agent/memory/feedback/mnvzilys-7h176w.md +0 -14
  89. package/.agent/memory/feedback/mnvziuh5-zjshci.md +0 -9
  90. package/.agent/memory/feedback/mnw07wde-6zqsc8.md +0 -9
  91. package/.agent/memory/feedback/mnw084bp-j0ba2a.md +0 -9
  92. package/.agent/memory/user/mnv3n62r-y0h79j.md +0 -21
  93. package/.agent/memory/user/mnv3n9yf-ead4g8.md +0 -13
  94. package/.agent/memory/user/mnv3ne3j-82tq1k.md +0 -19
  95. package/.agent/memory/user/mnv3nhgm-g2s2us.md +0 -11
  96. package/.agent/memory/user/mnv3nl9u-ejd998.md +0 -16
  97. package/.agent/memory/user/mnv3nofp-ya5szl.md +0 -10
  98. package/.agent/memory/user/mnv49qne-bhk0ki.md +0 -9
  99. package/.agent/memory/user/mnv49w3y-rzr8ju.md +0 -13
  100. package/.agent/sessions/test.json +0 -16
  101. package/plugins/python-plugin-loader.js.bak +0 -856
  102. package/src/core/agent-context.js +0 -188
@@ -0,0 +1,190 @@
1
+ /**
2
+ * TokenCounter - Token 计算工具
3
+ *
4
+ * 职责:
5
+ * 1. 计算文本的 token 数量
6
+ * 2. 计算消息数组的 token 总数
7
+ * 3. 计算工具定义的 token 总数
8
+ */
9
+
10
+ /**
11
+ * 简单的中英文混合 tokenizer
12
+ * 粗略估计:中文每个字符 2 字节,英文每个单词约 1.5 字节
13
+ * @param {string} text - 文本
14
+ * @param {number} bytesPerToken - 每 token 字节数,默认 4
15
+ * @returns {number} token 数量
16
+ */
17
+ function encode(text, bytesPerToken = 4) {
18
+ if (!text) return 0;
19
+ const bytes = Buffer.byteLength(String(text), 'utf8');
20
+ return Math.ceil(bytes / bytesPerToken);
21
+ }
22
+
23
+ /**
24
+ * 计算 JSON 字符串的 token 数量
25
+ * @param {string} text - JSON 字符串
26
+ * @returns {number} token 数量
27
+ */
28
+ function encodeForJSON(text) {
29
+ if (!text) return 0;
30
+ // JSON 字符串需要额外计算引号和转义
31
+ const encoded = encode(text);
32
+ return encoded + Math.ceil(Buffer.byteLength(JSON.stringify(text), 'utf8') / 100);
33
+ }
34
+
35
+ class TokenCounter {
36
+ /**
37
+ * @param {Object} config - 配置
38
+ * @param {Object} config.toolSchema - 工具 schema(用于工具 token 计算)
39
+ */
40
+ constructor(config = {}) {
41
+ this.toolSchema = config.toolSchema || null;
42
+ }
43
+
44
+ /**
45
+ * 计算文本 token
46
+ * @param {string} text - 文本
47
+ * @param {number} bytesPerToken - 每 token 字节数
48
+ * @returns {number} token 数量
49
+ */
50
+ countText(text, bytesPerToken = 4) {
51
+ return encode(text, bytesPerToken);
52
+ }
53
+
54
+ /**
55
+ * 计算消息数组的 token 总数
56
+ * @param {Array} messages - 消息数组
57
+ * @returns {number} token 总数
58
+ */
59
+ countMessages(messages) {
60
+ if (!Array.isArray(messages)) return 0;
61
+ return messages.reduce((sum, msg) => sum + this.countMessage(msg), 0);
62
+ }
63
+
64
+ /**
65
+ * 计算单条消息的 token
66
+ * @param {Object} msg - 消息
67
+ * @returns {number} token 数量
68
+ */
69
+ countMessage(msg) {
70
+ if (!msg) return 0;
71
+
72
+ let total = 0;
73
+
74
+ // 角色和格式开销
75
+ total += 4;
76
+
77
+ if (typeof msg.content === 'string') {
78
+ total += this.countText(msg.content);
79
+ } else if (Array.isArray(msg.content)) {
80
+ for (const block of msg.content) {
81
+ total += this.countContentBlock(block);
82
+ }
83
+ }
84
+
85
+ // tool_calls 开销
86
+ if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
87
+ total += 15; // overhead per tool_calls block
88
+ for (const tc of msg.tool_calls) {
89
+ if (tc.function) {
90
+ total += this.countText(tc.function.name) + this.countText(tc.function.arguments);
91
+ }
92
+ }
93
+ }
94
+
95
+ // tool_call_id 开销
96
+ if (msg.tool_call_id) {
97
+ total += 15;
98
+ }
99
+
100
+ return total;
101
+ }
102
+
103
+ /**
104
+ * 计算 content block 的 token
105
+ * @param {Object} block - content block
106
+ * @returns {number} token 数量
107
+ */
108
+ countContentBlock(block) {
109
+ if (!block) return 0;
110
+
111
+ switch (block.type) {
112
+ case 'text':
113
+ return this.countText(block.text);
114
+ case 'tool-call':
115
+ case 'tool-use':
116
+ if (block.input) {
117
+ return this.countText(JSON.stringify(block.input));
118
+ }
119
+ return 0;
120
+ case 'tool-result':
121
+ case 'tool_result':
122
+ if (block.content) {
123
+ const content =
124
+ typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
125
+ return this.countText(content);
126
+ }
127
+ return 0;
128
+ case 'image':
129
+ // 图片按 token 估算
130
+ return 85;
131
+ default:
132
+ return this.countText(JSON.stringify(block));
133
+ }
134
+ }
135
+
136
+ /**
137
+ * 计算工具定义的 token 总数
138
+ * @param {Array} tools - 工具数组
139
+ * @returns {number} token 总数
140
+ */
141
+ countTools(tools) {
142
+ if (!tools || !Array.isArray(tools)) return 0;
143
+
144
+ let total = 0;
145
+ for (const tool of tools) {
146
+ // 工具名和描述
147
+ total += 20; // overhead
148
+
149
+ if (tool.description) {
150
+ total += this.countText(tool.description);
151
+ }
152
+
153
+ // 参数
154
+ if (tool.inputSchema) {
155
+ const schema =
156
+ tool.inputSchema.jsonSchema || tool.inputSchema.inputSchema || tool.inputSchema;
157
+ if (schema.properties) {
158
+ for (const [name, prop] of Object.entries(schema.properties)) {
159
+ total += this.countText(name) + 10;
160
+ if (prop.description) {
161
+ total += this.countText(prop.description);
162
+ }
163
+ }
164
+ }
165
+ }
166
+ }
167
+
168
+ return total;
169
+ }
170
+
171
+ /**
172
+ * 估算完整请求的 token(消息 + 工具 + 系统提示)
173
+ * @param {Object} params - 请求参数
174
+ * @returns {Object} - { messagesTokens, toolsTokens, systemPromptTokens, total }
175
+ */
176
+ estimateRequest({ messages, tools, systemPrompt }) {
177
+ const messagesTokens = this.countMessages(messages);
178
+ const toolsTokens = this.countTools(tools);
179
+ const systemPromptTokens = systemPrompt ? this.countText(systemPrompt) : 0;
180
+
181
+ return {
182
+ messagesTokens,
183
+ toolsTokens,
184
+ systemPromptTokens,
185
+ total: messagesTokens + toolsTokens + systemPromptTokens,
186
+ };
187
+ }
188
+ }
189
+
190
+ module.exports = { TokenCounter, encode, encodeForJSON };
@@ -0,0 +1,270 @@
1
+ /**
2
+ * ToolExecutor - 工具执行器
3
+ *
4
+ * 职责:
5
+ * 1. 工具发现和注册
6
+ * 2. 工具执行
7
+ * 3. 工具调用验证
8
+ */
9
+
10
+ const { EventEmitter } = require('../utils/event-emitter');
11
+ const { logger } = require('../utils/logger');
12
+
13
+ class ToolExecutor extends EventEmitter {
14
+ /**
15
+ * @param {Object} config - 配置
16
+ */
17
+ constructor(config = {}) {
18
+ super();
19
+
20
+ this.config = config;
21
+ this.agent = config.agent;
22
+ this.framework = config.framework;
23
+
24
+ // 工具注册表: name -> toolDef
25
+ this._tools = new Map();
26
+
27
+ // 工具调用统计
28
+ this._toolStats = {
29
+ totalCalls: 0,
30
+ failedCalls: 0,
31
+ lastCall: null,
32
+ };
33
+ }
34
+
35
+ /**
36
+ * 注册工具
37
+ * @param {Object} tool - 工具定义
38
+ */
39
+ registerTool(tool) {
40
+ if (!tool || !tool.name) {
41
+ logger.warn('ToolExecutor', 'Ignoring tool with no name');
42
+ return;
43
+ }
44
+ this._tools.set(tool.name, tool);
45
+ logger.debug('ToolExecutor', `Registered tool: ${tool.name}`);
46
+ }
47
+
48
+ /**
49
+ * 批量注册工具
50
+ * @param {Array} tools - 工具数组
51
+ */
52
+ registerTools(tools) {
53
+ if (!Array.isArray(tools)) return;
54
+ for (const tool of tools) {
55
+ this.registerTool(tool);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * 注销工具
61
+ * @param {string} name - 工具名
62
+ */
63
+ unregisterTool(name) {
64
+ this._tools.delete(name);
65
+ }
66
+
67
+ /**
68
+ * 获取工具
69
+ * @param {string} name - 工具名
70
+ * @returns {Object|null}
71
+ */
72
+ getTool(name) {
73
+ return this._tools.get(name) || null;
74
+ }
75
+
76
+ /**
77
+ * 获取所有工具
78
+ * @returns {Array}
79
+ */
80
+ getAllTools() {
81
+ return Array.from(this._tools.values());
82
+ }
83
+
84
+ /**
85
+ * 检查工具是否存在
86
+ * @param {string} name - 工具名
87
+ * @returns {boolean}
88
+ */
89
+ hasTool(name) {
90
+ return this._tools.has(name);
91
+ }
92
+
93
+ /**
94
+ * 执行工具
95
+ * @param {string} name - 工具名
96
+ * @param {Object} args - 参数
97
+ * @param {Object} options - 选项
98
+ * @returns {Promise}
99
+ */
100
+ async executeTool(name, args = {}, options = {}) {
101
+ const tool = this._tools.get(name);
102
+ if (!tool) {
103
+ const error = `Tool '${name}' not found`;
104
+ logger.warn('ToolExecutor', error);
105
+ throw new Error(error);
106
+ }
107
+
108
+ this._toolStats.totalCalls++;
109
+ this._toolStats.lastCall = {
110
+ name,
111
+ args,
112
+ timestamp: Date.now(),
113
+ };
114
+
115
+ this.emit('tool:call', { name, args, source: options.source });
116
+
117
+ try {
118
+ // 统一 execute 签名为 (args, framework)
119
+ const result = await tool.execute(args, this.framework);
120
+
121
+ this.emit('tool:result', { name, args, result, source: options.source });
122
+
123
+ return result;
124
+ } catch (err) {
125
+ this._toolStats.failedCalls++;
126
+ logger.error('ToolExecutor', `Tool '${name}' failed:`, err.message);
127
+
128
+ this.emit('tool:error', {
129
+ name,
130
+ args,
131
+ error: err.message,
132
+ source: options.source,
133
+ });
134
+
135
+ throw err;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * 批量执行工具
141
+ * @param {Array} tools - 工具调用数组 [{name, args}, ...]
142
+ * @param {Object} options - 选项
143
+ * @returns {Promise<Array>}
144
+ */
145
+ async executeTools(tools, options = {}) {
146
+ if (!Array.isArray(tools)) {
147
+ throw new Error('tools must be an array');
148
+ }
149
+
150
+ const results = [];
151
+ for (const toolCall of tools) {
152
+ try {
153
+ const result = await this.executeTool(toolCall.name, toolCall.args || {}, options);
154
+ results.push({ success: true, name: toolCall.name, result });
155
+ } catch (err) {
156
+ results.push({ success: false, name: toolCall.name, error: err.message });
157
+ }
158
+ }
159
+ return results;
160
+ }
161
+
162
+ /**
163
+ * 获取工具的 AI 格式(用于发送给 AI)
164
+ * @returns {Object} AI SDK 格式的工具对象
165
+ */
166
+ getToolsForAI() {
167
+ const tools = {};
168
+ for (const [name, tool] of this._tools) {
169
+ if (!tool.description) continue;
170
+
171
+ tools[name] = {
172
+ description: tool.description,
173
+ inputSchema: tool.inputSchema,
174
+ execute: tool.execute ? tool.execute.bind(tool) : undefined,
175
+ };
176
+ }
177
+ return tools;
178
+ }
179
+
180
+ /**
181
+ * 验证工具调用的参数
182
+ * @param {Array} messages - 消息数组
183
+ * @returns {Array} 验证后的消息
184
+ */
185
+ validateToolCalls(messages) {
186
+ let fixedCount = 0;
187
+ // 收集被跳过的 toolCallId,用于清理对应的 tool-result
188
+ const invalidatedToolCallIds = new Set();
189
+
190
+ for (const msg of messages) {
191
+ // 清理 assistant 消息中的不完整 tool-call
192
+ if (msg.role === 'assistant' && Array.isArray(msg.content)) {
193
+ for (const item of msg.content) {
194
+ // 兼容 tool-call 和 tool-use 两种类型
195
+ if (item.type !== 'tool-call' && item.type !== 'tool-use') {
196
+ continue;
197
+ }
198
+
199
+ const input = item.input;
200
+ if (typeof input !== 'string') {
201
+ continue;
202
+ }
203
+
204
+ // 检查 input 是否是有效的 JSON(不是不完整的)
205
+ const trimmed = input.trim();
206
+ if (trimmed === '{' || trimmed === '' || !trimmed.startsWith('{')) {
207
+ // 不完整的 JSON,移除这个 tool-call
208
+ // 记录 toolCallId,以便后续清理对应的 tool-result
209
+ if (item.toolCallId) {
210
+ invalidatedToolCallIds.add(item.toolCallId);
211
+ }
212
+ logger.warn(
213
+ `_validateToolCalls: invalid tool-call input="${input}", toolCallId=${item.toolCallId}, converting to text`
214
+ );
215
+ item.type = 'text';
216
+ item.text = `(工具调用 ${item.toolName} 参数不完整,已跳过)`;
217
+ delete item.toolCallId;
218
+ delete item.toolName;
219
+ delete item.input;
220
+ fixedCount++;
221
+ }
222
+ }
223
+ }
224
+ }
225
+
226
+ // 如果有无效的 tool-call,清理对应的 tool-result
227
+ if (invalidatedToolCallIds.size > 0) {
228
+ logger.warn(
229
+ `_validateToolCalls: removing ${invalidatedToolCallIds.size} tool-results with invalidated toolCallIds`
230
+ );
231
+ for (const msg of messages) {
232
+ if (msg.role === 'tool' && Array.isArray(msg.content)) {
233
+ // 过滤掉引用了无效 toolCallId 的 tool-result
234
+ const oldLen = msg.content.length;
235
+ msg.content = msg.content.filter((item) => {
236
+ if (item.type !== 'tool-result' && item.type !== 'tool_result') {
237
+ return true;
238
+ }
239
+ // 如果 tool-result 引用的 toolCallId 已被标记为无效,则移除
240
+ if (item.toolCallId && invalidatedToolCallIds.has(item.toolCallId)) {
241
+ logger.warn(
242
+ `_validateToolCalls: removing orphaned tool-result with toolCallId=${item.toolCallId}`
243
+ );
244
+ fixedCount++;
245
+ return false;
246
+ }
247
+ return true;
248
+ });
249
+ }
250
+ }
251
+ }
252
+
253
+ if (fixedCount > 0) {
254
+ logger.info(`_validateToolCalls: Fixed ${fixedCount} incomplete tool calls/results`);
255
+ }
256
+ }
257
+
258
+ /**
259
+ * 获取工具统计
260
+ * @returns {Object}
261
+ */
262
+ getStats() {
263
+ return {
264
+ ...this._toolStats,
265
+ toolCount: this._tools.size,
266
+ };
267
+ }
268
+ }
269
+
270
+ module.exports = { ToolExecutor };
@@ -37,6 +37,10 @@ class MCPClientWrapper {
37
37
  async connect() {
38
38
  if (this.connected) return;
39
39
 
40
+ const controller = new AbortController();
41
+ const timeoutMs = this.timeout || 30000;
42
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
43
+
40
44
  try {
41
45
  // 动态导入 @ai-sdk/mcp
42
46
  let createMCPClient;
@@ -63,7 +67,10 @@ class MCPClientWrapper {
63
67
  args: shellResolve ? [shellResolve, ...this.args] : this.args,
64
68
  env: { ...process.env, ...this.env },
65
69
  });
66
- this.client = await createMCPClient({ transport });
70
+ this.client = await createMCPClient({
71
+ transport,
72
+ signal: controller.signal,
73
+ });
67
74
  } catch (transportErr) {
68
75
  log.error(` Transport error:`, transportErr.message);
69
76
  throw transportErr;
@@ -85,8 +92,14 @@ class MCPClientWrapper {
85
92
  this.connected = true;
86
93
  log.info(` Connected to ${this.serverName} with ${this.tools.length} tools`);
87
94
  } catch (err) {
95
+ if (controller.signal.aborted) {
96
+ log.error(` Connection timeout (${timeoutMs}ms) for ${this.serverName}`);
97
+ throw new Error(`连接超时 (${timeoutMs}ms): ${this.serverName}`);
98
+ }
88
99
  log.error(` Failed to connect to ${this.serverName}:`, err.message);
89
100
  // 不抛出错误,让框架继续运行
101
+ } finally {
102
+ clearTimeout(timeoutId);
90
103
  }
91
104
  }
92
105