@dcrays/dcgchat-test 0.3.21 → 0.3.23

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 { startDcgchatGatewaySocket } from './src/gateway/socket.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.3.21",
3
+ "version": "0.3.23",
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,18 +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
44
  }
54
45
  }
package/src/bot.ts CHANGED
@@ -1,20 +1,11 @@
1
- import fs from 'node:fs'
2
1
  import path from 'node:path'
3
2
  import type { ReplyPayload } from 'openclaw/plugin-sdk'
4
3
  import { createReplyPrefixContext } from 'openclaw/plugin-sdk'
5
4
  import type { InboundMessage } from './types.js'
6
- import os from 'node:os'
7
- import {
8
- clearSentMediaKeys,
9
- getDcgchatRuntime,
10
- getOpenClawConfig,
11
- getSessionKey,
12
- getWorkspaceDir,
13
- setMsgStatus
14
- } from './utils/global.js'
5
+ import { clearSentMediaKeys, getDcgchatRuntime, getOpenClawConfig, getSessionKey, setMsgStatus } from './utils/global.js'
15
6
  import { resolveAccount, sendDcgchatMedia } from './channel.js'
16
7
  import { generateSignUrl } from './request/api.js'
17
- import { extractMobookFiles } from './utils/searchFile.js'
8
+ import { extractMobookFiles, getFilePathByFile } from './utils/searchFile.js'
18
9
  import { sendChunk, sendFinal, sendText as sendTextMsg, sendError, wsSendRaw, sendText } from './transport.js'
19
10
  import { dcgLogger } from './utils/log.js'
20
11
  import { channelInfo, systemCommand, interruptCommand, ENV, ignoreToolCommand } from './utils/constant.js'
@@ -156,7 +147,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
156
147
 
157
148
  const route = core.channel.routing.resolveAgentRoute({
158
149
  cfg: config,
159
- channel: channelInfo[ENV],
150
+ channel: "dcgchat-test",
160
151
  accountId: account.accountId,
161
152
  peer: { kind: 'direct', id: conversationId }
162
153
  })
@@ -235,13 +226,13 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
235
226
  ChatType: 'direct',
236
227
  SenderName: agentDisplayName,
237
228
  SenderId: userId,
238
- Provider: channelInfo[ENV],
239
- Surface: channelInfo[ENV],
229
+ Provider: "dcgchat-test",
230
+ Surface: "dcgchat-test",
240
231
  MessageSid: msg.content.message_id,
241
232
  Timestamp: Date.now(),
242
233
  WasMentioned: true,
243
234
  CommandAuthorized: true,
244
- OriginatingChannel: channelInfo[ENV],
235
+ OriginatingChannel: "dcgchat-test",
245
236
  OriginatingTo: effectiveSessionKey,
246
237
  ...mediaPayload
247
238
  })
@@ -253,7 +244,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
253
244
  const prefixContext = createReplyPrefixContext({
254
245
  cfg: config,
255
246
  agentId: effectiveAgentId ?? '',
256
- channel: channelInfo[ENV],
247
+ channel: "dcgchat-test",
257
248
  accountId: account.accountId
258
249
  })
259
250
 
@@ -412,18 +403,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
412
403
  sessionStreamSuppressed.delete(effectiveSessionKey)
413
404
  } else {
414
405
  for (const file of extractMobookFiles(completeText)) {
415
- const candidates: string[] = [file]
416
- candidates.push(path.join(getWorkspaceDir(), file))
417
- candidates.push(path.join(getWorkspaceDir(), file.replace(/^\//, '')))
418
- const underMobook = file.replace(/^\/mobook\//i, '').replace(/^mobook[\\/]/i, '')
419
- if (underMobook) {
420
- if (process.platform === 'win32') {
421
- candidates.push(path.join('C:\\', 'mobook', underMobook))
422
- } else if (process.platform === 'darwin') {
423
- candidates.push(path.join(os.homedir(), 'mobook', underMobook))
424
- }
425
- }
426
- const resolved = candidates.find((p) => fs.existsSync(p))
406
+ const resolved = getFilePathByFile(file)
427
407
  if (!resolved) continue
428
408
  try {
429
409
  await sendDcgchatMedia({ sessionKey: effectiveSessionKey, mediaUrl: resolved, text: '' })
package/src/channel.ts CHANGED
@@ -5,7 +5,6 @@ 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'
9
8
  import { getEffectiveMsgParams, getCurrentSessionKey } from './utils/params.js'
10
9
  import { startDcgchatGatewaySocket } from './gateway/socket.js'
11
10
 
@@ -36,7 +35,7 @@ export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<v
36
35
  const fileName = mediaUrl?.split(/[\\/]/).pop() || ''
37
36
 
38
37
  try {
39
- const botToken = msgCtx.botToken ?? getOpenClawConfig()?.channels?.[channelInfo[ENV]]?.botToken ?? ''
38
+ const botToken = msgCtx.botToken ?? getOpenClawConfig()?.channels?.["dcgchat-test"]?.botToken ?? ''
40
39
  const url = opts.mediaUrl ? await ossUpload(opts.mediaUrl, botToken, 1) : ''
41
40
  wsSendRaw(msgCtx, {
42
41
  response: opts.text ?? '',
@@ -56,7 +55,7 @@ export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<v
56
55
 
57
56
  export function resolveAccount(cfg: OpenClawConfig, accountId?: string | null): ResolvedDcgchatAccount {
58
57
  const id = accountId ?? DEFAULT_ACCOUNT_ID
59
- const raw = (cfg.channels?.[channelInfo[ENV]] as DcgchatConfig | undefined) ?? {}
58
+ const raw = (cfg.channels?.["dcgchat-test"] as DcgchatConfig | undefined) ?? {}
60
59
  return {
61
60
  accountId: id,
62
61
  enabled: raw.enabled !== false,
@@ -70,13 +69,13 @@ export function resolveAccount(cfg: OpenClawConfig, accountId?: string | null):
70
69
  }
71
70
 
72
71
  export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
73
- id: channelInfo[ENV],
72
+ id: "dcgchat-test",
74
73
  meta: {
75
- id: channelInfo[ENV],
74
+ id: "dcgchat-test",
76
75
  label: '书灵墨宝',
77
76
  selectionLabel: '书灵墨宝',
78
77
  docsPath: '/channels/dcgchat',
79
- docsLabel: channelInfo[ENV],
78
+ docsLabel: "dcgchat-test",
80
79
  blurb: '连接 OpenClaw 与 书灵墨宝 产品',
81
80
  order: 80
82
81
  },
@@ -117,7 +116,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
117
116
  channels: {
118
117
  ...cfg.channels,
119
118
  dcgchat: {
120
- ...(cfg.channels?.[channelInfo[ENV]] as Record<string, unknown> | undefined),
119
+ ...(cfg.channels?.["dcgchat-test"] as Record<string, unknown> | undefined),
121
120
  enabled
122
121
  }
123
122
  }
@@ -170,7 +169,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
170
169
  }
171
170
  }
172
171
  return {
173
- channel: channelInfo[ENV],
172
+ channel: "dcgchat-test",
174
173
  messageId: `${messageId}`,
175
174
  chatId: to
176
175
  }
@@ -184,7 +183,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
184
183
  dcgLogger(`channel sendMedia to ${ctx.to}`)
185
184
  await sendDcgchatMedia({ sessionKey: to ?? '', mediaUrl: ctx.mediaUrl ?? '' })
186
185
  return {
187
- channel: channelInfo[ENV],
186
+ channel: "dcgchat-test",
188
187
  messageId: `${messageId}`,
189
188
  chatId: msgCtx.userId?.toString()
190
189
  }
package/src/cron.ts CHANGED
@@ -1,8 +1,15 @@
1
1
  import path from 'node:path'
2
2
  import fs from 'node:fs'
3
3
  import type { IMsgParams } from './types.js'
4
- import { mergeDefaultParams, sendEventMessage, sendFinal } from './transport.js'
5
- import { getCronMessageId, getWorkspaceDir, removeCronMessageId, setCronMessageId, setMsgStatus } from './utils/global.js'
4
+ import { isWsOpen, mergeDefaultParams, sendEventMessage, sendFinal } from './transport.js'
5
+ import {
6
+ getCronMessageId,
7
+ getWorkspaceDir,
8
+ getWsConnection,
9
+ removeCronMessageId,
10
+ setCronMessageId,
11
+ setMsgStatus
12
+ } from './utils/global.js'
6
13
  import { ossUpload } from './request/oss.js'
7
14
  import { dcgLogger } from './utils/log.js'
8
15
  import { sendMessageToGateway } from './gateway/socket.js'
@@ -16,13 +23,13 @@ export function getCronJobsPath(): string {
16
23
 
17
24
  type CronJobsFile = {
18
25
  version?: number
19
- jobs?: Array<{ id?: string; sessionKey?: string }>
26
+ jobs?: Array<{ id?: string; sessionKey?: string; name?: string }>
20
27
  }
21
28
 
22
29
  /**
23
30
  * 在 `jobPath` 指向的 jobs.json(通常为 getCronJobsPath())中按 id 查找任务并返回其 sessionKey。
24
31
  */
25
- export function readCronJobSessionKey(jobPath: string, jobId: string): string | null {
32
+ export function readCronJob(jobPath: string, jobId: string): Record<string, any> | null {
26
33
  const id = jobId?.trim()
27
34
  if (!id) return null
28
35
  if (!fs.existsSync(jobPath)) {
@@ -33,8 +40,7 @@ export function readCronJobSessionKey(jobPath: string, jobId: string): string |
33
40
  const raw = fs.readFileSync(jobPath, 'utf8')
34
41
  const data = JSON.parse(raw) as CronJobsFile
35
42
  const job = (data.jobs ?? []).find((j) => j.id === id)
36
- const sk = job?.sessionKey?.trim()
37
- return sk || null
43
+ return job || null
38
44
  } catch (e) {
39
45
  dcgLogger(`readCronJobSessionKey: failed to read ${jobPath}: ${String(e)}`, 'error')
40
46
  return null
@@ -131,13 +137,19 @@ export const onRunCronJob = async (jobId: string, messageId: string) => {
131
137
  return
132
138
  }
133
139
  const jobPath = getCronJobsPath()
134
- const sessionKey = readCronJobSessionKey(jobPath, jobId) || ''
140
+ const { sessionKey } = readCronJob(jobPath, jobId) || {}
135
141
  if (!sessionKey) {
136
142
  dcgLogger(`finishedDcgchatCron: no sessionKey for job id=${id}`, 'error')
137
143
  return
138
144
  }
139
145
  setCronMessageId(sessionKey, messageId)
140
- sendMessageToGateway(JSON.stringify({ method: 'cron.run', params: { id: jobId } }))
146
+ sendMessageToGateway(
147
+ JSON.stringify({
148
+ method: 'cron.runs',
149
+ params: { scope: 'job', id: jobId, limit: 50, offset: 0, status: 'all', sortDir: 'desc' }
150
+ })
151
+ )
152
+ sendMessageToGateway(JSON.stringify({ method: 'cron.run', params: { id: jobId, mode: 'force' } }))
141
153
  }
142
154
  export const finishedDcgchatCron = async (jobId: string) => {
143
155
  const id = jobId?.trim()
@@ -146,20 +158,20 @@ export const finishedDcgchatCron = async (jobId: string) => {
146
158
  return
147
159
  }
148
160
  const jobPath = getCronJobsPath()
149
- const sessionKey = readCronJobSessionKey(jobPath, id)
161
+ const { sessionKey, name } = readCronJob(jobPath, id) || {}
150
162
  if (!sessionKey) {
151
163
  dcgLogger(`finishedDcgchatCron: no sessionKey for job id=${id}`, 'error')
152
164
  return
153
165
  }
154
166
  const outboundCtx = getEffectiveMsgParams(sessionKey)
155
167
  const messageId = getCronMessageId(sessionKey)
168
+ const sessionInfo = sessionKey.split(':')
169
+ const sessionId = sessionInfo.at(-1) ?? ''
170
+ const agentId = sessionInfo.at(-2) ?? ''
156
171
  if (outboundCtx?.sessionId) {
157
172
  const newCtx = messageId ? { ...outboundCtx, messageId } : outboundCtx
158
173
  sendFinal(newCtx, 'cron send')
159
174
  } else {
160
- const sessionInfo = sessionKey.split(':')
161
- const sessionId = sessionInfo.at(-1) ?? ''
162
- const agentId = sessionInfo.at(-2) ?? ''
163
175
  const merged = mergeDefaultParams({
164
176
  agentId: agentId,
165
177
  sessionId: `${sessionId}`,
@@ -169,6 +181,24 @@ export const finishedDcgchatCron = async (jobId: string) => {
169
181
  })
170
182
  sendFinal(merged, 'cron send')
171
183
  }
184
+ const ws = getWsConnection()
185
+ if (isWsOpen()) {
186
+ ws?.send(
187
+ JSON.stringify({
188
+ messageType: 'openclaw_bot_event',
189
+ source: 'client',
190
+ content: {
191
+ event_type: 'notify',
192
+ operation_type: 'cron',
193
+ session_id: sessionId,
194
+ agentId: agentId,
195
+ real_mobook: !sessionId ? 1 : '',
196
+ title: name
197
+ }
198
+ })
199
+ )
200
+ dcgLogger(`定时任务执行成功: ${id}`)
201
+ }
172
202
  removeCronMessageId(sessionKey)
173
203
  dcgLogger(`finishedDcgchatCron: job=${id} sessionKey=${sessionKey}`)
174
204
  }
@@ -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 = `dcg-cron:${sk}`
129
129
  ;(newParams.delivery as CronDelivery).accountId = agentId
130
- ;(newParams.delivery as CronDelivery).channel = channelInfo[ENV]
130
+ ;(newParams.delivery as CronDelivery).channel = "dcgchat-test"
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 = `dcg-cron:${sk}`
140
140
  ;(newParams.delivery as CronDelivery).accountId = agentId
141
- ;(newParams.delivery as CronDelivery).channel = channelInfo[ENV]
141
+ ;(newParams.delivery as CronDelivery).channel = "dcgchat-test"
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 ${channelInfo[ENV]} --to dcg-cron:${sk} --json`
179
+ params.command.replace('--json', '') + ` --session-key ${sk} --channel ${"dcgchat-test"} --to dcg-cron:${sk} --json`
180
180
  return { params: newParams }
181
181
  } else {
182
182
  return params
@@ -4,6 +4,7 @@ import crypto from 'crypto'
4
4
  import { deriveDeviceIdFromPublicKey, publicKeyRawBase64UrlFromPem, buildDeviceAuthPayloadV3, signDevicePayload } from './security.js'
5
5
  import { dcgLogger } from '../utils/log.js'
6
6
  import { finishedDcgchatCron, sendDcgchatCron } from '../cron.js'
7
+ import { getWorkspaceDir } from '../utils/global.js'
7
8
 
8
9
  export interface GatewayEvent {
9
10
  type: string
@@ -114,7 +115,8 @@ export class GatewayConnection {
114
115
  private loadOrCreateDeviceIdentity() {
115
116
  const fs = require('fs')
116
117
  const path = require('path')
117
- const stateDir = path.join(process.cwd(), '.state')
118
+ const workspaceDir = getWorkspaceDir()
119
+ const stateDir = path.join(workspaceDir, '.state')
118
120
  const deviceFile = path.join(stateDir, 'device.json')
119
121
 
120
122
  // Try to load existing identity
@@ -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 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?.[channelInfo[ENV]]?.appId == 110 ? '.mobook' : '.openclaw',
36
+ config?.channels?.["dcgchat-test"]?.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: channelInfo[ENV],
126
+ channel: "dcgchat-test",
127
127
  accountId: accountId || 'default',
128
128
  peer: { kind: 'direct', id: session_id }
129
129
  })
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
  }
@@ -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?.[channelInfo[ENV]] as DcgchatConfig | undefined) ?? {}
15
+ const ch = (getOpenClawConfig()?.channels?.["dcgchat-test"] as DcgchatConfig | undefined) ?? {}
16
16
  return {
17
17
  userId: Number(ch.userId ?? 0),
18
18
  botToken: ch.botToken ?? '',
@@ -1,4 +1,8 @@
1
+ import { getWorkspaceDir } from './global.js'
1
2
  import { dcgLogger } from './log.js'
3
+ import fs from 'node:fs'
4
+ import os from 'node:os'
5
+ import path from 'node:path'
2
6
 
3
7
  /**
4
8
  * 从文本中提取 /mobook 目录下的文件
@@ -155,7 +159,7 @@ export function extractMobookFiles(text = '') {
155
159
  ;(text.match(backtickReg) || []).forEach((item) => {
156
160
  const name = item.replace(/`/g, '').trim()
157
161
  if (isValidFileName(name)) {
158
- result.add(`/mobook/${name}`)
162
+ result.add(`${name}`)
159
163
  }
160
164
  })
161
165
  // 2️⃣ /mobook/xxx.xxx
@@ -168,7 +172,7 @@ export function extractMobookFiles(text = '') {
168
172
  ;(text.match(winMobookReg) || []).forEach((full) => {
169
173
  const name = full.replace(/^(?:[a-zA-Z]:)?[/\\\\]mobook[/\\\\]/i, '').trim()
170
174
  if (isValidFileName(name)) {
171
- result.add(normalizePath(`/mobook/${name}`))
175
+ result.add(normalizePath(`${name}`))
172
176
  }
173
177
  })
174
178
  // 3️⃣ mobook下的 xxx.xxx
@@ -184,7 +188,7 @@ export function extractMobookFiles(text = '') {
184
188
  ;(text.match(boldReg) || []).forEach((item) => {
185
189
  const name = item.replace(/\*\*/g, '').trim()
186
190
  if (isValidFileName(name)) {
187
- result.add(`/mobook/${name}`)
191
+ result.add(`${name}`)
188
192
  }
189
193
  })
190
194
  // 🆕 5️⃣ xxx.xxx (123字节)
@@ -192,7 +196,7 @@ export function extractMobookFiles(text = '') {
192
196
  ;(text.match(looseReg) || []).forEach((item) => {
193
197
  const name = item.replace(/\s*\(.+$/, '').trim()
194
198
  if (isValidFileName(name)) {
195
- result.add(`/mobook/${name}`)
199
+ result.add(`${name}`)
196
200
  }
197
201
  })
198
202
  // 6️⃣ 兜底:绝对路径等 `.../mobook/<文件名>.<扩展名>` + 最长后缀匹配 + 去脏字符
@@ -226,3 +230,85 @@ function normalizePath(path: string) {
226
230
  .replace(/\/+/g, '/') // 多斜杠 → 单斜杠
227
231
  .replace(/\/$/, '') // 去掉结尾 /
228
232
  }
233
+
234
+ /** mobook 下按文件名递归查找:仅在直线路径均失败时调用;有深度/目录数上限,找到即停 */
235
+ const MOBOOK_FIND_MAX_DEPTH = 10
236
+ const MOBOOK_FIND_MAX_DIRS = 2000
237
+
238
+ function findMobookFileByBasename(mobookRoot: string, basename: string): string | undefined {
239
+ if (!basename || basename === '.' || basename === '..') return undefined
240
+ if (!fs.existsSync(mobookRoot)) return undefined
241
+ const caseInsensitive = process.platform === 'win32' || process.platform === 'darwin'
242
+ const want = caseInsensitive ? basename.toLowerCase() : basename
243
+
244
+ const stack: Array<{ dir: string; depth: number }> = [{ dir: mobookRoot, depth: 0 }]
245
+ let dirsVisited = 0
246
+
247
+ while (stack.length > 0 && dirsVisited < MOBOOK_FIND_MAX_DIRS) {
248
+ const { dir, depth } = stack.pop()!
249
+ if (depth > MOBOOK_FIND_MAX_DEPTH) continue
250
+ dirsVisited += 1
251
+
252
+ let entries: fs.Dirent[]
253
+ try {
254
+ entries = fs.readdirSync(dir, { withFileTypes: true })
255
+ } catch {
256
+ continue
257
+ }
258
+
259
+ const subdirs: string[] = []
260
+ for (const ent of entries) {
261
+ const full = path.join(dir, ent.name)
262
+ if (ent.isFile()) {
263
+ const ok = caseInsensitive ? ent.name.toLowerCase() === want : ent.name === basename
264
+ if (ok) return full
265
+ } else if (ent.isDirectory() && depth < MOBOOK_FIND_MAX_DEPTH) {
266
+ if (ent.name.startsWith('.')) continue
267
+ subdirs.push(full)
268
+ }
269
+ }
270
+ for (let i = subdirs.length - 1; i >= 0; i--) {
271
+ stack.push({ dir: subdirs[i]!, depth: depth + 1 })
272
+ }
273
+ }
274
+ return undefined
275
+ }
276
+
277
+ function getMobookRoot(): string | undefined {
278
+ if (process.platform === 'win32') return path.join('C:\\', 'mobook')
279
+ if (process.platform === 'darwin') return path.join(os.homedir(), 'mobook')
280
+ return undefined
281
+ }
282
+
283
+ export function getFilePathByFile(file: string) {
284
+ const ws = getWorkspaceDir()
285
+ const candidates: string[] = [file]
286
+ candidates.push(path.join(ws, file))
287
+ candidates.push(path.join(ws, file.replace(/^\//, '')))
288
+ const underMobook = file.replace(/^\/mobook\//i, '').replace(/^mobook[\\/]/i, '')
289
+ const workspaceMobookRoot = path.join(ws, 'mobook')
290
+ const homeMobookRoot = underMobook ? getMobookRoot() : undefined
291
+
292
+ if (underMobook) {
293
+ if (fs.existsSync(workspaceMobookRoot)) {
294
+ candidates.push(path.join(workspaceMobookRoot, underMobook))
295
+ }
296
+ if (homeMobookRoot) {
297
+ candidates.push(path.join(homeMobookRoot, underMobook))
298
+ }
299
+ }
300
+ const resolved = candidates.find((p) => fs.existsSync(p))
301
+ if (resolved) return resolved
302
+
303
+ if (!underMobook) return undefined
304
+ const base = path.basename(underMobook)
305
+ if (fs.existsSync(workspaceMobookRoot)) {
306
+ const inWorkspace = findMobookFileByBasename(workspaceMobookRoot, base)
307
+ if (inWorkspace) return inWorkspace
308
+ }
309
+ if (homeMobookRoot) {
310
+ const inHome = findMobookFileByBasename(homeMobookRoot, base)
311
+ if (inHome) return inHome
312
+ }
313
+ return undefined
314
+ }
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
- - [ ] 错误消息类型