@dcrays/dcgchat-test 0.6.10 → 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.
- package/README.md +167 -0
- package/index.ts +35 -0
- package/openclaw.plugin.json +12 -41
- package/package.json +35 -8
- package/src/agent.ts +568 -0
- package/src/bot.ts +646 -0
- package/src/channel/index.ts +329 -0
- package/src/channel/outboundMedia.ts +144 -0
- package/src/channel/outboundTarget.ts +62 -0
- package/src/channel/uploadMediaUrl.ts +76 -0
- package/src/cron/message.ts +114 -0
- package/src/cron/params.ts +164 -0
- package/src/cron/toolGuard.ts +466 -0
- package/src/cron/types.ts +15 -0
- package/src/gateway/index.ts +430 -0
- package/src/gateway/security.ts +95 -0
- package/src/gateway/socket.ts +256 -0
- 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 +269 -0
- package/src/request/api.ts +80 -0
- package/src/request/oss.ts +200 -0
- package/src/request/request.ts +191 -0
- package/src/request/userInfo.ts +44 -0
- package/src/session.ts +28 -0
- package/src/skill.ts +114 -0
- package/src/tool.ts +603 -0
- package/src/tools/messageTool.ts +334 -0
- package/src/tools/toolCallGuard.ts +327 -0
- package/src/transport.ts +217 -0
- package/src/types.ts +139 -0
- package/src/utils/agentErrors.ts +116 -0
- package/src/utils/constant.ts +60 -0
- package/src/utils/env-config.ts +19 -0
- package/src/utils/formatLlmInputEvent.ts +48 -0
- package/src/utils/gatewayMsgHandler.ts +147 -0
- package/src/utils/global.ts +309 -0
- package/src/utils/inboundTurnState.ts +66 -0
- package/src/utils/log.ts +77 -0
- package/src/utils/mediaAttached.ts +244 -0
- package/src/utils/mediaEmitter.ts +54 -0
- package/src/utils/outboundAssistantText.ts +117 -0
- package/src/utils/params.ts +104 -0
- package/src/utils/passTxt.ts +4 -0
- package/src/utils/resolveRegisterConfig.ts +33 -0
- package/src/utils/searchFile.ts +228 -0
- package/src/utils/sessionState.ts +137 -0
- package/src/utils/sessionTermination.ts +228 -0
- package/src/utils/streamMerge.ts +150 -0
- package/src/utils/subagentRunMap.ts +71 -0
- package/src/utils/undiciFetchInterceptor.ts +346 -0
- package/src/utils/workspaceFilePaths.ts +18 -0
- package/src/utils/wsMessageHandler.ts +124 -0
- package/src/utils/zipExtract.ts +97 -0
- package/src/utils/zipPath.ts +24 -0
- package/index.js +0 -304
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import type { OpenClawConfig } from 'openclaw/plugin-sdk'
|
|
2
|
+
import { getDcgchatRuntime, getOpenClawConfig } from '../utils/global.js'
|
|
3
|
+
import { GatewayConnection, type GatewayConfig } from './index.js'
|
|
4
|
+
import { dcgLogger } from '../utils/log.js'
|
|
5
|
+
import { openclawConfigLooksUsable } from '../utils/resolveRegisterConfig.js'
|
|
6
|
+
import { WS_HEARTBEAT_INTERVAL_MS, WS_RECONNECT_DELAY_MS } from '../utils/constant.js'
|
|
7
|
+
|
|
8
|
+
type OpenClawGatewaySection = {
|
|
9
|
+
port?: number
|
|
10
|
+
bind?: string
|
|
11
|
+
tls?: { enabled?: boolean }
|
|
12
|
+
auth?: { mode?: string; token?: string }
|
|
13
|
+
remote?: { url?: string }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getGatewaySection(cfg: OpenClawConfig): OpenClawGatewaySection | undefined {
|
|
17
|
+
return (cfg as OpenClawConfig & { gateway?: OpenClawGatewaySection }).gateway
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 从 OpenClaw 配置解析本地/远程 Gateway WebSocket 与 token。
|
|
22
|
+
* 可用环境变量覆盖:OPENCLAW_GATEWAY_URL、OPENCLAW_GATEWAY_TOKEN。
|
|
23
|
+
*/
|
|
24
|
+
export function resolveGatewayClientConfig(cfg: OpenClawConfig | null): GatewayConfig {
|
|
25
|
+
if (!cfg) {
|
|
26
|
+
throw new Error('OpenClaw 配置未初始化(需先完成插件 register)')
|
|
27
|
+
}
|
|
28
|
+
const g = getGatewaySection(cfg)
|
|
29
|
+
const token = process.env.OPENCLAW_GATEWAY_TOKEN || g?.auth?.token || ''
|
|
30
|
+
if (!token) {
|
|
31
|
+
throw new Error('缺少 Gateway token:请在 openclaw.json 的 gateway.auth.token 设置,或设置环境变量 OPENCLAW_GATEWAY_TOKEN')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const scheme = g?.tls?.enabled === true ? 'wss' : 'ws'
|
|
35
|
+
const url = process.env.OPENCLAW_GATEWAY_URL || g?.remote?.url || `${scheme}://127.0.0.1:${g?.port ?? 18789}`
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
url,
|
|
39
|
+
token,
|
|
40
|
+
role: 'operator',
|
|
41
|
+
scopes: ['operator.admin']
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** register 尚未写入全局缓存时,回退到 runtime.loadConfig(与 registerFull 内解析方式一致) */
|
|
45
|
+
function resolveOpenClawConfigForPersistentGateway(): OpenClawConfig | null {
|
|
46
|
+
const cached = getOpenClawConfig()
|
|
47
|
+
if (openclawConfigLooksUsable(cached)) return cached
|
|
48
|
+
try {
|
|
49
|
+
const fresh = getDcgchatRuntime().config.loadConfig() as OpenClawConfig
|
|
50
|
+
return openclawConfigLooksUsable(fresh) ? fresh : null
|
|
51
|
+
} catch {
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function resolveConfigSafe(): GatewayConfig | null {
|
|
57
|
+
try {
|
|
58
|
+
const cfg = resolveOpenClawConfigForPersistentGateway()
|
|
59
|
+
if (!cfg) {
|
|
60
|
+
dcgLogger('Gateway 持久连接未启动: 无可用 OpenClaw 配置(plugins.register 未完成或 loadConfig 失败)', 'error')
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
return resolveGatewayClientConfig(cfg)
|
|
64
|
+
} catch (e) {
|
|
65
|
+
dcgLogger(`Gateway 持久连接未启动: ${e}`, 'error')
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 任意 method + params,由调用方决定载荷 */
|
|
71
|
+
export type GatewayRpcPayload = {
|
|
72
|
+
method: string
|
|
73
|
+
params?: Record<string, unknown>
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let persistentConn: GatewayConnection | null = null
|
|
77
|
+
let pingTimer: ReturnType<typeof setInterval> | null = null
|
|
78
|
+
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
|
79
|
+
/** register 可能被调用多次,只保留一个「延迟首次连接」定时器,避免同一时刻触发两次 connect */
|
|
80
|
+
let startupConnectTimer: ReturnType<typeof setTimeout> | null = null
|
|
81
|
+
let connectInFlight = false
|
|
82
|
+
let socketStopped = true
|
|
83
|
+
/** 用于忽略「已被替换的旧连接」上的 close/error */
|
|
84
|
+
let socketGeneration = 0
|
|
85
|
+
|
|
86
|
+
function clearPingTimer(): void {
|
|
87
|
+
if (pingTimer) {
|
|
88
|
+
clearInterval(pingTimer)
|
|
89
|
+
pingTimer = null
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function clearReconnectTimer(): void {
|
|
94
|
+
if (reconnectTimer) {
|
|
95
|
+
clearTimeout(reconnectTimer)
|
|
96
|
+
reconnectTimer = null
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function startPingTimer(gw: GatewayConnection): void {
|
|
101
|
+
clearPingTimer()
|
|
102
|
+
pingTimer = setInterval(() => {
|
|
103
|
+
if (!gw.isConnected()) return
|
|
104
|
+
try {
|
|
105
|
+
gw.ping()
|
|
106
|
+
} catch (e) {
|
|
107
|
+
dcgLogger(`Gateway ping 失败: ${e}`, 'error')
|
|
108
|
+
}
|
|
109
|
+
}, WS_HEARTBEAT_INTERVAL_MS)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function scheduleReconnect(): void {
|
|
113
|
+
if (socketStopped || reconnectTimer) return
|
|
114
|
+
reconnectTimer = setTimeout(() => {
|
|
115
|
+
reconnectTimer = null
|
|
116
|
+
void connectPersistentGateway()
|
|
117
|
+
}, WS_RECONNECT_DELAY_MS)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function attachSocketLifecycle(gw: GatewayConnection, generation: number): void {
|
|
121
|
+
const ws = gw.getWebSocket()
|
|
122
|
+
if (!ws) return
|
|
123
|
+
|
|
124
|
+
const onDown = () => {
|
|
125
|
+
if (generation !== socketGeneration) return
|
|
126
|
+
clearPingTimer()
|
|
127
|
+
if (persistentConn === gw) {
|
|
128
|
+
persistentConn = null
|
|
129
|
+
}
|
|
130
|
+
dcgLogger('Gateway WebSocket 已断开,将在 5s 后重连', 'error')
|
|
131
|
+
if (!socketStopped) {
|
|
132
|
+
scheduleReconnect()
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
ws.once('close', onDown)
|
|
137
|
+
ws.once('error', onDown)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function connectPersistentGateway(): Promise<void> {
|
|
141
|
+
if (socketStopped) return
|
|
142
|
+
if (persistentConn?.isConnected()) return
|
|
143
|
+
if (connectInFlight) return
|
|
144
|
+
|
|
145
|
+
const cfg = resolveConfigSafe()
|
|
146
|
+
if (!cfg) return
|
|
147
|
+
|
|
148
|
+
connectInFlight = true
|
|
149
|
+
try {
|
|
150
|
+
socketGeneration += 1
|
|
151
|
+
const generation = socketGeneration
|
|
152
|
+
|
|
153
|
+
if (persistentConn) {
|
|
154
|
+
try {
|
|
155
|
+
persistentConn.close()
|
|
156
|
+
} catch {
|
|
157
|
+
/* ignore */
|
|
158
|
+
}
|
|
159
|
+
persistentConn = null
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const gw = new GatewayConnection(cfg)
|
|
163
|
+
await gw.connect()
|
|
164
|
+
if (socketStopped) {
|
|
165
|
+
try {
|
|
166
|
+
gw.close()
|
|
167
|
+
} catch {
|
|
168
|
+
/* ignore */
|
|
169
|
+
}
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
if (generation !== socketGeneration) {
|
|
173
|
+
try {
|
|
174
|
+
gw.close()
|
|
175
|
+
} catch {
|
|
176
|
+
/* ignore */
|
|
177
|
+
}
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
persistentConn = gw
|
|
181
|
+
attachSocketLifecycle(gw, generation)
|
|
182
|
+
startPingTimer(gw)
|
|
183
|
+
dcgLogger(`Gateway 持久连接成功 connId=${gw.getConnId() ?? '?'}`)
|
|
184
|
+
} catch (e) {
|
|
185
|
+
dcgLogger(`Gateway 连接失败: ${e}`, 'error')
|
|
186
|
+
persistentConn = null
|
|
187
|
+
clearPingTimer()
|
|
188
|
+
if (!socketStopped) {
|
|
189
|
+
scheduleReconnect()
|
|
190
|
+
}
|
|
191
|
+
} finally {
|
|
192
|
+
connectInFlight = false
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 插件 register 后调用:建立到 OpenClaw Gateway 的长连接,30s 一次 WebSocket ping,断线每 5s 重连。
|
|
198
|
+
*/
|
|
199
|
+
export function startDcgchatGatewaySocket(): void {
|
|
200
|
+
socketStopped = false
|
|
201
|
+
clearReconnectTimer()
|
|
202
|
+
if (startupConnectTimer != null) return
|
|
203
|
+
startupConnectTimer = setTimeout(() => {
|
|
204
|
+
startupConnectTimer = null
|
|
205
|
+
void connectPersistentGateway()
|
|
206
|
+
}, 0)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function isDcgchatGatewaySocketConnected(): boolean {
|
|
210
|
+
return persistentConn?.isConnected() ?? false
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** `gateway_start` 等场景下,持久连接可能仍在异步 connect;轮询至连通或超时 */
|
|
214
|
+
const DEFAULT_WAIT_GATEWAY_MS = 120_000
|
|
215
|
+
const DEFAULT_WAIT_GATEWAY_POLL_MS = 150
|
|
216
|
+
|
|
217
|
+
export async function waitForDcgchatGatewaySocketConnected(opts?: {
|
|
218
|
+
timeoutMs?: number
|
|
219
|
+
intervalMs?: number
|
|
220
|
+
signal?: AbortSignal
|
|
221
|
+
}): Promise<boolean> {
|
|
222
|
+
const timeoutMs = opts?.timeoutMs ?? DEFAULT_WAIT_GATEWAY_MS
|
|
223
|
+
const intervalMs = opts?.intervalMs ?? DEFAULT_WAIT_GATEWAY_POLL_MS
|
|
224
|
+
const deadline = Date.now() + timeoutMs
|
|
225
|
+
for (;;) {
|
|
226
|
+
if (opts?.signal?.aborted) return false
|
|
227
|
+
if (isDcgchatGatewaySocketConnected()) return true
|
|
228
|
+
if (Date.now() >= deadline) return isDcgchatGatewaySocketConnected()
|
|
229
|
+
await new Promise<void>((r) => setTimeout(r, intervalMs))
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 在已连接时调用 Gateway RPC;`params` 完全由调用方传入,不做固定结构。
|
|
235
|
+
*/
|
|
236
|
+
export async function callGatewayMethod<T = unknown>(
|
|
237
|
+
method: string,
|
|
238
|
+
params: Record<string, unknown> = {},
|
|
239
|
+
opts?: { timeoutMs?: number }
|
|
240
|
+
): Promise<T> {
|
|
241
|
+
const gw = persistentConn
|
|
242
|
+
if (!gw?.isConnected()) {
|
|
243
|
+
throw new Error('Gateway 未连接(等待重连或检查配置)')
|
|
244
|
+
}
|
|
245
|
+
return gw.callMethod<T>(method, params, opts)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* 使用对象形式发送 RPC。
|
|
250
|
+
*/
|
|
251
|
+
export async function sendGatewayRpc<T = unknown>(
|
|
252
|
+
payload: GatewayRpcPayload,
|
|
253
|
+
opts?: { timeoutMs?: number }
|
|
254
|
+
): Promise<T> {
|
|
255
|
+
return callGatewayMethod<T>(payload.method, payload.params ?? {}, opts)
|
|
256
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/monitor.ts
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import type { OpenClawConfig, RuntimeEnv } from 'openclaw/plugin-sdk'
|
|
2
|
+
import WebSocket from 'ws'
|
|
3
|
+
import { resolveAccount } from './channel/index.js'
|
|
4
|
+
import type { ResolvedDcgchatAccount } from './types.js'
|
|
5
|
+
import { setWsConnection, getOpenClawConfig, compressFields } from './utils/global.js'
|
|
6
|
+
import { dcgLogger } from './utils/log.js'
|
|
7
|
+
import { isWsOpen } from './transport.js'
|
|
8
|
+
import { handleParsedWsMessage } from './utils/wsMessageHandler.js'
|
|
9
|
+
import { WS_HEARTBEAT_INTERVAL_MS, WS_RECONNECT_DELAY_MS } from './utils/constant.js'
|
|
10
|
+
|
|
11
|
+
export type MonitorDcgchatOpts = {
|
|
12
|
+
config?: OpenClawConfig
|
|
13
|
+
runtime?: RuntimeEnv
|
|
14
|
+
abortSignal?: AbortSignal
|
|
15
|
+
accountId?: string
|
|
16
|
+
statusSink?: (patch: {
|
|
17
|
+
connected?: boolean
|
|
18
|
+
running?: boolean
|
|
19
|
+
lastConnectedAt?: number | null
|
|
20
|
+
lastDisconnect?:
|
|
21
|
+
| string
|
|
22
|
+
| {
|
|
23
|
+
at: number
|
|
24
|
+
status?: number
|
|
25
|
+
error?: string
|
|
26
|
+
loggedOut?: boolean
|
|
27
|
+
}
|
|
28
|
+
| null
|
|
29
|
+
lastError?: string | null
|
|
30
|
+
lastStartAt?: number | null
|
|
31
|
+
lastStopAt?: number | null
|
|
32
|
+
lastInboundAt?: number | null
|
|
33
|
+
lastOutboundAt?: number | null
|
|
34
|
+
reconnectAttempts?: number
|
|
35
|
+
healthState?: string
|
|
36
|
+
}) => void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildConnectUrl(account: ResolvedDcgchatAccount): string {
|
|
40
|
+
const { wsUrl, botToken, userId, domainId, appId } = account
|
|
41
|
+
const url = new URL(wsUrl)
|
|
42
|
+
if (botToken) url.searchParams.set('bot_token', botToken)
|
|
43
|
+
if (userId) url.searchParams.set('_userId', userId)
|
|
44
|
+
url.searchParams.set('_domainId', domainId || '1000')
|
|
45
|
+
url.searchParams.set('_appId', appId || '100')
|
|
46
|
+
return url.toString()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function monitorDcgchatProvider(opts: MonitorDcgchatOpts): Promise<void> {
|
|
50
|
+
const { abortSignal, accountId } = opts
|
|
51
|
+
|
|
52
|
+
const config = getOpenClawConfig()
|
|
53
|
+
if (!config) {
|
|
54
|
+
dcgLogger('no OpenClaw config available', 'error')
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const account = resolveAccount(config, accountId ?? 'default')
|
|
59
|
+
|
|
60
|
+
if (!account.wsUrl) {
|
|
61
|
+
dcgLogger(` wsUrl not configured`, 'error')
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let shouldReconnect = true
|
|
66
|
+
let ws: WebSocket | null = null
|
|
67
|
+
let heartbeatTimer: ReturnType<typeof setInterval> | null = null
|
|
68
|
+
let heartbeatLogCounter = 0
|
|
69
|
+
let lastReportedStatus: 'connecting' | 'connected' | 'disconnected' | 'error' | '' = ''
|
|
70
|
+
|
|
71
|
+
const isOpenclawBotHeartbeat = (raw: WebSocket.RawData, parsedMsg: { messageType?: string }): boolean => {
|
|
72
|
+
if (parsedMsg?.messageType === 'openclaw_bot_heartbeat') return true
|
|
73
|
+
if (typeof raw === 'object' && raw !== null && !Buffer.isBuffer(raw)) {
|
|
74
|
+
return (raw as { messageType?: string }).messageType === 'openclaw_bot_heartbeat'
|
|
75
|
+
}
|
|
76
|
+
return false
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const stopHeartbeat = () => {
|
|
80
|
+
if (heartbeatTimer) {
|
|
81
|
+
clearInterval(heartbeatTimer)
|
|
82
|
+
heartbeatTimer = null
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const reportChannelStatus = (status: 'connecting' | 'connected' | 'disconnected' | 'error', extra?: Record<string, unknown>) => {
|
|
87
|
+
if (lastReportedStatus === status) return
|
|
88
|
+
lastReportedStatus = status
|
|
89
|
+
if (!opts.statusSink) return
|
|
90
|
+
|
|
91
|
+
if (status === 'connecting') {
|
|
92
|
+
opts.statusSink({
|
|
93
|
+
running: true,
|
|
94
|
+
lastError: null,
|
|
95
|
+
lastDisconnect: null
|
|
96
|
+
})
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (status === 'connected') {
|
|
101
|
+
opts.statusSink({
|
|
102
|
+
running: true,
|
|
103
|
+
connected: true,
|
|
104
|
+
lastConnectedAt: Date.now(),
|
|
105
|
+
lastError: null,
|
|
106
|
+
lastDisconnect: null
|
|
107
|
+
})
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (status === 'disconnected') {
|
|
112
|
+
opts.statusSink({
|
|
113
|
+
connected: false,
|
|
114
|
+
lastDisconnect: {
|
|
115
|
+
at: Date.now(),
|
|
116
|
+
status: typeof extra?.code === 'number' ? extra.code : undefined,
|
|
117
|
+
error: typeof extra?.reason === 'string' ? extra.reason : undefined
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
opts.statusSink({
|
|
124
|
+
connected: false,
|
|
125
|
+
lastError: typeof extra?.error === 'string' ? extra.error : String(extra?.error ?? 'WebSocket error')
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function onHeartbeat() {
|
|
130
|
+
if (isWsOpen()) {
|
|
131
|
+
const heartbeat = {
|
|
132
|
+
messageType: 'openclaw_bot_heartbeat',
|
|
133
|
+
_userId: Number(account.userId) || 0,
|
|
134
|
+
source: 'client',
|
|
135
|
+
content: {
|
|
136
|
+
bot_token: account.botToken,
|
|
137
|
+
status: '1'
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
ws?.send(JSON.stringify(heartbeat))
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const startHeartbeat = () => {
|
|
144
|
+
stopHeartbeat()
|
|
145
|
+
heartbeatTimer = setInterval(() => {
|
|
146
|
+
onHeartbeat()
|
|
147
|
+
}, WS_HEARTBEAT_INTERVAL_MS)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** 移除当前 ws 实例上的事件监听器,防止重连时在旧实例上累积监听器 */
|
|
151
|
+
let removeWsListeners: (() => void) | null = null
|
|
152
|
+
|
|
153
|
+
const connect = () => {
|
|
154
|
+
if (!shouldReconnect) return
|
|
155
|
+
|
|
156
|
+
// 清理旧 ws 实例上的监听器(若有)
|
|
157
|
+
if (removeWsListeners) {
|
|
158
|
+
removeWsListeners()
|
|
159
|
+
removeWsListeners = null
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const start = Date.now()
|
|
163
|
+
const connectUrl = buildConnectUrl(account)
|
|
164
|
+
dcgLogger(`connecting to ${connectUrl}`)
|
|
165
|
+
ws = new WebSocket(connectUrl)
|
|
166
|
+
|
|
167
|
+
const currentWs = ws
|
|
168
|
+
|
|
169
|
+
const onOpen = () => {
|
|
170
|
+
dcgLogger(`socket connected, 用时: ${ Date.now() - start}ms`,)
|
|
171
|
+
setWsConnection(currentWs)
|
|
172
|
+
reportChannelStatus('connecting')
|
|
173
|
+
startHeartbeat()
|
|
174
|
+
onHeartbeat()
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const onMessage = async (data: WebSocket.RawData) => {
|
|
178
|
+
let parsed: { messageType?: string; content: any }
|
|
179
|
+
try {
|
|
180
|
+
parsed = JSON.parse(data.toString())
|
|
181
|
+
if (parsed.messageType !== 'openclaw_bot_heartbeat') {
|
|
182
|
+
dcgLogger(`[后端=>插件]-[消息解析成功]: ${JSON.stringify(compressFields(parsed, ['title']))}`)
|
|
183
|
+
}
|
|
184
|
+
} catch {
|
|
185
|
+
dcgLogger(`[后端=>插件]-[消息解析失败]: invalid JSON received`, 'error')
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const payloadStr = data.toString()
|
|
190
|
+
const heartbeat = isOpenclawBotHeartbeat(data, parsed)
|
|
191
|
+
if (heartbeat) {
|
|
192
|
+
heartbeatLogCounter += 1
|
|
193
|
+
if (heartbeatLogCounter % 10 === 0 || heartbeatLogCounter === 1) {
|
|
194
|
+
dcgLogger(`心跳链接, ${payloadStr}`)
|
|
195
|
+
}
|
|
196
|
+
reportChannelStatus('connected')
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
parsed.content = JSON.parse(parsed.content)
|
|
202
|
+
} catch {
|
|
203
|
+
dcgLogger(`用户消息content字段解析失败: invalid JSON received`, 'error')
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
await handleParsedWsMessage(parsed, payloadStr, account.accountId)
|
|
208
|
+
} catch (err) {
|
|
209
|
+
dcgLogger(`WS onMessage: handleParsedWsMessage failed: ${String(err)}`, 'error')
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const onClose = (code: number, reason: Buffer) => {
|
|
214
|
+
stopHeartbeat()
|
|
215
|
+
// 移除当前 ws 上的所有监听器
|
|
216
|
+
currentWs.removeListener('message', onMessage)
|
|
217
|
+
currentWs.removeListener('close', onClose)
|
|
218
|
+
currentWs.removeListener('error', onError)
|
|
219
|
+
currentWs.removeListener('open', onOpen)
|
|
220
|
+
removeWsListeners = null
|
|
221
|
+
setWsConnection(null)
|
|
222
|
+
reportChannelStatus('disconnected', { code, reason: reason?.toString() || '' })
|
|
223
|
+
dcgLogger(`disconnected (code=${code}, reason=${reason?.toString() || ''})`, 'error')
|
|
224
|
+
if (shouldReconnect) {
|
|
225
|
+
dcgLogger(`reconnecting in ${WS_RECONNECT_DELAY_MS}ms...`)
|
|
226
|
+
setTimeout(connect, WS_RECONNECT_DELAY_MS)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const onError = (e: Error) => {
|
|
231
|
+
reportChannelStatus('error', { error: String(e) })
|
|
232
|
+
dcgLogger(`WebSocket error: ${String(e)}`, 'error')
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
currentWs.on('open', onOpen)
|
|
236
|
+
currentWs.on('message', onMessage)
|
|
237
|
+
currentWs.on('close', onClose)
|
|
238
|
+
currentWs.on('error', onError)
|
|
239
|
+
|
|
240
|
+
removeWsListeners = () => {
|
|
241
|
+
currentWs.removeListener('open', onOpen)
|
|
242
|
+
currentWs.removeListener('message', onMessage)
|
|
243
|
+
currentWs.removeListener('close', onClose)
|
|
244
|
+
currentWs.removeListener('error', onError)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
connect()
|
|
249
|
+
|
|
250
|
+
await new Promise<void>((resolve) => {
|
|
251
|
+
if (abortSignal?.aborted) {
|
|
252
|
+
shouldReconnect = false
|
|
253
|
+
ws?.close()
|
|
254
|
+
resolve()
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
abortSignal?.addEventListener(
|
|
258
|
+
'abort',
|
|
259
|
+
() => {
|
|
260
|
+
dcgLogger(`socket stopping`)
|
|
261
|
+
stopHeartbeat()
|
|
262
|
+
shouldReconnect = false
|
|
263
|
+
ws?.close()
|
|
264
|
+
resolve()
|
|
265
|
+
},
|
|
266
|
+
{ once: true }
|
|
267
|
+
)
|
|
268
|
+
})
|
|
269
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { post } from './request.js'
|
|
2
|
+
import type { IStsToken, IStsTokenReq } from '../types.js'
|
|
3
|
+
import { getUserTokenCache, setUserTokenCache } from './userInfo.js'
|
|
4
|
+
import { getParamsDefaults } from '../utils/params.js'
|
|
5
|
+
import { dcgLogger } from '../utils/log.js'
|
|
6
|
+
|
|
7
|
+
export const getStsToken = async (name: string, botToken: string, isPrivate: 1 | 0) => {
|
|
8
|
+
// 确保 userToken 已缓存(如果未缓存会自动获取并缓存)
|
|
9
|
+
await getUserToken(botToken)
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const response = await post<IStsTokenReq, IStsToken>('/user/getStsTokenByAsync', { sourceFileName: name, isPrivate }, { botToken })
|
|
13
|
+
|
|
14
|
+
if (!response || !response.data || !response.data.bucket) {
|
|
15
|
+
throw new Error(`获取 OSS 临时凭证失败: response=${JSON.stringify(response)}`)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return response.data
|
|
19
|
+
} catch (err) {
|
|
20
|
+
dcgLogger(`getStsToken error: ${String(err)}`, 'error')
|
|
21
|
+
throw new Error(`获取 OSS 临时凭证失败: ${String(err)}`)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export const generateSignUrl = async (file_url: string, botToken: string) => {
|
|
25
|
+
try {
|
|
26
|
+
// 确保 userToken 已缓存(如果未缓存会自动获取并缓存)
|
|
27
|
+
await getUserToken(botToken)
|
|
28
|
+
|
|
29
|
+
const response = await post<{ loudPlatform: number; fileName: string }, { filePath?: string }>('/user/generateSignUrl', { loudPlatform: 0, fileName: file_url }, { botToken })
|
|
30
|
+
if (response.code !== 0 || !response?.data || !response.data?.filePath) {
|
|
31
|
+
throw new Error(`生成 OSS 链接失败: response=${JSON.stringify(response)}`)
|
|
32
|
+
}
|
|
33
|
+
return response.data.filePath
|
|
34
|
+
} catch (error) {
|
|
35
|
+
dcgLogger(`generateSignUrl error: ${String(error)}`, 'error')
|
|
36
|
+
throw new Error(`生成 OSS 链接失败: ${String(error)}`)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 通过 botToken 查询 userToken
|
|
42
|
+
* @param botToken 机器人 token
|
|
43
|
+
* @returns userToken
|
|
44
|
+
*/
|
|
45
|
+
export const queryUserTokenByBotToken = async (botToken: string): Promise<string> => {
|
|
46
|
+
const p = getParamsDefaults()
|
|
47
|
+
const response = await post<{ botToken: string; _userId: number }, { token: string }>('/organization/queryUserTokenByBotTokenV2', {
|
|
48
|
+
botToken,
|
|
49
|
+
_userId: p.userId ?? 0
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
if (!response || !response.data || !response.data.token) {
|
|
53
|
+
dcgLogger('获取绑定的用户信息失败: token:' + botToken + '|' + JSON.stringify(response), 'error')
|
|
54
|
+
return ''
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return response.data.token
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 获取 userToken(优先从缓存获取,缓存未命中则调用 API)
|
|
62
|
+
* @param botToken 机器人 token
|
|
63
|
+
* @returns userToken
|
|
64
|
+
*/
|
|
65
|
+
export const getUserToken = async (botToken: string): Promise<string> => {
|
|
66
|
+
// 1. 尝试从缓存获取
|
|
67
|
+
const cachedToken = getUserTokenCache(botToken)
|
|
68
|
+
if (cachedToken) {
|
|
69
|
+
return cachedToken
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 2. 缓存未命中,调用 API 获取
|
|
73
|
+
dcgLogger(`[api] cache miss, fetching userToken for botToken=${botToken.slice(0, 10)}...`)
|
|
74
|
+
const userToken = await queryUserTokenByBotToken(botToken)
|
|
75
|
+
|
|
76
|
+
// 3. 缓存新获取的 token
|
|
77
|
+
setUserTokenCache(botToken, userToken)
|
|
78
|
+
|
|
79
|
+
return userToken
|
|
80
|
+
}
|