@dcrays/dcgchat-test 0.4.1 → 0.4.3

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.4.1",
3
+ "version": "0.4.3",
4
4
  "type": "module",
5
5
  "description": "OpenClaw channel plugin for 书灵墨宝 (WebSocket)",
6
6
  "main": "index.ts",
package/src/agent.ts CHANGED
@@ -1,7 +1,4 @@
1
1
  import axios from 'axios'
2
- /** @ts-ignore */
3
- import unzipper from 'unzipper'
4
- import { pipeline } from 'stream/promises'
5
2
  import fs from 'fs'
6
3
  import path from 'path'
7
4
  import { getWorkspaceDir } from './utils/global.js'
@@ -9,7 +6,7 @@ import { getWsConnection } from './utils/global.js'
9
6
  import { dcgLogger } from './utils/log.js'
10
7
  import { isWsOpen } from './transport.js'
11
8
  import { sendMessageToGateway } from './gateway/socket.js'
12
- import { decodeZipEntryPath } from './utils/zipPath.js'
9
+ import { extractZipBufferToDirectory } from './utils/zipExtract.js'
13
10
 
14
11
  type IAgentParams = {
15
12
  url: string
@@ -110,81 +107,13 @@ export async function createAgent(msgContent: Record<string, any>) {
110
107
  }
111
108
 
112
109
  try {
113
- // 下载 zip 文件
114
110
  const response = await axios({
115
111
  method: 'get',
116
112
  url,
117
- responseType: 'stream'
113
+ responseType: 'arraybuffer'
118
114
  })
119
- // 创建目标目录
120
115
  fs.mkdirSync(workspaceDir, { recursive: true })
121
- // 解压文件到目标目录,跳过顶层文件夹
122
- await new Promise((resolve, reject) => {
123
- const tasks: Promise<void>[] = []
124
- let rootDir: string | null = null
125
- let hasError = false
126
-
127
- response.data
128
- .pipe(unzipper.Parse())
129
- .on('entry', (entry: any) => {
130
- if (hasError) {
131
- entry.autodrain()
132
- return
133
- }
134
- try {
135
- const flags = entry.props?.flags ?? 0
136
- const entryPath = decodeZipEntryPath(entry.props?.pathBuffer, flags, entry.path)
137
- const pathParts = entryPath.split('/')
138
-
139
- // 检测根目录
140
- if (!rootDir && pathParts.length > 1) {
141
- rootDir = pathParts[0]
142
- }
143
- let newPath = entryPath
144
- // 移除顶层文件夹
145
- if (rootDir && entryPath.startsWith(rootDir + '/')) {
146
- newPath = entryPath.slice(rootDir.length + 1)
147
- }
148
-
149
- if (!newPath) {
150
- entry.autodrain()
151
- return
152
- }
153
-
154
- const targetPath = path.join(workspaceDir, newPath)
155
-
156
- if (entry.type === 'Directory') {
157
- fs.mkdirSync(targetPath, { recursive: true })
158
- entry.autodrain()
159
- } else {
160
- const parentDir = path.dirname(targetPath)
161
- fs.mkdirSync(parentDir, { recursive: true })
162
- const writeStream = fs.createWriteStream(targetPath)
163
- const task = pipeline(entry, writeStream).catch((err) => {
164
- hasError = true
165
- throw new Error(`解压文件失败 ${entryPath}: ${err.message}`)
166
- })
167
- tasks.push(task)
168
- }
169
- } catch (err) {
170
- hasError = true
171
- entry.autodrain()
172
- reject(new Error(`处理entry失败: ${err}`))
173
- }
174
- })
175
- .on('close', async () => {
176
- try {
177
- await Promise.all(tasks)
178
- resolve(null)
179
- } catch (err) {
180
- reject(err)
181
- }
182
- })
183
- .on('error', (err: { message: any }) => {
184
- hasError = true
185
- reject(new Error(`解压流错误: ${err.message}`))
186
- })
187
- })
116
+ await extractZipBufferToDirectory(Buffer.from(response.data), workspaceDir)
188
117
  await onCreateAgent(msgContent)
189
118
  } catch (error) {
190
119
  // 如果安装失败,清理目录
package/src/bot.ts CHANGED
@@ -235,11 +235,12 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
235
235
  const getMediaKey = (url: string) => url.split(/[\\/]/).pop() ?? url
236
236
  let streamedTextLen = 0
237
237
 
238
- if (msg.content.skills_scope.length > 0) {
238
+ if (msg.content.skills_scope.length > 0 && !msg.content?.agent_clone_code) {
239
239
  const workspaceDir = getWorkspaceDir()
240
240
  const skillCode = msg.content.skills_scope.map((skill) => `${workspaceDir}/skills/${skill.skill_code}`).join('\n')
241
241
  const skillText = `技能${skillCode} 在目录${skillCode}下,在目录${skillCode}下读取技能 \n`
242
242
  text = skillText ? `${skillText} \n ${text}` : text
243
+ dcgLogger('skill: skillText: ${skillText}')
243
244
  }
244
245
  const prefixContext = createReplyPrefixContext({
245
246
  cfg: config,
@@ -336,7 +337,6 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
336
337
  clearSentMediaKeys(msg.content.message_id)
337
338
  clearParamsMessage(dcgSessionKey)
338
339
  clearParamsMessage(userId)
339
-
340
340
  sendFinal(outboundCtx, 'stop')
341
341
  return
342
342
  } else {
@@ -385,7 +385,9 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
385
385
  const prev = streamChunkIdxBySessionKey.get(dcgSessionKey) ?? 0
386
386
  streamChunkIdxBySessionKey.set(dcgSessionKey, prev + 1)
387
387
  sendChunk(delta, outboundCtx, prev)
388
- dcgLogger(`[stream]: chunkIdx=${prev} len=${delta.length} user=${outboundCtx.sessionId} ${delta.slice(0, 100)}`)
388
+ dcgLogger(
389
+ `[stream]: chunkIdx=${prev} len=${delta.length} sessionId=${outboundCtx.sessionId} ${delta.slice(0, 100)}`
390
+ )
389
391
  }
390
392
  streamedTextLen = payload.text.length
391
393
  } else {
package/src/cron.ts CHANGED
@@ -40,11 +40,6 @@ export function readCronJob(jobPath: string, jobId: string): Record<string, any>
40
40
  }
41
41
  }
42
42
 
43
- function msgParamsToCtx(p: IMsgParams): IMsgParams | null {
44
- if (!p?.botToken) return null
45
- return p
46
- }
47
-
48
43
  const CRON_UPLOAD_DEBOUNCE_MS = 2400
49
44
 
50
45
  let cronUploadFlushTimer: ReturnType<typeof setTimeout> | null = null
@@ -63,9 +58,10 @@ async function runCronJobsUpload(sessionKey: string): Promise<void> {
63
58
  event_type: 'cron',
64
59
  operation_type: 'install',
65
60
  session_id: sessionId,
66
- agent_id: agentId
61
+ agent_id: agentId,
62
+ oss_url: url
67
63
  }
68
- sendEventMessage(url, params)
64
+ sendEventMessage(params)
69
65
  } catch (error) {
70
66
  dcgLogger(`${jobPath} upload failed: ${error}`, 'error')
71
67
  }
@@ -170,6 +166,7 @@ export const finishedDcgchatCron = async (jobId: string) => {
170
166
  sendFinal(merged, 'cron send')
171
167
  }
172
168
  const ws = getWsConnection()
169
+ const baseContent = getParamsDefaults()
173
170
  if (isWsOpen()) {
174
171
  ws?.send(
175
172
  JSON.stringify({
@@ -178,15 +175,17 @@ export const finishedDcgchatCron = async (jobId: string) => {
178
175
  content: {
179
176
  event_type: 'notify',
180
177
  operation_type: 'cron',
178
+ bot_token: baseContent.botToken,
179
+ app_id: baseContent.appId,
181
180
  session_id: sessionId,
182
- agentId: agentId,
181
+ agent_id: agentId,
183
182
  real_mobook: !sessionId ? 1 : '',
184
183
  title: name
185
184
  }
186
185
  })
187
186
  )
188
- dcgLogger(`定时任务执行成功: ${id}`)
189
187
  }
188
+ dcgLogger(`定时任务执行成功: ${id}`)
190
189
  removeCronMessageId(sessionKey)
191
190
  dcgLogger(`finishedDcgchatCron: job=${id} sessionKey=${sessionKey}`)
192
191
  }
package/src/skill.ts CHANGED
@@ -9,7 +9,6 @@ import { getWsConnection } from './utils/global.js'
9
9
  import { dcgLogger } from './utils/log.js'
10
10
  import { isWsOpen } from './transport.js'
11
11
  import { sendMessageToGateway } from './gateway/socket.js'
12
- import { decodeZipEntryPath } from './utils/zipPath.js'
13
12
 
14
13
  type ISkillParams = {
15
14
  path: string
@@ -70,7 +69,13 @@ export async function installSkill(params: ISkillParams, msgContent: Record<stri
70
69
  }
71
70
  try {
72
71
  const flags = entry.props?.flags ?? 0
73
- const entryPath = decodeZipEntryPath(entry.props?.pathBuffer, flags, entry.path)
72
+ const isUtf8 = (flags & 0x800) !== 0
73
+ let entryPath: string
74
+ if (!isUtf8 && entry.props?.pathBuffer) {
75
+ entryPath = new TextDecoder('gbk').decode(entry.props.pathBuffer)
76
+ } else {
77
+ entryPath = entry.path
78
+ }
74
79
  const pathParts = entryPath.split('/')
75
80
 
76
81
  // 检测根目录
package/src/transport.ts CHANGED
@@ -169,7 +169,7 @@ export function sendChunk(text: string, ctx: IMsgParams, chunkIdx: number): bool
169
169
  }
170
170
 
171
171
  export function sendFinal(ctx: IMsgParams, tag: string): boolean {
172
- dcgLogger(` message handling complete state: final tag:${tag}`)
172
+ dcgLogger(` message handling complete state: to=${ctx.sessionId} final tag:${tag}`)
173
173
  return wsSend(ctx, { response: '', state: 'final' })
174
174
  }
175
175
 
@@ -181,7 +181,7 @@ export function sendError(errorMsg: string, ctx: IMsgParams): boolean {
181
181
  return wsSend(ctx, { response: `[错误] ${errorMsg}`, state: 'final' })
182
182
  }
183
183
 
184
- export function sendEventMessage(url: string, params: Record<string, string> = {}) {
184
+ export function sendEventMessage(params: Record<string, string> = {}) {
185
185
  const ctx = getParamsDefaults()
186
186
  const ws = getWsConnection()
187
187
  if (isWsOpen()) {
@@ -193,7 +193,6 @@ export function sendEventMessage(url: string, params: Record<string, string> = {
193
193
  bot_token: ctx.botToken,
194
194
  domain_id: ctx.domainId,
195
195
  app_id: ctx.appId,
196
- oss_url: url,
197
196
  bot_id: ctx.botId,
198
197
  ...params
199
198
  }
@@ -14,8 +14,10 @@ export function handleGatewayEventMessage(msg: { event?: string; payload?: Recor
14
14
  const sessionKey = getSessionKeyBySubAgentRunId(pl.runId)
15
15
  const outboundCtx = getEffectiveMsgParams(sessionKey)
16
16
  if (pl.data?.delta) {
17
- dcgLogger(`[Gateway] 收到agent事件: ${JSON.stringify(msg).slice(0, 100)}`)
18
- if (outboundCtx.sessionId) sendChunk(pl.data.delta as string, outboundCtx, 0)
17
+ if (outboundCtx.sessionId) {
18
+ dcgLogger(`[Gateway] 收到agent事件: ${JSON.stringify(msg).slice(0, 100)}`)
19
+ sendChunk(pl.data.delta as string, outboundCtx, 0)
20
+ }
19
21
  }
20
22
  }
21
23
  if (msg.event === 'cron') {
@@ -0,0 +1,97 @@
1
+ import path from 'path'
2
+ import fs from 'fs'
3
+ /** @ts-ignore */
4
+ import unzipper from 'unzipper'
5
+ import { pipeline } from 'stream/promises'
6
+ import { decodeZipEntryPath } from './zipPath.js'
7
+
8
+ /**
9
+ * 若且唯若所有条目都在同一顶层目录下(如 GitHub 下载的 repo-name/...),返回该目录名;否则返回 null。
10
+ * 不能再用「第一个多段路径的第一段」推断,否则 ZIP 条目顺序变化时会误判(例如先出现 .github/ 或 src/)。
11
+ */
12
+ export function computeSharedZipRootPrefix(decodedPaths: string[]): string | null {
13
+ const normalized = decodedPaths
14
+ .map((p) => p.replace(/\\/g, '/').replace(/\/+$/, ''))
15
+ .filter((p) => p.length > 0)
16
+
17
+ if (normalized.length === 0) return null
18
+
19
+ const firstSegs = new Set<string>()
20
+ for (const p of normalized) {
21
+ const seg = p.split('/').filter(Boolean)[0]
22
+ if (seg) firstSegs.add(seg)
23
+ }
24
+ if (firstSegs.size !== 1) return null
25
+
26
+ const root = [...firstSegs][0]!
27
+ const prefix = `${root}/`
28
+ for (const p of normalized) {
29
+ if (p !== root && !p.startsWith(prefix)) return null
30
+ }
31
+ return root
32
+ }
33
+
34
+ function assertSafeZipTarget(destDir: string, relativePath: string): string {
35
+ const resolvedPath = path.resolve(destDir, relativePath)
36
+ const resolvedDest = path.resolve(destDir)
37
+ const rel = path.relative(resolvedDest, resolvedPath)
38
+ if (rel.startsWith('..') || path.isAbsolute(rel)) {
39
+ throw new Error(`zip 路径越界: ${relativePath}`)
40
+ }
41
+ return resolvedPath
42
+ }
43
+
44
+ type ZipEntry = {
45
+ path: string
46
+ pathBuffer: Buffer
47
+ flags: number
48
+ type: string
49
+ stream: (password?: string) => NodeJS.ReadableStream
50
+ }
51
+
52
+ /**
53
+ * 将已下载的 zip 解压到 destDir;顶层单根目录(若存在)会被剥掉,与原先流式 Parse 行为一致,但根目录由全量路径计算,与条目顺序无关。
54
+ */
55
+ export async function extractZipBufferToDirectory(buf: Buffer, destDir: string): Promise<void> {
56
+ const directory = await unzipper.Open.buffer(buf)
57
+ const files = (await directory.files) as ZipEntry[]
58
+
59
+ const decodedPaths = files.map((entry) =>
60
+ decodeZipEntryPath(entry.pathBuffer, entry.flags ?? 0, entry.path)
61
+ )
62
+ const rootDir = computeSharedZipRootPrefix(decodedPaths)
63
+
64
+ // 与 unzipper 默认 extract 一致:串行读各 entry,避免同一 buffer 上多路解压竞争
65
+ for (let i = 0; i < files.length; i++) {
66
+ const entry = files[i]!
67
+ const entryPath = decodedPaths[i]!
68
+ let newPath = entryPath.replace(/\\/g, '/')
69
+ if (rootDir) {
70
+ if (newPath === rootDir || newPath === `${rootDir}/`) {
71
+ continue
72
+ }
73
+ if (newPath.startsWith(`${rootDir}/`)) {
74
+ newPath = newPath.slice(rootDir.length + 1)
75
+ }
76
+ }
77
+ newPath = newPath.replace(/\/+$/, '')
78
+ if (!newPath) continue
79
+
80
+ const targetPath = assertSafeZipTarget(destDir, newPath)
81
+
82
+ if (entry.type === 'Directory') {
83
+ fs.mkdirSync(targetPath, { recursive: true })
84
+ continue
85
+ }
86
+
87
+ const parentDir = path.dirname(targetPath)
88
+ fs.mkdirSync(parentDir, { recursive: true })
89
+ const writeStream = fs.createWriteStream(targetPath)
90
+ try {
91
+ await pipeline(entry.stream(), writeStream)
92
+ } catch (err) {
93
+ const message = err instanceof Error ? err.message : String(err)
94
+ throw new Error(`解压文件失败 ${entryPath}: ${message}`)
95
+ }
96
+ }
97
+ }