@dcrays/dcgchat 0.3.20 → 0.3.28

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcrays/dcgchat",
3
- "version": "0.3.20",
3
+ "version": "0.3.28",
4
4
  "type": "module",
5
5
  "description": "OpenClaw channel plugin for 书灵墨宝 (WebSocket)",
6
6
  "main": "index.ts",
package/src/bot.ts CHANGED
@@ -1,9 +1,9 @@
1
- import fs from 'node:fs'
2
1
  import path from 'node:path'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
3
4
  import type { ReplyPayload } from 'openclaw/plugin-sdk'
4
5
  import { createReplyPrefixContext } from 'openclaw/plugin-sdk'
5
6
  import type { InboundMessage } from './types.js'
6
- import os from 'node:os'
7
7
  import {
8
8
  clearSentMediaKeys,
9
9
  getDcgchatRuntime,
package/src/channel.ts CHANGED
@@ -5,18 +5,18 @@ import { ossUpload } from './request/oss.js'
5
5
  import { addSentMediaKey, getCronMessageId, getOpenClawConfig, hasSentMediaKey } from './utils/global.js'
6
6
  import { isWsOpen, mergeDefaultParams, mergeSessionParams, sendFinal, wsSendRaw } from './transport.js'
7
7
  import { dcgLogger, setLogger } from './utils/log.js'
8
- import { getEffectiveMsgParams, getCurrentSessionKey } from './utils/params.js'
8
+ import { getOutboundMsgParams } from './utils/params.js'
9
9
  import { startDcgchatGatewaySocket } from './gateway/socket.js'
10
10
 
11
11
  export type DcgchatMediaSendOptions = {
12
- /** 与 setParamsMessage / map 一致,用于 getEffectiveMsgParams */
12
+ /** 与 setParamsMessage / map 一致,用于 getOutboundMsgParams */
13
13
  sessionKey: string
14
14
  mediaUrl?: string
15
15
  text?: string
16
16
  }
17
17
 
18
18
  export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<void> {
19
- const msgCtx = getEffectiveMsgParams(opts.sessionKey)
19
+ const msgCtx = getOutboundMsgParams(opts.sessionKey ?? '')
20
20
  if (!isWsOpen()) {
21
21
  dcgLogger(`outbound media skipped -> ws not open: ${opts.mediaUrl ?? ''}`)
22
22
  return
@@ -36,6 +36,7 @@ export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<v
36
36
 
37
37
  try {
38
38
  const botToken = msgCtx.botToken ?? getOpenClawConfig()?.channels?.["dcgchat"]?.botToken ?? ''
39
+ console.log('🚀 ~ sendDcgchatMedia ~ botToken:', botToken)
39
40
  const url = opts.mediaUrl ? await ossUpload(opts.mediaUrl, botToken, 1) : ''
40
41
  wsSendRaw(msgCtx, {
41
42
  response: opts.text ?? '',
@@ -147,7 +148,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
147
148
  const isCron = ctx.to.indexOf('dcg-cron:') >= 0
148
149
  const to = ctx.to.replace('dcg-cron:', '')
149
150
  dcgLogger(`channel sendText to ${ctx.to} `)
150
- const outboundCtx = getEffectiveMsgParams(to)
151
+ const outboundCtx = getOutboundMsgParams(to)
151
152
  const cronMsgId = getCronMessageId(to)
152
153
  const messageId = !!cronMsgId ? cronMsgId : isCron ? `${Date.now()}` : outboundCtx?.messageId
153
154
  if (isWsOpen()) {
@@ -176,7 +177,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
176
177
  },
177
178
  sendMedia: async (ctx) => {
178
179
  const to = ctx.to.replace('dcg-cron:', '')
179
- const msgCtx = getEffectiveMsgParams(to)
180
+ const msgCtx = getOutboundMsgParams(to)
180
181
  const cronMsgId = getCronMessageId(to)
181
182
  const isCron = ctx.to.indexOf('dcg-cron:') >= 0
182
183
  const messageId = !!cronMsgId ? cronMsgId : isCron ? `${Date.now()}` : msgCtx?.messageId
package/src/cron.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  import path from 'node:path'
2
2
  import fs from 'node:fs'
3
3
  import type { IMsgParams } from './types.js'
4
- import { mergeDefaultParams, sendEventMessage, sendFinal } from './transport.js'
5
- import { getCronMessageId, getWorkspaceDir, removeCronMessageId, setCronMessageId, setMsgStatus } from './utils/global.js'
4
+ import { isWsOpen, mergeDefaultParams, sendEventMessage, sendFinal } from './transport.js'
5
+ import { getCronMessageId, getWorkspaceDir, getWsConnection, removeCronMessageId, setCronMessageId } from './utils/global.js'
6
6
  import { ossUpload } from './request/oss.js'
7
7
  import { dcgLogger } from './utils/log.js'
8
8
  import { sendMessageToGateway } from './gateway/socket.js'
9
- import { getCurrentSessionKey, getEffectiveMsgParams } from './utils/params.js'
9
+ import { getEffectiveMsgParams } from './utils/params.js'
10
10
 
11
11
  export function getCronJobsPath(): string {
12
12
  const workspaceDir = getWorkspaceDir()
@@ -16,13 +16,13 @@ export function getCronJobsPath(): string {
16
16
 
17
17
  type CronJobsFile = {
18
18
  version?: number
19
- jobs?: Array<{ id?: string; sessionKey?: string }>
19
+ jobs?: Array<{ id?: string; sessionKey?: string; name?: string }>
20
20
  }
21
21
 
22
22
  /**
23
23
  * 在 `jobPath` 指向的 jobs.json(通常为 getCronJobsPath())中按 id 查找任务并返回其 sessionKey。
24
24
  */
25
- export function readCronJobSessionKey(jobPath: string, jobId: string): string | null {
25
+ export function readCronJob(jobPath: string, jobId: string): Record<string, any> | null {
26
26
  const id = jobId?.trim()
27
27
  if (!id) return null
28
28
  if (!fs.existsSync(jobPath)) {
@@ -33,8 +33,7 @@ export function readCronJobSessionKey(jobPath: string, jobId: string): string |
33
33
  const raw = fs.readFileSync(jobPath, 'utf8')
34
34
  const data = JSON.parse(raw) as CronJobsFile
35
35
  const job = (data.jobs ?? []).find((j) => j.id === id)
36
- const sk = job?.sessionKey?.trim()
37
- return sk || null
36
+ return job || null
38
37
  } catch (e) {
39
38
  dcgLogger(`readCronJobSessionKey: failed to read ${jobPath}: ${String(e)}`, 'error')
40
39
  return null
@@ -83,12 +82,15 @@ function flushCronUploadQueue(): void {
83
82
  * 将 jobs.json 同步到 OSS 并推送事件。30s 内多次调用合并为一次上传;定时触发后清空待处理项,避免重复执行。
84
83
  * @param msgCtx 可选;省略时使用当前会话 getEffectiveMsgParams(sessionKey) 快照
85
84
  */
86
- export function sendDcgchatCron(): void {
87
- const ctx = msgParamsToCtx(getEffectiveMsgParams(getCurrentSessionKey() ?? ''))
85
+ export function sendDcgchatCron(jobId: string): void {
86
+ const jobPath = getCronJobsPath()
87
+ const { sessionKey } = readCronJob(jobPath, jobId) || {}
88
+ const ctx = msgParamsToCtx(getEffectiveMsgParams(sessionKey))
88
89
  if (!ctx) {
89
90
  dcgLogger('sendDcgchatCron: no message context (missing token / params)', 'error')
90
91
  return
91
92
  }
93
+ dcgLogger(`sessionKey: ${sessionKey}, jobId: ${jobId}`)
92
94
  pendingCronUploadCtx = ctx
93
95
  if (cronUploadFlushTimer !== null) {
94
96
  clearTimeout(cronUploadFlushTimer)
@@ -131,13 +133,19 @@ export const onRunCronJob = async (jobId: string, messageId: string) => {
131
133
  return
132
134
  }
133
135
  const jobPath = getCronJobsPath()
134
- const sessionKey = readCronJobSessionKey(jobPath, jobId) || ''
136
+ const { sessionKey } = readCronJob(jobPath, jobId) || {}
135
137
  if (!sessionKey) {
136
138
  dcgLogger(`finishedDcgchatCron: no sessionKey for job id=${id}`, 'error')
137
139
  return
138
140
  }
139
141
  setCronMessageId(sessionKey, messageId)
140
- sendMessageToGateway(JSON.stringify({ method: 'cron.run', params: { id: jobId } }))
142
+ sendMessageToGateway(
143
+ JSON.stringify({
144
+ method: 'cron.runs',
145
+ params: { scope: 'job', id: jobId, limit: 50, offset: 0, status: 'all', sortDir: 'desc' }
146
+ })
147
+ )
148
+ sendMessageToGateway(JSON.stringify({ method: 'cron.run', params: { id: jobId, mode: 'force' } }))
141
149
  }
142
150
  export const finishedDcgchatCron = async (jobId: string) => {
143
151
  const id = jobId?.trim()
@@ -146,20 +154,20 @@ export const finishedDcgchatCron = async (jobId: string) => {
146
154
  return
147
155
  }
148
156
  const jobPath = getCronJobsPath()
149
- const sessionKey = readCronJobSessionKey(jobPath, id)
157
+ const { sessionKey, name } = readCronJob(jobPath, id) || {}
150
158
  if (!sessionKey) {
151
159
  dcgLogger(`finishedDcgchatCron: no sessionKey for job id=${id}`, 'error')
152
160
  return
153
161
  }
154
162
  const outboundCtx = getEffectiveMsgParams(sessionKey)
155
163
  const messageId = getCronMessageId(sessionKey)
164
+ const sessionInfo = sessionKey.split(':')
165
+ const sessionId = sessionInfo.at(-1) ?? ''
166
+ const agentId = sessionInfo.at(-2) ?? ''
156
167
  if (outboundCtx?.sessionId) {
157
168
  const newCtx = messageId ? { ...outboundCtx, messageId } : outboundCtx
158
169
  sendFinal(newCtx, 'cron send')
159
170
  } else {
160
- const sessionInfo = sessionKey.split(':')
161
- const sessionId = sessionInfo.at(-1) ?? ''
162
- const agentId = sessionInfo.at(-2) ?? ''
163
171
  const merged = mergeDefaultParams({
164
172
  agentId: agentId,
165
173
  sessionId: `${sessionId}`,
@@ -169,6 +177,24 @@ export const finishedDcgchatCron = async (jobId: string) => {
169
177
  })
170
178
  sendFinal(merged, 'cron send')
171
179
  }
180
+ const ws = getWsConnection()
181
+ if (isWsOpen()) {
182
+ ws?.send(
183
+ JSON.stringify({
184
+ messageType: 'openclaw_bot_event',
185
+ source: 'client',
186
+ content: {
187
+ event_type: 'notify',
188
+ operation_type: 'cron',
189
+ session_id: sessionId,
190
+ agentId: agentId,
191
+ real_mobook: !sessionId ? 1 : '',
192
+ title: name
193
+ }
194
+ })
195
+ )
196
+ dcgLogger(`定时任务执行成功: ${id}`)
197
+ }
172
198
  removeCronMessageId(sessionKey)
173
199
  dcgLogger(`finishedDcgchatCron: job=${id} sessionKey=${sessionKey}`)
174
200
  }
@@ -4,6 +4,7 @@ import crypto from 'crypto'
4
4
  import { deriveDeviceIdFromPublicKey, publicKeyRawBase64UrlFromPem, buildDeviceAuthPayloadV3, signDevicePayload } from './security.js'
5
5
  import { dcgLogger } from '../utils/log.js'
6
6
  import { finishedDcgchatCron, sendDcgchatCron } from '../cron.js'
7
+ import { getWorkspaceDir } from '../utils/global.js'
7
8
 
8
9
  export interface GatewayEvent {
9
10
  type: string
@@ -114,7 +115,8 @@ export class GatewayConnection {
114
115
  private loadOrCreateDeviceIdentity() {
115
116
  const fs = require('fs')
116
117
  const path = require('path')
117
- const stateDir = path.join(process.cwd(), '.state')
118
+ const workspaceDir = getWorkspaceDir()
119
+ const stateDir = path.join(workspaceDir, '.state')
118
120
  const deviceFile = path.join(stateDir, 'device.json')
119
121
 
120
122
  // Try to load existing identity
@@ -7,7 +7,7 @@ export const getStsToken = async (name: string, botToken: string, isPrivate: 1 |
7
7
  // 确保 userToken 已缓存(如果未缓存会自动获取并缓存)
8
8
  await getUserToken(botToken)
9
9
 
10
- const response = await post<IStsTokenReq, IStsToken>('/user/getStsToken', { sourceFileName: name, isPrivate }, { botToken })
10
+ const response = await post<IStsTokenReq, IStsToken>('/user/getStsTokenByAsync', { sourceFileName: name, isPrivate }, { botToken })
11
11
 
12
12
  if (!response || !response.data || !response.data.bucket) {
13
13
  throw new Error('获取 OSS 临时凭证失败')
@@ -40,7 +40,7 @@ export const queryUserTokenByBotToken = async (botToken: string): Promise<string
40
40
  const response = await post<{ botToken: string }, { token: string }>('/organization/queryUserTokenByBotToken', { botToken })
41
41
 
42
42
  if (!response || !response.data || !response.data.token) {
43
- dcgLogger('获取绑定的用户信息失败', 'error')
43
+ dcgLogger('获取绑定的用户信息失败: ' + JSON.stringify(response), 'error')
44
44
  return ''
45
45
  }
46
46
 
@@ -38,7 +38,9 @@ export const ossUpload = async (file: File | string | Buffer, botToken: string,
38
38
  bucket: data.bucket,
39
39
  endpoint: data.endPoint,
40
40
  region: data.region,
41
- secure: true
41
+ secure: true,
42
+ cname: true,
43
+ authorizationV4: true
42
44
  }
43
45
 
44
46
  const client = new OSS(options)
@@ -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 { getCurrentSessionKey, getEffectiveMsgParams } from '../utils/params.js'
6
+ import { getEffectiveMsgParams } from '../utils/params.js'
7
7
  import { ENV } from '../utils/constant.js'
8
8
  import { dcgLogger } from '../utils/log.js'
9
9
 
@@ -39,9 +39,7 @@ function toCurl(config: {
39
39
  }): string {
40
40
  const base = config.baseURL ?? ''
41
41
  const path = config.url ?? ''
42
- const url = path.startsWith('http')
43
- ? path
44
- : `${base.replace(/\/$/, '')}/${path.replace(/^\//, '')}`
42
+ const url = path.startsWith('http') ? path : `${base.replace(/\/$/, '')}/${path.replace(/^\//, '')}`
45
43
  const method = (config.method ?? 'GET').toUpperCase()
46
44
  const headers = config.headers ?? {}
47
45
  const parts = ['curl', '-X', method, `'${url}'`]
@@ -82,9 +80,7 @@ export function getSignature(
82
80
  sortedKeys
83
81
  .map((key) => {
84
82
  const val = map[key as keyof typeof map]
85
- return val === undefined
86
- ? ''
87
- : `${key}${typeof val === 'object' ? JSON.stringify(val) : val}`
83
+ return val === undefined ? '' : `${key}${typeof val === 'object' ? JSON.stringify(val) : val}`
88
84
  })
89
85
  .join('') + signKey[ENV]
90
86
  // 4. MD5 加密并转大写
@@ -130,9 +126,7 @@ axiosInstance.interceptors.request.use(
130
126
  if (cachedToken) {
131
127
  config.headers = config.headers || {}
132
128
  config.headers.authorization = cachedToken
133
- dcgLogger(
134
- `[request] auto-injected userToken from cache for botToken=${botToken.slice(0, 10)}...`
135
- )
129
+ dcgLogger(`[request] auto-injected userToken from cache for botToken=${botToken.slice(0, 10)}...`)
136
130
  }
137
131
  }
138
132
 
@@ -172,7 +166,7 @@ export function post<T = Record<string, unknown>, R = unknown>(
172
166
  botToken?: string
173
167
  }
174
168
  ): Promise<IResponse<R>> {
175
- const params = getEffectiveMsgParams(getCurrentSessionKey() ?? '') || {}
169
+ const params = getEffectiveMsgParams() || { appId: 100 }
176
170
  const config: any = {
177
171
  method: 'POST',
178
172
  url,
package/src/tool.ts CHANGED
@@ -2,12 +2,9 @@ import type { OpenClawPluginApi } from 'openclaw/plugin-sdk'
2
2
  import { getMsgStatus } from './utils/global.js'
3
3
  import { dcgLogger } from './utils/log.js'
4
4
  import { sendFinal, sendText, wsSendRaw } from './transport.js'
5
- import { getCurrentSessionKey, getEffectiveMsgParams } from './utils/params.js'
6
- import { channelInfo, ENV } from './utils/constant.js'
5
+ import { getEffectiveMsgParams } from './utils/params.js'
7
6
  import { cronToolCall } from './cronToolCall.js'
8
7
 
9
- let toolCallId = ''
10
- let toolName = ''
11
8
  type PluginHookName =
12
9
  | 'before_model_resolve'
13
10
  | 'before_prompt_build'
@@ -56,13 +53,8 @@ const eventList = [
56
53
 
57
54
  function sendToolCallMessage(sk: string, text: string, toolCallId: string, isCover: number) {
58
55
  const params = getEffectiveMsgParams(sk)
59
- wsSendRaw(params, {
60
- is_finish: -1,
61
- tool_call_id: toolCallId,
62
- is_cover: isCover,
63
- thinking_content: text,
64
- response: ''
65
- })
56
+ const content = { is_finish: -1, tool_call_id: toolCallId, is_cover: isCover, thinking_content: text, response: '' }
57
+ wsSendRaw(params, content, false)
66
58
  }
67
59
 
68
60
  /**
package/src/transport.ts CHANGED
@@ -150,12 +150,13 @@ export function wsSend(ctx: IMsgParams, content: Record<string, unknown>): boole
150
150
  * 媒体 / channel 出站:content 保持嵌套对象(单次编码)。
151
151
  * `ctx` 须由调用方解析(如需合并覆盖可先 mergeSessionParams)。
152
152
  */
153
- export function wsSendRaw(ctx: IMsgParams, content: Record<string, unknown>): boolean {
153
+ export function wsSendRaw(ctx: IMsgParams, content: Record<string, unknown>, isLog = true): boolean {
154
154
  const ws = getWsConnection()
155
155
  if (isWsOpen()) {
156
156
  ws?.send(JSON.stringify(buildOpenclawBotChat(ctx, content, { mergeChannelDefaults: true })))
157
-
158
- dcgLogger('已发送:' + JSON.stringify(buildOpenclawBotChat(ctx, content, { mergeChannelDefaults: true })))
157
+ if (isLog) {
158
+ dcgLogger('已发送:' + JSON.stringify(buildOpenclawBotChat(ctx, content, { mergeChannelDefaults: true })))
159
+ }
159
160
  }
160
161
  return true
161
162
  }
@@ -195,7 +196,7 @@ export function sendEventMessage(url: string, sessionKey: string) {
195
196
  bot_id: ctx.botId,
196
197
  agent_id: ctx.agentId,
197
198
  session_id: ctx.sessionId,
198
- message_id: ctx.messageId || Date.now().toString()
199
+ message_id: Date.now().toString()
199
200
  }
200
201
  })
201
202
  )
@@ -7,12 +7,9 @@ import type { DcgchatConfig, IMsgParams } from '../types.js'
7
7
  */
8
8
  const paramsMessageMap = new Map<string, IMsgParams>()
9
9
 
10
- /** 最近一次 setParamsMessage 的 key,供不传参的 getEffectiveMsgParams() 使用 */
11
- let currentSessionKey: string | null = null
12
-
13
10
  /** 从 OpenClaw 配置读取当前 channel 的基础参数(唯一来源,供 transport / resolve 等复用) */
14
11
  export function getParamsDefaults(): IMsgParams {
15
- const ch = (getOpenClawConfig()?.channels?.["dcgchat"] as DcgchatConfig | undefined) ?? {}
12
+ const ch = (getOpenClawConfig()?.channels?.['dcgchat'] as DcgchatConfig | undefined) ?? {}
16
13
  return {
17
14
  userId: Number(ch.userId ?? 0),
18
15
  botToken: ch.botToken ?? '',
@@ -45,14 +42,25 @@ export function resolveParamsMessage(params: Partial<IMsgParams>): IMsgParams {
45
42
  * 统一取值入口:显式 sessionKey,或回落到当前会话;再与配置缺省 merge,保证字段完整。
46
43
  */
47
44
  export function getEffectiveMsgParams(sessionKey?: string): IMsgParams {
48
- const key = sessionKey ?? currentSessionKey
49
- const stored = key ? paramsMessageMap.get(key) : undefined
45
+ const stored = sessionKey ? paramsMessageMap.get(sessionKey) : undefined
50
46
  return stored ? resolveParamsMessage(stored) : getParamsDefaults()
51
47
  }
52
48
 
49
+ /**
50
+ * Agent `message` 工具常把 `target` 设为用户 ID(如 "150"),而 `setParamsMessage` 使用的 key 是
51
+ * `effectiveSessionKey`(如 `agent:main:mobook:direct:...`)。若按 preferredKey 查不到 map,
52
+ * 则回落到当前会话 `currentSessionKey`,避免拿到空 `messageId` / `sessionId` 导致无文件卡片、WS 上下文错误。
53
+ */
54
+ export function getOutboundMsgParams(preferredKey: string): IMsgParams {
55
+ const k = preferredKey?.trim()
56
+ if (k && paramsMessageMap.has(k)) {
57
+ return getEffectiveMsgParams(k)
58
+ }
59
+ return getEffectiveMsgParams()
60
+ }
61
+
53
62
  export function setParamsMessage(sessionKey: string, params: Partial<IMsgParams>) {
54
63
  if (!sessionKey) return
55
- currentSessionKey = sessionKey
56
64
  const previous = paramsMessageMap.get(sessionKey)
57
65
  const base = previous ? resolveParamsMessage(previous) : getParamsDefaults()
58
66
  paramsMessageMap.set(sessionKey, resolveParamsMessage({ ...base, ...params, sessionKey }))
@@ -61,7 +69,3 @@ export function setParamsMessage(sessionKey: string, params: Partial<IMsgParams>
61
69
  export function getParamsMessage(sessionKey: string): IMsgParams | undefined {
62
70
  return paramsMessageMap.get(sessionKey)
63
71
  }
64
-
65
- export function getCurrentSessionKey(): string | null {
66
- return currentSessionKey
67
- }