@dcrays/dcgchat-test 0.2.34 → 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/index.ts +2 -0
- package/package.json +1 -1
- package/src/bot.ts +77 -67
- package/src/channel.ts +25 -31
- package/src/cron.ts +138 -0
- package/src/gateway/index.ts +468 -0
- package/src/gateway/security.ts +95 -0
- package/src/gateway/socket.ts +285 -0
- package/src/monitor.ts +24 -27
- package/src/request/api.ts +4 -18
- package/src/request/oss.ts +5 -4
- package/src/request/request.ts +2 -2
- package/src/tool.ts +15 -18
- package/src/transport.ts +140 -47
- package/src/types.ts +12 -8
- package/src/utils/global.ts +0 -9
- package/src/utils/params.ts +65 -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
|
@@ -7,9 +7,11 @@ import { clearSentMediaKeys, getDcgchatRuntime, getOpenClawConfig, getWorkspaceD
|
|
|
7
7
|
import { resolveAccount, sendDcgchatMedia } from './channel.js'
|
|
8
8
|
import { generateSignUrl } from './request/api.js'
|
|
9
9
|
import { extractMobookFiles } from './utils/searchFile.js'
|
|
10
|
-
import {
|
|
10
|
+
import { sendChunk, sendFinal, sendText as sendTextMsg, sendError } from './transport.js'
|
|
11
11
|
import { dcgLogger } from './utils/log.js'
|
|
12
12
|
import { channelInfo, systemCommand, interruptCommand, ENV } from './utils/constant.js'
|
|
13
|
+
import { sendMessageToGateway } from './gateway/socket.js'
|
|
14
|
+
import { getEffectiveMsgParams, setParamsMessage } from './utils/params.js'
|
|
13
15
|
|
|
14
16
|
type MediaInfo = {
|
|
15
17
|
path: string
|
|
@@ -23,16 +25,7 @@ type TFileInfo = { name: string; url: string }
|
|
|
23
25
|
const mediaMaxBytes = 300 * 1024 * 1024
|
|
24
26
|
|
|
25
27
|
/** Active LLM generation abort controllers, keyed by conversationId */
|
|
26
|
-
const activeGenerations = new Map<string, AbortController>()
|
|
27
|
-
|
|
28
|
-
/** Abort an in-progress LLM generation for a given conversationId */
|
|
29
|
-
export function abortMobookappGeneration(conversationId: string): void {
|
|
30
|
-
const ctrl = activeGenerations.get(conversationId)
|
|
31
|
-
if (ctrl) {
|
|
32
|
-
ctrl.abort()
|
|
33
|
-
activeGenerations.delete(conversationId)
|
|
34
|
-
}
|
|
35
|
-
}
|
|
28
|
+
// const activeGenerations = new Map<string, AbortController>()
|
|
36
29
|
|
|
37
30
|
/**
|
|
38
31
|
* Extract agentId from conversation_id formatted as "agentId::suffix".
|
|
@@ -124,16 +117,8 @@ function resolveReplyMediaList(payload: ReplyPayload): string[] {
|
|
|
124
117
|
* 处理一条用户消息,调用 Agent 并返回回复
|
|
125
118
|
*/
|
|
126
119
|
export async function handleDcgchatMessage(msg: InboundMessage, accountId: string): Promise<void> {
|
|
127
|
-
const msgCtx = createMsgContext(msg)
|
|
128
|
-
|
|
129
120
|
let finalSent = false
|
|
130
121
|
|
|
131
|
-
const safeSendFinal = () => {
|
|
132
|
-
if (finalSent) return
|
|
133
|
-
finalSent = true
|
|
134
|
-
sendFinal(msgCtx)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
122
|
let completeText = ''
|
|
138
123
|
const config = getOpenClawConfig()
|
|
139
124
|
if (!config) {
|
|
@@ -142,43 +127,64 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
142
127
|
}
|
|
143
128
|
const account = resolveAccount(config, accountId)
|
|
144
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
|
+
|
|
145
173
|
const text = msg.content.text?.trim()
|
|
146
174
|
|
|
147
175
|
if (!text) {
|
|
148
|
-
sendTextMsg(
|
|
176
|
+
sendTextMsg('你需要我帮你做什么呢?', outboundCtx)
|
|
149
177
|
safeSendFinal()
|
|
150
178
|
return
|
|
151
179
|
}
|
|
152
180
|
|
|
153
181
|
try {
|
|
154
|
-
const core = getDcgchatRuntime()
|
|
155
|
-
|
|
156
|
-
const conversationId = msg.content.session_id?.trim()
|
|
157
|
-
|
|
158
|
-
const route = core.channel.routing.resolveAgentRoute({
|
|
159
|
-
cfg: config,
|
|
160
|
-
channel: "dcgchat-test",
|
|
161
|
-
accountId: account.accountId,
|
|
162
|
-
peer: { kind: 'direct', id: conversationId }
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
// If conversation_id encodes an agentId prefix ("agentId::suffix"), override the route.
|
|
166
|
-
const embeddedAgentId = extractAgentIdFromConversationId(conversationId)
|
|
167
|
-
const effectiveAgentId = embeddedAgentId ?? route.agentId
|
|
168
|
-
const effectiveSessionKey = embeddedAgentId
|
|
169
|
-
? `agent:${embeddedAgentId}:mobook:direct:${conversationId}`.toLowerCase()
|
|
170
|
-
: route.sessionKey
|
|
171
|
-
|
|
172
|
-
const agentEntry =
|
|
173
|
-
effectiveAgentId && effectiveAgentId !== 'main' ? config.agents?.list?.find((a) => a.id === effectiveAgentId) : undefined
|
|
174
|
-
const agentDisplayName = agentEntry?.name ?? (effectiveAgentId && effectiveAgentId !== 'main' ? effectiveAgentId : undefined)
|
|
175
|
-
|
|
176
182
|
// 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)
|
|
183
|
+
// const existingCtrl = activeGenerations.get(conversationId)
|
|
184
|
+
// if (existingCtrl) existingCtrl.abort()
|
|
185
|
+
// const genCtrl = new AbortController()
|
|
186
|
+
// const genSignal = genCtrl.signal
|
|
187
|
+
// activeGenerations.set(conversationId, genCtrl)
|
|
182
188
|
|
|
183
189
|
// 处理用户上传的文件
|
|
184
190
|
const files = msg.content.files ?? []
|
|
@@ -242,7 +248,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
242
248
|
const key = getMediaKey(mediaUrl)
|
|
243
249
|
if (sentMediaKeys.has(key)) continue
|
|
244
250
|
sentMediaKeys.add(key)
|
|
245
|
-
await sendDcgchatMedia({
|
|
251
|
+
await sendDcgchatMedia({ sessionKey: effectiveSessionKey, mediaUrl, text: '' })
|
|
246
252
|
}
|
|
247
253
|
},
|
|
248
254
|
onError: (err: unknown, info: { kind: string }) => {
|
|
@@ -254,7 +260,6 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
254
260
|
}
|
|
255
261
|
})
|
|
256
262
|
|
|
257
|
-
let wasAborted = false
|
|
258
263
|
try {
|
|
259
264
|
if (systemCommand.includes(text?.trim())) {
|
|
260
265
|
dcgLogger(`dispatching /new`)
|
|
@@ -269,7 +274,12 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
269
274
|
})
|
|
270
275
|
} else if (interruptCommand.includes(text?.trim())) {
|
|
271
276
|
dcgLogger(`interrupt command: ${text}`)
|
|
272
|
-
|
|
277
|
+
sendMessageToGateway(
|
|
278
|
+
JSON.stringify({
|
|
279
|
+
method: 'chat.abort',
|
|
280
|
+
params: { sessionKey: effectiveSessionKey }
|
|
281
|
+
})
|
|
282
|
+
)
|
|
273
283
|
safeSendFinal()
|
|
274
284
|
return
|
|
275
285
|
} else {
|
|
@@ -280,7 +290,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
280
290
|
dispatcher,
|
|
281
291
|
replyOptions: {
|
|
282
292
|
...replyOptions,
|
|
283
|
-
abortSignal: genSignal,
|
|
293
|
+
// abortSignal: genSignal,
|
|
284
294
|
onModelSelected: prefixContext.onModelSelected,
|
|
285
295
|
onPartialReply: async (payload: ReplyPayload) => {
|
|
286
296
|
// Accumulate full text
|
|
@@ -293,7 +303,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
293
303
|
? payload.text.slice(streamedTextLen)
|
|
294
304
|
: payload.text
|
|
295
305
|
if (delta.trim()) {
|
|
296
|
-
sendChunk(
|
|
306
|
+
sendChunk(delta, outboundCtx)
|
|
297
307
|
dcgLogger(`[stream]: chunk ${delta.length} chars to user ${msg._userId} ${delta.slice(0, 100)}`)
|
|
298
308
|
}
|
|
299
309
|
streamedTextLen = payload.text.length
|
|
@@ -304,26 +314,26 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
304
314
|
const key = getMediaKey(mediaUrl)
|
|
305
315
|
if (sentMediaKeys.has(key)) continue
|
|
306
316
|
sentMediaKeys.add(key)
|
|
307
|
-
await sendDcgchatMedia({
|
|
317
|
+
await sendDcgchatMedia({ sessionKey: effectiveSessionKey, mediaUrl, text: '' })
|
|
308
318
|
}
|
|
309
319
|
}
|
|
310
320
|
}
|
|
311
321
|
})
|
|
312
322
|
}
|
|
313
323
|
} catch (err: unknown) {
|
|
314
|
-
if (genSignal.aborted) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
} else if (err instanceof Error && err.name === 'AbortError') {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
} else {
|
|
321
|
-
|
|
322
|
-
}
|
|
324
|
+
// if (genSignal.aborted) {
|
|
325
|
+
// wasAborted = true
|
|
326
|
+
// dcgLogger(` generation aborted for conversationId=${conversationId}`)
|
|
327
|
+
// } else if (err instanceof Error && err.name === 'AbortError') {
|
|
328
|
+
// wasAborted = true
|
|
329
|
+
// dcgLogger(` generation aborted for conversationId=${conversationId}`)
|
|
330
|
+
// } else {
|
|
331
|
+
// dcgLogger(` dispatchReplyFromConfig error: ${String(err)}`, 'error')
|
|
332
|
+
// }
|
|
323
333
|
} finally {
|
|
324
|
-
if (activeGenerations.get(conversationId) === genCtrl) {
|
|
325
|
-
|
|
326
|
-
}
|
|
334
|
+
// if (activeGenerations.get(conversationId) === genCtrl) {
|
|
335
|
+
// activeGenerations.delete(conversationId)
|
|
336
|
+
// }
|
|
327
337
|
}
|
|
328
338
|
try {
|
|
329
339
|
markRunComplete()
|
|
@@ -345,7 +355,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
345
355
|
const resolved = candidates.find((p) => fs.existsSync(p))
|
|
346
356
|
if (!resolved) continue
|
|
347
357
|
try {
|
|
348
|
-
await sendDcgchatMedia({
|
|
358
|
+
await sendDcgchatMedia({ sessionKey: effectiveSessionKey, mediaUrl: resolved, text: '' })
|
|
349
359
|
} catch (err) {
|
|
350
360
|
dcgLogger(` sendDcgchatMedia error: ${String(err)}`, 'error')
|
|
351
361
|
}
|
|
@@ -371,7 +381,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
371
381
|
})
|
|
372
382
|
} catch (err) {
|
|
373
383
|
dcgLogger(` handle message failed: ${String(err)}`, 'error')
|
|
374
|
-
sendError(
|
|
384
|
+
sendError(err instanceof Error ? err.message : String(err), outboundCtx)
|
|
375
385
|
} finally {
|
|
376
386
|
safeSendFinal()
|
|
377
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
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
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 { sendEventMessage } from './transport.js'
|
|
10
|
+
import { 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
|
+
import { channelInfo, ENV } from './utils/constant.js'
|
|
15
|
+
import { getCurrentSessionKey, getEffectiveMsgParams } from './utils/params.js'
|
|
16
|
+
|
|
17
|
+
export function getCronJobsPath(): string {
|
|
18
|
+
const workspaceDir = getWorkspaceDir()
|
|
19
|
+
const cronDir = workspaceDir.replace('workspace', 'cron')
|
|
20
|
+
return path.join(cronDir, 'jobs.json')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function msgParamsToCtx(p: IMsgParams): IMsgParams | null {
|
|
24
|
+
if (!p?.botToken) return null
|
|
25
|
+
return p
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const CRON_UPLOAD_DEBOUNCE_MS = 30_000
|
|
29
|
+
|
|
30
|
+
/** 待合并的上传上下文(短时间内多次调用只保留最后一次) */
|
|
31
|
+
let pendingCronUploadCtx: IMsgParams | null = null
|
|
32
|
+
let cronUploadFlushTimer: ReturnType<typeof setTimeout> | null = null
|
|
33
|
+
|
|
34
|
+
async function runCronJobsUpload(msgCtx: IMsgParams): Promise<void> {
|
|
35
|
+
const jobPath = getCronJobsPath()
|
|
36
|
+
if (fs.existsSync(jobPath)) {
|
|
37
|
+
try {
|
|
38
|
+
const url = await ossUpload(jobPath, msgCtx.botToken ?? '', 0)
|
|
39
|
+
dcgLogger(`定时任务创建成功: ${url}`)
|
|
40
|
+
if (!msgCtx.sessionKey) {
|
|
41
|
+
dcgLogger('runCronJobsUpload: missing sessionKey on msgCtx', 'error')
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
sendEventMessage(url, msgCtx.sessionKey)
|
|
45
|
+
} catch (error) {
|
|
46
|
+
dcgLogger(`${jobPath} upload failed: ${error}`, 'error')
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
dcgLogger(`${jobPath} not found`, 'error')
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function flushCronUploadQueue(): void {
|
|
54
|
+
cronUploadFlushTimer = null
|
|
55
|
+
const ctx = pendingCronUploadCtx
|
|
56
|
+
pendingCronUploadCtx = null
|
|
57
|
+
if (!ctx) return
|
|
58
|
+
void runCronJobsUpload(ctx)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 将 jobs.json 同步到 OSS 并推送事件。30s 内多次调用合并为一次上传;定时触发后清空待处理项,避免重复执行。
|
|
63
|
+
* @param msgCtx 可选;省略时使用当前会话 getEffectiveMsgParams(sessionKey) 快照
|
|
64
|
+
*/
|
|
65
|
+
export function sendDcgchatCron(): void {
|
|
66
|
+
const ctx = msgParamsToCtx(getEffectiveMsgParams(getCurrentSessionKey() ?? ''))
|
|
67
|
+
if (!ctx) {
|
|
68
|
+
dcgLogger('sendDcgchatCron: no message context (missing token / params)', 'error')
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
pendingCronUploadCtx = ctx
|
|
72
|
+
if (cronUploadFlushTimer !== null) {
|
|
73
|
+
clearTimeout(cronUploadFlushTimer)
|
|
74
|
+
}
|
|
75
|
+
cronUploadFlushTimer = setTimeout(flushCronUploadQueue, CRON_UPLOAD_DEBOUNCE_MS)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 通过 OpenClaw CLI 删除定时任务(走 Gateway,与内存状态一致)。
|
|
80
|
+
* 文档:运行中请勿手改 jobs.json,应使用 `openclaw cron rm` 或工具 API。
|
|
81
|
+
*/
|
|
82
|
+
export const onRemoveCronJob = async (jobId: string) => {
|
|
83
|
+
const id = jobId?.trim()
|
|
84
|
+
if (!id) {
|
|
85
|
+
dcgLogger('onRemoveCronJob: empty jobId', 'error')
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
sendMessageToGateway(JSON.stringify({ method: 'cron.remove', params: { id: jobId } }))
|
|
89
|
+
}
|
|
90
|
+
export const onDisabledCronJob = async (jobId: string) => {
|
|
91
|
+
const id = jobId?.trim()
|
|
92
|
+
if (!id) {
|
|
93
|
+
dcgLogger('onRemoveCronJob: empty jobId', 'error')
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
sendMessageToGateway(JSON.stringify({ method: 'cron.update', params: { id: jobId, patch: { enabled: false } } }))
|
|
97
|
+
}
|
|
98
|
+
export const onEnabledCronJob = async (jobId: string) => {
|
|
99
|
+
const id = jobId?.trim()
|
|
100
|
+
if (!id) {
|
|
101
|
+
dcgLogger('onRemoveCronJob: empty jobId', 'error')
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
sendMessageToGateway(JSON.stringify({ method: 'cron.update', params: { id: jobId, patch: { enabled: true } } }))
|
|
105
|
+
}
|
|
106
|
+
export const onRunCronJob = async (jobId: string) => {
|
|
107
|
+
const id = jobId?.trim()
|
|
108
|
+
if (!id) {
|
|
109
|
+
dcgLogger('onRemoveCronJob: empty jobId', 'error')
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
sendMessageToGateway(JSON.stringify({ method: 'cron.update', jobId }))
|
|
113
|
+
}
|
|
114
|
+
export const updateCronJobSessionKey = async (jobId: string) => {
|
|
115
|
+
const id = jobId?.trim()
|
|
116
|
+
if (!id) {
|
|
117
|
+
dcgLogger('onRemoveCronJob: empty jobId', 'error')
|
|
118
|
+
return
|
|
119
|
+
}
|
|
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
|
+
)
|
|
138
|
+
}
|