@dcrays/dcgchat 0.4.27 → 0.5.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.
- package/index.js +292 -0
- package/openclaw.plugin.json +17 -1
- package/package.json +18 -13
- package/schemas/gateway-cron-finished.payload.json +39 -0
- package/index.ts +0 -26
- package/src/agent.ts +0 -128
- package/src/bot.ts +0 -500
- package/src/channel.ts +0 -470
- package/src/cron.ts +0 -194
- package/src/cronToolCall.ts +0 -202
- package/src/gateway/index.ts +0 -447
- package/src/gateway/security.ts +0 -95
- package/src/gateway/socket.ts +0 -285
- package/src/libs/ali-oss-6.23.0.tgz +0 -0
- package/src/libs/axios-1.13.6.tgz +0 -0
- package/src/libs/md5-2.3.0.tgz +0 -0
- package/src/libs/mime-types-3.0.2.tgz +0 -0
- package/src/libs/unzipper-0.12.3.tgz +0 -0
- package/src/libs/ws-8.19.0.tgz +0 -0
- package/src/monitor.ts +0 -165
- package/src/request/api.ts +0 -70
- package/src/request/oss.ts +0 -61
- package/src/request/request.ts +0 -192
- package/src/request/userInfo.ts +0 -99
- package/src/session.ts +0 -19
- package/src/sessionTermination.ts +0 -154
- package/src/skill.ts +0 -151
- package/src/tool.ts +0 -422
- package/src/tools/messageTool.ts +0 -224
- package/src/transport.ts +0 -203
- package/src/types.ts +0 -139
- package/src/utils/constant.ts +0 -7
- package/src/utils/gatewayMsgHanlder.ts +0 -55
- package/src/utils/global.ts +0 -160
- package/src/utils/log.ts +0 -15
- package/src/utils/params.ts +0 -88
- package/src/utils/searchFile.ts +0 -228
- package/src/utils/wsMessageHandler.ts +0 -64
- package/src/utils/zipExtract.ts +0 -97
- package/src/utils/zipPath.ts +0 -24
package/src/gateway/security.ts
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
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
|
-
}
|
package/src/gateway/socket.ts
DELETED
|
@@ -1,285 +0,0 @@
|
|
|
1
|
-
import type { OpenClawConfig } from 'openclaw/plugin-sdk'
|
|
2
|
-
import { getOpenClawConfig } from '../utils/global.js'
|
|
3
|
-
import { GatewayConnection, type GatewayConfig } from './index.js'
|
|
4
|
-
import { dcgLogger } from '../utils/log.js'
|
|
5
|
-
|
|
6
|
-
/** 与 gateway-methods 中 registerGatewayMethod 名称一致(供引用) */
|
|
7
|
-
export const DCGCHAT_GATEWAY_METHODS = ['dcgchat.cron.status', 'dcgchat.cron.add', 'dcgchat.cron.list', 'dcgchat.cron.remove'] as const
|
|
8
|
-
|
|
9
|
-
const PING_INTERVAL_MS = 30_000
|
|
10
|
-
const RECONNECT_DELAY_MS = 5_000
|
|
11
|
-
|
|
12
|
-
type OpenClawGatewaySection = {
|
|
13
|
-
port?: number
|
|
14
|
-
bind?: string
|
|
15
|
-
tls?: { enabled?: boolean }
|
|
16
|
-
auth?: { mode?: string; token?: string }
|
|
17
|
-
remote?: { url?: string }
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function getGatewaySection(cfg: OpenClawConfig): OpenClawGatewaySection | undefined {
|
|
21
|
-
return (cfg as OpenClawConfig & { gateway?: OpenClawGatewaySection }).gateway
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* 从 OpenClaw 配置解析本地/远程 Gateway WebSocket 与 token。
|
|
26
|
-
* 可用环境变量覆盖:OPENCLAW_GATEWAY_URL、OPENCLAW_GATEWAY_TOKEN。
|
|
27
|
-
*/
|
|
28
|
-
export function resolveGatewayClientConfig(cfg: OpenClawConfig | null): GatewayConfig {
|
|
29
|
-
if (!cfg) {
|
|
30
|
-
throw new Error('OpenClaw 配置未初始化(需先完成插件 register)')
|
|
31
|
-
}
|
|
32
|
-
const g = getGatewaySection(cfg)
|
|
33
|
-
const token = process.env.OPENCLAW_GATEWAY_TOKEN || g?.auth?.token || ''
|
|
34
|
-
if (!token) {
|
|
35
|
-
throw new Error('缺少 Gateway token:请在 openclaw.json 的 gateway.auth.token 设置,或设置环境变量 OPENCLAW_GATEWAY_TOKEN')
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const scheme = g?.tls?.enabled === true ? 'wss' : 'ws'
|
|
39
|
-
const url = process.env.OPENCLAW_GATEWAY_URL || g?.remote?.url || `${scheme}://127.0.0.1:${g?.port ?? 18789}`
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
url,
|
|
43
|
-
token,
|
|
44
|
-
role: 'operator',
|
|
45
|
-
scopes: ['operator.admin']
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function resolveConfigSafe(): GatewayConfig | null {
|
|
50
|
-
try {
|
|
51
|
-
return resolveGatewayClientConfig(getOpenClawConfig())
|
|
52
|
-
} catch (e) {
|
|
53
|
-
dcgLogger(`Gateway 持久连接未启动: ${e}`, 'error')
|
|
54
|
-
return null
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* 解析要调用的 Gateway 方法。
|
|
60
|
-
* - 纯文本:整段为 method,params 为空
|
|
61
|
-
* - JSON:`{ "method": "dcgchat.cron.list", "params": { "includeDisabled": true } }`
|
|
62
|
-
*/
|
|
63
|
-
export function parseGatewayRpcMessage(message: string): { method: string; params: Record<string, unknown> } {
|
|
64
|
-
const trimmed = message.trim()
|
|
65
|
-
if (!trimmed) {
|
|
66
|
-
throw new Error('message 为空')
|
|
67
|
-
}
|
|
68
|
-
if (!trimmed.startsWith('{')) {
|
|
69
|
-
return { method: trimmed, params: {} }
|
|
70
|
-
}
|
|
71
|
-
let parsed: unknown
|
|
72
|
-
try {
|
|
73
|
-
parsed = JSON.parse(trimmed)
|
|
74
|
-
} catch {
|
|
75
|
-
throw new Error('message 不是合法 JSON')
|
|
76
|
-
}
|
|
77
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
78
|
-
throw new Error('message JSON 必须是对象')
|
|
79
|
-
}
|
|
80
|
-
const o = parsed as Record<string, unknown>
|
|
81
|
-
if (typeof o.method !== 'string' || !o.method.trim()) {
|
|
82
|
-
throw new Error('message JSON 须包含非空字符串字段 method')
|
|
83
|
-
}
|
|
84
|
-
if (o.params != null && (typeof o.params !== 'object' || Array.isArray(o.params))) {
|
|
85
|
-
throw new Error('message JSON 的 params 必须是对象')
|
|
86
|
-
}
|
|
87
|
-
return {
|
|
88
|
-
method: o.method.trim(),
|
|
89
|
-
params: (o.params as Record<string, unknown>) || {}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** 任意 method + params,由调用方决定载荷 */
|
|
94
|
-
export type GatewayRpcPayload = {
|
|
95
|
-
method: string
|
|
96
|
-
params?: Record<string, unknown>
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
let persistentConn: GatewayConnection | null = null
|
|
100
|
-
let pingTimer: ReturnType<typeof setInterval> | null = null
|
|
101
|
-
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
|
102
|
-
/** register 可能被调用多次,只保留一个「延迟首次连接」定时器,避免同一时刻触发两次 connect */
|
|
103
|
-
let startupConnectTimer: ReturnType<typeof setTimeout> | null = null
|
|
104
|
-
let connectInFlight = false
|
|
105
|
-
let socketStopped = true
|
|
106
|
-
/** 用于忽略「已被替换的旧连接」上的 close/error */
|
|
107
|
-
let socketGeneration = 0
|
|
108
|
-
|
|
109
|
-
function clearPingTimer(): void {
|
|
110
|
-
if (pingTimer) {
|
|
111
|
-
clearInterval(pingTimer)
|
|
112
|
-
pingTimer = null
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function clearReconnectTimer(): void {
|
|
117
|
-
if (reconnectTimer) {
|
|
118
|
-
clearTimeout(reconnectTimer)
|
|
119
|
-
reconnectTimer = null
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function clearStartupConnectTimer(): void {
|
|
124
|
-
if (startupConnectTimer) {
|
|
125
|
-
clearTimeout(startupConnectTimer)
|
|
126
|
-
startupConnectTimer = null
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function startPingTimer(gw: GatewayConnection): void {
|
|
131
|
-
clearPingTimer()
|
|
132
|
-
pingTimer = setInterval(() => {
|
|
133
|
-
if (!gw.isConnected()) return
|
|
134
|
-
try {
|
|
135
|
-
gw.ping()
|
|
136
|
-
} catch (e) {
|
|
137
|
-
dcgLogger(`Gateway ping 失败: ${e}`, 'error')
|
|
138
|
-
}
|
|
139
|
-
}, PING_INTERVAL_MS)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function scheduleReconnect(): void {
|
|
143
|
-
if (socketStopped || reconnectTimer) return
|
|
144
|
-
reconnectTimer = setTimeout(() => {
|
|
145
|
-
reconnectTimer = null
|
|
146
|
-
void connectPersistentGateway()
|
|
147
|
-
}, RECONNECT_DELAY_MS)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function attachSocketLifecycle(gw: GatewayConnection, generation: number): void {
|
|
151
|
-
const ws = gw.getWebSocket()
|
|
152
|
-
if (!ws) return
|
|
153
|
-
|
|
154
|
-
const onDown = () => {
|
|
155
|
-
if (generation !== socketGeneration) return
|
|
156
|
-
clearPingTimer()
|
|
157
|
-
if (persistentConn === gw) {
|
|
158
|
-
persistentConn = null
|
|
159
|
-
}
|
|
160
|
-
dcgLogger('Gateway WebSocket 已断开,将在 5s 后重连', 'error')
|
|
161
|
-
if (!socketStopped) {
|
|
162
|
-
scheduleReconnect()
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
ws.once('close', onDown)
|
|
167
|
-
ws.once('error', onDown)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
async function connectPersistentGateway(): Promise<void> {
|
|
171
|
-
if (socketStopped) return
|
|
172
|
-
if (persistentConn?.isConnected()) return
|
|
173
|
-
if (connectInFlight) return
|
|
174
|
-
|
|
175
|
-
const cfg = resolveConfigSafe()
|
|
176
|
-
if (!cfg) return
|
|
177
|
-
|
|
178
|
-
connectInFlight = true
|
|
179
|
-
try {
|
|
180
|
-
socketGeneration += 1
|
|
181
|
-
const generation = socketGeneration
|
|
182
|
-
|
|
183
|
-
if (persistentConn) {
|
|
184
|
-
try {
|
|
185
|
-
persistentConn.close()
|
|
186
|
-
} catch {
|
|
187
|
-
/* ignore */
|
|
188
|
-
}
|
|
189
|
-
persistentConn = null
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const gw = new GatewayConnection(cfg)
|
|
193
|
-
await gw.connect()
|
|
194
|
-
if (socketStopped) {
|
|
195
|
-
try {
|
|
196
|
-
gw.close()
|
|
197
|
-
} catch {
|
|
198
|
-
/* ignore */
|
|
199
|
-
}
|
|
200
|
-
return
|
|
201
|
-
}
|
|
202
|
-
if (generation !== socketGeneration) {
|
|
203
|
-
try {
|
|
204
|
-
gw.close()
|
|
205
|
-
} catch {
|
|
206
|
-
/* ignore */
|
|
207
|
-
}
|
|
208
|
-
return
|
|
209
|
-
}
|
|
210
|
-
persistentConn = gw
|
|
211
|
-
attachSocketLifecycle(gw, generation)
|
|
212
|
-
startPingTimer(gw)
|
|
213
|
-
dcgLogger(`Gateway 持久连接成功 connId=${gw.getConnId() ?? '?'}`)
|
|
214
|
-
} catch (e) {
|
|
215
|
-
dcgLogger(`Gateway 连接失败: ${e}`, 'error')
|
|
216
|
-
persistentConn = null
|
|
217
|
-
clearPingTimer()
|
|
218
|
-
if (!socketStopped) {
|
|
219
|
-
scheduleReconnect()
|
|
220
|
-
}
|
|
221
|
-
} finally {
|
|
222
|
-
connectInFlight = false
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* 插件 register 后调用:建立到 OpenClaw Gateway 的长连接,30s 一次 WebSocket ping,断线每 5s 重连。
|
|
228
|
-
*/
|
|
229
|
-
export function startDcgchatGatewaySocket(): void {
|
|
230
|
-
socketStopped = false
|
|
231
|
-
clearReconnectTimer()
|
|
232
|
-
if (startupConnectTimer != null) return
|
|
233
|
-
startupConnectTimer = setTimeout(() => {
|
|
234
|
-
startupConnectTimer = null
|
|
235
|
-
void connectPersistentGateway()
|
|
236
|
-
}, 0)
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* 停止长连接与重连(例如测试或显式下线)。
|
|
241
|
-
*/
|
|
242
|
-
export function stopDcgchatGatewaySocket(): void {
|
|
243
|
-
socketStopped = true
|
|
244
|
-
clearReconnectTimer()
|
|
245
|
-
clearStartupConnectTimer()
|
|
246
|
-
clearPingTimer()
|
|
247
|
-
if (persistentConn) {
|
|
248
|
-
try {
|
|
249
|
-
persistentConn.close()
|
|
250
|
-
} catch {
|
|
251
|
-
/* ignore */
|
|
252
|
-
}
|
|
253
|
-
persistentConn = null
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
export function isDcgchatGatewaySocketConnected(): boolean {
|
|
258
|
-
return persistentConn?.isConnected() ?? false
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* 在已连接时调用 Gateway RPC;`params` 完全由调用方传入,不做固定结构。
|
|
263
|
-
*/
|
|
264
|
-
export async function callGatewayMethod<T = unknown>(method: string, params: Record<string, unknown> = {}): Promise<T> {
|
|
265
|
-
const gw = persistentConn
|
|
266
|
-
if (!gw?.isConnected()) {
|
|
267
|
-
throw new Error('Gateway 未连接(等待重连或检查配置)')
|
|
268
|
-
}
|
|
269
|
-
return gw.callMethod<T>(method, params)
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* 使用对象形式发送 RPC(推荐)。
|
|
274
|
-
*/
|
|
275
|
-
export async function sendGatewayRpc<T = unknown>(payload: GatewayRpcPayload): Promise<T> {
|
|
276
|
-
return callGatewayMethod<T>(payload.method, payload.params ?? {})
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* 兼容:字符串方法名,或 JSON 字符串 `{ method, params? }`。
|
|
281
|
-
*/
|
|
282
|
-
export async function sendMessageToGateway(message: string): Promise<unknown> {
|
|
283
|
-
const { method, params } = parseGatewayRpcMessage(message)
|
|
284
|
-
return callGatewayMethod(method, params)
|
|
285
|
-
}
|
|
Binary file
|
|
Binary file
|
package/src/libs/md5-2.3.0.tgz
DELETED
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/libs/ws-8.19.0.tgz
DELETED
|
Binary file
|
package/src/monitor.ts
DELETED
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import type { ClawdbotConfig, RuntimeEnv } from 'openclaw/plugin-sdk'
|
|
2
|
-
import WebSocket from 'ws'
|
|
3
|
-
import { resolveAccount } from './channel.js'
|
|
4
|
-
import { setWsConnection, getOpenClawConfig } from './utils/global.js'
|
|
5
|
-
import { dcgLogger } from './utils/log.js'
|
|
6
|
-
import { isWsOpen } from './transport.js'
|
|
7
|
-
import { handleParsedWsMessage } from './utils/wsMessageHandler.js'
|
|
8
|
-
|
|
9
|
-
export type MonitorDcgchatOpts = {
|
|
10
|
-
config?: ClawdbotConfig
|
|
11
|
-
runtime?: RuntimeEnv
|
|
12
|
-
abortSignal?: AbortSignal
|
|
13
|
-
accountId?: string
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const RECONNECT_DELAY_MS = 3000
|
|
17
|
-
const HEARTBEAT_INTERVAL_MS = 30_000
|
|
18
|
-
|
|
19
|
-
function buildConnectUrl(account: Record<string, string>): string {
|
|
20
|
-
const { wsUrl, botToken, userId, domainId, appId } = account
|
|
21
|
-
const url = new URL(wsUrl)
|
|
22
|
-
if (botToken) url.searchParams.set('bot_token', botToken)
|
|
23
|
-
if (userId) url.searchParams.set('_userId', userId)
|
|
24
|
-
url.searchParams.set('_domainId', domainId || '1000')
|
|
25
|
-
url.searchParams.set('_appId', appId || '100')
|
|
26
|
-
return url.toString()
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export async function monitorDcgchatProvider(opts: MonitorDcgchatOpts): Promise<void> {
|
|
30
|
-
const { abortSignal, accountId } = opts
|
|
31
|
-
|
|
32
|
-
const config = getOpenClawConfig()
|
|
33
|
-
if (!config) {
|
|
34
|
-
dcgLogger('no config available', 'error')
|
|
35
|
-
return
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const account = resolveAccount(config, accountId ?? 'default')
|
|
39
|
-
|
|
40
|
-
if (!account.wsUrl) {
|
|
41
|
-
dcgLogger(` wsUrl not configured`, 'error')
|
|
42
|
-
return
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
let shouldReconnect = true
|
|
46
|
-
let ws: WebSocket | null = null
|
|
47
|
-
let heartbeatTimer: ReturnType<typeof setInterval> | null = null
|
|
48
|
-
let heartbeatLogCounter = 0
|
|
49
|
-
|
|
50
|
-
const isOpenclawBotHeartbeat = (raw: WebSocket.RawData, parsedMsg: { messageType?: string }): boolean => {
|
|
51
|
-
if (parsedMsg?.messageType === 'openclaw_bot_heartbeat') return true
|
|
52
|
-
if (typeof raw === 'object' && raw !== null && !Buffer.isBuffer(raw)) {
|
|
53
|
-
return (raw as { messageType?: string }).messageType === 'openclaw_bot_heartbeat'
|
|
54
|
-
}
|
|
55
|
-
return false
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const stopHeartbeat = () => {
|
|
59
|
-
if (heartbeatTimer) {
|
|
60
|
-
clearInterval(heartbeatTimer)
|
|
61
|
-
heartbeatTimer = null
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
function onHeartbeat() {
|
|
65
|
-
if (isWsOpen()) {
|
|
66
|
-
const heartbeat = {
|
|
67
|
-
messageType: 'openclaw_bot_heartbeat',
|
|
68
|
-
_userId: Number(account.userId) || 0,
|
|
69
|
-
source: 'client',
|
|
70
|
-
content: {
|
|
71
|
-
bot_token: account.botToken,
|
|
72
|
-
status: '1'
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
ws?.send(JSON.stringify(heartbeat))
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
const startHeartbeat = () => {
|
|
79
|
-
stopHeartbeat()
|
|
80
|
-
heartbeatTimer = setInterval(() => {
|
|
81
|
-
onHeartbeat()
|
|
82
|
-
}, HEARTBEAT_INTERVAL_MS)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const connect = () => {
|
|
86
|
-
if (!shouldReconnect) return
|
|
87
|
-
|
|
88
|
-
const connectUrl = buildConnectUrl(account as Record<string, any>)
|
|
89
|
-
dcgLogger(`connecting to ${connectUrl}`)
|
|
90
|
-
ws = new WebSocket(connectUrl)
|
|
91
|
-
|
|
92
|
-
ws.on('open', () => {
|
|
93
|
-
dcgLogger(`socket connected`)
|
|
94
|
-
setWsConnection(ws)
|
|
95
|
-
startHeartbeat()
|
|
96
|
-
onHeartbeat()
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
ws.on('message', async (data) => {
|
|
100
|
-
let parsed: { messageType?: string; content: any }
|
|
101
|
-
try {
|
|
102
|
-
parsed = JSON.parse(data.toString())
|
|
103
|
-
} catch {
|
|
104
|
-
dcgLogger(`用户消息解析失败: invalid JSON received`, 'error')
|
|
105
|
-
return
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const payloadStr = data.toString()
|
|
109
|
-
const heartbeat = isOpenclawBotHeartbeat(data, parsed)
|
|
110
|
-
if (heartbeat) {
|
|
111
|
-
heartbeatLogCounter += 1
|
|
112
|
-
if (heartbeatLogCounter % 10 === 0 || heartbeatLogCounter === 1) {
|
|
113
|
-
dcgLogger(`${parsed?.messageType}, ${payloadStr}`)
|
|
114
|
-
dcgLogger(`heartbeat ack received, ${payloadStr}`)
|
|
115
|
-
}
|
|
116
|
-
return
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
dcgLogger(`${parsed?.messageType}, ${payloadStr}`)
|
|
120
|
-
try {
|
|
121
|
-
parsed.content = JSON.parse(parsed.content)
|
|
122
|
-
} catch {
|
|
123
|
-
dcgLogger(`用户消息content字段解析失败: invalid JSON received`, 'error')
|
|
124
|
-
return
|
|
125
|
-
}
|
|
126
|
-
await handleParsedWsMessage(parsed, payloadStr, account.accountId)
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
ws.on('close', (code, reason) => {
|
|
130
|
-
stopHeartbeat()
|
|
131
|
-
setWsConnection(null)
|
|
132
|
-
dcgLogger(`disconnected (code=${code}, reason=${reason?.toString() || ''})`, 'error')
|
|
133
|
-
if (shouldReconnect) {
|
|
134
|
-
dcgLogger(`reconnecting in ${RECONNECT_DELAY_MS}ms...`)
|
|
135
|
-
setTimeout(connect, RECONNECT_DELAY_MS)
|
|
136
|
-
}
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
ws.on('error', (e) => {
|
|
140
|
-
dcgLogger(`WebSocket error: ${String(e)}`, 'error')
|
|
141
|
-
})
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
connect()
|
|
145
|
-
|
|
146
|
-
await new Promise<void>((resolve) => {
|
|
147
|
-
if (abortSignal?.aborted) {
|
|
148
|
-
shouldReconnect = false
|
|
149
|
-
ws?.close()
|
|
150
|
-
resolve()
|
|
151
|
-
return
|
|
152
|
-
}
|
|
153
|
-
abortSignal?.addEventListener(
|
|
154
|
-
'abort',
|
|
155
|
-
() => {
|
|
156
|
-
dcgLogger(`socket stopping`)
|
|
157
|
-
stopHeartbeat()
|
|
158
|
-
shouldReconnect = false
|
|
159
|
-
ws?.close()
|
|
160
|
-
resolve()
|
|
161
|
-
},
|
|
162
|
-
{ once: true }
|
|
163
|
-
)
|
|
164
|
-
})
|
|
165
|
-
}
|
package/src/request/api.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { post } from './request.js'
|
|
2
|
-
import type { IStsToken, IStsTokenReq } from '../types.js'
|
|
3
|
-
import { getUserTokenCache, setUserTokenCache } from './userInfo.js'
|
|
4
|
-
import { dcgLogger } from '../utils/log.js'
|
|
5
|
-
|
|
6
|
-
export const getStsToken = async (name: string, botToken: string, isPrivate: 1 | 0) => {
|
|
7
|
-
// 确保 userToken 已缓存(如果未缓存会自动获取并缓存)
|
|
8
|
-
await getUserToken(botToken)
|
|
9
|
-
|
|
10
|
-
const response = await post<IStsTokenReq, IStsToken>('/user/getStsTokenByAsync', { sourceFileName: name, isPrivate }, { botToken })
|
|
11
|
-
|
|
12
|
-
if (!response || !response.data || !response.data.bucket) {
|
|
13
|
-
throw new Error('获取 OSS 临时凭证失败')
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
return response.data
|
|
17
|
-
}
|
|
18
|
-
export const generateSignUrl = async (file_url: string, botToken: string) => {
|
|
19
|
-
try {
|
|
20
|
-
// 确保 userToken 已缓存(如果未缓存会自动获取并缓存)
|
|
21
|
-
await getUserToken(botToken)
|
|
22
|
-
|
|
23
|
-
const response = await post<any>('/user/generateSignUrl', { loudPlatform: 0, fileName: file_url }, { botToken })
|
|
24
|
-
if (response.code === 0 && response.data) {
|
|
25
|
-
// @ts-ignore
|
|
26
|
-
return response.data?.filePath
|
|
27
|
-
}
|
|
28
|
-
return ''
|
|
29
|
-
} catch (error) {
|
|
30
|
-
return ''
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* 通过 botToken 查询 userToken
|
|
36
|
-
* @param botToken 机器人 token
|
|
37
|
-
* @returns userToken
|
|
38
|
-
*/
|
|
39
|
-
export const queryUserTokenByBotToken = async (botToken: string): Promise<string> => {
|
|
40
|
-
const response = await post<{ botToken: string }, { token: string }>('/organization/queryUserTokenByBotToken', { botToken })
|
|
41
|
-
|
|
42
|
-
if (!response || !response.data || !response.data.token) {
|
|
43
|
-
dcgLogger('获取绑定的用户信息失败: token:' + botToken + '|' + JSON.stringify(response), 'error')
|
|
44
|
-
return ''
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return response.data.token
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* 获取 userToken(优先从缓存获取,缓存未命中则调用 API)
|
|
52
|
-
* @param botToken 机器人 token
|
|
53
|
-
* @returns userToken
|
|
54
|
-
*/
|
|
55
|
-
export const getUserToken = async (botToken: string): Promise<string> => {
|
|
56
|
-
// 1. 尝试从缓存获取
|
|
57
|
-
const cachedToken = getUserTokenCache(botToken)
|
|
58
|
-
if (cachedToken) {
|
|
59
|
-
return cachedToken
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// 2. 缓存未命中,调用 API 获取
|
|
63
|
-
dcgLogger(`[api] cache miss, fetching userToken for botToken=${botToken.slice(0, 10)}...`)
|
|
64
|
-
const userToken = await queryUserTokenByBotToken(botToken)
|
|
65
|
-
|
|
66
|
-
// 3. 缓存新获取的 token
|
|
67
|
-
setUserTokenCache(botToken, userToken)
|
|
68
|
-
|
|
69
|
-
return userToken
|
|
70
|
-
}
|
package/src/request/oss.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { createReadStream } from 'node:fs'
|
|
2
|
-
// @ts-ignore
|
|
3
|
-
import OSS from 'ali-oss'
|
|
4
|
-
import { getStsToken, getUserToken } from './api.js'
|
|
5
|
-
import { dcgLogger } from '../utils/log.js'
|
|
6
|
-
|
|
7
|
-
/** 将 File/路径/Buffer 转为 ali-oss put 所需的 Buffer 或 ReadableStream */
|
|
8
|
-
async function toUploadContent(
|
|
9
|
-
input: File | string | Buffer
|
|
10
|
-
): Promise<{ content: Buffer | ReturnType<typeof createReadStream>; fileName: string }> {
|
|
11
|
-
if (Buffer.isBuffer(input)) {
|
|
12
|
-
return { content: input, fileName: 'file' }
|
|
13
|
-
}
|
|
14
|
-
if (typeof input === 'string') {
|
|
15
|
-
return {
|
|
16
|
-
content: createReadStream(input),
|
|
17
|
-
fileName: input.split('/').pop() ?? 'file'
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
// File: ali-oss 需要 Buffer/Stream,用 arrayBuffer 转 Buffer
|
|
21
|
-
const buf = Buffer.from(await input.arrayBuffer())
|
|
22
|
-
return { content: buf, fileName: input.name }
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export const ossUpload = async (file: File | string | Buffer, botToken: string, isPrivate: 0 | 1 = 1) => {
|
|
26
|
-
await getUserToken(botToken)
|
|
27
|
-
|
|
28
|
-
const { content, fileName } = await toUploadContent(file)
|
|
29
|
-
const data = await getStsToken(fileName, botToken, isPrivate)
|
|
30
|
-
|
|
31
|
-
const options: OSS.Options = {
|
|
32
|
-
// 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。
|
|
33
|
-
accessKeyId: data.tempAccessKeyId,
|
|
34
|
-
accessKeySecret: data.tempAccessKeySecret,
|
|
35
|
-
// 从STS服务获取的安全令牌(SecurityToken)。
|
|
36
|
-
stsToken: data.tempSecurityToken,
|
|
37
|
-
// 填写Bucket名称。
|
|
38
|
-
bucket: data.bucket,
|
|
39
|
-
endpoint: data.endPoint,
|
|
40
|
-
region: data.region,
|
|
41
|
-
secure: true,
|
|
42
|
-
cname: true,
|
|
43
|
-
authorizationV4: true
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const client = new OSS(options)
|
|
47
|
-
|
|
48
|
-
const name = `${data.uploadDir}${data.ossFileKey}`
|
|
49
|
-
|
|
50
|
-
try {
|
|
51
|
-
const objectResult = await client.put(name, content)
|
|
52
|
-
if (objectResult?.res?.status !== 200) {
|
|
53
|
-
dcgLogger(`OSS 上传失败, ${objectResult?.res?.status}`)
|
|
54
|
-
}
|
|
55
|
-
dcgLogger(`OSS 上传成功, ${objectResult.name || objectResult.url}`)
|
|
56
|
-
// const url = `${data.protocol || 'http'}://${data.bucket}.${data.endPoint}/${data.uploadDir}${data.ossFileKey}`
|
|
57
|
-
return isPrivate === 1 ? objectResult.name || objectResult.url : objectResult.url
|
|
58
|
-
} catch (error) {
|
|
59
|
-
dcgLogger(`OSS 上传失败: ${error}`, 'error')
|
|
60
|
-
}
|
|
61
|
-
}
|