@raolin2025/claude-code-node 2.5.2 → 2.6.2
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/README.md +33 -2
- package/package.json +2 -2
- package/src/__tests__/llm-server.test.js +62 -0
- package/src/channel/notify-daemon.js +105 -18
- package/src/channel/qqbot-listener.js +3 -7
- package/src/channel/tg-listener.js +1 -1
- package/src/channel/tg-proxy.js +105 -32
- package/src/core/cli.js +220 -58
- package/src/core/index.js +2 -0
- package/src/core/query-engine.js +40 -9
- package/src/stdio/server.js +426 -0
- package/src/tools/index.js +3 -3
- package/src/tools/telegram-tools.js +319 -0
- package/src/types/index.js +6 -1
- package/src/utils/index.js +1 -0
- package/src/utils/llm-server.js +106 -0
- package/src/tools/qqbot-channel-api.js +0 -147
- package/src/tools/qqbot-media.js +0 -177
- package/src/tools/qqbot-remind.js +0 -42
- package/src/tools/qqbot-tools-wrapper.js +0 -185
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server.js — cc-node --stdio 服务器(JSON-RPC 2.0 over NDJSON)
|
|
3
|
+
*
|
|
4
|
+
* 协议:docs/stdio-protocol.md v1.0(详见 cc-node-bridge 项目)
|
|
5
|
+
*
|
|
6
|
+
* 职责:
|
|
7
|
+
* - 通过 stdin 接收 JSON-RPC 请求,stdout 返回响应与事件(每行一个 JSON)
|
|
8
|
+
* - 引擎子进程形态:每会话一个进程(由桥接层 spawn 本模块)
|
|
9
|
+
* - remote 工具模式:LLM 产生工具调用 → 发 event/toolCall → 挂起等待
|
|
10
|
+
* toolCall/result → 结果喂回 LLM
|
|
11
|
+
*
|
|
12
|
+
* 用法:
|
|
13
|
+
* cc-node --stdio [--api-base ...] [--model ...] [--api-key ...]
|
|
14
|
+
* node src/stdio/server.js 等同
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { createInterface } from 'readline'
|
|
18
|
+
import { randomUUID } from 'crypto'
|
|
19
|
+
import { homedir } from 'os'
|
|
20
|
+
import { join, resolve } from 'path'
|
|
21
|
+
import {
|
|
22
|
+
QueryEngine,
|
|
23
|
+
QueryEngineConfig,
|
|
24
|
+
SessionManager,
|
|
25
|
+
} from '../core/index.js'
|
|
26
|
+
import { CostTracker } from '../core/cost-tracker.js'
|
|
27
|
+
import { builtinTools } from '../tools/index.js'
|
|
28
|
+
import { isLocalLlmServer } from '../utils/index.js'
|
|
29
|
+
|
|
30
|
+
const DEFAULT_SYSTEM_PROMPT =
|
|
31
|
+
'You are cc-node, an AI coding assistant running as a stdio server. ' +
|
|
32
|
+
'Tools are executed by the client (VS Code / Web / Telegram). ' +
|
|
33
|
+
'When you call tools, the client performs them and returns results.'
|
|
34
|
+
|
|
35
|
+
export class StdioServer {
|
|
36
|
+
constructor(options = {}) {
|
|
37
|
+
this.cliArgs = options.cliArgs || {}
|
|
38
|
+
this.config = {
|
|
39
|
+
apiBase: this.cliArgs.apiBase || process.env.LLM_API_BASE || '',
|
|
40
|
+
apiKey: this.cliArgs.apiKey || process.env.LLM_API_KEY || '',
|
|
41
|
+
model: this.cliArgs.model || '',
|
|
42
|
+
systemPrompt: this.cliArgs.systemPrompt || DEFAULT_SYSTEM_PROMPT,
|
|
43
|
+
maxTurns: this.cliArgs.maxTurns || 50,
|
|
44
|
+
}
|
|
45
|
+
this.initialized = false
|
|
46
|
+
this.running = false
|
|
47
|
+
this.aborted = false
|
|
48
|
+
this.engine = null
|
|
49
|
+
this.toolDefs = null // 客户端 tools/define 下发(null = 用内置定义)
|
|
50
|
+
this.pendingToolCalls = new Map() // toolCallId → resolve({content, isError})
|
|
51
|
+
this.sessionManager = null
|
|
52
|
+
this.currentSession = null
|
|
53
|
+
this.shuttingDown = false
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ─────────────────────────── 协议基础 ───────────────────────────
|
|
57
|
+
|
|
58
|
+
start() {
|
|
59
|
+
const rl = createInterface({ input: process.stdin, crlfDelay: Infinity })
|
|
60
|
+
this._chain = Promise.resolve()
|
|
61
|
+
rl.on('line', (line) => {
|
|
62
|
+
if (!line.trim()) return
|
|
63
|
+
let msg
|
|
64
|
+
try {
|
|
65
|
+
msg = JSON.parse(line)
|
|
66
|
+
} catch {
|
|
67
|
+
this._sendError(null, -32700, '解析错误: 无效 JSON')
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
// 串行处理:保证响应/事件顺序(协议要求按序)
|
|
71
|
+
this._chain = this._chain.then(() => this._dispatch(msg)).catch((e) => {
|
|
72
|
+
console.error(`[cc-node-stdio] 处理异常: ${e.message}`)
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
rl.on('close', () => {
|
|
76
|
+
this._shutdownNow()
|
|
77
|
+
})
|
|
78
|
+
// stdout 只用于协议,日志走 stderr
|
|
79
|
+
console.error('[cc-node-stdio] 服务器已启动,等待请求…')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async _dispatch(msg) {
|
|
83
|
+
const { id, method, params } = msg
|
|
84
|
+
|
|
85
|
+
// 通知(无 id)
|
|
86
|
+
if (id === undefined || id === null) {
|
|
87
|
+
if (method === 'abort') this._abort()
|
|
88
|
+
else if (method === 'shutdown') this._shutdownNow()
|
|
89
|
+
// 其他通知忽略
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
switch (method) {
|
|
95
|
+
case 'initialize':
|
|
96
|
+
this._sendResponse(id, await this._initialize(params))
|
|
97
|
+
break
|
|
98
|
+
case 'chat':
|
|
99
|
+
this._sendResponse(id, await this._chat(params))
|
|
100
|
+
break
|
|
101
|
+
case 'toolCall/result':
|
|
102
|
+
this._sendResponse(id, await this._toolCallResult(params))
|
|
103
|
+
break
|
|
104
|
+
case 'tools/define':
|
|
105
|
+
this._sendResponse(id, await this._toolsDefine(params))
|
|
106
|
+
break
|
|
107
|
+
case 'session/new':
|
|
108
|
+
this._sendResponse(id, await this._sessionNew(params))
|
|
109
|
+
break
|
|
110
|
+
case 'session/load':
|
|
111
|
+
this._sendResponse(id, await this._sessionLoad(params))
|
|
112
|
+
break
|
|
113
|
+
case 'session/list':
|
|
114
|
+
this._sendResponse(id, await this._sessionList())
|
|
115
|
+
break
|
|
116
|
+
case 'session/delete':
|
|
117
|
+
this._sendResponse(id, await this._sessionDelete(params))
|
|
118
|
+
break
|
|
119
|
+
case 'config/get':
|
|
120
|
+
this._sendResponse(id, this._configGet())
|
|
121
|
+
break
|
|
122
|
+
case 'config/set':
|
|
123
|
+
this._sendResponse(id, await this._configSet(params))
|
|
124
|
+
break
|
|
125
|
+
case 'shutdown':
|
|
126
|
+
this._sendResponse(id, { ok: true })
|
|
127
|
+
this._shutdownNow()
|
|
128
|
+
break
|
|
129
|
+
default:
|
|
130
|
+
this._sendError(id, -32601, `方法不存在: ${method}`)
|
|
131
|
+
}
|
|
132
|
+
} catch (err) {
|
|
133
|
+
this._sendError(id, err.code || -32603, err.message)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
_sendResponse(id, result) {
|
|
138
|
+
process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, result }) + '\n')
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
_sendError(id, code, message) {
|
|
142
|
+
process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } }) + '\n')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
_sendEvent(method, params) {
|
|
146
|
+
process.stdout.write(JSON.stringify({ jsonrpc: '2.0', method, params }) + '\n')
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ─────────────────────────── 方法实现 ───────────────────────────
|
|
150
|
+
|
|
151
|
+
async _initialize(params = {}) {
|
|
152
|
+
this.initialized = true
|
|
153
|
+
const clientInfo = params.clientInfo || {}
|
|
154
|
+
console.error(`[cc-node-stdio] 客户端接入: ${clientInfo.name || 'unknown'} ${clientInfo.version || ''}`)
|
|
155
|
+
return {
|
|
156
|
+
serverInfo: { name: 'cc-node', version: '2.6.2' },
|
|
157
|
+
capabilities: {
|
|
158
|
+
toolExecution: 'remote',
|
|
159
|
+
streaming: true,
|
|
160
|
+
images: true,
|
|
161
|
+
modes: ['plan', 'code'],
|
|
162
|
+
sessions: true,
|
|
163
|
+
abort: true,
|
|
164
|
+
},
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async _chat(params = {}) {
|
|
169
|
+
if (!this.initialized) throw this._err(3, '未初始化,先发送 initialize')
|
|
170
|
+
if (this.running) throw this._err(1, '引擎正在运行中')
|
|
171
|
+
const text = params.text
|
|
172
|
+
if (typeof text !== 'string' || !text.trim()) throw this._err(-32602, '缺少 text 参数')
|
|
173
|
+
|
|
174
|
+
// 模式切换(若提供)
|
|
175
|
+
if (params.mode && params.mode !== this._mode) {
|
|
176
|
+
this._mode = params.mode
|
|
177
|
+
this._rebuildEngine()
|
|
178
|
+
this._sendEvent('event/mode', { mode: this._mode })
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
this.running = true
|
|
182
|
+
this.aborted = false
|
|
183
|
+
this._sendEvent('event/status', { state: 'running' })
|
|
184
|
+
|
|
185
|
+
// 异步执行(不阻塞响应)
|
|
186
|
+
this._runChat(text, params.images || [], params.sessionId).catch((err) => {
|
|
187
|
+
this._sendEvent('event/error', { message: err.message })
|
|
188
|
+
this.running = false
|
|
189
|
+
this._sendEvent('event/status', { state: 'idle' })
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
return { accepted: true }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async _runChat(text, images, sessionId) {
|
|
196
|
+
try {
|
|
197
|
+
const engine = await this._getEngine()
|
|
198
|
+
const result = await engine.processMessage(text, images)
|
|
199
|
+
if (this.aborted) {
|
|
200
|
+
this._sendEvent('event/done', { content: '', aborted: true, turns: engine.state.turnCount })
|
|
201
|
+
} else {
|
|
202
|
+
// 持久化到会话
|
|
203
|
+
try {
|
|
204
|
+
const session = await this._getSession(sessionId)
|
|
205
|
+
await this.sessionManager.appendMessage({ role: 'assistant', content: result.response })
|
|
206
|
+
} catch (e) { /* 会话保存失败不影响响应 */ }
|
|
207
|
+
this._sendEvent('event/done', {
|
|
208
|
+
content: result.response,
|
|
209
|
+
usage: engine.costTracker ? this._lastUsage(engine) : {},
|
|
210
|
+
turns: result.turns,
|
|
211
|
+
aborted: false,
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
} catch (err) {
|
|
215
|
+
if (err.message === '操作已取消' || err.name === 'AbortError') {
|
|
216
|
+
this._sendEvent('event/done', { content: '', aborted: true, turns: 0 })
|
|
217
|
+
} else {
|
|
218
|
+
this._sendEvent('event/error', { message: err.message })
|
|
219
|
+
}
|
|
220
|
+
} finally {
|
|
221
|
+
this.running = false
|
|
222
|
+
this._sendEvent('event/status', { state: 'idle' })
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
_lastUsage(engine) {
|
|
227
|
+
// QueryEngine 内部 costTracker 记录在 usageHistory 或直接读取最近记录
|
|
228
|
+
try {
|
|
229
|
+
const history = engine.costTracker.usageHistory || []
|
|
230
|
+
return history.length ? history[history.length - 1] : {}
|
|
231
|
+
} catch {
|
|
232
|
+
return {}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
_abort() {
|
|
237
|
+
if (this.engine && this.engine.abortController) {
|
|
238
|
+
this.aborted = true
|
|
239
|
+
this.engine.abortController.abort()
|
|
240
|
+
console.error('[cc-node-stdio] 已请求中断')
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async _toolCallResult(params = {}) {
|
|
245
|
+
const { toolCallId, result, isError } = params
|
|
246
|
+
const resolve = this.pendingToolCalls.get(toolCallId)
|
|
247
|
+
if (!resolve) throw this._err(-32602, `未知的 toolCallId: ${toolCallId}`)
|
|
248
|
+
this.pendingToolCalls.delete(toolCallId)
|
|
249
|
+
resolve({ content: String(result ?? ''), isError: !!isError })
|
|
250
|
+
return { accepted: true }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async _toolsDefine(params = {}) {
|
|
254
|
+
const tools = params.tools
|
|
255
|
+
if (!Array.isArray(tools)) throw this._err(-32602, 'tools 必须为数组')
|
|
256
|
+
this.toolDefs = tools
|
|
257
|
+
this._rebuildEngine()
|
|
258
|
+
return { ok: true, count: tools.length }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async _sessionNew(params = {}) {
|
|
262
|
+
const sm = await this._getSessionManager()
|
|
263
|
+
const session = await sm.create(params.title || '')
|
|
264
|
+
this.currentSession = session
|
|
265
|
+
this._rebuildEngine()
|
|
266
|
+
return { sessionId: session.id, title: session.title }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async _sessionLoad(params = {}) {
|
|
270
|
+
const sm = await this._getSessionManager()
|
|
271
|
+
const session = await sm.load(params.sessionId)
|
|
272
|
+
if (!session) throw this._err(-32602, `会话不存在: ${params.sessionId}`)
|
|
273
|
+
this.currentSession = session
|
|
274
|
+
this._rebuildEngine()
|
|
275
|
+
return { sessionId: session.id, title: session.title, messageCount: session.messages?.length || 0 }
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async _sessionList() {
|
|
279
|
+
const sm = await this._getSessionManager()
|
|
280
|
+
const sessions = await sm.list()
|
|
281
|
+
return { sessions }
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async _sessionDelete(params = {}) {
|
|
285
|
+
const sm = await this._getSessionManager()
|
|
286
|
+
return { ok: await sm.delete(params.sessionId) }
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
_configGet() {
|
|
290
|
+
return {
|
|
291
|
+
config: {
|
|
292
|
+
apiBase: this.config.apiBase,
|
|
293
|
+
apiKey: this.config.apiKey ? '***' : '',
|
|
294
|
+
model: this.config.model,
|
|
295
|
+
mode: this._mode,
|
|
296
|
+
},
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async _configSet(params = {}) {
|
|
301
|
+
const c = params.config || {}
|
|
302
|
+
if (c.apiBase !== undefined) this.config.apiBase = String(c.apiBase)
|
|
303
|
+
if (c.apiKey !== undefined) this.config.apiKey = String(c.apiKey)
|
|
304
|
+
if (c.model !== undefined) this.config.model = String(c.model)
|
|
305
|
+
if (c.systemPrompt !== undefined) this.config.systemPrompt = String(c.systemPrompt)
|
|
306
|
+
this._rebuildEngine()
|
|
307
|
+
return { ok: true }
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
_shutdownNow() {
|
|
311
|
+
if (this.shuttingDown) return
|
|
312
|
+
this.shuttingDown = true
|
|
313
|
+
console.error('[cc-node-stdio] 关闭中…')
|
|
314
|
+
process.exitCode = 0
|
|
315
|
+
// 等待 stdout 缓冲刷完再退出(立即 exit 会丢弃未写入的响应)
|
|
316
|
+
setTimeout(() => process.exit(0), 50)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ─────────────────────────── 引擎 ───────────────────────────
|
|
320
|
+
|
|
321
|
+
get _mode() {
|
|
322
|
+
return this._currentMode || 'code'
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
set _mode(v) {
|
|
326
|
+
this._currentMode = v
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async _getSessionManager() {
|
|
330
|
+
if (!this.sessionManager) {
|
|
331
|
+
const sessionsDir = join(homedir(), '.cc-node', 'sessions')
|
|
332
|
+
this.sessionManager = new SessionManager({ sessionsDir })
|
|
333
|
+
await this.sessionManager.ensureDir()
|
|
334
|
+
}
|
|
335
|
+
return this.sessionManager
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async _getSession(sessionId) {
|
|
339
|
+
const sm = await this._getSessionManager()
|
|
340
|
+
if (sessionId) {
|
|
341
|
+
const s = await sm.load(sessionId)
|
|
342
|
+
if (s) {
|
|
343
|
+
this.currentSession = s
|
|
344
|
+
return s
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (!this.currentSession) {
|
|
348
|
+
this.currentSession = await sm.create()
|
|
349
|
+
}
|
|
350
|
+
return this.currentSession
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** 构建工具列表(remote 模式:handler 转发给客户端执行) */
|
|
354
|
+
_buildTools() {
|
|
355
|
+
// 客户端下发优先;否则用内置工具定义
|
|
356
|
+
const defs = this.toolDefs || builtinTools.map((t) => ({
|
|
357
|
+
name: t.name,
|
|
358
|
+
description: t.description,
|
|
359
|
+
parameters: t.parameters,
|
|
360
|
+
}))
|
|
361
|
+
return defs.map((t) => ({
|
|
362
|
+
name: t.name,
|
|
363
|
+
description: t.description || '',
|
|
364
|
+
parameters: t.parameters || { type: 'object', properties: {} },
|
|
365
|
+
handler: async (input) => {
|
|
366
|
+
const toolCallId = `call_${randomUUID()}`
|
|
367
|
+
this._sendEvent('event/toolCall', {
|
|
368
|
+
toolCall: { id: toolCallId, name: t.name, input: input || {} },
|
|
369
|
+
})
|
|
370
|
+
return new Promise((resolve) => {
|
|
371
|
+
this.pendingToolCalls.set(toolCallId, resolve)
|
|
372
|
+
})
|
|
373
|
+
},
|
|
374
|
+
}))
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async _getEngine() {
|
|
378
|
+
if (this.engine) return this.engine
|
|
379
|
+
if (!this.config.apiBase) {
|
|
380
|
+
throw this._err(4, '未设置 apiBase(config/set 或 --api-base)')
|
|
381
|
+
}
|
|
382
|
+
if (!this.config.model) {
|
|
383
|
+
throw this._err(4, '未设置 model(config/set 或 --model)')
|
|
384
|
+
}
|
|
385
|
+
return this._rebuildEngine()
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
_rebuildEngine() {
|
|
389
|
+
const apiBase = this.config.apiBase
|
|
390
|
+
const localServer = isLocalLlmServer(apiBase)
|
|
391
|
+
const apiKey = this.config.apiKey
|
|
392
|
+
if (!localServer && !apiKey) {
|
|
393
|
+
console.error('[cc-node-stdio] 警告: 非本地服务未设置 apiKey')
|
|
394
|
+
}
|
|
395
|
+
this.engine = new QueryEngine(
|
|
396
|
+
new QueryEngineConfig({
|
|
397
|
+
cwd: resolve(this.cliArgs.cwd || process.cwd()),
|
|
398
|
+
model: this.config.model,
|
|
399
|
+
apiBase,
|
|
400
|
+
apiKey,
|
|
401
|
+
systemPrompt: this.config.systemPrompt,
|
|
402
|
+
maxTurns: this.config.maxTurns,
|
|
403
|
+
permissionMode: 'always-allow', // 安全底线仍检查;执行确认由客户端负责
|
|
404
|
+
tools: this._buildTools(),
|
|
405
|
+
verbose: false,
|
|
406
|
+
costTracker: new CostTracker({ model: this.config.model }),
|
|
407
|
+
onDelta: (evt) => {
|
|
408
|
+
this._sendEvent('event/delta', { kind: evt.type, text: evt.text })
|
|
409
|
+
},
|
|
410
|
+
})
|
|
411
|
+
)
|
|
412
|
+
return this.engine
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
_err(code, message) {
|
|
416
|
+
const e = new Error(message)
|
|
417
|
+
e.code = code
|
|
418
|
+
return e
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** 直接运行:node src/stdio/server.js */
|
|
423
|
+
if (process.argv[1] && process.argv[1].endsWith('server.js')) {
|
|
424
|
+
const server = new StdioServer()
|
|
425
|
+
server.start()
|
|
426
|
+
}
|
package/src/tools/index.js
CHANGED
|
@@ -11,8 +11,8 @@ import { webFetchTool } from './web-fetch.js'
|
|
|
11
11
|
import { webSearchTool } from './web-search.js'
|
|
12
12
|
import { askUserTool } from './ask-user.js'
|
|
13
13
|
import { gitTool } from './git-tool.js'
|
|
14
|
-
// QQ Bot
|
|
15
|
-
import {
|
|
14
|
+
// Telegram 工具(替代原 QQ Bot 工具,QQ 通道已放弃)
|
|
15
|
+
import { telegramTools } from './telegram-tools.js'
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* 所有内置工具列表
|
|
@@ -28,7 +28,7 @@ export const builtinTools = [
|
|
|
28
28
|
webSearchTool,
|
|
29
29
|
askUserTool,
|
|
30
30
|
gitTool,
|
|
31
|
-
...
|
|
31
|
+
...telegramTools
|
|
32
32
|
]
|
|
33
33
|
|
|
34
34
|
/**
|