@dcrays/dcgchat-test 0.4.23 → 0.4.26
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 +13 -3
- package/src/bot.ts +13 -10
- package/src/channel.ts +100 -22
- package/src/cronToolCall.ts +5 -33
- package/src/tool.ts +4 -10
- package/src/utils/constant.ts +4 -0
- package/src/utils/global.ts +3 -3
- package/src/utils/log.ts +2 -1
- package/src/utils/params.ts +1 -1
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 { createDcgchatMessageTool } from './src/tools/messageTool.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.4.
|
|
3
|
+
"version": "0.4.26",
|
|
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,19 @@
|
|
|
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",
|
|
53
|
+
"typescript": "~5.8.0"
|
|
44
54
|
}
|
|
45
55
|
}
|
package/src/bot.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
getWorkspaceDir,
|
|
12
12
|
setMsgStatus
|
|
13
13
|
} from './utils/global.js'
|
|
14
|
-
import { resolveAccount, sendDcgchatMedia } from './channel.js'
|
|
14
|
+
import { normalizeOutboundMediaPaths, resolveAccount, sendDcgchatMedia } from './channel.js'
|
|
15
15
|
import { generateSignUrl } from './request/api.js'
|
|
16
16
|
import { sendChunk, sendFinal, sendText as sendTextMsg, sendError, wsSendRaw, sendText } from './transport.js'
|
|
17
17
|
import { dcgLogger } from './utils/log.js'
|
|
@@ -143,8 +143,11 @@ function buildMediaPayload(mediaList: MediaInfo[]): MediaPayload {
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
function resolveReplyMediaList(payload: ReplyPayload): string[] {
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
const p = payload as { mediaUrls?: unknown[]; mediaUrl?: unknown }
|
|
147
|
+
if (p.mediaUrls != null && Array.isArray(p.mediaUrls) && p.mediaUrls.length > 0) {
|
|
148
|
+
return normalizeOutboundMediaPaths(p.mediaUrls)
|
|
149
|
+
}
|
|
150
|
+
return normalizeOutboundMediaPaths(p.mediaUrl ?? null)
|
|
148
151
|
}
|
|
149
152
|
|
|
150
153
|
const typingCallbacks = createTypingCallbacks({
|
|
@@ -187,7 +190,7 @@ async function handleDcgchatMessageInboundTurn(msg: InboundMessage, accountId: s
|
|
|
187
190
|
|
|
188
191
|
const route = core.channel.routing.resolveAgentRoute({
|
|
189
192
|
cfg: config,
|
|
190
|
-
channel:
|
|
193
|
+
channel: channelInfo[ENV],
|
|
191
194
|
accountId: account.accountId,
|
|
192
195
|
peer: { kind: 'direct', id: conversationId }
|
|
193
196
|
})
|
|
@@ -204,7 +207,7 @@ async function handleDcgchatMessageInboundTurn(msg: InboundMessage, accountId: s
|
|
|
204
207
|
sessionId: conversationId,
|
|
205
208
|
messageId: msg.content.message_id,
|
|
206
209
|
domainId: msg.content.domain_id,
|
|
207
|
-
appId: config.channels?.[
|
|
210
|
+
appId: config.channels?.[channelInfo[ENV]]?.appId || 100,
|
|
208
211
|
botId: msg.content.bot_id ?? '',
|
|
209
212
|
agentId: msg.content.agent_id ?? '',
|
|
210
213
|
sessionKey: dcgSessionKey,
|
|
@@ -266,13 +269,13 @@ async function handleDcgchatMessageInboundTurn(msg: InboundMessage, accountId: s
|
|
|
266
269
|
ChatType: 'direct',
|
|
267
270
|
SenderName: agentDisplayName,
|
|
268
271
|
SenderId: userId,
|
|
269
|
-
Provider:
|
|
270
|
-
Surface:
|
|
272
|
+
Provider: channelInfo[ENV],
|
|
273
|
+
Surface: channelInfo[ENV],
|
|
271
274
|
MessageSid: msg.content.message_id,
|
|
272
275
|
Timestamp: Date.now(),
|
|
273
276
|
WasMentioned: true,
|
|
274
277
|
CommandAuthorized: true,
|
|
275
|
-
OriginatingChannel:
|
|
278
|
+
OriginatingChannel: channelInfo[ENV],
|
|
276
279
|
OriginatingTo: dcgSessionKey,
|
|
277
280
|
Target: dcgSessionKey,
|
|
278
281
|
SourceTarget: dcgSessionKey,
|
|
@@ -295,7 +298,7 @@ async function handleDcgchatMessageInboundTurn(msg: InboundMessage, accountId: s
|
|
|
295
298
|
const prefixContext = createReplyPrefixContext({
|
|
296
299
|
cfg: config,
|
|
297
300
|
agentId: effectiveAgentId ?? '',
|
|
298
|
-
channel:
|
|
301
|
+
channel: channelInfo[ENV],
|
|
299
302
|
accountId: account.accountId
|
|
300
303
|
})
|
|
301
304
|
|
|
@@ -476,7 +479,7 @@ async function handleDcgchatMessageInboundTurn(msg: InboundMessage, accountId: s
|
|
|
476
479
|
ctx: ctxPayload,
|
|
477
480
|
updateLastRoute: {
|
|
478
481
|
sessionKey: dcgSessionKey,
|
|
479
|
-
channel:
|
|
482
|
+
channel: channelInfo[ENV],
|
|
480
483
|
to: dcgSessionKey,
|
|
481
484
|
accountId: route.accountId
|
|
482
485
|
},
|
package/src/channel.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
1
2
|
import type { ChannelPlugin, OpenClawConfig, PluginRuntime } from 'openclaw/plugin-sdk'
|
|
2
3
|
import { createPluginRuntimeStore, DEFAULT_ACCOUNT_ID } from 'openclaw/plugin-sdk'
|
|
3
4
|
import type { ResolvedDcgchatAccount, DcgchatConfig } from './types.js'
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
setCronMessageId
|
|
13
14
|
} from './utils/global.js'
|
|
14
15
|
import { isWsOpen, mergeDefaultParams, mergeSessionParams, sendFinal, wsSendRaw } from './transport.js'
|
|
16
|
+
import { channelInfo, ENV } from './utils/constant.js'
|
|
15
17
|
import { dcgLogger, setLogger } from './utils/log.js'
|
|
16
18
|
import { getOutboundMsgParams, getParamsMessage } from './utils/params.js'
|
|
17
19
|
import { isSessionActiveForTool } from './tool.js'
|
|
@@ -19,7 +21,7 @@ import { startDcgchatGatewaySocket } from './gateway/socket.js'
|
|
|
19
21
|
import { getCronJobsPath, readCronJob } from './cron.js'
|
|
20
22
|
|
|
21
23
|
function dcgchatChannelCfg(): DcgchatConfig {
|
|
22
|
-
return (getOpenClawConfig()?.channels?.[
|
|
24
|
+
return (getOpenClawConfig()?.channels?.[channelInfo[ENV]] as DcgchatConfig | undefined) ?? {}
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
/** `agent:<code>:mobook:direct:<agentId>:<sessionId>`(与 getSessionKey 非 real_mobook 分支一致) */
|
|
@@ -103,6 +105,53 @@ function outboundChatId(rawTo: string | undefined, normalizedTo: string): string
|
|
|
103
105
|
return raw.indexOf('dcg-cron:') >= 0 ? raw : normalizedTo
|
|
104
106
|
}
|
|
105
107
|
|
|
108
|
+
/**
|
|
109
|
+
* 仅从 JSON / `{ file | path | url }` 等结构里取出路径字符串,不做改写(不拼 workspace、不 normalize)。
|
|
110
|
+
*/
|
|
111
|
+
function collectOutboundMediaPaths(item: unknown, out: string[]): void {
|
|
112
|
+
if (item == null) return
|
|
113
|
+
if (typeof item === 'string') {
|
|
114
|
+
const t = item.trim()
|
|
115
|
+
if (!t) return
|
|
116
|
+
if (t.startsWith('[')) {
|
|
117
|
+
try {
|
|
118
|
+
const parsed = JSON.parse(t) as unknown
|
|
119
|
+
collectOutboundMediaPaths(parsed, out)
|
|
120
|
+
return
|
|
121
|
+
} catch {
|
|
122
|
+
/* 非 JSON,按普通路径处理 */
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
out.push(t)
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
if (Array.isArray(item)) {
|
|
129
|
+
for (const el of item) collectOutboundMediaPaths(el, out)
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
if (typeof item === 'object') {
|
|
133
|
+
const o = item as Record<string, unknown>
|
|
134
|
+
const raw = o.file ?? o.path ?? o.url
|
|
135
|
+
if (typeof raw === 'string' && raw.trim()) {
|
|
136
|
+
out.push(raw.trim())
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 将出站 media 展平为路径字符串列表(去重保序;路径保持 Core 原样) */
|
|
142
|
+
export function normalizeOutboundMediaPaths(raw: unknown): string[] {
|
|
143
|
+
const acc: string[] = []
|
|
144
|
+
collectOutboundMediaPaths(raw, acc)
|
|
145
|
+
const seen = new Set<string>()
|
|
146
|
+
const deduped: string[] = []
|
|
147
|
+
for (const p of acc) {
|
|
148
|
+
if (!p || seen.has(p)) continue
|
|
149
|
+
seen.add(p)
|
|
150
|
+
deduped.push(p)
|
|
151
|
+
}
|
|
152
|
+
return deduped
|
|
153
|
+
}
|
|
154
|
+
|
|
106
155
|
export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<void> {
|
|
107
156
|
const rawOpt = (opts.sessionKey ?? '').trim()
|
|
108
157
|
const strippedForCron = rawOpt.replace(/^dcg-cron:/i, '').trim()
|
|
@@ -129,7 +178,34 @@ export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<v
|
|
|
129
178
|
return
|
|
130
179
|
}
|
|
131
180
|
|
|
132
|
-
const
|
|
181
|
+
const expanded = normalizeOutboundMediaPaths(opts.mediaUrl)
|
|
182
|
+
if (expanded.length === 0) {
|
|
183
|
+
dcgLogger(
|
|
184
|
+
`dcgchat: sendMedia skipped (no resolvable path): ${typeof opts.mediaUrl === 'string' ? opts.mediaUrl : JSON.stringify(opts.mediaUrl)} sessionKey=${sessionKey}`,
|
|
185
|
+
'error'
|
|
186
|
+
)
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
if (expanded.length > 1) {
|
|
190
|
+
for (const single of expanded) {
|
|
191
|
+
await sendDcgchatMedia({ ...opts, mediaUrl: single })
|
|
192
|
+
}
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
const mediaUrl = expanded[0]
|
|
196
|
+
if (!mediaUrl || !msgCtx.sessionId) {
|
|
197
|
+
dcgLogger(`dcgchat: sendMedia skipped (duplicate in session): ${mediaUrl} sessionId=${msgCtx.sessionId} sessionKey=${sessionKey}`)
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
// 判断文件存在
|
|
201
|
+
try {
|
|
202
|
+
if (!fs.existsSync(mediaUrl)) {
|
|
203
|
+
dcgLogger(`dcgchat: sendMedia skipped (file not found): ${mediaUrl} sessionKey=${sessionKey}`, 'error')
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
} catch (err) {
|
|
207
|
+
dcgLogger(`dcgchat: sendMedia skipped (cannot stat path): ${mediaUrl} ${String(err)} sessionKey=${sessionKey}`, 'error')
|
|
208
|
+
}
|
|
133
209
|
|
|
134
210
|
if (mediaUrl && msgCtx.sessionId) {
|
|
135
211
|
if (hasSentMediaKey(msgCtx.sessionId, mediaUrl)) {
|
|
@@ -143,14 +219,10 @@ export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<v
|
|
|
143
219
|
if (!msgCtx.sessionId) {
|
|
144
220
|
msgCtx.sessionId = sessionId
|
|
145
221
|
}
|
|
146
|
-
if (!mediaUrl || !msgCtx.sessionId) {
|
|
147
|
-
dcgLogger(`dcgchat: sendMedia skipped (duplicate in session): ${mediaUrl} sessionId=${msgCtx.sessionId} sessionKey=${sessionKey}`)
|
|
148
|
-
return
|
|
149
|
-
}
|
|
150
222
|
const fileName = mediaUrl?.split(/[\\/]/).pop() || ''
|
|
151
223
|
const notMessageId = `${msgCtx?.messageId}`?.length === 13 || !msgCtx?.messageId
|
|
152
224
|
try {
|
|
153
|
-
const botToken = msgCtx.botToken ?? getOpenClawConfig()?.channels?.[
|
|
225
|
+
const botToken = msgCtx.botToken ?? getOpenClawConfig()?.channels?.[channelInfo[ENV]]?.botToken ?? ''
|
|
154
226
|
const url = opts.mediaUrl ? await ossUpload(opts.mediaUrl, botToken, 1) : ''
|
|
155
227
|
if (!msgCtx.agentId) {
|
|
156
228
|
msgCtx.agentId = agentId
|
|
@@ -175,7 +247,7 @@ export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<v
|
|
|
175
247
|
|
|
176
248
|
export function resolveAccount(cfg: OpenClawConfig, accountId?: string | null): ResolvedDcgchatAccount {
|
|
177
249
|
const id = accountId ?? DEFAULT_ACCOUNT_ID
|
|
178
|
-
const raw = (cfg.channels?.[
|
|
250
|
+
const raw = (cfg.channels?.[channelInfo[ENV]] as DcgchatConfig | undefined) ?? {}
|
|
179
251
|
return {
|
|
180
252
|
accountId: id,
|
|
181
253
|
enabled: raw.enabled !== false,
|
|
@@ -189,13 +261,13 @@ export function resolveAccount(cfg: OpenClawConfig, accountId?: string | null):
|
|
|
189
261
|
}
|
|
190
262
|
|
|
191
263
|
export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
192
|
-
id:
|
|
264
|
+
id: channelInfo[ENV],
|
|
193
265
|
meta: {
|
|
194
|
-
id:
|
|
266
|
+
id: channelInfo[ENV],
|
|
195
267
|
label: '书灵墨宝',
|
|
196
268
|
selectionLabel: '书灵墨宝',
|
|
197
269
|
docsPath: '/channels/dcgchat',
|
|
198
|
-
docsLabel:
|
|
270
|
+
docsLabel: channelInfo[ENV],
|
|
199
271
|
blurb: '连接 OpenClaw 与 书灵墨宝 产品',
|
|
200
272
|
order: 80
|
|
201
273
|
},
|
|
@@ -212,7 +284,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
212
284
|
// blockStreaming: true,
|
|
213
285
|
},
|
|
214
286
|
/** 当前构建的 channel id + 兼容旧配置键 `channels.dcgchat` */
|
|
215
|
-
reload: { configPrefixes: [`channels.${
|
|
287
|
+
reload: { configPrefixes: [`channels.${channelInfo[ENV]}`, 'channels.dcgchat'] },
|
|
216
288
|
configSchema: {
|
|
217
289
|
schema: {
|
|
218
290
|
type: 'object',
|
|
@@ -248,7 +320,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
248
320
|
resolveAccount: (cfg, accountId) => resolveAccount(cfg, accountId),
|
|
249
321
|
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
250
322
|
setAccountEnabled: ({ cfg, enabled }) => {
|
|
251
|
-
const channelKey =
|
|
323
|
+
const channelKey = channelInfo[ENV]
|
|
252
324
|
const prev = (cfg.channels?.[channelKey as keyof NonNullable<typeof cfg.channels>] as Record<string, unknown> | undefined) ?? {}
|
|
253
325
|
return {
|
|
254
326
|
...cfg,
|
|
@@ -320,7 +392,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
320
392
|
if (!isSessionActiveForTool(to)) {
|
|
321
393
|
dcgLogger(`channel sendText dropped (session not active): to=${to}`)
|
|
322
394
|
return {
|
|
323
|
-
channel:
|
|
395
|
+
channel: channelInfo[ENV],
|
|
324
396
|
messageId: '',
|
|
325
397
|
chatId: outboundChatId(ctx.to, to)
|
|
326
398
|
}
|
|
@@ -332,7 +404,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
332
404
|
}
|
|
333
405
|
}
|
|
334
406
|
return {
|
|
335
|
-
channel:
|
|
407
|
+
channel: channelInfo[ENV],
|
|
336
408
|
messageId: `${messageId}`,
|
|
337
409
|
chatId: outboundChatId(ctx.to, to)
|
|
338
410
|
}
|
|
@@ -353,20 +425,26 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
353
425
|
if (!sessionId) {
|
|
354
426
|
dcgLogger(`channel sendMedia to ${ctx.to} -> sessionId not found`, 'error')
|
|
355
427
|
return {
|
|
356
|
-
channel:
|
|
428
|
+
channel: channelInfo[ENV],
|
|
357
429
|
messageId,
|
|
358
430
|
chatId: outboundChatId(ctx.to, to || '')
|
|
359
431
|
}
|
|
360
432
|
}
|
|
361
433
|
|
|
362
434
|
dcgLogger(`channel sendMedia to ${ctx.to}`)
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
435
|
+
|
|
436
|
+
const ctxExt = ctx as { mediaUrls?: unknown; mediaUrl?: string }
|
|
437
|
+
const rawMedia = ctxExt.mediaUrls ?? ctxExt.mediaUrl
|
|
438
|
+
const paths = normalizeOutboundMediaPaths(rawMedia)
|
|
439
|
+
for (const mediaUrl of paths) {
|
|
440
|
+
await sendDcgchatMedia({
|
|
441
|
+
sessionKey: to || '',
|
|
442
|
+
mediaUrl,
|
|
443
|
+
...(isCron ? { messageId } : {})
|
|
444
|
+
})
|
|
445
|
+
}
|
|
368
446
|
return {
|
|
369
|
-
channel:
|
|
447
|
+
channel: channelInfo[ENV],
|
|
370
448
|
messageId,
|
|
371
449
|
chatId: outboundChatId(ctx.to, to || '')
|
|
372
450
|
}
|
package/src/cronToolCall.ts
CHANGED
|
@@ -7,9 +7,6 @@ import { channelInfo, ENV } from './utils/constant.js'
|
|
|
7
7
|
* 自动注入 bestEffort: true,使投递失败时静默降级,
|
|
8
8
|
* 不影响 cron 执行结果的保存。
|
|
9
9
|
*
|
|
10
|
-
* 另:拦截高危 exec,避免对话内 `openclaw gateway start` 向已运行网关发 SIGTERM、
|
|
11
|
-
* 以及反复 `openclaw cron add` 阻塞与握手超时——应改用内置 `cron` 工具或 Gateway RPC。
|
|
12
|
-
*
|
|
13
10
|
* 背景:
|
|
14
11
|
* - 定时任务的 delivery 设为 announce 模式,如果没有指定 channel,
|
|
15
12
|
* 投递可能因找不到有效渠道而失败
|
|
@@ -82,23 +79,6 @@ function isCronTool(toolName: string): boolean {
|
|
|
82
79
|
return toolName === 'cron'
|
|
83
80
|
}
|
|
84
81
|
|
|
85
|
-
/** 非 running 会话(如定时触发)也需跑本钩子的 exec 子串 */
|
|
86
|
-
export function execCommandNeedsCronToolHook(command: string): boolean {
|
|
87
|
-
const c = command.trim()
|
|
88
|
-
if (!c) return false
|
|
89
|
-
if (c.includes('cron create') || c.includes('cron add')) return true
|
|
90
|
-
return /\bopenclaw\s+gateway\s+start\b/i.test(c)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const BLOCK_GATEWAY_START =
|
|
94
|
-
'禁止在对话内 exec `openclaw gateway start`:当前会话可能正跑在本网关上,重复启动会先 SIGTERM 旧进程导致断连。' +
|
|
95
|
-
'若仅需确认存活,请改用只读命令,例如 `openclaw gateway health --json` 或 `openclaw gateway probe`(以本机 CLI 为准)。' +
|
|
96
|
-
'若确认网关未运行,请在宿主机/服务管理(systemd 等)中启动,勿由 Agent 代 exec。'
|
|
97
|
-
|
|
98
|
-
const BLOCK_CRON_CLI =
|
|
99
|
-
'请勿用 exec 跑 `openclaw cron add` / `openclaw cron create`:请改用内置 **`cron` 工具**(框架走 Gateway `cron.*`,由本插件自动补全 delivery / sessionKey)。' +
|
|
100
|
-
'高频 exec CLI 易长时间占满子进程并加剧网关 WebSocket 握手压力。'
|
|
101
|
-
|
|
102
82
|
/**
|
|
103
83
|
* 从 cron 参数中提取 delivery 配置
|
|
104
84
|
* cron 工具的参数结构可能是:
|
|
@@ -157,7 +137,7 @@ function patchCronDeliveryInParams(
|
|
|
157
137
|
if (agentId) d.accountId = agentId
|
|
158
138
|
if (announceNoChannel) {
|
|
159
139
|
d.bestEffort = true
|
|
160
|
-
d.channel =
|
|
140
|
+
d.channel = 'dcgchat-test'
|
|
161
141
|
}
|
|
162
142
|
}
|
|
163
143
|
|
|
@@ -208,22 +188,14 @@ export function cronToolCall(event: { toolName: any; params: any; toolCallId: an
|
|
|
208
188
|
|
|
209
189
|
return { params: newParams }
|
|
210
190
|
} else if (toolName === 'exec') {
|
|
211
|
-
|
|
212
|
-
if (/\bopenclaw\s+gateway\s+start\b/i.test(cmd)) {
|
|
213
|
-
dcgLogger(`[${LOG_TAG}] blocked exec gateway start (${toolCallId})`)
|
|
214
|
-
return { block: true, blockReason: BLOCK_GATEWAY_START }
|
|
215
|
-
}
|
|
216
|
-
if (/\bopenclaw\s+cron\s+(add|create)\b/i.test(cmd)) {
|
|
217
|
-
dcgLogger(`[${LOG_TAG}] blocked exec openclaw cron add/create (${toolCallId})`)
|
|
218
|
-
return { block: true, blockReason: BLOCK_CRON_CLI }
|
|
219
|
-
}
|
|
220
|
-
if (cmd.includes('cron create') || cmd.includes('cron add')) {
|
|
191
|
+
if (params.command.indexOf('cron create') > -1 || params.command.indexOf('cron add') > -1) {
|
|
221
192
|
const newParams = JSON.parse(JSON.stringify(params)) as Record<string, unknown>
|
|
222
193
|
newParams.command =
|
|
223
|
-
|
|
194
|
+
params.command.replace('--json', '') + ` --session-key ${sk} --channel ${'dcgchat-test'} --to dcg-cron:${sk} --json`
|
|
224
195
|
return { params: newParams }
|
|
196
|
+
} else {
|
|
197
|
+
return undefined
|
|
225
198
|
}
|
|
226
|
-
return undefined
|
|
227
199
|
}
|
|
228
200
|
|
|
229
201
|
return undefined
|
package/src/tool.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { getMsgStatus } from './utils/global.js'
|
|
|
3
3
|
import { dcgLogger } from './utils/log.js'
|
|
4
4
|
import { sendFinal, sendText, wsSendRaw } from './transport.js'
|
|
5
5
|
import { getEffectiveMsgParams, deleteSessionKeyBySubAgentRunId, setSessionKeyBySubAgentRunId } from './utils/params.js'
|
|
6
|
-
import { cronToolCall
|
|
6
|
+
import { cronToolCall } from './cronToolCall.js'
|
|
7
7
|
|
|
8
8
|
type PluginHookName =
|
|
9
9
|
| 'before_model_resolve'
|
|
@@ -330,7 +330,7 @@ function shouldRunBeforeToolCallWithoutRunningSession(event: { toolName?: string
|
|
|
330
330
|
if (event?.toolName === 'cron') return true
|
|
331
331
|
const cmd = event?.params?.command
|
|
332
332
|
if (event?.toolName === 'exec' && typeof cmd === 'string') {
|
|
333
|
-
return
|
|
333
|
+
return cmd.includes('cron create') || cmd.includes('cron add')
|
|
334
334
|
}
|
|
335
335
|
return false
|
|
336
336
|
}
|
|
@@ -362,8 +362,7 @@ export function monitoringToolMessage(api: OpenClawPluginApi) {
|
|
|
362
362
|
const sk = resolveHookSessionKey(item.event, args ?? {})
|
|
363
363
|
if (sk) {
|
|
364
364
|
const toolHooksOk =
|
|
365
|
-
isSessionActiveForTool(sk) ||
|
|
366
|
-
(item.event === 'before_tool_call' && shouldRunBeforeToolCallWithoutRunningSession(event))
|
|
365
|
+
isSessionActiveForTool(sk) || (item.event === 'before_tool_call' && shouldRunBeforeToolCallWithoutRunningSession(event))
|
|
367
366
|
if (toolHooksOk) {
|
|
368
367
|
if (['after_tool_call', 'before_tool_call'].includes(item.event)) {
|
|
369
368
|
const { result: _result, ...rest } = event
|
|
@@ -378,12 +377,7 @@ export function monitoringToolMessage(api: OpenClawPluginApi) {
|
|
|
378
377
|
...rest,
|
|
379
378
|
status: 'running'
|
|
380
379
|
})
|
|
381
|
-
sendToolCallMessage(
|
|
382
|
-
sk,
|
|
383
|
-
text,
|
|
384
|
-
event.toolCallId || event.runId || Date.now().toString(),
|
|
385
|
-
0
|
|
386
|
-
)
|
|
380
|
+
sendToolCallMessage(sk, text, event.toolCallId || event.runId || Date.now().toString(), 0)
|
|
387
381
|
return hookResult
|
|
388
382
|
}
|
|
389
383
|
const text = JSON.stringify({
|
package/src/utils/constant.ts
CHANGED
package/src/utils/global.ts
CHANGED
|
@@ -30,7 +30,7 @@ export function getOpenClawConfig(): OpenClawConfig | null {
|
|
|
30
30
|
function getWorkspacePath(): string | null {
|
|
31
31
|
const workspacePath = path.join(
|
|
32
32
|
os.homedir(),
|
|
33
|
-
config?.channels?.[
|
|
33
|
+
config?.channels?.[channelInfo[ENV]]?.appId == 110 ? '.mobook' : '.openclaw',
|
|
34
34
|
'workspace'
|
|
35
35
|
)
|
|
36
36
|
if (fs.existsSync(workspacePath)) {
|
|
@@ -55,7 +55,7 @@ export function getWorkspaceDir(): string {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
const { setRuntime: setDcgchatRuntime, getRuntime: getDcgchatRuntime } = createPluginRuntimeStore<PluginRuntime>(
|
|
58
|
-
`${
|
|
58
|
+
`${channelInfo[ENV]} runtime not initialized`
|
|
59
59
|
)
|
|
60
60
|
export { setDcgchatRuntime, getDcgchatRuntime }
|
|
61
61
|
|
|
@@ -147,7 +147,7 @@ export const getSessionKey = (content: any, accountId: string) => {
|
|
|
147
147
|
|
|
148
148
|
const route = core.channel.routing.resolveAgentRoute({
|
|
149
149
|
cfg: getOpenClawConfig() as OpenClawConfig,
|
|
150
|
-
channel:
|
|
150
|
+
channel: channelInfo[ENV],
|
|
151
151
|
accountId: accountId || 'default',
|
|
152
152
|
peer: { kind: 'direct', id: session_id }
|
|
153
153
|
})
|
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
|
@@ -9,7 +9,7 @@ const paramsMessageMap = new Map<string, IMsgParams>()
|
|
|
9
9
|
|
|
10
10
|
/** 从 OpenClaw 配置读取当前 channel 的基础参数(唯一来源,供 transport / resolve 等复用) */
|
|
11
11
|
export function getParamsDefaults(): IMsgParams {
|
|
12
|
-
const ch = (getOpenClawConfig()?.channels?.[
|
|
12
|
+
const ch = (getOpenClawConfig()?.channels?.[channelInfo[ENV]] as DcgchatConfig | undefined) ?? {}
|
|
13
13
|
return {
|
|
14
14
|
userId: Number(ch.userId ?? 0),
|
|
15
15
|
botToken: ch.botToken ?? '',
|