@dcrays/dcgchat-test 0.6.8 → 0.6.12

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 (60) hide show
  1. package/README.md +167 -0
  2. package/index.ts +35 -0
  3. package/openclaw.plugin.json +12 -41
  4. package/package.json +35 -8
  5. package/src/agent.ts +568 -0
  6. package/src/bot.ts +646 -0
  7. package/src/channel/index.ts +329 -0
  8. package/src/channel/outboundMedia.ts +144 -0
  9. package/src/channel/outboundTarget.ts +62 -0
  10. package/src/channel/uploadMediaUrl.ts +76 -0
  11. package/src/cron/message.ts +114 -0
  12. package/src/cron/params.ts +164 -0
  13. package/src/cron/toolGuard.ts +466 -0
  14. package/src/cron/types.ts +15 -0
  15. package/src/gateway/index.ts +430 -0
  16. package/src/gateway/security.ts +95 -0
  17. package/src/gateway/socket.ts +256 -0
  18. package/src/libs/ali-oss-6.23.0.tgz +0 -0
  19. package/src/libs/axios-1.13.6.tgz +0 -0
  20. package/src/libs/md5-2.3.0.tgz +0 -0
  21. package/src/libs/mime-types-3.0.2.tgz +0 -0
  22. package/src/libs/unzipper-0.12.3.tgz +0 -0
  23. package/src/libs/ws-8.19.0.tgz +0 -0
  24. package/src/monitor.ts +269 -0
  25. package/src/request/api.ts +80 -0
  26. package/src/request/oss.ts +200 -0
  27. package/src/request/request.ts +191 -0
  28. package/src/request/userInfo.ts +44 -0
  29. package/src/session.ts +28 -0
  30. package/src/skill.ts +114 -0
  31. package/src/tool.ts +603 -0
  32. package/src/tools/messageTool.ts +334 -0
  33. package/src/tools/toolCallGuard.ts +327 -0
  34. package/src/transport.ts +217 -0
  35. package/src/types.ts +139 -0
  36. package/src/utils/agentErrors.ts +116 -0
  37. package/src/utils/constant.ts +60 -0
  38. package/src/utils/env-config.ts +19 -0
  39. package/src/utils/formatLlmInputEvent.ts +48 -0
  40. package/src/utils/gatewayMsgHandler.ts +147 -0
  41. package/src/utils/global.ts +309 -0
  42. package/src/utils/inboundTurnState.ts +66 -0
  43. package/src/utils/log.ts +77 -0
  44. package/src/utils/mediaAttached.ts +244 -0
  45. package/src/utils/mediaEmitter.ts +54 -0
  46. package/src/utils/outboundAssistantText.ts +117 -0
  47. package/src/utils/params.ts +104 -0
  48. package/src/utils/passTxt.ts +4 -0
  49. package/src/utils/resolveRegisterConfig.ts +33 -0
  50. package/src/utils/searchFile.ts +228 -0
  51. package/src/utils/sessionState.ts +137 -0
  52. package/src/utils/sessionTermination.ts +228 -0
  53. package/src/utils/streamMerge.ts +150 -0
  54. package/src/utils/subagentRunMap.ts +71 -0
  55. package/src/utils/undiciFetchInterceptor.ts +346 -0
  56. package/src/utils/workspaceFilePaths.ts +18 -0
  57. package/src/utils/wsMessageHandler.ts +124 -0
  58. package/src/utils/zipExtract.ts +97 -0
  59. package/src/utils/zipPath.ts +24 -0
  60. package/index.js +0 -302
@@ -0,0 +1,430 @@
1
+ // Gateway connection handler - connects to local OpenClaw gateway
2
+ import { WebSocket } from 'ws'
3
+ import crypto from 'crypto'
4
+ import fs from 'node:fs'
5
+ import path from 'node:path'
6
+ import { deriveDeviceIdFromPublicKey, publicKeyRawBase64UrlFromPem, buildDeviceAuthPayloadV3, signDevicePayload } from './security.js'
7
+ import { dcgLogger } from '../utils/log.js'
8
+ import { getWorkspaceDir } from '../utils/global.js'
9
+ import { handleGatewayEventMessage } from '../utils/gatewayMsgHandler.js'
10
+ import { GATEWAY_PROTOCOL_VERSION } from '../utils/constant.js'
11
+
12
+ export interface GatewayEvent {
13
+ type: string
14
+ payload?: Record<string, unknown>
15
+ seq?: number
16
+ }
17
+
18
+ export interface GatewayHelloOk {
19
+ server: {
20
+ connId: string
21
+ }
22
+ features: {
23
+ methods: string[]
24
+ events: string[]
25
+ }
26
+ policy?: {
27
+ tickIntervalMs?: number
28
+ }
29
+ auth?: {
30
+ deviceToken?: string
31
+ role?: string
32
+ scopes?: string[]
33
+ }
34
+ }
35
+
36
+ export interface GatewayResponse {
37
+ type: 'res'
38
+ id: string
39
+ ok: boolean
40
+ /** 多数 RPC 成功时的返回值 */
41
+ result?: unknown
42
+ /** connect 及部分响应使用 payload(见 docs/gateway/protocol.md) */
43
+ payload?: unknown
44
+ error?: {
45
+ code: string
46
+ message: string
47
+ details?: Record<string, unknown>
48
+ }
49
+ }
50
+
51
+ export interface GatewayConfig {
52
+ url: string
53
+ token: string
54
+ role: string
55
+ scopes: string[]
56
+ reconnectInterval?: number
57
+ maxReconnectAttempts?: number
58
+ }
59
+
60
+ /**
61
+ * Gateway connection handler
62
+ */
63
+ export class GatewayConnection {
64
+ private ws: WebSocket | null = null
65
+ private config: Required<GatewayConfig>
66
+ private deviceId: string
67
+ private privateKeyPem: string
68
+ private publicKeyB64Url: string
69
+ private connected: boolean = false
70
+ private connId: string | null = null
71
+ /** 服务端 connect.challenge 提供的 nonce,须与签名载荷一致 */
72
+ private connectChallengeNonce: string | null = null
73
+ private connectSent: boolean = false
74
+ private pendingRpcById: Map<string, (response: GatewayResponse) => void> = new Map()
75
+ private eventHandlers: Set<(event: GatewayEvent) => void> = new Set()
76
+
77
+ constructor(config: GatewayConfig) {
78
+ this.config = {
79
+ url: config.url,
80
+ token: config.token,
81
+ role: config.role || 'operator',
82
+ scopes: config.scopes || ['operator.admin'],
83
+ reconnectInterval: config.reconnectInterval || 5000,
84
+ maxReconnectAttempts: config.maxReconnectAttempts || 10
85
+ }
86
+
87
+ const identity = this.loadOrCreateDeviceIdentity()
88
+ // 必须与公钥指纹一致(deriveDeviceIdFromPublicKey),不可用随机 UUID
89
+ this.deviceId = identity.deviceId
90
+ this.privateKeyPem = identity.privateKeyPem
91
+ this.publicKeyB64Url = identity.publicKeyB64Url
92
+ }
93
+
94
+ private loadOrCreateDeviceIdentity() {
95
+ const workspaceDir = getWorkspaceDir()
96
+ const stateDir = path.join(workspaceDir, '.state')
97
+ const deviceFile = path.join(stateDir, 'device.json')
98
+
99
+ // Try to load existing identity
100
+ if (fs.existsSync(deviceFile)) {
101
+ const stored = JSON.parse(fs.readFileSync(deviceFile, 'utf8'))
102
+ if (stored.deviceId && stored.publicKeyPem && stored.privateKeyPem) {
103
+ const derivedId = deriveDeviceIdFromPublicKey(stored.publicKeyPem)
104
+ const deviceId = derivedId !== stored.deviceId ? derivedId : stored.deviceId
105
+ if (derivedId !== stored.deviceId) {
106
+ try {
107
+ fs.writeFileSync(deviceFile, JSON.stringify({ ...stored, deviceId: derivedId }, null, 2))
108
+ } catch {
109
+ /* keep in-memory fixed id only */
110
+ }
111
+ }
112
+ return {
113
+ deviceId,
114
+ publicKeyPem: stored.publicKeyPem,
115
+ privateKeyPem: stored.privateKeyPem,
116
+ publicKeyB64Url: publicKeyRawBase64UrlFromPem(stored.publicKeyPem)
117
+ }
118
+ }
119
+ }
120
+
121
+ // Create new identity
122
+ const keyPair = crypto.generateKeyPairSync('ed25519')
123
+ const publicKeyPem = keyPair.publicKey.export({ type: 'spki', format: 'pem' }).toString()
124
+ const privateKeyPem = keyPair.privateKey.export({ type: 'pkcs8', format: 'pem' }).toString()
125
+ const deviceId = deriveDeviceIdFromPublicKey(publicKeyPem)
126
+ const publicKeyB64Url = publicKeyRawBase64UrlFromPem(publicKeyPem)
127
+
128
+ // Ensure directory exists
129
+ if (!fs.existsSync(stateDir)) {
130
+ fs.mkdirSync(stateDir, { recursive: true })
131
+ }
132
+
133
+ // Save identity
134
+ fs.writeFileSync(
135
+ deviceFile,
136
+ JSON.stringify(
137
+ {
138
+ version: 1,
139
+ deviceId,
140
+ publicKeyPem,
141
+ privateKeyPem,
142
+ createdAtMs: Date.now()
143
+ },
144
+ null,
145
+ 2
146
+ )
147
+ )
148
+ fs.chmodSync(deviceFile, 0o600)
149
+
150
+ return { deviceId, publicKeyPem, privateKeyPem, publicKeyB64Url }
151
+ }
152
+
153
+ /**
154
+ * Connect to the gateway
155
+ */
156
+ async connect(): Promise<GatewayHelloOk> {
157
+ return new Promise((resolve, reject) => {
158
+ this.connectChallengeNonce = null
159
+ this.connectSent = false
160
+ this.ws = new WebSocket(this.config.url)
161
+
162
+ let handshakeSettled = false
163
+ const finishHandshake = (fn: () => void) => {
164
+ if (handshakeSettled) return
165
+ handshakeSettled = true
166
+ clearTimeout(timeout)
167
+ fn()
168
+ }
169
+
170
+ const timeout = setTimeout(() => {
171
+ finishHandshake(() => reject(new Error('Gateway connection timeout')))
172
+ }, 15000)
173
+
174
+ this.ws.on('open', () => {
175
+ dcgLogger('Gateway connection opened(等待 connect.challenge)')
176
+ })
177
+
178
+ this.ws.on('message', (data: WebSocket.Data) => {
179
+ try {
180
+ const msg = JSON.parse(data.toString())
181
+ this.handleMessage(
182
+ msg,
183
+ (hello) => finishHandshake(() => resolve(hello)),
184
+ (err) => finishHandshake(() => reject(err)),
185
+ timeout
186
+ )
187
+ } catch (err) {
188
+ dcgLogger(`[Gateway=>插件]-[消息解析失败]: ${err}`, 'error')
189
+ }
190
+ })
191
+
192
+ this.ws.on('close', () => {
193
+ this.connected = false
194
+ finishHandshake(() => reject(new Error('Gateway 在握手完成前关闭了连接')))
195
+ })
196
+
197
+ this.ws.on('error', (err: Error) => {
198
+ dcgLogger(`Gateway 连接错误: ${err}`, 'error')
199
+ finishHandshake(() => reject(err))
200
+ })
201
+ })
202
+ }
203
+
204
+ /**
205
+ * Send initial connect request
206
+ */
207
+ private sendConnect(): void {
208
+ if (this.connectSent) return
209
+ const nonce = this.connectChallengeNonce?.trim() ?? ''
210
+ if (!nonce) return
211
+
212
+ this.connectSent = true
213
+ const signedAtMs = Date.now()
214
+ const platform = process.platform
215
+
216
+ const payload = buildDeviceAuthPayloadV3({
217
+ deviceId: this.deviceId,
218
+ clientId: 'gateway-client',
219
+ clientMode: 'backend',
220
+ role: this.config.role,
221
+ scopes: this.config.scopes,
222
+ signedAtMs,
223
+ token: this.config.token,
224
+ nonce,
225
+ platform,
226
+ deviceFamily: ''
227
+ })
228
+
229
+ const signature = signDevicePayload(this.privateKeyPem, payload)
230
+ const _params = JSON.stringify({
231
+ type: 'req',
232
+ id: '1',
233
+ method: 'connect',
234
+ params: {
235
+ minProtocol: GATEWAY_PROTOCOL_VERSION,
236
+ maxProtocol: GATEWAY_PROTOCOL_VERSION,
237
+ client: {
238
+ id: 'gateway-client',
239
+ version: '1.0.0',
240
+ platform,
241
+ mode: 'backend'
242
+ },
243
+ auth: { token: this.config.token },
244
+ role: this.config.role,
245
+ scopes: this.config.scopes,
246
+ device: {
247
+ id: this.deviceId,
248
+ publicKey: this.publicKeyB64Url,
249
+ signature,
250
+ signedAt: signedAtMs,
251
+ nonce
252
+ }
253
+ }
254
+ })
255
+ this.ws?.send(_params)
256
+ // dcgLogger(`[插件=>Gateway]-[sendConnect]: ${_params}`)
257
+ }
258
+
259
+ /**
260
+ * Handle incoming messages
261
+ */
262
+ private mapHelloPayloadToHelloOk(payload: Record<string, unknown>): GatewayHelloOk {
263
+ const serverRaw = payload.server as Record<string, unknown> | undefined
264
+ const featuresRaw = payload.features as Record<string, unknown> | undefined
265
+ return {
266
+ server: { connId: typeof serverRaw?.connId === 'string' ? serverRaw.connId : '' },
267
+ features: {
268
+ methods: Array.isArray(featuresRaw?.methods) ? (featuresRaw.methods as string[]) : [],
269
+ events: Array.isArray(featuresRaw?.events) ? (featuresRaw.events as string[]) : []
270
+ },
271
+ policy: payload.policy as GatewayHelloOk['policy'],
272
+ auth: payload.auth as GatewayHelloOk['auth']
273
+ }
274
+ }
275
+
276
+ private acknowledgeGatewayHelloOk(timeout: NodeJS.Timeout): void {
277
+ clearTimeout(timeout)
278
+ }
279
+
280
+ private handleMessage(
281
+ msg: Record<string, any>,
282
+ resolveHello: (helloOk: GatewayHelloOk) => void,
283
+ rejectHello: (err: Error) => void,
284
+ timeout: NodeJS.Timeout
285
+ ): void {
286
+ const msgType = msg.type as string | undefined
287
+
288
+ if (msg.type === 'event' && msg.event === 'connect.challenge') {
289
+ const payload = msg.payload as Record<string, unknown> | undefined
290
+ const nonce = typeof payload?.nonce === 'string' ? payload.nonce.trim() : ''
291
+ if (!nonce) {
292
+ rejectHello(new Error('connect.challenge 缺少 nonce'))
293
+ return
294
+ }
295
+ this.connectChallengeNonce = nonce
296
+ this.sendConnect()
297
+ return
298
+ }
299
+
300
+ // 协议 v4:握手成功为 type:"res" + payload.type:"hello-ok"(见 openclaw docs/gateway/protocol.md)
301
+ if (msg.type === 'res' && !this.connected) {
302
+ const ok = msg.ok === true
303
+ const inner = msg.payload as Record<string, unknown> | undefined
304
+ if (ok && inner && inner.type === 'hello-ok') {
305
+ this.connected = true
306
+ const serverRaw = inner.server as Record<string, unknown> | undefined
307
+ this.connId = typeof serverRaw?.connId === 'string' ? serverRaw.connId : null
308
+ this.acknowledgeGatewayHelloOk(timeout)
309
+ resolveHello(this.mapHelloPayloadToHelloOk(inner))
310
+ return
311
+ }
312
+ if (msg.ok === false) {
313
+ const errObj = msg.error as Record<string, unknown> | undefined
314
+ const message = (typeof errObj?.message === 'string' && errObj.message) || 'Gateway connect 被拒绝'
315
+ clearTimeout(timeout)
316
+ rejectHello(new Error(message))
317
+ return
318
+ }
319
+ }
320
+
321
+ // 旧式或其它实现:顶层 hello-ok
322
+ if (msgType === 'hello-ok' || (!msgType && msg.server)) {
323
+ this.connected = true
324
+ this.connId = ((msg.server as Record<string, unknown>)?.connId as string) || null
325
+ this.acknowledgeGatewayHelloOk(timeout)
326
+ resolveHello(msg as unknown as GatewayHelloOk)
327
+ return
328
+ }
329
+ if (msg.type === 'res') {
330
+ const handler = this.pendingRpcById.get(msg.id as string)
331
+ if (handler) {
332
+ this.pendingRpcById.delete(msg.id as string)
333
+ handler(msg as unknown as GatewayResponse)
334
+ }
335
+ return
336
+ }
337
+
338
+ if (msg.type === 'event') {
339
+ void handleGatewayEventMessage(msg)
340
+ .then((event) => {
341
+ this.eventHandlers.forEach((h) => h(event))
342
+ })
343
+ .catch((err) => {
344
+ dcgLogger(`[Gateway] event 处理异步失败: ${String(err)}`, 'error')
345
+ })
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Call a gateway method
351
+ */
352
+ async callMethod<T = unknown>(method: string, params: Record<string, unknown> = {}, opts?: { timeoutMs?: number }): Promise<T> {
353
+ const id = crypto.randomUUID()
354
+ const timeoutMs = opts?.timeoutMs ?? 30000
355
+
356
+ return new Promise((resolve, reject) => {
357
+ if (!this.connected || !this.ws) {
358
+ reject(new Error('Not connected to gateway'))
359
+ return
360
+ }
361
+
362
+ const timeout = setTimeout(() => {
363
+ this.pendingRpcById.delete(id)
364
+ reject(new Error('Method call timeout'))
365
+ }, timeoutMs)
366
+
367
+ this.pendingRpcById.set(id, (response) => {
368
+ clearTimeout(timeout)
369
+ if (response.ok) {
370
+ const body = response.result !== undefined ? response.result : (response as GatewayResponse).payload
371
+ resolve(body as T)
372
+ } else {
373
+ reject(new Error(response.error?.message || 'Method call failed'))
374
+ }
375
+ })
376
+ const _params = JSON.stringify({
377
+ type: 'req',
378
+ id,
379
+ method,
380
+ params
381
+ })
382
+ this.ws.send(_params)
383
+ dcgLogger(`[插件=>Gateway]-[callMethod]: ${_params}`)
384
+ })
385
+ }
386
+
387
+ /**
388
+ * Register event handler
389
+ */
390
+ onEvent(handler: (event: GatewayEvent) => void): () => void {
391
+ this.eventHandlers.add(handler)
392
+ return () => this.eventHandlers.delete(handler)
393
+ }
394
+
395
+ /**
396
+ * Close connection
397
+ */
398
+ close(): void {
399
+ this.ws?.close(1000, 'Plugin stopped')
400
+ this.connected = false
401
+ }
402
+
403
+ /**
404
+ * Check if connected
405
+ */
406
+ isConnected(): boolean {
407
+ return this.connected
408
+ }
409
+
410
+ /**
411
+ * Get connection ID
412
+ */
413
+ getConnId(): string | null {
414
+ return this.connId
415
+ }
416
+
417
+ /**
418
+ * Get the WebSocket instance (for external use)
419
+ */
420
+ getWebSocket(): WebSocket | null {
421
+ return this.ws
422
+ }
423
+
424
+ /**
425
+ * Ping the gateway
426
+ */
427
+ ping(): void {
428
+ this.ws?.ping()
429
+ }
430
+ }
@@ -0,0 +1,95 @@
1
+ // Security utilities for the tunnel plugin
2
+ import crypto from 'crypto'
3
+
4
+ // ED25519 SubjectPublicKeyInfo prefix (must match openclaw gateway `ED25519_SPKI_PREFIX`)
5
+ const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex')
6
+
7
+ /**
8
+ * Raw 32-byte Ed25519 public key bytes from PEM (same rules as openclaw `derivePublicKeyRaw`).
9
+ */
10
+ export function derivePublicKeyRawFromPem(publicKeyPem: string): Buffer {
11
+ const spki = crypto.createPublicKey(publicKeyPem).export({ type: 'spki', format: 'der' }) as Buffer
12
+ if (spki.length === ED25519_SPKI_PREFIX.length + 32 && spki.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)) {
13
+ return spki.subarray(ED25519_SPKI_PREFIX.length)
14
+ }
15
+ return spki
16
+ }
17
+
18
+ /**
19
+ * Base64url encode (no padding)
20
+ */
21
+ export function base64UrlEncode(buffer: Buffer): string {
22
+ return buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
23
+ }
24
+
25
+ /** Wire-format device public key (base64url of raw 32 bytes), matches gateway expectations. */
26
+ export function publicKeyRawBase64UrlFromPem(publicKeyPem: string): string {
27
+ return base64UrlEncode(derivePublicKeyRawFromPem(publicKeyPem))
28
+ }
29
+
30
+ /**
31
+ * Derive device ID from ED25519 public key
32
+ * Device ID = sha256(rawPublicKey)
33
+ */
34
+ export function deriveDeviceIdFromPublicKey(publicKeyPem: string): string {
35
+ const rawPublicKey = derivePublicKeyRawFromPem(publicKeyPem)
36
+ return crypto.createHash('sha256').update(rawPublicKey).digest('hex')
37
+ }
38
+
39
+ /**
40
+ * Sign device payload
41
+ */
42
+ export function signDevicePayload(privateKeyPem: string, payload: string, encoding: 'base64' | 'base64url' = 'base64url'): string {
43
+ const key = crypto.createPrivateKey(privateKeyPem)
44
+ const sig = crypto.sign(null, Buffer.from(payload, 'utf8'), key)
45
+ return encoding === 'base64url' ? base64UrlEncode(sig) : sig.toString('base64')
46
+ }
47
+ function normalizeTrimmedMetadata(value: unknown): string {
48
+ if (typeof value !== 'string') return ''
49
+ const trimmed = value.trim()
50
+ return trimmed ? trimmed : ''
51
+ }
52
+
53
+ function toLowerAscii(input: string): string {
54
+ return input.replace(/[A-Z]/g, (char) => String.fromCharCode(char.charCodeAt(0) + 32))
55
+ }
56
+
57
+ function normalizeDeviceMetadataForAuth(value: unknown): string {
58
+ const trimmed = normalizeTrimmedMetadata(value)
59
+ if (!trimmed) return ''
60
+ return toLowerAscii(trimmed)
61
+ }
62
+
63
+ /**
64
+ * Device authentication payload v3(与 openclaw `buildDeviceAuthPayloadV3` 一致)
65
+ */
66
+ export function buildDeviceAuthPayloadV3(params: {
67
+ deviceId: string
68
+ clientId: string
69
+ clientMode: string
70
+ role: string
71
+ scopes: string[]
72
+ signedAtMs: number
73
+ token: string
74
+ nonce: string
75
+ platform?: string
76
+ deviceFamily?: string
77
+ }): string {
78
+ const scopes = params.scopes.join(',')
79
+ const token = params.token ?? ''
80
+ const platform = normalizeDeviceMetadataForAuth(params.platform ?? '')
81
+ const deviceFamily = normalizeDeviceMetadataForAuth(params.deviceFamily ?? '')
82
+ return [
83
+ 'v3',
84
+ params.deviceId,
85
+ params.clientId,
86
+ params.clientMode,
87
+ params.role,
88
+ scopes,
89
+ String(params.signedAtMs),
90
+ token,
91
+ params.nonce,
92
+ platform,
93
+ deviceFamily
94
+ ].join('|')
95
+ }