ddchat 0.1.0
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 +14 -0
- package/index.ts +13 -0
- package/openclaw.plugin.json +15 -0
- package/package.json +36 -0
- package/setup-entry.ts +4 -0
- package/src/channel.ts +101 -0
- package/src/constants.ts +5 -0
- package/src/dedupe.ts +31 -0
- package/src/gateway.ts +237 -0
- package/src/inbound.ts +379 -0
- package/src/outbound.ts +95 -0
- package/src/pairing.ts +9 -0
- package/src/runtime.ts +24 -0
- package/src/session.ts +19 -0
- package/src/types.ts +126 -0
- package/task/BLOCKERS.md +3 -0
- package/task/DOING.md +3 -0
- package/task/DONE.md +8 -0
- package/task/README.md +17 -0
- package/task/TODO.md +10 -0
- package/test/README.md +48 -0
- package/test/chat.html +304 -0
- package/test/server.mjs +143 -0
package/task/TODO.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# TODO
|
|
2
|
+
|
|
3
|
+
- [ ] Finalize plugin skeleton (`package.json`, `openclaw.plugin.json`, `index.ts`, `setup-entry.ts`)
|
|
4
|
+
- [ ] Implement Feishu-style multi-account config (`channels.ddchat.accounts.*`)
|
|
5
|
+
- [ ] Implement websocket inbound loop (webhook reserved only)
|
|
6
|
+
- [ ] Implement inbound routing for `group` (groupId) and `direct` (userId)
|
|
7
|
+
- [ ] Implement inbound text + file/image handling (base64/url -> saveMediaBuffer)
|
|
8
|
+
- [ ] Implement chunk-level streaming push; keep token-level mode as reserved option
|
|
9
|
+
- [ ] Add compatibility notes for future plugin rename
|
|
10
|
+
- [ ] Add minimal tests for route/media parsing and duplicate suppression
|
package/test/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# DDChat Local Mock IM
|
|
2
|
+
|
|
3
|
+
Local WebSocket IM simulator for testing the `ddchat` plugin.
|
|
4
|
+
|
|
5
|
+
## Start
|
|
6
|
+
|
|
7
|
+
From repo root:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
node ddchat/test/server.mjs
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then open:
|
|
14
|
+
|
|
15
|
+
- UI: `http://127.0.0.1:9020`
|
|
16
|
+
- WS endpoint for plugin: `ws://127.0.0.1:9001` (override plugin `constants.ts` for local dev, or run against your mock URL)
|
|
17
|
+
|
|
18
|
+
## Plugin config example
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"channels": {
|
|
23
|
+
"ddchat": {
|
|
24
|
+
"defaultAccount": "xkx",
|
|
25
|
+
"accounts": {
|
|
26
|
+
"xkx": {
|
|
27
|
+
"enabled": true,
|
|
28
|
+
"token": "your-plugin-token",
|
|
29
|
+
"connectionMode": "websocket",
|
|
30
|
+
"wsUrl": "ws://127.0.0.1:9001",
|
|
31
|
+
"dmPolicy": "open",
|
|
32
|
+
"groupPolicy": "open",
|
|
33
|
+
"requireMention": false,
|
|
34
|
+
"streaming": true,
|
|
35
|
+
"streamingMode": "chunk"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Features
|
|
44
|
+
|
|
45
|
+
- Send direct/group inbound message payloads.
|
|
46
|
+
- Upload files/images from browser (sent as base64 in `files[]`).
|
|
47
|
+
- Optional URL-based file input.
|
|
48
|
+
- View stream chunks (`stream_chunk`) and final outbound messages.
|
package/test/chat.html
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>DDChat Mock IM Chat</title>
|
|
7
|
+
<style>
|
|
8
|
+
body {
|
|
9
|
+
font-family: Consolas, Menlo, monospace;
|
|
10
|
+
background: #0b1020;
|
|
11
|
+
color: #d4ddff;
|
|
12
|
+
margin: 0;
|
|
13
|
+
}
|
|
14
|
+
.wrap {
|
|
15
|
+
max-width: 1080px;
|
|
16
|
+
margin: 24px auto;
|
|
17
|
+
padding: 0 16px;
|
|
18
|
+
}
|
|
19
|
+
h1 {
|
|
20
|
+
font-size: 18px;
|
|
21
|
+
margin: 0 0 12px;
|
|
22
|
+
}
|
|
23
|
+
.panel {
|
|
24
|
+
background: #121a33;
|
|
25
|
+
border: 1px solid #24315f;
|
|
26
|
+
border-radius: 10px;
|
|
27
|
+
padding: 12px;
|
|
28
|
+
margin-bottom: 12px;
|
|
29
|
+
}
|
|
30
|
+
label {
|
|
31
|
+
font-size: 12px;
|
|
32
|
+
color: #9fb1f1;
|
|
33
|
+
display: block;
|
|
34
|
+
margin-bottom: 4px;
|
|
35
|
+
}
|
|
36
|
+
input,
|
|
37
|
+
textarea,
|
|
38
|
+
button,
|
|
39
|
+
select {
|
|
40
|
+
width: 100%;
|
|
41
|
+
box-sizing: border-box;
|
|
42
|
+
border-radius: 8px;
|
|
43
|
+
border: 1px solid #2f3e74;
|
|
44
|
+
background: #0f1630;
|
|
45
|
+
color: #d4ddff;
|
|
46
|
+
padding: 8px;
|
|
47
|
+
}
|
|
48
|
+
textarea {
|
|
49
|
+
min-height: 80px;
|
|
50
|
+
}
|
|
51
|
+
.row {
|
|
52
|
+
display: grid;
|
|
53
|
+
gap: 8px;
|
|
54
|
+
grid-template-columns: repeat(4, 1fr);
|
|
55
|
+
}
|
|
56
|
+
.row-3 {
|
|
57
|
+
display: grid;
|
|
58
|
+
gap: 8px;
|
|
59
|
+
grid-template-columns: repeat(3, 1fr);
|
|
60
|
+
}
|
|
61
|
+
.log {
|
|
62
|
+
height: 220px;
|
|
63
|
+
overflow: auto;
|
|
64
|
+
background: #090e1f;
|
|
65
|
+
border: 1px solid #24315f;
|
|
66
|
+
border-radius: 8px;
|
|
67
|
+
padding: 8px;
|
|
68
|
+
white-space: pre-wrap;
|
|
69
|
+
line-height: 1.35;
|
|
70
|
+
}
|
|
71
|
+
.btn {
|
|
72
|
+
background: #2442ad;
|
|
73
|
+
cursor: pointer;
|
|
74
|
+
}
|
|
75
|
+
.chat {
|
|
76
|
+
height: 320px;
|
|
77
|
+
overflow: auto;
|
|
78
|
+
background: #090e1f;
|
|
79
|
+
border: 1px solid #24315f;
|
|
80
|
+
border-radius: 8px;
|
|
81
|
+
padding: 10px;
|
|
82
|
+
margin-bottom: 12px;
|
|
83
|
+
}
|
|
84
|
+
.msg {
|
|
85
|
+
margin: 8px 0;
|
|
86
|
+
padding: 8px 10px;
|
|
87
|
+
border-radius: 10px;
|
|
88
|
+
max-width: 86%;
|
|
89
|
+
white-space: pre-wrap;
|
|
90
|
+
}
|
|
91
|
+
.msg.user {
|
|
92
|
+
margin-left: auto;
|
|
93
|
+
background: #1e3b87;
|
|
94
|
+
}
|
|
95
|
+
.msg.assistant {
|
|
96
|
+
margin-right: auto;
|
|
97
|
+
background: #1e2750;
|
|
98
|
+
}
|
|
99
|
+
.msg.done {
|
|
100
|
+
border: 1px solid #3954b3;
|
|
101
|
+
}
|
|
102
|
+
.meta {
|
|
103
|
+
margin-top: 6px;
|
|
104
|
+
font-size: 12px;
|
|
105
|
+
color: #9fb1f1;
|
|
106
|
+
}
|
|
107
|
+
.preview {
|
|
108
|
+
margin-top: 8px;
|
|
109
|
+
border: 1px solid #2f3e74;
|
|
110
|
+
border-radius: 8px;
|
|
111
|
+
padding: 8px;
|
|
112
|
+
background: #0f1630;
|
|
113
|
+
}
|
|
114
|
+
.preview img {
|
|
115
|
+
max-width: 280px;
|
|
116
|
+
max-height: 220px;
|
|
117
|
+
border-radius: 6px;
|
|
118
|
+
display: block;
|
|
119
|
+
}
|
|
120
|
+
</style>
|
|
121
|
+
</head>
|
|
122
|
+
<body>
|
|
123
|
+
<div class="wrap">
|
|
124
|
+
<h1>DDChat Mock IM (WebSocket)</h1>
|
|
125
|
+
<div class="panel row">
|
|
126
|
+
<div>
|
|
127
|
+
<label>Account ID</label>
|
|
128
|
+
<input id="accountId" value="xkx" />
|
|
129
|
+
</div>
|
|
130
|
+
<div>
|
|
131
|
+
<label>Chat Type</label>
|
|
132
|
+
<select id="chatType">
|
|
133
|
+
<option value="direct">direct</option>
|
|
134
|
+
<option value="group">group</option>
|
|
135
|
+
</select>
|
|
136
|
+
</div>
|
|
137
|
+
<div>
|
|
138
|
+
<label>User ID</label>
|
|
139
|
+
<input id="userId" value="u-1001" />
|
|
140
|
+
</div>
|
|
141
|
+
<div>
|
|
142
|
+
<label>Group ID (group 模式必填)</label>
|
|
143
|
+
<input id="groupId" value="g-1001" />
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
<div class="panel row-3">
|
|
147
|
+
<div>
|
|
148
|
+
<label>Message ID</label>
|
|
149
|
+
<input id="messageId" value="" placeholder="默认自动生成" />
|
|
150
|
+
</div>
|
|
151
|
+
<div>
|
|
152
|
+
<label>文件上传(支持多选)</label>
|
|
153
|
+
<input id="fileInput" type="file" multiple />
|
|
154
|
+
</div>
|
|
155
|
+
<div>
|
|
156
|
+
<label>或填远程文件 URL(可选)</label>
|
|
157
|
+
<input id="mediaUrl" placeholder="https://example.com/photo.png" />
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
<div class="panel">
|
|
161
|
+
<label>Text</label>
|
|
162
|
+
<textarea id="text">请介绍一下你自己,并分三段回答。</textarea>
|
|
163
|
+
<button class="btn" id="sendBtn">Send inbound_message</button>
|
|
164
|
+
</div>
|
|
165
|
+
<div class="panel">
|
|
166
|
+
<div id="status">Connecting to ws://127.0.0.1:9001 ...</div>
|
|
167
|
+
<div class="chat" id="chat"></div>
|
|
168
|
+
<div class="log" id="log"></div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
<script>
|
|
172
|
+
const ws = new WebSocket("ws://127.0.0.1:9001/?role=ui");
|
|
173
|
+
const log = document.getElementById("log");
|
|
174
|
+
const chat = document.getElementById("chat");
|
|
175
|
+
const status = document.getElementById("status");
|
|
176
|
+
const streamMap = new Map();
|
|
177
|
+
const append = (line) => {
|
|
178
|
+
const now = new Date().toISOString();
|
|
179
|
+
log.textContent += `[${now}] ${line}\n`;
|
|
180
|
+
log.scrollTop = log.scrollHeight;
|
|
181
|
+
};
|
|
182
|
+
const addUserMessage = (payload) => {
|
|
183
|
+
const div = document.createElement("div");
|
|
184
|
+
div.className = "msg user";
|
|
185
|
+
div.textContent = payload.text || "(empty text)";
|
|
186
|
+
const meta = document.createElement("div");
|
|
187
|
+
meta.className = "meta";
|
|
188
|
+
meta.textContent = `account=${payload.accountId} | ${payload.chatType}:${payload.chatType === "group" ? payload.groupId : payload.userId} | files=${payload.files?.length || 0}`;
|
|
189
|
+
div.appendChild(meta);
|
|
190
|
+
chat.appendChild(div);
|
|
191
|
+
chat.scrollTop = chat.scrollHeight;
|
|
192
|
+
};
|
|
193
|
+
const addAssistantMessage = (payload) => {
|
|
194
|
+
const div = document.createElement("div");
|
|
195
|
+
div.className = "msg assistant";
|
|
196
|
+
const text = payload.text || "(no text)";
|
|
197
|
+
div.textContent = text;
|
|
198
|
+
if (payload.mediaUrl) {
|
|
199
|
+
const preview = document.createElement("div");
|
|
200
|
+
preview.className = "preview";
|
|
201
|
+
const link = document.createElement("a");
|
|
202
|
+
link.href = payload.mediaUrl;
|
|
203
|
+
link.target = "_blank";
|
|
204
|
+
link.rel = "noreferrer";
|
|
205
|
+
link.textContent = `media: ${payload.mediaUrl}`;
|
|
206
|
+
link.style.color = "#9bc4ff";
|
|
207
|
+
preview.appendChild(link);
|
|
208
|
+
div.appendChild(preview);
|
|
209
|
+
}
|
|
210
|
+
chat.appendChild(div);
|
|
211
|
+
chat.scrollTop = chat.scrollHeight;
|
|
212
|
+
};
|
|
213
|
+
const upsertAssistantStream = (payload) => {
|
|
214
|
+
const key = payload.streamId || `stream-${Date.now()}`;
|
|
215
|
+
let node = streamMap.get(key);
|
|
216
|
+
if (!node) {
|
|
217
|
+
node = document.createElement("div");
|
|
218
|
+
node.className = "msg assistant";
|
|
219
|
+
node.textContent = "";
|
|
220
|
+
chat.appendChild(node);
|
|
221
|
+
streamMap.set(key, node);
|
|
222
|
+
}
|
|
223
|
+
node.textContent = payload.fullText || node.textContent + (payload.delta || "");
|
|
224
|
+
if (payload.done) {
|
|
225
|
+
node.classList.add("done");
|
|
226
|
+
const meta = document.createElement("div");
|
|
227
|
+
meta.className = "meta";
|
|
228
|
+
meta.textContent = `stream done | mode=${payload.mode || "chunk"}`;
|
|
229
|
+
node.appendChild(meta);
|
|
230
|
+
}
|
|
231
|
+
chat.scrollTop = chat.scrollHeight;
|
|
232
|
+
};
|
|
233
|
+
ws.addEventListener("open", () => {
|
|
234
|
+
status.textContent = "Connected";
|
|
235
|
+
append("connected");
|
|
236
|
+
});
|
|
237
|
+
ws.addEventListener("close", () => {
|
|
238
|
+
status.textContent = "Disconnected";
|
|
239
|
+
append("disconnected");
|
|
240
|
+
});
|
|
241
|
+
ws.addEventListener("message", (ev) => {
|
|
242
|
+
append(`recv: ${ev.data}`);
|
|
243
|
+
try {
|
|
244
|
+
const data = JSON.parse(ev.data);
|
|
245
|
+
const payload = data?.payload || data;
|
|
246
|
+
if (payload?.type === "stream_chunk") {
|
|
247
|
+
upsertAssistantStream(payload);
|
|
248
|
+
}
|
|
249
|
+
if (payload?.type === "outbound_message") {
|
|
250
|
+
addAssistantMessage(payload);
|
|
251
|
+
}
|
|
252
|
+
} catch {}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
async function fileToBase64(file) {
|
|
256
|
+
const buf = await file.arrayBuffer();
|
|
257
|
+
let binary = "";
|
|
258
|
+
const bytes = new Uint8Array(buf);
|
|
259
|
+
const chunkSize = 0x8000;
|
|
260
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
261
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
|
|
262
|
+
}
|
|
263
|
+
return btoa(binary);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
document.getElementById("sendBtn").addEventListener("click", async () => {
|
|
267
|
+
const fileInput = document.getElementById("fileInput");
|
|
268
|
+
const files = [];
|
|
269
|
+
for (const file of Array.from(fileInput.files || [])) {
|
|
270
|
+
files.push({
|
|
271
|
+
name: file.name,
|
|
272
|
+
type: file.type || "application/octet-stream",
|
|
273
|
+
base64: await fileToBase64(file),
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
const mediaUrl = document.getElementById("mediaUrl").value || "";
|
|
277
|
+
if (mediaUrl.trim()) {
|
|
278
|
+
files.push({
|
|
279
|
+
name: undefined,
|
|
280
|
+
type: undefined,
|
|
281
|
+
url: mediaUrl.trim(),
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const payload = {
|
|
286
|
+
type: "inbound_message",
|
|
287
|
+
accountId: document.getElementById("accountId").value || "xkx",
|
|
288
|
+
chatType: document.getElementById("chatType").value === "group" ? "group" : "direct",
|
|
289
|
+
userId: document.getElementById("userId").value || "u-1001",
|
|
290
|
+
groupId: document.getElementById("groupId").value || undefined,
|
|
291
|
+
messageId: document.getElementById("messageId").value || `m-${Date.now()}`,
|
|
292
|
+
text: document.getElementById("text").value || "",
|
|
293
|
+
files: files.length > 0 ? files : undefined,
|
|
294
|
+
};
|
|
295
|
+
if (payload.chatType === "direct") {
|
|
296
|
+
delete payload.groupId;
|
|
297
|
+
}
|
|
298
|
+
addUserMessage(payload);
|
|
299
|
+
ws.send(JSON.stringify(payload));
|
|
300
|
+
append(`send: ${JSON.stringify({ ...payload, files: payload.files ? `[${payload.files.length} file(s)]` : undefined })}`);
|
|
301
|
+
});
|
|
302
|
+
</script>
|
|
303
|
+
</body>
|
|
304
|
+
</html>
|
package/test/server.mjs
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { WebSocketServer } from "ws";
|
|
4
|
+
|
|
5
|
+
const HTTP_PORT = Number(process.env.DDCHAT_MOCK_HTTP_PORT ?? 9020);
|
|
6
|
+
const WS_PORT = Number(process.env.DDCHAT_MOCK_WS_PORT ?? 9001);
|
|
7
|
+
|
|
8
|
+
const clients = new Set();
|
|
9
|
+
const uiClients = new Set();
|
|
10
|
+
let pluginClient = null;
|
|
11
|
+
const history = [];
|
|
12
|
+
|
|
13
|
+
const server = http.createServer(async (req, res) => {
|
|
14
|
+
if (!req.url || req.url === "/") {
|
|
15
|
+
try {
|
|
16
|
+
const html = await readFile(new URL("./chat.html", import.meta.url), "utf-8");
|
|
17
|
+
res.statusCode = 200;
|
|
18
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
19
|
+
res.end(html);
|
|
20
|
+
return;
|
|
21
|
+
} catch (error) {
|
|
22
|
+
res.statusCode = 500;
|
|
23
|
+
res.end(String(error));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (req.url === "/history") {
|
|
28
|
+
res.statusCode = 200;
|
|
29
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
30
|
+
res.end(JSON.stringify(history.slice(-300)));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
res.statusCode = 404;
|
|
34
|
+
res.end("not found");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
server.listen(HTTP_PORT, () => {
|
|
38
|
+
console.log(`[ddchat-mock] ui: http://127.0.0.1:${HTTP_PORT}`);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const wss = new WebSocketServer({ port: WS_PORT });
|
|
42
|
+
wss.on("connection", (ws, req) => {
|
|
43
|
+
clients.add(ws);
|
|
44
|
+
const role = new URL(req.url ?? "/", "http://localhost").searchParams.get("role");
|
|
45
|
+
const isUi = role === "ui";
|
|
46
|
+
if (isUi) {
|
|
47
|
+
uiClients.add(ws);
|
|
48
|
+
console.log("[ddchat-mock] ui connected");
|
|
49
|
+
} else {
|
|
50
|
+
pluginClient = ws;
|
|
51
|
+
console.log("[ddchat-mock] ddchat plugin connected");
|
|
52
|
+
}
|
|
53
|
+
ws.send(
|
|
54
|
+
JSON.stringify({
|
|
55
|
+
type: "hello",
|
|
56
|
+
role: "ddchat-mock",
|
|
57
|
+
ts: Date.now(),
|
|
58
|
+
peerRole: isUi ? "ui" : "plugin",
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
ws.on("close", () => {
|
|
62
|
+
clients.delete(ws);
|
|
63
|
+
uiClients.delete(ws);
|
|
64
|
+
if (pluginClient === ws) {
|
|
65
|
+
pluginClient = null;
|
|
66
|
+
console.log("[ddchat-mock] ddchat plugin disconnected");
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
ws.on("message", (buf) => {
|
|
70
|
+
const text = buf.toString("utf-8");
|
|
71
|
+
let payload;
|
|
72
|
+
try {
|
|
73
|
+
payload = JSON.parse(text);
|
|
74
|
+
} catch {
|
|
75
|
+
payload = { type: "raw", text };
|
|
76
|
+
}
|
|
77
|
+
if (uiClients.has(ws)) {
|
|
78
|
+
history.push({ direction: "to_plugin", payload, ts: Date.now() });
|
|
79
|
+
if (pluginClient && pluginClient.readyState === 1) {
|
|
80
|
+
pluginClient.send(JSON.stringify(payload));
|
|
81
|
+
}
|
|
82
|
+
broadcastToUi({
|
|
83
|
+
type: "event",
|
|
84
|
+
event: "to_plugin",
|
|
85
|
+
payload,
|
|
86
|
+
ts: Date.now(),
|
|
87
|
+
});
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
history.push({ direction: "from_plugin", payload, ts: Date.now() });
|
|
91
|
+
broadcastToUi({
|
|
92
|
+
type: "event",
|
|
93
|
+
event: "from_plugin",
|
|
94
|
+
payload,
|
|
95
|
+
ts: Date.now(),
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
console.log(`[ddchat-mock] websocket: ws://127.0.0.1:${WS_PORT}`);
|
|
101
|
+
|
|
102
|
+
function broadcastToUi(data) {
|
|
103
|
+
const text = JSON.stringify(data);
|
|
104
|
+
for (const ws of uiClients) {
|
|
105
|
+
try {
|
|
106
|
+
ws.send(text);
|
|
107
|
+
} catch {}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
process.stdin.setEncoding("utf-8");
|
|
112
|
+
process.stdin.on("data", (chunk) => {
|
|
113
|
+
const text = chunk.trim();
|
|
114
|
+
if (!text) return;
|
|
115
|
+
if (text === "help") {
|
|
116
|
+
console.log(
|
|
117
|
+
'stdin example: {"accountId":"xkx","chatType":"direct","userId":"u-1001","messageId":"m-1","text":"hello"}',
|
|
118
|
+
);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const payload = JSON.parse(text);
|
|
123
|
+
const message = {
|
|
124
|
+
type: "inbound_message",
|
|
125
|
+
accountId: payload.accountId ?? "xkx",
|
|
126
|
+
messageId: payload.messageId ?? `m-${Date.now()}`,
|
|
127
|
+
chatType: payload.chatType === "group" ? "group" : "direct",
|
|
128
|
+
userId: payload.userId ?? "u-1001",
|
|
129
|
+
groupId: payload.groupId,
|
|
130
|
+
text: payload.text ?? "",
|
|
131
|
+
files: Array.isArray(payload.files) ? payload.files : undefined,
|
|
132
|
+
ts: Date.now(),
|
|
133
|
+
};
|
|
134
|
+
history.push({ direction: "to_plugin", payload: message, ts: Date.now() });
|
|
135
|
+
broadcastToUi({ type: "event", event: "to_plugin", payload: message, ts: Date.now() });
|
|
136
|
+
if (pluginClient && pluginClient.readyState === 1) {
|
|
137
|
+
pluginClient.send(JSON.stringify(message));
|
|
138
|
+
}
|
|
139
|
+
console.log("[ddchat-mock] sent inbound_message");
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error("[ddchat-mock] invalid json:", error);
|
|
142
|
+
}
|
|
143
|
+
});
|