@dcrays/dcgchat-test 0.2.34 → 0.3.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.ts +2 -0
- package/package.json +1 -1
- package/src/bot.ts +49 -34
- package/src/channel.ts +1 -1
- package/src/cron.ts +125 -0
- package/src/gateway/index.ts +458 -0
- package/src/gateway/security.ts +101 -0
- package/src/gateway/socket.ts +271 -0
- package/src/monitor.ts +23 -12
- package/src/request/api.ts +4 -18
- package/src/request/oss.ts +5 -4
- package/src/tool.ts +4 -2
- package/src/transport.ts +23 -0
- package/src/types.ts +2 -0
- package/src/utils/global.ts +3 -0
package/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { dcgchatPlugin } from './src/channel.js'
|
|
|
4
4
|
import { setDcgchatRuntime, setWorkspaceDir } from './src/utils/global.js'
|
|
5
5
|
import { monitoringToolMessage } from './src/tool.js'
|
|
6
6
|
import { setOpenClawConfig } from './src/utils/global.js'
|
|
7
|
+
import { startDcgchatGatewaySocket } from './src/gateway/socket.js'
|
|
7
8
|
|
|
8
9
|
const plugin = {
|
|
9
10
|
id: "dcgchat-test",
|
|
@@ -15,6 +16,7 @@ const plugin = {
|
|
|
15
16
|
|
|
16
17
|
monitoringToolMessage(api)
|
|
17
18
|
setOpenClawConfig(api.config)
|
|
19
|
+
startDcgchatGatewaySocket()
|
|
18
20
|
api.registerChannel({ plugin: dcgchatPlugin })
|
|
19
21
|
setWorkspaceDir(api.config?.agents?.defaults?.workspace)
|
|
20
22
|
api.registerTool((ctx) => {
|
package/package.json
CHANGED
package/src/bot.ts
CHANGED
|
@@ -3,13 +3,21 @@ import path from 'node:path'
|
|
|
3
3
|
import type { ReplyPayload } from 'openclaw/plugin-sdk'
|
|
4
4
|
import { createReplyPrefixContext } from 'openclaw/plugin-sdk'
|
|
5
5
|
import type { InboundMessage } from './types.js'
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
clearSentMediaKeys,
|
|
8
|
+
getDcgchatRuntime,
|
|
9
|
+
getOpenClawConfig,
|
|
10
|
+
getWorkspaceDir,
|
|
11
|
+
setMsgParamsSessionKey,
|
|
12
|
+
setMsgStatus
|
|
13
|
+
} from './utils/global.js'
|
|
7
14
|
import { resolveAccount, sendDcgchatMedia } from './channel.js'
|
|
8
15
|
import { generateSignUrl } from './request/api.js'
|
|
9
16
|
import { extractMobookFiles } from './utils/searchFile.js'
|
|
10
17
|
import { createMsgContext, sendChunk, sendFinal, sendText as sendTextMsg, sendError, sendText } from './transport.js'
|
|
11
18
|
import { dcgLogger } from './utils/log.js'
|
|
12
19
|
import { channelInfo, systemCommand, interruptCommand, ENV } from './utils/constant.js'
|
|
20
|
+
import { sendMessageToGateway } from './gateway/socket.js'
|
|
13
21
|
|
|
14
22
|
type MediaInfo = {
|
|
15
23
|
path: string
|
|
@@ -23,16 +31,16 @@ type TFileInfo = { name: string; url: string }
|
|
|
23
31
|
const mediaMaxBytes = 300 * 1024 * 1024
|
|
24
32
|
|
|
25
33
|
/** Active LLM generation abort controllers, keyed by conversationId */
|
|
26
|
-
const activeGenerations = new Map<string, AbortController>()
|
|
34
|
+
// const activeGenerations = new Map<string, AbortController>()
|
|
27
35
|
|
|
28
|
-
/** Abort an in-progress LLM generation for a given conversationId */
|
|
29
|
-
export function abortMobookappGeneration(conversationId: string): void {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
+
// /** Abort an in-progress LLM generation for a given conversationId */
|
|
37
|
+
// export function abortMobookappGeneration(conversationId: string): void {
|
|
38
|
+
// const ctrl = activeGenerations.get(conversationId)
|
|
39
|
+
// if (ctrl) {
|
|
40
|
+
// ctrl.abort()
|
|
41
|
+
// activeGenerations.delete(conversationId)
|
|
42
|
+
// }
|
|
43
|
+
// }
|
|
36
44
|
|
|
37
45
|
/**
|
|
38
46
|
* Extract agentId from conversation_id formatted as "agentId::suffix".
|
|
@@ -154,6 +162,8 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
154
162
|
const core = getDcgchatRuntime()
|
|
155
163
|
|
|
156
164
|
const conversationId = msg.content.session_id?.trim()
|
|
165
|
+
const agentId = msg.content.agent_id?.trim()
|
|
166
|
+
const realMobook = msg.content.real_mobook?.toString().trim()
|
|
157
167
|
|
|
158
168
|
const route = core.channel.routing.resolveAgentRoute({
|
|
159
169
|
cfg: config,
|
|
@@ -165,20 +175,20 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
165
175
|
// If conversation_id encodes an agentId prefix ("agentId::suffix"), override the route.
|
|
166
176
|
const embeddedAgentId = extractAgentIdFromConversationId(conversationId)
|
|
167
177
|
const effectiveAgentId = embeddedAgentId ?? route.agentId
|
|
168
|
-
const effectiveSessionKey =
|
|
169
|
-
? `agent
|
|
170
|
-
|
|
178
|
+
const effectiveSessionKey =
|
|
179
|
+
realMobook === '1' ? route.sessionKey : `agent:main:mobook:direct:${agentId}:${conversationId}`.toLowerCase()
|
|
180
|
+
setMsgParamsSessionKey(effectiveSessionKey)
|
|
171
181
|
|
|
172
182
|
const agentEntry =
|
|
173
183
|
effectiveAgentId && effectiveAgentId !== 'main' ? config.agents?.list?.find((a) => a.id === effectiveAgentId) : undefined
|
|
174
184
|
const agentDisplayName = agentEntry?.name ?? (effectiveAgentId && effectiveAgentId !== 'main' ? effectiveAgentId : undefined)
|
|
175
185
|
|
|
176
186
|
// Abort any existing generation for this conversation, then start a new one
|
|
177
|
-
const existingCtrl = activeGenerations.get(conversationId)
|
|
178
|
-
if (existingCtrl) existingCtrl.abort()
|
|
179
|
-
const genCtrl = new AbortController()
|
|
180
|
-
const genSignal = genCtrl.signal
|
|
181
|
-
activeGenerations.set(conversationId, genCtrl)
|
|
187
|
+
// const existingCtrl = activeGenerations.get(conversationId)
|
|
188
|
+
// if (existingCtrl) existingCtrl.abort()
|
|
189
|
+
// const genCtrl = new AbortController()
|
|
190
|
+
// const genSignal = genCtrl.signal
|
|
191
|
+
// activeGenerations.set(conversationId, genCtrl)
|
|
182
192
|
|
|
183
193
|
// 处理用户上传的文件
|
|
184
194
|
const files = msg.content.files ?? []
|
|
@@ -254,7 +264,6 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
254
264
|
}
|
|
255
265
|
})
|
|
256
266
|
|
|
257
|
-
let wasAborted = false
|
|
258
267
|
try {
|
|
259
268
|
if (systemCommand.includes(text?.trim())) {
|
|
260
269
|
dcgLogger(`dispatching /new`)
|
|
@@ -269,8 +278,14 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
269
278
|
})
|
|
270
279
|
} else if (interruptCommand.includes(text?.trim())) {
|
|
271
280
|
dcgLogger(`interrupt command: ${text}`)
|
|
272
|
-
abortMobookappGeneration(conversationId)
|
|
273
|
-
|
|
281
|
+
// abortMobookappGeneration(conversationId)
|
|
282
|
+
sendMessageToGateway(
|
|
283
|
+
JSON.stringify({
|
|
284
|
+
method: 'chat.abort',
|
|
285
|
+
params: { sessionKey: effectiveSessionKey }
|
|
286
|
+
})
|
|
287
|
+
)
|
|
288
|
+
sendFinal(msgCtx)
|
|
274
289
|
return
|
|
275
290
|
} else {
|
|
276
291
|
dcgLogger(`dispatching to agent (session=${route.sessionKey})`)
|
|
@@ -280,7 +295,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
280
295
|
dispatcher,
|
|
281
296
|
replyOptions: {
|
|
282
297
|
...replyOptions,
|
|
283
|
-
abortSignal: genSignal,
|
|
298
|
+
// abortSignal: genSignal,
|
|
284
299
|
onModelSelected: prefixContext.onModelSelected,
|
|
285
300
|
onPartialReply: async (payload: ReplyPayload) => {
|
|
286
301
|
// Accumulate full text
|
|
@@ -311,19 +326,19 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
311
326
|
})
|
|
312
327
|
}
|
|
313
328
|
} catch (err: unknown) {
|
|
314
|
-
if (genSignal.aborted) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
} else if (err instanceof Error && err.name === 'AbortError') {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
} else {
|
|
321
|
-
|
|
322
|
-
}
|
|
329
|
+
// if (genSignal.aborted) {
|
|
330
|
+
// wasAborted = true
|
|
331
|
+
// dcgLogger(` generation aborted for conversationId=${conversationId}`)
|
|
332
|
+
// } else if (err instanceof Error && err.name === 'AbortError') {
|
|
333
|
+
// wasAborted = true
|
|
334
|
+
// dcgLogger(` generation aborted for conversationId=${conversationId}`)
|
|
335
|
+
// } else {
|
|
336
|
+
// dcgLogger(` dispatchReplyFromConfig error: ${String(err)}`, 'error')
|
|
337
|
+
// }
|
|
323
338
|
} finally {
|
|
324
|
-
if (activeGenerations.get(conversationId) === genCtrl) {
|
|
325
|
-
|
|
326
|
-
}
|
|
339
|
+
// if (activeGenerations.get(conversationId) === genCtrl) {
|
|
340
|
+
// activeGenerations.delete(conversationId)
|
|
341
|
+
// }
|
|
327
342
|
}
|
|
328
343
|
try {
|
|
329
344
|
markRunComplete()
|
package/src/channel.ts
CHANGED
|
@@ -31,7 +31,7 @@ export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<v
|
|
|
31
31
|
const fileName = mediaUrl?.split(/[\\/]/).pop() || ''
|
|
32
32
|
|
|
33
33
|
try {
|
|
34
|
-
const url = opts.mediaUrl ? await ossUpload(opts.mediaUrl, msgCtx.botToken) : ''
|
|
34
|
+
const url = opts.mediaUrl ? await ossUpload(opts.mediaUrl, msgCtx.botToken, 1) : ''
|
|
35
35
|
wsSendRaw(msgCtx, {
|
|
36
36
|
response: opts.text ?? '',
|
|
37
37
|
files: [{ url, name: fileName }]
|
package/src/cron.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import crypto from 'node:crypto'
|
|
4
|
+
import { execFile } from 'node:child_process'
|
|
5
|
+
import { promisify } from 'node:util'
|
|
6
|
+
|
|
7
|
+
const execFileAsync = promisify(execFile)
|
|
8
|
+
import type { IMsgParams } from './types.js'
|
|
9
|
+
import { DcgchatMsgContext, sendEventMessage } from './transport.js'
|
|
10
|
+
import { getMsgParams, getWorkspaceDir } from './utils/global.js'
|
|
11
|
+
import { ossUpload } from './request/oss.js'
|
|
12
|
+
import { dcgLogger } from './utils/log.js'
|
|
13
|
+
import { sendMessageToGateway } from './gateway/socket.js'
|
|
14
|
+
|
|
15
|
+
export function getCronJobsPath(): string {
|
|
16
|
+
const workspaceDir = getWorkspaceDir()
|
|
17
|
+
const cronDir = workspaceDir.replace('workspace', 'cron')
|
|
18
|
+
return path.join(cronDir, 'jobs.json')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function msgParamsToCtx(p: IMsgParams): DcgchatMsgContext | null {
|
|
22
|
+
if (!p?.token) return null
|
|
23
|
+
return {
|
|
24
|
+
userId: p.userId,
|
|
25
|
+
botToken: p.token,
|
|
26
|
+
domainId: p.domainId,
|
|
27
|
+
appId: p.appId,
|
|
28
|
+
botId: p.botId,
|
|
29
|
+
agentId: p.agentId,
|
|
30
|
+
sessionId: p.sessionId,
|
|
31
|
+
messageId: p.messageId
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const CRON_UPLOAD_DEBOUNCE_MS = 30_000
|
|
36
|
+
|
|
37
|
+
/** 待合并的上传上下文(短时间内多次调用只保留最后一次) */
|
|
38
|
+
let pendingCronUploadCtx: DcgchatMsgContext | null = null
|
|
39
|
+
let cronUploadFlushTimer: ReturnType<typeof setTimeout> | null = null
|
|
40
|
+
|
|
41
|
+
async function runCronJobsUpload(msgCtx: DcgchatMsgContext): Promise<void> {
|
|
42
|
+
const jobPath = getCronJobsPath()
|
|
43
|
+
if (fs.existsSync(jobPath)) {
|
|
44
|
+
try {
|
|
45
|
+
const url = await ossUpload(jobPath, msgCtx.botToken, 0)
|
|
46
|
+
dcgLogger(`定时任务创建成功: ${url}`)
|
|
47
|
+
sendEventMessage(msgCtx, url)
|
|
48
|
+
} catch (error) {
|
|
49
|
+
dcgLogger(`${jobPath} upload failed: ${error}`, 'error')
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
dcgLogger(`${jobPath} not found`, 'error')
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function flushCronUploadQueue(): void {
|
|
57
|
+
cronUploadFlushTimer = null
|
|
58
|
+
const ctx = pendingCronUploadCtx
|
|
59
|
+
pendingCronUploadCtx = null
|
|
60
|
+
if (!ctx) return
|
|
61
|
+
void runCronJobsUpload(ctx)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 将 jobs.json 同步到 OSS 并推送事件。30s 内多次调用合并为一次上传;定时触发后清空待处理项,避免重复执行。
|
|
66
|
+
* @param msgCtx 可选;省略时使用当前 getMsgParams() 快照
|
|
67
|
+
*/
|
|
68
|
+
export function sendDcgchatCron(): void {
|
|
69
|
+
const ctx = msgParamsToCtx(getMsgParams() as IMsgParams)
|
|
70
|
+
if (!ctx) {
|
|
71
|
+
dcgLogger('sendDcgchatCron: no message context (missing token / params)', 'error')
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
pendingCronUploadCtx = ctx
|
|
75
|
+
if (cronUploadFlushTimer !== null) {
|
|
76
|
+
clearTimeout(cronUploadFlushTimer)
|
|
77
|
+
}
|
|
78
|
+
cronUploadFlushTimer = setTimeout(flushCronUploadQueue, CRON_UPLOAD_DEBOUNCE_MS)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 通过 OpenClaw CLI 删除定时任务(走 Gateway,与内存状态一致)。
|
|
83
|
+
* 文档:运行中请勿手改 jobs.json,应使用 `openclaw cron rm` 或工具 API。
|
|
84
|
+
*/
|
|
85
|
+
export const onRemoveCronJob = async (jobId: string) => {
|
|
86
|
+
const id = jobId?.trim()
|
|
87
|
+
if (!id) {
|
|
88
|
+
dcgLogger('onRemoveCronJob: empty jobId', 'error')
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
sendMessageToGateway(JSON.stringify({ method: 'cron.remove', params: { id: jobId } }))
|
|
92
|
+
}
|
|
93
|
+
export const onDisabledCronJob = async (jobId: string) => {
|
|
94
|
+
const id = jobId?.trim()
|
|
95
|
+
if (!id) {
|
|
96
|
+
dcgLogger('onRemoveCronJob: empty jobId', 'error')
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
sendMessageToGateway(JSON.stringify({ method: 'cron.update', params: { id: jobId, patch: { enabled: false } } }))
|
|
100
|
+
}
|
|
101
|
+
export const onEnabledCronJob = async (jobId: string) => {
|
|
102
|
+
const id = jobId?.trim()
|
|
103
|
+
if (!id) {
|
|
104
|
+
dcgLogger('onRemoveCronJob: empty jobId', 'error')
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
sendMessageToGateway(JSON.stringify({ method: 'cron.update', params: { id: jobId, patch: { enabled: true } } }))
|
|
108
|
+
}
|
|
109
|
+
export const onRunCronJob = async (jobId: string) => {
|
|
110
|
+
const id = jobId?.trim()
|
|
111
|
+
if (!id) {
|
|
112
|
+
dcgLogger('onRemoveCronJob: empty jobId', 'error')
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
sendMessageToGateway(JSON.stringify({ method: 'cron.update', jobId }))
|
|
116
|
+
}
|
|
117
|
+
export const updateCronJobSessionKey = async (jobId: string) => {
|
|
118
|
+
const id = jobId?.trim()
|
|
119
|
+
if (!id) {
|
|
120
|
+
dcgLogger('onRemoveCronJob: empty jobId', 'error')
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
const params = getMsgParams()
|
|
124
|
+
sendMessageToGateway(JSON.stringify({ method: 'cron.update', params: { id: jobId, patch: { sessionKey: params.sessionKey } } }))
|
|
125
|
+
}
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
// Gateway connection handler - connects to local OpenClaw gateway
|
|
2
|
+
import { WebSocket } from 'ws'
|
|
3
|
+
import crypto from 'crypto'
|
|
4
|
+
import { deriveDeviceIdFromPublicKey, publicKeyRawBase64UrlFromPem, buildDeviceAuthPayloadV3, signDevicePayload } from './security.js'
|
|
5
|
+
import { dcgLogger } from '../utils/log.js'
|
|
6
|
+
import { sendDcgchatCron, updateCronJobSessionKey } from '../cron.js'
|
|
7
|
+
|
|
8
|
+
export interface GatewayEvent {
|
|
9
|
+
type: string
|
|
10
|
+
payload?: Record<string, unknown>
|
|
11
|
+
seq?: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface GatewayHelloOk {
|
|
15
|
+
server: {
|
|
16
|
+
connId: string
|
|
17
|
+
}
|
|
18
|
+
features: {
|
|
19
|
+
methods: string[]
|
|
20
|
+
events: string[]
|
|
21
|
+
}
|
|
22
|
+
policy?: {
|
|
23
|
+
tickIntervalMs?: number
|
|
24
|
+
}
|
|
25
|
+
auth?: {
|
|
26
|
+
deviceToken?: string
|
|
27
|
+
role?: string
|
|
28
|
+
scopes?: string[]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface GatewayResponse {
|
|
33
|
+
type: 'res'
|
|
34
|
+
id: string
|
|
35
|
+
ok: boolean
|
|
36
|
+
/** 多数 RPC 成功时的返回值 */
|
|
37
|
+
result?: unknown
|
|
38
|
+
/** connect 及部分响应使用 payload(见 docs/gateway/protocol.md) */
|
|
39
|
+
payload?: unknown
|
|
40
|
+
error?: {
|
|
41
|
+
code: string
|
|
42
|
+
message: string
|
|
43
|
+
details?: Record<string, unknown>
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface GatewayEventFrame {
|
|
48
|
+
type: 'event'
|
|
49
|
+
event: string
|
|
50
|
+
payload?: Record<string, unknown>
|
|
51
|
+
seq?: number
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface GatewayHelloOkFrame {
|
|
55
|
+
type: 'hello-ok'
|
|
56
|
+
server: {
|
|
57
|
+
connId: string
|
|
58
|
+
}
|
|
59
|
+
features: {
|
|
60
|
+
methods: string[]
|
|
61
|
+
events: string[]
|
|
62
|
+
}
|
|
63
|
+
policy?: {
|
|
64
|
+
tickIntervalMs?: number
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Union of all possible gateway messages
|
|
69
|
+
export type GatewayMessage = GatewayEventFrame | GatewayResponse | GatewayHelloOkFrame
|
|
70
|
+
|
|
71
|
+
export interface GatewayConfig {
|
|
72
|
+
url: string
|
|
73
|
+
token: string
|
|
74
|
+
role: string
|
|
75
|
+
scopes: string[]
|
|
76
|
+
reconnectInterval?: number
|
|
77
|
+
maxReconnectAttempts?: number
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Gateway connection handler
|
|
82
|
+
*/
|
|
83
|
+
export class GatewayConnection {
|
|
84
|
+
private ws: WebSocket | null = null
|
|
85
|
+
private config: Required<GatewayConfig>
|
|
86
|
+
private deviceId: string
|
|
87
|
+
private privateKeyPem: string
|
|
88
|
+
private publicKeyB64Url: string
|
|
89
|
+
private connected: boolean = false
|
|
90
|
+
private connId: string | null = null
|
|
91
|
+
/** 服务端 connect.challenge 提供的 nonce,须与签名载荷一致 */
|
|
92
|
+
private connectChallengeNonce: string | null = null
|
|
93
|
+
private connectSent: boolean = false
|
|
94
|
+
private messageHandlers: Map<string, (response: GatewayResponse) => void> = new Map()
|
|
95
|
+
private eventHandlers: Set<(event: GatewayEvent) => void> = new Set()
|
|
96
|
+
|
|
97
|
+
constructor(config: GatewayConfig) {
|
|
98
|
+
this.config = {
|
|
99
|
+
url: config.url,
|
|
100
|
+
token: config.token,
|
|
101
|
+
role: config.role || 'operator',
|
|
102
|
+
scopes: config.scopes || ['operator.admin'],
|
|
103
|
+
reconnectInterval: config.reconnectInterval || 5000,
|
|
104
|
+
maxReconnectAttempts: config.maxReconnectAttempts || 10
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const identity = this.loadOrCreateDeviceIdentity()
|
|
108
|
+
// 必须与公钥指纹一致(deriveDeviceIdFromPublicKey),不可用随机 UUID
|
|
109
|
+
this.deviceId = identity.deviceId
|
|
110
|
+
this.privateKeyPem = identity.privateKeyPem
|
|
111
|
+
this.publicKeyB64Url = identity.publicKeyB64Url
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private loadOrCreateDeviceIdentity() {
|
|
115
|
+
const fs = require('fs')
|
|
116
|
+
const path = require('path')
|
|
117
|
+
const stateDir = path.join(process.cwd(), '.state')
|
|
118
|
+
const deviceFile = path.join(stateDir, 'device.json')
|
|
119
|
+
|
|
120
|
+
// Try to load existing identity
|
|
121
|
+
if (fs.existsSync(deviceFile)) {
|
|
122
|
+
const stored = JSON.parse(fs.readFileSync(deviceFile, 'utf8'))
|
|
123
|
+
if (stored.deviceId && stored.publicKeyPem && stored.privateKeyPem) {
|
|
124
|
+
const derivedId = deriveDeviceIdFromPublicKey(stored.publicKeyPem)
|
|
125
|
+
const deviceId = derivedId !== stored.deviceId ? derivedId : stored.deviceId
|
|
126
|
+
if (derivedId !== stored.deviceId) {
|
|
127
|
+
try {
|
|
128
|
+
fs.writeFileSync(deviceFile, JSON.stringify({ ...stored, deviceId: derivedId }, null, 2))
|
|
129
|
+
} catch {
|
|
130
|
+
/* keep in-memory fixed id only */
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
deviceId,
|
|
135
|
+
publicKeyPem: stored.publicKeyPem,
|
|
136
|
+
privateKeyPem: stored.privateKeyPem,
|
|
137
|
+
publicKeyB64Url: publicKeyRawBase64UrlFromPem(stored.publicKeyPem)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Create new identity
|
|
143
|
+
const keyPair = crypto.generateKeyPairSync('ed25519')
|
|
144
|
+
const publicKeyPem = keyPair.publicKey.export({ type: 'spki', format: 'pem' }).toString()
|
|
145
|
+
const privateKeyPem = keyPair.privateKey.export({ type: 'pkcs8', format: 'pem' }).toString()
|
|
146
|
+
const deviceId = deriveDeviceIdFromPublicKey(publicKeyPem)
|
|
147
|
+
const publicKeyB64Url = publicKeyRawBase64UrlFromPem(publicKeyPem)
|
|
148
|
+
|
|
149
|
+
// Ensure directory exists
|
|
150
|
+
if (!fs.existsSync(stateDir)) {
|
|
151
|
+
fs.mkdirSync(stateDir, { recursive: true })
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Save identity
|
|
155
|
+
fs.writeFileSync(
|
|
156
|
+
deviceFile,
|
|
157
|
+
JSON.stringify(
|
|
158
|
+
{
|
|
159
|
+
version: 1,
|
|
160
|
+
deviceId,
|
|
161
|
+
publicKeyPem,
|
|
162
|
+
privateKeyPem,
|
|
163
|
+
createdAtMs: Date.now()
|
|
164
|
+
},
|
|
165
|
+
null,
|
|
166
|
+
2
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
fs.chmodSync(deviceFile, 0o600)
|
|
170
|
+
|
|
171
|
+
return { deviceId, publicKeyPem, privateKeyPem, publicKeyB64Url }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Connect to the gateway
|
|
176
|
+
*/
|
|
177
|
+
async connect(): Promise<GatewayHelloOk> {
|
|
178
|
+
return new Promise((resolve, reject) => {
|
|
179
|
+
this.connectChallengeNonce = null
|
|
180
|
+
this.connectSent = false
|
|
181
|
+
this.ws = new WebSocket(this.config.url)
|
|
182
|
+
|
|
183
|
+
let handshakeSettled = false
|
|
184
|
+
const finishHandshake = (fn: () => void) => {
|
|
185
|
+
if (handshakeSettled) return
|
|
186
|
+
handshakeSettled = true
|
|
187
|
+
clearTimeout(timeout)
|
|
188
|
+
fn()
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const timeout = setTimeout(() => {
|
|
192
|
+
finishHandshake(() => reject(new Error('Gateway connection timeout')))
|
|
193
|
+
}, 15000)
|
|
194
|
+
|
|
195
|
+
this.ws.on('open', () => {
|
|
196
|
+
dcgLogger('Gateway connection opened(等待 connect.challenge)')
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
this.ws.on('message', (data) => {
|
|
200
|
+
try {
|
|
201
|
+
const msg = JSON.parse(data.toString())
|
|
202
|
+
this.handleMessage(
|
|
203
|
+
msg,
|
|
204
|
+
(hello) => finishHandshake(() => resolve(hello)),
|
|
205
|
+
(err) => finishHandshake(() => reject(err)),
|
|
206
|
+
timeout
|
|
207
|
+
)
|
|
208
|
+
} catch (err) {
|
|
209
|
+
dcgLogger(`[Gateway] 解析消息失败: ${err}`, 'error')
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
this.ws.on('close', () => {
|
|
214
|
+
this.connected = false
|
|
215
|
+
finishHandshake(() => reject(new Error('Gateway 在握手完成前关闭了连接')))
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
this.ws.on('error', (err) => {
|
|
219
|
+
console.log('🚀 ~ GatewayConnection ~ connect ~ err:', err)
|
|
220
|
+
finishHandshake(() => reject(err))
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Send initial connect request
|
|
227
|
+
*/
|
|
228
|
+
private sendConnect(): void {
|
|
229
|
+
if (this.connectSent) return
|
|
230
|
+
const nonce = this.connectChallengeNonce?.trim() ?? ''
|
|
231
|
+
if (!nonce) return
|
|
232
|
+
|
|
233
|
+
this.connectSent = true
|
|
234
|
+
const signedAtMs = Date.now()
|
|
235
|
+
const platform = process.platform
|
|
236
|
+
|
|
237
|
+
const payload = buildDeviceAuthPayloadV3({
|
|
238
|
+
deviceId: this.deviceId,
|
|
239
|
+
clientId: 'gateway-client',
|
|
240
|
+
clientMode: 'backend',
|
|
241
|
+
role: this.config.role,
|
|
242
|
+
scopes: this.config.scopes,
|
|
243
|
+
signedAtMs,
|
|
244
|
+
token: this.config.token,
|
|
245
|
+
nonce,
|
|
246
|
+
platform,
|
|
247
|
+
deviceFamily: ''
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
const signature = signDevicePayload(this.privateKeyPem, payload)
|
|
251
|
+
|
|
252
|
+
this.ws?.send(
|
|
253
|
+
JSON.stringify({
|
|
254
|
+
type: 'req',
|
|
255
|
+
id: '1',
|
|
256
|
+
method: 'connect',
|
|
257
|
+
params: {
|
|
258
|
+
minProtocol: 3,
|
|
259
|
+
maxProtocol: 3,
|
|
260
|
+
client: {
|
|
261
|
+
id: 'gateway-client',
|
|
262
|
+
version: '1.0.0',
|
|
263
|
+
platform,
|
|
264
|
+
mode: 'backend'
|
|
265
|
+
},
|
|
266
|
+
auth: { token: this.config.token },
|
|
267
|
+
role: this.config.role,
|
|
268
|
+
scopes: this.config.scopes,
|
|
269
|
+
device: {
|
|
270
|
+
id: this.deviceId,
|
|
271
|
+
publicKey: this.publicKeyB64Url,
|
|
272
|
+
signature,
|
|
273
|
+
signedAt: signedAtMs,
|
|
274
|
+
nonce
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Handle incoming messages
|
|
283
|
+
*/
|
|
284
|
+
private mapHelloPayloadToHelloOk(payload: Record<string, unknown>): GatewayHelloOk {
|
|
285
|
+
const serverRaw = payload.server as Record<string, unknown> | undefined
|
|
286
|
+
const featuresRaw = payload.features as Record<string, unknown> | undefined
|
|
287
|
+
return {
|
|
288
|
+
server: { connId: typeof serverRaw?.connId === 'string' ? serverRaw.connId : '' },
|
|
289
|
+
features: {
|
|
290
|
+
methods: Array.isArray(featuresRaw?.methods) ? (featuresRaw.methods as string[]) : [],
|
|
291
|
+
events: Array.isArray(featuresRaw?.events) ? (featuresRaw.events as string[]) : []
|
|
292
|
+
},
|
|
293
|
+
policy: payload.policy as GatewayHelloOk['policy'],
|
|
294
|
+
auth: payload.auth as GatewayHelloOk['auth']
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private handleMessage(
|
|
299
|
+
msg: Record<string, any>,
|
|
300
|
+
resolveHello: (helloOk: GatewayHelloOk) => void,
|
|
301
|
+
rejectHello: (err: Error) => void,
|
|
302
|
+
timeout: NodeJS.Timeout
|
|
303
|
+
): void {
|
|
304
|
+
const msgType = msg.type as string | undefined
|
|
305
|
+
|
|
306
|
+
if (msg.type === 'event' && msg.event === 'connect.challenge') {
|
|
307
|
+
const payload = msg.payload as Record<string, unknown> | undefined
|
|
308
|
+
const nonce = typeof payload?.nonce === 'string' ? payload.nonce.trim() : ''
|
|
309
|
+
if (!nonce) {
|
|
310
|
+
rejectHello(new Error('connect.challenge 缺少 nonce'))
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
this.connectChallengeNonce = nonce
|
|
314
|
+
this.sendConnect()
|
|
315
|
+
return
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// 协议 v3:握手成功为 type:"res" + payload.type:"hello-ok"(见 openclaw docs/gateway/protocol.md)
|
|
319
|
+
if (msg.type === 'res' && !this.connected) {
|
|
320
|
+
const ok = msg.ok === true
|
|
321
|
+
const inner = msg.payload as Record<string, unknown> | undefined
|
|
322
|
+
if (ok && inner && inner.type === 'hello-ok') {
|
|
323
|
+
this.connected = true
|
|
324
|
+
const serverRaw = inner.server as Record<string, unknown> | undefined
|
|
325
|
+
this.connId = typeof serverRaw?.connId === 'string' ? serverRaw.connId : null
|
|
326
|
+
clearTimeout(timeout)
|
|
327
|
+
dcgLogger(`[Gateway] 已连接 connId=${this.connId ?? '(none)'}`)
|
|
328
|
+
resolveHello(this.mapHelloPayloadToHelloOk(inner))
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
if (msg.ok === false) {
|
|
332
|
+
const errObj = msg.error as Record<string, unknown> | undefined
|
|
333
|
+
const message = (typeof errObj?.message === 'string' && errObj.message) || 'Gateway connect 被拒绝'
|
|
334
|
+
clearTimeout(timeout)
|
|
335
|
+
rejectHello(new Error(message))
|
|
336
|
+
return
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// 旧式或其它实现:顶层 hello-ok
|
|
341
|
+
if (msgType === 'hello-ok' || (!msgType && msg.server)) {
|
|
342
|
+
this.connected = true
|
|
343
|
+
this.connId = ((msg.server as Record<string, unknown>)?.connId as string) || null
|
|
344
|
+
clearTimeout(timeout)
|
|
345
|
+
dcgLogger(`[Gateway] 已连接 connId=${this.connId ?? '(none)'}`)
|
|
346
|
+
resolveHello(msg as unknown as GatewayHelloOk)
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
if (msg.type === 'res') {
|
|
350
|
+
const handler = this.messageHandlers.get(msg.id as string)
|
|
351
|
+
if (handler) {
|
|
352
|
+
this.messageHandlers.delete(msg.id as string)
|
|
353
|
+
handler(msg as unknown as GatewayResponse)
|
|
354
|
+
}
|
|
355
|
+
return
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (msg.type === 'event') {
|
|
359
|
+
if (msg.event === 'cron') {
|
|
360
|
+
dcgLogger(`[Gateway] 收到事件: ${JSON.stringify(msg)}`)
|
|
361
|
+
if (msg.payload?.action === 'added') {
|
|
362
|
+
updateCronJobSessionKey(msg.payload?.jobId as string)
|
|
363
|
+
}
|
|
364
|
+
if (msg.payload?.action === 'updated') {
|
|
365
|
+
sendDcgchatCron()
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const event: GatewayEvent = {
|
|
369
|
+
type: msg.event as string,
|
|
370
|
+
payload: msg.payload as Record<string, unknown> | undefined,
|
|
371
|
+
seq: msg.seq as number | undefined
|
|
372
|
+
}
|
|
373
|
+
this.eventHandlers.forEach((h) => h(event))
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Call a gateway method
|
|
379
|
+
*/
|
|
380
|
+
async callMethod<T = unknown>(method: string, params: Record<string, unknown> = {}): Promise<T> {
|
|
381
|
+
const id = crypto.randomUUID()
|
|
382
|
+
|
|
383
|
+
return new Promise((resolve, reject) => {
|
|
384
|
+
if (!this.connected || !this.ws) {
|
|
385
|
+
reject(new Error('Not connected to gateway'))
|
|
386
|
+
return
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const timeout = setTimeout(() => {
|
|
390
|
+
this.messageHandlers.delete(id)
|
|
391
|
+
reject(new Error('Method call timeout'))
|
|
392
|
+
}, 30000)
|
|
393
|
+
|
|
394
|
+
this.messageHandlers.set(id, (response) => {
|
|
395
|
+
clearTimeout(timeout)
|
|
396
|
+
if (response.ok) {
|
|
397
|
+
const body = response.result !== undefined ? response.result : (response as GatewayResponse).payload
|
|
398
|
+
resolve(body as T)
|
|
399
|
+
} else {
|
|
400
|
+
reject(new Error(response.error?.message || 'Method call failed'))
|
|
401
|
+
}
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
this.ws.send(
|
|
405
|
+
JSON.stringify({
|
|
406
|
+
type: 'req',
|
|
407
|
+
id,
|
|
408
|
+
method,
|
|
409
|
+
params
|
|
410
|
+
})
|
|
411
|
+
)
|
|
412
|
+
})
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Register event handler
|
|
417
|
+
*/
|
|
418
|
+
onEvent(handler: (event: GatewayEvent) => void): () => void {
|
|
419
|
+
this.eventHandlers.add(handler)
|
|
420
|
+
return () => this.eventHandlers.delete(handler)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Close connection
|
|
425
|
+
*/
|
|
426
|
+
close(): void {
|
|
427
|
+
this.ws?.close(1000, 'Plugin stopped')
|
|
428
|
+
this.connected = false
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Check if connected
|
|
433
|
+
*/
|
|
434
|
+
isConnected(): boolean {
|
|
435
|
+
return this.connected
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Get connection ID
|
|
440
|
+
*/
|
|
441
|
+
getConnId(): string | null {
|
|
442
|
+
return this.connId
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Get the WebSocket instance (for external use)
|
|
447
|
+
*/
|
|
448
|
+
getWebSocket(): WebSocket | null {
|
|
449
|
+
return this.ws
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Ping the gateway
|
|
454
|
+
*/
|
|
455
|
+
ping(): void {
|
|
456
|
+
this.ws?.ping()
|
|
457
|
+
}
|
|
458
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Security utilities for the tunnel plugin
|
|
2
|
+
import crypto from 'crypto'
|
|
3
|
+
import fs from 'fs'
|
|
4
|
+
import jwt from 'jsonwebtoken'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
|
|
7
|
+
// ED25519 SubjectPublicKeyInfo prefix (must match openclaw gateway `ED25519_SPKI_PREFIX`)
|
|
8
|
+
const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex')
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Raw 32-byte Ed25519 public key bytes from PEM (same rules as openclaw `derivePublicKeyRaw`).
|
|
12
|
+
*/
|
|
13
|
+
export function derivePublicKeyRawFromPem(publicKeyPem: string): Buffer {
|
|
14
|
+
const spki = crypto.createPublicKey(publicKeyPem).export({ type: 'spki', format: 'der' }) as Buffer
|
|
15
|
+
if (
|
|
16
|
+
spki.length === ED25519_SPKI_PREFIX.length + 32 &&
|
|
17
|
+
spki.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)
|
|
18
|
+
) {
|
|
19
|
+
return spki.subarray(ED25519_SPKI_PREFIX.length)
|
|
20
|
+
}
|
|
21
|
+
return spki
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Base64url encode (no padding)
|
|
26
|
+
*/
|
|
27
|
+
export function base64UrlEncode(buffer: Buffer): string {
|
|
28
|
+
return buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Wire-format device public key (base64url of raw 32 bytes), matches gateway expectations. */
|
|
32
|
+
export function publicKeyRawBase64UrlFromPem(publicKeyPem: string): string {
|
|
33
|
+
return base64UrlEncode(derivePublicKeyRawFromPem(publicKeyPem))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Derive device ID from ED25519 public key
|
|
38
|
+
* Device ID = sha256(rawPublicKey)
|
|
39
|
+
*/
|
|
40
|
+
export function deriveDeviceIdFromPublicKey(publicKeyPem: string): string {
|
|
41
|
+
const rawPublicKey = derivePublicKeyRawFromPem(publicKeyPem)
|
|
42
|
+
return crypto.createHash('sha256').update(rawPublicKey).digest('hex')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Sign device payload
|
|
47
|
+
*/
|
|
48
|
+
export function signDevicePayload(privateKeyPem: string, payload: string, encoding: 'base64' | 'base64url' = 'base64url'): string {
|
|
49
|
+
const key = crypto.createPrivateKey(privateKeyPem)
|
|
50
|
+
const sig = crypto.sign(null, Buffer.from(payload, 'utf8'), key)
|
|
51
|
+
return encoding === 'base64url' ? base64UrlEncode(sig) : sig.toString('base64')
|
|
52
|
+
}
|
|
53
|
+
function normalizeTrimmedMetadata(value: unknown): string {
|
|
54
|
+
if (typeof value !== 'string') return ''
|
|
55
|
+
const trimmed = value.trim()
|
|
56
|
+
return trimmed ? trimmed : ''
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function toLowerAscii(input: string): string {
|
|
60
|
+
return input.replace(/[A-Z]/g, (char) => String.fromCharCode(char.charCodeAt(0) + 32))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalizeDeviceMetadataForAuth(value: unknown): string {
|
|
64
|
+
const trimmed = normalizeTrimmedMetadata(value)
|
|
65
|
+
if (!trimmed) return ''
|
|
66
|
+
return toLowerAscii(trimmed)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Device authentication payload v3(与 openclaw `buildDeviceAuthPayloadV3` 一致)
|
|
71
|
+
*/
|
|
72
|
+
export function buildDeviceAuthPayloadV3(params: {
|
|
73
|
+
deviceId: string
|
|
74
|
+
clientId: string
|
|
75
|
+
clientMode: string
|
|
76
|
+
role: string
|
|
77
|
+
scopes: string[]
|
|
78
|
+
signedAtMs: number
|
|
79
|
+
token: string
|
|
80
|
+
nonce: string
|
|
81
|
+
platform?: string
|
|
82
|
+
deviceFamily?: string
|
|
83
|
+
}): string {
|
|
84
|
+
const scopes = params.scopes.join(',')
|
|
85
|
+
const token = params.token ?? ''
|
|
86
|
+
const platform = normalizeDeviceMetadataForAuth(params.platform ?? '')
|
|
87
|
+
const deviceFamily = normalizeDeviceMetadataForAuth(params.deviceFamily ?? '')
|
|
88
|
+
return [
|
|
89
|
+
'v3',
|
|
90
|
+
params.deviceId,
|
|
91
|
+
params.clientId,
|
|
92
|
+
params.clientMode,
|
|
93
|
+
params.role,
|
|
94
|
+
scopes,
|
|
95
|
+
String(params.signedAtMs),
|
|
96
|
+
token,
|
|
97
|
+
params.nonce,
|
|
98
|
+
platform,
|
|
99
|
+
deviceFamily
|
|
100
|
+
].join('|')
|
|
101
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
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
|
+
let connectInFlight = false
|
|
103
|
+
let socketStopped = true
|
|
104
|
+
/** 用于忽略「已被替换的旧连接」上的 close/error */
|
|
105
|
+
let socketGeneration = 0
|
|
106
|
+
|
|
107
|
+
function clearPingTimer(): void {
|
|
108
|
+
if (pingTimer) {
|
|
109
|
+
clearInterval(pingTimer)
|
|
110
|
+
pingTimer = null
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function clearReconnectTimer(): void {
|
|
115
|
+
if (reconnectTimer) {
|
|
116
|
+
clearTimeout(reconnectTimer)
|
|
117
|
+
reconnectTimer = null
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function startPingTimer(gw: GatewayConnection): void {
|
|
122
|
+
clearPingTimer()
|
|
123
|
+
pingTimer = setInterval(() => {
|
|
124
|
+
if (!gw.isConnected()) return
|
|
125
|
+
try {
|
|
126
|
+
gw.ping()
|
|
127
|
+
} catch (e) {
|
|
128
|
+
dcgLogger(`Gateway ping 失败: ${e}`, 'error')
|
|
129
|
+
}
|
|
130
|
+
}, PING_INTERVAL_MS)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function scheduleReconnect(): void {
|
|
134
|
+
if (socketStopped || reconnectTimer) return
|
|
135
|
+
reconnectTimer = setTimeout(() => {
|
|
136
|
+
reconnectTimer = null
|
|
137
|
+
void connectPersistentGateway()
|
|
138
|
+
}, RECONNECT_DELAY_MS)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function attachSocketLifecycle(gw: GatewayConnection, generation: number): void {
|
|
142
|
+
const ws = gw.getWebSocket()
|
|
143
|
+
if (!ws) return
|
|
144
|
+
|
|
145
|
+
const onDown = () => {
|
|
146
|
+
if (generation !== socketGeneration) return
|
|
147
|
+
clearPingTimer()
|
|
148
|
+
if (persistentConn === gw) {
|
|
149
|
+
persistentConn = null
|
|
150
|
+
}
|
|
151
|
+
dcgLogger('Gateway WebSocket 已断开,将在 5s 后重连', 'error')
|
|
152
|
+
if (!socketStopped) {
|
|
153
|
+
scheduleReconnect()
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
ws.once('close', onDown)
|
|
158
|
+
ws.once('error', onDown)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function connectPersistentGateway(): Promise<void> {
|
|
162
|
+
if (socketStopped || connectInFlight) return
|
|
163
|
+
|
|
164
|
+
const cfg = resolveConfigSafe()
|
|
165
|
+
if (!cfg) return
|
|
166
|
+
|
|
167
|
+
connectInFlight = true
|
|
168
|
+
try {
|
|
169
|
+
socketGeneration += 1
|
|
170
|
+
const generation = socketGeneration
|
|
171
|
+
|
|
172
|
+
if (persistentConn) {
|
|
173
|
+
try {
|
|
174
|
+
persistentConn.close()
|
|
175
|
+
} catch {
|
|
176
|
+
/* ignore */
|
|
177
|
+
}
|
|
178
|
+
persistentConn = null
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const gw = new GatewayConnection(cfg)
|
|
182
|
+
await gw.connect()
|
|
183
|
+
if (socketStopped) {
|
|
184
|
+
try {
|
|
185
|
+
gw.close()
|
|
186
|
+
} catch {
|
|
187
|
+
/* ignore */
|
|
188
|
+
}
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
if (generation !== socketGeneration) {
|
|
192
|
+
try {
|
|
193
|
+
gw.close()
|
|
194
|
+
} catch {
|
|
195
|
+
/* ignore */
|
|
196
|
+
}
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
persistentConn = gw
|
|
200
|
+
attachSocketLifecycle(gw, generation)
|
|
201
|
+
startPingTimer(gw)
|
|
202
|
+
dcgLogger(`Gateway 持久连接成功 connId=${gw.getConnId() ?? '?'}`)
|
|
203
|
+
} catch (e) {
|
|
204
|
+
dcgLogger(`Gateway 连接失败11: ${e}`, 'error')
|
|
205
|
+
persistentConn = null
|
|
206
|
+
clearPingTimer()
|
|
207
|
+
if (!socketStopped) {
|
|
208
|
+
scheduleReconnect()
|
|
209
|
+
}
|
|
210
|
+
} finally {
|
|
211
|
+
connectInFlight = false
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 插件 register 后调用:建立到 OpenClaw Gateway 的长连接,30s 一次 WebSocket ping,断线每 5s 重连。
|
|
217
|
+
*/
|
|
218
|
+
export function startDcgchatGatewaySocket(): void {
|
|
219
|
+
socketStopped = false
|
|
220
|
+
clearReconnectTimer()
|
|
221
|
+
setTimeout(() => {
|
|
222
|
+
void connectPersistentGateway()
|
|
223
|
+
}, 10000)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 停止长连接与重连(例如测试或显式下线)。
|
|
228
|
+
*/
|
|
229
|
+
export function stopDcgchatGatewaySocket(): void {
|
|
230
|
+
socketStopped = true
|
|
231
|
+
clearReconnectTimer()
|
|
232
|
+
clearPingTimer()
|
|
233
|
+
if (persistentConn) {
|
|
234
|
+
try {
|
|
235
|
+
persistentConn.close()
|
|
236
|
+
} catch {
|
|
237
|
+
/* ignore */
|
|
238
|
+
}
|
|
239
|
+
persistentConn = null
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function isDcgchatGatewaySocketConnected(): boolean {
|
|
244
|
+
return persistentConn?.isConnected() ?? false
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 在已连接时调用 Gateway RPC;`params` 完全由调用方传入,不做固定结构。
|
|
249
|
+
*/
|
|
250
|
+
export async function callGatewayMethod<T = unknown>(method: string, params: Record<string, unknown> = {}): Promise<T> {
|
|
251
|
+
const gw = persistentConn
|
|
252
|
+
if (!gw?.isConnected()) {
|
|
253
|
+
throw new Error('Gateway 未连接(等待重连或检查配置)')
|
|
254
|
+
}
|
|
255
|
+
return gw.callMethod<T>(method, params)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* 使用对象形式发送 RPC(推荐)。
|
|
260
|
+
*/
|
|
261
|
+
export async function sendGatewayRpc<T = unknown>(payload: GatewayRpcPayload): Promise<T> {
|
|
262
|
+
return callGatewayMethod<T>(payload.method, payload.params ?? {})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* 兼容:字符串方法名,或 JSON 字符串 `{ method, params? }`。
|
|
267
|
+
*/
|
|
268
|
+
export async function sendMessageToGateway(message: string): Promise<unknown> {
|
|
269
|
+
const { method, params } = parseGatewayRpcMessage(message)
|
|
270
|
+
return callGatewayMethod(method, params)
|
|
271
|
+
}
|
package/src/monitor.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { setMsgParams, setMsgStatus } from './utils/global.js'
|
|
|
8
8
|
import { installSkill, uninstallSkill } from './skill.js'
|
|
9
9
|
import { dcgLogger } from './utils/log.js'
|
|
10
10
|
import { ignoreToolCommand } from './utils/constant.js'
|
|
11
|
+
import { onDisabledCronJob, onEnabledCronJob, onRemoveCronJob, onRunCronJob } from './cron.js'
|
|
11
12
|
|
|
12
13
|
export type MonitorDcgchatOpts = {
|
|
13
14
|
config?: ClawdbotConfig
|
|
@@ -145,19 +146,18 @@ export async function monitorDcgchatProvider(opts: MonitorDcgchatOpts): Promise<
|
|
|
145
146
|
|
|
146
147
|
await handleDcgchatMessage(msg, account.accountId)
|
|
147
148
|
} else if (parsed.messageType == 'openclaw_bot_event') {
|
|
148
|
-
const { event_type, operation_type
|
|
149
|
-
? parsed.content
|
|
150
|
-
: ({} as Record<string, any>)
|
|
151
|
-
const content = {
|
|
152
|
-
event_type,
|
|
153
|
-
operation_type,
|
|
154
|
-
skill_url,
|
|
155
|
-
skill_code,
|
|
156
|
-
skill_id,
|
|
157
|
-
bot_token,
|
|
158
|
-
websocket_trace_id
|
|
159
|
-
}
|
|
149
|
+
const { event_type, operation_type } = parsed.content ? parsed.content : ({} as Record<string, any>)
|
|
160
150
|
if (event_type === 'skill') {
|
|
151
|
+
const { skill_url, skill_code, skill_id, bot_token, websocket_trace_id } = parsed.content
|
|
152
|
+
const content = {
|
|
153
|
+
event_type,
|
|
154
|
+
operation_type,
|
|
155
|
+
skill_url,
|
|
156
|
+
skill_code,
|
|
157
|
+
skill_id,
|
|
158
|
+
bot_token,
|
|
159
|
+
websocket_trace_id
|
|
160
|
+
}
|
|
161
161
|
if (operation_type === 'install' || operation_type === 'enable' || operation_type === 'update') {
|
|
162
162
|
installSkill({ path: skill_url, code: skill_code }, content)
|
|
163
163
|
} else if (operation_type === 'remove' || operation_type === 'disable') {
|
|
@@ -165,6 +165,17 @@ export async function monitorDcgchatProvider(opts: MonitorDcgchatOpts): Promise<
|
|
|
165
165
|
} else {
|
|
166
166
|
dcgLogger(`openclaw_bot_event unknown event_type: ${event_type}, ${data.toString()}`)
|
|
167
167
|
}
|
|
168
|
+
} else if (event_type === 'cron') {
|
|
169
|
+
const { job_id } = parsed.content
|
|
170
|
+
if (operation_type === 'remove') {
|
|
171
|
+
await onRemoveCronJob(job_id)
|
|
172
|
+
} else if (operation_type === 'enable') {
|
|
173
|
+
await onEnabledCronJob(job_id)
|
|
174
|
+
} else if (operation_type === 'disable') {
|
|
175
|
+
await onDisabledCronJob(job_id)
|
|
176
|
+
} else if (operation_type === 'exec') {
|
|
177
|
+
await onRunCronJob(job_id)
|
|
178
|
+
}
|
|
168
179
|
} else {
|
|
169
180
|
dcgLogger(`openclaw_bot_event unknown operation_type: ${operation_type}, ${data.toString()}`)
|
|
170
181
|
}
|
package/src/request/api.ts
CHANGED
|
@@ -3,18 +3,11 @@ import type { IStsToken, IStsTokenReq } from '../types.js'
|
|
|
3
3
|
import { getUserTokenCache, setUserTokenCache } from './userInfo.js'
|
|
4
4
|
import { dcgLogger } from '../utils/log.js'
|
|
5
5
|
|
|
6
|
-
export const getStsToken = async (name: string, botToken: string) => {
|
|
6
|
+
export const getStsToken = async (name: string, botToken: string, isPrivate: 1 | 0) => {
|
|
7
7
|
// 确保 userToken 已缓存(如果未缓存会自动获取并缓存)
|
|
8
8
|
await getUserToken(botToken)
|
|
9
9
|
|
|
10
|
-
const response = await post<IStsTokenReq, IStsToken>(
|
|
11
|
-
'/user/getStsToken',
|
|
12
|
-
{
|
|
13
|
-
sourceFileName: name,
|
|
14
|
-
isPrivate: 1
|
|
15
|
-
},
|
|
16
|
-
{ botToken }
|
|
17
|
-
)
|
|
10
|
+
const response = await post<IStsTokenReq, IStsToken>('/user/getStsToken', { sourceFileName: name, isPrivate }, { botToken })
|
|
18
11
|
|
|
19
12
|
if (!response || !response.data || !response.data.bucket) {
|
|
20
13
|
throw new Error('获取 OSS 临时凭证失败')
|
|
@@ -27,11 +20,7 @@ export const generateSignUrl = async (file_url: string, botToken: string) => {
|
|
|
27
20
|
// 确保 userToken 已缓存(如果未缓存会自动获取并缓存)
|
|
28
21
|
await getUserToken(botToken)
|
|
29
22
|
|
|
30
|
-
const response = await post<any>(
|
|
31
|
-
'/user/generateSignUrl',
|
|
32
|
-
{ loudPlatform: 0, fileName: file_url },
|
|
33
|
-
{ botToken }
|
|
34
|
-
)
|
|
23
|
+
const response = await post<any>('/user/generateSignUrl', { loudPlatform: 0, fileName: file_url }, { botToken })
|
|
35
24
|
if (response.code === 0 && response.data) {
|
|
36
25
|
// @ts-ignore
|
|
37
26
|
return response.data?.filePath
|
|
@@ -48,10 +37,7 @@ export const generateSignUrl = async (file_url: string, botToken: string) => {
|
|
|
48
37
|
* @returns userToken
|
|
49
38
|
*/
|
|
50
39
|
export const queryUserTokenByBotToken = async (botToken: string): Promise<string> => {
|
|
51
|
-
const response = await post<{ botToken: string }, { token: string }>(
|
|
52
|
-
'/organization/queryUserTokenByBotToken',
|
|
53
|
-
{ botToken }
|
|
54
|
-
)
|
|
40
|
+
const response = await post<{ botToken: string }, { token: string }>('/organization/queryUserTokenByBotToken', { botToken })
|
|
55
41
|
|
|
56
42
|
if (!response || !response.data || !response.data.token) {
|
|
57
43
|
dcgLogger('获取绑定的用户信息失败', 'error')
|
package/src/request/oss.ts
CHANGED
|
@@ -22,11 +22,11 @@ async function toUploadContent(
|
|
|
22
22
|
return { content: buf, fileName: input.name }
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export const ossUpload = async (file: File | string | Buffer, botToken: string) => {
|
|
25
|
+
export const ossUpload = async (file: File | string | Buffer, botToken: string, isPrivate: 0 | 1 = 1) => {
|
|
26
26
|
await getUserToken(botToken)
|
|
27
27
|
|
|
28
28
|
const { content, fileName } = await toUploadContent(file)
|
|
29
|
-
const data = await getStsToken(fileName, botToken)
|
|
29
|
+
const data = await getStsToken(fileName, botToken, isPrivate)
|
|
30
30
|
|
|
31
31
|
const options: OSS.Options = {
|
|
32
32
|
// 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。
|
|
@@ -50,8 +50,9 @@ export const ossUpload = async (file: File | string | Buffer, botToken: string)
|
|
|
50
50
|
if (objectResult?.res?.status !== 200) {
|
|
51
51
|
dcgLogger(`OSS 上传失败, ${objectResult?.res?.status}`)
|
|
52
52
|
}
|
|
53
|
-
dcgLogger(
|
|
54
|
-
|
|
53
|
+
dcgLogger(`OSS 上传成功, ${objectResult.name || objectResult.url}`)
|
|
54
|
+
// const url = `${data.protocol || 'http'}://${data.bucket}.${data.endPoint}/${data.uploadDir}${data.ossFileKey}`
|
|
55
|
+
return isPrivate === 1 ? objectResult.name || objectResult.url : objectResult.url
|
|
55
56
|
} catch (error) {
|
|
56
57
|
dcgLogger(`OSS 上传失败: ${error}`, 'error')
|
|
57
58
|
}
|
package/src/tool.ts
CHANGED
|
@@ -80,12 +80,12 @@ function sendToolCallMessage(text: string, toolCallId: string, isCover: number)
|
|
|
80
80
|
|
|
81
81
|
export function monitoringToolMessage(api: OpenClawPluginApi) {
|
|
82
82
|
for (const item of eventList) {
|
|
83
|
-
api.on(item.event as PluginHookName, (event: any) => {
|
|
83
|
+
api.on(item.event as PluginHookName, (event: any, args: any) => {
|
|
84
84
|
const status = getMsgStatus()
|
|
85
85
|
if (status === 'running') {
|
|
86
|
-
dcgLogger(`工具调用结果: ~ event:${item.event}`)
|
|
87
86
|
if (['after_tool_call', 'before_tool_call'].includes(item.event)) {
|
|
88
87
|
const { result: _result, ...rest } = event
|
|
88
|
+
dcgLogger(`工具调用结果: ~ event:${item.event} ~ params:${JSON.stringify(rest)}`)
|
|
89
89
|
const text = JSON.stringify({
|
|
90
90
|
type: item.event,
|
|
91
91
|
specialIdentification: 'dcgchat_tool_call_special_identification',
|
|
@@ -95,7 +95,9 @@ export function monitoringToolMessage(api: OpenClawPluginApi) {
|
|
|
95
95
|
})
|
|
96
96
|
sendToolCallMessage(text, event.toolCallId || event.runId || Date.now().toString(), item.event === 'after_tool_call' ? 1 : 0)
|
|
97
97
|
} else if (item.event) {
|
|
98
|
+
dcgLogger(`工具调用结果: ~ event:${item.event}`)
|
|
98
99
|
if (item.event === 'llm_output') {
|
|
100
|
+
dcgLogger(`llm_output工具调用结果: ~ event:${JSON.stringify(event)}`)
|
|
99
101
|
if (event.lastAssistant?.errorMessage === '429-账户额度耗尽') {
|
|
100
102
|
const params = getMsgParams()
|
|
101
103
|
const message = '您的积分已消耗完,您可以通过充值积分来继续使用'
|
package/src/transport.ts
CHANGED
|
@@ -106,3 +106,26 @@ export function sendText(ctx: DcgchatMsgContext, text: string, event?: Record<st
|
|
|
106
106
|
export function sendError(ctx: DcgchatMsgContext, errorMsg: string): boolean {
|
|
107
107
|
return wsSend(ctx, { response: `[错误] ${errorMsg}`, state: 'final' })
|
|
108
108
|
}
|
|
109
|
+
export function sendEventMessage(ctx: DcgchatMsgContext, url: string) {
|
|
110
|
+
const ws = getWsConnection()
|
|
111
|
+
if (isWsOpen()) {
|
|
112
|
+
ws?.send(
|
|
113
|
+
JSON.stringify({
|
|
114
|
+
messageType: 'openclaw_bot_event',
|
|
115
|
+
source: 'client',
|
|
116
|
+
content: {
|
|
117
|
+
event_type: 'cron',
|
|
118
|
+
operation_type: 'install',
|
|
119
|
+
bot_token: ctx.botToken,
|
|
120
|
+
domain_id: ctx.domainId,
|
|
121
|
+
app_id: ctx.appId,
|
|
122
|
+
oss_url: url,
|
|
123
|
+
bot_id: ctx.botId,
|
|
124
|
+
agent_id: ctx.agentId,
|
|
125
|
+
session_id: ctx.sessionId,
|
|
126
|
+
message_id: ctx.messageId || Date.now().toString()
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -44,6 +44,7 @@ export type InboundMessage = {
|
|
|
44
44
|
bot_id?: string
|
|
45
45
|
agent_id?: string
|
|
46
46
|
session_id: string
|
|
47
|
+
real_mobook: string | number
|
|
47
48
|
message_id: string
|
|
48
49
|
text: string
|
|
49
50
|
files?: {
|
|
@@ -123,4 +124,5 @@ export interface IMsgParams {
|
|
|
123
124
|
appId: string
|
|
124
125
|
botId: string
|
|
125
126
|
agentId: string
|
|
127
|
+
sessionKey: string
|
|
126
128
|
}
|
package/src/utils/global.ts
CHANGED
|
@@ -76,6 +76,9 @@ export function setMsgParams(params: any) {
|
|
|
76
76
|
export function getMsgParams() {
|
|
77
77
|
return msgParams
|
|
78
78
|
}
|
|
79
|
+
export function setMsgParamsSessionKey(sessionKey: string) {
|
|
80
|
+
if (sessionKey) msgParams.sessionKey = sessionKey
|
|
81
|
+
}
|
|
79
82
|
|
|
80
83
|
let msgStatus: 'running' | 'finished' | '' = ''
|
|
81
84
|
export function setMsgStatus(status: 'running' | 'finished' | '') {
|