ddchat 0.1.0 → 0.2.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/test/chat.html CHANGED
@@ -1,304 +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>
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>