@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.
@@ -0,0 +1,285 @@
1
+ import type { OpenClawConfig } from 'openclaw/plugin-sdk'
2
+ import { getOpenClawConfig } from '../utils/global.js'
3
+ import { GatewayConnection, type GatewayConfig } from './index.js'
4
+ import { dcgLogger } from '../utils/log.js'
5
+
6
+ /** 与 gateway-methods 中 registerGatewayMethod 名称一致(供引用) */
7
+ export const DCGCHAT_GATEWAY_METHODS = ['dcgchat.cron.status', 'dcgchat.cron.add', 'dcgchat.cron.list', 'dcgchat.cron.remove'] as const
8
+
9
+ const PING_INTERVAL_MS = 30_000
10
+ const RECONNECT_DELAY_MS = 5_000
11
+
12
+ type OpenClawGatewaySection = {
13
+ port?: number
14
+ bind?: string
15
+ tls?: { enabled?: boolean }
16
+ auth?: { mode?: string; token?: string }
17
+ remote?: { url?: string }
18
+ }
19
+
20
+ function getGatewaySection(cfg: OpenClawConfig): OpenClawGatewaySection | undefined {
21
+ return (cfg as OpenClawConfig & { gateway?: OpenClawGatewaySection }).gateway
22
+ }
23
+
24
+ /**
25
+ * 从 OpenClaw 配置解析本地/远程 Gateway WebSocket 与 token。
26
+ * 可用环境变量覆盖:OPENCLAW_GATEWAY_URL、OPENCLAW_GATEWAY_TOKEN。
27
+ */
28
+ export function resolveGatewayClientConfig(cfg: OpenClawConfig | null): GatewayConfig {
29
+ if (!cfg) {
30
+ throw new Error('OpenClaw 配置未初始化(需先完成插件 register)')
31
+ }
32
+ const g = getGatewaySection(cfg)
33
+ const token = process.env.OPENCLAW_GATEWAY_TOKEN || g?.auth?.token || ''
34
+ if (!token) {
35
+ throw new Error('缺少 Gateway token:请在 openclaw.json 的 gateway.auth.token 设置,或设置环境变量 OPENCLAW_GATEWAY_TOKEN')
36
+ }
37
+
38
+ const scheme = g?.tls?.enabled === true ? 'wss' : 'ws'
39
+ const url = process.env.OPENCLAW_GATEWAY_URL || g?.remote?.url || `${scheme}://127.0.0.1:${g?.port ?? 18789}`
40
+
41
+ return {
42
+ url,
43
+ token,
44
+ role: 'operator',
45
+ scopes: ['operator.admin']
46
+ }
47
+ }
48
+
49
+ function resolveConfigSafe(): GatewayConfig | null {
50
+ try {
51
+ return resolveGatewayClientConfig(getOpenClawConfig())
52
+ } catch (e) {
53
+ dcgLogger(`Gateway 持久连接未启动: ${e}`, 'error')
54
+ return null
55
+ }
56
+ }
57
+
58
+ /**
59
+ * 解析要调用的 Gateway 方法。
60
+ * - 纯文本:整段为 method,params 为空
61
+ * - JSON:`{ "method": "dcgchat.cron.list", "params": { "includeDisabled": true } }`
62
+ */
63
+ export function parseGatewayRpcMessage(message: string): { method: string; params: Record<string, unknown> } {
64
+ const trimmed = message.trim()
65
+ if (!trimmed) {
66
+ throw new Error('message 为空')
67
+ }
68
+ if (!trimmed.startsWith('{')) {
69
+ return { method: trimmed, params: {} }
70
+ }
71
+ let parsed: unknown
72
+ try {
73
+ parsed = JSON.parse(trimmed)
74
+ } catch {
75
+ throw new Error('message 不是合法 JSON')
76
+ }
77
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
78
+ throw new Error('message JSON 必须是对象')
79
+ }
80
+ const o = parsed as Record<string, unknown>
81
+ if (typeof o.method !== 'string' || !o.method.trim()) {
82
+ throw new Error('message JSON 须包含非空字符串字段 method')
83
+ }
84
+ if (o.params != null && (typeof o.params !== 'object' || Array.isArray(o.params))) {
85
+ throw new Error('message JSON 的 params 必须是对象')
86
+ }
87
+ return {
88
+ method: o.method.trim(),
89
+ params: (o.params as Record<string, unknown>) || {}
90
+ }
91
+ }
92
+
93
+ /** 任意 method + params,由调用方决定载荷 */
94
+ export type GatewayRpcPayload = {
95
+ method: string
96
+ params?: Record<string, unknown>
97
+ }
98
+
99
+ let persistentConn: GatewayConnection | null = null
100
+ let pingTimer: ReturnType<typeof setInterval> | null = null
101
+ let reconnectTimer: ReturnType<typeof setTimeout> | null = null
102
+ /** register 可能被调用多次,只保留一个「延迟首次连接」定时器,避免同一时刻触发两次 connect */
103
+ let startupConnectTimer: ReturnType<typeof setTimeout> | null = null
104
+ let connectInFlight = false
105
+ let socketStopped = true
106
+ /** 用于忽略「已被替换的旧连接」上的 close/error */
107
+ let socketGeneration = 0
108
+
109
+ function clearPingTimer(): void {
110
+ if (pingTimer) {
111
+ clearInterval(pingTimer)
112
+ pingTimer = null
113
+ }
114
+ }
115
+
116
+ function clearReconnectTimer(): void {
117
+ if (reconnectTimer) {
118
+ clearTimeout(reconnectTimer)
119
+ reconnectTimer = null
120
+ }
121
+ }
122
+
123
+ function clearStartupConnectTimer(): void {
124
+ if (startupConnectTimer) {
125
+ clearTimeout(startupConnectTimer)
126
+ startupConnectTimer = null
127
+ }
128
+ }
129
+
130
+ function startPingTimer(gw: GatewayConnection): void {
131
+ clearPingTimer()
132
+ pingTimer = setInterval(() => {
133
+ if (!gw.isConnected()) return
134
+ try {
135
+ gw.ping()
136
+ } catch (e) {
137
+ dcgLogger(`Gateway ping 失败: ${e}`, 'error')
138
+ }
139
+ }, PING_INTERVAL_MS)
140
+ }
141
+
142
+ function scheduleReconnect(): void {
143
+ if (socketStopped || reconnectTimer) return
144
+ reconnectTimer = setTimeout(() => {
145
+ reconnectTimer = null
146
+ void connectPersistentGateway()
147
+ }, RECONNECT_DELAY_MS)
148
+ }
149
+
150
+ function attachSocketLifecycle(gw: GatewayConnection, generation: number): void {
151
+ const ws = gw.getWebSocket()
152
+ if (!ws) return
153
+
154
+ const onDown = () => {
155
+ if (generation !== socketGeneration) return
156
+ clearPingTimer()
157
+ if (persistentConn === gw) {
158
+ persistentConn = null
159
+ }
160
+ dcgLogger('Gateway WebSocket 已断开,将在 5s 后重连', 'error')
161
+ if (!socketStopped) {
162
+ scheduleReconnect()
163
+ }
164
+ }
165
+
166
+ ws.once('close', onDown)
167
+ ws.once('error', onDown)
168
+ }
169
+
170
+ async function connectPersistentGateway(): Promise<void> {
171
+ if (socketStopped) return
172
+ if (persistentConn?.isConnected()) return
173
+ if (connectInFlight) return
174
+
175
+ const cfg = resolveConfigSafe()
176
+ if (!cfg) return
177
+
178
+ connectInFlight = true
179
+ try {
180
+ socketGeneration += 1
181
+ const generation = socketGeneration
182
+
183
+ if (persistentConn) {
184
+ try {
185
+ persistentConn.close()
186
+ } catch {
187
+ /* ignore */
188
+ }
189
+ persistentConn = null
190
+ }
191
+
192
+ const gw = new GatewayConnection(cfg)
193
+ await gw.connect()
194
+ if (socketStopped) {
195
+ try {
196
+ gw.close()
197
+ } catch {
198
+ /* ignore */
199
+ }
200
+ return
201
+ }
202
+ if (generation !== socketGeneration) {
203
+ try {
204
+ gw.close()
205
+ } catch {
206
+ /* ignore */
207
+ }
208
+ return
209
+ }
210
+ persistentConn = gw
211
+ attachSocketLifecycle(gw, generation)
212
+ startPingTimer(gw)
213
+ dcgLogger(`Gateway 持久连接成功 connId=${gw.getConnId() ?? '?'}`)
214
+ } catch (e) {
215
+ dcgLogger(`Gateway 连接失败11: ${e}`, 'error')
216
+ persistentConn = null
217
+ clearPingTimer()
218
+ if (!socketStopped) {
219
+ scheduleReconnect()
220
+ }
221
+ } finally {
222
+ connectInFlight = false
223
+ }
224
+ }
225
+
226
+ /**
227
+ * 插件 register 后调用:建立到 OpenClaw Gateway 的长连接,30s 一次 WebSocket ping,断线每 5s 重连。
228
+ */
229
+ export function startDcgchatGatewaySocket(): void {
230
+ socketStopped = false
231
+ clearReconnectTimer()
232
+ if (startupConnectTimer != null) return
233
+ startupConnectTimer = setTimeout(() => {
234
+ startupConnectTimer = null
235
+ void connectPersistentGateway()
236
+ }, 10000)
237
+ }
238
+
239
+ /**
240
+ * 停止长连接与重连(例如测试或显式下线)。
241
+ */
242
+ export function stopDcgchatGatewaySocket(): void {
243
+ socketStopped = true
244
+ clearReconnectTimer()
245
+ clearStartupConnectTimer()
246
+ clearPingTimer()
247
+ if (persistentConn) {
248
+ try {
249
+ persistentConn.close()
250
+ } catch {
251
+ /* ignore */
252
+ }
253
+ persistentConn = null
254
+ }
255
+ }
256
+
257
+ export function isDcgchatGatewaySocketConnected(): boolean {
258
+ return persistentConn?.isConnected() ?? false
259
+ }
260
+
261
+ /**
262
+ * 在已连接时调用 Gateway RPC;`params` 完全由调用方传入,不做固定结构。
263
+ */
264
+ export async function callGatewayMethod<T = unknown>(method: string, params: Record<string, unknown> = {}): Promise<T> {
265
+ const gw = persistentConn
266
+ if (!gw?.isConnected()) {
267
+ throw new Error('Gateway 未连接(等待重连或检查配置)')
268
+ }
269
+ return gw.callMethod<T>(method, params)
270
+ }
271
+
272
+ /**
273
+ * 使用对象形式发送 RPC(推荐)。
274
+ */
275
+ export async function sendGatewayRpc<T = unknown>(payload: GatewayRpcPayload): Promise<T> {
276
+ return callGatewayMethod<T>(payload.method, payload.params ?? {})
277
+ }
278
+
279
+ /**
280
+ * 兼容:字符串方法名,或 JSON 字符串 `{ method, params? }`。
281
+ */
282
+ export async function sendMessageToGateway(message: string): Promise<unknown> {
283
+ const { method, params } = parseGatewayRpcMessage(message)
284
+ return callGatewayMethod(method, params)
285
+ }
package/src/monitor.ts CHANGED
@@ -2,12 +2,12 @@ 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'
10
+ import { onDisabledCronJob, onEnabledCronJob, onRemoveCronJob, onRunCronJob } from './cron.js'
11
11
 
12
12
  export type MonitorDcgchatOpts = {
13
13
  config?: ClawdbotConfig
@@ -129,35 +129,21 @@ export async function monitorDcgchatProvider(opts: MonitorDcgchatOpts): Promise<
129
129
  if (!ignoreToolCommand.includes(msg.content.text?.trim())) {
130
130
  setMsgStatus('running')
131
131
  }
132
- // 设置获取用户消息消息参数
133
- setMsgParams({
134
- userId: msg._userId,
135
- token: msg.content.bot_token,
136
- sessionId: msg.content.session_id,
137
- messageId: msg.content.message_id,
138
- domainId: account.domainId || 1000,
139
- appId: account.appId || '100',
140
- botId: msg.content.bot_id,
141
- agentId: msg.content.agent_id
142
- })
143
- msg.content.app_id = account.appId || '100'
144
- msg.content.domain_id = account.domainId || '1000'
145
132
 
146
133
  await handleDcgchatMessage(msg, account.accountId)
147
134
  } else if (parsed.messageType == 'openclaw_bot_event') {
148
- const { event_type, operation_type, skill_url, skill_code, skill_id, bot_token, websocket_trace_id } = parsed.content
149
- ? parsed.content
150
- : ({} as Record<string, any>)
151
- const content = {
152
- event_type,
153
- operation_type,
154
- skill_url,
155
- skill_code,
156
- skill_id,
157
- bot_token,
158
- websocket_trace_id
159
- }
135
+ const { event_type, operation_type } = parsed.content ? parsed.content : ({} as Record<string, any>)
160
136
  if (event_type === 'skill') {
137
+ const { skill_url, skill_code, skill_id, bot_token, websocket_trace_id } = parsed.content
138
+ const content = {
139
+ event_type,
140
+ operation_type,
141
+ skill_url,
142
+ skill_code,
143
+ skill_id,
144
+ bot_token,
145
+ websocket_trace_id
146
+ }
161
147
  if (operation_type === 'install' || operation_type === 'enable' || operation_type === 'update') {
162
148
  installSkill({ path: skill_url, code: skill_code }, content)
163
149
  } else if (operation_type === 'remove' || operation_type === 'disable') {
@@ -165,6 +151,17 @@ export async function monitorDcgchatProvider(opts: MonitorDcgchatOpts): Promise<
165
151
  } else {
166
152
  dcgLogger(`openclaw_bot_event unknown event_type: ${event_type}, ${data.toString()}`)
167
153
  }
154
+ } else if (event_type === 'cron') {
155
+ const { job_id } = parsed.content
156
+ if (operation_type === 'remove') {
157
+ await onRemoveCronJob(job_id)
158
+ } else if (operation_type === 'enable') {
159
+ await onEnabledCronJob(job_id)
160
+ } else if (operation_type === 'disable') {
161
+ await onDisabledCronJob(job_id)
162
+ } else if (operation_type === 'exec') {
163
+ await onRunCronJob(job_id)
164
+ }
168
165
  } else {
169
166
  dcgLogger(`openclaw_bot_event unknown operation_type: ${operation_type}, ${data.toString()}`)
170
167
  }
@@ -3,18 +3,11 @@ import type { IStsToken, IStsTokenReq } from '../types.js'
3
3
  import { getUserTokenCache, setUserTokenCache } from './userInfo.js'
4
4
  import { dcgLogger } from '../utils/log.js'
5
5
 
6
- export const getStsToken = async (name: string, botToken: string) => {
6
+ export const getStsToken = async (name: string, botToken: string, isPrivate: 1 | 0) => {
7
7
  // 确保 userToken 已缓存(如果未缓存会自动获取并缓存)
8
8
  await getUserToken(botToken)
9
9
 
10
- const response = await post<IStsTokenReq, IStsToken>(
11
- '/user/getStsToken',
12
- {
13
- sourceFileName: name,
14
- isPrivate: 1
15
- },
16
- { botToken }
17
- )
10
+ const response = await post<IStsTokenReq, IStsToken>('/user/getStsToken', { sourceFileName: name, isPrivate }, { botToken })
18
11
 
19
12
  if (!response || !response.data || !response.data.bucket) {
20
13
  throw new Error('获取 OSS 临时凭证失败')
@@ -27,11 +20,7 @@ export const generateSignUrl = async (file_url: string, botToken: string) => {
27
20
  // 确保 userToken 已缓存(如果未缓存会自动获取并缓存)
28
21
  await getUserToken(botToken)
29
22
 
30
- const response = await post<any>(
31
- '/user/generateSignUrl',
32
- { loudPlatform: 0, fileName: file_url },
33
- { botToken }
34
- )
23
+ const response = await post<any>('/user/generateSignUrl', { loudPlatform: 0, fileName: file_url }, { botToken })
35
24
  if (response.code === 0 && response.data) {
36
25
  // @ts-ignore
37
26
  return response.data?.filePath
@@ -48,10 +37,7 @@ export const generateSignUrl = async (file_url: string, botToken: string) => {
48
37
  * @returns userToken
49
38
  */
50
39
  export const queryUserTokenByBotToken = async (botToken: string): Promise<string> => {
51
- const response = await post<{ botToken: string }, { token: string }>(
52
- '/organization/queryUserTokenByBotToken',
53
- { botToken }
54
- )
40
+ const response = await post<{ botToken: string }, { token: string }>('/organization/queryUserTokenByBotToken', { botToken })
55
41
 
56
42
  if (!response || !response.data || !response.data.token) {
57
43
  dcgLogger('获取绑定的用户信息失败', 'error')
@@ -22,11 +22,11 @@ async function toUploadContent(
22
22
  return { content: buf, fileName: input.name }
23
23
  }
24
24
 
25
- export const ossUpload = async (file: File | string | Buffer, botToken: string) => {
25
+ export const ossUpload = async (file: File | string | Buffer, botToken: string, isPrivate: 0 | 1 = 1) => {
26
26
  await getUserToken(botToken)
27
27
 
28
28
  const { content, fileName } = await toUploadContent(file)
29
- const data = await getStsToken(fileName, botToken)
29
+ const data = await getStsToken(fileName, botToken, isPrivate)
30
30
 
31
31
  const options: OSS.Options = {
32
32
  // 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。
@@ -50,8 +50,9 @@ export const ossUpload = async (file: File | string | Buffer, botToken: string)
50
50
  if (objectResult?.res?.status !== 200) {
51
51
  dcgLogger(`OSS 上传失败, ${objectResult?.res?.status}`)
52
52
  }
53
- dcgLogger(JSON.stringify(objectResult))
54
- return objectResult.name || objectResult.url
53
+ dcgLogger(`OSS 上传成功, ${objectResult.name || objectResult.url}`)
54
+ // const url = `${data.protocol || 'http'}://${data.bucket}.${data.endPoint}/${data.uploadDir}${data.ossFileKey}`
55
+ return isPrivate === 1 ? objectResult.name || objectResult.url : objectResult.url
55
56
  } catch (error) {
56
57
  dcgLogger(`OSS 上传失败: ${error}`, 'error')
57
58
  }
@@ -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 { getMsgParams } from '../utils/global.js'
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 = getMsgParams() || {}
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 { getMsgParams, getMsgStatus, getWsConnection } from './utils/global.js'
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 params = getMsgParams()
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?.token,
67
+ bot_token: params?.botToken,
65
68
  domain_id: params?.domainId,
66
69
  app_id: params?.appId,
67
70
  bot_id: params?.botId,
@@ -80,12 +83,12 @@ function sendToolCallMessage(text: string, toolCallId: string, isCover: number)
80
83
 
81
84
  export function monitoringToolMessage(api: OpenClawPluginApi) {
82
85
  for (const item of eventList) {
83
- api.on(item.event as PluginHookName, (event: any) => {
86
+ api.on(item.event as PluginHookName, (event: any, args: any) => {
84
87
  const status = getMsgStatus()
85
88
  if (status === 'running') {
86
- dcgLogger(`工具调用结果: ~ event:${item.event}`)
87
89
  if (['after_tool_call', 'before_tool_call'].includes(item.event)) {
88
90
  const { result: _result, ...rest } = event
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',
@@ -95,22 +98,16 @@ export function monitoringToolMessage(api: OpenClawPluginApi) {
95
98
  })
96
99
  sendToolCallMessage(text, event.toolCallId || event.runId || Date.now().toString(), item.event === 'after_tool_call' ? 1 : 0)
97
100
  } else if (item.event) {
101
+ dcgLogger(`工具调用结果: ~ event:${item.event}`)
98
102
  if (item.event === 'llm_output') {
103
+ dcgLogger(`llm_output工具调用结果: ~ event:${JSON.stringify(event)}, args: ${JSON.stringify(args)}`)
99
104
  if (event.lastAssistant?.errorMessage === '429-账户额度耗尽') {
100
- const params = getMsgParams()
101
105
  const message = '您的积分已消耗完,您可以通过充值积分来继续使用'
102
- const ctx = {
103
- userId: params.userId,
104
- botToken: params.token,
105
- domainId: params.domainId,
106
- appId: params.appId,
107
- botId: params.botId,
108
- agentId: params.agentId,
109
- sessionId: params.sessionId,
110
- messageId: params.messageId
111
- }
112
- sendText(ctx, message, { message_tags: { insufficient_balance: 1 }, is_finish: -1 })
113
- 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)
114
111
  return
115
112
  }
116
113
  }