@dcrays/dcgchat-test 0.3.14 → 0.3.15

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,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: "dcgchat-test",
11
+ id: channelInfo[ENV],
11
12
  name: '书灵墨宝',
12
13
  description: '连接 OpenClaw 与 书灵墨宝 产品(WebSocket)',
13
14
  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.3.14",
3
+ "version": "0.3.15",
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-test",
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-test",
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
@@ -155,7 +155,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
155
155
 
156
156
  const route = core.channel.routing.resolveAgentRoute({
157
157
  cfg: config,
158
- channel: "dcgchat-test",
158
+ channel: channelInfo[ENV],
159
159
  accountId: account.accountId,
160
160
  peer: { kind: 'direct', id: conversationId }
161
161
  })
@@ -234,13 +234,13 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
234
234
  ChatType: 'direct',
235
235
  SenderName: agentDisplayName,
236
236
  SenderId: userId,
237
- Provider: "dcgchat-test",
238
- Surface: "dcgchat-test",
237
+ Provider: channelInfo[ENV],
238
+ Surface: channelInfo[ENV],
239
239
  MessageSid: msg.content.message_id,
240
240
  Timestamp: Date.now(),
241
241
  WasMentioned: true,
242
242
  CommandAuthorized: true,
243
- OriginatingChannel: "dcgchat-test",
243
+ OriginatingChannel: channelInfo[ENV],
244
244
  OriginatingTo: effectiveSessionKey,
245
245
  ...mediaPayload
246
246
  })
@@ -252,7 +252,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
252
252
  const prefixContext = createReplyPrefixContext({
253
253
  cfg: config,
254
254
  agentId: effectiveAgentId ?? '',
255
- channel: "dcgchat-test",
255
+ channel: channelInfo[ENV],
256
256
  accountId: account.accountId
257
257
  })
258
258
 
@@ -274,10 +274,15 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
274
274
  onError: (err: unknown, info: { kind: string }) => {
275
275
  safeSendFinal('error')
276
276
  dcgLogger(`${info.kind} reply failed: ${String(err)}`, 'error')
277
+ activeRunIdBySessionKey.delete(effectiveSessionKey)
278
+ streamChunkIdxBySessionKey.delete(effectiveSessionKey)
279
+ if (sessionStreamSuppressed.has(effectiveSessionKey)) {
280
+ dcgLogger(`${info.kind} reply failed (stream suppressed): ${String(err)}`, 'error')
281
+ return
282
+ }
283
+ dcgLogger(`${info.kind} reply failed: ${String(err)}`, 'error')
277
284
  },
278
- onIdle: () => {
279
- // safeSendFinal()
280
- }
285
+ onIdle: () => {}
281
286
  })
282
287
 
283
288
  try {
package/src/channel.ts CHANGED
@@ -5,6 +5,7 @@ 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
7
  import { dcgLogger, setLogger } from './utils/log.js'
8
+ import { channelInfo, ENV } from './utils/constant.js'
8
9
  import { getEffectiveMsgParams, getCurrentSessionKey } from './utils/params.js'
9
10
  import { startDcgchatGatewaySocket } from './gateway/socket.js'
10
11
 
@@ -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?.["dcgchat-test"]?.botToken ?? ''
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 ?? '',
@@ -53,7 +54,7 @@ export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<v
53
54
 
54
55
  export function resolveAccount(cfg: OpenClawConfig, accountId?: string | null): ResolvedDcgchatAccount {
55
56
  const id = accountId ?? DEFAULT_ACCOUNT_ID
56
- const raw = (cfg.channels?.["dcgchat-test"] as DcgchatConfig | undefined) ?? {}
57
+ const raw = (cfg.channels?.[channelInfo[ENV]] as DcgchatConfig | undefined) ?? {}
57
58
  return {
58
59
  accountId: id,
59
60
  enabled: raw.enabled !== false,
@@ -67,13 +68,13 @@ export function resolveAccount(cfg: OpenClawConfig, accountId?: string | null):
67
68
  }
68
69
 
69
70
  export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
70
- id: "dcgchat-test",
71
+ id: channelInfo[ENV],
71
72
  meta: {
72
- id: "dcgchat-test",
73
+ id: channelInfo[ENV],
73
74
  label: '书灵墨宝',
74
75
  selectionLabel: '书灵墨宝',
75
76
  docsPath: '/channels/dcgchat',
76
- docsLabel: "dcgchat-test",
77
+ docsLabel: channelInfo[ENV],
77
78
  blurb: '连接 OpenClaw 与 书灵墨宝 产品',
78
79
  order: 80
79
80
  },
@@ -114,7 +115,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
114
115
  channels: {
115
116
  ...cfg.channels,
116
117
  dcgchat: {
117
- ...(cfg.channels?.["dcgchat-test"] as Record<string, unknown> | undefined),
118
+ ...(cfg.channels?.[channelInfo[ENV]] as Record<string, unknown> | undefined),
118
119
  enabled
119
120
  }
120
121
  }
@@ -178,7 +179,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
178
179
  }
179
180
  }
180
181
  return {
181
- channel: "dcgchat-test",
182
+ channel: channelInfo[ENV],
182
183
  messageId: `dcg-${Date.now()}`,
183
184
  chatId: ctx.to
184
185
  }
@@ -188,7 +189,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
188
189
  dcgLogger(`channel sendMedia to ${ctx.to} ${ctx.mediaUrl?.slice(0, 50)}`)
189
190
  await sendDcgchatMedia({ sessionKey: ctx.to ?? '', mediaUrl: ctx.mediaUrl ?? '' })
190
191
  return {
191
- channel: "dcgchat-test",
192
+ channel: channelInfo[ENV],
192
193
  messageId: `dcg-${Date.now()}`,
193
194
  chatId: msgCtx.userId?.toString()
194
195
  }
package/src/cron.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import path from 'node:path'
2
2
  import fs from 'node:fs'
3
3
  import type { IMsgParams } from './types.js'
4
- import { sendEventMessage, sendFinal } from './transport.js'
5
- import { getWorkspaceDir, removeCronMessageId, setCronMessageId, setMsgStatus } from './utils/global.js'
4
+ import { mergeDefaultParams, sendEventMessage, sendFinal } from './transport.js'
5
+ import { getCronMessageId, getWorkspaceDir, removeCronMessageId, setCronMessageId, setMsgStatus } from './utils/global.js'
6
6
  import { ossUpload } from './request/oss.js'
7
7
  import { dcgLogger } from './utils/log.js'
8
8
  import { sendMessageToGateway } from './gateway/socket.js'
@@ -152,7 +152,23 @@ export const finishedDcgchatCron = async (jobId: string) => {
152
152
  return
153
153
  }
154
154
  const outboundCtx = getEffectiveMsgParams(sessionKey)
155
- sendFinal(outboundCtx)
155
+ const messageId = getCronMessageId(sessionKey)
156
+ if (outboundCtx?.sessionId) {
157
+ const newCtx = messageId ? { ...outboundCtx, messageId } : outboundCtx
158
+ sendFinal(newCtx)
159
+ } else {
160
+ const sessionInfo = sessionKey.split(':')
161
+ const sessionId = sessionInfo.at(-1) ?? ''
162
+ const agentId = sessionInfo.at(-2) ?? ''
163
+ const merged = mergeDefaultParams({
164
+ agentId: agentId,
165
+ sessionId: `${sessionId}`,
166
+ messageId: messageId,
167
+ is_finish: -1,
168
+ real_mobook: !sessionId ? 1 : ''
169
+ })
170
+ sendFinal(merged, 'cron send')
171
+ }
156
172
  removeCronMessageId(sessionKey)
157
173
  dcgLogger(`finishedDcgchatCron: job=${id} sessionKey=${sessionKey}`)
158
174
  }
@@ -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 = sessionId
129
129
  ;(newParams.delivery as CronDelivery).accountId = agentId
130
- ;(newParams.delivery as CronDelivery).channel = "dcgchat-test"
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 = sessionId
140
140
  ;(newParams.delivery as CronDelivery).accountId = agentId
141
- ;(newParams.delivery as CronDelivery).channel = "dcgchat-test"
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 ${"dcgchat-test"} --to ${sk} --json`
179
+ params.command.replace('--json', '') + ` --session-key ${sk} --channel ${channelInfo[ENV]} --to ${sk} --json`
180
180
  return { params: newParams }
181
181
  } else {
182
182
  return params
@@ -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']
@@ -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?.["dcgchat-test"]?.appId == 110 ? '.mobook' : '.openclaw',
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: "dcgchat-test",
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()} [${"dcgchat-test"}]: ${message}`)
14
+ console[type](`书灵墨宝🚀 ~ ${new Date().toISOString()} [${channelInfo[ENV]}]: ${message}`)
14
15
  }
15
16
  }
@@ -12,7 +12,7 @@ let currentSessionKey: string | null = null
12
12
 
13
13
  /** 从 OpenClaw 配置读取当前 channel 的基础参数(唯一来源,供 transport / resolve 等复用) */
14
14
  export function getParamsDefaults(): IMsgParams {
15
- const ch = (getOpenClawConfig()?.channels?.["dcgchat-test"] as DcgchatConfig | undefined) ?? {}
15
+ const ch = (getOpenClawConfig()?.channels?.[channelInfo[ENV]] as DcgchatConfig | undefined) ?? {}
16
16
  return {
17
17
  userId: Number(ch.userId ?? 0),
18
18
  botToken: ch.botToken ?? '',