@dcrays/dcgchat-test 0.4.2 → 0.4.4
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/README.md +83 -0
- package/index.ts +2 -1
- package/openclaw.plugin.json +2 -4
- package/package.json +13 -3
- package/src/bot.ts +24 -11
- package/src/channel.ts +12 -11
- package/src/cron.ts +8 -9
- package/src/cronToolCall.ts +3 -3
- package/src/skill.ts +79 -3
- package/src/transport.ts +1 -2
- package/src/utils/constant.ts +4 -0
- package/src/utils/gatewayMsgHanlder.ts +4 -2
- package/src/utils/global.ts +3 -3
- package/src/utils/log.ts +2 -1
- package/src/utils/params.ts +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
- [ ] 错误消息类型
|
package/index.ts
CHANGED
|
@@ -3,11 +3,12 @@ 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'
|
|
6
7
|
import { setOpenClawConfig } from './src/utils/global.js'
|
|
7
8
|
import { createDcgchatMessageTool } from './src/tools/messageTool.js'
|
|
8
9
|
|
|
9
10
|
const plugin = {
|
|
10
|
-
id:
|
|
11
|
+
id: channelInfo[ENV],
|
|
11
12
|
name: '书灵墨宝',
|
|
12
13
|
description: '连接 OpenClaw 与 书灵墨宝 产品(WebSocket)',
|
|
13
14
|
configSchema: emptyPluginConfigSchema(),
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dcrays/dcgchat-test",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw channel plugin for 书灵墨宝 (WebSocket)",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -16,6 +16,12 @@
|
|
|
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
|
+
},
|
|
19
25
|
"dependencies": {
|
|
20
26
|
"ali-oss": "file:src/libs/ali-oss-6.23.0.tgz",
|
|
21
27
|
"axios": "file:src/libs/axios-1.13.6.tgz",
|
|
@@ -31,15 +37,19 @@
|
|
|
31
37
|
"id": "dcgchat-test",
|
|
32
38
|
"label": "书灵墨宝",
|
|
33
39
|
"selectionLabel": "书灵墨宝",
|
|
34
|
-
"docsPath": "/channels/dcgchat
|
|
40
|
+
"docsPath": "/channels/dcgchat",
|
|
35
41
|
"docsLabel": "dcgchat-test",
|
|
36
42
|
"blurb": "连接 OpenClaw 与 书灵墨宝 产品",
|
|
37
43
|
"order": 80
|
|
38
44
|
},
|
|
39
45
|
"install": {
|
|
40
46
|
"npmSpec": "@dcrays/dcgchat-test",
|
|
41
|
-
"localPath": "extensions/dcgchat
|
|
47
|
+
"localPath": "extensions/dcgchat",
|
|
42
48
|
"defaultChoice": "npm"
|
|
43
49
|
}
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"openclaw": "^2026.3.13",
|
|
53
|
+
"typescript": "~5.8.0"
|
|
44
54
|
}
|
|
45
55
|
}
|
package/src/bot.ts
CHANGED
|
@@ -151,7 +151,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
151
151
|
|
|
152
152
|
const route = core.channel.routing.resolveAgentRoute({
|
|
153
153
|
cfg: config,
|
|
154
|
-
channel:
|
|
154
|
+
channel: channelInfo[ENV],
|
|
155
155
|
accountId: account.accountId,
|
|
156
156
|
peer: { kind: 'direct', id: conversationId }
|
|
157
157
|
})
|
|
@@ -173,6 +173,8 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
173
173
|
sessionKey: dcgSessionKey,
|
|
174
174
|
real_mobook
|
|
175
175
|
}
|
|
176
|
+
/** 写入本条消息参数前快照:流式/abort 的 final 须对齐「上一轮」触发的对话 messageId,而非打断指令本身 */
|
|
177
|
+
const priorOutboundCtx = getEffectiveMsgParams(dcgSessionKey)
|
|
176
178
|
setParamsMessage(dcgSessionKey, mergedParams)
|
|
177
179
|
dcgLogger(`target alias bound: aliasTarget=${userId} -> sessionKey=${dcgSessionKey}`)
|
|
178
180
|
const outboundCtx = getEffectiveMsgParams(dcgSessionKey)
|
|
@@ -218,13 +220,13 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
218
220
|
ChatType: 'direct',
|
|
219
221
|
SenderName: agentDisplayName,
|
|
220
222
|
SenderId: userId,
|
|
221
|
-
Provider:
|
|
222
|
-
Surface:
|
|
223
|
+
Provider: channelInfo[ENV],
|
|
224
|
+
Surface: channelInfo[ENV],
|
|
223
225
|
MessageSid: msg.content.message_id,
|
|
224
226
|
Timestamp: Date.now(),
|
|
225
227
|
WasMentioned: true,
|
|
226
228
|
CommandAuthorized: true,
|
|
227
|
-
OriginatingChannel:
|
|
229
|
+
OriginatingChannel: channelInfo[ENV],
|
|
228
230
|
OriginatingTo: dcgSessionKey,
|
|
229
231
|
Target: dcgSessionKey,
|
|
230
232
|
SourceTarget: dcgSessionKey,
|
|
@@ -237,14 +239,19 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
237
239
|
|
|
238
240
|
if (msg.content.skills_scope.length > 0 && !msg.content?.agent_clone_code) {
|
|
239
241
|
const workspaceDir = getWorkspaceDir()
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
242
|
+
const skillText = msg.content.skills_scope
|
|
243
|
+
.map((skill) => {
|
|
244
|
+
const skillDir = `${workspaceDir}/skills/${skill.skill_code}`
|
|
245
|
+
return `技能${skill.skill_code} 在目录${skillDir}下,在目录${skillDir}下读取技能 \n`
|
|
246
|
+
})
|
|
247
|
+
.join('\n')
|
|
248
|
+
text = skillText ? `${skillText} ${text}` : text
|
|
249
|
+
dcgLogger(`skill: text: ${text}`)
|
|
243
250
|
}
|
|
244
251
|
const prefixContext = createReplyPrefixContext({
|
|
245
252
|
cfg: config,
|
|
246
253
|
agentId: effectiveAgentId ?? '',
|
|
247
|
-
channel:
|
|
254
|
+
channel: channelInfo[ENV],
|
|
248
255
|
accountId: account.accountId
|
|
249
256
|
})
|
|
250
257
|
|
|
@@ -303,7 +310,14 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
303
310
|
})
|
|
304
311
|
} else if (interruptCommand.includes(text?.trim())) {
|
|
305
312
|
dcgLogger(`interrupt command: ${text}`)
|
|
306
|
-
|
|
313
|
+
const ctxForAbort =
|
|
314
|
+
priorOutboundCtx.messageId?.trim() || priorOutboundCtx.sessionId?.trim()
|
|
315
|
+
? priorOutboundCtx
|
|
316
|
+
: outboundCtx
|
|
317
|
+
sendFinal(
|
|
318
|
+
ctxForAbort.messageId?.trim() ? ctxForAbort : { ...ctxForAbort, messageId: `${Date.now()}` },
|
|
319
|
+
'abort'
|
|
320
|
+
)
|
|
307
321
|
sendText('会话已终止', outboundCtx)
|
|
308
322
|
sessionStreamSuppressed.add(dcgSessionKey)
|
|
309
323
|
const abortOneSession = async (sessionKey: string) => {
|
|
@@ -336,7 +350,6 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
336
350
|
clearSentMediaKeys(msg.content.message_id)
|
|
337
351
|
clearParamsMessage(dcgSessionKey)
|
|
338
352
|
clearParamsMessage(userId)
|
|
339
|
-
|
|
340
353
|
sendFinal(outboundCtx, 'stop')
|
|
341
354
|
return
|
|
342
355
|
} else {
|
|
@@ -427,7 +440,7 @@ export async function handleDcgchatMessage(msg: InboundMessage, accountId: strin
|
|
|
427
440
|
ctx: ctxPayload,
|
|
428
441
|
updateLastRoute: {
|
|
429
442
|
sessionKey: dcgSessionKey,
|
|
430
|
-
channel:
|
|
443
|
+
channel: channelInfo[ENV],
|
|
431
444
|
to: dcgSessionKey,
|
|
432
445
|
accountId: route.accountId
|
|
433
446
|
},
|
package/src/channel.ts
CHANGED
|
@@ -11,12 +11,13 @@ import {
|
|
|
11
11
|
hasSentMediaKey
|
|
12
12
|
} from './utils/global.js'
|
|
13
13
|
import { isWsOpen, mergeDefaultParams, mergeSessionParams, sendFinal, wsSendRaw } from './transport.js'
|
|
14
|
+
import { channelInfo, ENV } from './utils/constant.js'
|
|
14
15
|
import { dcgLogger, setLogger } from './utils/log.js'
|
|
15
16
|
import { getOutboundMsgParams, getParamsMessage } from './utils/params.js'
|
|
16
17
|
import { startDcgchatGatewaySocket } from './gateway/socket.js'
|
|
17
18
|
|
|
18
19
|
function dcgchatChannelCfg(): DcgchatConfig {
|
|
19
|
-
return (getOpenClawConfig()?.channels?.[
|
|
20
|
+
return (getOpenClawConfig()?.channels?.[channelInfo[ENV]] as DcgchatConfig | undefined) ?? {}
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
/** `agent:<code>:mobook:direct:<agentId>:<sessionId>`(与 getSessionKey 非 real_mobook 分支一致) */
|
|
@@ -90,7 +91,7 @@ export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<v
|
|
|
90
91
|
const fileName = mediaUrl?.split(/[\\/]/).pop() || ''
|
|
91
92
|
|
|
92
93
|
try {
|
|
93
|
-
const botToken = msgCtx.botToken ?? getOpenClawConfig()?.channels?.[
|
|
94
|
+
const botToken = msgCtx.botToken ?? getOpenClawConfig()?.channels?.[channelInfo[ENV]]?.botToken ?? ''
|
|
94
95
|
const url = opts.mediaUrl ? await ossUpload(opts.mediaUrl, botToken, 1) : ''
|
|
95
96
|
wsSendRaw(msgCtx, {
|
|
96
97
|
response: opts.text ?? '',
|
|
@@ -110,7 +111,7 @@ export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<v
|
|
|
110
111
|
|
|
111
112
|
export function resolveAccount(cfg: OpenClawConfig, accountId?: string | null): ResolvedDcgchatAccount {
|
|
112
113
|
const id = accountId ?? DEFAULT_ACCOUNT_ID
|
|
113
|
-
const raw = (cfg.channels?.[
|
|
114
|
+
const raw = (cfg.channels?.[channelInfo[ENV]] as DcgchatConfig | undefined) ?? {}
|
|
114
115
|
return {
|
|
115
116
|
accountId: id,
|
|
116
117
|
enabled: raw.enabled !== false,
|
|
@@ -124,13 +125,13 @@ export function resolveAccount(cfg: OpenClawConfig, accountId?: string | null):
|
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
127
|
-
id:
|
|
128
|
+
id: channelInfo[ENV],
|
|
128
129
|
meta: {
|
|
129
|
-
id:
|
|
130
|
+
id: channelInfo[ENV],
|
|
130
131
|
label: '书灵墨宝',
|
|
131
132
|
selectionLabel: '书灵墨宝',
|
|
132
133
|
docsPath: '/channels/dcgchat',
|
|
133
|
-
docsLabel:
|
|
134
|
+
docsLabel: channelInfo[ENV],
|
|
134
135
|
blurb: '连接 OpenClaw 与 书灵墨宝 产品',
|
|
135
136
|
order: 80
|
|
136
137
|
},
|
|
@@ -147,7 +148,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
147
148
|
// blockStreaming: true,
|
|
148
149
|
},
|
|
149
150
|
/** 当前构建的 channel id + 兼容旧配置键 `channels.dcgchat` */
|
|
150
|
-
reload: { configPrefixes: [`channels.${
|
|
151
|
+
reload: { configPrefixes: [`channels.${channelInfo[ENV]}`, 'channels.dcgchat'] },
|
|
151
152
|
configSchema: {
|
|
152
153
|
schema: {
|
|
153
154
|
type: 'object',
|
|
@@ -183,7 +184,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
183
184
|
resolveAccount: (cfg, accountId) => resolveAccount(cfg, accountId),
|
|
184
185
|
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
185
186
|
setAccountEnabled: ({ cfg, enabled }) => {
|
|
186
|
-
const channelKey =
|
|
187
|
+
const channelKey = channelInfo[ENV]
|
|
187
188
|
const prev = (cfg.channels?.[channelKey as keyof NonNullable<typeof cfg.channels>] as Record<string, unknown> | undefined) ?? {}
|
|
188
189
|
return {
|
|
189
190
|
...cfg,
|
|
@@ -271,7 +272,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
271
272
|
}
|
|
272
273
|
}
|
|
273
274
|
return {
|
|
274
|
-
channel:
|
|
275
|
+
channel: channelInfo[ENV],
|
|
275
276
|
messageId: `${messageId}`,
|
|
276
277
|
chatId: to
|
|
277
278
|
}
|
|
@@ -288,7 +289,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
288
289
|
if (!outboundCtx?.sessionId) {
|
|
289
290
|
dcgLogger(`channel sendMedia to ${ctx.to} -> sessionId not found`, 'error')
|
|
290
291
|
return {
|
|
291
|
-
channel:
|
|
292
|
+
channel: channelInfo[ENV],
|
|
292
293
|
messageId,
|
|
293
294
|
chatId: to || ''
|
|
294
295
|
}
|
|
@@ -297,7 +298,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
297
298
|
dcgLogger(`channel sendMedia to ${ctx.to}`)
|
|
298
299
|
await sendDcgchatMedia({ sessionKey: to || '', mediaUrl: ctx.mediaUrl || '' })
|
|
299
300
|
return {
|
|
300
|
-
channel:
|
|
301
|
+
channel: channelInfo[ENV],
|
|
301
302
|
messageId,
|
|
302
303
|
chatId: to || ''
|
|
303
304
|
}
|
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(
|
|
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
|
-
|
|
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/cronToolCall.ts
CHANGED
|
@@ -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 =
|
|
130
|
+
;(newParams.delivery as CronDelivery).channel = channelInfo[ENV]
|
|
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 =
|
|
141
|
+
;(newParams.delivery as CronDelivery).channel = channelInfo[ENV]
|
|
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 ${
|
|
179
|
+
params.command.replace('--json', '') + ` --session-key ${sk} --channel ${channelInfo[ENV]} --to dcg-cron:${sk} --json`
|
|
180
180
|
return { params: newParams }
|
|
181
181
|
} else {
|
|
182
182
|
return params
|
package/src/skill.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import axios from 'axios'
|
|
2
|
+
/** @ts-ignore */
|
|
3
|
+
import unzipper from 'unzipper'
|
|
4
|
+
import { pipeline } from 'stream/promises'
|
|
2
5
|
import fs from 'fs'
|
|
3
6
|
import path from 'path'
|
|
4
7
|
import { getWorkspaceDir } from './utils/global.js'
|
|
@@ -6,7 +9,6 @@ import { getWsConnection } from './utils/global.js'
|
|
|
6
9
|
import { dcgLogger } from './utils/log.js'
|
|
7
10
|
import { isWsOpen } from './transport.js'
|
|
8
11
|
import { sendMessageToGateway } from './gateway/socket.js'
|
|
9
|
-
import { extractZipBufferToDirectory } from './utils/zipExtract.js'
|
|
10
12
|
|
|
11
13
|
type ISkillParams = {
|
|
12
14
|
path: string
|
|
@@ -44,13 +46,87 @@ export async function installSkill(params: ISkillParams, msgContent: Record<stri
|
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
try {
|
|
49
|
+
// 下载 zip 文件
|
|
47
50
|
const response = await axios({
|
|
48
51
|
method: 'get',
|
|
49
52
|
url: cdnUrl,
|
|
50
|
-
responseType: '
|
|
53
|
+
responseType: 'stream'
|
|
51
54
|
})
|
|
55
|
+
// 创建目标目录
|
|
52
56
|
fs.mkdirSync(skillDir, { recursive: true })
|
|
53
|
-
|
|
57
|
+
// 解压文件到目标目录,跳过顶层文件夹
|
|
58
|
+
await new Promise((resolve, reject) => {
|
|
59
|
+
const tasks: Promise<void>[] = []
|
|
60
|
+
let rootDir: string | null = null
|
|
61
|
+
let hasError = false
|
|
62
|
+
|
|
63
|
+
response.data
|
|
64
|
+
.pipe(unzipper.Parse())
|
|
65
|
+
.on('entry', (entry: any) => {
|
|
66
|
+
if (hasError) {
|
|
67
|
+
entry.autodrain()
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
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
|
+
}
|
|
79
|
+
const pathParts = entryPath.split('/')
|
|
80
|
+
|
|
81
|
+
// 检测根目录
|
|
82
|
+
if (!rootDir && pathParts.length > 1) {
|
|
83
|
+
rootDir = pathParts[0]
|
|
84
|
+
}
|
|
85
|
+
let newPath = entryPath
|
|
86
|
+
// 移除顶层文件夹
|
|
87
|
+
if (rootDir && entryPath.startsWith(rootDir + '/')) {
|
|
88
|
+
newPath = entryPath.slice(rootDir.length + 1)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!newPath) {
|
|
92
|
+
entry.autodrain()
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const targetPath = path.join(skillDir, newPath)
|
|
97
|
+
|
|
98
|
+
if (entry.type === 'Directory') {
|
|
99
|
+
fs.mkdirSync(targetPath, { recursive: true })
|
|
100
|
+
entry.autodrain()
|
|
101
|
+
} else {
|
|
102
|
+
const parentDir = path.dirname(targetPath)
|
|
103
|
+
fs.mkdirSync(parentDir, { recursive: true })
|
|
104
|
+
const writeStream = fs.createWriteStream(targetPath)
|
|
105
|
+
const task = pipeline(entry, writeStream).catch((err) => {
|
|
106
|
+
hasError = true
|
|
107
|
+
throw new Error(`解压文件失败 ${entryPath}: ${err.message}`)
|
|
108
|
+
})
|
|
109
|
+
tasks.push(task)
|
|
110
|
+
}
|
|
111
|
+
} catch (err) {
|
|
112
|
+
hasError = true
|
|
113
|
+
entry.autodrain()
|
|
114
|
+
reject(new Error(`处理entry失败: ${err}`))
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
.on('close', async () => {
|
|
118
|
+
try {
|
|
119
|
+
await Promise.all(tasks)
|
|
120
|
+
resolve(null)
|
|
121
|
+
} catch (err) {
|
|
122
|
+
reject(err)
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
.on('error', (err: { message: any }) => {
|
|
126
|
+
hasError = true
|
|
127
|
+
reject(new Error(`解压流错误: ${err.message}`))
|
|
128
|
+
})
|
|
129
|
+
})
|
|
54
130
|
sendEvent({ ...msgContent, status: 'ok' })
|
|
55
131
|
sendMessageToGateway(JSON.stringify({ method: 'skills.status', params: {} }))
|
|
56
132
|
} catch (error) {
|
package/src/transport.ts
CHANGED
|
@@ -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(
|
|
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
|
}
|
package/src/utils/constant.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
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
|
+
}
|
|
3
7
|
|
|
4
8
|
export const systemCommand = ['/new', '/status']
|
|
5
9
|
export const interruptCommand = ['/stop']
|
|
@@ -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
|
-
|
|
18
|
-
|
|
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') {
|
package/src/utils/global.ts
CHANGED
|
@@ -30,7 +30,7 @@ export function getOpenClawConfig(): OpenClawConfig | null {
|
|
|
30
30
|
function getWorkspacePath(): string | null {
|
|
31
31
|
const workspacePath = path.join(
|
|
32
32
|
os.homedir(),
|
|
33
|
-
config?.channels?.[
|
|
33
|
+
config?.channels?.[channelInfo[ENV]]?.appId == 110 ? '.mobook' : '.openclaw',
|
|
34
34
|
'workspace'
|
|
35
35
|
)
|
|
36
36
|
if (fs.existsSync(workspacePath)) {
|
|
@@ -55,7 +55,7 @@ export function getWorkspaceDir(): string {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
const { setRuntime: setDcgchatRuntime, getRuntime: getDcgchatRuntime } = createPluginRuntimeStore<PluginRuntime>(
|
|
58
|
-
`${
|
|
58
|
+
`${channelInfo[ENV]} runtime not initialized`
|
|
59
59
|
)
|
|
60
60
|
export { setDcgchatRuntime, getDcgchatRuntime }
|
|
61
61
|
|
|
@@ -130,7 +130,7 @@ export const getSessionKey = (content: any, accountId: string) => {
|
|
|
130
130
|
|
|
131
131
|
const route = core.channel.routing.resolveAgentRoute({
|
|
132
132
|
cfg: getOpenClawConfig() as OpenClawConfig,
|
|
133
|
-
channel:
|
|
133
|
+
channel: channelInfo[ENV],
|
|
134
134
|
accountId: accountId || 'default',
|
|
135
135
|
peer: { kind: 'direct', id: session_id }
|
|
136
136
|
})
|
package/src/utils/log.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RuntimeEnv } from 'openclaw/plugin-sdk'
|
|
2
|
+
import { channelInfo, ENV } from './constant.js'
|
|
2
3
|
|
|
3
4
|
let logger: RuntimeEnv | null = null
|
|
4
5
|
|
|
@@ -10,6 +11,6 @@ export function dcgLogger(message: string, type: 'log' | 'error' = 'log'): void
|
|
|
10
11
|
if (logger) {
|
|
11
12
|
logger[type](`书灵墨宝🚀 ~ [${new Date().toISOString()}] ${message}`)
|
|
12
13
|
} else {
|
|
13
|
-
console[type](`书灵墨宝🚀 ~ ${new Date().toISOString()} [${
|
|
14
|
+
console[type](`书灵墨宝🚀 ~ ${new Date().toISOString()} [${channelInfo[ENV]}]: ${message}`)
|
|
14
15
|
}
|
|
15
16
|
}
|
package/src/utils/params.ts
CHANGED
|
@@ -9,7 +9,7 @@ const paramsMessageMap = new Map<string, IMsgParams>()
|
|
|
9
9
|
|
|
10
10
|
/** 从 OpenClaw 配置读取当前 channel 的基础参数(唯一来源,供 transport / resolve 等复用) */
|
|
11
11
|
export function getParamsDefaults(): IMsgParams {
|
|
12
|
-
const ch = (getOpenClawConfig()?.channels?.[
|
|
12
|
+
const ch = (getOpenClawConfig()?.channels?.[channelInfo[ENV]] as DcgchatConfig | undefined) ?? {}
|
|
13
13
|
return {
|
|
14
14
|
userId: Number(ch.userId ?? 0),
|
|
15
15
|
botToken: ch.botToken ?? '',
|