@dcrays/dcgchat-test 0.3.20 → 0.3.23
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 +1 -1
- package/src/bot.ts +3 -23
- package/src/cron.ts +42 -12
- package/src/gateway/index.ts +3 -1
- package/src/request/api.ts +1 -1
- package/src/request/oss.ts +3 -1
- package/src/utils/searchFile.ts +90 -4
package/package.json
CHANGED
package/src/bot.ts
CHANGED
|
@@ -1,20 +1,11 @@
|
|
|
1
|
-
import fs from 'node:fs'
|
|
2
1
|
import path from 'node:path'
|
|
3
2
|
import type { ReplyPayload } from 'openclaw/plugin-sdk'
|
|
4
3
|
import { createReplyPrefixContext } from 'openclaw/plugin-sdk'
|
|
5
4
|
import type { InboundMessage } from './types.js'
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
clearSentMediaKeys,
|
|
9
|
-
getDcgchatRuntime,
|
|
10
|
-
getOpenClawConfig,
|
|
11
|
-
getSessionKey,
|
|
12
|
-
getWorkspaceDir,
|
|
13
|
-
setMsgStatus
|
|
14
|
-
} from './utils/global.js'
|
|
5
|
+
import { clearSentMediaKeys, getDcgchatRuntime, getOpenClawConfig, getSessionKey, setMsgStatus } from './utils/global.js'
|
|
15
6
|
import { resolveAccount, sendDcgchatMedia } from './channel.js'
|
|
16
7
|
import { generateSignUrl } from './request/api.js'
|
|
17
|
-
import { extractMobookFiles } from './utils/searchFile.js'
|
|
8
|
+
import { extractMobookFiles, getFilePathByFile } from './utils/searchFile.js'
|
|
18
9
|
import { sendChunk, sendFinal, sendText as sendTextMsg, sendError, wsSendRaw, sendText } from './transport.js'
|
|
19
10
|
import { dcgLogger } from './utils/log.js'
|
|
20
11
|
import { channelInfo, systemCommand, interruptCommand, ENV, ignoreToolCommand } from './utils/constant.js'
|
|
@@ -412,18 +403,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
412
403
|
sessionStreamSuppressed.delete(effectiveSessionKey)
|
|
413
404
|
} else {
|
|
414
405
|
for (const file of extractMobookFiles(completeText)) {
|
|
415
|
-
const
|
|
416
|
-
candidates.push(path.join(getWorkspaceDir(), file))
|
|
417
|
-
candidates.push(path.join(getWorkspaceDir(), file.replace(/^\//, '')))
|
|
418
|
-
const underMobook = file.replace(/^\/mobook\//i, '').replace(/^mobook[\\/]/i, '')
|
|
419
|
-
if (underMobook) {
|
|
420
|
-
if (process.platform === 'win32') {
|
|
421
|
-
candidates.push(path.join('C:\\', 'mobook', underMobook))
|
|
422
|
-
} else if (process.platform === 'darwin') {
|
|
423
|
-
candidates.push(path.join(os.homedir(), 'mobook', underMobook))
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
const resolved = candidates.find((p) => fs.existsSync(p))
|
|
406
|
+
const resolved = getFilePathByFile(file)
|
|
427
407
|
if (!resolved) continue
|
|
428
408
|
try {
|
|
429
409
|
await sendDcgchatMedia({ sessionKey: effectiveSessionKey, mediaUrl: resolved, text: '' })
|
package/src/cron.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
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 {
|
|
4
|
+
import { isWsOpen, mergeDefaultParams, sendEventMessage, sendFinal } from './transport.js'
|
|
5
|
+
import {
|
|
6
|
+
getCronMessageId,
|
|
7
|
+
getWorkspaceDir,
|
|
8
|
+
getWsConnection,
|
|
9
|
+
removeCronMessageId,
|
|
10
|
+
setCronMessageId,
|
|
11
|
+
setMsgStatus
|
|
12
|
+
} from './utils/global.js'
|
|
6
13
|
import { ossUpload } from './request/oss.js'
|
|
7
14
|
import { dcgLogger } from './utils/log.js'
|
|
8
15
|
import { sendMessageToGateway } from './gateway/socket.js'
|
|
@@ -16,13 +23,13 @@ export function getCronJobsPath(): string {
|
|
|
16
23
|
|
|
17
24
|
type CronJobsFile = {
|
|
18
25
|
version?: number
|
|
19
|
-
jobs?: Array<{ id?: string; sessionKey?: string }>
|
|
26
|
+
jobs?: Array<{ id?: string; sessionKey?: string; name?: string }>
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
/**
|
|
23
30
|
* 在 `jobPath` 指向的 jobs.json(通常为 getCronJobsPath())中按 id 查找任务并返回其 sessionKey。
|
|
24
31
|
*/
|
|
25
|
-
export function
|
|
32
|
+
export function readCronJob(jobPath: string, jobId: string): Record<string, any> | null {
|
|
26
33
|
const id = jobId?.trim()
|
|
27
34
|
if (!id) return null
|
|
28
35
|
if (!fs.existsSync(jobPath)) {
|
|
@@ -33,8 +40,7 @@ export function readCronJobSessionKey(jobPath: string, jobId: string): string |
|
|
|
33
40
|
const raw = fs.readFileSync(jobPath, 'utf8')
|
|
34
41
|
const data = JSON.parse(raw) as CronJobsFile
|
|
35
42
|
const job = (data.jobs ?? []).find((j) => j.id === id)
|
|
36
|
-
|
|
37
|
-
return sk || null
|
|
43
|
+
return job || null
|
|
38
44
|
} catch (e) {
|
|
39
45
|
dcgLogger(`readCronJobSessionKey: failed to read ${jobPath}: ${String(e)}`, 'error')
|
|
40
46
|
return null
|
|
@@ -131,13 +137,19 @@ export const onRunCronJob = async (jobId: string, messageId: string) => {
|
|
|
131
137
|
return
|
|
132
138
|
}
|
|
133
139
|
const jobPath = getCronJobsPath()
|
|
134
|
-
const sessionKey =
|
|
140
|
+
const { sessionKey } = readCronJob(jobPath, jobId) || {}
|
|
135
141
|
if (!sessionKey) {
|
|
136
142
|
dcgLogger(`finishedDcgchatCron: no sessionKey for job id=${id}`, 'error')
|
|
137
143
|
return
|
|
138
144
|
}
|
|
139
145
|
setCronMessageId(sessionKey, messageId)
|
|
140
|
-
sendMessageToGateway(
|
|
146
|
+
sendMessageToGateway(
|
|
147
|
+
JSON.stringify({
|
|
148
|
+
method: 'cron.runs',
|
|
149
|
+
params: { scope: 'job', id: jobId, limit: 50, offset: 0, status: 'all', sortDir: 'desc' }
|
|
150
|
+
})
|
|
151
|
+
)
|
|
152
|
+
sendMessageToGateway(JSON.stringify({ method: 'cron.run', params: { id: jobId, mode: 'force' } }))
|
|
141
153
|
}
|
|
142
154
|
export const finishedDcgchatCron = async (jobId: string) => {
|
|
143
155
|
const id = jobId?.trim()
|
|
@@ -146,20 +158,20 @@ export const finishedDcgchatCron = async (jobId: string) => {
|
|
|
146
158
|
return
|
|
147
159
|
}
|
|
148
160
|
const jobPath = getCronJobsPath()
|
|
149
|
-
const sessionKey =
|
|
161
|
+
const { sessionKey, name } = readCronJob(jobPath, id) || {}
|
|
150
162
|
if (!sessionKey) {
|
|
151
163
|
dcgLogger(`finishedDcgchatCron: no sessionKey for job id=${id}`, 'error')
|
|
152
164
|
return
|
|
153
165
|
}
|
|
154
166
|
const outboundCtx = getEffectiveMsgParams(sessionKey)
|
|
155
167
|
const messageId = getCronMessageId(sessionKey)
|
|
168
|
+
const sessionInfo = sessionKey.split(':')
|
|
169
|
+
const sessionId = sessionInfo.at(-1) ?? ''
|
|
170
|
+
const agentId = sessionInfo.at(-2) ?? ''
|
|
156
171
|
if (outboundCtx?.sessionId) {
|
|
157
172
|
const newCtx = messageId ? { ...outboundCtx, messageId } : outboundCtx
|
|
158
173
|
sendFinal(newCtx, 'cron send')
|
|
159
174
|
} else {
|
|
160
|
-
const sessionInfo = sessionKey.split(':')
|
|
161
|
-
const sessionId = sessionInfo.at(-1) ?? ''
|
|
162
|
-
const agentId = sessionInfo.at(-2) ?? ''
|
|
163
175
|
const merged = mergeDefaultParams({
|
|
164
176
|
agentId: agentId,
|
|
165
177
|
sessionId: `${sessionId}`,
|
|
@@ -169,6 +181,24 @@ export const finishedDcgchatCron = async (jobId: string) => {
|
|
|
169
181
|
})
|
|
170
182
|
sendFinal(merged, 'cron send')
|
|
171
183
|
}
|
|
184
|
+
const ws = getWsConnection()
|
|
185
|
+
if (isWsOpen()) {
|
|
186
|
+
ws?.send(
|
|
187
|
+
JSON.stringify({
|
|
188
|
+
messageType: 'openclaw_bot_event',
|
|
189
|
+
source: 'client',
|
|
190
|
+
content: {
|
|
191
|
+
event_type: 'notify',
|
|
192
|
+
operation_type: 'cron',
|
|
193
|
+
session_id: sessionId,
|
|
194
|
+
agentId: agentId,
|
|
195
|
+
real_mobook: !sessionId ? 1 : '',
|
|
196
|
+
title: name
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
)
|
|
200
|
+
dcgLogger(`定时任务执行成功: ${id}`)
|
|
201
|
+
}
|
|
172
202
|
removeCronMessageId(sessionKey)
|
|
173
203
|
dcgLogger(`finishedDcgchatCron: job=${id} sessionKey=${sessionKey}`)
|
|
174
204
|
}
|
package/src/gateway/index.ts
CHANGED
|
@@ -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
|
|
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
|
package/src/request/api.ts
CHANGED
|
@@ -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/
|
|
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 临时凭证失败')
|
package/src/request/oss.ts
CHANGED
|
@@ -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)
|
package/src/utils/searchFile.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import { getWorkspaceDir } from './global.js'
|
|
1
2
|
import { dcgLogger } from './log.js'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import path from 'node:path'
|
|
2
6
|
|
|
3
7
|
/**
|
|
4
8
|
* 从文本中提取 /mobook 目录下的文件
|
|
@@ -155,7 +159,7 @@ export function extractMobookFiles(text = '') {
|
|
|
155
159
|
;(text.match(backtickReg) || []).forEach((item) => {
|
|
156
160
|
const name = item.replace(/`/g, '').trim()
|
|
157
161
|
if (isValidFileName(name)) {
|
|
158
|
-
result.add(
|
|
162
|
+
result.add(`${name}`)
|
|
159
163
|
}
|
|
160
164
|
})
|
|
161
165
|
// 2️⃣ /mobook/xxx.xxx
|
|
@@ -168,7 +172,7 @@ export function extractMobookFiles(text = '') {
|
|
|
168
172
|
;(text.match(winMobookReg) || []).forEach((full) => {
|
|
169
173
|
const name = full.replace(/^(?:[a-zA-Z]:)?[/\\\\]mobook[/\\\\]/i, '').trim()
|
|
170
174
|
if (isValidFileName(name)) {
|
|
171
|
-
result.add(normalizePath(
|
|
175
|
+
result.add(normalizePath(`${name}`))
|
|
172
176
|
}
|
|
173
177
|
})
|
|
174
178
|
// 3️⃣ mobook下的 xxx.xxx
|
|
@@ -184,7 +188,7 @@ export function extractMobookFiles(text = '') {
|
|
|
184
188
|
;(text.match(boldReg) || []).forEach((item) => {
|
|
185
189
|
const name = item.replace(/\*\*/g, '').trim()
|
|
186
190
|
if (isValidFileName(name)) {
|
|
187
|
-
result.add(
|
|
191
|
+
result.add(`${name}`)
|
|
188
192
|
}
|
|
189
193
|
})
|
|
190
194
|
// 🆕 5️⃣ xxx.xxx (123字节)
|
|
@@ -192,7 +196,7 @@ export function extractMobookFiles(text = '') {
|
|
|
192
196
|
;(text.match(looseReg) || []).forEach((item) => {
|
|
193
197
|
const name = item.replace(/\s*\(.+$/, '').trim()
|
|
194
198
|
if (isValidFileName(name)) {
|
|
195
|
-
result.add(
|
|
199
|
+
result.add(`${name}`)
|
|
196
200
|
}
|
|
197
201
|
})
|
|
198
202
|
// 6️⃣ 兜底:绝对路径等 `.../mobook/<文件名>.<扩展名>` + 最长后缀匹配 + 去脏字符
|
|
@@ -226,3 +230,85 @@ function normalizePath(path: string) {
|
|
|
226
230
|
.replace(/\/+/g, '/') // 多斜杠 → 单斜杠
|
|
227
231
|
.replace(/\/$/, '') // 去掉结尾 /
|
|
228
232
|
}
|
|
233
|
+
|
|
234
|
+
/** mobook 下按文件名递归查找:仅在直线路径均失败时调用;有深度/目录数上限,找到即停 */
|
|
235
|
+
const MOBOOK_FIND_MAX_DEPTH = 10
|
|
236
|
+
const MOBOOK_FIND_MAX_DIRS = 2000
|
|
237
|
+
|
|
238
|
+
function findMobookFileByBasename(mobookRoot: string, basename: string): string | undefined {
|
|
239
|
+
if (!basename || basename === '.' || basename === '..') return undefined
|
|
240
|
+
if (!fs.existsSync(mobookRoot)) return undefined
|
|
241
|
+
const caseInsensitive = process.platform === 'win32' || process.platform === 'darwin'
|
|
242
|
+
const want = caseInsensitive ? basename.toLowerCase() : basename
|
|
243
|
+
|
|
244
|
+
const stack: Array<{ dir: string; depth: number }> = [{ dir: mobookRoot, depth: 0 }]
|
|
245
|
+
let dirsVisited = 0
|
|
246
|
+
|
|
247
|
+
while (stack.length > 0 && dirsVisited < MOBOOK_FIND_MAX_DIRS) {
|
|
248
|
+
const { dir, depth } = stack.pop()!
|
|
249
|
+
if (depth > MOBOOK_FIND_MAX_DEPTH) continue
|
|
250
|
+
dirsVisited += 1
|
|
251
|
+
|
|
252
|
+
let entries: fs.Dirent[]
|
|
253
|
+
try {
|
|
254
|
+
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
255
|
+
} catch {
|
|
256
|
+
continue
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const subdirs: string[] = []
|
|
260
|
+
for (const ent of entries) {
|
|
261
|
+
const full = path.join(dir, ent.name)
|
|
262
|
+
if (ent.isFile()) {
|
|
263
|
+
const ok = caseInsensitive ? ent.name.toLowerCase() === want : ent.name === basename
|
|
264
|
+
if (ok) return full
|
|
265
|
+
} else if (ent.isDirectory() && depth < MOBOOK_FIND_MAX_DEPTH) {
|
|
266
|
+
if (ent.name.startsWith('.')) continue
|
|
267
|
+
subdirs.push(full)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
for (let i = subdirs.length - 1; i >= 0; i--) {
|
|
271
|
+
stack.push({ dir: subdirs[i]!, depth: depth + 1 })
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return undefined
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function getMobookRoot(): string | undefined {
|
|
278
|
+
if (process.platform === 'win32') return path.join('C:\\', 'mobook')
|
|
279
|
+
if (process.platform === 'darwin') return path.join(os.homedir(), 'mobook')
|
|
280
|
+
return undefined
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function getFilePathByFile(file: string) {
|
|
284
|
+
const ws = getWorkspaceDir()
|
|
285
|
+
const candidates: string[] = [file]
|
|
286
|
+
candidates.push(path.join(ws, file))
|
|
287
|
+
candidates.push(path.join(ws, file.replace(/^\//, '')))
|
|
288
|
+
const underMobook = file.replace(/^\/mobook\//i, '').replace(/^mobook[\\/]/i, '')
|
|
289
|
+
const workspaceMobookRoot = path.join(ws, 'mobook')
|
|
290
|
+
const homeMobookRoot = underMobook ? getMobookRoot() : undefined
|
|
291
|
+
|
|
292
|
+
if (underMobook) {
|
|
293
|
+
if (fs.existsSync(workspaceMobookRoot)) {
|
|
294
|
+
candidates.push(path.join(workspaceMobookRoot, underMobook))
|
|
295
|
+
}
|
|
296
|
+
if (homeMobookRoot) {
|
|
297
|
+
candidates.push(path.join(homeMobookRoot, underMobook))
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const resolved = candidates.find((p) => fs.existsSync(p))
|
|
301
|
+
if (resolved) return resolved
|
|
302
|
+
|
|
303
|
+
if (!underMobook) return undefined
|
|
304
|
+
const base = path.basename(underMobook)
|
|
305
|
+
if (fs.existsSync(workspaceMobookRoot)) {
|
|
306
|
+
const inWorkspace = findMobookFileByBasename(workspaceMobookRoot, base)
|
|
307
|
+
if (inWorkspace) return inWorkspace
|
|
308
|
+
}
|
|
309
|
+
if (homeMobookRoot) {
|
|
310
|
+
const inHome = findMobookFileByBasename(homeMobookRoot, base)
|
|
311
|
+
if (inHome) return inHome
|
|
312
|
+
}
|
|
313
|
+
return undefined
|
|
314
|
+
}
|