@dcrays/dcgchat-test 0.3.41 → 0.4.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcrays/dcgchat-test",
3
- "version": "0.3.41",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "description": "OpenClaw channel plugin for 书灵墨宝 (WebSocket)",
6
6
  "main": "index.ts",
package/src/agent.ts CHANGED
@@ -32,20 +32,12 @@ function sendEvent(msgContent: Record<string, any>) {
32
32
  }
33
33
  }
34
34
 
35
- interface ICreateAgentParams {
36
- code: string
37
- workspace: string
38
- name?: string
39
- description?: string
40
- msgContent?: Record<string, any>
41
- }
42
-
43
- /** 若 workspace-${code}/agent 存在,则复制到 agents/${code}/agent */
44
- function copyAgentsFiles(code: string) {
35
+ /** workspace-${clone_code}/agent 存在,则复制到 agents/${clone_code}/agent */
36
+ function copyAgentsFiles(clone_code: string) {
45
37
  const workspacePath = getWorkspaceDir()
46
38
  if (!workspacePath) return
47
- const workspaceDir = path.join(workspacePath, '../', `workspace-${code}`)
48
- const agentDir = path.join(workspacePath, '../', `agents/${code}`)
39
+ const workspaceDir = path.join(workspacePath, '../', `workspace-${clone_code}`)
40
+ const agentDir = path.join(workspacePath, '../', `agents/${clone_code}`)
49
41
  const sourceAgent = path.join(workspaceDir, 'agent')
50
42
  try {
51
43
  if (!fs.existsSync(sourceAgent)) return
@@ -62,15 +54,15 @@ function copyAgentsFiles(code: string) {
62
54
  }
63
55
 
64
56
  export async function onCreateAgent(params: Record<string, any>) {
65
- const { code, name, description } = params
57
+ const { clone_code, name, description } = params
66
58
  try {
67
- await sendMessageToGateway(JSON.stringify({ method: 'agents.create', params: { name: code, workspace: code } }))
59
+ await sendMessageToGateway(JSON.stringify({ method: 'agents.create', params: { name: clone_code, workspace: clone_code } }))
68
60
  } catch (err: unknown) {
69
61
  dcgLogger(`agents.create failed: ${String(err)}`, 'error')
70
62
  }
71
63
  // Update config.name to the user-supplied display name (may contain CJK, spaces, etc.)
72
64
  try {
73
- await sendMessageToGateway(JSON.stringify({ method: 'agents.update', params: { name: name, agentId: code } }))
65
+ await sendMessageToGateway(JSON.stringify({ method: 'agents.update', params: { name: name, agentId: clone_code } }))
74
66
  } catch (err: unknown) {
75
67
  dcgLogger(`agents.update failed: ${String(err)}`, 'error')
76
68
  }
@@ -79,7 +71,7 @@ export async function onCreateAgent(params: Record<string, any>) {
79
71
  await sendMessageToGateway(
80
72
  JSON.stringify({
81
73
  method: 'agents.files.set',
82
- params: { agentId: code, name: 'IDENTITY.md', content: description.trim() }
74
+ params: { agentId: clone_code, name: 'IDENTITY.md', content: description.trim() }
83
75
  })
84
76
  )
85
77
  } catch {
@@ -91,26 +83,26 @@ export async function onCreateAgent(params: Record<string, any>) {
91
83
  await sendMessageToGateway(
92
84
  JSON.stringify({
93
85
  method: 'agents.files.set',
94
- params: { agentId: code, name: 'USER.md', content: name.trim() }
86
+ params: { agentId: clone_code, name: 'USER.md', content: name.trim() }
95
87
  })
96
88
  )
97
89
  } catch {
98
90
  // Non-fatal
99
91
  }
100
92
  }
101
- copyAgentsFiles(code)
93
+ copyAgentsFiles(clone_code)
102
94
  sendEvent({ ...params, status: 'ok' })
103
95
  }
104
96
 
105
97
  export async function createAgent(msgContent: Record<string, any>) {
106
- const { url, code } = msgContent
107
- if (!url || !code) {
108
- dcgLogger(`createAgent failed empty url&code: ${JSON.stringify(msgContent)}`, 'error')
98
+ const { url, clone_code } = msgContent
99
+ if (!url || !clone_code) {
100
+ dcgLogger(`createAgent failed empty url&clone_code: ${JSON.stringify(msgContent)}`, 'error')
109
101
  sendEvent({ ...msgContent, status: 'fail' })
110
102
  return
111
103
  }
112
104
  const workspacePath = getWorkspaceDir()
113
- const workspaceDir = path.join(workspacePath, '../', `workspace-${code}`)
105
+ const workspaceDir = path.join(workspacePath, '../', `workspace-${clone_code}`)
114
106
 
115
107
  // 如果目标目录已存在,先删除
116
108
  if (fs.existsSync(workspaceDir)) {
@@ -159,7 +151,7 @@ export async function createAgent(msgContent: Record<string, any>) {
159
151
  return
160
152
  }
161
153
 
162
- const targetPath = path.join(workspacePath, newPath)
154
+ const targetPath = path.join(workspaceDir, newPath)
163
155
 
164
156
  if (entry.type === 'Directory') {
165
157
  fs.mkdirSync(targetPath, { recursive: true })
package/src/bot.ts CHANGED
@@ -238,7 +238,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
238
238
  if (msg.content.skills_scope.length > 0) {
239
239
  const workspaceDir = getWorkspaceDir()
240
240
  const skillCode = msg.content.skills_scope.map((skill) => `${workspaceDir}/skills/${skill.skill_code}`).join('\n')
241
- const skillText = `在这个目录${skillCode}下有你需要的技能,`
241
+ const skillText = `技能${skillCode} 在目录${skillCode}下,在目录${skillCode}下读取技能 \n`
242
242
  text = skillText ? `${skillText} \n ${text}` : text
243
243
  }
244
244
  const prefixContext = createReplyPrefixContext({
package/src/session.ts CHANGED
@@ -6,11 +6,11 @@ interface TSession {
6
6
  agent_id: string
7
7
  session_id: string
8
8
  agent_clone_code?: string
9
- account_id?: string
9
+ account_id: string
10
10
  }
11
11
 
12
12
  export const onRemoveSession = async ({ agent_id, session_id, agent_clone_code, account_id }: TSession) => {
13
- const sessionKey = getSessionKey({ agent_id, session_id }, account_id)
13
+ const sessionKey = getSessionKey({ agent_id, session_id, agent_clone_code }, account_id)
14
14
  if (!session_id) {
15
15
  dcgLogger('onRemoveSession: empty session_id', 'error')
16
16
  return
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs'
2
+ import os from 'node:os'
2
3
  import path from 'node:path'
3
4
  import type { AnyAgentTool } from 'openclaw/plugin-sdk'
4
5
  import { jsonResult } from 'openclaw/plugin-sdk'
@@ -12,7 +13,39 @@ export type DcgchatMessageToolContext = {
12
13
  workspaceDir?: string
13
14
  }
14
15
 
15
- const SAFE_PREFIXES = ['/workspace/', '/mobook/']
16
+ /** 统一为 POSIX 风格斜杠,便于跨平台判断(不改变语义,仅用于匹配)。 */
17
+ function toPosixPath(p: string): string {
18
+ return path.normalize(p.trim()).replace(/\\/g, '/')
19
+ }
20
+
21
+ /** `filepath` 解析后在 `rootDir` 内或等于 `rootDir`(防 `..` 逃逸)。 */
22
+ function isPathInsideDir(filepath: string, rootDir: string): boolean {
23
+ const root = path.resolve(rootDir)
24
+ const resolved = path.resolve(filepath)
25
+ const rel = path.relative(root, resolved)
26
+ if (rel.startsWith('..') || path.isAbsolute(rel)) return false
27
+ return true
28
+ }
29
+
30
+ /**
31
+ * 允许发送的路径:
32
+ * - 当前 Agent 工作区根及其子路径(`workspaceDir`,如 ~/.openclaw/workspace-xxx/output/...);
33
+ * - 兼容旧挂载:Unix `/workspace`、`/mobook`;Windows 盘符下 `workspace`、`mobook`。
34
+ */
35
+ function isSafePath(filepath: string, workspaceDir?: string): boolean {
36
+ const ws = workspaceDir?.trim()
37
+ if (ws && isPathInsideDir(filepath, ws)) return true
38
+ const p = toPosixPath(filepath)
39
+ if (p.startsWith('/workspace/') || p === '/workspace') return true
40
+ if (p === '/mobook') return true
41
+ return /^[A-Za-z]:\/(workspace|mobook)(\/|$)/.test(p)
42
+ }
43
+
44
+ /** 同一路径在 Windows 上可能大小写不同,用于 Set 去重。 */
45
+ function pathKey(filepath: string): string {
46
+ const n = path.normalize(filepath.trim())
47
+ return os.platform() === 'win32' ? n.toLowerCase() : n
48
+ }
16
49
 
17
50
  const fileType1 = ['.webp', '.gif', '.bmp', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.pdf', '.txt', '.rtf', '.odt', '.json']
18
51
  const fileType2 = ['.xml', '.csv', '.yaml', '.yml', '.html', '.htm', '.md', '.markdown', '.css', '.js', '.ts', '.png', '.jpg', '.jpeg']
@@ -26,8 +59,7 @@ const messageToolParameters = {
26
59
  properties: {
27
60
  target: {
28
61
  type: 'string',
29
- description:
30
- '目标会话键(sessionKey),必须与当前会话 SessionKey 一致,禁止填写 userId。'
62
+ description: '目标会话键(sessionKey),必须与当前会话 SessionKey 一致,禁止填写 userId。'
31
63
  },
32
64
  content: {
33
65
  type: 'string',
@@ -42,7 +74,8 @@ const messageToolParameters = {
42
74
  properties: {
43
75
  file: {
44
76
  type: 'string',
45
- description: '文件路径,例如 /workspace/output/report.pdf'
77
+ description:
78
+ '文件绝对路径:须在「当前 Agent 工作区」目录下(如 /root/.openclaw/workspace-xxx/output/28337/slices_result.json),或为兼容环境的 /workspace/、/mobook/(Windows 盘符下 workspace、mobook)'
46
79
  }
47
80
  },
48
81
  required: ['file']
@@ -52,14 +85,32 @@ const messageToolParameters = {
52
85
  oneOf: [{ required: ['content'] }, { required: ['media'] }]
53
86
  }
54
87
 
55
- function extractPaths(text?: string) {
88
+ /** 从正文提取可发送的文件路径(固定挂载 + 当前工作区前缀)。 */
89
+ function extractPaths(text: string | undefined, workspaceDir?: string): string[] {
56
90
  if (!text) return []
57
- const regex = /(\/workspace\/[^\s]+|\/mobook\/[^\s]+)/g
58
- return text.match(regex) ?? []
59
- }
60
-
61
- function isSafePath(filepath: string) {
62
- return SAFE_PREFIXES.some((prefix) => filepath.startsWith(prefix))
91
+ const unix = text.match(/\/workspace\/[^\s]+|\/mobook\/[^\s]+/g) ?? []
92
+ const win = text.match(/[A-Za-z]:[/\\](?:workspace|mobook)[/\\][^\s]+/g) ?? []
93
+ const underWs: string[] = []
94
+ const ws = workspaceDir?.trim()
95
+ if (ws) {
96
+ const variants = new Set<string>()
97
+ variants.add(ws)
98
+ variants.add(toPosixPath(ws))
99
+ if (path.sep === '\\') variants.add(ws.replace(/\//g, '\\'))
100
+ for (const prefix of variants) {
101
+ if (!prefix) continue
102
+ let from = 0
103
+ while (from < text.length) {
104
+ const i = text.indexOf(prefix, from)
105
+ if (i === -1) break
106
+ let end = i + prefix.length
107
+ while (end < text.length && !/\s/.test(text[end])) end++
108
+ underWs.push(text.slice(i, end))
109
+ from = i + 1
110
+ }
111
+ }
112
+ }
113
+ return [...new Set([...unix, ...win, ...underWs])]
63
114
  }
64
115
 
65
116
  function isSafeFile(filepath: string) {
@@ -67,7 +118,7 @@ function isSafeFile(filepath: string) {
67
118
  const stat = fs.statSync(filepath)
68
119
  if (!stat.isFile()) return false
69
120
  if (stat.size === 0) return false
70
- const ext = path.extname(filepath)
121
+ const ext = path.extname(filepath).toLowerCase()
71
122
  return SAFE_EXTENSIONS.has(ext)
72
123
  }
73
124
 
@@ -84,10 +135,8 @@ export function createDcgchatMessageTool(pluginCtx: DcgchatMessageToolContext):
84
135
  向用户发送消息。
85
136
  若传 target,target 必须是 sessionKey,不能是 userId。
86
137
  如果发送附件:必须使用 media 字段
87
- 支持路径目录:
88
- /workspace/
89
- /mobook/
90
- 禁止直接输出路径文本
138
+ 文件路径须在当前 Agent 工作区目录下(随部署变化,如 ~/.openclaw/workspace-xxx/...),或为兼容环境的 /workspace/、/mobook/(Windows 盘符下 workspace、mobook)。
139
+ 禁止在正文中直接输出可访问路径(应通过 media 发送)
91
140
  `,
92
141
  parameters: messageToolParameters,
93
142
  execute: async (_toolCallId, args, signal) => {
@@ -104,33 +153,46 @@ export function createDcgchatMessageTool(pluginCtx: DcgchatMessageToolContext):
104
153
 
105
154
  try {
106
155
  const sentFiles = new Set<string>()
156
+ const sentKeys = new Set<string>()
157
+ const workspaceDir = pluginCtx.workspaceDir
107
158
 
108
159
  if (args.media?.length) {
109
160
  for (const media of args.media) {
110
161
  const filepath = media.file
111
162
  if (!filepath) continue
112
- if (!isSafePath(filepath)) continue
163
+ if (!isSafePath(filepath, workspaceDir)) continue
113
164
  if (!isSafeFile(filepath)) continue
114
- if (sentFiles.has(filepath)) continue
165
+ const key = pathKey(filepath)
166
+ if (sentKeys.has(key)) continue
115
167
 
116
168
  await sendDcgchatMedia({ sessionKey, mediaUrl: filepath })
117
169
  sentFiles.add(filepath)
170
+ sentKeys.add(key)
118
171
  }
119
172
  }
120
173
 
121
- const fallbackPaths = extractPaths(args.content)
174
+ const fallbackPaths = extractPaths(args.content, workspaceDir)
122
175
  for (const filepath of fallbackPaths) {
123
- if (!isSafePath(filepath)) continue
176
+ if (!isSafePath(filepath, workspaceDir)) continue
124
177
  if (!isSafeFile(filepath)) continue
125
- if (sentFiles.has(filepath)) continue
178
+ const key = pathKey(filepath)
179
+ if (sentKeys.has(key)) continue
126
180
 
127
181
  await sendDcgchatMedia({ sessionKey, mediaUrl: filepath })
128
182
  sentFiles.add(filepath)
183
+ sentKeys.add(key)
129
184
  }
130
185
 
131
186
  let content = args.content ?? ''
132
187
  for (const filepath of sentFiles) {
133
- content = content.replace(filepath, '')
188
+ const posix = toPosixPath(filepath)
189
+ const variants = posix === filepath ? [filepath] : [filepath, posix]
190
+ const seen = new Set<string>()
191
+ for (const v of variants) {
192
+ if (!v || seen.has(v)) continue
193
+ seen.add(v)
194
+ content = content.split(v).join('')
195
+ }
134
196
  }
135
197
  content = content.trim()
136
198
 
@@ -39,8 +39,8 @@ export async function handleParsedWsMessage(parsed: ParsedWsPayload, rawPayload:
39
39
  dcgLogger(`openclaw_bot_event unknown event_type: ${event_type}, ${rawPayload}`)
40
40
  }
41
41
  } else if (event_type === 'agent') {
42
- if (operation_type === 'install') {
43
- createAgent(parsed.content)
42
+ if (operation_type === 'create') {
43
+ await createAgent(parsed.content)
44
44
  } else {
45
45
  dcgLogger(`openclaw_bot_event unknown event_type: ${event_type}, ${rawPayload}`)
46
46
  }