ddchat 0.3.0 → 0.4.1
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/{index.ts → index.js} +5 -7
- package/openclaw.plugin.json +137 -3
- package/package.json +6 -6
- package/setup-entry.js +8 -0
- package/src/channel.js +99 -0
- package/src/{constants.ts → constants.js} +0 -1
- package/src/dedupe.js +44 -0
- package/src/gateway.js +211 -0
- package/src/inbound.js +363 -0
- package/src/outbound.js +150 -0
- package/src/pairing.js +8 -0
- package/src/runtime.js +20 -0
- package/src/session.js +13 -0
- package/src/types.js +73 -0
- package/CLAUDE.md +0 -51
- package/OPTIMIZATION.md +0 -129
- package/README.md +0 -22
- package/setup-entry.ts +0 -4
- package/src/channel.ts +0 -101
- package/src/dedupe.ts +0 -51
- package/src/gateway.ts +0 -255
- package/src/inbound.ts +0 -451
- package/src/outbound.ts +0 -167
- package/src/pairing.ts +0 -9
- package/src/runtime.ts +0 -41
- package/src/session.ts +0 -19
- package/src/types.ts +0 -136
- package/task/BLOCKERS.md +0 -3
- package/task/DOING.md +0 -3
- package/task/DONE.md +0 -8
- package/task/README.md +0 -17
- package/task/TODO.md +0 -10
- package/test/README.md +0 -48
- package/test/chat.html +0 -304
- package/test/server.mjs +0 -143
package/src/types.ts
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
2
|
-
import { DDCHAT_CHANNEL_ID, DDCHAT_DEFAULT_ACCOUNT_ID } from "./constants.js";
|
|
3
|
-
|
|
4
|
-
export type DdchatStreamingMode = "chunk" | "token";
|
|
5
|
-
export type DdchatConnectionMode = "websocket" | "webhook";
|
|
6
|
-
export type DdchatPolicy = "open" | "pairing" | "allowlist";
|
|
7
|
-
|
|
8
|
-
export type DdchatResolvedAccount = {
|
|
9
|
-
accountId: string;
|
|
10
|
-
enabled: boolean;
|
|
11
|
-
configured: boolean;
|
|
12
|
-
token?: string;
|
|
13
|
-
wsUrl?: string;
|
|
14
|
-
webhookPath?: string;
|
|
15
|
-
webhookPort?: number;
|
|
16
|
-
connectionMode: DdchatConnectionMode;
|
|
17
|
-
dmPolicy: DdchatPolicy;
|
|
18
|
-
groupPolicy: "open" | "allowlist" | "disabled";
|
|
19
|
-
requireMention: boolean;
|
|
20
|
-
streaming: boolean;
|
|
21
|
-
streamingMode: DdchatStreamingMode;
|
|
22
|
-
allowFrom: string[];
|
|
23
|
-
groupAllowFrom: string[];
|
|
24
|
-
heartbeatSec: number;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
type DdchatAccountConfig = {
|
|
28
|
-
enabled?: boolean;
|
|
29
|
-
token?: string;
|
|
30
|
-
wsUrl?: string;
|
|
31
|
-
webhookPath?: string;
|
|
32
|
-
webhookPort?: number;
|
|
33
|
-
connectionMode?: DdchatConnectionMode;
|
|
34
|
-
dmPolicy?: DdchatPolicy;
|
|
35
|
-
groupPolicy?: "open" | "allowlist" | "disabled";
|
|
36
|
-
requireMention?: boolean;
|
|
37
|
-
streaming?: boolean;
|
|
38
|
-
streamingMode?: DdchatStreamingMode;
|
|
39
|
-
allowFrom?: Array<string | number>;
|
|
40
|
-
groupAllowFrom?: Array<string | number>;
|
|
41
|
-
heartbeatSec?: number;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
type DdchatChannelConfig = DdchatAccountConfig & {
|
|
45
|
-
defaultAccount?: string;
|
|
46
|
-
accounts?: Record<string, DdchatAccountConfig | undefined>;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export function resolveDdchatMediaMaxBytes(cfg: OpenClawConfig): number | undefined {
|
|
50
|
-
const mb = cfg.agents?.defaults?.mediaMaxMb;
|
|
51
|
-
if (!mb || mb <= 0) {
|
|
52
|
-
return undefined;
|
|
53
|
-
}
|
|
54
|
-
return mb * 1024 * 1024;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function toStringList(input: Array<string | number> | undefined): string[] {
|
|
58
|
-
if (!input) {
|
|
59
|
-
return [];
|
|
60
|
-
}
|
|
61
|
-
return input.map((entry) => String(entry).trim()).filter(Boolean);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function normalizeWebhookPath(path: string | undefined): string {
|
|
65
|
-
const trimmed = path?.trim();
|
|
66
|
-
return trimmed?.startsWith("/") ? trimmed : "/ddchat/webhook";
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function normalizeConnectionMode(mode: string | undefined): DdchatConnectionMode {
|
|
70
|
-
return mode === "webhook" ? "webhook" : "websocket";
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function normalizeStreamingMode(mode: string | undefined): DdchatStreamingMode {
|
|
74
|
-
return mode === "token" ? "token" : "chunk";
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function readChannelConfig(cfg: OpenClawConfig): DdchatChannelConfig {
|
|
78
|
-
return ((cfg.channels as Record<string, unknown> | undefined)?.[DDCHAT_CHANNEL_ID] ?? {}) as DdchatChannelConfig;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function mergeAccountConfig(params: {
|
|
82
|
-
root: DdchatChannelConfig;
|
|
83
|
-
account?: DdchatAccountConfig;
|
|
84
|
-
}): DdchatAccountConfig {
|
|
85
|
-
const { root, account } = params;
|
|
86
|
-
return {
|
|
87
|
-
...root,
|
|
88
|
-
...account,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export function listDdchatAccountIds(cfg: OpenClawConfig): string[] {
|
|
93
|
-
const channelCfg = readChannelConfig(cfg);
|
|
94
|
-
const keys = Object.keys(channelCfg.accounts ?? {}).filter((key) => key.trim().length > 0);
|
|
95
|
-
return keys.length > 0 ? keys : [DDCHAT_DEFAULT_ACCOUNT_ID];
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function resolveDefaultDdchatAccountId(cfg: OpenClawConfig): string {
|
|
99
|
-
const channelCfg = readChannelConfig(cfg);
|
|
100
|
-
const ids = listDdchatAccountIds(cfg);
|
|
101
|
-
const configured = channelCfg.defaultAccount?.trim();
|
|
102
|
-
return configured && ids.includes(configured) ? configured : ids[0];
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function resolveDdchatAccount(
|
|
106
|
-
cfg: OpenClawConfig,
|
|
107
|
-
accountId?: string | null,
|
|
108
|
-
): DdchatResolvedAccount {
|
|
109
|
-
const channelCfg = readChannelConfig(cfg);
|
|
110
|
-
const resolvedAccountId = accountId?.trim() || resolveDefaultDdchatAccountId(cfg);
|
|
111
|
-
const named = channelCfg.accounts?.[resolvedAccountId];
|
|
112
|
-
const merged = mergeAccountConfig({ root: channelCfg, account: named });
|
|
113
|
-
const token = typeof merged.token === "string" ? merged.token.trim() : "";
|
|
114
|
-
const wsUrl = typeof merged.wsUrl === "string" ? merged.wsUrl.trim() : "";
|
|
115
|
-
return {
|
|
116
|
-
accountId: resolvedAccountId,
|
|
117
|
-
enabled: merged.enabled !== false,
|
|
118
|
-
configured: Boolean(token),
|
|
119
|
-
token: token || undefined,
|
|
120
|
-
wsUrl: wsUrl || undefined,
|
|
121
|
-
webhookPath: normalizeWebhookPath(merged.webhookPath),
|
|
122
|
-
webhookPort:
|
|
123
|
-
typeof merged.webhookPort === "number" && Number.isFinite(merged.webhookPort)
|
|
124
|
-
? merged.webhookPort
|
|
125
|
-
: 3010,
|
|
126
|
-
connectionMode: normalizeConnectionMode(merged.connectionMode),
|
|
127
|
-
dmPolicy: merged.dmPolicy ?? "pairing",
|
|
128
|
-
groupPolicy: merged.groupPolicy ?? "allowlist",
|
|
129
|
-
requireMention: merged.requireMention === true,
|
|
130
|
-
streaming: merged.streaming !== false,
|
|
131
|
-
streamingMode: normalizeStreamingMode(merged.streamingMode),
|
|
132
|
-
allowFrom: toStringList(merged.allowFrom),
|
|
133
|
-
groupAllowFrom: toStringList(merged.groupAllowFrom),
|
|
134
|
-
heartbeatSec: Math.max(15, Number(merged.heartbeatSec ?? 60) || 60),
|
|
135
|
-
};
|
|
136
|
-
}
|
package/task/BLOCKERS.md
DELETED
package/task/DOING.md
DELETED
package/task/DONE.md
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
# DONE
|
|
2
|
-
|
|
3
|
-
- [x] Created `ddchat/task` workflow files (`README/TODO/DOING/DONE/BLOCKERS`)
|
|
4
|
-
- Date: 2026-04-01
|
|
5
|
-
- Verification: files exist under `ddchat/task/`
|
|
6
|
-
- [x] Initialized DDChat plugin skeleton and websocket-first runtime scaffold
|
|
7
|
-
- Date: 2026-04-01
|
|
8
|
-
- Verification: `ddchat/package.json`, `ddchat/openclaw.plugin.json`, and `ddchat/src/*` core files created
|
package/task/README.md
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# DDChat Task Records
|
|
2
|
-
|
|
3
|
-
This directory tracks development progress for the `ddchat` plugin.
|
|
4
|
-
|
|
5
|
-
## Files
|
|
6
|
-
|
|
7
|
-
- `TODO.md`: all pending tasks
|
|
8
|
-
- `DOING.md`: the single task currently in progress
|
|
9
|
-
- `DONE.md`: completed tasks with verification notes
|
|
10
|
-
- `BLOCKERS.md`: blockers, decisions, and risks
|
|
11
|
-
|
|
12
|
-
## Workflow
|
|
13
|
-
|
|
14
|
-
1. Move one item from `TODO.md` to `DOING.md` before coding.
|
|
15
|
-
2. When finished, move it to `DONE.md` with date and verification notes.
|
|
16
|
-
3. If blocked, add details to `BLOCKERS.md` and stop starting new work.
|
|
17
|
-
4. Keep `DOING.md` to one active task only.
|
package/task/TODO.md
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,304 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
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
|
-
});
|