@dcrays/dcgchat 0.2.25 → 0.2.34

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/src/monitor.ts CHANGED
@@ -1,176 +1,153 @@
1
- import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
2
- import WebSocket from "ws";
3
- import { abortMobookappGeneration, handleDcgchatMessage } from "./bot.js";
4
- import { resolveAccount } from "./channel.js";
5
- import { setWsConnection } from "./connection.js";
6
- import type { InboundMessage } from "./types.js";
7
- import { setMsgParams, setMsgStatus } from "./tool.js";
8
- import { installSkill, uninstallSkill } from "./skill.js";
1
+ import type { ClawdbotConfig, RuntimeEnv } from 'openclaw/plugin-sdk'
2
+ import WebSocket from 'ws'
3
+ import { handleDcgchatMessage } from './bot.js'
4
+ import { resolveAccount } from './channel.js'
5
+ import { setWsConnection, getOpenClawConfig } from './utils/global.js'
6
+ import type { InboundMessage } from './types.js'
7
+ import { setMsgParams, setMsgStatus } from './utils/global.js'
8
+ import { installSkill, uninstallSkill } from './skill.js'
9
+ import { dcgLogger } from './utils/log.js'
10
+ import { ignoreToolCommand } from './utils/constant.js'
9
11
 
10
12
  export type MonitorDcgchatOpts = {
11
- config?: ClawdbotConfig;
12
- runtime?: RuntimeEnv;
13
- abortSignal?: AbortSignal;
14
- accountId?: string;
15
- };
16
-
17
- const RECONNECT_DELAY_MS = 3000;
18
- const HEARTBEAT_INTERVAL_MS = 30_000;
19
- const emptyToolText = [
20
- "/new",
21
- "/search",
22
- "/stop",
23
- "/abort",
24
- "/queue interrupt",
25
- ];
13
+ config?: ClawdbotConfig
14
+ runtime?: RuntimeEnv
15
+ abortSignal?: AbortSignal
16
+ accountId?: string
17
+ }
18
+
19
+ const RECONNECT_DELAY_MS = 3000
20
+ const HEARTBEAT_INTERVAL_MS = 30_000
26
21
 
27
22
  function buildConnectUrl(account: Record<string, string>): string {
28
- const { wsUrl, botToken, userId, domainId, appId } = account;
29
- const url = new URL(wsUrl);
30
- if (botToken) url.searchParams.set("bot_token", botToken);
31
- if (userId) url.searchParams.set("_userId", userId);
32
- url.searchParams.set("_domainId", domainId || "1000");
33
- url.searchParams.set("_appId", appId || "100");
34
- return url.toString();
23
+ const { wsUrl, botToken, userId, domainId, appId } = account
24
+ const url = new URL(wsUrl)
25
+ if (botToken) url.searchParams.set('bot_token', botToken)
26
+ if (userId) url.searchParams.set('_userId', userId)
27
+ url.searchParams.set('_domainId', domainId || '1000')
28
+ url.searchParams.set('_appId', appId || '100')
29
+ return url.toString()
35
30
  }
36
31
 
37
- export async function monitorDcgchatProvider(
38
- opts: MonitorDcgchatOpts,
39
- ): Promise<void> {
40
- const { config, runtime, abortSignal, accountId } = opts;
41
- // @ts-ignore
42
- const cfg = config ?? (runtime?.config?.() as ClawdbotConfig | undefined);
43
- if (!cfg) {
44
- runtime?.error?.("dcgchat: no config available");
45
- return;
32
+ export async function monitorDcgchatProvider(opts: MonitorDcgchatOpts): Promise<void> {
33
+ const { abortSignal, accountId } = opts
34
+
35
+ const config = getOpenClawConfig()
36
+ if (!config) {
37
+ dcgLogger('no config available', 'error')
38
+ return
46
39
  }
47
40
 
48
- const account = resolveAccount(cfg, accountId ?? "default");
49
- const log = runtime?.log ?? console.log;
50
- const err = runtime?.error ?? console.error;
41
+ const account = resolveAccount(config, accountId ?? 'default')
51
42
 
52
43
  if (!account.wsUrl) {
53
- err(`dcgchat[${account.accountId}]: wsUrl not configured`);
54
- return;
44
+ dcgLogger(` wsUrl not configured`, 'error')
45
+ return
55
46
  }
56
47
 
57
- let shouldReconnect = true;
58
- let ws: WebSocket | null = null;
59
- let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
48
+ let shouldReconnect = true
49
+ let ws: WebSocket | null = null
50
+ let heartbeatTimer: ReturnType<typeof setInterval> | null = null
51
+ let heartbeatLogCounter = 0
52
+
53
+ const isOpenclawBotHeartbeat = (raw: WebSocket.RawData, parsedMsg: { messageType?: string }): boolean => {
54
+ if (parsedMsg?.messageType === 'openclaw_bot_heartbeat') return true
55
+ if (typeof raw === 'object' && raw !== null && !Buffer.isBuffer(raw)) {
56
+ return (raw as { messageType?: string }).messageType === 'openclaw_bot_heartbeat'
57
+ }
58
+ return false
59
+ }
60
60
 
61
61
  const stopHeartbeat = () => {
62
62
  if (heartbeatTimer) {
63
- clearInterval(heartbeatTimer);
64
- heartbeatTimer = null;
63
+ clearInterval(heartbeatTimer)
64
+ heartbeatTimer = null
65
65
  }
66
- };
66
+ }
67
67
 
68
68
  const startHeartbeat = () => {
69
- stopHeartbeat();
69
+ stopHeartbeat()
70
70
  heartbeatTimer = setInterval(() => {
71
71
  if (ws?.readyState === WebSocket.OPEN) {
72
72
  const heartbeat = {
73
- messageType: "openclaw_bot_heartbeat",
73
+ messageType: 'openclaw_bot_heartbeat',
74
74
  _userId: Number(account.userId) || 0,
75
- source: "client",
75
+ source: 'client',
76
76
  content: {
77
77
  bot_token: account.botToken,
78
- status: "1",
79
- },
80
- };
81
- ws.send(JSON.stringify(heartbeat));
82
- // log(`dcgchat[${account.accountId}]: heartbeat sent, ${JSON.stringify(heartbeat)}`);
78
+ status: '1'
79
+ }
80
+ }
81
+ ws.send(JSON.stringify(heartbeat))
83
82
  }
84
- }, HEARTBEAT_INTERVAL_MS);
85
- };
83
+ }, HEARTBEAT_INTERVAL_MS)
84
+ }
86
85
 
87
86
  const connect = () => {
88
- if (!shouldReconnect) return;
87
+ if (!shouldReconnect) return
89
88
 
90
- const connectUrl = buildConnectUrl(account as Record<string, any>);
91
- log(`dcgchat[${account.accountId}]: connecting to ${connectUrl}`);
92
- ws = new WebSocket(connectUrl);
89
+ const connectUrl = buildConnectUrl(account as Record<string, any>)
90
+ dcgLogger(`connecting to ${connectUrl}`)
91
+ ws = new WebSocket(connectUrl)
93
92
 
94
- ws.on("open", () => {
95
- log(`dcgchat[${account.accountId}]: connected`);
96
- setWsConnection(ws);
97
- startHeartbeat();
98
- });
93
+ ws.on('open', () => {
94
+ dcgLogger(`socket connected`)
95
+ setWsConnection(ws)
96
+ startHeartbeat()
97
+ })
99
98
 
100
- ws.on("message", async (data) => {
101
- log(`dcgchat[${account.accountId}]: on message, ${data.toString()}`);
102
- let parsed: { messageType?: string; content: any };
99
+ ws.on('message', async (data) => {
100
+ let parsed: { messageType?: string; content: any }
103
101
  try {
104
- parsed = JSON.parse(data.toString());
102
+ parsed = JSON.parse(data.toString())
105
103
  } catch {
106
- err(`dcgchat[${account.accountId}]: invalid JSON received`);
107
- return;
104
+ dcgLogger(`用户消息解析失败: invalid JSON received`, 'error')
105
+ return
108
106
  }
109
107
 
110
- if (parsed.messageType === "openclaw_bot_heartbeat") {
111
- log(
112
- `dcgchat[${account.accountId}]: heartbeat ack received, ${data.toString()}`,
113
- );
114
- return;
108
+ const payloadStr = data.toString()
109
+ const heartbeat = isOpenclawBotHeartbeat(data, parsed)
110
+ if (heartbeat) {
111
+ heartbeatLogCounter += 1
112
+ if (heartbeatLogCounter % 10 === 0) {
113
+ dcgLogger(`${parsed?.messageType}, ${payloadStr}`)
114
+ dcgLogger(`heartbeat ack received, ${payloadStr}`)
115
+ }
116
+ return
115
117
  }
118
+
119
+ dcgLogger(`${parsed?.messageType}, ${payloadStr}`)
116
120
  try {
117
- parsed.content = JSON.parse(parsed.content);
121
+ parsed.content = JSON.parse(parsed.content)
118
122
  } catch {
119
- err(`dcgchat[${account.accountId}]: invalid JSON received`);
120
- return;
123
+ dcgLogger(`用户消息content字段解析失败: invalid JSON received`, 'error')
124
+ return
121
125
  }
122
126
 
123
- if (parsed.messageType == "openclaw_bot_chat") {
124
- const msg = parsed as unknown as InboundMessage;
125
- if (!emptyToolText.includes(msg.content.text?.trim())) {
126
- setMsgStatus("running");
127
- }
128
- log(
129
- `dcgchat[${account.accountId}]: openclaw_bot_chat received, ${JSON.stringify(msg)}`,
130
- );
131
- if (msg.content.text === "/stop") {
132
- const rawConvId = msg.content.session_id as string | undefined;
133
- const conversationId =
134
- rawConvId || `${accountId}:${account.botToken}`;
135
- console.log("🚀 ~ connect ~ conversationId:", conversationId)
136
- abortMobookappGeneration(conversationId);
137
- log(`[dcgchat][in] abort conversationId=${conversationId}`);
138
- return;
127
+ if (parsed.messageType == 'openclaw_bot_chat') {
128
+ const msg = parsed as unknown as InboundMessage
129
+ if (!ignoreToolCommand.includes(msg.content.text?.trim())) {
130
+ setMsgStatus('running')
139
131
  }
132
+ // 设置获取用户消息消息参数
140
133
  setMsgParams({
141
134
  userId: msg._userId,
142
135
  token: msg.content.bot_token,
143
136
  sessionId: msg.content.session_id,
144
137
  messageId: msg.content.message_id,
145
138
  domainId: account.domainId || 1000,
146
- appId: account.appId || "100",
139
+ appId: account.appId || '100',
147
140
  botId: msg.content.bot_id,
148
- agentId: msg.content.agent_id,
149
- });
150
- msg.content.app_id = account.appId || "100";
151
- msg.content.domain_id = account.domainId || "1000";
152
- await handleDcgchatMessage({
153
- cfg,
154
- msg,
155
- accountId: account.accountId,
156
- runtime,
157
- onChunk: (reply) => {
158
- if (ws?.readyState === WebSocket.OPEN) {
159
- const res = { ...reply, content: JSON.stringify(reply.content) };
160
- ws.send(JSON.stringify(res));
161
- }
162
- },
163
- });
164
- } else if (parsed.messageType == "openclaw_bot_event") {
165
- const {
166
- event_type,
167
- operation_type,
168
- skill_url,
169
- skill_code,
170
- skill_id,
171
- bot_token,
172
- websocket_trace_id,
173
- } = parsed.content ? parsed.content : ({} as Record<string, any>);
141
+ agentId: msg.content.agent_id
142
+ })
143
+ msg.content.app_id = account.appId || '100'
144
+ msg.content.domain_id = account.domainId || '1000'
145
+
146
+ await handleDcgchatMessage(msg, account.accountId)
147
+ } 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>)
174
151
  const content = {
175
152
  event_type,
176
153
  operation_type,
@@ -178,75 +155,58 @@ export async function monitorDcgchatProvider(
178
155
  skill_code,
179
156
  skill_id,
180
157
  bot_token,
181
- websocket_trace_id,
182
- };
183
- if (event_type === "skill") {
184
- if (
185
- operation_type === "install" ||
186
- operation_type === "enable" ||
187
- operation_type === "update"
188
- ) {
189
- installSkill({ path: skill_url, code: skill_code }, content);
190
- } else if (
191
- operation_type === "remove" ||
192
- operation_type === "disable"
193
- ) {
194
- uninstallSkill({ code: skill_code }, content);
158
+ websocket_trace_id
159
+ }
160
+ if (event_type === 'skill') {
161
+ if (operation_type === 'install' || operation_type === 'enable' || operation_type === 'update') {
162
+ installSkill({ path: skill_url, code: skill_code }, content)
163
+ } else if (operation_type === 'remove' || operation_type === 'disable') {
164
+ uninstallSkill({ code: skill_code }, content)
195
165
  } else {
196
- log(
197
- `dcgchat[${account.accountId}]: openclaw_bot_event unknown event_type: ${event_type}, ${data.toString()}`,
198
- );
166
+ dcgLogger(`openclaw_bot_event unknown event_type: ${event_type}, ${data.toString()}`)
199
167
  }
200
168
  } else {
201
- log(
202
- `dcgchat[${account.accountId}]: openclaw_bot_event unknown operation_type: ${operation_type}, ${data.toString()}`,
203
- );
169
+ dcgLogger(`openclaw_bot_event unknown operation_type: ${operation_type}, ${data.toString()}`)
204
170
  }
205
171
  } else {
206
- log(
207
- `dcgchat[${account.accountId}]: ignoring unknown messageType: ${parsed.messageType}`,
208
- );
172
+ dcgLogger(`ignoring unknown messageType: ${parsed.messageType}`, 'error')
209
173
  }
210
- });
211
-
212
- ws.on("close", (code, reason) => {
213
- stopHeartbeat();
214
- setWsConnection(null);
215
- log(
216
- `dcgchat[${account.accountId}]: disconnected (code=${code}, reason=${reason?.toString() || ""})`,
217
- );
174
+ })
175
+
176
+ ws.on('close', (code, reason) => {
177
+ stopHeartbeat()
178
+ setWsConnection(null)
179
+ dcgLogger(`disconnected (code=${code}, reason=${reason?.toString() || ''})`, 'error')
218
180
  if (shouldReconnect) {
219
- log(
220
- `dcgchat[${account.accountId}]: reconnecting in ${RECONNECT_DELAY_MS}ms...`,
221
- );
222
- setTimeout(connect, RECONNECT_DELAY_MS);
181
+ dcgLogger(`reconnecting in ${RECONNECT_DELAY_MS}ms...`)
182
+ setTimeout(connect, RECONNECT_DELAY_MS)
223
183
  }
224
- });
184
+ })
225
185
 
226
- ws.on("error", (e) => {
227
- err(`dcgchat[${account.accountId}]: WebSocket error: ${String(e)}`);
228
- });
229
- };
186
+ ws.on('error', (e) => {
187
+ dcgLogger(`WebSocket error: ${String(e)}`, 'error')
188
+ })
189
+ }
230
190
 
231
- connect();
191
+ connect()
232
192
 
233
193
  await new Promise<void>((resolve) => {
234
194
  if (abortSignal?.aborted) {
235
- shouldReconnect = false;
236
- ws?.close();
237
- resolve();
238
- return;
195
+ shouldReconnect = false
196
+ ws?.close()
197
+ resolve()
198
+ return
239
199
  }
240
200
  abortSignal?.addEventListener(
241
- "abort",
201
+ 'abort',
242
202
  () => {
243
- log(`dcgchat[${account.accountId}]: stopping`);
244
- stopHeartbeat();
245
- shouldReconnect = false;
246
- ws?.close();
247
- resolve();
203
+ dcgLogger(`socket stopping`)
204
+ stopHeartbeat()
205
+ shouldReconnect = false
206
+ ws?.close()
207
+ resolve()
248
208
  },
249
- { once: true },
250
- );
251
- });
209
+ { once: true }
210
+ )
211
+ })
252
212
  }
@@ -1,49 +1,46 @@
1
- import { post } from "./request.js";
2
- import type { IStsToken, IStsTokenReq } from "./types.js";
3
- import { getUserTokenCache, setUserTokenCache } from "./userInfo.js";
1
+ import { post } from './request.js'
2
+ import type { IStsToken, IStsTokenReq } from '../types.js'
3
+ import { getUserTokenCache, setUserTokenCache } from './userInfo.js'
4
+ import { dcgLogger } from '../utils/log.js'
4
5
 
5
6
  export const getStsToken = async (name: string, botToken: string) => {
6
7
  // 确保 userToken 已缓存(如果未缓存会自动获取并缓存)
7
- await getUserToken(botToken);
8
+ await getUserToken(botToken)
8
9
 
9
10
  const response = await post<IStsTokenReq, IStsToken>(
10
- "/user/getStsToken",
11
+ '/user/getStsToken',
11
12
  {
12
13
  sourceFileName: name,
13
- isPrivate: 1,
14
+ isPrivate: 1
14
15
  },
15
- { botToken },
16
- );
16
+ { botToken }
17
+ )
17
18
 
18
19
  if (!response || !response.data || !response.data.bucket) {
19
- throw new Error("获取 OSS 临时凭证失败");
20
+ throw new Error('获取 OSS 临时凭证失败')
20
21
  }
21
22
 
22
- return response.data;
23
- };
23
+ return response.data
24
+ }
24
25
  export const generateSignUrl = async (file_url: string, botToken: string) => {
25
26
  try {
26
27
  // 确保 userToken 已缓存(如果未缓存会自动获取并缓存)
27
- await getUserToken(botToken);
28
-
28
+ await getUserToken(botToken)
29
+
29
30
  const response = await post<any>(
30
- "/user/generateSignUrl",
31
- {
32
- loudPlatform: 0,
33
- fileName: file_url
34
- },
35
- { botToken },
36
- );
31
+ '/user/generateSignUrl',
32
+ { loudPlatform: 0, fileName: file_url },
33
+ { botToken }
34
+ )
37
35
  if (response.code === 0 && response.data) {
38
36
  // @ts-ignore
39
37
  return response.data?.filePath
40
38
  }
41
39
  return ''
42
-
43
40
  } catch (error) {
44
41
  return ''
45
42
  }
46
- };
43
+ }
47
44
 
48
45
  /**
49
46
  * 通过 botToken 查询 userToken
@@ -51,16 +48,18 @@ export const generateSignUrl = async (file_url: string, botToken: string) => {
51
48
  * @returns userToken
52
49
  */
53
50
  export const queryUserTokenByBotToken = async (botToken: string): Promise<string> => {
54
- const response = await post<{botToken: string}, {token: string}>("/organization/queryUserTokenByBotToken", {
55
- botToken,
56
- });
51
+ const response = await post<{ botToken: string }, { token: string }>(
52
+ '/organization/queryUserTokenByBotToken',
53
+ { botToken }
54
+ )
57
55
 
58
56
  if (!response || !response.data || !response.data.token) {
59
- throw new Error("获取绑定的用户信息失败");
57
+ dcgLogger('获取绑定的用户信息失败', 'error')
58
+ return ''
60
59
  }
61
60
 
62
- return response.data.token;
63
- };
61
+ return response.data.token
62
+ }
64
63
 
65
64
  /**
66
65
  * 获取 userToken(优先从缓存获取,缓存未命中则调用 API)
@@ -69,17 +68,17 @@ export const queryUserTokenByBotToken = async (botToken: string): Promise<string
69
68
  */
70
69
  export const getUserToken = async (botToken: string): Promise<string> => {
71
70
  // 1. 尝试从缓存获取
72
- const cachedToken = getUserTokenCache(botToken);
71
+ const cachedToken = getUserTokenCache(botToken)
73
72
  if (cachedToken) {
74
- return cachedToken;
73
+ return cachedToken
75
74
  }
76
75
 
77
76
  // 2. 缓存未命中,调用 API 获取
78
- console.log(`[api] cache miss, fetching userToken for botToken=${botToken.slice(0, 10)}...`);
79
- const userToken = await queryUserTokenByBotToken(botToken);
77
+ dcgLogger(`[api] cache miss, fetching userToken for botToken=${botToken.slice(0, 10)}...`)
78
+ const userToken = await queryUserTokenByBotToken(botToken)
80
79
 
81
80
  // 3. 缓存新获取的 token
82
- setUserTokenCache(botToken, userToken);
81
+ setUserTokenCache(botToken, userToken)
83
82
 
84
- return userToken;
85
- };
83
+ return userToken
84
+ }
@@ -0,0 +1,58 @@
1
+ import { createReadStream } from 'node:fs'
2
+ // @ts-ignore
3
+ import OSS from 'ali-oss'
4
+ import { getStsToken, getUserToken } from './api.js'
5
+ import { dcgLogger } from '../utils/log.js'
6
+
7
+ /** 将 File/路径/Buffer 转为 ali-oss put 所需的 Buffer 或 ReadableStream */
8
+ async function toUploadContent(
9
+ input: File | string | Buffer
10
+ ): Promise<{ content: Buffer | ReturnType<typeof createReadStream>; fileName: string }> {
11
+ if (Buffer.isBuffer(input)) {
12
+ return { content: input, fileName: 'file' }
13
+ }
14
+ if (typeof input === 'string') {
15
+ return {
16
+ content: createReadStream(input),
17
+ fileName: input.split('/').pop() ?? 'file'
18
+ }
19
+ }
20
+ // File: ali-oss 需要 Buffer/Stream,用 arrayBuffer 转 Buffer
21
+ const buf = Buffer.from(await input.arrayBuffer())
22
+ return { content: buf, fileName: input.name }
23
+ }
24
+
25
+ export const ossUpload = async (file: File | string | Buffer, botToken: string) => {
26
+ await getUserToken(botToken)
27
+
28
+ const { content, fileName } = await toUploadContent(file)
29
+ const data = await getStsToken(fileName, botToken)
30
+
31
+ const options: OSS.Options = {
32
+ // 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。
33
+ accessKeyId: data.tempAccessKeyId,
34
+ accessKeySecret: data.tempAccessKeySecret,
35
+ // 从STS服务获取的安全令牌(SecurityToken)。
36
+ stsToken: data.tempSecurityToken,
37
+ // 填写Bucket名称。
38
+ bucket: data.bucket,
39
+ endpoint: data.endPoint,
40
+ region: data.region,
41
+ secure: true
42
+ }
43
+
44
+ const client = new OSS(options)
45
+
46
+ const name = `${data.uploadDir}${data.ossFileKey}`
47
+
48
+ try {
49
+ const objectResult = await client.put(name, content)
50
+ if (objectResult?.res?.status !== 200) {
51
+ dcgLogger(`OSS 上传失败, ${objectResult?.res?.status}`)
52
+ }
53
+ dcgLogger(JSON.stringify(objectResult))
54
+ return objectResult.name || objectResult.url
55
+ } catch (error) {
56
+ dcgLogger(`OSS 上传失败: ${error}`, 'error')
57
+ }
58
+ }