@dcrays/dcgchat-test 0.3.0 → 0.3.1
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/package.json +1 -1
- package/src/bot.ts +53 -58
- package/src/channel.ts +25 -31
- package/src/cron.ts +35 -22
- package/src/gateway/index.ts +22 -12
- package/src/gateway/security.ts +1 -7
- package/src/gateway/socket.ts +16 -2
- package/src/monitor.ts +1 -15
- package/src/request/request.ts +2 -2
- package/src/tool.ts +13 -18
- package/src/transport.ts +118 -48
- package/src/types.ts +11 -9
- package/src/utils/global.ts +0 -12
- package/src/utils/params.ts +65 -0
package/package.json
CHANGED
package/src/bot.ts
CHANGED
|
@@ -3,21 +3,15 @@ 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 {
|
|
7
|
-
clearSentMediaKeys,
|
|
8
|
-
getDcgchatRuntime,
|
|
9
|
-
getOpenClawConfig,
|
|
10
|
-
getWorkspaceDir,
|
|
11
|
-
setMsgParamsSessionKey,
|
|
12
|
-
setMsgStatus
|
|
13
|
-
} from './utils/global.js'
|
|
6
|
+
import { clearSentMediaKeys, getDcgchatRuntime, getOpenClawConfig, getWorkspaceDir, setMsgStatus } from './utils/global.js'
|
|
14
7
|
import { resolveAccount, sendDcgchatMedia } from './channel.js'
|
|
15
8
|
import { generateSignUrl } from './request/api.js'
|
|
16
9
|
import { extractMobookFiles } from './utils/searchFile.js'
|
|
17
|
-
import {
|
|
10
|
+
import { sendChunk, sendFinal, sendText as sendTextMsg, sendError } from './transport.js'
|
|
18
11
|
import { dcgLogger } from './utils/log.js'
|
|
19
12
|
import { channelInfo, systemCommand, interruptCommand, ENV } from './utils/constant.js'
|
|
20
13
|
import { sendMessageToGateway } from './gateway/socket.js'
|
|
14
|
+
import { getEffectiveMsgParams, setParamsMessage } from './utils/params.js'
|
|
21
15
|
|
|
22
16
|
type MediaInfo = {
|
|
23
17
|
path: string
|
|
@@ -33,15 +27,6 @@ const mediaMaxBytes = 300 * 1024 * 1024
|
|
|
33
27
|
/** Active LLM generation abort controllers, keyed by conversationId */
|
|
34
28
|
// const activeGenerations = new Map<string, AbortController>()
|
|
35
29
|
|
|
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
|
-
// }
|
|
44
|
-
|
|
45
30
|
/**
|
|
46
31
|
* Extract agentId from conversation_id formatted as "agentId::suffix".
|
|
47
32
|
* Returns null if the conversation_id does not contain the "::" separator.
|
|
@@ -132,16 +117,8 @@ function resolveReplyMediaList(payload: ReplyPayload): string[] {
|
|
|
132
117
|
* 处理一条用户消息,调用 Agent 并返回回复
|
|
133
118
|
*/
|
|
134
119
|
export async function handleDcgchatMessage(msg: InboundMessage, accountId: string): Promise<void> {
|
|
135
|
-
const msgCtx = createMsgContext(msg)
|
|
136
|
-
|
|
137
120
|
let finalSent = false
|
|
138
121
|
|
|
139
|
-
const safeSendFinal = () => {
|
|
140
|
-
if (finalSent) return
|
|
141
|
-
finalSent = true
|
|
142
|
-
sendFinal(msgCtx)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
122
|
let completeText = ''
|
|
146
123
|
const config = getOpenClawConfig()
|
|
147
124
|
if (!config) {
|
|
@@ -150,39 +127,58 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
150
127
|
}
|
|
151
128
|
const account = resolveAccount(config, accountId)
|
|
152
129
|
const userId = msg._userId.toString()
|
|
130
|
+
|
|
131
|
+
const core = getDcgchatRuntime()
|
|
132
|
+
|
|
133
|
+
const conversationId = msg.content.session_id?.trim()
|
|
134
|
+
const agentId = msg.content.agent_id?.trim()
|
|
135
|
+
const real_mobook = msg.content.real_mobook?.toString().trim()
|
|
136
|
+
|
|
137
|
+
const route = core.channel.routing.resolveAgentRoute({
|
|
138
|
+
cfg: config,
|
|
139
|
+
channel: "dcgchat-test",
|
|
140
|
+
accountId: account.accountId,
|
|
141
|
+
peer: { kind: 'direct', id: conversationId }
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
const embeddedAgentId = extractAgentIdFromConversationId(conversationId)
|
|
145
|
+
|
|
146
|
+
const effectiveAgentId = embeddedAgentId ?? route.agentId
|
|
147
|
+
const effectiveSessionKey =
|
|
148
|
+
real_mobook === '1' ? route.sessionKey : `agent:main:mobook:direct:${agentId}:${conversationId}`.toLowerCase()
|
|
149
|
+
|
|
150
|
+
setParamsMessage(effectiveSessionKey, {
|
|
151
|
+
userId: msg._userId,
|
|
152
|
+
botToken: msg.content.bot_token,
|
|
153
|
+
sessionId: conversationId,
|
|
154
|
+
messageId: msg.content.message_id,
|
|
155
|
+
domainId: msg.content.domain_id,
|
|
156
|
+
appId: msg.content.app_id,
|
|
157
|
+
botId: msg.content.bot_id ?? '',
|
|
158
|
+
agentId: msg.content.agent_id ?? '',
|
|
159
|
+
sessionKey: effectiveSessionKey,
|
|
160
|
+
real_mobook
|
|
161
|
+
})
|
|
162
|
+
const outboundCtx = getEffectiveMsgParams(effectiveSessionKey)
|
|
163
|
+
const agentEntry =
|
|
164
|
+
effectiveAgentId && effectiveAgentId !== 'main' ? config.agents?.list?.find((a) => a.id === effectiveAgentId) : undefined
|
|
165
|
+
const agentDisplayName = agentEntry?.name ?? (effectiveAgentId && effectiveAgentId !== 'main' ? effectiveAgentId : undefined)
|
|
166
|
+
|
|
167
|
+
const safeSendFinal = () => {
|
|
168
|
+
if (finalSent) return
|
|
169
|
+
finalSent = true
|
|
170
|
+
sendFinal(outboundCtx)
|
|
171
|
+
}
|
|
172
|
+
|
|
153
173
|
const text = msg.content.text?.trim()
|
|
154
174
|
|
|
155
175
|
if (!text) {
|
|
156
|
-
sendTextMsg(
|
|
176
|
+
sendTextMsg('你需要我帮你做什么呢?', outboundCtx)
|
|
157
177
|
safeSendFinal()
|
|
158
178
|
return
|
|
159
179
|
}
|
|
160
180
|
|
|
161
181
|
try {
|
|
162
|
-
const core = getDcgchatRuntime()
|
|
163
|
-
|
|
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()
|
|
167
|
-
|
|
168
|
-
const route = core.channel.routing.resolveAgentRoute({
|
|
169
|
-
cfg: config,
|
|
170
|
-
channel: "dcgchat-test",
|
|
171
|
-
accountId: account.accountId,
|
|
172
|
-
peer: { kind: 'direct', id: conversationId }
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
// If conversation_id encodes an agentId prefix ("agentId::suffix"), override the route.
|
|
176
|
-
const embeddedAgentId = extractAgentIdFromConversationId(conversationId)
|
|
177
|
-
const effectiveAgentId = embeddedAgentId ?? route.agentId
|
|
178
|
-
const effectiveSessionKey =
|
|
179
|
-
realMobook === '1' ? route.sessionKey : `agent:main:mobook:direct:${agentId}:${conversationId}`.toLowerCase()
|
|
180
|
-
setMsgParamsSessionKey(effectiveSessionKey)
|
|
181
|
-
|
|
182
|
-
const agentEntry =
|
|
183
|
-
effectiveAgentId && effectiveAgentId !== 'main' ? config.agents?.list?.find((a) => a.id === effectiveAgentId) : undefined
|
|
184
|
-
const agentDisplayName = agentEntry?.name ?? (effectiveAgentId && effectiveAgentId !== 'main' ? effectiveAgentId : undefined)
|
|
185
|
-
|
|
186
182
|
// Abort any existing generation for this conversation, then start a new one
|
|
187
183
|
// const existingCtrl = activeGenerations.get(conversationId)
|
|
188
184
|
// if (existingCtrl) existingCtrl.abort()
|
|
@@ -252,7 +248,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
252
248
|
const key = getMediaKey(mediaUrl)
|
|
253
249
|
if (sentMediaKeys.has(key)) continue
|
|
254
250
|
sentMediaKeys.add(key)
|
|
255
|
-
await sendDcgchatMedia({
|
|
251
|
+
await sendDcgchatMedia({ sessionKey: effectiveSessionKey, mediaUrl, text: '' })
|
|
256
252
|
}
|
|
257
253
|
},
|
|
258
254
|
onError: (err: unknown, info: { kind: string }) => {
|
|
@@ -278,14 +274,13 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
278
274
|
})
|
|
279
275
|
} else if (interruptCommand.includes(text?.trim())) {
|
|
280
276
|
dcgLogger(`interrupt command: ${text}`)
|
|
281
|
-
// abortMobookappGeneration(conversationId)
|
|
282
277
|
sendMessageToGateway(
|
|
283
278
|
JSON.stringify({
|
|
284
279
|
method: 'chat.abort',
|
|
285
280
|
params: { sessionKey: effectiveSessionKey }
|
|
286
281
|
})
|
|
287
282
|
)
|
|
288
|
-
|
|
283
|
+
safeSendFinal()
|
|
289
284
|
return
|
|
290
285
|
} else {
|
|
291
286
|
dcgLogger(`dispatching to agent (session=${route.sessionKey})`)
|
|
@@ -308,7 +303,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
308
303
|
? payload.text.slice(streamedTextLen)
|
|
309
304
|
: payload.text
|
|
310
305
|
if (delta.trim()) {
|
|
311
|
-
sendChunk(
|
|
306
|
+
sendChunk(delta, outboundCtx)
|
|
312
307
|
dcgLogger(`[stream]: chunk ${delta.length} chars to user ${msg._userId} ${delta.slice(0, 100)}`)
|
|
313
308
|
}
|
|
314
309
|
streamedTextLen = payload.text.length
|
|
@@ -319,7 +314,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
319
314
|
const key = getMediaKey(mediaUrl)
|
|
320
315
|
if (sentMediaKeys.has(key)) continue
|
|
321
316
|
sentMediaKeys.add(key)
|
|
322
|
-
await sendDcgchatMedia({
|
|
317
|
+
await sendDcgchatMedia({ sessionKey: effectiveSessionKey, mediaUrl, text: '' })
|
|
323
318
|
}
|
|
324
319
|
}
|
|
325
320
|
}
|
|
@@ -360,7 +355,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
360
355
|
const resolved = candidates.find((p) => fs.existsSync(p))
|
|
361
356
|
if (!resolved) continue
|
|
362
357
|
try {
|
|
363
|
-
await sendDcgchatMedia({
|
|
358
|
+
await sendDcgchatMedia({ sessionKey: effectiveSessionKey, mediaUrl: resolved, text: '' })
|
|
364
359
|
} catch (err) {
|
|
365
360
|
dcgLogger(` sendDcgchatMedia error: ${String(err)}`, 'error')
|
|
366
361
|
}
|
|
@@ -386,7 +381,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
386
381
|
})
|
|
387
382
|
} catch (err) {
|
|
388
383
|
dcgLogger(` handle message failed: ${String(err)}`, 'error')
|
|
389
|
-
sendError(
|
|
384
|
+
sendError(err instanceof Error ? err.message : String(err), outboundCtx)
|
|
390
385
|
} finally {
|
|
391
386
|
safeSendFinal()
|
|
392
387
|
}
|
package/src/channel.ts
CHANGED
|
@@ -2,36 +2,40 @@ import type { ChannelPlugin, OpenClawConfig } from 'openclaw/plugin-sdk'
|
|
|
2
2
|
import { DEFAULT_ACCOUNT_ID } from 'openclaw/plugin-sdk'
|
|
3
3
|
import type { ResolvedDcgchatAccount, DcgchatConfig } from './types.js'
|
|
4
4
|
import { ossUpload } from './request/oss.js'
|
|
5
|
-
import { addSentMediaKey,
|
|
6
|
-
import {
|
|
5
|
+
import { addSentMediaKey, getOpenClawConfig, hasSentMediaKey } from './utils/global.js'
|
|
6
|
+
import { isWsOpen, mergeDefaultParams, mergeSessionParams, sendFinal, wsSendRaw } from './transport.js'
|
|
7
7
|
import { dcgLogger, setLogger } from './utils/log.js'
|
|
8
|
+
import { getParamsDefaults, getEffectiveMsgParams, getCurrentSessionKey } from './utils/params.js'
|
|
8
9
|
|
|
9
10
|
export type DcgchatMediaSendOptions = {
|
|
10
|
-
|
|
11
|
+
/** 与 setParamsMessage / map 一致,用于 getEffectiveMsgParams */
|
|
12
|
+
sessionKey: string
|
|
11
13
|
mediaUrl?: string
|
|
12
14
|
text?: string
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<void> {
|
|
16
|
-
const
|
|
18
|
+
const msgCtx = getEffectiveMsgParams(opts.sessionKey)
|
|
17
19
|
if (!isWsOpen()) {
|
|
18
20
|
dcgLogger(`outbound media skipped -> ws not open: ${opts.mediaUrl ?? ''}`)
|
|
19
21
|
return
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
const mediaUrl = opts.mediaUrl
|
|
23
|
-
|
|
25
|
+
const dedupeId = msgCtx.messageId
|
|
26
|
+
if (mediaUrl && dedupeId && hasSentMediaKey(dedupeId, mediaUrl)) {
|
|
24
27
|
dcgLogger(`dcgchat: sendMedia skipped (duplicate in session): ${mediaUrl}`)
|
|
25
28
|
return
|
|
26
29
|
}
|
|
27
|
-
if (mediaUrl) {
|
|
28
|
-
addSentMediaKey(
|
|
30
|
+
if (mediaUrl && dedupeId) {
|
|
31
|
+
addSentMediaKey(dedupeId, mediaUrl)
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
const fileName = mediaUrl?.split(/[\\/]/).pop() || ''
|
|
32
35
|
|
|
33
36
|
try {
|
|
34
|
-
const
|
|
37
|
+
const botToken = msgCtx.botToken ?? getOpenClawConfig()?.channels?.["dcgchat-test"]?.botToken ?? ''
|
|
38
|
+
const url = opts.mediaUrl ? await ossUpload(opts.mediaUrl, botToken, 1) : ''
|
|
35
39
|
wsSendRaw(msgCtx, {
|
|
36
40
|
response: opts.text ?? '',
|
|
37
41
|
files: [{ url, name: fileName }]
|
|
@@ -61,22 +65,6 @@ export function resolveAccount(cfg: OpenClawConfig, accountId?: string | null):
|
|
|
61
65
|
}
|
|
62
66
|
}
|
|
63
67
|
|
|
64
|
-
/** Build a DcgchatMsgContext for the outbound pipeline (uses global msgParams). */
|
|
65
|
-
function createOutboundMsgContext(cfg: OpenClawConfig, accountId?: string | null): DcgchatMsgContext {
|
|
66
|
-
const params = getMsgParams()
|
|
67
|
-
const { botToken } = resolveAccount(cfg, accountId)
|
|
68
|
-
return {
|
|
69
|
-
userId: params.userId,
|
|
70
|
-
botToken,
|
|
71
|
-
domainId: params.domainId,
|
|
72
|
-
appId: params.appId,
|
|
73
|
-
botId: params.botId,
|
|
74
|
-
agentId: params.agentId,
|
|
75
|
-
sessionId: params.sessionId,
|
|
76
|
-
messageId: params.messageId
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
68
|
export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
81
69
|
id: "dcgchat-test",
|
|
82
70
|
meta: {
|
|
@@ -153,24 +141,30 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
153
141
|
deliveryMode: 'direct',
|
|
154
142
|
textChunkLimit: 4000,
|
|
155
143
|
sendText: async (ctx) => {
|
|
156
|
-
const msgCtx = createOutboundMsgContext(ctx.cfg, ctx.accountId)
|
|
157
144
|
if (isWsOpen()) {
|
|
158
|
-
|
|
159
|
-
|
|
145
|
+
const merged = mergeDefaultParams({
|
|
146
|
+
agentId: ctx.accountId ?? '',
|
|
147
|
+
sessionId: ctx.to,
|
|
148
|
+
messageId: `${Date.now()}`
|
|
149
|
+
})
|
|
150
|
+
wsSendRaw(merged, { response: ctx.text })
|
|
151
|
+
sendFinal(merged)
|
|
152
|
+
dcgLogger(`channel sendText to ${ctx.to} ${ctx.text?.slice(0, 50)}`)
|
|
160
153
|
}
|
|
161
154
|
return {
|
|
162
155
|
channel: "dcgchat-test",
|
|
163
156
|
messageId: `dcg-${Date.now()}`,
|
|
164
|
-
chatId:
|
|
157
|
+
chatId: ctx.to
|
|
165
158
|
}
|
|
166
159
|
},
|
|
167
160
|
sendMedia: async (ctx) => {
|
|
168
|
-
const
|
|
169
|
-
|
|
161
|
+
const sk = getCurrentSessionKey()
|
|
162
|
+
const msgCtx = getEffectiveMsgParams('sk')
|
|
163
|
+
await sendDcgchatMedia({ sessionKey: sk ?? '', mediaUrl: ctx.mediaUrl ?? '' })
|
|
170
164
|
return {
|
|
171
165
|
channel: "dcgchat-test",
|
|
172
166
|
messageId: `dcg-${Date.now()}`,
|
|
173
|
-
chatId: msgCtx.userId
|
|
167
|
+
chatId: msgCtx.userId?.toString()
|
|
174
168
|
}
|
|
175
169
|
}
|
|
176
170
|
},
|
package/src/cron.ts
CHANGED
|
@@ -6,11 +6,13 @@ import { promisify } from 'node:util'
|
|
|
6
6
|
|
|
7
7
|
const execFileAsync = promisify(execFile)
|
|
8
8
|
import type { IMsgParams } from './types.js'
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { sendEventMessage } from './transport.js'
|
|
10
|
+
import { getWorkspaceDir } from './utils/global.js'
|
|
11
11
|
import { ossUpload } from './request/oss.js'
|
|
12
12
|
import { dcgLogger } from './utils/log.js'
|
|
13
13
|
import { sendMessageToGateway } from './gateway/socket.js'
|
|
14
|
+
import { channelInfo, ENV } from './utils/constant.js'
|
|
15
|
+
import { getCurrentSessionKey, getEffectiveMsgParams } from './utils/params.js'
|
|
14
16
|
|
|
15
17
|
export function getCronJobsPath(): string {
|
|
16
18
|
const workspaceDir = getWorkspaceDir()
|
|
@@ -18,33 +20,28 @@ export function getCronJobsPath(): string {
|
|
|
18
20
|
return path.join(cronDir, 'jobs.json')
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
function msgParamsToCtx(p: IMsgParams):
|
|
22
|
-
if (!p?.
|
|
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
|
-
}
|
|
23
|
+
function msgParamsToCtx(p: IMsgParams): IMsgParams | null {
|
|
24
|
+
if (!p?.botToken) return null
|
|
25
|
+
return p
|
|
33
26
|
}
|
|
34
27
|
|
|
35
28
|
const CRON_UPLOAD_DEBOUNCE_MS = 30_000
|
|
36
29
|
|
|
37
30
|
/** 待合并的上传上下文(短时间内多次调用只保留最后一次) */
|
|
38
|
-
let pendingCronUploadCtx:
|
|
31
|
+
let pendingCronUploadCtx: IMsgParams | null = null
|
|
39
32
|
let cronUploadFlushTimer: ReturnType<typeof setTimeout> | null = null
|
|
40
33
|
|
|
41
|
-
async function runCronJobsUpload(msgCtx:
|
|
34
|
+
async function runCronJobsUpload(msgCtx: IMsgParams): Promise<void> {
|
|
42
35
|
const jobPath = getCronJobsPath()
|
|
43
36
|
if (fs.existsSync(jobPath)) {
|
|
44
37
|
try {
|
|
45
|
-
const url = await ossUpload(jobPath, msgCtx.botToken, 0)
|
|
38
|
+
const url = await ossUpload(jobPath, msgCtx.botToken ?? '', 0)
|
|
46
39
|
dcgLogger(`定时任务创建成功: ${url}`)
|
|
47
|
-
|
|
40
|
+
if (!msgCtx.sessionKey) {
|
|
41
|
+
dcgLogger('runCronJobsUpload: missing sessionKey on msgCtx', 'error')
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
sendEventMessage(url, msgCtx.sessionKey)
|
|
48
45
|
} catch (error) {
|
|
49
46
|
dcgLogger(`${jobPath} upload failed: ${error}`, 'error')
|
|
50
47
|
}
|
|
@@ -63,10 +60,10 @@ function flushCronUploadQueue(): void {
|
|
|
63
60
|
|
|
64
61
|
/**
|
|
65
62
|
* 将 jobs.json 同步到 OSS 并推送事件。30s 内多次调用合并为一次上传;定时触发后清空待处理项,避免重复执行。
|
|
66
|
-
* @param msgCtx
|
|
63
|
+
* @param msgCtx 可选;省略时使用当前会话 getEffectiveMsgParams(sessionKey) 快照
|
|
67
64
|
*/
|
|
68
65
|
export function sendDcgchatCron(): void {
|
|
69
|
-
const ctx = msgParamsToCtx(
|
|
66
|
+
const ctx = msgParamsToCtx(getEffectiveMsgParams(getCurrentSessionKey() ?? ''))
|
|
70
67
|
if (!ctx) {
|
|
71
68
|
dcgLogger('sendDcgchatCron: no message context (missing token / params)', 'error')
|
|
72
69
|
return
|
|
@@ -120,6 +117,22 @@ export const updateCronJobSessionKey = async (jobId: string) => {
|
|
|
120
117
|
dcgLogger('onRemoveCronJob: empty jobId', 'error')
|
|
121
118
|
return
|
|
122
119
|
}
|
|
123
|
-
const params =
|
|
124
|
-
sendMessageToGateway(
|
|
120
|
+
const params = getEffectiveMsgParams(getCurrentSessionKey() ?? '')
|
|
121
|
+
sendMessageToGateway(
|
|
122
|
+
JSON.stringify({
|
|
123
|
+
method: 'cron.update',
|
|
124
|
+
params: {
|
|
125
|
+
id: jobId,
|
|
126
|
+
patch: {
|
|
127
|
+
sessionKey: params.sessionKey,
|
|
128
|
+
delivery: {
|
|
129
|
+
channel: "dcgchat-test",
|
|
130
|
+
to: params.sessionId,
|
|
131
|
+
accountId: 14,
|
|
132
|
+
bestEffort: true
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
)
|
|
125
138
|
}
|
package/src/gateway/index.ts
CHANGED
|
@@ -91,7 +91,7 @@ export class GatewayConnection {
|
|
|
91
91
|
/** 服务端 connect.challenge 提供的 nonce,须与签名载荷一致 */
|
|
92
92
|
private connectChallengeNonce: string | null = null
|
|
93
93
|
private connectSent: boolean = false
|
|
94
|
-
private
|
|
94
|
+
private pendingRpcById: Map<string, (response: GatewayResponse) => void> = new Map()
|
|
95
95
|
private eventHandlers: Set<(event: GatewayEvent) => void> = new Set()
|
|
96
96
|
|
|
97
97
|
constructor(config: GatewayConfig) {
|
|
@@ -347,23 +347,33 @@ export class GatewayConnection {
|
|
|
347
347
|
return
|
|
348
348
|
}
|
|
349
349
|
if (msg.type === 'res') {
|
|
350
|
-
const handler = this.
|
|
350
|
+
const handler = this.pendingRpcById.get(msg.id as string)
|
|
351
351
|
if (handler) {
|
|
352
|
-
this.
|
|
352
|
+
this.pendingRpcById.delete(msg.id as string)
|
|
353
353
|
handler(msg as unknown as GatewayResponse)
|
|
354
354
|
}
|
|
355
355
|
return
|
|
356
356
|
}
|
|
357
357
|
|
|
358
358
|
if (msg.type === 'event') {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
359
|
+
try {
|
|
360
|
+
if (msg.event === 'cron') {
|
|
361
|
+
dcgLogger(`[Gateway] 收到事件: ${JSON.stringify(msg)}`)
|
|
362
|
+
if (msg.payload?.action === 'added') {
|
|
363
|
+
updateCronJobSessionKey(msg.payload?.jobId as string)
|
|
364
|
+
}
|
|
365
|
+
if (msg.payload?.action === 'updated') {
|
|
366
|
+
sendDcgchatCron()
|
|
367
|
+
}
|
|
368
|
+
if (msg.payload?.action === 'added') {
|
|
369
|
+
updateCronJobSessionKey(msg.payload?.jobId as string)
|
|
370
|
+
}
|
|
371
|
+
if (msg.payload?.action === 'updated') {
|
|
372
|
+
sendDcgchatCron()
|
|
373
|
+
}
|
|
366
374
|
}
|
|
375
|
+
} catch (error) {
|
|
376
|
+
dcgLogger(`[Gateway] 处理事件失败: ${error}`, 'error')
|
|
367
377
|
}
|
|
368
378
|
const event: GatewayEvent = {
|
|
369
379
|
type: msg.event as string,
|
|
@@ -387,11 +397,11 @@ export class GatewayConnection {
|
|
|
387
397
|
}
|
|
388
398
|
|
|
389
399
|
const timeout = setTimeout(() => {
|
|
390
|
-
this.
|
|
400
|
+
this.pendingRpcById.delete(id)
|
|
391
401
|
reject(new Error('Method call timeout'))
|
|
392
402
|
}, 30000)
|
|
393
403
|
|
|
394
|
-
this.
|
|
404
|
+
this.pendingRpcById.set(id, (response) => {
|
|
395
405
|
clearTimeout(timeout)
|
|
396
406
|
if (response.ok) {
|
|
397
407
|
const body = response.result !== undefined ? response.result : (response as GatewayResponse).payload
|
package/src/gateway/security.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
// Security utilities for the tunnel plugin
|
|
2
2
|
import crypto from 'crypto'
|
|
3
|
-
import fs from 'fs'
|
|
4
|
-
import jwt from 'jsonwebtoken'
|
|
5
|
-
import { z } from 'zod'
|
|
6
3
|
|
|
7
4
|
// ED25519 SubjectPublicKeyInfo prefix (must match openclaw gateway `ED25519_SPKI_PREFIX`)
|
|
8
5
|
const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex')
|
|
@@ -12,10 +9,7 @@ const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex')
|
|
|
12
9
|
*/
|
|
13
10
|
export function derivePublicKeyRawFromPem(publicKeyPem: string): Buffer {
|
|
14
11
|
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
|
-
) {
|
|
12
|
+
if (spki.length === ED25519_SPKI_PREFIX.length + 32 && spki.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)) {
|
|
19
13
|
return spki.subarray(ED25519_SPKI_PREFIX.length)
|
|
20
14
|
}
|
|
21
15
|
return spki
|
package/src/gateway/socket.ts
CHANGED
|
@@ -99,6 +99,8 @@ export type GatewayRpcPayload = {
|
|
|
99
99
|
let persistentConn: GatewayConnection | null = null
|
|
100
100
|
let pingTimer: ReturnType<typeof setInterval> | null = null
|
|
101
101
|
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
|
102
|
+
/** register 可能被调用多次,只保留一个「延迟首次连接」定时器,避免同一时刻触发两次 connect */
|
|
103
|
+
let startupConnectTimer: ReturnType<typeof setTimeout> | null = null
|
|
102
104
|
let connectInFlight = false
|
|
103
105
|
let socketStopped = true
|
|
104
106
|
/** 用于忽略「已被替换的旧连接」上的 close/error */
|
|
@@ -118,6 +120,13 @@ function clearReconnectTimer(): void {
|
|
|
118
120
|
}
|
|
119
121
|
}
|
|
120
122
|
|
|
123
|
+
function clearStartupConnectTimer(): void {
|
|
124
|
+
if (startupConnectTimer) {
|
|
125
|
+
clearTimeout(startupConnectTimer)
|
|
126
|
+
startupConnectTimer = null
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
121
130
|
function startPingTimer(gw: GatewayConnection): void {
|
|
122
131
|
clearPingTimer()
|
|
123
132
|
pingTimer = setInterval(() => {
|
|
@@ -159,7 +168,9 @@ function attachSocketLifecycle(gw: GatewayConnection, generation: number): void
|
|
|
159
168
|
}
|
|
160
169
|
|
|
161
170
|
async function connectPersistentGateway(): Promise<void> {
|
|
162
|
-
if (socketStopped
|
|
171
|
+
if (socketStopped) return
|
|
172
|
+
if (persistentConn?.isConnected()) return
|
|
173
|
+
if (connectInFlight) return
|
|
163
174
|
|
|
164
175
|
const cfg = resolveConfigSafe()
|
|
165
176
|
if (!cfg) return
|
|
@@ -218,7 +229,9 @@ async function connectPersistentGateway(): Promise<void> {
|
|
|
218
229
|
export function startDcgchatGatewaySocket(): void {
|
|
219
230
|
socketStopped = false
|
|
220
231
|
clearReconnectTimer()
|
|
221
|
-
|
|
232
|
+
if (startupConnectTimer != null) return
|
|
233
|
+
startupConnectTimer = setTimeout(() => {
|
|
234
|
+
startupConnectTimer = null
|
|
222
235
|
void connectPersistentGateway()
|
|
223
236
|
}, 10000)
|
|
224
237
|
}
|
|
@@ -229,6 +242,7 @@ export function startDcgchatGatewaySocket(): void {
|
|
|
229
242
|
export function stopDcgchatGatewaySocket(): void {
|
|
230
243
|
socketStopped = true
|
|
231
244
|
clearReconnectTimer()
|
|
245
|
+
clearStartupConnectTimer()
|
|
232
246
|
clearPingTimer()
|
|
233
247
|
if (persistentConn) {
|
|
234
248
|
try {
|
package/src/monitor.ts
CHANGED
|
@@ -2,9 +2,8 @@ import type { ClawdbotConfig, RuntimeEnv } from 'openclaw/plugin-sdk'
|
|
|
2
2
|
import WebSocket from 'ws'
|
|
3
3
|
import { handleDcgchatMessage } from './bot.js'
|
|
4
4
|
import { resolveAccount } from './channel.js'
|
|
5
|
-
import { setWsConnection, getOpenClawConfig } from './utils/global.js'
|
|
5
|
+
import { setWsConnection, getOpenClawConfig, setMsgStatus } from './utils/global.js'
|
|
6
6
|
import type { InboundMessage } from './types.js'
|
|
7
|
-
import { setMsgParams, setMsgStatus } from './utils/global.js'
|
|
8
7
|
import { installSkill, uninstallSkill } from './skill.js'
|
|
9
8
|
import { dcgLogger } from './utils/log.js'
|
|
10
9
|
import { ignoreToolCommand } from './utils/constant.js'
|
|
@@ -130,19 +129,6 @@ export async function monitorDcgchatProvider(opts: MonitorDcgchatOpts): Promise<
|
|
|
130
129
|
if (!ignoreToolCommand.includes(msg.content.text?.trim())) {
|
|
131
130
|
setMsgStatus('running')
|
|
132
131
|
}
|
|
133
|
-
// 设置获取用户消息消息参数
|
|
134
|
-
setMsgParams({
|
|
135
|
-
userId: msg._userId,
|
|
136
|
-
token: msg.content.bot_token,
|
|
137
|
-
sessionId: msg.content.session_id,
|
|
138
|
-
messageId: msg.content.message_id,
|
|
139
|
-
domainId: account.domainId || 1000,
|
|
140
|
-
appId: account.appId || '100',
|
|
141
|
-
botId: msg.content.bot_id,
|
|
142
|
-
agentId: msg.content.agent_id
|
|
143
|
-
})
|
|
144
|
-
msg.content.app_id = account.appId || '100'
|
|
145
|
-
msg.content.domain_id = account.domainId || '1000'
|
|
146
132
|
|
|
147
133
|
await handleDcgchatMessage(msg, account.accountId)
|
|
148
134
|
} else if (parsed.messageType == 'openclaw_bot_event') {
|
package/src/request/request.ts
CHANGED
|
@@ -3,7 +3,7 @@ import axios from 'axios'
|
|
|
3
3
|
import md5 from 'md5'
|
|
4
4
|
import type { IResponse } from '../types.js'
|
|
5
5
|
import { getUserTokenCache } from './userInfo.js'
|
|
6
|
-
import {
|
|
6
|
+
import { getCurrentSessionKey, getEffectiveMsgParams } from '../utils/params.js'
|
|
7
7
|
import { ENV } from '../utils/constant.js'
|
|
8
8
|
import { dcgLogger } from '../utils/log.js'
|
|
9
9
|
|
|
@@ -172,7 +172,7 @@ export function post<T = Record<string, unknown>, R = unknown>(
|
|
|
172
172
|
botToken?: string
|
|
173
173
|
}
|
|
174
174
|
): Promise<IResponse<R>> {
|
|
175
|
-
const params =
|
|
175
|
+
const params = getEffectiveMsgParams(getCurrentSessionKey() ?? '') || {}
|
|
176
176
|
const config: any = {
|
|
177
177
|
method: 'POST',
|
|
178
178
|
url,
|
package/src/tool.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from 'openclaw/plugin-sdk'
|
|
2
|
-
import {
|
|
2
|
+
import { getMsgStatus, getWsConnection } from './utils/global.js'
|
|
3
3
|
import { dcgLogger } from './utils/log.js'
|
|
4
4
|
import { isWsOpen, sendFinal, sendText } from './transport.js'
|
|
5
|
+
import { getCurrentSessionKey, getEffectiveMsgParams } from './utils/params.js'
|
|
5
6
|
|
|
6
7
|
let toolCallId = ''
|
|
7
8
|
let toolName = ''
|
|
@@ -51,7 +52,9 @@ const eventList = [
|
|
|
51
52
|
|
|
52
53
|
function sendToolCallMessage(text: string, toolCallId: string, isCover: number) {
|
|
53
54
|
const ws = getWsConnection()
|
|
54
|
-
const
|
|
55
|
+
const sk = getCurrentSessionKey()
|
|
56
|
+
if (!sk) return
|
|
57
|
+
const params = getEffectiveMsgParams(sk)
|
|
55
58
|
if (isWsOpen()) {
|
|
56
59
|
ws?.send(
|
|
57
60
|
JSON.stringify({
|
|
@@ -61,7 +64,7 @@ function sendToolCallMessage(text: string, toolCallId: string, isCover: number)
|
|
|
61
64
|
is_finish: -1,
|
|
62
65
|
content: {
|
|
63
66
|
is_finish: -1,
|
|
64
|
-
bot_token: params?.
|
|
67
|
+
bot_token: params?.botToken,
|
|
65
68
|
domain_id: params?.domainId,
|
|
66
69
|
app_id: params?.appId,
|
|
67
70
|
bot_id: params?.botId,
|
|
@@ -85,7 +88,7 @@ export function monitoringToolMessage(api: OpenClawPluginApi) {
|
|
|
85
88
|
if (status === 'running') {
|
|
86
89
|
if (['after_tool_call', 'before_tool_call'].includes(item.event)) {
|
|
87
90
|
const { result: _result, ...rest } = event
|
|
88
|
-
dcgLogger(`工具调用结果: ~ event:${item.event} ~ params:${JSON.stringify(rest)}`)
|
|
91
|
+
dcgLogger(`工具调用结果: ~ event:${item.event} ~ params:${JSON.stringify(rest)}, args: ${JSON.stringify(args)}`)
|
|
89
92
|
const text = JSON.stringify({
|
|
90
93
|
type: item.event,
|
|
91
94
|
specialIdentification: 'dcgchat_tool_call_special_identification',
|
|
@@ -97,22 +100,14 @@ export function monitoringToolMessage(api: OpenClawPluginApi) {
|
|
|
97
100
|
} else if (item.event) {
|
|
98
101
|
dcgLogger(`工具调用结果: ~ event:${item.event}`)
|
|
99
102
|
if (item.event === 'llm_output') {
|
|
100
|
-
dcgLogger(`llm_output工具调用结果: ~ event:${JSON.stringify(event)}`)
|
|
103
|
+
dcgLogger(`llm_output工具调用结果: ~ event:${JSON.stringify(event)}, args: ${JSON.stringify(args)}`)
|
|
101
104
|
if (event.lastAssistant?.errorMessage === '429-账户额度耗尽') {
|
|
102
|
-
const params = getMsgParams()
|
|
103
105
|
const message = '您的积分已消耗完,您可以通过充值积分来继续使用'
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
botId: params.botId,
|
|
110
|
-
agentId: params.agentId,
|
|
111
|
-
sessionId: params.sessionId,
|
|
112
|
-
messageId: params.messageId
|
|
113
|
-
}
|
|
114
|
-
sendText(ctx, message, { message_tags: { insufficient_balance: 1 }, is_finish: -1 })
|
|
115
|
-
sendFinal(ctx)
|
|
106
|
+
const sk = ((args?.sessionKey as string | undefined) ?? getCurrentSessionKey()) || ''
|
|
107
|
+
if (!sk) return
|
|
108
|
+
const msgCtx = getEffectiveMsgParams(sk)
|
|
109
|
+
sendText(message, msgCtx, { message_tags: { insufficient_balance: 1 }, is_finish: -1 })
|
|
110
|
+
sendFinal(msgCtx)
|
|
116
111
|
return
|
|
117
112
|
}
|
|
118
113
|
}
|
package/src/transport.ts
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import { getWsConnection } from './utils/global.js'
|
|
2
2
|
import { dcgLogger } from './utils/log.js'
|
|
3
|
+
import type { IMsgParams } from './types.js'
|
|
4
|
+
import { getEffectiveMsgParams, getParamsDefaults } from './utils/params.js'
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
/** 用 sessionKey 从 map 取参,再合并 overrides(channel 出站、媒体等) */
|
|
7
|
+
export function mergeSessionParams(sessionKey: string, overrides?: Partial<IMsgParams>): IMsgParams {
|
|
8
|
+
const base = getEffectiveMsgParams(sessionKey)
|
|
9
|
+
if (!overrides) return base
|
|
10
|
+
return { ...base, ...overrides }
|
|
11
|
+
}
|
|
12
|
+
export function mergeDefaultParams(overrides?: Partial<IMsgParams>): IMsgParams {
|
|
13
|
+
const base = getParamsDefaults()
|
|
14
|
+
if (!overrides) return base
|
|
15
|
+
return { ...base, ...overrides }
|
|
13
16
|
}
|
|
14
17
|
|
|
15
|
-
export
|
|
16
|
-
_userId: number
|
|
18
|
+
export type InboundMsgForContext = {
|
|
19
|
+
_userId: number | string
|
|
17
20
|
content: {
|
|
18
21
|
bot_token: string
|
|
19
22
|
domain_id?: string
|
|
@@ -23,90 +26,157 @@ export function createMsgContext(msg: {
|
|
|
23
26
|
session_id: string
|
|
24
27
|
message_id: string
|
|
25
28
|
}
|
|
26
|
-
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type OpenclawBotChatEnvelope = {
|
|
32
|
+
messageType: 'openclaw_bot_chat'
|
|
33
|
+
_userId: number | undefined
|
|
34
|
+
source: 'client'
|
|
35
|
+
content: Record<string, unknown>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isInboundWire(arg: unknown): arg is InboundMsgForContext {
|
|
39
|
+
return Boolean(arg && typeof arg === 'object' && '_userId' in arg && 'content' in arg)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 下行 WebSocket 帧 → 内部上下文(字段缺省用 channel 配置补) */
|
|
43
|
+
function inboundToCtx(msg: InboundMsgForContext, d: IMsgParams): IMsgParams {
|
|
44
|
+
const c = msg.content
|
|
27
45
|
return {
|
|
28
|
-
userId: msg._userId,
|
|
29
|
-
botToken:
|
|
30
|
-
domainId:
|
|
31
|
-
appId:
|
|
32
|
-
botId:
|
|
33
|
-
agentId:
|
|
34
|
-
sessionId:
|
|
35
|
-
messageId:
|
|
46
|
+
userId: Number(msg._userId ?? d.userId),
|
|
47
|
+
botToken: c.bot_token ?? d.botToken,
|
|
48
|
+
domainId: String(c.domain_id ?? d.domainId),
|
|
49
|
+
appId: String(c.app_id ?? d.appId),
|
|
50
|
+
botId: c.bot_id,
|
|
51
|
+
agentId: c.agent_id,
|
|
52
|
+
sessionId: c.session_id,
|
|
53
|
+
messageId: c.message_id
|
|
36
54
|
}
|
|
37
55
|
}
|
|
38
56
|
|
|
39
|
-
|
|
57
|
+
/** 上行:与配置合并缺省后再 `...ctx` 覆盖(原 wsSendRaw) */
|
|
58
|
+
function mergeOutboundWithDefaults(ctx: IMsgParams, d: IMsgParams): IMsgParams {
|
|
40
59
|
return {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
session_id: ctx.sessionId,
|
|
47
|
-
message_id: ctx.messageId || Date.now().toString(),
|
|
48
|
-
...extra
|
|
60
|
+
userId: Number(ctx.userId ?? d.userId),
|
|
61
|
+
botToken: ctx.botToken ?? d.botToken,
|
|
62
|
+
domainId: String(ctx.domainId ?? d.domainId),
|
|
63
|
+
appId: String(ctx.appId ?? d.appId),
|
|
64
|
+
...ctx
|
|
49
65
|
}
|
|
50
66
|
}
|
|
51
67
|
|
|
52
|
-
|
|
68
|
+
/**
|
|
69
|
+
* 组装完整 wire `content` 对象:先写会话/机器人基础字段(回落到 d),再合并调用方传入的 payload。
|
|
70
|
+
* `content` 在使用处构造(如 response、state、files),同名键可覆盖基础字段。
|
|
71
|
+
*/
|
|
72
|
+
export function buildWireContent(base: IMsgParams, d: IMsgParams, content: Record<string, unknown>): Record<string, unknown> {
|
|
73
|
+
const resolvedBotToken = base.botToken ?? d.botToken
|
|
74
|
+
const domain = base.domainId ?? d.domainId
|
|
75
|
+
const app = base.appId ?? d.appId
|
|
53
76
|
return {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
77
|
+
bot_token: base.botToken || resolvedBotToken,
|
|
78
|
+
domain_id: base.domainId || domain,
|
|
79
|
+
app_id: base.appId || app,
|
|
80
|
+
bot_id: base.botId,
|
|
81
|
+
agent_id: base.agentId,
|
|
82
|
+
session_id: base.sessionId,
|
|
83
|
+
message_id: base.messageId || Date.now().toString(),
|
|
84
|
+
...content
|
|
58
85
|
}
|
|
59
86
|
}
|
|
60
87
|
|
|
88
|
+
/** 上行:在已合并的 ctx 上套 openclaw_bot_chat 信封(messageType / _userId / source + content) */
|
|
89
|
+
function buildOutboundOpenclawBotChatEnvelope(
|
|
90
|
+
ctx: IMsgParams,
|
|
91
|
+
content: Record<string, unknown>,
|
|
92
|
+
opts?: { mergeChannelDefaults?: boolean }
|
|
93
|
+
): OpenclawBotChatEnvelope {
|
|
94
|
+
const d = getParamsDefaults()
|
|
95
|
+
const base = opts?.mergeChannelDefaults ? mergeOutboundWithDefaults(ctx, d) : ctx
|
|
96
|
+
return {
|
|
97
|
+
messageType: 'openclaw_bot_chat',
|
|
98
|
+
_userId: base.userId,
|
|
99
|
+
source: 'client',
|
|
100
|
+
content: buildWireContent(base, d, content)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 下行解析为 DcgchatMsgContext,或上行组装 openclaw_bot_chat 信封。
|
|
106
|
+
* 上行时 `content` 由调用方传入;基础参数来自 `ctx` 与 `getParamsDefaults()`(可选 mergeChannelDefaults,同原 wsSendRaw)。
|
|
107
|
+
*/
|
|
108
|
+
export function buildOpenclawBotChat(msg: InboundMsgForContext): IMsgParams
|
|
109
|
+
export function buildOpenclawBotChat(
|
|
110
|
+
ctx: IMsgParams,
|
|
111
|
+
content: Record<string, unknown>,
|
|
112
|
+
opts?: { mergeChannelDefaults?: boolean }
|
|
113
|
+
): OpenclawBotChatEnvelope
|
|
114
|
+
export function buildOpenclawBotChat(
|
|
115
|
+
arg1: InboundMsgForContext | IMsgParams,
|
|
116
|
+
arg2?: Record<string, unknown>,
|
|
117
|
+
opts?: { mergeChannelDefaults?: boolean }
|
|
118
|
+
): IMsgParams | OpenclawBotChatEnvelope {
|
|
119
|
+
const d = getParamsDefaults()
|
|
120
|
+
|
|
121
|
+
if (arg2 === undefined && isInboundWire(arg1)) {
|
|
122
|
+
return inboundToCtx(arg1, d)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const ctx = arg1 as IMsgParams
|
|
126
|
+
return buildOutboundOpenclawBotChatEnvelope(ctx, arg2 ?? {}, opts)
|
|
127
|
+
}
|
|
128
|
+
|
|
61
129
|
export function isWsOpen(): boolean {
|
|
62
130
|
const isOpen = getWsConnection()?.readyState === WebSocket.OPEN
|
|
63
131
|
if (!isOpen) {
|
|
64
|
-
dcgLogger(`socket not ready ${getWsConnection()?.readyState}`, 'error')
|
|
132
|
+
dcgLogger(`server socket not ready ${getWsConnection()?.readyState}`, 'error')
|
|
65
133
|
}
|
|
66
134
|
return isOpen
|
|
67
135
|
}
|
|
68
136
|
|
|
69
137
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
138
|
+
* 聊天流路径:content 单独 JSON.stringify(双重编码),符合 dcgchat 协议。
|
|
139
|
+
* `ctx` 须由调用方用 getEffectiveMsgParams(sessionKey) 等解析好;`content` 为完整业务 payload。
|
|
72
140
|
*/
|
|
73
|
-
export function wsSend(ctx:
|
|
141
|
+
export function wsSend(ctx: IMsgParams, content: Record<string, unknown>): boolean {
|
|
74
142
|
const ws = getWsConnection()
|
|
75
143
|
if (ws?.readyState !== WebSocket.OPEN) return false
|
|
76
|
-
const envelope =
|
|
144
|
+
const envelope = buildOpenclawBotChat(ctx, content)
|
|
77
145
|
ws.send(JSON.stringify({ ...envelope, content: JSON.stringify(envelope.content) }))
|
|
78
146
|
return true
|
|
79
147
|
}
|
|
80
148
|
|
|
81
149
|
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
150
|
+
* 媒体 / channel 出站:content 保持嵌套对象(单次编码)。
|
|
151
|
+
* `ctx` 须由调用方解析(如需合并覆盖可先 mergeSessionParams)。
|
|
84
152
|
*/
|
|
85
|
-
export function wsSendRaw(ctx:
|
|
153
|
+
export function wsSendRaw(ctx: IMsgParams, content: Record<string, unknown>): boolean {
|
|
86
154
|
const ws = getWsConnection()
|
|
87
155
|
if (isWsOpen()) {
|
|
88
|
-
ws?.send(JSON.stringify(
|
|
156
|
+
ws?.send(JSON.stringify(buildOpenclawBotChat(ctx, content, { mergeChannelDefaults: true })))
|
|
89
157
|
}
|
|
90
158
|
return true
|
|
91
159
|
}
|
|
92
160
|
|
|
93
|
-
export function sendChunk(
|
|
161
|
+
export function sendChunk(text: string, ctx: IMsgParams): boolean {
|
|
94
162
|
return wsSend(ctx, { response: text, state: 'chunk' })
|
|
95
163
|
}
|
|
96
164
|
|
|
97
|
-
export function sendFinal(ctx:
|
|
165
|
+
export function sendFinal(ctx: IMsgParams): boolean {
|
|
98
166
|
dcgLogger(` message handling complete state: final`)
|
|
99
167
|
return wsSend(ctx, { response: '', state: 'final' })
|
|
100
168
|
}
|
|
101
169
|
|
|
102
|
-
export function sendText(
|
|
170
|
+
export function sendText(text: string, ctx: IMsgParams, event?: Record<string, unknown>): boolean {
|
|
103
171
|
return wsSend(ctx, { response: text, ...event })
|
|
104
172
|
}
|
|
105
173
|
|
|
106
|
-
export function sendError(
|
|
174
|
+
export function sendError(errorMsg: string, ctx: IMsgParams): boolean {
|
|
107
175
|
return wsSend(ctx, { response: `[错误] ${errorMsg}`, state: 'final' })
|
|
108
176
|
}
|
|
109
|
-
|
|
177
|
+
|
|
178
|
+
export function sendEventMessage(url: string, sessionKey: string) {
|
|
179
|
+
const ctx = getEffectiveMsgParams(sessionKey)
|
|
110
180
|
const ws = getWsConnection()
|
|
111
181
|
if (isWsOpen()) {
|
|
112
182
|
ws?.send(
|
package/src/types.ts
CHANGED
|
@@ -116,13 +116,15 @@ export interface IStsTokenReq {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
export interface IMsgParams {
|
|
119
|
-
userId
|
|
120
|
-
|
|
121
|
-
sessionId
|
|
122
|
-
messageId
|
|
123
|
-
domainId
|
|
124
|
-
appId
|
|
125
|
-
botId
|
|
126
|
-
agentId
|
|
127
|
-
|
|
119
|
+
userId?: number
|
|
120
|
+
botToken?: string
|
|
121
|
+
sessionId?: string
|
|
122
|
+
messageId?: string
|
|
123
|
+
domainId?: string
|
|
124
|
+
appId?: string
|
|
125
|
+
botId?: string
|
|
126
|
+
agentId?: string
|
|
127
|
+
/** 与 OpenClaw 路由一致,用于 map 与异步链路(工具 / HTTP / cron)对齐当前会话 */
|
|
128
|
+
sessionKey?: string
|
|
129
|
+
real_mobook?: string | number
|
|
128
130
|
}
|
package/src/utils/global.ts
CHANGED
|
@@ -24,7 +24,6 @@ export function getOpenClawConfig(): OpenClawConfig | null {
|
|
|
24
24
|
|
|
25
25
|
import type { OpenClawConfig, PluginRuntime } from 'openclaw/plugin-sdk'
|
|
26
26
|
import { dcgLogger } from './log.js'
|
|
27
|
-
import { IMsgParams } from '../types.js'
|
|
28
27
|
import { channelInfo, ENV } from './constant.js'
|
|
29
28
|
|
|
30
29
|
const path = require('path')
|
|
@@ -69,17 +68,6 @@ export function getDcgchatRuntime(): PluginRuntime {
|
|
|
69
68
|
return runtime as PluginRuntime
|
|
70
69
|
}
|
|
71
70
|
|
|
72
|
-
let msgParams = {} as IMsgParams
|
|
73
|
-
export function setMsgParams(params: any) {
|
|
74
|
-
msgParams = params
|
|
75
|
-
}
|
|
76
|
-
export function getMsgParams() {
|
|
77
|
-
return msgParams
|
|
78
|
-
}
|
|
79
|
-
export function setMsgParamsSessionKey(sessionKey: string) {
|
|
80
|
-
if (sessionKey) msgParams.sessionKey = sessionKey
|
|
81
|
-
}
|
|
82
|
-
|
|
83
71
|
let msgStatus: 'running' | 'finished' | '' = ''
|
|
84
72
|
export function setMsgStatus(status: 'running' | 'finished' | '') {
|
|
85
73
|
msgStatus = status
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { channelInfo, ENV } from './constant.js'
|
|
2
|
+
import { getOpenClawConfig } from './global.js'
|
|
3
|
+
import type { DcgchatConfig, IMsgParams } from '../types.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* map key 是 session_key,value 为该会话下已 merge 后的消息参数。
|
|
7
|
+
*/
|
|
8
|
+
const paramsMessageMap = new Map<string, IMsgParams>()
|
|
9
|
+
|
|
10
|
+
/** 最近一次 setParamsMessage 的 key,供不传参的 getEffectiveMsgParams() 使用 */
|
|
11
|
+
let currentSessionKey: string | null = null
|
|
12
|
+
|
|
13
|
+
/** 从 OpenClaw 配置读取当前 channel 的基础参数(唯一来源,供 transport / resolve 等复用) */
|
|
14
|
+
export function getParamsDefaults(): IMsgParams {
|
|
15
|
+
const ch = (getOpenClawConfig()?.channels?.["dcgchat-test"] as DcgchatConfig | undefined) ?? {}
|
|
16
|
+
return {
|
|
17
|
+
userId: Number(ch.userId ?? 0),
|
|
18
|
+
botToken: ch.botToken ?? '',
|
|
19
|
+
sessionId: '',
|
|
20
|
+
messageId: '',
|
|
21
|
+
domainId: String(ch.domainId ?? '1000'),
|
|
22
|
+
appId: String(ch.appId ?? '100'),
|
|
23
|
+
botId: '',
|
|
24
|
+
agentId: '',
|
|
25
|
+
sessionKey: ''
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function resolveParamsMessage(params: Partial<IMsgParams>): IMsgParams {
|
|
30
|
+
const defaults = getParamsDefaults()
|
|
31
|
+
return {
|
|
32
|
+
userId: Number(params.userId ?? defaults.userId),
|
|
33
|
+
botToken: params.botToken ?? defaults.botToken,
|
|
34
|
+
sessionId: params.sessionId ?? defaults.sessionId,
|
|
35
|
+
messageId: params.messageId ?? defaults.messageId,
|
|
36
|
+
domainId: String(params.domainId ?? defaults.domainId),
|
|
37
|
+
appId: String(params.appId ?? defaults.appId),
|
|
38
|
+
botId: params.botId ?? defaults.botId,
|
|
39
|
+
agentId: params.agentId ?? defaults.agentId,
|
|
40
|
+
sessionKey: params.sessionKey ?? defaults.sessionKey
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 统一取值入口:显式 sessionKey,或回落到当前会话;再与配置缺省 merge,保证字段完整。
|
|
46
|
+
*/
|
|
47
|
+
export function getEffectiveMsgParams(sessionKey?: string): IMsgParams {
|
|
48
|
+
const key = sessionKey ?? currentSessionKey
|
|
49
|
+
const stored = key ? paramsMessageMap.get(key) : undefined
|
|
50
|
+
return stored ? resolveParamsMessage(stored) : getParamsDefaults()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function setParamsMessage(sessionKey: string, params: Partial<IMsgParams>) {
|
|
54
|
+
if (!sessionKey) return
|
|
55
|
+
currentSessionKey = sessionKey
|
|
56
|
+
paramsMessageMap.set(sessionKey, resolveParamsMessage({ ...params, sessionKey }))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getParamsMessage(sessionKey: string): IMsgParams | undefined {
|
|
60
|
+
return paramsMessageMap.get(sessionKey)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getCurrentSessionKey(): string | null {
|
|
64
|
+
return currentSessionKey
|
|
65
|
+
}
|