@dcrays/dcgchat-test 0.3.27 → 0.3.31
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 +51 -13
- package/src/channel.ts +61 -24
- package/src/cronToolCall.ts +3 -3
- package/src/request/api.ts +1 -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 +2 -2
- package/src/utils/searchFile.ts +56 -162
- 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.31",
|
|
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,11 +1,20 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
2
4
|
import type { ReplyPayload } from 'openclaw/plugin-sdk'
|
|
3
5
|
import { createReplyPrefixContext } from 'openclaw/plugin-sdk'
|
|
4
6
|
import type { InboundMessage } from './types.js'
|
|
5
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
clearSentMediaKeys,
|
|
9
|
+
getDcgchatRuntime,
|
|
10
|
+
getOpenClawConfig,
|
|
11
|
+
getSessionKey,
|
|
12
|
+
getWorkspaceDir,
|
|
13
|
+
setMsgStatus
|
|
14
|
+
} from './utils/global.js'
|
|
6
15
|
import { resolveAccount, sendDcgchatMedia } from './channel.js'
|
|
7
16
|
import { generateSignUrl } from './request/api.js'
|
|
8
|
-
import { extractMobookFiles
|
|
17
|
+
import { extractMobookFiles } from './utils/searchFile.js'
|
|
9
18
|
import { sendChunk, sendFinal, sendText as sendTextMsg, sendError, wsSendRaw, sendText } from './transport.js'
|
|
10
19
|
import { dcgLogger } from './utils/log.js'
|
|
11
20
|
import { channelInfo, systemCommand, interruptCommand, ENV, ignoreToolCommand } from './utils/constant.js'
|
|
@@ -147,7 +156,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
147
156
|
|
|
148
157
|
const route = core.channel.routing.resolveAgentRoute({
|
|
149
158
|
cfg: config,
|
|
150
|
-
channel:
|
|
159
|
+
channel: "dcgchat-test",
|
|
151
160
|
accountId: account.accountId,
|
|
152
161
|
peer: { kind: 'direct', id: conversationId }
|
|
153
162
|
})
|
|
@@ -157,7 +166,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
157
166
|
const effectiveAgentId = embeddedAgentId ?? route.agentId
|
|
158
167
|
const effectiveSessionKey = getSessionKey(msg.content, account.accountId)
|
|
159
168
|
|
|
160
|
-
|
|
169
|
+
const mergedParams = {
|
|
161
170
|
userId: msg._userId,
|
|
162
171
|
botToken: msg.content.bot_token,
|
|
163
172
|
sessionId: conversationId,
|
|
@@ -168,7 +177,14 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
168
177
|
agentId: msg.content.agent_id ?? '',
|
|
169
178
|
sessionKey: effectiveSessionKey,
|
|
170
179
|
real_mobook
|
|
171
|
-
}
|
|
180
|
+
}
|
|
181
|
+
setParamsMessage(effectiveSessionKey, mergedParams)
|
|
182
|
+
// 与 OpenClaw 会话投递里仍可能出现的 ctx.to=SenderId(userId)对齐,便于 getOutboundMsgParams 命中
|
|
183
|
+
dcgLogger(
|
|
184
|
+
`target normalize: rawTarget=${userId}, normalizedTarget=${effectiveSessionKey}, conversationId=${conversationId ?? ''}, messageId=${msg.content.message_id}`
|
|
185
|
+
)
|
|
186
|
+
setParamsMessage(userId, mergedParams)
|
|
187
|
+
dcgLogger(`target alias bound: aliasTarget=${userId} -> sessionKey=${effectiveSessionKey}`)
|
|
172
188
|
const outboundCtx = getEffectiveMsgParams(effectiveSessionKey)
|
|
173
189
|
const agentEntry =
|
|
174
190
|
effectiveAgentId && effectiveAgentId !== 'main' ? config.agents?.list?.find((a) => a.id === effectiveAgentId) : undefined
|
|
@@ -226,16 +242,19 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
226
242
|
ChatType: 'direct',
|
|
227
243
|
SenderName: agentDisplayName,
|
|
228
244
|
SenderId: userId,
|
|
229
|
-
Provider:
|
|
230
|
-
Surface:
|
|
245
|
+
Provider: "dcgchat-test",
|
|
246
|
+
Surface: "dcgchat-test",
|
|
231
247
|
MessageSid: msg.content.message_id,
|
|
232
248
|
Timestamp: Date.now(),
|
|
233
249
|
WasMentioned: true,
|
|
234
250
|
CommandAuthorized: true,
|
|
235
|
-
OriginatingChannel:
|
|
251
|
+
OriginatingChannel: "dcgchat-test",
|
|
236
252
|
OriginatingTo: effectiveSessionKey,
|
|
237
253
|
...mediaPayload
|
|
238
254
|
})
|
|
255
|
+
dcgLogger(
|
|
256
|
+
`inbound context target: rawTarget=${userId}, normalizedTarget=${effectiveSessionKey}, ctx.To=${String(ctxPayload.To ?? '')}, ctx.SessionKey=${String(ctxPayload.SessionKey ?? '')}, ctx.OriginatingTo=${String(ctxPayload.OriginatingTo ?? '')}`
|
|
257
|
+
)
|
|
239
258
|
|
|
240
259
|
const sentMediaKeys = new Set<string>()
|
|
241
260
|
const getMediaKey = (url: string) => url.split(/[\\/]/).pop() ?? url
|
|
@@ -244,7 +263,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
244
263
|
const prefixContext = createReplyPrefixContext({
|
|
245
264
|
cfg: config,
|
|
246
265
|
agentId: effectiveAgentId ?? '',
|
|
247
|
-
channel:
|
|
266
|
+
channel: "dcgchat-test",
|
|
248
267
|
accountId: account.accountId
|
|
249
268
|
})
|
|
250
269
|
|
|
@@ -402,10 +421,19 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
402
421
|
if (sessionStreamSuppressed.has(effectiveSessionKey)) {
|
|
403
422
|
sessionStreamSuppressed.delete(effectiveSessionKey)
|
|
404
423
|
} else {
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
424
|
+
for (const file of extractMobookFiles(completeText)) {
|
|
425
|
+
const candidates: string[] = [file]
|
|
426
|
+
candidates.push(path.join(getWorkspaceDir(), file))
|
|
427
|
+
candidates.push(path.join(getWorkspaceDir(), file.replace(/^\//, '')))
|
|
428
|
+
const underMobook = file.replace(/^\/mobook\//i, '').replace(/^mobook[\\/]/i, '')
|
|
429
|
+
if (underMobook) {
|
|
430
|
+
if (process.platform === 'win32') {
|
|
431
|
+
candidates.push(path.join('C:\\', 'mobook', underMobook))
|
|
432
|
+
} else if (process.platform === 'darwin') {
|
|
433
|
+
candidates.push(path.join(os.homedir(), 'mobook', underMobook))
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const resolved = candidates.find((p) => fs.existsSync(p))
|
|
409
437
|
if (!resolved) continue
|
|
410
438
|
try {
|
|
411
439
|
await sendDcgchatMedia({ sessionKey: effectiveSessionKey, mediaUrl: resolved, text: '' })
|
|
@@ -420,11 +448,21 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
420
448
|
|
|
421
449
|
// Record session metadata
|
|
422
450
|
const storePath = core.channel.session.resolveStorePath(config.session?.store)
|
|
451
|
+
dcgLogger(
|
|
452
|
+
`record session route: rawTarget=${userId}, normalizedTarget=${effectiveSessionKey}, updateLastRoute.to=${effectiveSessionKey}, accountId=${route.accountId}`
|
|
453
|
+
)
|
|
423
454
|
core.channel.session
|
|
424
455
|
.recordInboundSession({
|
|
425
456
|
storePath,
|
|
426
457
|
sessionKey: effectiveSessionKey,
|
|
427
458
|
ctx: ctxPayload,
|
|
459
|
+
// 与 Telegram/Discord 等一致:写入 deliveryContext.to,否则投递可能回退为 From(userId),channel sendMedia 里 ctx.to 会变成数字 userId
|
|
460
|
+
updateLastRoute: {
|
|
461
|
+
sessionKey: effectiveSessionKey,
|
|
462
|
+
channel: "dcgchat-test",
|
|
463
|
+
to: effectiveSessionKey,
|
|
464
|
+
accountId: route.accountId
|
|
465
|
+
},
|
|
428
466
|
onRecordError: (err) => {
|
|
429
467
|
dcgLogger(` session record error: ${String(err)}`, 'error')
|
|
430
468
|
}
|
package/src/channel.ts
CHANGED
|
@@ -4,9 +4,8 @@ import type { ResolvedDcgchatAccount, DcgchatConfig } from './types.js'
|
|
|
4
4
|
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
|
-
import { channelInfo, ENV } from './utils/constant.js'
|
|
8
7
|
import { dcgLogger, setLogger } from './utils/log.js'
|
|
9
|
-
import { getOutboundMsgParams } from './utils/params.js'
|
|
8
|
+
import { getOutboundMsgParams, getParamsMessage } from './utils/params.js'
|
|
10
9
|
import { startDcgchatGatewaySocket } from './gateway/socket.js'
|
|
11
10
|
|
|
12
11
|
export type DcgchatMediaSendOptions = {
|
|
@@ -16,8 +15,15 @@ export type DcgchatMediaSendOptions = {
|
|
|
16
15
|
text?: string
|
|
17
16
|
}
|
|
18
17
|
|
|
18
|
+
function normalizeSessionTarget(rawTo: string): string {
|
|
19
|
+
const cleaned = rawTo.replace('dcg-cron:', '').trim()
|
|
20
|
+
if (!cleaned) return ''
|
|
21
|
+
return getParamsMessage(cleaned)?.sessionKey?.trim() || cleaned
|
|
22
|
+
}
|
|
23
|
+
|
|
19
24
|
export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<void> {
|
|
20
|
-
const
|
|
25
|
+
const sessionKey = normalizeSessionTarget(opts.sessionKey ?? '')
|
|
26
|
+
const msgCtx = getOutboundMsgParams(sessionKey)
|
|
21
27
|
if (!isWsOpen()) {
|
|
22
28
|
dcgLogger(`outbound media skipped -> ws not open: ${opts.mediaUrl ?? ''}`)
|
|
23
29
|
return
|
|
@@ -36,27 +42,27 @@ export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<v
|
|
|
36
42
|
const fileName = mediaUrl?.split(/[\\/]/).pop() || ''
|
|
37
43
|
|
|
38
44
|
try {
|
|
39
|
-
const botToken = msgCtx.botToken ?? getOpenClawConfig()?.channels?.[
|
|
45
|
+
const botToken = msgCtx.botToken ?? getOpenClawConfig()?.channels?.["dcgchat-test"]?.botToken ?? ''
|
|
40
46
|
const url = opts.mediaUrl ? await ossUpload(opts.mediaUrl, botToken, 1) : ''
|
|
41
47
|
wsSendRaw(msgCtx, {
|
|
42
48
|
response: opts.text ?? '',
|
|
43
49
|
message_tags: { source: 'file' },
|
|
44
50
|
files: [{ url, name: fileName }]
|
|
45
51
|
})
|
|
46
|
-
dcgLogger(`dcgchat: sendMedia
|
|
52
|
+
dcgLogger(`dcgchat: sendMedia session=${sessionKey}, file=${fileName}`)
|
|
47
53
|
} catch (error) {
|
|
48
54
|
wsSendRaw(msgCtx, {
|
|
49
55
|
response: opts.text ?? '',
|
|
50
56
|
message_tags: { source: 'file' },
|
|
51
57
|
files: [{ url: opts.mediaUrl ?? '', name: fileName }]
|
|
52
58
|
})
|
|
53
|
-
dcgLogger(`dcgchat: error sendMedia
|
|
59
|
+
dcgLogger(`dcgchat: error sendMedia session=${sessionKey}: ${String(error)}`, 'error')
|
|
54
60
|
}
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
export function resolveAccount(cfg: OpenClawConfig, accountId?: string | null): ResolvedDcgchatAccount {
|
|
58
64
|
const id = accountId ?? DEFAULT_ACCOUNT_ID
|
|
59
|
-
const raw = (cfg.channels?.[
|
|
65
|
+
const raw = (cfg.channels?.["dcgchat-test"] as DcgchatConfig | undefined) ?? {}
|
|
60
66
|
return {
|
|
61
67
|
accountId: id,
|
|
62
68
|
enabled: raw.enabled !== false,
|
|
@@ -70,13 +76,13 @@ export function resolveAccount(cfg: OpenClawConfig, accountId?: string | null):
|
|
|
70
76
|
}
|
|
71
77
|
|
|
72
78
|
export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
73
|
-
id:
|
|
79
|
+
id: "dcgchat-test",
|
|
74
80
|
meta: {
|
|
75
|
-
id:
|
|
81
|
+
id: "dcgchat-test",
|
|
76
82
|
label: '书灵墨宝',
|
|
77
83
|
selectionLabel: '书灵墨宝',
|
|
78
84
|
docsPath: '/channels/dcgchat',
|
|
79
|
-
docsLabel:
|
|
85
|
+
docsLabel: "dcgchat-test",
|
|
80
86
|
blurb: '连接 OpenClaw 与 书灵墨宝 产品',
|
|
81
87
|
order: 80
|
|
82
88
|
},
|
|
@@ -101,11 +107,17 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
101
107
|
enabled: { type: 'boolean' },
|
|
102
108
|
wsUrl: { type: 'string' },
|
|
103
109
|
botToken: { type: 'string' },
|
|
104
|
-
userId: { type: 'string' },
|
|
110
|
+
userId: { type: 'string', description: 'WebSocket 连接参数 _userId,与 message 工具的 target(effectiveSessionKey)无关' },
|
|
105
111
|
appId: { type: 'string' },
|
|
106
112
|
domainId: { type: 'string' },
|
|
107
113
|
capabilities: { type: 'array', items: { type: 'string' } }
|
|
108
114
|
}
|
|
115
|
+
},
|
|
116
|
+
uiHints: {
|
|
117
|
+
userId: {
|
|
118
|
+
label: 'WS 连接 _userId',
|
|
119
|
+
help: '仅用于拼接网关 WebSocket URL 的查询参数,不是 Agent 发消息时的 target。发消息请使用 effectiveSessionKey(与入站上下文 SessionKey 相同,格式如 agent:main:mobook:direct:…)。'
|
|
120
|
+
}
|
|
109
121
|
}
|
|
110
122
|
},
|
|
111
123
|
config: {
|
|
@@ -117,7 +129,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
117
129
|
channels: {
|
|
118
130
|
...cfg.channels,
|
|
119
131
|
dcgchat: {
|
|
120
|
-
...(cfg.channels?.[
|
|
132
|
+
...(cfg.channels?.["dcgchat-test"] as Record<string, unknown> | undefined),
|
|
121
133
|
enabled
|
|
122
134
|
}
|
|
123
135
|
}
|
|
@@ -135,18 +147,31 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
135
147
|
})
|
|
136
148
|
},
|
|
137
149
|
messaging: {
|
|
138
|
-
normalizeTarget: (raw) => raw
|
|
150
|
+
normalizeTarget: (raw) => normalizeSessionTarget(raw ?? '') || undefined,
|
|
139
151
|
targetResolver: {
|
|
140
152
|
looksLikeId: (raw) => Boolean(raw?.trim()),
|
|
141
|
-
hint: 'userId'
|
|
153
|
+
hint: 'effectiveSessionKey(与 SessionKey 一致;勿填配置里的 WS userId)'
|
|
142
154
|
}
|
|
143
155
|
},
|
|
156
|
+
agentPrompt: {
|
|
157
|
+
messageToolHints: () => [
|
|
158
|
+
'书灵墨宝:message 工具的 target 必须填 effectiveSessionKey(与当前会话 SessionKey / OriginatingTo 相同),形如 agent:main:mobook:direct:<agent_id>:<session_id>;不要填 channels.dcgchat.userId 或纯数字 userId。',
|
|
159
|
+
'OpenClaw 自带的 target 字段说明里仍可能出现 “user id”,在本频道请忽略该字样,一律按 effectiveSessionKey 理解。'
|
|
160
|
+
]
|
|
161
|
+
},
|
|
144
162
|
outbound: {
|
|
145
163
|
deliveryMode: 'direct',
|
|
164
|
+
resolveTarget: ({ to }) => {
|
|
165
|
+
const normalized = normalizeSessionTarget(to ?? '')
|
|
166
|
+
if (!normalized) {
|
|
167
|
+
return { ok: false, error: new Error('target is empty') }
|
|
168
|
+
}
|
|
169
|
+
return { ok: true, to: normalized }
|
|
170
|
+
},
|
|
146
171
|
textChunkLimit: 4000,
|
|
147
172
|
sendText: async (ctx) => {
|
|
148
173
|
const isCron = ctx.to.indexOf('dcg-cron:') >= 0
|
|
149
|
-
const to = ctx.to
|
|
174
|
+
const to = normalizeSessionTarget(ctx.to)
|
|
150
175
|
dcgLogger(`channel sendText to ${ctx.to} `)
|
|
151
176
|
const outboundCtx = getOutboundMsgParams(to)
|
|
152
177
|
const cronMsgId = getCronMessageId(to)
|
|
@@ -170,23 +195,35 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
170
195
|
}
|
|
171
196
|
}
|
|
172
197
|
return {
|
|
173
|
-
channel:
|
|
198
|
+
channel: "dcgchat-test",
|
|
174
199
|
messageId: `${messageId}`,
|
|
175
200
|
chatId: to
|
|
176
201
|
}
|
|
177
202
|
},
|
|
178
203
|
sendMedia: async (ctx) => {
|
|
179
|
-
const to = ctx.to.replace('dcg-cron:', '')
|
|
180
|
-
const msgCtx = getOutboundMsgParams(to)
|
|
181
|
-
const cronMsgId = getCronMessageId(to)
|
|
182
204
|
const isCron = ctx.to.indexOf('dcg-cron:') >= 0
|
|
183
|
-
const
|
|
205
|
+
const to = normalizeSessionTarget(ctx.to)
|
|
206
|
+
const outboundCtx = getOutboundMsgParams(to)
|
|
207
|
+
const msgCtx = getParamsMessage(to) ?? outboundCtx
|
|
208
|
+
const cronMsgId = getCronMessageId(to)
|
|
209
|
+
const fallbackMessageId = `${Date.now()}`
|
|
210
|
+
const messageId = cronMsgId || (isCron ? fallbackMessageId : msgCtx?.messageId || fallbackMessageId)
|
|
211
|
+
|
|
212
|
+
if (!outboundCtx?.sessionId) {
|
|
213
|
+
dcgLogger(`channel sendMedia to ${ctx.to} -> sessionId not found`, 'error')
|
|
214
|
+
return {
|
|
215
|
+
channel: "dcgchat-test",
|
|
216
|
+
messageId,
|
|
217
|
+
chatId: to || ''
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
184
221
|
dcgLogger(`channel sendMedia to ${ctx.to}`)
|
|
185
|
-
await sendDcgchatMedia({ sessionKey: to
|
|
222
|
+
await sendDcgchatMedia({ sessionKey: to || '', mediaUrl: ctx.mediaUrl || '' })
|
|
186
223
|
return {
|
|
187
|
-
channel:
|
|
188
|
-
messageId
|
|
189
|
-
chatId:
|
|
224
|
+
channel: "dcgchat-test",
|
|
225
|
+
messageId,
|
|
226
|
+
chatId: to || ''
|
|
190
227
|
}
|
|
191
228
|
}
|
|
192
229
|
},
|
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/request/api.ts
CHANGED
|
@@ -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('
|
|
43
|
+
dcgLogger('获取绑定的用户信息失败: ' + JSON.stringify(response), 'error')
|
|
44
44
|
return ''
|
|
45
45
|
}
|
|
46
46
|
|
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
|
@@ -47,8 +47,8 @@ export function getEffectiveMsgParams(sessionKey?: string): IMsgParams {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
* Agent `message`
|
|
51
|
-
* `
|
|
50
|
+
* Agent `message` 工具的 `target` 应为 `effectiveSessionKey`(如 `agent:main:mobook:direct:...`)。
|
|
51
|
+
* `setParamsMessage` 使用的 key 与此一致。若按 preferredKey 查不到 map,
|
|
52
52
|
* 则回落到当前会话 `currentSessionKey`,避免拿到空 `messageId` / `sessionId` 导致无文件卡片、WS 上下文错误。
|
|
53
53
|
*/
|
|
54
54
|
export function getOutboundMsgParams(preferredKey: string): IMsgParams {
|
package/src/utils/searchFile.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { getWorkspaceDir } from './global.js'
|
|
2
1
|
import { dcgLogger } from './log.js'
|
|
3
|
-
import fs from 'node:fs'
|
|
4
|
-
import os from 'node:os'
|
|
5
|
-
import path from 'node:path'
|
|
6
2
|
|
|
7
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* 从文本中提取 /mobook 目录下的文件
|
|
5
|
+
* @param {string} text
|
|
6
|
+
* @returns {string[]}
|
|
7
|
+
*/
|
|
8
8
|
const EXT_LIST = [
|
|
9
9
|
// 文档类
|
|
10
10
|
'doc',
|
|
@@ -88,56 +88,11 @@ 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
|
-
/** 文件名片段:中文、常见符号、含 `#`、路径 `/`(多段 /mobook/...)、非贪婪 */
|
|
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
|
-
/** Markdown 列表:`- 文件名.ext` — 用 matchAll 取捕获组 1,避免 FILE_NAME 含 `-`/空格 时误把「- 」并入文件名 */
|
|
105
|
-
markdownList: new RegExp(`[-*•]\\s+(${FILE_NAME_CLASS}\\.${EXT_ALT})`, 'gi'),
|
|
106
|
-
inlineFile: new RegExp(`${FILE_NAME_CLASS}\\.${EXT_ALT}`, 'i')
|
|
107
|
-
}
|
|
108
|
-
|
|
109
91
|
/** 去除控制符、零宽字符等常见脏值 */
|
|
110
92
|
function stripMobookNoise(s: string) {
|
|
111
93
|
return s.replace(/[\u0000-\u001F\u007F\u200B-\u200D\u200E\u200F\uFEFF]/g, '')
|
|
112
94
|
}
|
|
113
95
|
|
|
114
|
-
/**
|
|
115
|
-
* 从路径或 mobook 引用中取出「提到的文件名」:去掉盘符、/mobook、\\mobook\\ 等前缀后取 basename
|
|
116
|
-
*/
|
|
117
|
-
function toMobookReferencedBasename(p: string): string {
|
|
118
|
-
let s = stripMobookNoise(p).trim()
|
|
119
|
-
if (!s) return ''
|
|
120
|
-
s = s.replace(/^(?:[a-zA-Z]:)?[/\\]+mobook[/\\]/i, '')
|
|
121
|
-
s = s.replace(/^\/mobook\//i, '')
|
|
122
|
-
s = s.replace(/\\/g, '/')
|
|
123
|
-
const parts = s.split('/').filter(Boolean)
|
|
124
|
-
return parts.length ? parts[parts.length - 1]! : s
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* 去掉误带入的 Markdown 列表前缀(`- 」「* 」「• 」后须有空格)。
|
|
129
|
-
* loose 等规则里 FILE_NAME 含 `-`/空白,会把「- 西游记_1.png (…」整段捕获成文件名。
|
|
130
|
-
* 不要求空格会误伤真实文件名如 `-report.pdf`。
|
|
131
|
-
*/
|
|
132
|
-
function stripMarkdownListPrefixFromBasename(name: string): string {
|
|
133
|
-
return name.replace(/^[-*•]\s+/, '')
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function addMobookMentionedFile(result: Set<string>, raw: string) {
|
|
137
|
-
const base = stripMarkdownListPrefixFromBasename(toMobookReferencedBasename(raw))
|
|
138
|
-
if (base && isValidFileName(base)) result.add(base)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
96
|
/**
|
|
142
97
|
* 从文本中扫描 `.../mobook/...` 或 `...\mobook\...` 片段,按最长后缀匹配合法扩展名(兜底)
|
|
143
98
|
*/
|
|
@@ -172,10 +127,8 @@ function collectMobookPathsAfterNeedle(text: string, lower: string, needle: stri
|
|
|
172
127
|
}
|
|
173
128
|
const base = raw.slice(0, -(matchedExt.length + 1))
|
|
174
129
|
const fileName = `${base}.${matchedExt}`
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (isValidFileName(leaf)) {
|
|
178
|
-
addMobookMentionedFile(result, fileName)
|
|
130
|
+
if (isValidFileName(fileName)) {
|
|
131
|
+
result.add(normalizePath(`/mobook/${fileName}`))
|
|
179
132
|
}
|
|
180
133
|
from = start + 1
|
|
181
134
|
}
|
|
@@ -187,38 +140,62 @@ function collectMobookPathsByScan(text: string, result: Set<string>): void {
|
|
|
187
140
|
collectMobookPathsAfterNeedle(text, lower, '\\mobook\\', result)
|
|
188
141
|
}
|
|
189
142
|
|
|
190
|
-
/**
|
|
191
|
-
* 从文本中提取提到的 mobook 相关文件名(仅 basename,不含目录)
|
|
192
|
-
* @param text 原始文本
|
|
193
|
-
* @returns 去重后的文件名列表,例如 `['报告.pdf', 'data.xlsx']`
|
|
194
|
-
*/
|
|
195
143
|
export function extractMobookFiles(text = '') {
|
|
196
144
|
if (typeof text !== 'string' || !text.trim()) return []
|
|
197
145
|
// 全角冒号(中文输入常见)→ 半角,便于匹配 c:\mobook\
|
|
198
146
|
text = text.replace(/\uFF1A/g, ':')
|
|
199
147
|
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]+?`
|
|
200
152
|
try {
|
|
201
|
-
|
|
202
|
-
|
|
153
|
+
// 1️⃣ `xxx.xxx`
|
|
154
|
+
const backtickReg = new RegExp(`\`([^\\\`]+?\\.${EXT})\``, 'gi')
|
|
155
|
+
;(text.match(backtickReg) || []).forEach((item) => {
|
|
156
|
+
const name = item.replace(/`/g, '').trim()
|
|
157
|
+
if (isValidFileName(name)) {
|
|
158
|
+
result.add(`/mobook/${name}`)
|
|
159
|
+
}
|
|
203
160
|
})
|
|
204
|
-
|
|
205
|
-
|
|
161
|
+
// 2️⃣ /mobook/xxx.xxx
|
|
162
|
+
const fullPathReg = new RegExp(`/mobook/${FILE_NAME}\\.${EXT}`, 'gi')
|
|
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) => {
|
|
206
169
|
const name = full.replace(/^(?:[a-zA-Z]:)?[/\\\\]mobook[/\\\\]/i, '').trim()
|
|
207
|
-
|
|
170
|
+
if (isValidFileName(name)) {
|
|
171
|
+
result.add(normalizePath(`/mobook/${name}`))
|
|
172
|
+
}
|
|
208
173
|
})
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
174
|
+
// 3️⃣ mobook下的 xxx.xxx
|
|
175
|
+
const inlineReg = new RegExp(`mobook下的\\s*(${FILE_NAME}\\.${EXT})`, 'gi')
|
|
176
|
+
;(text.match(inlineReg) || []).forEach((item) => {
|
|
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
|
+
}
|
|
212
181
|
})
|
|
213
|
-
|
|
214
|
-
|
|
182
|
+
// 🆕 4️⃣ **xxx.xxx**
|
|
183
|
+
const boldReg = new RegExp(`\\*\\*(${FILE_NAME}\\.${EXT})\\*\\*`, 'gi')
|
|
184
|
+
;(text.match(boldReg) || []).forEach((item) => {
|
|
185
|
+
const name = item.replace(/\*\*/g, '').trim()
|
|
186
|
+
if (isValidFileName(name)) {
|
|
187
|
+
result.add(`/mobook/${name}`)
|
|
188
|
+
}
|
|
215
189
|
})
|
|
216
|
-
|
|
217
|
-
|
|
190
|
+
// 🆕 5️⃣ xxx.xxx (123字节)
|
|
191
|
+
const looseReg = new RegExp(`(${FILE_NAME}\\.${EXT})\\s*\\(`, 'gi')
|
|
192
|
+
;(text.match(looseReg) || []).forEach((item) => {
|
|
193
|
+
const name = item.replace(/\s*\(.+$/, '').trim()
|
|
194
|
+
if (isValidFileName(name)) {
|
|
195
|
+
result.add(`/mobook/${name}`)
|
|
196
|
+
}
|
|
218
197
|
})
|
|
219
|
-
|
|
220
|
-
if (m[1]) addMobookMentionedFile(result, m[1].trim())
|
|
221
|
-
}
|
|
198
|
+
// 6️⃣ 兜底:绝对路径等 `.../mobook/<文件名>.<扩展名>` + 最长后缀匹配 + 去脏字符
|
|
222
199
|
collectMobookPathsByScan(text, result)
|
|
223
200
|
} catch (e) {
|
|
224
201
|
dcgLogger(`extractMobookFiles error:${e}`)
|
|
@@ -241,94 +218,11 @@ function isValidFileName(name: string) {
|
|
|
241
218
|
return true
|
|
242
219
|
}
|
|
243
220
|
|
|
244
|
-
/** mobook 下按文件名递归查找:仅在直线路径均失败时调用;有深度/目录数上限,找到即停 */
|
|
245
|
-
const MOBOOK_FIND_MAX_DEPTH = 10
|
|
246
|
-
const MOBOOK_FIND_MAX_DIRS = 2000
|
|
247
|
-
|
|
248
|
-
function findMobookFileByBasename(mobookRoot: string, basename: string): string | undefined {
|
|
249
|
-
if (!basename || basename === '.' || basename === '..') return undefined
|
|
250
|
-
if (!fs.existsSync(mobookRoot)) return undefined
|
|
251
|
-
const caseInsensitive = process.platform === 'win32' || process.platform === 'darwin'
|
|
252
|
-
const want = caseInsensitive ? basename.toLowerCase() : basename
|
|
253
|
-
|
|
254
|
-
const stack: Array<{ dir: string; depth: number }> = [{ dir: mobookRoot, depth: 0 }]
|
|
255
|
-
let dirsVisited = 0
|
|
256
|
-
|
|
257
|
-
while (stack.length > 0 && dirsVisited < MOBOOK_FIND_MAX_DIRS) {
|
|
258
|
-
const { dir, depth } = stack.pop()!
|
|
259
|
-
if (depth > MOBOOK_FIND_MAX_DEPTH) continue
|
|
260
|
-
dirsVisited += 1
|
|
261
|
-
|
|
262
|
-
let entries: fs.Dirent[]
|
|
263
|
-
try {
|
|
264
|
-
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
265
|
-
} catch {
|
|
266
|
-
continue
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const subdirs: string[] = []
|
|
270
|
-
for (const ent of entries) {
|
|
271
|
-
const full = path.join(dir, ent.name)
|
|
272
|
-
if (ent.isFile()) {
|
|
273
|
-
const ok = caseInsensitive ? ent.name.toLowerCase() === want : ent.name === basename
|
|
274
|
-
if (ok) return full
|
|
275
|
-
} else if (ent.isDirectory() && depth < MOBOOK_FIND_MAX_DEPTH) {
|
|
276
|
-
if (ent.name.startsWith('.')) continue
|
|
277
|
-
subdirs.push(full)
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
for (let i = subdirs.length - 1; i >= 0; i--) {
|
|
281
|
-
stack.push({ dir: subdirs[i]!, depth: depth + 1 })
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
return undefined
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/** Windows 默认 `C:\mobook`;类 Unix(含 Linux/macOS)为 `~/mobook`。原先 Linux 返回 undefined,会导致只认工作区 mobook、无法回落到用户目录。 */
|
|
288
|
-
function getMobookRoot(): string {
|
|
289
|
-
if (process.platform === 'win32') return path.join('C:\\', 'mobook')
|
|
290
|
-
return path.join(os.homedir(), 'mobook')
|
|
291
|
-
}
|
|
292
|
-
|
|
293
221
|
/**
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
* 仍可能找不到的情况:
|
|
297
|
-
* - 文件不在候选路径(工作区根、工作区/mobook、用户 mobook)下,或路径写错。
|
|
298
|
-
* - Windows 下 mobook 不在 `C:\\mobook`(仅尝试该盘符下的 mobook + 上述候选)。
|
|
299
|
-
* - 递归查找超出深度/目录数上限(见 MOBOOK_FIND_*)。
|
|
300
|
-
* - 多子目录下存在同名文件:按深度优先先命中哪一个即返回哪一个。
|
|
301
|
-
* - Linux 等大小写敏感文件系统:模型输出的文件名大小写与磁盘不一致会失败。
|
|
222
|
+
* 规范路径(去重用)
|
|
302
223
|
*/
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
candidates.push(path.join(ws, file.replace(/^\//, '')))
|
|
308
|
-
const underMobook = file.replace(/^\/mobook\//i, '').replace(/^mobook[\\/]/i, '')
|
|
309
|
-
const workspaceMobookRoot = path.join(ws, 'mobook')
|
|
310
|
-
const homeMobookRoot = underMobook ? getMobookRoot() : undefined
|
|
311
|
-
|
|
312
|
-
if (underMobook) {
|
|
313
|
-
if (fs.existsSync(workspaceMobookRoot)) {
|
|
314
|
-
candidates.push(path.join(workspaceMobookRoot, underMobook))
|
|
315
|
-
}
|
|
316
|
-
if (homeMobookRoot) {
|
|
317
|
-
candidates.push(path.join(homeMobookRoot, underMobook))
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
const resolved = candidates.find((p) => fs.existsSync(p))
|
|
321
|
-
if (resolved) return resolved
|
|
322
|
-
|
|
323
|
-
if (!underMobook) return undefined
|
|
324
|
-
const base = path.basename(underMobook)
|
|
325
|
-
if (fs.existsSync(workspaceMobookRoot)) {
|
|
326
|
-
const inWorkspace = findMobookFileByBasename(workspaceMobookRoot, base)
|
|
327
|
-
if (inWorkspace) return inWorkspace
|
|
328
|
-
}
|
|
329
|
-
if (homeMobookRoot) {
|
|
330
|
-
const inHome = findMobookFileByBasename(homeMobookRoot, base)
|
|
331
|
-
if (inHome) return inHome
|
|
332
|
-
}
|
|
333
|
-
return undefined
|
|
224
|
+
function normalizePath(path: string) {
|
|
225
|
+
return path
|
|
226
|
+
.replace(/\/+/g, '/') // 多斜杠 → 单斜杠
|
|
227
|
+
.replace(/\/$/, '') // 去掉结尾 /
|
|
334
228
|
}
|
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
|
-
- [ ] 错误消息类型
|