@dcrays/dcgchat-test 0.3.21 → 0.3.24
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 +1 -2
- package/openclaw.plugin.json +4 -2
- package/package.json +3 -12
- package/src/bot.ts +8 -28
- package/src/channel.ts +8 -9
- package/src/cron.ts +42 -12
- package/src/cronToolCall.ts +3 -3
- package/src/gateway/index.ts +3 -1
- package/src/utils/constant.ts +0 -4
- package/src/utils/global.ts +2 -2
- package/src/utils/log.ts +1 -2
- package/src/utils/params.ts +1 -1
- package/src/utils/searchFile.ts +137 -57
- package/README.md +0 -83
package/index.ts
CHANGED
|
@@ -3,12 +3,11 @@ import { emptyPluginConfigSchema } from 'openclaw/plugin-sdk'
|
|
|
3
3
|
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
|
-
import { channelInfo, ENV } from './src/utils/constant.js'
|
|
7
6
|
import { setOpenClawConfig } from './src/utils/global.js'
|
|
8
7
|
import { startDcgchatGatewaySocket } from './src/gateway/socket.js'
|
|
9
8
|
|
|
10
9
|
const plugin = {
|
|
11
|
-
id:
|
|
10
|
+
id: "dcgchat-test",
|
|
12
11
|
name: '书灵墨宝',
|
|
13
12
|
description: '连接 OpenClaw 与 书灵墨宝 产品(WebSocket)',
|
|
14
13
|
configSchema: emptyPluginConfigSchema(),
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dcrays/dcgchat-test",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.24",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw channel plugin for 书灵墨宝 (WebSocket)",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -16,12 +16,6 @@
|
|
|
16
16
|
"websocket",
|
|
17
17
|
"ai"
|
|
18
18
|
],
|
|
19
|
-
"scripts": {
|
|
20
|
-
"typecheck": "tsc --noEmit",
|
|
21
|
-
"build:production": "npx tsx scripts/build.ts production",
|
|
22
|
-
"build:prod": "npx tsx scripts/build.ts production",
|
|
23
|
-
"build:test": "npx tsx scripts/build.ts test"
|
|
24
|
-
},
|
|
25
19
|
"dependencies": {
|
|
26
20
|
"ali-oss": "file:src/libs/ali-oss-6.23.0.tgz",
|
|
27
21
|
"axios": "file:src/libs/axios-1.13.6.tgz",
|
|
@@ -37,18 +31,15 @@
|
|
|
37
31
|
"id": "dcgchat-test",
|
|
38
32
|
"label": "书灵墨宝",
|
|
39
33
|
"selectionLabel": "书灵墨宝",
|
|
40
|
-
"docsPath": "/channels/dcgchat",
|
|
34
|
+
"docsPath": "/channels/dcgchat-test",
|
|
41
35
|
"docsLabel": "dcgchat-test",
|
|
42
36
|
"blurb": "连接 OpenClaw 与 书灵墨宝 产品",
|
|
43
37
|
"order": 80
|
|
44
38
|
},
|
|
45
39
|
"install": {
|
|
46
40
|
"npmSpec": "@dcrays/dcgchat-test",
|
|
47
|
-
"localPath": "extensions/dcgchat",
|
|
41
|
+
"localPath": "extensions/dcgchat-test",
|
|
48
42
|
"defaultChoice": "npm"
|
|
49
43
|
}
|
|
50
|
-
},
|
|
51
|
-
"devDependencies": {
|
|
52
|
-
"openclaw": "^2026.3.13"
|
|
53
44
|
}
|
|
54
45
|
}
|
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'
|
|
@@ -156,7 +147,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
156
147
|
|
|
157
148
|
const route = core.channel.routing.resolveAgentRoute({
|
|
158
149
|
cfg: config,
|
|
159
|
-
channel:
|
|
150
|
+
channel: "dcgchat-test",
|
|
160
151
|
accountId: account.accountId,
|
|
161
152
|
peer: { kind: 'direct', id: conversationId }
|
|
162
153
|
})
|
|
@@ -235,13 +226,13 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
235
226
|
ChatType: 'direct',
|
|
236
227
|
SenderName: agentDisplayName,
|
|
237
228
|
SenderId: userId,
|
|
238
|
-
Provider:
|
|
239
|
-
Surface:
|
|
229
|
+
Provider: "dcgchat-test",
|
|
230
|
+
Surface: "dcgchat-test",
|
|
240
231
|
MessageSid: msg.content.message_id,
|
|
241
232
|
Timestamp: Date.now(),
|
|
242
233
|
WasMentioned: true,
|
|
243
234
|
CommandAuthorized: true,
|
|
244
|
-
OriginatingChannel:
|
|
235
|
+
OriginatingChannel: "dcgchat-test",
|
|
245
236
|
OriginatingTo: effectiveSessionKey,
|
|
246
237
|
...mediaPayload
|
|
247
238
|
})
|
|
@@ -253,7 +244,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
253
244
|
const prefixContext = createReplyPrefixContext({
|
|
254
245
|
cfg: config,
|
|
255
246
|
agentId: effectiveAgentId ?? '',
|
|
256
|
-
channel:
|
|
247
|
+
channel: "dcgchat-test",
|
|
257
248
|
accountId: account.accountId
|
|
258
249
|
})
|
|
259
250
|
|
|
@@ -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/channel.ts
CHANGED
|
@@ -5,7 +5,6 @@ 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 { channelInfo, ENV } from './utils/constant.js'
|
|
9
8
|
import { getEffectiveMsgParams, getCurrentSessionKey } from './utils/params.js'
|
|
10
9
|
import { startDcgchatGatewaySocket } from './gateway/socket.js'
|
|
11
10
|
|
|
@@ -36,7 +35,7 @@ export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<v
|
|
|
36
35
|
const fileName = mediaUrl?.split(/[\\/]/).pop() || ''
|
|
37
36
|
|
|
38
37
|
try {
|
|
39
|
-
const botToken = msgCtx.botToken ?? getOpenClawConfig()?.channels?.[
|
|
38
|
+
const botToken = msgCtx.botToken ?? getOpenClawConfig()?.channels?.["dcgchat-test"]?.botToken ?? ''
|
|
40
39
|
const url = opts.mediaUrl ? await ossUpload(opts.mediaUrl, botToken, 1) : ''
|
|
41
40
|
wsSendRaw(msgCtx, {
|
|
42
41
|
response: opts.text ?? '',
|
|
@@ -56,7 +55,7 @@ export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<v
|
|
|
56
55
|
|
|
57
56
|
export function resolveAccount(cfg: OpenClawConfig, accountId?: string | null): ResolvedDcgchatAccount {
|
|
58
57
|
const id = accountId ?? DEFAULT_ACCOUNT_ID
|
|
59
|
-
const raw = (cfg.channels?.[
|
|
58
|
+
const raw = (cfg.channels?.["dcgchat-test"] as DcgchatConfig | undefined) ?? {}
|
|
60
59
|
return {
|
|
61
60
|
accountId: id,
|
|
62
61
|
enabled: raw.enabled !== false,
|
|
@@ -70,13 +69,13 @@ export function resolveAccount(cfg: OpenClawConfig, accountId?: string | null):
|
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
73
|
-
id:
|
|
72
|
+
id: "dcgchat-test",
|
|
74
73
|
meta: {
|
|
75
|
-
id:
|
|
74
|
+
id: "dcgchat-test",
|
|
76
75
|
label: '书灵墨宝',
|
|
77
76
|
selectionLabel: '书灵墨宝',
|
|
78
77
|
docsPath: '/channels/dcgchat',
|
|
79
|
-
docsLabel:
|
|
78
|
+
docsLabel: "dcgchat-test",
|
|
80
79
|
blurb: '连接 OpenClaw 与 书灵墨宝 产品',
|
|
81
80
|
order: 80
|
|
82
81
|
},
|
|
@@ -117,7 +116,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
117
116
|
channels: {
|
|
118
117
|
...cfg.channels,
|
|
119
118
|
dcgchat: {
|
|
120
|
-
...(cfg.channels?.[
|
|
119
|
+
...(cfg.channels?.["dcgchat-test"] as Record<string, unknown> | undefined),
|
|
121
120
|
enabled
|
|
122
121
|
}
|
|
123
122
|
}
|
|
@@ -170,7 +169,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
170
169
|
}
|
|
171
170
|
}
|
|
172
171
|
return {
|
|
173
|
-
channel:
|
|
172
|
+
channel: "dcgchat-test",
|
|
174
173
|
messageId: `${messageId}`,
|
|
175
174
|
chatId: to
|
|
176
175
|
}
|
|
@@ -184,7 +183,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
184
183
|
dcgLogger(`channel sendMedia to ${ctx.to}`)
|
|
185
184
|
await sendDcgchatMedia({ sessionKey: to ?? '', mediaUrl: ctx.mediaUrl ?? '' })
|
|
186
185
|
return {
|
|
187
|
-
channel:
|
|
186
|
+
channel: "dcgchat-test",
|
|
188
187
|
messageId: `${messageId}`,
|
|
189
188
|
chatId: msgCtx.userId?.toString()
|
|
190
189
|
}
|
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/cronToolCall.ts
CHANGED
|
@@ -127,7 +127,7 @@ function injectBestEffort(params: Record<string, unknown>, sk: string): Record<s
|
|
|
127
127
|
;(newParams.delivery as CronDelivery).bestEffort = true
|
|
128
128
|
;(newParams.delivery as CronDelivery).to = `dcg-cron:${sk}`
|
|
129
129
|
;(newParams.delivery as CronDelivery).accountId = agentId
|
|
130
|
-
;(newParams.delivery as CronDelivery).channel =
|
|
130
|
+
;(newParams.delivery as CronDelivery).channel = "dcgchat-test"
|
|
131
131
|
newParams.sessionKey = sk
|
|
132
132
|
return newParams
|
|
133
133
|
}
|
|
@@ -138,7 +138,7 @@ function injectBestEffort(params: Record<string, unknown>, sk: string): Record<s
|
|
|
138
138
|
;(job.delivery as CronDelivery).bestEffort = true
|
|
139
139
|
;(newParams.delivery as CronDelivery).to = `dcg-cron:${sk}`
|
|
140
140
|
;(newParams.delivery as CronDelivery).accountId = agentId
|
|
141
|
-
;(newParams.delivery as CronDelivery).channel =
|
|
141
|
+
;(newParams.delivery as CronDelivery).channel = "dcgchat-test"
|
|
142
142
|
newParams.sessionKey = sk
|
|
143
143
|
return newParams
|
|
144
144
|
}
|
|
@@ -176,7 +176,7 @@ export function cronToolCall(event: { toolName: any; params: any; toolCallId: an
|
|
|
176
176
|
if (params.command.indexOf('cron create') > -1 || params.command.indexOf('cron add') > -1) {
|
|
177
177
|
const newParams = JSON.parse(JSON.stringify(params)) as Record<string, unknown>
|
|
178
178
|
newParams.command =
|
|
179
|
-
params.command.replace('--json', '') + ` --session-key ${sk} --channel ${
|
|
179
|
+
params.command.replace('--json', '') + ` --session-key ${sk} --channel ${"dcgchat-test"} --to dcg-cron:${sk} --json`
|
|
180
180
|
return { params: newParams }
|
|
181
181
|
} else {
|
|
182
182
|
return params
|
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/utils/constant.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
export const ENV: 'production' | 'test' | 'develop' = 'test'
|
|
2
2
|
|
|
3
|
-
export const channelInfo: Record<string, string> = {
|
|
4
|
-
production: 'dcgchat',
|
|
5
|
-
test: 'dcgchat-test'
|
|
6
|
-
}
|
|
7
3
|
|
|
8
4
|
export const systemCommand = ['/new', '/status']
|
|
9
5
|
export const interruptCommand = ['chat.stop']
|
package/src/utils/global.ts
CHANGED
|
@@ -33,7 +33,7 @@ const os = require('os')
|
|
|
33
33
|
function getWorkspacePath() {
|
|
34
34
|
const workspacePath = path.join(
|
|
35
35
|
os.homedir(),
|
|
36
|
-
config?.channels?.[
|
|
36
|
+
config?.channels?.["dcgchat-test"]?.appId == 110 ? '.mobook' : '.openclaw',
|
|
37
37
|
'workspace'
|
|
38
38
|
)
|
|
39
39
|
if (fs.existsSync(workspacePath)) {
|
|
@@ -123,7 +123,7 @@ export const getSessionKey = (content: any, accountId: string) => {
|
|
|
123
123
|
|
|
124
124
|
const route = core.channel.routing.resolveAgentRoute({
|
|
125
125
|
cfg: getOpenClawConfig() as OpenClawConfig,
|
|
126
|
-
channel:
|
|
126
|
+
channel: "dcgchat-test",
|
|
127
127
|
accountId: accountId || 'default',
|
|
128
128
|
peer: { kind: 'direct', id: session_id }
|
|
129
129
|
})
|
package/src/utils/log.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { RuntimeEnv } from 'openclaw/plugin-sdk'
|
|
2
|
-
import { channelInfo, ENV } from './constant.js'
|
|
3
2
|
|
|
4
3
|
let logger: RuntimeEnv | null = null
|
|
5
4
|
|
|
@@ -11,6 +10,6 @@ export function dcgLogger(message: string, type: 'log' | 'error' = 'log'): void
|
|
|
11
10
|
if (logger) {
|
|
12
11
|
logger[type](`书灵墨宝🚀 ~ [${new Date().toISOString()}] ${message}`)
|
|
13
12
|
} else {
|
|
14
|
-
console[type](`书灵墨宝🚀 ~ ${new Date().toISOString()} [${
|
|
13
|
+
console[type](`书灵墨宝🚀 ~ ${new Date().toISOString()} [${"dcgchat-test"}]: ${message}`)
|
|
15
14
|
}
|
|
16
15
|
}
|
package/src/utils/params.ts
CHANGED
|
@@ -12,7 +12,7 @@ let currentSessionKey: string | null = null
|
|
|
12
12
|
|
|
13
13
|
/** 从 OpenClaw 配置读取当前 channel 的基础参数(唯一来源,供 transport / resolve 等复用) */
|
|
14
14
|
export function getParamsDefaults(): IMsgParams {
|
|
15
|
-
const ch = (getOpenClawConfig()?.channels?.[
|
|
15
|
+
const ch = (getOpenClawConfig()?.channels?.["dcgchat-test"] as DcgchatConfig | undefined) ?? {}
|
|
16
16
|
return {
|
|
17
17
|
userId: Number(ch.userId ?? 0),
|
|
18
18
|
botToken: ch.botToken ?? '',
|
package/src/utils/searchFile.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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
|
-
/**
|
|
4
|
-
* 从文本中提取 /mobook 目录下的文件
|
|
5
|
-
* @param {string} text
|
|
6
|
-
* @returns {string[]}
|
|
7
|
-
*/
|
|
7
|
+
/** 参与提取的常见文件扩展名 */
|
|
8
8
|
const EXT_LIST = [
|
|
9
9
|
// 文档类
|
|
10
10
|
'doc',
|
|
@@ -88,11 +88,45 @@ const EXT_LIST = [
|
|
|
88
88
|
*/
|
|
89
89
|
const EXT_SORTED_FOR_REGEX = [...EXT_LIST].sort((a, b) => b.length - a.length)
|
|
90
90
|
|
|
91
|
+
/** 正则交替串(长扩展名优先) */
|
|
92
|
+
const EXT_ALT = `(${EXT_SORTED_FOR_REGEX.join('|')})`
|
|
93
|
+
/** 文件名片段:中文、常见符号、非贪婪 */
|
|
94
|
+
const FILE_NAME_CLASS = `[\\w\\u4e00-\\u9fa5::《》()()\\-\\s]+?`
|
|
95
|
+
|
|
96
|
+
/** 预编译,避免 extractMobookFiles 每次调用重复构建正则 */
|
|
97
|
+
const RX_EXTRACT = {
|
|
98
|
+
backtick: new RegExp(`\`([^\\\`]+?\\.${EXT_ALT})\``, 'gi'),
|
|
99
|
+
fullPath: new RegExp(`/mobook/${FILE_NAME_CLASS}\\.${EXT_ALT}`, 'gi'),
|
|
100
|
+
winMobook: new RegExp(`(?:[a-zA-Z]:)?[/\\\\]mobook[/\\\\]${FILE_NAME_CLASS}\\.${EXT_ALT}`, 'gi'),
|
|
101
|
+
inline: new RegExp(`mobook下的\\s*(${FILE_NAME_CLASS}\\.${EXT_ALT})`, 'gi'),
|
|
102
|
+
bold: new RegExp(`\\*\\*(${FILE_NAME_CLASS}\\.${EXT_ALT})\\*\\*`, 'gi'),
|
|
103
|
+
loose: new RegExp(`(${FILE_NAME_CLASS}\\.${EXT_ALT})\\s*\\(`, 'gi'),
|
|
104
|
+
inlineFile: new RegExp(`${FILE_NAME_CLASS}\\.${EXT_ALT}`, 'i')
|
|
105
|
+
}
|
|
106
|
+
|
|
91
107
|
/** 去除控制符、零宽字符等常见脏值 */
|
|
92
108
|
function stripMobookNoise(s: string) {
|
|
93
109
|
return s.replace(/[\u0000-\u001F\u007F\u200B-\u200D\u200E\u200F\uFEFF]/g, '')
|
|
94
110
|
}
|
|
95
111
|
|
|
112
|
+
/**
|
|
113
|
+
* 从路径或 mobook 引用中取出「提到的文件名」:去掉盘符、/mobook、\\mobook\\ 等前缀后取 basename
|
|
114
|
+
*/
|
|
115
|
+
function toMobookReferencedBasename(p: string): string {
|
|
116
|
+
let s = stripMobookNoise(p).trim()
|
|
117
|
+
if (!s) return ''
|
|
118
|
+
s = s.replace(/^(?:[a-zA-Z]:)?[/\\]+mobook[/\\]/i, '')
|
|
119
|
+
s = s.replace(/^\/mobook\//i, '')
|
|
120
|
+
s = s.replace(/\\/g, '/')
|
|
121
|
+
const parts = s.split('/').filter(Boolean)
|
|
122
|
+
return parts.length ? parts[parts.length - 1]! : s
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function addMobookMentionedFile(result: Set<string>, raw: string) {
|
|
126
|
+
const base = toMobookReferencedBasename(raw)
|
|
127
|
+
if (base && isValidFileName(base)) result.add(base)
|
|
128
|
+
}
|
|
129
|
+
|
|
96
130
|
/**
|
|
97
131
|
* 从文本中扫描 `.../mobook/...` 或 `...\mobook\...` 片段,按最长后缀匹配合法扩展名(兜底)
|
|
98
132
|
*/
|
|
@@ -128,7 +162,7 @@ function collectMobookPathsAfterNeedle(text: string, lower: string, needle: stri
|
|
|
128
162
|
const base = raw.slice(0, -(matchedExt.length + 1))
|
|
129
163
|
const fileName = `${base}.${matchedExt}`
|
|
130
164
|
if (isValidFileName(fileName)) {
|
|
131
|
-
result
|
|
165
|
+
addMobookMentionedFile(result, fileName)
|
|
132
166
|
}
|
|
133
167
|
from = start + 1
|
|
134
168
|
}
|
|
@@ -140,62 +174,35 @@ function collectMobookPathsByScan(text: string, result: Set<string>): void {
|
|
|
140
174
|
collectMobookPathsAfterNeedle(text, lower, '\\mobook\\', result)
|
|
141
175
|
}
|
|
142
176
|
|
|
177
|
+
/**
|
|
178
|
+
* 从文本中提取提到的 mobook 相关文件名(仅 basename,不含目录)
|
|
179
|
+
* @param text 原始文本
|
|
180
|
+
* @returns 去重后的文件名列表,例如 `['报告.pdf', 'data.xlsx']`
|
|
181
|
+
*/
|
|
143
182
|
export function extractMobookFiles(text = '') {
|
|
144
183
|
if (typeof text !== 'string' || !text.trim()) return []
|
|
145
184
|
// 全角冒号(中文输入常见)→ 半角,便于匹配 c:\mobook\
|
|
146
185
|
text = text.replace(/\uFF1A/g, ':')
|
|
147
186
|
const result = new Set<string>()
|
|
148
|
-
// ✅ 扩展名(必须长扩展名优先,见 EXT_SORTED_FOR_REGEX)
|
|
149
|
-
const EXT = `(${EXT_SORTED_FOR_REGEX.join('|')})`
|
|
150
|
-
// ✅ 文件名字符(增强:支持中文、符号)
|
|
151
|
-
const FILE_NAME = `[\\w\\u4e00-\\u9fa5::《》()()\\-\\s]+?`
|
|
152
187
|
try {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
;(text.match(backtickReg) || []).forEach((item) => {
|
|
156
|
-
const name = item.replace(/`/g, '').trim()
|
|
157
|
-
if (isValidFileName(name)) {
|
|
158
|
-
result.add(`/mobook/${name}`)
|
|
159
|
-
}
|
|
188
|
+
;(text.match(RX_EXTRACT.backtick) || []).forEach((item) => {
|
|
189
|
+
addMobookMentionedFile(result, item.replace(/`/g, '').trim())
|
|
160
190
|
})
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
;(text.match(fullPathReg) || []).forEach((p) => {
|
|
164
|
-
result.add(normalizePath(p))
|
|
165
|
-
})
|
|
166
|
-
// 2️⃣b Windows 实际保存路径:C:\mobook\xxx、c:/mobook/xxx、\mobook\xxx(模型常写反斜杠,原先无法识别)
|
|
167
|
-
const winMobookReg = new RegExp(`(?:[a-zA-Z]:)?[/\\\\]mobook[/\\\\]${FILE_NAME}\\.${EXT}`, 'gi')
|
|
168
|
-
;(text.match(winMobookReg) || []).forEach((full) => {
|
|
191
|
+
;(text.match(RX_EXTRACT.fullPath) || []).forEach((p) => addMobookMentionedFile(result, p))
|
|
192
|
+
;(text.match(RX_EXTRACT.winMobook) || []).forEach((full) => {
|
|
169
193
|
const name = full.replace(/^(?:[a-zA-Z]:)?[/\\\\]mobook[/\\\\]/i, '').trim()
|
|
170
|
-
|
|
171
|
-
result.add(normalizePath(`/mobook/${name}`))
|
|
172
|
-
}
|
|
194
|
+
addMobookMentionedFile(result, name)
|
|
173
195
|
})
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const match = item.match(new RegExp(`${FILE_NAME}\\.${EXT}`, 'i'))
|
|
178
|
-
if (match && isValidFileName(match[0])) {
|
|
179
|
-
result.add(`/mobook/${match[0].trim()}`)
|
|
180
|
-
}
|
|
196
|
+
;(text.match(RX_EXTRACT.inline) || []).forEach((item) => {
|
|
197
|
+
const match = item.match(RX_EXTRACT.inlineFile)
|
|
198
|
+
if (match?.[0]) addMobookMentionedFile(result, match[0].trim())
|
|
181
199
|
})
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
;(text.match(boldReg) || []).forEach((item) => {
|
|
185
|
-
const name = item.replace(/\*\*/g, '').trim()
|
|
186
|
-
if (isValidFileName(name)) {
|
|
187
|
-
result.add(`/mobook/${name}`)
|
|
188
|
-
}
|
|
200
|
+
;(text.match(RX_EXTRACT.bold) || []).forEach((item) => {
|
|
201
|
+
addMobookMentionedFile(result, item.replace(/\*\*/g, '').trim())
|
|
189
202
|
})
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
;(text.match(looseReg) || []).forEach((item) => {
|
|
193
|
-
const name = item.replace(/\s*\(.+$/, '').trim()
|
|
194
|
-
if (isValidFileName(name)) {
|
|
195
|
-
result.add(`/mobook/${name}`)
|
|
196
|
-
}
|
|
203
|
+
;(text.match(RX_EXTRACT.loose) || []).forEach((item) => {
|
|
204
|
+
addMobookMentionedFile(result, item.replace(/\s*\(.+$/, '').trim())
|
|
197
205
|
})
|
|
198
|
-
// 6️⃣ 兜底:绝对路径等 `.../mobook/<文件名>.<扩展名>` + 最长后缀匹配 + 去脏字符
|
|
199
206
|
collectMobookPathsByScan(text, result)
|
|
200
207
|
} catch (e) {
|
|
201
208
|
dcgLogger(`extractMobookFiles error:${e}`)
|
|
@@ -218,11 +225,84 @@ function isValidFileName(name: string) {
|
|
|
218
225
|
return true
|
|
219
226
|
}
|
|
220
227
|
|
|
221
|
-
/**
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
+
/** mobook 下按文件名递归查找:仅在直线路径均失败时调用;有深度/目录数上限,找到即停 */
|
|
229
|
+
const MOBOOK_FIND_MAX_DEPTH = 10
|
|
230
|
+
const MOBOOK_FIND_MAX_DIRS = 2000
|
|
231
|
+
|
|
232
|
+
function findMobookFileByBasename(mobookRoot: string, basename: string): string | undefined {
|
|
233
|
+
if (!basename || basename === '.' || basename === '..') return undefined
|
|
234
|
+
if (!fs.existsSync(mobookRoot)) return undefined
|
|
235
|
+
const caseInsensitive = process.platform === 'win32' || process.platform === 'darwin'
|
|
236
|
+
const want = caseInsensitive ? basename.toLowerCase() : basename
|
|
237
|
+
|
|
238
|
+
const stack: Array<{ dir: string; depth: number }> = [{ dir: mobookRoot, depth: 0 }]
|
|
239
|
+
let dirsVisited = 0
|
|
240
|
+
|
|
241
|
+
while (stack.length > 0 && dirsVisited < MOBOOK_FIND_MAX_DIRS) {
|
|
242
|
+
const { dir, depth } = stack.pop()!
|
|
243
|
+
if (depth > MOBOOK_FIND_MAX_DEPTH) continue
|
|
244
|
+
dirsVisited += 1
|
|
245
|
+
|
|
246
|
+
let entries: fs.Dirent[]
|
|
247
|
+
try {
|
|
248
|
+
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
249
|
+
} catch {
|
|
250
|
+
continue
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const subdirs: string[] = []
|
|
254
|
+
for (const ent of entries) {
|
|
255
|
+
const full = path.join(dir, ent.name)
|
|
256
|
+
if (ent.isFile()) {
|
|
257
|
+
const ok = caseInsensitive ? ent.name.toLowerCase() === want : ent.name === basename
|
|
258
|
+
if (ok) return full
|
|
259
|
+
} else if (ent.isDirectory() && depth < MOBOOK_FIND_MAX_DEPTH) {
|
|
260
|
+
if (ent.name.startsWith('.')) continue
|
|
261
|
+
subdirs.push(full)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
for (let i = subdirs.length - 1; i >= 0; i--) {
|
|
265
|
+
stack.push({ dir: subdirs[i]!, depth: depth + 1 })
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return undefined
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function getMobookRoot(): string | undefined {
|
|
272
|
+
if (process.platform === 'win32') return path.join('C:\\', 'mobook')
|
|
273
|
+
if (process.platform === 'darwin') return path.join(os.homedir(), 'mobook')
|
|
274
|
+
return undefined
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function getFilePathByFile(file: string) {
|
|
278
|
+
const ws = getWorkspaceDir()
|
|
279
|
+
const candidates: string[] = [file]
|
|
280
|
+
candidates.push(path.join(ws, file))
|
|
281
|
+
candidates.push(path.join(ws, file.replace(/^\//, '')))
|
|
282
|
+
const underMobook = file.replace(/^\/mobook\//i, '').replace(/^mobook[\\/]/i, '')
|
|
283
|
+
const workspaceMobookRoot = path.join(ws, 'mobook')
|
|
284
|
+
const homeMobookRoot = underMobook ? getMobookRoot() : undefined
|
|
285
|
+
|
|
286
|
+
if (underMobook) {
|
|
287
|
+
if (fs.existsSync(workspaceMobookRoot)) {
|
|
288
|
+
candidates.push(path.join(workspaceMobookRoot, underMobook))
|
|
289
|
+
}
|
|
290
|
+
if (homeMobookRoot) {
|
|
291
|
+
candidates.push(path.join(homeMobookRoot, underMobook))
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const resolved = candidates.find((p) => fs.existsSync(p))
|
|
295
|
+
if (resolved) return resolved
|
|
296
|
+
|
|
297
|
+
if (!underMobook) return undefined
|
|
298
|
+
const base = path.basename(underMobook)
|
|
299
|
+
if (fs.existsSync(workspaceMobookRoot)) {
|
|
300
|
+
const inWorkspace = findMobookFileByBasename(workspaceMobookRoot, base)
|
|
301
|
+
if (inWorkspace) return inWorkspace
|
|
302
|
+
}
|
|
303
|
+
if (homeMobookRoot) {
|
|
304
|
+
const inHome = findMobookFileByBasename(homeMobookRoot, base)
|
|
305
|
+
if (inHome) return inHome
|
|
306
|
+
}
|
|
307
|
+
return undefined
|
|
228
308
|
}
|
package/README.md
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
# OpenClaw 书灵墨宝 插件
|
|
2
|
-
|
|
3
|
-
连接 OpenClaw 与 书灵墨宝 产品的通道插件。
|
|
4
|
-
|
|
5
|
-
## 架构
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
┌──────────┐ WebSocket ┌──────────────┐ WebSocket ┌─────────────────────┐
|
|
9
|
-
│ Web 前端 │ ←───────────────→ │ 公司后端服务 │ ←───────────────→ │ OpenClaw(工作电脑) │
|
|
10
|
-
└──────────┘ └──────────────┘ (OpenClaw 主动连) └─────────────────────┘
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
- OpenClaw 插件**主动连接**后端的 WebSocket 服务(不需要公网 IP)
|
|
14
|
-
- 后端收到用户消息后转发给 OpenClaw,OpenClaw 回复后发回后端
|
|
15
|
-
|
|
16
|
-
## 快速开始
|
|
17
|
-
|
|
18
|
-
### 1. 安装插件
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
pnpm openclaw plugins install -l /path/to/openclaw-dcgchat
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
### 2. 配置
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
openclaw config set channels.dcgchat.enabled true
|
|
28
|
-
openclaw config set channels.dcgchat.wsUrl "ws://your-backend:8080/openclaw/ws"
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
### 3. 启动
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
pnpm openclaw gateway
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## 消息协议(MVP)
|
|
38
|
-
|
|
39
|
-
### 下行:后端 → OpenClaw(用户消息)
|
|
40
|
-
|
|
41
|
-
```json
|
|
42
|
-
{ "type": "message", "userId": "user_001", "text": "你好" }
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
### 上行:OpenClaw → 后端(Agent 回复)
|
|
46
|
-
|
|
47
|
-
```json
|
|
48
|
-
{ "type": "reply", "userId": "user_001", "text": "你好!有什么可以帮你的?" }
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
## 配置项
|
|
52
|
-
|
|
53
|
-
| 配置键 | 类型 | 说明 |
|
|
54
|
-
|--------|------|------|
|
|
55
|
-
| `channels.dcgchat.enabled` | boolean | 是否启用 |
|
|
56
|
-
| `channels.dcgchat.wsUrl` | string | 后端 WebSocket 地址 |
|
|
57
|
-
|
|
58
|
-
## 开发
|
|
59
|
-
|
|
60
|
-
```bash
|
|
61
|
-
# 安装依赖
|
|
62
|
-
pnpm install
|
|
63
|
-
|
|
64
|
-
# 类型检查
|
|
65
|
-
pnpm typecheck
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
## 文件结构
|
|
69
|
-
|
|
70
|
-
- `index.ts` - 插件入口
|
|
71
|
-
- `src/channel.ts` - ChannelPlugin 定义
|
|
72
|
-
- `src/runtime.ts` - 插件 runtime
|
|
73
|
-
- `src/types.ts` - 类型定义
|
|
74
|
-
- `src/monitor.ts` - WebSocket 连接与断线重连
|
|
75
|
-
- `src/bot.ts` - 消息处理与 Agent 调用
|
|
76
|
-
|
|
77
|
-
## 后续迭代
|
|
78
|
-
|
|
79
|
-
- [ ] Token 认证
|
|
80
|
-
- [ ] 流式输出
|
|
81
|
-
- [ ] Typing 指示
|
|
82
|
-
- [ ] messageId 去重
|
|
83
|
-
- [ ] 错误消息类型
|