@dcrays/dcgchat-test 0.2.33 → 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 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",
@@ -13,9 +14,9 @@ const plugin = {
13
14
  register(api: OpenClawPluginApi) {
14
15
  setDcgchatRuntime(api.runtime)
15
16
 
16
- console.log('🚀 ~ handleDcgchatMessage ~ process.platform:', process.platform)
17
17
  monitoringToolMessage(api)
18
18
  setOpenClawConfig(api.config)
19
+ startDcgchatGatewaySocket()
19
20
  api.registerChannel({ plugin: dcgchatPlugin })
20
21
  setWorkspaceDir(api.config?.agents?.defaults?.workspace)
21
22
  api.registerTool((ctx) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcrays/dcgchat-test",
3
- "version": "0.2.33",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "OpenClaw channel plugin for 书灵墨宝 (WebSocket)",
6
6
  "main": "index.ts",
package/src/bot.ts CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  getDcgchatRuntime,
9
9
  getOpenClawConfig,
10
10
  getWorkspaceDir,
11
- getWsConnection,
11
+ setMsgParamsSessionKey,
12
12
  setMsgStatus
13
13
  } from './utils/global.js'
14
14
  import { resolveAccount, sendDcgchatMedia } from './channel.js'
@@ -17,6 +17,7 @@ import { extractMobookFiles } from './utils/searchFile.js'
17
17
  import { createMsgContext, sendChunk, sendFinal, sendText as sendTextMsg, sendError, sendText } from './transport.js'
18
18
  import { dcgLogger } from './utils/log.js'
19
19
  import { channelInfo, systemCommand, interruptCommand, ENV } from './utils/constant.js'
20
+ import { sendMessageToGateway } from './gateway/socket.js'
20
21
 
21
22
  type MediaInfo = {
22
23
  path: string
@@ -30,16 +31,16 @@ type TFileInfo = { name: string; url: string }
30
31
  const mediaMaxBytes = 300 * 1024 * 1024
31
32
 
32
33
  /** Active LLM generation abort controllers, keyed by conversationId */
33
- const activeGenerations = new Map<string, AbortController>()
34
+ // const activeGenerations = new Map<string, AbortController>()
34
35
 
35
- /** Abort an in-progress LLM generation for a given conversationId */
36
- export function abortMobookappGeneration(conversationId: string): void {
37
- const ctrl = activeGenerations.get(conversationId)
38
- if (ctrl) {
39
- ctrl.abort()
40
- activeGenerations.delete(conversationId)
41
- }
42
- }
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
+ // }
43
44
 
44
45
  /**
45
46
  * Extract agentId from conversation_id formatted as "agentId::suffix".
@@ -133,6 +134,14 @@ function resolveReplyMediaList(payload: ReplyPayload): string[] {
133
134
  export async function handleDcgchatMessage(msg: InboundMessage, accountId: string): Promise<void> {
134
135
  const msgCtx = createMsgContext(msg)
135
136
 
137
+ let finalSent = false
138
+
139
+ const safeSendFinal = () => {
140
+ if (finalSent) return
141
+ finalSent = true
142
+ sendFinal(msgCtx)
143
+ }
144
+
136
145
  let completeText = ''
137
146
  const config = getOpenClawConfig()
138
147
  if (!config) {
@@ -145,7 +154,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
145
154
 
146
155
  if (!text) {
147
156
  sendTextMsg(msgCtx, '你需要我帮你做什么呢?')
148
- sendFinal(msgCtx)
157
+ safeSendFinal()
149
158
  return
150
159
  }
151
160
 
@@ -153,6 +162,8 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
153
162
  const core = getDcgchatRuntime()
154
163
 
155
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()
156
167
 
157
168
  const route = core.channel.routing.resolveAgentRoute({
158
169
  cfg: config,
@@ -164,20 +175,20 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
164
175
  // If conversation_id encodes an agentId prefix ("agentId::suffix"), override the route.
165
176
  const embeddedAgentId = extractAgentIdFromConversationId(conversationId)
166
177
  const effectiveAgentId = embeddedAgentId ?? route.agentId
167
- const effectiveSessionKey = embeddedAgentId
168
- ? `agent:${embeddedAgentId}:mobook:direct:${conversationId}`.toLowerCase()
169
- : route.sessionKey
178
+ const effectiveSessionKey =
179
+ realMobook === '1' ? route.sessionKey : `agent:main:mobook:direct:${agentId}:${conversationId}`.toLowerCase()
180
+ setMsgParamsSessionKey(effectiveSessionKey)
170
181
 
171
182
  const agentEntry =
172
183
  effectiveAgentId && effectiveAgentId !== 'main' ? config.agents?.list?.find((a) => a.id === effectiveAgentId) : undefined
173
184
  const agentDisplayName = agentEntry?.name ?? (effectiveAgentId && effectiveAgentId !== 'main' ? effectiveAgentId : undefined)
174
185
 
175
186
  // Abort any existing generation for this conversation, then start a new one
176
- const existingCtrl = activeGenerations.get(conversationId)
177
- if (existingCtrl) existingCtrl.abort()
178
- const genCtrl = new AbortController()
179
- const genSignal = genCtrl.signal
180
- 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)
181
192
 
182
193
  // 处理用户上传的文件
183
194
  const files = msg.content.files ?? []
@@ -245,14 +256,14 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
245
256
  }
246
257
  },
247
258
  onError: (err: unknown, info: { kind: string }) => {
259
+ safeSendFinal()
248
260
  dcgLogger(`${info.kind} reply failed: ${String(err)}`, 'error')
249
261
  },
250
262
  onIdle: () => {
251
- sendFinal(msgCtx)
263
+ safeSendFinal()
252
264
  }
253
265
  })
254
266
 
255
- let wasAborted = false
256
267
  try {
257
268
  if (systemCommand.includes(text?.trim())) {
258
269
  dcgLogger(`dispatching /new`)
@@ -267,7 +278,13 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
267
278
  })
268
279
  } else if (interruptCommand.includes(text?.trim())) {
269
280
  dcgLogger(`interrupt command: ${text}`)
270
- abortMobookappGeneration(conversationId)
281
+ // abortMobookappGeneration(conversationId)
282
+ sendMessageToGateway(
283
+ JSON.stringify({
284
+ method: 'chat.abort',
285
+ params: { sessionKey: effectiveSessionKey }
286
+ })
287
+ )
271
288
  sendFinal(msgCtx)
272
289
  return
273
290
  } else {
@@ -278,7 +295,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
278
295
  dispatcher,
279
296
  replyOptions: {
280
297
  ...replyOptions,
281
- abortSignal: genSignal,
298
+ // abortSignal: genSignal,
282
299
  onModelSelected: prefixContext.onModelSelected,
283
300
  onPartialReply: async (payload: ReplyPayload) => {
284
301
  // Accumulate full text
@@ -309,26 +326,26 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
309
326
  })
310
327
  }
311
328
  } catch (err: unknown) {
312
- if (genSignal.aborted) {
313
- wasAborted = true
314
- dcgLogger(` generation aborted for conversationId=${conversationId}`)
315
- } else if (err instanceof Error && err.name === 'AbortError') {
316
- wasAborted = true
317
- dcgLogger(` generation aborted for conversationId=${conversationId}`)
318
- } else {
319
- dcgLogger(` dispatchReplyFromConfig error: ${String(err)}`, 'error')
320
- }
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
+ // }
321
338
  } finally {
322
- if (activeGenerations.get(conversationId) === genCtrl) {
323
- activeGenerations.delete(conversationId)
324
- }
339
+ // if (activeGenerations.get(conversationId) === genCtrl) {
340
+ // activeGenerations.delete(conversationId)
341
+ // }
325
342
  }
326
343
  try {
327
344
  markRunComplete()
345
+ markDispatchIdle()
328
346
  } catch (err) {
329
- dcgLogger(` markRunComplete error: ${String(err)}`, 'error')
347
+ dcgLogger(` markRunComplete||markRunComplete error: ${String(err)}`, 'error')
330
348
  }
331
- markDispatchIdle()
332
349
  if (![...systemCommand, ...interruptCommand].includes(text?.trim())) {
333
350
  for (const file of extractMobookFiles(completeText)) {
334
351
  const candidates: string[] = [file]
@@ -342,10 +359,14 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
342
359
  }
343
360
  const resolved = candidates.find((p) => fs.existsSync(p))
344
361
  if (!resolved) continue
345
- await sendDcgchatMedia({ msgCtx, mediaUrl: resolved, text: '' })
362
+ try {
363
+ await sendDcgchatMedia({ msgCtx, mediaUrl: resolved, text: '' })
364
+ } catch (err) {
365
+ dcgLogger(` sendDcgchatMedia error: ${String(err)}`, 'error')
366
+ }
346
367
  }
347
368
  }
348
- sendFinal(msgCtx)
369
+ safeSendFinal()
349
370
  clearSentMediaKeys(msg.content.message_id)
350
371
  setMsgStatus('finished')
351
372
 
@@ -366,5 +387,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
366
387
  } catch (err) {
367
388
  dcgLogger(` handle message failed: ${String(err)}`, 'error')
368
389
  sendError(msgCtx, err instanceof Error ? err.message : String(err))
390
+ } finally {
391
+ safeSendFinal()
369
392
  }
370
393
  }
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
+ }