@dcrays/dcgchat-test 0.2.28 → 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 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: "dcgchat-test",
10
+ id: channelInfo[ENV],
10
11
  name: '书灵墨宝',
11
12
  description: '连接 OpenClaw 与 书灵墨宝 产品(WebSocket)',
12
13
  configSchema: emptyPluginConfigSchema(),
@@ -1,11 +1,9 @@
1
1
  {
2
2
  "id": "dcgchat-test",
3
- "channels": [
4
- "dcgchat-test"
5
- ],
3
+ "channels": ["dcgchat-test"],
6
4
  "configSchema": {
7
5
  "type": "object",
8
6
  "additionalProperties": false,
9
7
  "properties": {}
10
8
  }
11
- }
9
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcrays/dcgchat-test",
3
- "version": "0.2.28",
3
+ "version": "0.2.30",
4
4
  "type": "module",
5
5
  "description": "OpenClaw channel plugin for 书灵墨宝 (WebSocket)",
6
6
  "main": "index.ts",
@@ -37,15 +37,18 @@
37
37
  "id": "dcgchat-test",
38
38
  "label": "书灵墨宝",
39
39
  "selectionLabel": "书灵墨宝",
40
- "docsPath": "/channels/dcgchat-test",
40
+ "docsPath": "/channels/dcgchat",
41
41
  "docsLabel": "dcgchat-test",
42
42
  "blurb": "连接 OpenClaw 与 书灵墨宝 产品",
43
43
  "order": 80
44
44
  },
45
45
  "install": {
46
46
  "npmSpec": "@dcrays/dcgchat-test",
47
- "localPath": "extensions/dcgchat-test",
47
+ "localPath": "extensions/dcgchat",
48
48
  "defaultChoice": "npm"
49
49
  }
50
+ },
51
+ "devDependencies": {
52
+ "openclaw": "^2026.3.13"
50
53
  }
51
54
  }
package/src/bot.ts CHANGED
@@ -3,13 +3,20 @@ 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 { clearSentMediaKeys, getDcgchatRuntime, getOpenClawConfig, getWorkspaceDir, setMsgStatus } from './utils/global.js'
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
- import { emptyToolText } from './utils/constant.js'
19
+ import { channelInfo, systemCommand, interruptCommand, ENV } from './utils/constant.js'
13
20
 
14
21
  type MediaInfo = {
15
22
  path: string
@@ -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: "dcgchat-test",
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: "dcgchat-test",
205
- Surface: "dcgchat-test",
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: "dcgchat-test",
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: "dcgchat-test",
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`)
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) {
@@ -247,7 +254,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
247
254
 
248
255
  let wasAborted = false
249
256
  try {
250
- if (emptyToolText.includes(text?.trim())) {
257
+ if (systemCommand.includes(text?.trim())) {
251
258
  dcgLogger(`dispatching /new`)
252
259
  await core.channel.reply.dispatchReplyFromConfig({
253
260
  ctx: ctxPayload,
@@ -258,6 +265,11 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
258
265
  onModelSelected: prefixContext.onModelSelected
259
266
  }
260
267
  })
268
+ } else if (interruptCommand.includes(text?.trim())) {
269
+ dcgLogger(`interrupt command: ${text}`)
270
+ abortMobookappGeneration(conversationId)
271
+ sendFinal(msgCtx)
272
+ return
261
273
  } else {
262
274
  dcgLogger(`dispatching to agent (session=${route.sessionKey})`)
263
275
  await core.channel.reply.dispatchReplyFromConfig({
@@ -311,10 +323,13 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
311
323
  activeGenerations.delete(conversationId)
312
324
  }
313
325
  }
314
-
315
- markRunComplete()
326
+ try {
327
+ markRunComplete()
328
+ } catch (err) {
329
+ dcgLogger(` markRunComplete error: ${String(err)}`, 'error')
330
+ }
316
331
  markDispatchIdle()
317
- if (!emptyToolText.includes(text?.trim())) {
332
+ if (![...systemCommand, ...interruptCommand].includes(text?.trim())) {
318
333
  for (const file of extractMobookFiles(completeText)) {
319
334
  let resolved = file
320
335
  if (!fs.existsSync(resolved)) {
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?.["dcgchat-test"] as DcgchatConfig | undefined) ?? {}
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: "dcgchat-test",
84
+ id: channelInfo[ENV],
84
85
  meta: {
85
- id: "dcgchat-test",
86
+ id: channelInfo[ENV],
86
87
  label: '书灵墨宝',
87
88
  selectionLabel: '书灵墨宝',
88
89
  docsPath: '/channels/dcgchat',
89
- docsLabel: "dcgchat-test",
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?.["dcgchat-test"] as Record<string, unknown> | undefined),
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: "dcgchat-test",
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: "dcgchat-test",
175
+ channel: channelInfo[ENV],
175
176
  messageId: `dcg-${Date.now()}`,
176
177
  chatId: msgCtx.userId.toString()
177
178
  }
package/src/monitor.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import type { ClawdbotConfig, RuntimeEnv } from 'openclaw/plugin-sdk'
2
2
  import WebSocket from 'ws'
3
- import { abortMobookappGeneration, handleDcgchatMessage } from './bot.js'
3
+ import { handleDcgchatMessage } from './bot.js'
4
4
  import { resolveAccount } from './channel.js'
5
5
  import { setWsConnection, getOpenClawConfig } from './utils/global.js'
6
6
  import type { InboundMessage } from './types.js'
7
7
  import { setMsgParams, setMsgStatus } from './utils/global.js'
8
8
  import { installSkill, uninstallSkill } from './skill.js'
9
9
  import { dcgLogger } from './utils/log.js'
10
- import { emptyToolText } from './utils/constant.js'
10
+ import { ignoreToolCommand } from './utils/constant.js'
11
11
 
12
12
  export type MonitorDcgchatOpts = {
13
13
  config?: ClawdbotConfig
@@ -30,7 +30,7 @@ function buildConnectUrl(account: Record<string, string>): string {
30
30
  }
31
31
 
32
32
  export async function monitorDcgchatProvider(opts: MonitorDcgchatOpts): Promise<void> {
33
- const { runtime, abortSignal, accountId } = opts
33
+ const { abortSignal, accountId } = opts
34
34
 
35
35
  const config = getOpenClawConfig()
36
36
  if (!config) {
@@ -110,16 +110,9 @@ export async function monitorDcgchatProvider(opts: MonitorDcgchatOpts): Promise<
110
110
 
111
111
  if (parsed.messageType == 'openclaw_bot_chat') {
112
112
  const msg = parsed as unknown as InboundMessage
113
- if (!emptyToolText.includes(msg.content.text?.trim())) {
113
+ if (!ignoreToolCommand.includes(msg.content.text?.trim())) {
114
114
  setMsgStatus('running')
115
115
  }
116
- if (msg.content.text === '/stop') {
117
- const rawConvId = msg.content.session_id as string | undefined
118
- const conversationId = rawConvId || `${accountId}:${account.botToken}`
119
- abortMobookappGeneration(conversationId)
120
- dcgLogger(`abort conversationId=${conversationId}`)
121
- return
122
- }
123
116
  // 设置获取用户消息消息参数
124
117
  setMsgParams({
125
118
  userId: msg._userId,
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 = ''
@@ -83,7 +83,7 @@ export function monitoringToolMessage(api: OpenClawPluginApi) {
83
83
  api.on(item.event as PluginHookName, (event: any) => {
84
84
  const status = getMsgStatus()
85
85
  if (status === 'running') {
86
- dcgLogger(`工具调用结果: ~ event:${item.event} ${JSON.stringify(event)}`)
86
+ dcgLogger(`工具调用结果: ~ event:${item.event}`)
87
87
  if (['after_tool_call', 'before_tool_call'].includes(item.event)) {
88
88
  const { result: _result, ...rest } = event
89
89
  const text = JSON.stringify({
@@ -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
- if (isWsOpen()) {
103
- ws?.send(
104
- JSON.stringify({
105
- messageType: 'openclaw_bot_cost',
106
- _userId: params?.userId,
107
- source: 'client',
108
- content: {
109
- bot_token: params?.token,
110
- domain_id: params?.domainId,
111
- app_id: params?.appId,
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
@@ -95,12 +95,12 @@ export function sendChunk(ctx: DcgchatMsgContext, text: string): boolean {
95
95
  }
96
96
 
97
97
  export function sendFinal(ctx: DcgchatMsgContext): boolean {
98
- dcgLogger(` message handling complete`)
98
+ dcgLogger(` message handling complete state: final`)
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 {
@@ -1,4 +1,11 @@
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
- export const emptyToolText = ['/new', '/search', '/stop', '/abort', '/queue interrupt']
8
+ export const systemCommand = ['/new', '/status']
9
+ export const interruptCommand = ['/stop']
10
+
11
+ export const ignoreToolCommand = ['/search', '/abort', '/queue interrupt', ...systemCommand, ...interruptCommand]
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()} [${"dcgchat-test"}]: ${message}`)
14
+ console[type](`书灵墨宝🚀 ~ ${new Date().toISOString()} [${channelInfo[ENV]}]: ${message}`)
14
15
  }
15
16
  }