@dcrays/dcgchat-test 0.3.25 → 0.3.27
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/README.md +83 -0
- package/index.ts +2 -1
- package/openclaw.plugin.json +2 -4
- package/package.json +12 -3
- package/src/bot.ts +8 -6
- package/src/channel.ts +14 -13
- package/src/cron.ts +7 -11
- package/src/cronToolCall.ts +3 -3
- package/src/request/request.ts +5 -11
- package/src/tool.ts +3 -11
- package/src/transport.ts +5 -4
- package/src/utils/constant.ts +4 -0
- package/src/utils/global.ts +2 -2
- package/src/utils/log.ts +2 -1
- package/src/utils/params.ts +15 -11
- package/src/utils/searchFile.ts +28 -7
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
- [ ] 错误消息类型
|
package/index.ts
CHANGED
|
@@ -3,11 +3,12 @@ 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'
|
|
6
7
|
import { setOpenClawConfig } from './src/utils/global.js'
|
|
7
8
|
import { startDcgchatGatewaySocket } from './src/gateway/socket.js'
|
|
8
9
|
|
|
9
10
|
const plugin = {
|
|
10
|
-
id:
|
|
11
|
+
id: channelInfo[ENV],
|
|
11
12
|
name: '书灵墨宝',
|
|
12
13
|
description: '连接 OpenClaw 与 书灵墨宝 产品(WebSocket)',
|
|
13
14
|
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.27",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw channel plugin for 书灵墨宝 (WebSocket)",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -16,6 +16,12 @@
|
|
|
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
|
+
},
|
|
19
25
|
"dependencies": {
|
|
20
26
|
"ali-oss": "file:src/libs/ali-oss-6.23.0.tgz",
|
|
21
27
|
"axios": "file:src/libs/axios-1.13.6.tgz",
|
|
@@ -31,15 +37,18 @@
|
|
|
31
37
|
"id": "dcgchat-test",
|
|
32
38
|
"label": "书灵墨宝",
|
|
33
39
|
"selectionLabel": "书灵墨宝",
|
|
34
|
-
"docsPath": "/channels/dcgchat
|
|
40
|
+
"docsPath": "/channels/dcgchat",
|
|
35
41
|
"docsLabel": "dcgchat-test",
|
|
36
42
|
"blurb": "连接 OpenClaw 与 书灵墨宝 产品",
|
|
37
43
|
"order": 80
|
|
38
44
|
},
|
|
39
45
|
"install": {
|
|
40
46
|
"npmSpec": "@dcrays/dcgchat-test",
|
|
41
|
-
"localPath": "extensions/dcgchat
|
|
47
|
+
"localPath": "extensions/dcgchat",
|
|
42
48
|
"defaultChoice": "npm"
|
|
43
49
|
}
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"openclaw": "^2026.3.13"
|
|
44
53
|
}
|
|
45
54
|
}
|
package/src/bot.ts
CHANGED
|
@@ -147,7 +147,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
147
147
|
|
|
148
148
|
const route = core.channel.routing.resolveAgentRoute({
|
|
149
149
|
cfg: config,
|
|
150
|
-
channel:
|
|
150
|
+
channel: channelInfo[ENV],
|
|
151
151
|
accountId: account.accountId,
|
|
152
152
|
peer: { kind: 'direct', id: conversationId }
|
|
153
153
|
})
|
|
@@ -226,13 +226,13 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
226
226
|
ChatType: 'direct',
|
|
227
227
|
SenderName: agentDisplayName,
|
|
228
228
|
SenderId: userId,
|
|
229
|
-
Provider:
|
|
230
|
-
Surface:
|
|
229
|
+
Provider: channelInfo[ENV],
|
|
230
|
+
Surface: channelInfo[ENV],
|
|
231
231
|
MessageSid: msg.content.message_id,
|
|
232
232
|
Timestamp: Date.now(),
|
|
233
233
|
WasMentioned: true,
|
|
234
234
|
CommandAuthorized: true,
|
|
235
|
-
OriginatingChannel:
|
|
235
|
+
OriginatingChannel: channelInfo[ENV],
|
|
236
236
|
OriginatingTo: effectiveSessionKey,
|
|
237
237
|
...mediaPayload
|
|
238
238
|
})
|
|
@@ -244,7 +244,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
244
244
|
const prefixContext = createReplyPrefixContext({
|
|
245
245
|
cfg: config,
|
|
246
246
|
agentId: effectiveAgentId ?? '',
|
|
247
|
-
channel:
|
|
247
|
+
channel: channelInfo[ENV],
|
|
248
248
|
accountId: account.accountId
|
|
249
249
|
})
|
|
250
250
|
|
|
@@ -402,7 +402,9 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
402
402
|
if (sessionStreamSuppressed.has(effectiveSessionKey)) {
|
|
403
403
|
sessionStreamSuppressed.delete(effectiveSessionKey)
|
|
404
404
|
} else {
|
|
405
|
-
|
|
405
|
+
const files = extractMobookFiles(completeText)
|
|
406
|
+
dcgLogger(`检索到文件:${JSON.stringify(files)}`)
|
|
407
|
+
for (const file of files) {
|
|
406
408
|
const resolved = getFilePathByFile(file)
|
|
407
409
|
if (!resolved) continue
|
|
408
410
|
try {
|
package/src/channel.ts
CHANGED
|
@@ -4,19 +4,20 @@ 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'
|
|
7
8
|
import { dcgLogger, setLogger } from './utils/log.js'
|
|
8
|
-
import {
|
|
9
|
+
import { getOutboundMsgParams } from './utils/params.js'
|
|
9
10
|
import { startDcgchatGatewaySocket } from './gateway/socket.js'
|
|
10
11
|
|
|
11
12
|
export type DcgchatMediaSendOptions = {
|
|
12
|
-
/** 与 setParamsMessage / map 一致,用于
|
|
13
|
+
/** 与 setParamsMessage / map 一致,用于 getOutboundMsgParams */
|
|
13
14
|
sessionKey: string
|
|
14
15
|
mediaUrl?: string
|
|
15
16
|
text?: string
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<void> {
|
|
19
|
-
const msgCtx =
|
|
20
|
+
const msgCtx = getOutboundMsgParams(opts.sessionKey ?? '')
|
|
20
21
|
if (!isWsOpen()) {
|
|
21
22
|
dcgLogger(`outbound media skipped -> ws not open: ${opts.mediaUrl ?? ''}`)
|
|
22
23
|
return
|
|
@@ -35,7 +36,7 @@ export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<v
|
|
|
35
36
|
const fileName = mediaUrl?.split(/[\\/]/).pop() || ''
|
|
36
37
|
|
|
37
38
|
try {
|
|
38
|
-
const botToken = msgCtx.botToken ?? getOpenClawConfig()?.channels?.[
|
|
39
|
+
const botToken = msgCtx.botToken ?? getOpenClawConfig()?.channels?.[channelInfo[ENV]]?.botToken ?? ''
|
|
39
40
|
const url = opts.mediaUrl ? await ossUpload(opts.mediaUrl, botToken, 1) : ''
|
|
40
41
|
wsSendRaw(msgCtx, {
|
|
41
42
|
response: opts.text ?? '',
|
|
@@ -55,7 +56,7 @@ export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<v
|
|
|
55
56
|
|
|
56
57
|
export function resolveAccount(cfg: OpenClawConfig, accountId?: string | null): ResolvedDcgchatAccount {
|
|
57
58
|
const id = accountId ?? DEFAULT_ACCOUNT_ID
|
|
58
|
-
const raw = (cfg.channels?.[
|
|
59
|
+
const raw = (cfg.channels?.[channelInfo[ENV]] as DcgchatConfig | undefined) ?? {}
|
|
59
60
|
return {
|
|
60
61
|
accountId: id,
|
|
61
62
|
enabled: raw.enabled !== false,
|
|
@@ -69,13 +70,13 @@ export function resolveAccount(cfg: OpenClawConfig, accountId?: string | null):
|
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
72
|
-
id:
|
|
73
|
+
id: channelInfo[ENV],
|
|
73
74
|
meta: {
|
|
74
|
-
id:
|
|
75
|
+
id: channelInfo[ENV],
|
|
75
76
|
label: '书灵墨宝',
|
|
76
77
|
selectionLabel: '书灵墨宝',
|
|
77
78
|
docsPath: '/channels/dcgchat',
|
|
78
|
-
docsLabel:
|
|
79
|
+
docsLabel: channelInfo[ENV],
|
|
79
80
|
blurb: '连接 OpenClaw 与 书灵墨宝 产品',
|
|
80
81
|
order: 80
|
|
81
82
|
},
|
|
@@ -116,7 +117,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
116
117
|
channels: {
|
|
117
118
|
...cfg.channels,
|
|
118
119
|
dcgchat: {
|
|
119
|
-
...(cfg.channels?.[
|
|
120
|
+
...(cfg.channels?.[channelInfo[ENV]] as Record<string, unknown> | undefined),
|
|
120
121
|
enabled
|
|
121
122
|
}
|
|
122
123
|
}
|
|
@@ -147,7 +148,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
147
148
|
const isCron = ctx.to.indexOf('dcg-cron:') >= 0
|
|
148
149
|
const to = ctx.to.replace('dcg-cron:', '')
|
|
149
150
|
dcgLogger(`channel sendText to ${ctx.to} `)
|
|
150
|
-
const outboundCtx =
|
|
151
|
+
const outboundCtx = getOutboundMsgParams(to)
|
|
151
152
|
const cronMsgId = getCronMessageId(to)
|
|
152
153
|
const messageId = !!cronMsgId ? cronMsgId : isCron ? `${Date.now()}` : outboundCtx?.messageId
|
|
153
154
|
if (isWsOpen()) {
|
|
@@ -169,21 +170,21 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
169
170
|
}
|
|
170
171
|
}
|
|
171
172
|
return {
|
|
172
|
-
channel:
|
|
173
|
+
channel: channelInfo[ENV],
|
|
173
174
|
messageId: `${messageId}`,
|
|
174
175
|
chatId: to
|
|
175
176
|
}
|
|
176
177
|
},
|
|
177
178
|
sendMedia: async (ctx) => {
|
|
178
179
|
const to = ctx.to.replace('dcg-cron:', '')
|
|
179
|
-
const msgCtx =
|
|
180
|
+
const msgCtx = getOutboundMsgParams(to)
|
|
180
181
|
const cronMsgId = getCronMessageId(to)
|
|
181
182
|
const isCron = ctx.to.indexOf('dcg-cron:') >= 0
|
|
182
183
|
const messageId = !!cronMsgId ? cronMsgId : isCron ? `${Date.now()}` : msgCtx?.messageId
|
|
183
184
|
dcgLogger(`channel sendMedia to ${ctx.to}`)
|
|
184
185
|
await sendDcgchatMedia({ sessionKey: to ?? '', mediaUrl: ctx.mediaUrl ?? '' })
|
|
185
186
|
return {
|
|
186
|
-
channel:
|
|
187
|
+
channel: channelInfo[ENV],
|
|
187
188
|
messageId: `${messageId}`,
|
|
188
189
|
chatId: msgCtx.userId?.toString()
|
|
189
190
|
}
|
package/src/cron.ts
CHANGED
|
@@ -2,18 +2,11 @@ import path from 'node:path'
|
|
|
2
2
|
import fs from 'node:fs'
|
|
3
3
|
import type { IMsgParams } from './types.js'
|
|
4
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'
|
|
5
|
+
import { getCronMessageId, getWorkspaceDir, getWsConnection, removeCronMessageId, setCronMessageId } from './utils/global.js'
|
|
13
6
|
import { ossUpload } from './request/oss.js'
|
|
14
7
|
import { dcgLogger } from './utils/log.js'
|
|
15
8
|
import { sendMessageToGateway } from './gateway/socket.js'
|
|
16
|
-
import {
|
|
9
|
+
import { getEffectiveMsgParams } from './utils/params.js'
|
|
17
10
|
|
|
18
11
|
export function getCronJobsPath(): string {
|
|
19
12
|
const workspaceDir = getWorkspaceDir()
|
|
@@ -89,12 +82,15 @@ function flushCronUploadQueue(): void {
|
|
|
89
82
|
* 将 jobs.json 同步到 OSS 并推送事件。30s 内多次调用合并为一次上传;定时触发后清空待处理项,避免重复执行。
|
|
90
83
|
* @param msgCtx 可选;省略时使用当前会话 getEffectiveMsgParams(sessionKey) 快照
|
|
91
84
|
*/
|
|
92
|
-
export function sendDcgchatCron(): void {
|
|
93
|
-
const
|
|
85
|
+
export function sendDcgchatCron(jobId: string): void {
|
|
86
|
+
const jobPath = getCronJobsPath()
|
|
87
|
+
const { sessionKey } = readCronJob(jobPath, jobId) || {}
|
|
88
|
+
const ctx = msgParamsToCtx(getEffectiveMsgParams(sessionKey))
|
|
94
89
|
if (!ctx) {
|
|
95
90
|
dcgLogger('sendDcgchatCron: no message context (missing token / params)', 'error')
|
|
96
91
|
return
|
|
97
92
|
}
|
|
93
|
+
dcgLogger(`sessionKey: ${sessionKey}, jobId: ${jobId}`)
|
|
98
94
|
pendingCronUploadCtx = ctx
|
|
99
95
|
if (cronUploadFlushTimer !== null) {
|
|
100
96
|
clearTimeout(cronUploadFlushTimer)
|
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 = channelInfo[ENV]
|
|
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 = channelInfo[ENV]
|
|
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 ${channelInfo[ENV]} --to dcg-cron:${sk} --json`
|
|
180
180
|
return { params: newParams }
|
|
181
181
|
} else {
|
|
182
182
|
return params
|
package/src/request/request.ts
CHANGED
|
@@ -3,7 +3,7 @@ import axios from 'axios'
|
|
|
3
3
|
import md5 from 'md5'
|
|
4
4
|
import type { IResponse } from '../types.js'
|
|
5
5
|
import { getUserTokenCache } from './userInfo.js'
|
|
6
|
-
import {
|
|
6
|
+
import { getEffectiveMsgParams } from '../utils/params.js'
|
|
7
7
|
import { ENV } from '../utils/constant.js'
|
|
8
8
|
import { dcgLogger } from '../utils/log.js'
|
|
9
9
|
|
|
@@ -39,9 +39,7 @@ function toCurl(config: {
|
|
|
39
39
|
}): string {
|
|
40
40
|
const base = config.baseURL ?? ''
|
|
41
41
|
const path = config.url ?? ''
|
|
42
|
-
const url = path.startsWith('http')
|
|
43
|
-
? path
|
|
44
|
-
: `${base.replace(/\/$/, '')}/${path.replace(/^\//, '')}`
|
|
42
|
+
const url = path.startsWith('http') ? path : `${base.replace(/\/$/, '')}/${path.replace(/^\//, '')}`
|
|
45
43
|
const method = (config.method ?? 'GET').toUpperCase()
|
|
46
44
|
const headers = config.headers ?? {}
|
|
47
45
|
const parts = ['curl', '-X', method, `'${url}'`]
|
|
@@ -82,9 +80,7 @@ export function getSignature(
|
|
|
82
80
|
sortedKeys
|
|
83
81
|
.map((key) => {
|
|
84
82
|
const val = map[key as keyof typeof map]
|
|
85
|
-
return val === undefined
|
|
86
|
-
? ''
|
|
87
|
-
: `${key}${typeof val === 'object' ? JSON.stringify(val) : val}`
|
|
83
|
+
return val === undefined ? '' : `${key}${typeof val === 'object' ? JSON.stringify(val) : val}`
|
|
88
84
|
})
|
|
89
85
|
.join('') + signKey[ENV]
|
|
90
86
|
// 4. MD5 加密并转大写
|
|
@@ -130,9 +126,7 @@ axiosInstance.interceptors.request.use(
|
|
|
130
126
|
if (cachedToken) {
|
|
131
127
|
config.headers = config.headers || {}
|
|
132
128
|
config.headers.authorization = cachedToken
|
|
133
|
-
dcgLogger(
|
|
134
|
-
`[request] auto-injected userToken from cache for botToken=${botToken.slice(0, 10)}...`
|
|
135
|
-
)
|
|
129
|
+
dcgLogger(`[request] auto-injected userToken from cache for botToken=${botToken.slice(0, 10)}...`)
|
|
136
130
|
}
|
|
137
131
|
}
|
|
138
132
|
|
|
@@ -172,7 +166,7 @@ export function post<T = Record<string, unknown>, R = unknown>(
|
|
|
172
166
|
botToken?: string
|
|
173
167
|
}
|
|
174
168
|
): Promise<IResponse<R>> {
|
|
175
|
-
const params = getEffectiveMsgParams(
|
|
169
|
+
const params = getEffectiveMsgParams() || { appId: 100 }
|
|
176
170
|
const config: any = {
|
|
177
171
|
method: 'POST',
|
|
178
172
|
url,
|
package/src/tool.ts
CHANGED
|
@@ -2,12 +2,9 @@ import type { OpenClawPluginApi } from 'openclaw/plugin-sdk'
|
|
|
2
2
|
import { getMsgStatus } from './utils/global.js'
|
|
3
3
|
import { dcgLogger } from './utils/log.js'
|
|
4
4
|
import { sendFinal, sendText, wsSendRaw } from './transport.js'
|
|
5
|
-
import {
|
|
6
|
-
import { channelInfo, ENV } from './utils/constant.js'
|
|
5
|
+
import { getEffectiveMsgParams } from './utils/params.js'
|
|
7
6
|
import { cronToolCall } from './cronToolCall.js'
|
|
8
7
|
|
|
9
|
-
let toolCallId = ''
|
|
10
|
-
let toolName = ''
|
|
11
8
|
type PluginHookName =
|
|
12
9
|
| 'before_model_resolve'
|
|
13
10
|
| 'before_prompt_build'
|
|
@@ -56,13 +53,8 @@ const eventList = [
|
|
|
56
53
|
|
|
57
54
|
function sendToolCallMessage(sk: string, text: string, toolCallId: string, isCover: number) {
|
|
58
55
|
const params = getEffectiveMsgParams(sk)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
tool_call_id: toolCallId,
|
|
62
|
-
is_cover: isCover,
|
|
63
|
-
thinking_content: text,
|
|
64
|
-
response: ''
|
|
65
|
-
})
|
|
56
|
+
const content = { is_finish: -1, tool_call_id: toolCallId, is_cover: isCover, thinking_content: text, response: '' }
|
|
57
|
+
wsSendRaw(params, content, false)
|
|
66
58
|
}
|
|
67
59
|
|
|
68
60
|
/**
|
package/src/transport.ts
CHANGED
|
@@ -150,12 +150,13 @@ export function wsSend(ctx: IMsgParams, content: Record<string, unknown>): boole
|
|
|
150
150
|
* 媒体 / channel 出站:content 保持嵌套对象(单次编码)。
|
|
151
151
|
* `ctx` 须由调用方解析(如需合并覆盖可先 mergeSessionParams)。
|
|
152
152
|
*/
|
|
153
|
-
export function wsSendRaw(ctx: IMsgParams, content: Record<string, unknown
|
|
153
|
+
export function wsSendRaw(ctx: IMsgParams, content: Record<string, unknown>, isLog = true): boolean {
|
|
154
154
|
const ws = getWsConnection()
|
|
155
155
|
if (isWsOpen()) {
|
|
156
156
|
ws?.send(JSON.stringify(buildOpenclawBotChat(ctx, content, { mergeChannelDefaults: true })))
|
|
157
|
-
|
|
158
|
-
|
|
157
|
+
if (isLog) {
|
|
158
|
+
dcgLogger('已发送:' + JSON.stringify(buildOpenclawBotChat(ctx, content, { mergeChannelDefaults: true })))
|
|
159
|
+
}
|
|
159
160
|
}
|
|
160
161
|
return true
|
|
161
162
|
}
|
|
@@ -195,7 +196,7 @@ export function sendEventMessage(url: string, sessionKey: string) {
|
|
|
195
196
|
bot_id: ctx.botId,
|
|
196
197
|
agent_id: ctx.agentId,
|
|
197
198
|
session_id: ctx.sessionId,
|
|
198
|
-
message_id:
|
|
199
|
+
message_id: Date.now().toString()
|
|
199
200
|
}
|
|
200
201
|
})
|
|
201
202
|
)
|
package/src/utils/constant.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
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
|
+
}
|
|
3
7
|
|
|
4
8
|
export const systemCommand = ['/new', '/status']
|
|
5
9
|
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?.[channelInfo[ENV]]?.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: channelInfo[ENV],
|
|
127
127
|
accountId: accountId || 'default',
|
|
128
128
|
peer: { kind: 'direct', id: session_id }
|
|
129
129
|
})
|
package/src/utils/log.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RuntimeEnv } from 'openclaw/plugin-sdk'
|
|
2
|
+
import { channelInfo, ENV } from './constant.js'
|
|
2
3
|
|
|
3
4
|
let logger: RuntimeEnv | null = null
|
|
4
5
|
|
|
@@ -10,6 +11,6 @@ export function dcgLogger(message: string, type: 'log' | 'error' = 'log'): void
|
|
|
10
11
|
if (logger) {
|
|
11
12
|
logger[type](`书灵墨宝🚀 ~ [${new Date().toISOString()}] ${message}`)
|
|
12
13
|
} else {
|
|
13
|
-
console[type](`书灵墨宝🚀 ~ ${new Date().toISOString()} [${
|
|
14
|
+
console[type](`书灵墨宝🚀 ~ ${new Date().toISOString()} [${channelInfo[ENV]}]: ${message}`)
|
|
14
15
|
}
|
|
15
16
|
}
|
package/src/utils/params.ts
CHANGED
|
@@ -7,12 +7,9 @@ import type { DcgchatConfig, IMsgParams } from '../types.js'
|
|
|
7
7
|
*/
|
|
8
8
|
const paramsMessageMap = new Map<string, IMsgParams>()
|
|
9
9
|
|
|
10
|
-
/** 最近一次 setParamsMessage 的 key,供不传参的 getEffectiveMsgParams() 使用 */
|
|
11
|
-
let currentSessionKey: string | null = null
|
|
12
|
-
|
|
13
10
|
/** 从 OpenClaw 配置读取当前 channel 的基础参数(唯一来源,供 transport / resolve 等复用) */
|
|
14
11
|
export function getParamsDefaults(): IMsgParams {
|
|
15
|
-
const ch = (getOpenClawConfig()?.channels?.[
|
|
12
|
+
const ch = (getOpenClawConfig()?.channels?.['dcgchat'] as DcgchatConfig | undefined) ?? {}
|
|
16
13
|
return {
|
|
17
14
|
userId: Number(ch.userId ?? 0),
|
|
18
15
|
botToken: ch.botToken ?? '',
|
|
@@ -45,14 +42,25 @@ export function resolveParamsMessage(params: Partial<IMsgParams>): IMsgParams {
|
|
|
45
42
|
* 统一取值入口:显式 sessionKey,或回落到当前会话;再与配置缺省 merge,保证字段完整。
|
|
46
43
|
*/
|
|
47
44
|
export function getEffectiveMsgParams(sessionKey?: string): IMsgParams {
|
|
48
|
-
const
|
|
49
|
-
const stored = key ? paramsMessageMap.get(key) : undefined
|
|
45
|
+
const stored = sessionKey ? paramsMessageMap.get(sessionKey) : undefined
|
|
50
46
|
return stored ? resolveParamsMessage(stored) : getParamsDefaults()
|
|
51
47
|
}
|
|
52
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Agent `message` 工具常把 `target` 设为用户 ID(如 "150"),而 `setParamsMessage` 使用的 key 是
|
|
51
|
+
* `effectiveSessionKey`(如 `agent:main:mobook:direct:...`)。若按 preferredKey 查不到 map,
|
|
52
|
+
* 则回落到当前会话 `currentSessionKey`,避免拿到空 `messageId` / `sessionId` 导致无文件卡片、WS 上下文错误。
|
|
53
|
+
*/
|
|
54
|
+
export function getOutboundMsgParams(preferredKey: string): IMsgParams {
|
|
55
|
+
const k = preferredKey?.trim()
|
|
56
|
+
if (k && paramsMessageMap.has(k)) {
|
|
57
|
+
return getEffectiveMsgParams(k)
|
|
58
|
+
}
|
|
59
|
+
return getEffectiveMsgParams()
|
|
60
|
+
}
|
|
61
|
+
|
|
53
62
|
export function setParamsMessage(sessionKey: string, params: Partial<IMsgParams>) {
|
|
54
63
|
if (!sessionKey) return
|
|
55
|
-
currentSessionKey = sessionKey
|
|
56
64
|
const previous = paramsMessageMap.get(sessionKey)
|
|
57
65
|
const base = previous ? resolveParamsMessage(previous) : getParamsDefaults()
|
|
58
66
|
paramsMessageMap.set(sessionKey, resolveParamsMessage({ ...base, ...params, sessionKey }))
|
|
@@ -61,7 +69,3 @@ export function setParamsMessage(sessionKey: string, params: Partial<IMsgParams>
|
|
|
61
69
|
export function getParamsMessage(sessionKey: string): IMsgParams | undefined {
|
|
62
70
|
return paramsMessageMap.get(sessionKey)
|
|
63
71
|
}
|
|
64
|
-
|
|
65
|
-
export function getCurrentSessionKey(): string | null {
|
|
66
|
-
return currentSessionKey
|
|
67
|
-
}
|
package/src/utils/searchFile.ts
CHANGED
|
@@ -90,8 +90,8 @@ const EXT_SORTED_FOR_REGEX = [...EXT_LIST].sort((a, b) => b.length - a.length)
|
|
|
90
90
|
|
|
91
91
|
/** 正则交替串(长扩展名优先) */
|
|
92
92
|
const EXT_ALT = `(${EXT_SORTED_FOR_REGEX.join('|')})`
|
|
93
|
-
/**
|
|
94
|
-
const FILE_NAME_CLASS = `[\\w\\u4e00-\\u9fa5::《》()()\\-\\s]+?`
|
|
93
|
+
/** 文件名片段:中文、常见符号、含 `#`、路径 `/`(多段 /mobook/...)、非贪婪 */
|
|
94
|
+
const FILE_NAME_CLASS = `[\\w\\u4e00-\\u9fa5::《》()()\\-\\s#/]+?`
|
|
95
95
|
|
|
96
96
|
/** 预编译,避免 extractMobookFiles 每次调用重复构建正则 */
|
|
97
97
|
const RX_EXTRACT = {
|
|
@@ -124,8 +124,17 @@ function toMobookReferencedBasename(p: string): string {
|
|
|
124
124
|
return parts.length ? parts[parts.length - 1]! : s
|
|
125
125
|
}
|
|
126
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
|
+
|
|
127
136
|
function addMobookMentionedFile(result: Set<string>, raw: string) {
|
|
128
|
-
const base = toMobookReferencedBasename(raw)
|
|
137
|
+
const base = stripMarkdownListPrefixFromBasename(toMobookReferencedBasename(raw))
|
|
129
138
|
if (base && isValidFileName(base)) result.add(base)
|
|
130
139
|
}
|
|
131
140
|
|
|
@@ -163,7 +172,9 @@ function collectMobookPathsAfterNeedle(text: string, lower: string, needle: stri
|
|
|
163
172
|
}
|
|
164
173
|
const base = raw.slice(0, -(matchedExt.length + 1))
|
|
165
174
|
const fileName = `${base}.${matchedExt}`
|
|
166
|
-
|
|
175
|
+
// 可能是 mobook 下多级路径;整段含 / 会被 isValidFileName 误判为非法,只校验最后一段(与 addMobookMentionedFile 最终取的 basename 一致)
|
|
176
|
+
const leaf = path.basename(fileName.replace(/\\/g, '/'))
|
|
177
|
+
if (isValidFileName(leaf)) {
|
|
167
178
|
addMobookMentionedFile(result, fileName)
|
|
168
179
|
}
|
|
169
180
|
from = start + 1
|
|
@@ -273,12 +284,22 @@ function findMobookFileByBasename(mobookRoot: string, basename: string): string
|
|
|
273
284
|
return undefined
|
|
274
285
|
}
|
|
275
286
|
|
|
276
|
-
|
|
287
|
+
/** Windows 默认 `C:\mobook`;类 Unix(含 Linux/macOS)为 `~/mobook`。原先 Linux 返回 undefined,会导致只认工作区 mobook、无法回落到用户目录。 */
|
|
288
|
+
function getMobookRoot(): string {
|
|
277
289
|
if (process.platform === 'win32') return path.join('C:\\', 'mobook')
|
|
278
|
-
|
|
279
|
-
return undefined
|
|
290
|
+
return path.join(os.homedir(), 'mobook')
|
|
280
291
|
}
|
|
281
292
|
|
|
293
|
+
/**
|
|
294
|
+
* 按「文件名或 /mobook/ 相对路径」解析磁盘上的真实路径。
|
|
295
|
+
*
|
|
296
|
+
* 仍可能找不到的情况:
|
|
297
|
+
* - 文件不在候选路径(工作区根、工作区/mobook、用户 mobook)下,或路径写错。
|
|
298
|
+
* - Windows 下 mobook 不在 `C:\\mobook`(仅尝试该盘符下的 mobook + 上述候选)。
|
|
299
|
+
* - 递归查找超出深度/目录数上限(见 MOBOOK_FIND_*)。
|
|
300
|
+
* - 多子目录下存在同名文件:按深度优先先命中哪一个即返回哪一个。
|
|
301
|
+
* - Linux 等大小写敏感文件系统:模型输出的文件名大小写与磁盘不一致会失败。
|
|
302
|
+
*/
|
|
282
303
|
export function getFilePathByFile(file: string) {
|
|
283
304
|
const ws = getWorkspaceDir()
|
|
284
305
|
const candidates: string[] = [file]
|