@dcrays/dcgchat-test 0.4.26 → 0.4.28

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/index.ts CHANGED
@@ -3,12 +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'
7
6
  import { setOpenClawConfig } from './src/utils/global.js'
8
7
  import { createDcgchatMessageTool } from './src/tools/messageTool.js'
9
8
 
10
9
  const plugin = {
11
- id: channelInfo[ENV],
10
+ id: "dcgchat-test",
12
11
  name: '书灵墨宝',
13
12
  description: '连接 OpenClaw 与 书灵墨宝 产品(WebSocket)',
14
13
  configSchema: emptyPluginConfigSchema(),
@@ -1,9 +1,11 @@
1
1
  {
2
2
  "id": "dcgchat-test",
3
- "channels": ["dcgchat-test"],
3
+ "channels": [
4
+ "dcgchat-test"
5
+ ],
4
6
  "configSchema": {
5
7
  "type": "object",
6
8
  "additionalProperties": false,
7
9
  "properties": {}
8
10
  }
9
- }
11
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcrays/dcgchat-test",
3
- "version": "0.4.26",
3
+ "version": "0.4.28",
4
4
  "type": "module",
5
5
  "description": "OpenClaw channel plugin for 书灵墨宝 (WebSocket)",
6
6
  "main": "index.ts",
@@ -16,12 +16,6 @@
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
- },
25
19
  "dependencies": {
26
20
  "ali-oss": "file:src/libs/ali-oss-6.23.0.tgz",
27
21
  "axios": "file:src/libs/axios-1.13.6.tgz",
@@ -37,19 +31,15 @@
37
31
  "id": "dcgchat-test",
38
32
  "label": "书灵墨宝",
39
33
  "selectionLabel": "书灵墨宝",
40
- "docsPath": "/channels/dcgchat",
34
+ "docsPath": "/channels/dcgchat-test",
41
35
  "docsLabel": "dcgchat-test",
42
36
  "blurb": "连接 OpenClaw 与 书灵墨宝 产品",
43
37
  "order": 80
44
38
  },
45
39
  "install": {
46
40
  "npmSpec": "@dcrays/dcgchat-test",
47
- "localPath": "extensions/dcgchat",
41
+ "localPath": "extensions/dcgchat-test",
48
42
  "defaultChoice": "npm"
49
43
  }
50
- },
51
- "devDependencies": {
52
- "openclaw": "^2026.3.13",
53
- "typescript": "~5.8.0"
54
44
  }
55
45
  }
package/src/bot.ts CHANGED
@@ -190,7 +190,7 @@ async function handleDcgchatMessageInboundTurn(msg: InboundMessage, accountId: s
190
190
 
191
191
  const route = core.channel.routing.resolveAgentRoute({
192
192
  cfg: config,
193
- channel: channelInfo[ENV],
193
+ channel: "dcgchat-test",
194
194
  accountId: account.accountId,
195
195
  peer: { kind: 'direct', id: conversationId }
196
196
  })
@@ -207,7 +207,7 @@ async function handleDcgchatMessageInboundTurn(msg: InboundMessage, accountId: s
207
207
  sessionId: conversationId,
208
208
  messageId: msg.content.message_id,
209
209
  domainId: msg.content.domain_id,
210
- appId: config.channels?.[channelInfo[ENV]]?.appId || 100,
210
+ appId: config.channels?.["dcgchat-test"]?.appId || 100,
211
211
  botId: msg.content.bot_id ?? '',
212
212
  agentId: msg.content.agent_id ?? '',
213
213
  sessionKey: dcgSessionKey,
@@ -269,13 +269,13 @@ async function handleDcgchatMessageInboundTurn(msg: InboundMessage, accountId: s
269
269
  ChatType: 'direct',
270
270
  SenderName: agentDisplayName,
271
271
  SenderId: userId,
272
- Provider: channelInfo[ENV],
273
- Surface: channelInfo[ENV],
272
+ Provider: "dcgchat-test",
273
+ Surface: "dcgchat-test",
274
274
  MessageSid: msg.content.message_id,
275
275
  Timestamp: Date.now(),
276
276
  WasMentioned: true,
277
277
  CommandAuthorized: true,
278
- OriginatingChannel: channelInfo[ENV],
278
+ OriginatingChannel: "dcgchat-test",
279
279
  OriginatingTo: dcgSessionKey,
280
280
  Target: dcgSessionKey,
281
281
  SourceTarget: dcgSessionKey,
@@ -298,7 +298,7 @@ async function handleDcgchatMessageInboundTurn(msg: InboundMessage, accountId: s
298
298
  const prefixContext = createReplyPrefixContext({
299
299
  cfg: config,
300
300
  agentId: effectiveAgentId ?? '',
301
- channel: channelInfo[ENV],
301
+ channel: "dcgchat-test",
302
302
  accountId: account.accountId
303
303
  })
304
304
 
@@ -479,7 +479,7 @@ async function handleDcgchatMessageInboundTurn(msg: InboundMessage, accountId: s
479
479
  ctx: ctxPayload,
480
480
  updateLastRoute: {
481
481
  sessionKey: dcgSessionKey,
482
- channel: channelInfo[ENV],
482
+ channel: "dcgchat-test",
483
483
  to: dcgSessionKey,
484
484
  accountId: route.accountId
485
485
  },
package/src/channel.ts CHANGED
@@ -13,7 +13,6 @@ import {
13
13
  setCronMessageId
14
14
  } from './utils/global.js'
15
15
  import { isWsOpen, mergeDefaultParams, mergeSessionParams, sendFinal, wsSendRaw } from './transport.js'
16
- import { channelInfo, ENV } from './utils/constant.js'
17
16
  import { dcgLogger, setLogger } from './utils/log.js'
18
17
  import { getOutboundMsgParams, getParamsMessage } from './utils/params.js'
19
18
  import { isSessionActiveForTool } from './tool.js'
@@ -21,7 +20,7 @@ import { startDcgchatGatewaySocket } from './gateway/socket.js'
21
20
  import { getCronJobsPath, readCronJob } from './cron.js'
22
21
 
23
22
  function dcgchatChannelCfg(): DcgchatConfig {
24
- return (getOpenClawConfig()?.channels?.[channelInfo[ENV]] as DcgchatConfig | undefined) ?? {}
23
+ return (getOpenClawConfig()?.channels?.["dcgchat-test"] as DcgchatConfig | undefined) ?? {}
25
24
  }
26
25
 
27
26
  /** `agent:<code>:mobook:direct:<agentId>:<sessionId>`(与 getSessionKey 非 real_mobook 分支一致) */
@@ -222,8 +221,8 @@ export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<v
222
221
  const fileName = mediaUrl?.split(/[\\/]/).pop() || ''
223
222
  const notMessageId = `${msgCtx?.messageId}`?.length === 13 || !msgCtx?.messageId
224
223
  try {
225
- const botToken = msgCtx.botToken ?? getOpenClawConfig()?.channels?.[channelInfo[ENV]]?.botToken ?? ''
226
- const url = opts.mediaUrl ? await ossUpload(opts.mediaUrl, botToken, 1) : ''
224
+ const botToken = msgCtx.botToken ?? getOpenClawConfig()?.channels?.["dcgchat-test"]?.botToken ?? ''
225
+ const url = mediaUrl ? await ossUpload(mediaUrl, botToken, 1) : ''
227
226
  if (!msgCtx.agentId) {
228
227
  msgCtx.agentId = agentId
229
228
  }
@@ -247,7 +246,7 @@ export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<v
247
246
 
248
247
  export function resolveAccount(cfg: OpenClawConfig, accountId?: string | null): ResolvedDcgchatAccount {
249
248
  const id = accountId ?? DEFAULT_ACCOUNT_ID
250
- const raw = (cfg.channels?.[channelInfo[ENV]] as DcgchatConfig | undefined) ?? {}
249
+ const raw = (cfg.channels?.["dcgchat-test"] as DcgchatConfig | undefined) ?? {}
251
250
  return {
252
251
  accountId: id,
253
252
  enabled: raw.enabled !== false,
@@ -261,13 +260,13 @@ export function resolveAccount(cfg: OpenClawConfig, accountId?: string | null):
261
260
  }
262
261
 
263
262
  export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
264
- id: channelInfo[ENV],
263
+ id: "dcgchat-test",
265
264
  meta: {
266
- id: channelInfo[ENV],
265
+ id: "dcgchat-test",
267
266
  label: '书灵墨宝',
268
267
  selectionLabel: '书灵墨宝',
269
268
  docsPath: '/channels/dcgchat',
270
- docsLabel: channelInfo[ENV],
269
+ docsLabel: "dcgchat-test",
271
270
  blurb: '连接 OpenClaw 与 书灵墨宝 产品',
272
271
  order: 80
273
272
  },
@@ -284,7 +283,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
284
283
  // blockStreaming: true,
285
284
  },
286
285
  /** 当前构建的 channel id + 兼容旧配置键 `channels.dcgchat` */
287
- reload: { configPrefixes: [`channels.${channelInfo[ENV]}`, 'channels.dcgchat'] },
286
+ reload: { configPrefixes: [`channels.${"dcgchat-test"}`, 'channels.dcgchat'] },
288
287
  configSchema: {
289
288
  schema: {
290
289
  type: 'object',
@@ -320,7 +319,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
320
319
  resolveAccount: (cfg, accountId) => resolveAccount(cfg, accountId),
321
320
  defaultAccountId: () => DEFAULT_ACCOUNT_ID,
322
321
  setAccountEnabled: ({ cfg, enabled }) => {
323
- const channelKey = channelInfo[ENV]
322
+ const channelKey = "dcgchat-test"
324
323
  const prev = (cfg.channels?.[channelKey as keyof NonNullable<typeof cfg.channels>] as Record<string, unknown> | undefined) ?? {}
325
324
  return {
326
325
  ...cfg,
@@ -392,7 +391,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
392
391
  if (!isSessionActiveForTool(to)) {
393
392
  dcgLogger(`channel sendText dropped (session not active): to=${to}`)
394
393
  return {
395
- channel: channelInfo[ENV],
394
+ channel: "dcgchat-test",
396
395
  messageId: '',
397
396
  chatId: outboundChatId(ctx.to, to)
398
397
  }
@@ -404,7 +403,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
404
403
  }
405
404
  }
406
405
  return {
407
- channel: channelInfo[ENV],
406
+ channel: "dcgchat-test",
408
407
  messageId: `${messageId}`,
409
408
  chatId: outboundChatId(ctx.to, to)
410
409
  }
@@ -425,7 +424,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
425
424
  if (!sessionId) {
426
425
  dcgLogger(`channel sendMedia to ${ctx.to} -> sessionId not found`, 'error')
427
426
  return {
428
- channel: channelInfo[ENV],
427
+ channel: "dcgchat-test",
429
428
  messageId,
430
429
  chatId: outboundChatId(ctx.to, to || '')
431
430
  }
@@ -444,7 +443,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
444
443
  })
445
444
  }
446
445
  return {
447
- channel: channelInfo[ENV],
446
+ channel: "dcgchat-test",
448
447
  messageId,
449
448
  chatId: outboundChatId(ctx.to, to || '')
450
449
  }
@@ -137,7 +137,7 @@ function patchCronDeliveryInParams(
137
137
  if (agentId) d.accountId = agentId
138
138
  if (announceNoChannel) {
139
139
  d.bestEffort = true
140
- d.channel = 'dcgchat-test'
140
+ d.channel = "dcgchat-test"
141
141
  }
142
142
  }
143
143
 
@@ -191,7 +191,7 @@ export function cronToolCall(event: { toolName: any; params: any; toolCallId: an
191
191
  if (params.command.indexOf('cron create') > -1 || params.command.indexOf('cron add') > -1) {
192
192
  const newParams = JSON.parse(JSON.stringify(params)) as Record<string, unknown>
193
193
  newParams.command =
194
- params.command.replace('--json', '') + ` --session-key ${sk} --channel ${'dcgchat-test'} --to dcg-cron:${sk} --json`
194
+ params.command.replace('--json', '') + ` --session-key ${sk} --channel ${"dcgchat-test"} --to dcg-cron:${sk} --json`
195
195
  return { params: newParams }
196
196
  } else {
197
197
  return undefined
@@ -1,32 +1,89 @@
1
- import { createReadStream } from 'node:fs'
1
+ import { fileURLToPath } from 'node:url'
2
2
  // @ts-ignore
3
3
  import OSS from 'ali-oss'
4
4
  import { getStsToken, getUserToken } from './api.js'
5
5
  import { dcgLogger } from '../utils/log.js'
6
6
 
7
- /** File/路径/Buffer 转为 ali-oss put 所需的 Buffer 或 ReadableStream */
8
- async function toUploadContent(
9
- input: File | string | Buffer
10
- ): Promise<{ content: Buffer | ReturnType<typeof createReadStream>; fileName: string }> {
7
+ /** 分片大小:OSS 要求每片 ≥100 KB(最后一片可更小) */
8
+ const MULTIPART_PART_SIZE = 1024 * 1024
9
+
10
+ /** ali-oss 默认 timeout 60s,大文件单 PUT 或慢网易触发 ResponseTimeoutError */
11
+ const OSS_HTTP_TIMEOUT_MS = 10 * 60 * 1000
12
+
13
+ /** 归一化入参,避免 file://、包装对象、TypedArray 等导致 SDK 识别失败 */
14
+ function coerceOssFileInput(input: File | string | Buffer): File | string | Buffer {
15
+ if (typeof input === 'string') {
16
+ const t = input.trim()
17
+ if (t.startsWith('file:')) {
18
+ try {
19
+ return fileURLToPath(t)
20
+ } catch {
21
+ return input
22
+ }
23
+ }
24
+ return input
25
+ }
26
+ if (Buffer.isBuffer(input)) {
27
+ return input
28
+ }
29
+ if (input && typeof input === 'object') {
30
+ if (ArrayBuffer.isView(input) && !(input instanceof DataView) && !Buffer.isBuffer(input)) {
31
+ const v = input as ArrayBufferView
32
+ return Buffer.from(v.buffer, v.byteOffset, v.byteLength)
33
+ }
34
+ const o = input as unknown as Record<string, unknown>
35
+ const p = o.path ?? o.filePath
36
+ if (typeof p === 'string' && p.trim()) return p.trim()
37
+ }
38
+ return input
39
+ }
40
+
41
+ function resolveMime(input: File | string | Buffer): string {
42
+ if (typeof input !== 'string' && !Buffer.isBuffer(input) && input.type) {
43
+ return input.type
44
+ }
45
+ return 'application/octet-stream'
46
+ }
47
+
48
+ /**
49
+ * 将 File/路径/Buffer 转为 ali-oss 接受的类型。
50
+ * 本地路径保持为字符串:put 内部用 contentLength + ReadStream,大文件也稳定。
51
+ */
52
+ async function toUploadContent(input: File | string | Buffer): Promise<{ content: Buffer | string; fileName: string }> {
11
53
  if (Buffer.isBuffer(input)) {
12
54
  return { content: input, fileName: 'file' }
13
55
  }
14
56
  if (typeof input === 'string') {
15
57
  return {
16
- content: createReadStream(input),
17
- fileName: input.split('/').pop() ?? 'file'
58
+ content: input,
59
+ fileName: input.split(/[/\\]/).pop() ?? 'file'
18
60
  }
19
61
  }
20
- // File: ali-oss 需要 Buffer/Stream,用 arrayBuffer 转 Buffer
21
62
  const buf = Buffer.from(await input.arrayBuffer())
22
- return { content: buf, fileName: input.name }
63
+ const n = (input as { name?: string }).name
64
+ return { content: buf, fileName: typeof n === 'string' && n ? n : 'file' }
23
65
  }
24
66
 
25
- export const ossUpload = async (file: File | string | Buffer, botToken: string, isPrivate: 0 | 1 = 1) => {
67
+ export type OssUploadOptions = {
68
+ /** 分片上传进度,p 为 0~1(仅大 Buffer 分片时触发) */
69
+ onProgress?: (p: number) => void
70
+ /** HTTP 超时(毫秒),覆盖默认 15 分钟;可传 `30 * 60 * 1000` 等 */
71
+ timeoutMs?: number
72
+ }
73
+
74
+ export const ossUpload = async (
75
+ rawFile: File | string | Buffer,
76
+ botToken: string,
77
+ isPrivate: 0 | 1 = 1,
78
+ uploadOptions?: OssUploadOptions
79
+ ) => {
26
80
  await getUserToken(botToken)
27
81
 
82
+ const file = coerceOssFileInput(rawFile)
28
83
  const { content, fileName } = await toUploadContent(file)
29
84
  const data = await getStsToken(fileName, botToken, isPrivate)
85
+ const mime = resolveMime(file)
86
+ const onProgress = uploadOptions?.onProgress
30
87
 
31
88
  const options: OSS.Options = {
32
89
  // 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。
@@ -40,7 +97,8 @@ export const ossUpload = async (file: File | string | Buffer, botToken: string,
40
97
  region: data.region,
41
98
  secure: true,
42
99
  cname: true,
43
- authorizationV4: true
100
+ authorizationV4: true,
101
+ timeout: uploadOptions?.timeoutMs ?? OSS_HTTP_TIMEOUT_MS
44
102
  }
45
103
 
46
104
  const client = new OSS(options)
@@ -48,12 +106,22 @@ export const ossUpload = async (file: File | string | Buffer, botToken: string,
48
106
  const name = `${data.uploadDir}${data.ossFileKey}`
49
107
 
50
108
  try {
51
- const objectResult = await client.put(name, content)
109
+ let objectResult: OSS.PutObjectResult | OSS.CompleteMultipartUploadResult
110
+
111
+ const multipartUploadOptions: OSS.MultipartUploadOptions = {
112
+ progress: (p: number) => {
113
+ onProgress?.(p)
114
+ },
115
+ parallel: 4,
116
+ partSize: MULTIPART_PART_SIZE,
117
+ mime
118
+ }
119
+ objectResult = await client.multipartUpload(name, content, multipartUploadOptions)
120
+
52
121
  if (objectResult?.res?.status !== 200) {
53
122
  dcgLogger(`OSS 上传失败, ${objectResult?.res?.status}`)
54
123
  }
55
124
  dcgLogger(`OSS 上传成功, ${objectResult.name || objectResult.url}`)
56
- // const url = `${data.protocol || 'http'}://${data.bucket}.${data.endPoint}/${data.uploadDir}${data.ossFileKey}`
57
125
  return isPrivate === 1 ? objectResult.name || objectResult.url : objectResult.url
58
126
  } catch (error) {
59
127
  dcgLogger(`OSS 上传失败: ${error}`, 'error')
@@ -1,9 +1,5 @@
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
- }
7
3
 
8
4
  export const systemCommand = ['/new', '/status']
9
5
  export const stopCommand = ['/stop']
@@ -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?.[channelInfo[ENV]]?.appId == 110 ? '.mobook' : '.openclaw',
33
+ config?.channels?.["dcgchat-test"]?.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
- `${channelInfo[ENV]} runtime not initialized`
58
+ `${"dcgchat-test"} 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: channelInfo[ENV],
150
+ channel: "dcgchat-test",
151
151
  accountId: accountId || 'default',
152
152
  peer: { kind: 'direct', id: session_id }
153
153
  })
package/src/utils/log.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { RuntimeEnv } from 'openclaw/plugin-sdk'
2
- import { channelInfo, ENV } from './constant.js'
3
2
 
4
3
  let logger: RuntimeEnv | null = null
5
4
 
@@ -11,6 +10,6 @@ export function dcgLogger(message: string, type: 'log' | 'error' = 'log'): void
11
10
  if (logger) {
12
11
  logger[type](`书灵墨宝🚀 ~ [${new Date().toISOString()}] ${message}`)
13
12
  } else {
14
- console[type](`书灵墨宝🚀 ~ ${new Date().toISOString()} [${channelInfo[ENV]}]: ${message}`)
13
+ console[type](`书灵墨宝🚀 ~ ${new Date().toISOString()} [${"dcgchat-test"}]: ${message}`)
15
14
  }
16
15
  }
@@ -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?.[channelInfo[ENV]] as DcgchatConfig | undefined) ?? {}
12
+ const ch = (getOpenClawConfig()?.channels?.["dcgchat-test"] as DcgchatConfig | undefined) ?? {}
13
13
  return {
14
14
  userId: Number(ch.userId ?? 0),
15
15
  botToken: ch.botToken ?? '',
package/README.md DELETED
@@ -1,83 +0,0 @@
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
- - [ ] 错误消息类型