@dcrays/dcgchat-test 0.3.27 → 0.3.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 { 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.27",
3
+ "version": "0.3.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,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,11 +1,20 @@
1
1
  import path from 'node:path'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
2
4
  import type { ReplyPayload } from 'openclaw/plugin-sdk'
3
5
  import { createReplyPrefixContext } from 'openclaw/plugin-sdk'
4
6
  import type { InboundMessage } from './types.js'
5
- import { clearSentMediaKeys, getDcgchatRuntime, getOpenClawConfig, getSessionKey, setMsgStatus } from './utils/global.js'
7
+ import {
8
+ clearSentMediaKeys,
9
+ getDcgchatRuntime,
10
+ getOpenClawConfig,
11
+ getSessionKey,
12
+ getWorkspaceDir,
13
+ setMsgStatus
14
+ } from './utils/global.js'
6
15
  import { resolveAccount, sendDcgchatMedia } from './channel.js'
7
16
  import { generateSignUrl } from './request/api.js'
8
- import { extractMobookFiles, getFilePathByFile } from './utils/searchFile.js'
17
+ import { extractMobookFiles } from './utils/searchFile.js'
9
18
  import { sendChunk, sendFinal, sendText as sendTextMsg, sendError, wsSendRaw, sendText } from './transport.js'
10
19
  import { dcgLogger } from './utils/log.js'
11
20
  import { channelInfo, systemCommand, interruptCommand, ENV, ignoreToolCommand } from './utils/constant.js'
@@ -147,7 +156,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
147
156
 
148
157
  const route = core.channel.routing.resolveAgentRoute({
149
158
  cfg: config,
150
- channel: channelInfo[ENV],
159
+ channel: "dcgchat-test",
151
160
  accountId: account.accountId,
152
161
  peer: { kind: 'direct', id: conversationId }
153
162
  })
@@ -226,13 +235,13 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
226
235
  ChatType: 'direct',
227
236
  SenderName: agentDisplayName,
228
237
  SenderId: userId,
229
- Provider: channelInfo[ENV],
230
- Surface: channelInfo[ENV],
238
+ Provider: "dcgchat-test",
239
+ Surface: "dcgchat-test",
231
240
  MessageSid: msg.content.message_id,
232
241
  Timestamp: Date.now(),
233
242
  WasMentioned: true,
234
243
  CommandAuthorized: true,
235
- OriginatingChannel: channelInfo[ENV],
244
+ OriginatingChannel: "dcgchat-test",
236
245
  OriginatingTo: effectiveSessionKey,
237
246
  ...mediaPayload
238
247
  })
@@ -244,7 +253,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
244
253
  const prefixContext = createReplyPrefixContext({
245
254
  cfg: config,
246
255
  agentId: effectiveAgentId ?? '',
247
- channel: channelInfo[ENV],
256
+ channel: "dcgchat-test",
248
257
  accountId: account.accountId
249
258
  })
250
259
 
@@ -402,10 +411,19 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
402
411
  if (sessionStreamSuppressed.has(effectiveSessionKey)) {
403
412
  sessionStreamSuppressed.delete(effectiveSessionKey)
404
413
  } else {
405
- const files = extractMobookFiles(completeText)
406
- dcgLogger(`检索到文件:${JSON.stringify(files)}`)
407
- for (const file of files) {
408
- const resolved = getFilePathByFile(file)
414
+ 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))
409
427
  if (!resolved) continue
410
428
  try {
411
429
  await sendDcgchatMedia({ sessionKey: effectiveSessionKey, mediaUrl: resolved, text: '' })
package/src/channel.ts CHANGED
@@ -4,7 +4,6 @@ import type { ResolvedDcgchatAccount, DcgchatConfig } from './types.js'
4
4
  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
- import { channelInfo, ENV } from './utils/constant.js'
8
7
  import { dcgLogger, setLogger } from './utils/log.js'
9
8
  import { getOutboundMsgParams } from './utils/params.js'
10
9
  import { startDcgchatGatewaySocket } from './gateway/socket.js'
@@ -36,7 +35,8 @@ 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 ?? ''
39
+ console.log('🚀 ~ sendDcgchatMedia ~ botToken:', botToken)
40
40
  const url = opts.mediaUrl ? await ossUpload(opts.mediaUrl, botToken, 1) : ''
41
41
  wsSendRaw(msgCtx, {
42
42
  response: opts.text ?? '',
@@ -56,7 +56,7 @@ export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<v
56
56
 
57
57
  export function resolveAccount(cfg: OpenClawConfig, accountId?: string | null): ResolvedDcgchatAccount {
58
58
  const id = accountId ?? DEFAULT_ACCOUNT_ID
59
- const raw = (cfg.channels?.[channelInfo[ENV]] as DcgchatConfig | undefined) ?? {}
59
+ const raw = (cfg.channels?.["dcgchat-test"] as DcgchatConfig | undefined) ?? {}
60
60
  return {
61
61
  accountId: id,
62
62
  enabled: raw.enabled !== false,
@@ -70,13 +70,13 @@ export function resolveAccount(cfg: OpenClawConfig, accountId?: string | null):
70
70
  }
71
71
 
72
72
  export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
73
- id: channelInfo[ENV],
73
+ id: "dcgchat-test",
74
74
  meta: {
75
- id: channelInfo[ENV],
75
+ id: "dcgchat-test",
76
76
  label: '书灵墨宝',
77
77
  selectionLabel: '书灵墨宝',
78
78
  docsPath: '/channels/dcgchat',
79
- docsLabel: channelInfo[ENV],
79
+ docsLabel: "dcgchat-test",
80
80
  blurb: '连接 OpenClaw 与 书灵墨宝 产品',
81
81
  order: 80
82
82
  },
@@ -117,7 +117,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
117
117
  channels: {
118
118
  ...cfg.channels,
119
119
  dcgchat: {
120
- ...(cfg.channels?.[channelInfo[ENV]] as Record<string, unknown> | undefined),
120
+ ...(cfg.channels?.["dcgchat-test"] as Record<string, unknown> | undefined),
121
121
  enabled
122
122
  }
123
123
  }
@@ -170,7 +170,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
170
170
  }
171
171
  }
172
172
  return {
173
- channel: channelInfo[ENV],
173
+ channel: "dcgchat-test",
174
174
  messageId: `${messageId}`,
175
175
  chatId: to
176
176
  }
@@ -184,7 +184,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
184
184
  dcgLogger(`channel sendMedia to ${ctx.to}`)
185
185
  await sendDcgchatMedia({ sessionKey: to ?? '', mediaUrl: ctx.mediaUrl ?? '' })
186
186
  return {
187
- channel: channelInfo[ENV],
187
+ channel: "dcgchat-test",
188
188
  messageId: `${messageId}`,
189
189
  chatId: msgCtx.userId?.toString()
190
190
  }
@@ -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
@@ -40,7 +40,7 @@ export const queryUserTokenByBotToken = async (botToken: string): Promise<string
40
40
  const response = await post<{ botToken: string }, { token: string }>('/organization/queryUserTokenByBotToken', { botToken })
41
41
 
42
42
  if (!response || !response.data || !response.data.token) {
43
- dcgLogger('获取绑定的用户信息失败', 'error')
43
+ dcgLogger('获取绑定的用户信息失败: ' + JSON.stringify(response), 'error')
44
44
  return ''
45
45
  }
46
46
 
@@ -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
  }
@@ -1,10 +1,10 @@
1
- import { getWorkspaceDir } from './global.js'
2
1
  import { dcgLogger } from './log.js'
3
- import fs from 'node:fs'
4
- import os from 'node:os'
5
- import path from 'node:path'
6
2
 
7
- /** 参与提取的常见文件扩展名 */
3
+ /**
4
+ * 从文本中提取 /mobook 目录下的文件
5
+ * @param {string} text
6
+ * @returns {string[]}
7
+ */
8
8
  const EXT_LIST = [
9
9
  // 文档类
10
10
  'doc',
@@ -88,56 +88,11 @@ const EXT_LIST = [
88
88
  */
89
89
  const EXT_SORTED_FOR_REGEX = [...EXT_LIST].sort((a, b) => b.length - a.length)
90
90
 
91
- /** 正则交替串(长扩展名优先) */
92
- const EXT_ALT = `(${EXT_SORTED_FOR_REGEX.join('|')})`
93
- /** 文件名片段:中文、常见符号、含 `#`、路径 `/`(多段 /mobook/...)、非贪婪 */
94
- const FILE_NAME_CLASS = `[\\w\\u4e00-\\u9fa5::《》()()\\-\\s#/]+?`
95
-
96
- /** 预编译,避免 extractMobookFiles 每次调用重复构建正则 */
97
- const RX_EXTRACT = {
98
- backtick: new RegExp(`\`([^\\\`]+?\\.${EXT_ALT})\``, 'gi'),
99
- fullPath: new RegExp(`/mobook/${FILE_NAME_CLASS}\\.${EXT_ALT}`, 'gi'),
100
- winMobook: new RegExp(`(?:[a-zA-Z]:)?[/\\\\]mobook[/\\\\]${FILE_NAME_CLASS}\\.${EXT_ALT}`, 'gi'),
101
- inline: new RegExp(`mobook下的\\s*(${FILE_NAME_CLASS}\\.${EXT_ALT})`, 'gi'),
102
- bold: new RegExp(`\\*\\*(${FILE_NAME_CLASS}\\.${EXT_ALT})\\*\\*`, 'gi'),
103
- loose: new RegExp(`(${FILE_NAME_CLASS}\\.${EXT_ALT})\\s*\\(`, 'gi'),
104
- /** Markdown 列表:`- 文件名.ext` — 用 matchAll 取捕获组 1,避免 FILE_NAME 含 `-`/空格 时误把「- 」并入文件名 */
105
- markdownList: new RegExp(`[-*•]\\s+(${FILE_NAME_CLASS}\\.${EXT_ALT})`, 'gi'),
106
- inlineFile: new RegExp(`${FILE_NAME_CLASS}\\.${EXT_ALT}`, 'i')
107
- }
108
-
109
91
  /** 去除控制符、零宽字符等常见脏值 */
110
92
  function stripMobookNoise(s: string) {
111
93
  return s.replace(/[\u0000-\u001F\u007F\u200B-\u200D\u200E\u200F\uFEFF]/g, '')
112
94
  }
113
95
 
114
- /**
115
- * 从路径或 mobook 引用中取出「提到的文件名」:去掉盘符、/mobook、\\mobook\\ 等前缀后取 basename
116
- */
117
- function toMobookReferencedBasename(p: string): string {
118
- let s = stripMobookNoise(p).trim()
119
- if (!s) return ''
120
- s = s.replace(/^(?:[a-zA-Z]:)?[/\\]+mobook[/\\]/i, '')
121
- s = s.replace(/^\/mobook\//i, '')
122
- s = s.replace(/\\/g, '/')
123
- const parts = s.split('/').filter(Boolean)
124
- return parts.length ? parts[parts.length - 1]! : s
125
- }
126
-
127
- /**
128
- * 去掉误带入的 Markdown 列表前缀(`- 」「* 」「• 」后须有空格)。
129
- * loose 等规则里 FILE_NAME 含 `-`/空白,会把「- 西游记_1.png (…」整段捕获成文件名。
130
- * 不要求空格会误伤真实文件名如 `-report.pdf`。
131
- */
132
- function stripMarkdownListPrefixFromBasename(name: string): string {
133
- return name.replace(/^[-*•]\s+/, '')
134
- }
135
-
136
- function addMobookMentionedFile(result: Set<string>, raw: string) {
137
- const base = stripMarkdownListPrefixFromBasename(toMobookReferencedBasename(raw))
138
- if (base && isValidFileName(base)) result.add(base)
139
- }
140
-
141
96
  /**
142
97
  * 从文本中扫描 `.../mobook/...` 或 `...\mobook\...` 片段,按最长后缀匹配合法扩展名(兜底)
143
98
  */
@@ -172,10 +127,8 @@ function collectMobookPathsAfterNeedle(text: string, lower: string, needle: stri
172
127
  }
173
128
  const base = raw.slice(0, -(matchedExt.length + 1))
174
129
  const fileName = `${base}.${matchedExt}`
175
- // 可能是 mobook 下多级路径;整段含 / 会被 isValidFileName 误判为非法,只校验最后一段(与 addMobookMentionedFile 最终取的 basename 一致)
176
- const leaf = path.basename(fileName.replace(/\\/g, '/'))
177
- if (isValidFileName(leaf)) {
178
- addMobookMentionedFile(result, fileName)
130
+ if (isValidFileName(fileName)) {
131
+ result.add(normalizePath(`/mobook/${fileName}`))
179
132
  }
180
133
  from = start + 1
181
134
  }
@@ -187,38 +140,62 @@ function collectMobookPathsByScan(text: string, result: Set<string>): void {
187
140
  collectMobookPathsAfterNeedle(text, lower, '\\mobook\\', result)
188
141
  }
189
142
 
190
- /**
191
- * 从文本中提取提到的 mobook 相关文件名(仅 basename,不含目录)
192
- * @param text 原始文本
193
- * @returns 去重后的文件名列表,例如 `['报告.pdf', 'data.xlsx']`
194
- */
195
143
  export function extractMobookFiles(text = '') {
196
144
  if (typeof text !== 'string' || !text.trim()) return []
197
145
  // 全角冒号(中文输入常见)→ 半角,便于匹配 c:\mobook\
198
146
  text = text.replace(/\uFF1A/g, ':')
199
147
  const result = new Set<string>()
148
+ // ✅ 扩展名(必须长扩展名优先,见 EXT_SORTED_FOR_REGEX)
149
+ const EXT = `(${EXT_SORTED_FOR_REGEX.join('|')})`
150
+ // ✅ 文件名字符(增强:支持中文、符号)
151
+ const FILE_NAME = `[\\w\\u4e00-\\u9fa5::《》()()\\-\\s]+?`
200
152
  try {
201
- ;(text.match(RX_EXTRACT.backtick) || []).forEach((item) => {
202
- addMobookMentionedFile(result, item.replace(/`/g, '').trim())
153
+ // 1️⃣ `xxx.xxx`
154
+ const backtickReg = new RegExp(`\`([^\\\`]+?\\.${EXT})\``, 'gi')
155
+ ;(text.match(backtickReg) || []).forEach((item) => {
156
+ const name = item.replace(/`/g, '').trim()
157
+ if (isValidFileName(name)) {
158
+ result.add(`/mobook/${name}`)
159
+ }
203
160
  })
204
- ;(text.match(RX_EXTRACT.fullPath) || []).forEach((p) => addMobookMentionedFile(result, p))
205
- ;(text.match(RX_EXTRACT.winMobook) || []).forEach((full) => {
161
+ // 2️⃣ /mobook/xxx.xxx
162
+ const fullPathReg = new RegExp(`/mobook/${FILE_NAME}\\.${EXT}`, 'gi')
163
+ ;(text.match(fullPathReg) || []).forEach((p) => {
164
+ result.add(normalizePath(p))
165
+ })
166
+ // 2️⃣b Windows 实际保存路径:C:\mobook\xxx、c:/mobook/xxx、\mobook\xxx(模型常写反斜杠,原先无法识别)
167
+ const winMobookReg = new RegExp(`(?:[a-zA-Z]:)?[/\\\\]mobook[/\\\\]${FILE_NAME}\\.${EXT}`, 'gi')
168
+ ;(text.match(winMobookReg) || []).forEach((full) => {
206
169
  const name = full.replace(/^(?:[a-zA-Z]:)?[/\\\\]mobook[/\\\\]/i, '').trim()
207
- addMobookMentionedFile(result, name)
170
+ if (isValidFileName(name)) {
171
+ result.add(normalizePath(`/mobook/${name}`))
172
+ }
208
173
  })
209
- ;(text.match(RX_EXTRACT.inline) || []).forEach((item) => {
210
- const match = item.match(RX_EXTRACT.inlineFile)
211
- if (match?.[0]) addMobookMentionedFile(result, match[0].trim())
174
+ // 3️⃣ mobook下的 xxx.xxx
175
+ const inlineReg = new RegExp(`mobook下的\\s*(${FILE_NAME}\\.${EXT})`, 'gi')
176
+ ;(text.match(inlineReg) || []).forEach((item) => {
177
+ const match = item.match(new RegExp(`${FILE_NAME}\\.${EXT}`, 'i'))
178
+ if (match && isValidFileName(match[0])) {
179
+ result.add(`/mobook/${match[0].trim()}`)
180
+ }
212
181
  })
213
- ;(text.match(RX_EXTRACT.bold) || []).forEach((item) => {
214
- addMobookMentionedFile(result, item.replace(/\*\*/g, '').trim())
182
+ // 🆕 4️⃣ **xxx.xxx**
183
+ const boldReg = new RegExp(`\\*\\*(${FILE_NAME}\\.${EXT})\\*\\*`, 'gi')
184
+ ;(text.match(boldReg) || []).forEach((item) => {
185
+ const name = item.replace(/\*\*/g, '').trim()
186
+ if (isValidFileName(name)) {
187
+ result.add(`/mobook/${name}`)
188
+ }
215
189
  })
216
- ;(text.match(RX_EXTRACT.loose) || []).forEach((item) => {
217
- addMobookMentionedFile(result, item.replace(/\s*\(.+$/, '').trim())
190
+ // 🆕 5️⃣ xxx.xxx (123字节)
191
+ const looseReg = new RegExp(`(${FILE_NAME}\\.${EXT})\\s*\\(`, 'gi')
192
+ ;(text.match(looseReg) || []).forEach((item) => {
193
+ const name = item.replace(/\s*\(.+$/, '').trim()
194
+ if (isValidFileName(name)) {
195
+ result.add(`/mobook/${name}`)
196
+ }
218
197
  })
219
- for (const m of text.matchAll(RX_EXTRACT.markdownList)) {
220
- if (m[1]) addMobookMentionedFile(result, m[1].trim())
221
- }
198
+ // 6️⃣ 兜底:绝对路径等 `.../mobook/<文件名>.<扩展名>` + 最长后缀匹配 + 去脏字符
222
199
  collectMobookPathsByScan(text, result)
223
200
  } catch (e) {
224
201
  dcgLogger(`extractMobookFiles error:${e}`)
@@ -241,94 +218,11 @@ function isValidFileName(name: string) {
241
218
  return true
242
219
  }
243
220
 
244
- /** mobook 下按文件名递归查找:仅在直线路径均失败时调用;有深度/目录数上限,找到即停 */
245
- const MOBOOK_FIND_MAX_DEPTH = 10
246
- const MOBOOK_FIND_MAX_DIRS = 2000
247
-
248
- function findMobookFileByBasename(mobookRoot: string, basename: string): string | undefined {
249
- if (!basename || basename === '.' || basename === '..') return undefined
250
- if (!fs.existsSync(mobookRoot)) return undefined
251
- const caseInsensitive = process.platform === 'win32' || process.platform === 'darwin'
252
- const want = caseInsensitive ? basename.toLowerCase() : basename
253
-
254
- const stack: Array<{ dir: string; depth: number }> = [{ dir: mobookRoot, depth: 0 }]
255
- let dirsVisited = 0
256
-
257
- while (stack.length > 0 && dirsVisited < MOBOOK_FIND_MAX_DIRS) {
258
- const { dir, depth } = stack.pop()!
259
- if (depth > MOBOOK_FIND_MAX_DEPTH) continue
260
- dirsVisited += 1
261
-
262
- let entries: fs.Dirent[]
263
- try {
264
- entries = fs.readdirSync(dir, { withFileTypes: true })
265
- } catch {
266
- continue
267
- }
268
-
269
- const subdirs: string[] = []
270
- for (const ent of entries) {
271
- const full = path.join(dir, ent.name)
272
- if (ent.isFile()) {
273
- const ok = caseInsensitive ? ent.name.toLowerCase() === want : ent.name === basename
274
- if (ok) return full
275
- } else if (ent.isDirectory() && depth < MOBOOK_FIND_MAX_DEPTH) {
276
- if (ent.name.startsWith('.')) continue
277
- subdirs.push(full)
278
- }
279
- }
280
- for (let i = subdirs.length - 1; i >= 0; i--) {
281
- stack.push({ dir: subdirs[i]!, depth: depth + 1 })
282
- }
283
- }
284
- return undefined
285
- }
286
-
287
- /** Windows 默认 `C:\mobook`;类 Unix(含 Linux/macOS)为 `~/mobook`。原先 Linux 返回 undefined,会导致只认工作区 mobook、无法回落到用户目录。 */
288
- function getMobookRoot(): string {
289
- if (process.platform === 'win32') return path.join('C:\\', 'mobook')
290
- return path.join(os.homedir(), 'mobook')
291
- }
292
-
293
221
  /**
294
- * 按「文件名或 /mobook/ 相对路径」解析磁盘上的真实路径。
295
- *
296
- * 仍可能找不到的情况:
297
- * - 文件不在候选路径(工作区根、工作区/mobook、用户 mobook)下,或路径写错。
298
- * - Windows 下 mobook 不在 `C:\\mobook`(仅尝试该盘符下的 mobook + 上述候选)。
299
- * - 递归查找超出深度/目录数上限(见 MOBOOK_FIND_*)。
300
- * - 多子目录下存在同名文件:按深度优先先命中哪一个即返回哪一个。
301
- * - Linux 等大小写敏感文件系统:模型输出的文件名大小写与磁盘不一致会失败。
222
+ * 规范路径(去重用)
302
223
  */
303
- export function getFilePathByFile(file: string) {
304
- const ws = getWorkspaceDir()
305
- const candidates: string[] = [file]
306
- candidates.push(path.join(ws, file))
307
- candidates.push(path.join(ws, file.replace(/^\//, '')))
308
- const underMobook = file.replace(/^\/mobook\//i, '').replace(/^mobook[\\/]/i, '')
309
- const workspaceMobookRoot = path.join(ws, 'mobook')
310
- const homeMobookRoot = underMobook ? getMobookRoot() : undefined
311
-
312
- if (underMobook) {
313
- if (fs.existsSync(workspaceMobookRoot)) {
314
- candidates.push(path.join(workspaceMobookRoot, underMobook))
315
- }
316
- if (homeMobookRoot) {
317
- candidates.push(path.join(homeMobookRoot, underMobook))
318
- }
319
- }
320
- const resolved = candidates.find((p) => fs.existsSync(p))
321
- if (resolved) return resolved
322
-
323
- if (!underMobook) return undefined
324
- const base = path.basename(underMobook)
325
- if (fs.existsSync(workspaceMobookRoot)) {
326
- const inWorkspace = findMobookFileByBasename(workspaceMobookRoot, base)
327
- if (inWorkspace) return inWorkspace
328
- }
329
- if (homeMobookRoot) {
330
- const inHome = findMobookFileByBasename(homeMobookRoot, base)
331
- if (inHome) return inHome
332
- }
333
- return undefined
224
+ function normalizePath(path: string) {
225
+ return path
226
+ .replace(/\/+/g, '/') // 多斜杠 → 单斜杠
227
+ .replace(/\/$/, '') // 去掉结尾 /
334
228
  }
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
- - [ ] 错误消息类型