@dcrays/dcgchat-test 0.2.24 → 0.2.26
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/openclaw.plugin.json +2 -4
- package/package.json +7 -4
- package/src/bot.ts +64 -7
- package/src/channel.ts +10 -10
- package/src/request.ts +1 -1
- package/src/skill.ts +4 -4
- package/src/tool.ts +2 -2
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/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dcrays/dcgchat-test",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.26",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw channel plugin for 书灵墨宝 (WebSocket)",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -36,15 +36,18 @@
|
|
|
36
36
|
"id": "dcgchat-test",
|
|
37
37
|
"label": "书灵墨宝",
|
|
38
38
|
"selectionLabel": "书灵墨宝",
|
|
39
|
-
"docsPath": "/channels/dcgchat
|
|
39
|
+
"docsPath": "/channels/dcgchat",
|
|
40
40
|
"docsLabel": "dcgchat-test",
|
|
41
41
|
"blurb": "连接 OpenClaw 与 书灵墨宝 产品",
|
|
42
42
|
"order": 80
|
|
43
43
|
},
|
|
44
44
|
"install": {
|
|
45
45
|
"npmSpec": "@dcrays/dcgchat-test",
|
|
46
|
-
"localPath": "extensions/dcgchat
|
|
46
|
+
"localPath": "extensions/dcgchat",
|
|
47
47
|
"defaultChoice": "npm"
|
|
48
48
|
}
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"openclaw": "^2026.3.13"
|
|
49
52
|
}
|
|
50
|
-
}
|
|
53
|
+
}
|
package/src/bot.ts
CHANGED
|
@@ -226,11 +226,64 @@ const EXT_LIST = [
|
|
|
226
226
|
"bin",
|
|
227
227
|
];
|
|
228
228
|
|
|
229
|
+
/**
|
|
230
|
+
* 扩展名按长度降序,用于正则交替,避免 xls 抢先匹配 xlsx、htm 抢先匹配 html 等
|
|
231
|
+
*/
|
|
232
|
+
const EXT_SORTED_FOR_REGEX = [...EXT_LIST].sort((a, b) => b.length - a.length);
|
|
233
|
+
|
|
234
|
+
/** 去除控制符、零宽字符等常见脏值 */
|
|
235
|
+
function stripMobookNoise(s: string) {
|
|
236
|
+
return s.replace(/[\u0000-\u001F\u007F\u200B-\u200D\u200E\u200F\uFEFF]/g, "");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 从文本中扫描 `/mobook/` 片段,按最长后缀匹配合法扩展名(兜底,不依赖 FILE_NAME 字符集)
|
|
241
|
+
*/
|
|
242
|
+
function collectMobookPathsByScan(text: string, result: Set<string>): void {
|
|
243
|
+
const lower = text.toLowerCase();
|
|
244
|
+
const needle = "/mobook/";
|
|
245
|
+
let from = 0;
|
|
246
|
+
while (from < text.length) {
|
|
247
|
+
const i = lower.indexOf(needle, from);
|
|
248
|
+
if (i < 0) break;
|
|
249
|
+
const start = i + needle.length;
|
|
250
|
+
const tail = text.slice(start);
|
|
251
|
+
const seg = tail.match(/^([^\s\]\)'"}\u3002,,]+)/);
|
|
252
|
+
if (!seg) {
|
|
253
|
+
from = start + 1;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
let raw = stripMobookNoise(seg[1]).trim();
|
|
257
|
+
if (!raw || raw.includes("\uFFFD")) {
|
|
258
|
+
from = start + 1;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const low = raw.toLowerCase();
|
|
262
|
+
let matchedExt: string | undefined;
|
|
263
|
+
for (const ext of EXT_SORTED_FOR_REGEX) {
|
|
264
|
+
if (low.endsWith(`.${ext}`)) {
|
|
265
|
+
matchedExt = ext;
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (!matchedExt) {
|
|
270
|
+
from = start + 1;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const base = raw.slice(0, -(matchedExt.length + 1));
|
|
274
|
+
const fileName = `${base}.${matchedExt}`;
|
|
275
|
+
if (isValidFileName(fileName)) {
|
|
276
|
+
result.add(normalizePath(`/mobook/${fileName}`));
|
|
277
|
+
}
|
|
278
|
+
from = start + 1;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
229
282
|
function extractMobookFiles(text = "") {
|
|
230
283
|
if (typeof text !== "string" || !text.trim()) return [];
|
|
231
|
-
const result = new Set();
|
|
232
|
-
// ✅
|
|
233
|
-
const EXT = `(${
|
|
284
|
+
const result = new Set<string>();
|
|
285
|
+
// ✅ 扩展名(必须长扩展名优先,见 EXT_SORTED_FOR_REGEX)
|
|
286
|
+
const EXT = `(${EXT_SORTED_FOR_REGEX.join("|")})`;
|
|
234
287
|
// ✅ 文件名字符(增强:支持中文、符号)
|
|
235
288
|
const FILE_NAME = `[\\w\\u4e00-\\u9fa5::《》()()\\-\\s]+?`;
|
|
236
289
|
try {
|
|
@@ -271,6 +324,8 @@ function extractMobookFiles(text = "") {
|
|
|
271
324
|
result.add(`/mobook/${name}`);
|
|
272
325
|
}
|
|
273
326
|
});
|
|
327
|
+
// 6️⃣ 兜底:绝对路径等 `.../mobook/<文件名>.<扩展名>` + 最长后缀匹配 + 去脏字符
|
|
328
|
+
collectMobookPathsByScan(text, result);
|
|
274
329
|
} catch (e) {
|
|
275
330
|
console.warn("extractMobookFiles error:", e);
|
|
276
331
|
}
|
|
@@ -282,10 +337,13 @@ function extractMobookFiles(text = "") {
|
|
|
282
337
|
*/
|
|
283
338
|
function isValidFileName(name: string) {
|
|
284
339
|
if (!name) return false;
|
|
340
|
+
const cleaned = stripMobookNoise(name).trim();
|
|
341
|
+
if (!cleaned) return false;
|
|
342
|
+
if (cleaned.includes("\uFFFD")) return false;
|
|
285
343
|
// 过滤异常字符
|
|
286
|
-
if (/[\/\\<>:"|?*]/.test(
|
|
344
|
+
if (/[\/\\<>:"|?*]/.test(cleaned)) return false;
|
|
287
345
|
// 长度限制(防止异常长字符串)
|
|
288
|
-
if (
|
|
346
|
+
if (cleaned.length > 200) return false;
|
|
289
347
|
return true;
|
|
290
348
|
}
|
|
291
349
|
|
|
@@ -369,11 +427,9 @@ export async function handleDcgchatMessage(params: {
|
|
|
369
427
|
|
|
370
428
|
// Abort any existing generation for this conversation, then start a new one
|
|
371
429
|
const existingCtrl = activeGenerations.get(conversationId);
|
|
372
|
-
console.log("🚀 ~ handleDcgchatMessage ~ conversationId:", existingCtrl)
|
|
373
430
|
if (existingCtrl) existingCtrl.abort();
|
|
374
431
|
const genCtrl = new AbortController();
|
|
375
432
|
const genSignal = genCtrl.signal;
|
|
376
|
-
console.log("🚀 ~ handleDcgchatMessage ~ conversationId:", conversationId)
|
|
377
433
|
activeGenerations.set(conversationId, genCtrl);
|
|
378
434
|
|
|
379
435
|
// 处理用户上传的文件
|
|
@@ -668,3 +724,4 @@ export async function handleDcgchatMessage(params: {
|
|
|
668
724
|
});
|
|
669
725
|
}
|
|
670
726
|
}
|
|
727
|
+
|
package/src/channel.ts
CHANGED
|
@@ -95,7 +95,7 @@ export async function sendDcgchatMedia(ctx: DcgchatMediaSendContext): Promise<vo
|
|
|
95
95
|
|
|
96
96
|
export function resolveAccount(cfg: OpenClawConfig, accountId?: string | null): ResolvedDcgchatAccount {
|
|
97
97
|
const id = accountId ?? DEFAULT_ACCOUNT_ID;
|
|
98
|
-
const raw = (cfg.channels?.["dcgchat
|
|
98
|
+
const raw = (cfg.channels?.["dcgchat"] as DcgchatConfig | undefined) ?? {};
|
|
99
99
|
return {
|
|
100
100
|
accountId: id,
|
|
101
101
|
enabled: raw.enabled !== false,
|
|
@@ -109,13 +109,13 @@ export function resolveAccount(cfg: OpenClawConfig, accountId?: string | null):
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
112
|
-
id: "dcgchat
|
|
112
|
+
id: "dcgchat",
|
|
113
113
|
meta: {
|
|
114
|
-
id: "dcgchat
|
|
114
|
+
id: "dcgchat",
|
|
115
115
|
label: "书灵墨宝",
|
|
116
116
|
selectionLabel: "书灵墨宝",
|
|
117
|
-
docsPath: "/channels/dcgchat
|
|
118
|
-
docsLabel: "dcgchat
|
|
117
|
+
docsPath: "/channels/dcgchat",
|
|
118
|
+
docsLabel: "dcgchat",
|
|
119
119
|
blurb: "连接 OpenClaw 与 书灵墨宝 产品",
|
|
120
120
|
order: 80,
|
|
121
121
|
},
|
|
@@ -131,7 +131,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
131
131
|
effects: true,
|
|
132
132
|
// blockStreaming: true,
|
|
133
133
|
},
|
|
134
|
-
reload: { configPrefixes: ["channels.dcgchat
|
|
134
|
+
reload: { configPrefixes: ["channels.dcgchat"] },
|
|
135
135
|
configSchema: {
|
|
136
136
|
schema: {
|
|
137
137
|
type: "object",
|
|
@@ -155,8 +155,8 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
155
155
|
...cfg,
|
|
156
156
|
channels: {
|
|
157
157
|
...cfg.channels,
|
|
158
|
-
"dcgchat
|
|
159
|
-
...(cfg.channels?.["dcgchat
|
|
158
|
+
"dcgchat": {
|
|
159
|
+
...(cfg.channels?.["dcgchat"] as Record<string, unknown> | undefined),
|
|
160
160
|
enabled,
|
|
161
161
|
},
|
|
162
162
|
},
|
|
@@ -227,7 +227,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
227
227
|
log(`[dcgchat][${ctx.accountId ?? DEFAULT_ACCOUNT_ID}] outbound -> : ${ctx.text}`);
|
|
228
228
|
}
|
|
229
229
|
return {
|
|
230
|
-
channel: "dcgchat
|
|
230
|
+
channel: "dcgchat",
|
|
231
231
|
messageId: `dcg-${Date.now()}`,
|
|
232
232
|
chatId: params.userId.toString(),
|
|
233
233
|
};
|
|
@@ -236,7 +236,7 @@ export const dcgchatPlugin: ChannelPlugin<ResolvedDcgchatAccount> = {
|
|
|
236
236
|
const params = getMsgParams();
|
|
237
237
|
await sendDcgchatMedia(ctx);
|
|
238
238
|
return {
|
|
239
|
-
channel: "dcgchat
|
|
239
|
+
channel: "dcgchat",
|
|
240
240
|
messageId: `dcg-${Date.now()}`,
|
|
241
241
|
chatId: params.userId.toString(),
|
|
242
242
|
};
|
package/src/request.ts
CHANGED
package/src/skill.ts
CHANGED
|
@@ -46,7 +46,7 @@ function sendEvent(msgContent: Record<string, any>) {
|
|
|
46
46
|
|
|
47
47
|
// const route = core.channel.routing.resolveAgentRoute({
|
|
48
48
|
// cfg: ctx.cfg,
|
|
49
|
-
// channel: "dcgchat
|
|
49
|
+
// channel: "dcgchat",
|
|
50
50
|
// accountId: account.accountId,
|
|
51
51
|
// peer: { kind: "direct", id: userId },
|
|
52
52
|
// });
|
|
@@ -71,13 +71,13 @@ function sendEvent(msgContent: Record<string, any>) {
|
|
|
71
71
|
// ChatType: "direct",
|
|
72
72
|
// SenderName: userId,
|
|
73
73
|
// SenderId: userId,
|
|
74
|
-
// Provider: "dcgchat
|
|
75
|
-
// Surface: "dcgchat
|
|
74
|
+
// Provider: "dcgchat" as const,
|
|
75
|
+
// Surface: "dcgchat" as const,
|
|
76
76
|
// MessageSid: Date.now().toString(),
|
|
77
77
|
// Timestamp: Date.now(),
|
|
78
78
|
// WasMentioned: true,
|
|
79
79
|
// CommandAuthorized: true,
|
|
80
|
-
// OriginatingChannel: "dcgchat
|
|
80
|
+
// OriginatingChannel: "dcgchat" as const,
|
|
81
81
|
// OriginatingTo: `user:${userId}`,
|
|
82
82
|
// });
|
|
83
83
|
|
package/src/tool.ts
CHANGED
|
@@ -33,13 +33,13 @@ let toolName = '';
|
|
|
33
33
|
type PluginHookName = "before_model_resolve" | "before_prompt_build" | "before_agent_start" | "llm_input" | "llm_output" | "agent_end" | "before_compaction" | "after_compaction" | "before_reset" | "message_received" | "message_sending" | "message_sent" | "before_tool_call" | "after_tool_call" | "tool_result_persist" | "before_message_write" | "session_start" | "session_end" | "subagent_spawning" | "subagent_delivery_target" | "subagent_spawned" | "subagent_ended" | "gateway_start" | "gateway_stop";
|
|
34
34
|
const eventList = [
|
|
35
35
|
{event: 'message_received', message: ''},
|
|
36
|
-
{event: 'before_model_resolve', message: ''},
|
|
36
|
+
// {event: 'before_model_resolve', message: ''},
|
|
37
37
|
// {event: 'before_prompt_build', message: '正在查阅背景资料,构建思考逻辑'},
|
|
38
38
|
// {event: 'before_agent_start', message: '书灵墨宝已就位,准备开始执行任务'},
|
|
39
39
|
{event: 'subagent_spawning', message: ''},
|
|
40
40
|
{event: 'subagent_spawned', message: ''},
|
|
41
41
|
{event: 'subagent_delivery_target', message: ''},
|
|
42
|
-
{event: 'llm_input', message: ''},
|
|
42
|
+
// {event: 'llm_input', message: ''},
|
|
43
43
|
{event: 'llm_output', message: ''},
|
|
44
44
|
// {event: 'agent_end', message: '核心任务已处理完毕...'},
|
|
45
45
|
{event: 'subagent_ended', message: ''},
|