@dcrays/dcgchat-test 0.3.34 → 0.3.36

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.34",
3
+ "version": "0.3.36",
4
4
  "type": "module",
5
5
  "description": "OpenClaw channel plugin for 书灵墨宝 (WebSocket)",
6
6
  "main": "index.ts",
package/src/agent.ts ADDED
@@ -0,0 +1,204 @@
1
+ import axios from 'axios'
2
+ /** @ts-ignore */
3
+ import unzipper from 'unzipper'
4
+ import { pipeline } from 'stream/promises'
5
+ import fs from 'fs'
6
+ import path from 'path'
7
+ import { getWorkspaceDir } from './utils/global.js'
8
+ import { getWsConnection } from './utils/global.js'
9
+ import { dcgLogger } from './utils/log.js'
10
+ import { isWsOpen } from './transport.js'
11
+ import { sendMessageToGateway } from './gateway/socket.js'
12
+ import { decodeZipEntryPath } from './utils/zipPath.js'
13
+
14
+ type IAgentParams = {
15
+ url: string
16
+ agent_code: string
17
+ agent_name: string
18
+ agent_description: string
19
+ }
20
+
21
+ function sendEvent(msgContent: Record<string, any>) {
22
+ const ws = getWsConnection()
23
+ if (isWsOpen()) {
24
+ ws?.send(
25
+ JSON.stringify({
26
+ messageType: 'openclaw_bot_event',
27
+ source: 'client',
28
+ content: msgContent
29
+ })
30
+ )
31
+ dcgLogger(`agent安装: ${JSON.stringify(msgContent)}`)
32
+ }
33
+ }
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) {
45
+ const workspacePath = getWorkspaceDir()
46
+ if (!workspacePath) return
47
+ const workspaceDir = path.join(workspacePath, '../', `workspace-${code}`)
48
+ const agentDir = path.join(workspacePath, '../', `agents/${code}`)
49
+ const sourceAgent = path.join(workspaceDir, 'agent')
50
+ try {
51
+ if (!fs.existsSync(sourceAgent)) return
52
+ if (!fs.statSync(sourceAgent).isDirectory()) return
53
+ fs.mkdirSync(agentDir, { recursive: true })
54
+ const dest = path.join(agentDir, 'agent')
55
+ if (fs.existsSync(dest)) {
56
+ fs.rmSync(dest, { recursive: true, force: true })
57
+ }
58
+ fs.cpSync(sourceAgent, dest, { recursive: true })
59
+ } catch (err: unknown) {
60
+ dcgLogger(`copyAgentsFiles failed: ${String(err)}`, 'error')
61
+ }
62
+ }
63
+
64
+ export async function onCreateAgent(params: Record<string, any>) {
65
+ const { code, name, description } = params
66
+ try {
67
+ await sendMessageToGateway(JSON.stringify({ method: 'agents.create', params: { name: code, workspace: code } }))
68
+ } catch (err: unknown) {
69
+ dcgLogger(`agents.create failed: ${String(err)}`, 'error')
70
+ }
71
+ // Update config.name to the user-supplied display name (may contain CJK, spaces, etc.)
72
+ try {
73
+ await sendMessageToGateway(JSON.stringify({ method: 'agents.update', params: { name: name, agentId: code } }))
74
+ } catch (err: unknown) {
75
+ dcgLogger(`agents.update failed: ${String(err)}`, 'error')
76
+ }
77
+ if (description?.trim()) {
78
+ try {
79
+ await sendMessageToGateway(
80
+ JSON.stringify({
81
+ method: 'agents.files.set',
82
+ params: { agentId: code, name: 'IDENTITY.md', content: description.trim() }
83
+ })
84
+ )
85
+ } catch {
86
+ // Non-fatal
87
+ }
88
+ }
89
+ if (name?.trim()) {
90
+ try {
91
+ await sendMessageToGateway(
92
+ JSON.stringify({
93
+ method: 'agents.files.set',
94
+ params: { agentId: code, name: 'USER.md', content: name.trim() }
95
+ })
96
+ )
97
+ } catch {
98
+ // Non-fatal
99
+ }
100
+ }
101
+ copyAgentsFiles(code)
102
+ sendEvent({ ...params, status: 'ok' })
103
+ }
104
+
105
+ 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')
109
+ sendEvent({ ...msgContent, status: 'fail' })
110
+ return
111
+ }
112
+ const workspacePath = getWorkspaceDir()
113
+ const workspaceDir = path.join(workspacePath, '../', `workspace-${code}`)
114
+
115
+ // 如果目标目录已存在,先删除
116
+ if (fs.existsSync(workspaceDir)) {
117
+ fs.rmSync(workspaceDir, { recursive: true, force: true })
118
+ }
119
+
120
+ try {
121
+ // 下载 zip 文件
122
+ const response = await axios({
123
+ method: 'get',
124
+ url,
125
+ responseType: 'stream'
126
+ })
127
+ // 创建目标目录
128
+ fs.mkdirSync(workspaceDir, { recursive: true })
129
+ // 解压文件到目标目录,跳过顶层文件夹
130
+ await new Promise((resolve, reject) => {
131
+ const tasks: Promise<void>[] = []
132
+ let rootDir: string | null = null
133
+ let hasError = false
134
+
135
+ response.data
136
+ .pipe(unzipper.Parse())
137
+ .on('entry', (entry: any) => {
138
+ if (hasError) {
139
+ entry.autodrain()
140
+ return
141
+ }
142
+ try {
143
+ const flags = entry.props?.flags ?? 0
144
+ const entryPath = decodeZipEntryPath(entry.props?.pathBuffer, flags, entry.path)
145
+ const pathParts = entryPath.split('/')
146
+
147
+ // 检测根目录
148
+ if (!rootDir && pathParts.length > 1) {
149
+ rootDir = pathParts[0]
150
+ }
151
+ let newPath = entryPath
152
+ // 移除顶层文件夹
153
+ if (rootDir && entryPath.startsWith(rootDir + '/')) {
154
+ newPath = entryPath.slice(rootDir.length + 1)
155
+ }
156
+
157
+ if (!newPath) {
158
+ entry.autodrain()
159
+ return
160
+ }
161
+
162
+ const targetPath = path.join(workspacePath, newPath)
163
+
164
+ if (entry.type === 'Directory') {
165
+ fs.mkdirSync(targetPath, { recursive: true })
166
+ entry.autodrain()
167
+ } else {
168
+ const parentDir = path.dirname(targetPath)
169
+ fs.mkdirSync(parentDir, { recursive: true })
170
+ const writeStream = fs.createWriteStream(targetPath)
171
+ const task = pipeline(entry, writeStream).catch((err) => {
172
+ hasError = true
173
+ throw new Error(`解压文件失败 ${entryPath}: ${err.message}`)
174
+ })
175
+ tasks.push(task)
176
+ }
177
+ } catch (err) {
178
+ hasError = true
179
+ entry.autodrain()
180
+ reject(new Error(`处理entry失败: ${err}`))
181
+ }
182
+ })
183
+ .on('close', async () => {
184
+ try {
185
+ await Promise.all(tasks)
186
+ resolve(null)
187
+ } catch (err) {
188
+ reject(err)
189
+ }
190
+ })
191
+ .on('error', (err: { message: any }) => {
192
+ hasError = true
193
+ reject(new Error(`解压流错误: ${err.message}`))
194
+ })
195
+ })
196
+ await onCreateAgent(msgContent)
197
+ } catch (error) {
198
+ // 如果安装失败,清理目录
199
+ if (fs.existsSync(workspaceDir)) {
200
+ fs.rmSync(workspaceDir, { recursive: true, force: true })
201
+ }
202
+ sendEvent({ ...msgContent, status: 'fail' })
203
+ }
204
+ }
package/src/bot.ts CHANGED
@@ -151,7 +151,6 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
151
151
  const core = getDcgchatRuntime()
152
152
 
153
153
  const conversationId = msg.content.session_id?.trim()
154
- const agentId = msg.content.agent_id?.trim()
155
154
  const real_mobook = msg.content.real_mobook?.toString().trim()
156
155
 
157
156
  const route = core.channel.routing.resolveAgentRoute({
@@ -252,9 +251,9 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
252
251
  OriginatingTo: effectiveSessionKey,
253
252
  ...mediaPayload
254
253
  })
255
- dcgLogger(
256
- `inbound context target: rawTarget=${userId}, normalizedTarget=${effectiveSessionKey}, ctx.To=${String(ctxPayload.To ?? '')}, ctx.SessionKey=${String(ctxPayload.SessionKey ?? '')}, ctx.OriginatingTo=${String(ctxPayload.OriginatingTo ?? '')}`
257
- )
254
+ dcgLogger(
255
+ `inbound context target: rawTarget=${userId}, normalizedTarget=${effectiveSessionKey}, ctx.To=${String(ctxPayload.To ?? '')}, ctx.SessionKey=${String(ctxPayload.SessionKey ?? '')}, ctx.OriginatingTo=${String(ctxPayload.OriginatingTo ?? '')}`
256
+ )
258
257
 
259
258
  const sentMediaKeys = new Set<string>()
260
259
  const getMediaKey = (url: string) => url.split(/[\\/]/).pop() ?? url
@@ -318,7 +317,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
318
317
  })
319
318
  } else if (interruptCommand.includes(text?.trim())) {
320
319
  dcgLogger(`interrupt command: ${text}`)
321
- safeSendFinal('abort')
320
+ sendFinal(outboundCtx, 'abort')
322
321
  sendText('会话已终止', outboundCtx)
323
322
  sessionStreamSuppressed.add(effectiveSessionKey)
324
323
  const runId = activeRunIdBySessionKey.get(effectiveSessionKey)
package/src/monitor.ts CHANGED
@@ -1,14 +1,10 @@
1
1
  import type { ClawdbotConfig, RuntimeEnv } from 'openclaw/plugin-sdk'
2
2
  import WebSocket from 'ws'
3
- import { handleDcgchatMessage } from './bot.js'
4
3
  import { resolveAccount } from './channel.js'
5
- import { setWsConnection, getOpenClawConfig, setMsgStatus, getSessionKey } from './utils/global.js'
6
- import type { InboundMessage } from './types.js'
7
- import { installSkill, uninstallSkill } from './skill.js'
4
+ import { setWsConnection, getOpenClawConfig } from './utils/global.js'
8
5
  import { dcgLogger } from './utils/log.js'
9
- import { onDisabledCronJob, onEnabledCronJob, onRemoveCronJob, onRunCronJob } from './cron.js'
10
- import { ignoreToolCommand } from './utils/constant.js'
11
6
  import { isWsOpen } from './transport.js'
7
+ import { handleParsedWsMessage } from './utils/wsMessageHandler.js'
12
8
 
13
9
  export type MonitorDcgchatOpts = {
14
10
  config?: ClawdbotConfig
@@ -128,53 +124,7 @@ export async function monitorDcgchatProvider(opts: MonitorDcgchatOpts): Promise<
128
124
  return
129
125
  }
130
126
 
131
- if (parsed.messageType == 'openclaw_bot_chat') {
132
- const msg = parsed as unknown as InboundMessage
133
- // 与 monitor 原逻辑一致:工具类指令不进入 running,避免误触工具链监控
134
- const effectiveSessionKey = getSessionKey(msg.content, account.accountId)
135
- if (!ignoreToolCommand.includes(msg.content.text?.trim() ?? '')) {
136
- setMsgStatus(effectiveSessionKey, 'running')
137
- } else {
138
- setMsgStatus(effectiveSessionKey, 'finished')
139
- }
140
- await handleDcgchatMessage(msg, account.accountId)
141
- } else if (parsed.messageType == 'openclaw_bot_event') {
142
- const { event_type, operation_type } = parsed.content ? parsed.content : ({} as Record<string, any>)
143
- if (event_type === 'skill') {
144
- const { skill_url, skill_code, skill_id, bot_token, websocket_trace_id } = parsed.content
145
- const content = {
146
- event_type,
147
- operation_type,
148
- skill_url,
149
- skill_code,
150
- skill_id,
151
- bot_token,
152
- websocket_trace_id
153
- }
154
- if (operation_type === 'install' || operation_type === 'enable' || operation_type === 'update') {
155
- installSkill({ path: skill_url, code: skill_code }, content)
156
- } else if (operation_type === 'remove' || operation_type === 'disable') {
157
- uninstallSkill({ code: skill_code }, content)
158
- } else {
159
- dcgLogger(`openclaw_bot_event unknown event_type: ${event_type}, ${data.toString()}`)
160
- }
161
- } else if (event_type === 'cron') {
162
- const { job_id, message_id } = parsed.content
163
- if (operation_type === 'remove') {
164
- await onRemoveCronJob(job_id)
165
- } else if (operation_type === 'enable') {
166
- await onEnabledCronJob(job_id)
167
- } else if (operation_type === 'disable') {
168
- await onDisabledCronJob(job_id)
169
- } else if (operation_type === 'run') {
170
- await onRunCronJob(job_id, message_id)
171
- }
172
- } else {
173
- dcgLogger(`openclaw_bot_event unknown operation_type: ${operation_type}, ${data.toString()}`)
174
- }
175
- } else {
176
- dcgLogger(`ignoring unknown messageType: ${parsed.messageType}`, 'error')
177
- }
127
+ await handleParsedWsMessage(parsed, payloadStr, account.accountId)
178
128
  })
179
129
 
180
130
  ws.on('close', (code, reason) => {
package/src/session.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { sendMessageToGateway } from './gateway/socket.js'
2
+ import { getSessionKey } from './utils/global.js'
3
+ import { dcgLogger } from './utils/log.js'
4
+
5
+ interface TSession {
6
+ agent_id: string
7
+ session_id: string
8
+ agent_clone_code?: string
9
+ account_id?: string
10
+ }
11
+
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)
14
+ if (!session_id) {
15
+ dcgLogger('onRemoveSession: empty session_id', 'error')
16
+ return
17
+ }
18
+ sendMessageToGateway(JSON.stringify({ method: 'sessions.delete', params: { key: sessionKey, deleteTranscript: true } }))
19
+ }
package/src/skill.ts CHANGED
@@ -9,6 +9,7 @@ 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'
12
13
 
13
14
  type ISkillParams = {
14
15
  path: string
@@ -69,13 +70,7 @@ export async function installSkill(params: ISkillParams, msgContent: Record<stri
69
70
  }
70
71
  try {
71
72
  const flags = entry.props?.flags ?? 0
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
- }
73
+ const entryPath = decodeZipEntryPath(entry.props?.pathBuffer, flags, entry.path)
79
74
  const pathParts = entryPath.split('/')
80
75
 
81
76
  // 检测根目录
package/src/types.ts CHANGED
@@ -39,6 +39,7 @@ export type InboundMessage = {
39
39
  // content: string;
40
40
  content: {
41
41
  bot_token: string
42
+ agent_clone_code?: string
42
43
  domain_id?: string
43
44
  app_id?: string
44
45
  bot_id?: string
@@ -117,19 +117,6 @@ export function clearSentMediaKeys(messageId?: string) {
117
117
  }
118
118
  }
119
119
 
120
- export const getSessionKey = (content: any, accountId: string) => {
121
- const { real_mobook, agent_id, conversation_id, session_id } = content
122
- const core = getDcgchatRuntime()
123
-
124
- const route = core.channel.routing.resolveAgentRoute({
125
- cfg: getOpenClawConfig() as OpenClawConfig,
126
- channel: "dcgchat-test",
127
- accountId: accountId || 'default',
128
- peer: { kind: 'direct', id: session_id }
129
- })
130
- return real_mobook == '1' ? route.sessionKey : `agent:main:mobook:direct:${agent_id}:${session_id}`.toLowerCase()
131
- }
132
-
133
120
  const cronMessageIdMap = new Map<string, string>()
134
121
 
135
122
  export function setCronMessageId(sk: string, messageId: string) {
@@ -144,6 +131,21 @@ export function removeCronMessageId(sk: string) {
144
131
  cronMessageIdMap.delete(sk)
145
132
  }
146
133
 
134
+ export const getSessionKey = (content: any, accountId: string) => {
135
+ const { real_mobook, agent_id, agent_clone_code, session_id } = content
136
+ const core = getDcgchatRuntime()
137
+
138
+ const anentCode = agent_clone_code || 'main'
139
+
140
+ const route = core.channel.routing.resolveAgentRoute({
141
+ cfg: getOpenClawConfig() as OpenClawConfig,
142
+ channel: "dcgchat-test",
143
+ accountId: accountId || 'default',
144
+ peer: { kind: 'direct', id: session_id }
145
+ })
146
+ return real_mobook == '1' ? route.sessionKey : `agent:${anentCode}:mobook:direct:${agent_id}:${session_id}`.toLowerCase()
147
+ }
148
+
147
149
  export function getInfoBySessionKey(sk: string): { sessionId: string; agentId: string } {
148
150
  const sessionInfo = sk.split(':')
149
151
  return { sessionId: sessionInfo.at(-1) ?? '', agentId: sessionInfo.at(-2) ?? '' }
@@ -0,0 +1,64 @@
1
+ import { handleDcgchatMessage } from '../bot.js'
2
+ import { setMsgStatus, getSessionKey } from './global.js'
3
+ import type { InboundMessage } from '../types.js'
4
+ import { installSkill, uninstallSkill } from '../skill.js'
5
+ import { dcgLogger } from './log.js'
6
+ import { onDisabledCronJob, onEnabledCronJob, onRemoveCronJob, onRunCronJob } from '../cron.js'
7
+ import { ignoreToolCommand } from './constant.js'
8
+ import { createAgent } from '../agent.js'
9
+
10
+ export type ParsedWsPayload = {
11
+ messageType?: string
12
+ content: any
13
+ }
14
+
15
+ /**
16
+ * 处理 WebSocket 已解析 JSON 且 content 已二次 parse 后的业务消息(openclaw_bot_chat / openclaw_bot_event)。
17
+ */
18
+ export async function handleParsedWsMessage(parsed: ParsedWsPayload, rawPayload: string, accountId: string): Promise<void> {
19
+ if (parsed.messageType == 'openclaw_bot_chat') {
20
+ const msg = parsed as unknown as InboundMessage
21
+ // 与 monitor 原逻辑一致:工具类指令不进入 running,避免误触工具链监控
22
+ const effectiveSessionKey = getSessionKey(msg.content, accountId)
23
+ if (!ignoreToolCommand.includes(msg.content.text?.trim() ?? '')) {
24
+ setMsgStatus(effectiveSessionKey, 'running')
25
+ } else {
26
+ setMsgStatus(effectiveSessionKey, 'finished')
27
+ }
28
+ await handleDcgchatMessage(msg, accountId)
29
+ } else if (parsed.messageType == 'openclaw_bot_event') {
30
+ const { event_type, operation_type } = parsed.content ? parsed.content : ({} as Record<string, any>)
31
+ if (event_type === 'skill') {
32
+ const { skill_url, skill_code, skill_id, bot_token, websocket_trace_id } = parsed.content
33
+ const content = { event_type, operation_type, skill_url, skill_code, skill_id, bot_token, websocket_trace_id }
34
+ if (operation_type === 'install' || operation_type === 'enable' || operation_type === 'update') {
35
+ installSkill({ path: skill_url, code: skill_code }, content)
36
+ } else if (operation_type === 'remove' || operation_type === 'disable') {
37
+ uninstallSkill({ code: skill_code }, content)
38
+ } else {
39
+ dcgLogger(`openclaw_bot_event unknown event_type: ${event_type}, ${rawPayload}`)
40
+ }
41
+ } else if (event_type === 'agent') {
42
+ if (operation_type === 'install') {
43
+ createAgent(parsed.content)
44
+ } else {
45
+ dcgLogger(`openclaw_bot_event unknown event_type: ${event_type}, ${rawPayload}`)
46
+ }
47
+ } else if (event_type === 'cron') {
48
+ const { job_id, message_id } = parsed.content
49
+ if (operation_type === 'remove') {
50
+ await onRemoveCronJob(job_id)
51
+ } else if (operation_type === 'enable') {
52
+ await onEnabledCronJob(job_id)
53
+ } else if (operation_type === 'disable') {
54
+ await onDisabledCronJob(job_id)
55
+ } else if (operation_type === 'run') {
56
+ await onRunCronJob(job_id, message_id)
57
+ }
58
+ } else {
59
+ dcgLogger(`openclaw_bot_event unknown operation_type: ${operation_type}, ${rawPayload}`)
60
+ }
61
+ } else {
62
+ dcgLogger(`ignoring unknown messageType: ${parsed.messageType}`, 'error')
63
+ }
64
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * ZIP 文件名编码:规范要求 UTF-8 时设置 0x800;很多工具未设标志但仍写 UTF-8 字节。
3
+ * 无标志时若一律按 GBK 解码,会得到「鍥句功…」类乱码。先严格 UTF-8,失败再 GBK(兼容 Windows 中文 ZIP)。
4
+ */
5
+ export function decodeZipEntryPath(
6
+ pathBuffer: Buffer | Uint8Array | undefined,
7
+ flags: number,
8
+ fallbackPath: string
9
+ ): string {
10
+ if ((flags & 0x800) !== 0) {
11
+ if (pathBuffer) {
12
+ return new TextDecoder('utf-8').decode(pathBuffer)
13
+ }
14
+ return fallbackPath
15
+ }
16
+ if (pathBuffer && pathBuffer.length > 0) {
17
+ try {
18
+ return new TextDecoder('utf-8', { fatal: true }).decode(pathBuffer)
19
+ } catch {
20
+ return new TextDecoder('gbk').decode(pathBuffer)
21
+ }
22
+ }
23
+ return fallbackPath
24
+ }