@dcrays/dcgchat-test 0.2.29 → 0.2.30
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 +15 -9
- package/src/channel.ts +8 -7
- package/src/skill.ts +1 -7
- package/src/tool.ts +13 -20
- package/src/transport.ts +2 -2
- package/src/utils/constant.ts +4 -0
- package/src/utils/log.ts +2 -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,10 +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'
|
|
6
7
|
import { setOpenClawConfig } from './src/utils/global.js'
|
|
7
8
|
|
|
8
9
|
const plugin = {
|
|
9
|
-
id:
|
|
10
|
+
id: channelInfo[ENV],
|
|
10
11
|
name: '书灵墨宝',
|
|
11
12
|
description: '连接 OpenClaw 与 书灵墨宝 产品(WebSocket)',
|
|
12
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.2.
|
|
3
|
+
"version": "0.2.30",
|
|
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
|
@@ -3,11 +3,18 @@ import path from 'node:path'
|
|
|
3
3
|
import type { ReplyPayload } from 'openclaw/plugin-sdk'
|
|
4
4
|
import { createReplyPrefixContext } from 'openclaw/plugin-sdk'
|
|
5
5
|
import type { InboundMessage } from './types.js'
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
clearSentMediaKeys,
|
|
8
|
+
getDcgchatRuntime,
|
|
9
|
+
getOpenClawConfig,
|
|
10
|
+
getWorkspaceDir,
|
|
11
|
+
getWsConnection,
|
|
12
|
+
setMsgStatus
|
|
13
|
+
} from './utils/global.js'
|
|
7
14
|
import { resolveAccount, sendDcgchatMedia } from './channel.js'
|
|
8
15
|
import { generateSignUrl } from './request/api.js'
|
|
9
16
|
import { extractMobookFiles } from './utils/searchFile.js'
|
|
10
|
-
import { createMsgContext, sendChunk, sendFinal, sendText as sendTextMsg, sendError } from './transport.js'
|
|
17
|
+
import { createMsgContext, sendChunk, sendFinal, sendText as sendTextMsg, sendError, sendText } from './transport.js'
|
|
11
18
|
import { dcgLogger } from './utils/log.js'
|
|
12
19
|
import { channelInfo, systemCommand, interruptCommand, ENV } from './utils/constant.js'
|
|
13
20
|
|
|
@@ -149,7 +156,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
149
156
|
|
|
150
157
|
const route = core.channel.routing.resolveAgentRoute({
|
|
151
158
|
cfg: config,
|
|
152
|
-
channel:
|
|
159
|
+
channel: channelInfo[ENV],
|
|
153
160
|
accountId: account.accountId,
|
|
154
161
|
peer: { kind: 'direct', id: conversationId }
|
|
155
162
|
})
|
|
@@ -201,13 +208,13 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
201
208
|
ChatType: 'direct',
|
|
202
209
|
SenderName: agentDisplayName,
|
|
203
210
|
SenderId: userId,
|
|
204
|
-
Provider:
|
|
205
|
-
Surface:
|
|
211
|
+
Provider: channelInfo[ENV],
|
|
212
|
+
Surface: channelInfo[ENV],
|
|
206
213
|
MessageSid: msg.content.message_id,
|
|
207
214
|
Timestamp: Date.now(),
|
|
208
215
|
WasMentioned: true,
|
|
209
216
|
CommandAuthorized: true,
|
|
210
|
-
OriginatingChannel:
|
|
217
|
+
OriginatingChannel: channelInfo[ENV],
|
|
211
218
|
OriginatingTo: `user:${userId}`,
|
|
212
219
|
...mediaPayload
|
|
213
220
|
})
|
|
@@ -219,7 +226,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
219
226
|
const prefixContext = createReplyPrefixContext({
|
|
220
227
|
cfg: config,
|
|
221
228
|
agentId: effectiveAgentId ?? '',
|
|
222
|
-
channel:
|
|
229
|
+
channel: channelInfo[ENV],
|
|
223
230
|
accountId: account.accountId
|
|
224
231
|
})
|
|
225
232
|
|
|
@@ -229,7 +236,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
229
236
|
humanDelay: core.channel.reply.resolveHumanDelayConfig(config, route.agentId),
|
|
230
237
|
onReplyStart: async () => {},
|
|
231
238
|
deliver: async (payload: ReplyPayload, info) => {
|
|
232
|
-
dcgLogger(`[deliver]: kind=${info.kind}, text=${payload.text?.length ?? 0} chars, ${payload.text?.slice(0,
|
|
239
|
+
dcgLogger(`[deliver]: kind=${info.kind}, text=${payload.text?.length ?? 0} chars, ${payload.text?.slice(0, 50)}`)
|
|
233
240
|
// Media from the outbound pipeline (post-streaming)
|
|
234
241
|
const mediaList = resolveReplyMediaList(payload)
|
|
235
242
|
for (const mediaUrl of mediaList) {
|
|
@@ -238,7 +245,6 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
238
245
|
sentMediaKeys.add(key)
|
|
239
246
|
await sendDcgchatMedia({ msgCtx, mediaUrl, text: '' })
|
|
240
247
|
}
|
|
241
|
-
sendFinal(msgCtx)
|
|
242
248
|
},
|
|
243
249
|
onError: (err: unknown, info: { kind: string }) => {
|
|
244
250
|
dcgLogger(`${info.kind} reply failed: ${String(err)}`, 'error')
|
package/src/channel.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { ossUpload } from './request/oss.js'
|
|
|
5
5
|
import { addSentMediaKey, getMsgParams, hasSentMediaKey } from './utils/global.js'
|
|
6
6
|
import { type DcgchatMsgContext, isWsOpen, sendFinal, wsSendRaw } from './transport.js'
|
|
7
7
|
import { dcgLogger, setLogger } from './utils/log.js'
|
|
8
|
+
import { channelInfo, ENV } from './utils/constant.js'
|
|
8
9
|
|
|
9
10
|
export type DcgchatMediaSendOptions = {
|
|
10
11
|
msgCtx: DcgchatMsgContext
|
|
@@ -50,7 +51,7 @@ export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<v
|
|
|
50
51
|
|
|
51
52
|
export function resolveAccount(cfg: OpenClawConfig, accountId?: string | null): ResolvedDcgchatAccount {
|
|
52
53
|
const id = accountId ?? DEFAULT_ACCOUNT_ID
|
|
53
|
-
const raw = (cfg.channels?.[
|
|
54
|
+
const raw = (cfg.channels?.[channelInfo[ENV]] as DcgchatConfig | undefined) ?? {}
|
|
54
55
|
return {
|
|
55
56
|
accountId: id,
|
|
56
57
|
enabled: raw.enabled !== false,
|
|
@@ -80,13 +81,13 @@ function createOutboundMsgContext(cfg: OpenClawConfig, accountId?: string | null
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
83
|
-
id:
|
|
84
|
+
id: channelInfo[ENV],
|
|
84
85
|
meta: {
|
|
85
|
-
id:
|
|
86
|
+
id: channelInfo[ENV],
|
|
86
87
|
label: '书灵墨宝',
|
|
87
88
|
selectionLabel: '书灵墨宝',
|
|
88
89
|
docsPath: '/channels/dcgchat',
|
|
89
|
-
docsLabel:
|
|
90
|
+
docsLabel: channelInfo[ENV],
|
|
90
91
|
blurb: '连接 OpenClaw 与 书灵墨宝 产品',
|
|
91
92
|
order: 80
|
|
92
93
|
},
|
|
@@ -127,7 +128,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
127
128
|
channels: {
|
|
128
129
|
...cfg.channels,
|
|
129
130
|
dcgchat: {
|
|
130
|
-
...(cfg.channels?.[
|
|
131
|
+
...(cfg.channels?.[channelInfo[ENV]] as Record<string, unknown> | undefined),
|
|
131
132
|
enabled
|
|
132
133
|
}
|
|
133
134
|
}
|
|
@@ -162,7 +163,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
162
163
|
dcgLogger(`channel sendText to ${msgCtx.userId}`)
|
|
163
164
|
}
|
|
164
165
|
return {
|
|
165
|
-
channel:
|
|
166
|
+
channel: channelInfo[ENV],
|
|
166
167
|
messageId: `dcg-${Date.now()}`,
|
|
167
168
|
chatId: msgCtx.userId.toString()
|
|
168
169
|
}
|
|
@@ -171,7 +172,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
171
172
|
const msgCtx = createOutboundMsgContext(ctx.cfg, ctx.accountId)
|
|
172
173
|
await sendDcgchatMedia({ msgCtx, mediaUrl: ctx.mediaUrl })
|
|
173
174
|
return {
|
|
174
|
-
channel:
|
|
175
|
+
channel: channelInfo[ENV],
|
|
175
176
|
messageId: `dcg-${Date.now()}`,
|
|
176
177
|
chatId: msgCtx.userId.toString()
|
|
177
178
|
}
|
package/src/skill.ts
CHANGED
|
@@ -66,7 +66,6 @@ export async function installSkill(params: ISkillParams, msgContent: Record<stri
|
|
|
66
66
|
entry.autodrain()
|
|
67
67
|
return
|
|
68
68
|
}
|
|
69
|
-
|
|
70
69
|
try {
|
|
71
70
|
const flags = entry.props?.flags ?? 0
|
|
72
71
|
const isUtf8 = (flags & 0x800) !== 0
|
|
@@ -82,9 +81,7 @@ export async function installSkill(params: ISkillParams, msgContent: Record<stri
|
|
|
82
81
|
if (!rootDir && pathParts.length > 1) {
|
|
83
82
|
rootDir = pathParts[0]
|
|
84
83
|
}
|
|
85
|
-
|
|
86
84
|
let newPath = entryPath
|
|
87
|
-
|
|
88
85
|
// 移除顶层文件夹
|
|
89
86
|
if (rootDir && entryPath.startsWith(rootDir + '/')) {
|
|
90
87
|
newPath = entryPath.slice(rootDir.length + 1)
|
|
@@ -139,10 +136,7 @@ export async function installSkill(params: ISkillParams, msgContent: Record<stri
|
|
|
139
136
|
}
|
|
140
137
|
}
|
|
141
138
|
|
|
142
|
-
export function uninstallSkill(
|
|
143
|
-
params: Omit<ISkillParams, 'path'>,
|
|
144
|
-
msgContent: Record<string, any>
|
|
145
|
-
) {
|
|
139
|
+
export function uninstallSkill(params: Omit<ISkillParams, 'path'>, msgContent: Record<string, any>) {
|
|
146
140
|
const { code } = params
|
|
147
141
|
|
|
148
142
|
const workspacePath = getWorkspaceDir()
|
package/src/tool.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from 'openclaw/plugin-sdk'
|
|
2
2
|
import { getMsgParams, getMsgStatus, getWsConnection } from './utils/global.js'
|
|
3
3
|
import { dcgLogger } from './utils/log.js'
|
|
4
|
-
import { isWsOpen } from './transport.js'
|
|
4
|
+
import { isWsOpen, sendFinal, sendText } from './transport.js'
|
|
5
5
|
|
|
6
6
|
let toolCallId = ''
|
|
7
7
|
let toolName = ''
|
|
@@ -97,27 +97,20 @@ export function monitoringToolMessage(api: OpenClawPluginApi) {
|
|
|
97
97
|
} else if (item.event) {
|
|
98
98
|
if (item.event === 'llm_output') {
|
|
99
99
|
if (event.lastAssistant?.errorMessage === '429-账户额度耗尽') {
|
|
100
|
-
const ws = getWsConnection()
|
|
101
100
|
const params = getMsgParams()
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
bot_id: params?.botId,
|
|
113
|
-
agent_id: params?.agentId,
|
|
114
|
-
response: '您的余额不足,请充值后继续使用',
|
|
115
|
-
session_id: params?.sessionId,
|
|
116
|
-
message_id: params?.messageId || Date.now().toString()
|
|
117
|
-
}
|
|
118
|
-
})
|
|
119
|
-
)
|
|
101
|
+
const message = '您的积分已消耗完,您可以通过充值积分来继续使用'
|
|
102
|
+
const ctx = {
|
|
103
|
+
userId: params.userId,
|
|
104
|
+
botToken: params.token,
|
|
105
|
+
domainId: params.domainId,
|
|
106
|
+
appId: params.appId,
|
|
107
|
+
botId: params.botId,
|
|
108
|
+
agentId: params.agentId,
|
|
109
|
+
sessionId: params.sessionId,
|
|
110
|
+
messageId: params.messageId
|
|
120
111
|
}
|
|
112
|
+
sendText(ctx, message, { message_tags: { insufficient_balance: 1 }, is_finish: -1 })
|
|
113
|
+
sendFinal(ctx)
|
|
121
114
|
return
|
|
122
115
|
}
|
|
123
116
|
}
|
package/src/transport.ts
CHANGED
|
@@ -99,8 +99,8 @@ export function sendFinal(ctx: DcgchatMsgContext): boolean {
|
|
|
99
99
|
return wsSend(ctx, { response: '', state: 'final' })
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
export function sendText(ctx: DcgchatMsgContext, text: string): boolean {
|
|
103
|
-
return wsSend(ctx, { response: text })
|
|
102
|
+
export function sendText(ctx: DcgchatMsgContext, text: string, event?: Record<string, unknown>): boolean {
|
|
103
|
+
return wsSend(ctx, { response: text, ...event })
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
export function sendError(ctx: DcgchatMsgContext, errorMsg: string): boolean {
|
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 = ['/stop']
|
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
|
}
|