ddchat 0.2.0 → 0.3.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/CLAUDE.md +51 -51
- package/OPTIMIZATION.md +129 -105
- package/README.md +22 -14
- package/index.ts +13 -13
- package/openclaw.plugin.json +15 -15
- package/package.json +36 -36
- package/setup-entry.ts +4 -4
- package/src/channel.ts +101 -101
- package/src/constants.ts +5 -5
- package/src/dedupe.ts +51 -31
- package/src/gateway.ts +255 -237
- package/src/inbound.ts +451 -394
- package/src/outbound.ts +167 -183
- package/src/pairing.ts +9 -9
- package/src/runtime.ts +41 -27
- package/src/session.ts +19 -19
- package/src/types.ts +136 -126
- package/task/BLOCKERS.md +3 -3
- package/task/DOING.md +3 -3
- package/task/DONE.md +8 -8
- package/task/README.md +17 -17
- package/task/TODO.md +10 -10
- package/test/README.md +48 -48
- package/test/chat.html +304 -304
- package/test/server.mjs +143 -143
package/src/outbound.ts
CHANGED
|
@@ -1,183 +1,167 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (normalized.startsWith("
|
|
61
|
-
return { targetType: "
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
mediaType,
|
|
163
|
-
mediaName,
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (!sent) {
|
|
169
|
-
throw new Error("Failed to send media message via WebSocket");
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return {
|
|
173
|
-
messageId,
|
|
174
|
-
to,
|
|
175
|
-
channel: DDCHAT_CHANNEL_ID,
|
|
176
|
-
text,
|
|
177
|
-
mediaUrl,
|
|
178
|
-
mediaType,
|
|
179
|
-
mediaName,
|
|
180
|
-
transport: "ws",
|
|
181
|
-
};
|
|
182
|
-
},
|
|
183
|
-
};
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import { loadWebMedia } from "openclaw/plugin-sdk/web-media";
|
|
4
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
5
|
+
import { DDCHAT_CHANNEL_ID } from "./constants.js";
|
|
6
|
+
import { getDdchatWsRuntime } from "./runtime.js";
|
|
7
|
+
import { resolveDdchatMediaMaxBytes } from "./types.js";
|
|
8
|
+
|
|
9
|
+
function createDdchatMessageId(prefix: string): string {
|
|
10
|
+
return `${prefix}-${randomUUID()}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isLocalFilePath(url: string): boolean {
|
|
14
|
+
if (url.startsWith("file://")) return true;
|
|
15
|
+
if (/^[a-zA-Z]:[\\/]/.test(url)) return true;
|
|
16
|
+
if (url.startsWith("/") && !url.startsWith("//")) return true;
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function toFileUrl(path: string): string {
|
|
21
|
+
if (path.startsWith("file://")) return path;
|
|
22
|
+
return pathToFileURL(path).href;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function resolveDdchatOutboundMediaFields(
|
|
26
|
+
cfg: OpenClawConfig,
|
|
27
|
+
mediaUrl: string,
|
|
28
|
+
): Promise<{ mediaBase64?: string; mediaType?: string; mediaName?: string; error?: string }> {
|
|
29
|
+
try {
|
|
30
|
+
let resolvedUrl = mediaUrl;
|
|
31
|
+
if (isLocalFilePath(mediaUrl)) {
|
|
32
|
+
resolvedUrl = toFileUrl(mediaUrl);
|
|
33
|
+
}
|
|
34
|
+
console.log(`[ddchat] Resolved media URL: ${resolvedUrl}`);
|
|
35
|
+
|
|
36
|
+
const media = await loadWebMedia(resolvedUrl, {
|
|
37
|
+
maxBytes: resolveDdchatMediaMaxBytes(cfg),
|
|
38
|
+
});
|
|
39
|
+
console.log(`[ddchat] Media loaded successfully: ${media.fileName}, type: ${media.contentType}, size: ${media.buffer.length}`);
|
|
40
|
+
return {
|
|
41
|
+
mediaBase64: media.buffer.toString("base64"),
|
|
42
|
+
mediaType: media.contentType,
|
|
43
|
+
mediaName: media.fileName,
|
|
44
|
+
};
|
|
45
|
+
} catch (error) {
|
|
46
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
47
|
+
console.error(`[ddchat] Failed to load media from ${mediaUrl}: ${errorMsg}`);
|
|
48
|
+
return { error: errorMsg };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resolveTarget(to: string): { targetType: "group" | "direct"; targetId: string } {
|
|
53
|
+
const normalized = to.trim();
|
|
54
|
+
if (normalized.startsWith("group:")) {
|
|
55
|
+
return { targetType: "group", targetId: normalized.slice("group:".length) };
|
|
56
|
+
}
|
|
57
|
+
if (normalized.startsWith("chat:")) {
|
|
58
|
+
return { targetType: "group", targetId: normalized.slice("chat:".length) };
|
|
59
|
+
}
|
|
60
|
+
if (normalized.startsWith("user:")) {
|
|
61
|
+
return { targetType: "direct", targetId: normalized.slice("user:".length) };
|
|
62
|
+
}
|
|
63
|
+
return { targetType: "direct", targetId: normalized };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const ddchatOutbound = {
|
|
67
|
+
deliveryMode: "direct",
|
|
68
|
+
textChunkLimit: 4000,
|
|
69
|
+
chunkerMode: "markdown",
|
|
70
|
+
sendText: async ({ to, text, accountId }) => {
|
|
71
|
+
const messageId = createDdchatMessageId("ddchat-text");
|
|
72
|
+
const target = resolveTarget(to);
|
|
73
|
+
|
|
74
|
+
const runtime = getDdchatWsRuntime(accountId);
|
|
75
|
+
if (!runtime.send || !runtime.connected) {
|
|
76
|
+
throw new Error("DDChat WebSocket not connected");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const payload = {
|
|
80
|
+
type: "outbound_message",
|
|
81
|
+
from: "claw",
|
|
82
|
+
channel: DDCHAT_CHANNEL_ID,
|
|
83
|
+
accountId,
|
|
84
|
+
messageId,
|
|
85
|
+
targetType: target.targetType,
|
|
86
|
+
targetId: target.targetId,
|
|
87
|
+
text,
|
|
88
|
+
};
|
|
89
|
+
console.log(`[ddchat] Sending text payload:`, JSON.stringify(payload, null, 2));
|
|
90
|
+
const sent = runtime.send(payload);
|
|
91
|
+
if (!sent) {
|
|
92
|
+
throw new Error("Failed to send text message via WebSocket");
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
messageId,
|
|
96
|
+
to,
|
|
97
|
+
channel: DDCHAT_CHANNEL_ID,
|
|
98
|
+
text,
|
|
99
|
+
transport: "ws",
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
sendMedia: async ({ cfg, to, text, mediaUrl, accountId }) => {
|
|
103
|
+
const messageId = createDdchatMessageId("ddchat-media");
|
|
104
|
+
const target = resolveTarget(to);
|
|
105
|
+
|
|
106
|
+
const runtime = getDdchatWsRuntime(accountId);
|
|
107
|
+
console.log(`[ddchat] sendMedia called - wsConnected: ${runtime.connected}, wsSend available: ${!!runtime.send}`);
|
|
108
|
+
if (!runtime.send || !runtime.connected) {
|
|
109
|
+
throw new Error("DDChat WebSocket not connected");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.log(`[ddchat] Loading media from: ${mediaUrl}`);
|
|
113
|
+
const { mediaBase64, mediaType, mediaName, error } = await resolveDdchatOutboundMediaFields(
|
|
114
|
+
cfg,
|
|
115
|
+
mediaUrl,
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
if (error) {
|
|
119
|
+
console.error(`[ddchat] Media loading failed: ${error}`);
|
|
120
|
+
throw new Error(`Failed to load media: ${error}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!mediaBase64) {
|
|
124
|
+
console.error(`[ddchat] No media data available for ${mediaUrl}`);
|
|
125
|
+
throw new Error("No media data available");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const payload = {
|
|
129
|
+
type: "outbound_message",
|
|
130
|
+
from: "claw",
|
|
131
|
+
channel: DDCHAT_CHANNEL_ID,
|
|
132
|
+
accountId,
|
|
133
|
+
messageId,
|
|
134
|
+
targetType: target.targetType,
|
|
135
|
+
targetId: target.targetId,
|
|
136
|
+
text,
|
|
137
|
+
mediaUrl,
|
|
138
|
+
mediaBase64,
|
|
139
|
+
mediaType,
|
|
140
|
+
mediaName,
|
|
141
|
+
};
|
|
142
|
+
console.log(`[ddchat] Sending media message:`, {
|
|
143
|
+
messageId,
|
|
144
|
+
targetType: target.targetType,
|
|
145
|
+
targetId: target.targetId,
|
|
146
|
+
mediaType,
|
|
147
|
+
mediaName,
|
|
148
|
+
mediaSize: mediaBase64?.length,
|
|
149
|
+
});
|
|
150
|
+
const sent = runtime.send(payload);
|
|
151
|
+
if (!sent) {
|
|
152
|
+
console.error(`[ddchat] Failed to send media message via WebSocket`);
|
|
153
|
+
throw new Error("Failed to send media message via WebSocket");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
messageId,
|
|
158
|
+
to,
|
|
159
|
+
channel: DDCHAT_CHANNEL_ID,
|
|
160
|
+
text,
|
|
161
|
+
mediaUrl,
|
|
162
|
+
mediaType,
|
|
163
|
+
mediaName,
|
|
164
|
+
transport: "ws",
|
|
165
|
+
};
|
|
166
|
+
},
|
|
167
|
+
};
|
package/src/pairing.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { createPairingPrefixStripper } from "openclaw/plugin-sdk/channel-pairing";
|
|
2
|
-
|
|
3
|
-
export const ddchatPairing = {
|
|
4
|
-
idLabel: "ddchatUserId",
|
|
5
|
-
normalizeAllowEntry: createPairingPrefixStripper(/^(ddchat|user):/i),
|
|
6
|
-
notifyApproval: async ({ runtime, id }) => {
|
|
7
|
-
runtime?.log?.(`[ddchat] pairing approved for ${id}`);
|
|
8
|
-
},
|
|
9
|
-
};
|
|
1
|
+
import { createPairingPrefixStripper } from "openclaw/plugin-sdk/channel-pairing";
|
|
2
|
+
|
|
3
|
+
export const ddchatPairing = {
|
|
4
|
+
idLabel: "ddchatUserId",
|
|
5
|
+
normalizeAllowEntry: createPairingPrefixStripper(/^(ddchat|user):/i),
|
|
6
|
+
notifyApproval: async ({ runtime, id }) => {
|
|
7
|
+
runtime?.log?.(`[ddchat] pairing approved for ${id}`);
|
|
8
|
+
},
|
|
9
|
+
};
|
package/src/runtime.ts
CHANGED
|
@@ -1,27 +1,41 @@
|
|
|
1
|
-
import { DdchatDedupeStore } from "./dedupe.js";
|
|
2
|
-
|
|
3
|
-
export type
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
state.
|
|
26
|
-
|
|
27
|
-
|
|
1
|
+
import { DdchatDedupeStore } from "./dedupe.js";
|
|
2
|
+
|
|
3
|
+
export type DdchatWsSend = (payload: Record<string, unknown>) => boolean;
|
|
4
|
+
|
|
5
|
+
export type DdchatWsRuntime = {
|
|
6
|
+
send?: DdchatWsSend;
|
|
7
|
+
connected: boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type DdchatPluginRuntimeState = {
|
|
11
|
+
dedupe: DdchatDedupeStore;
|
|
12
|
+
wsByAccount: Map<string, DdchatWsRuntime>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const state: DdchatPluginRuntimeState = {
|
|
16
|
+
dedupe: new DdchatDedupeStore(),
|
|
17
|
+
wsByAccount: new Map(),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function getDdchatState(): DdchatPluginRuntimeState {
|
|
21
|
+
return state;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getDdchatWsRuntime(accountId: string): DdchatWsRuntime {
|
|
25
|
+
return state.wsByAccount.get(accountId) ?? { connected: false };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function setDdchatWsRuntime(params: {
|
|
29
|
+
accountId: string;
|
|
30
|
+
send?: DdchatWsSend;
|
|
31
|
+
connected: boolean;
|
|
32
|
+
}): void {
|
|
33
|
+
state.wsByAccount.set(params.accountId, {
|
|
34
|
+
send: params.send,
|
|
35
|
+
connected: params.connected,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function clearDdchatWsRuntime(accountId: string): void {
|
|
40
|
+
state.wsByAccount.delete(accountId);
|
|
41
|
+
}
|
package/src/session.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
export function buildDdchatSessionKey(params: {
|
|
2
|
-
peerKind: "direct" | "group";
|
|
3
|
-
peerId: string;
|
|
4
|
-
senderId?: string;
|
|
5
|
-
threadId?: string | null;
|
|
6
|
-
}): string {
|
|
7
|
-
const base =
|
|
8
|
-
params.peerKind === "group"
|
|
9
|
-
? `ddchat:group:${params.peerId}`
|
|
10
|
-
: `ddchat:direct:${params.peerId}`;
|
|
11
|
-
const thread = params.threadId?.trim();
|
|
12
|
-
if (!thread) {
|
|
13
|
-
return base;
|
|
14
|
-
}
|
|
15
|
-
if (params.peerKind === "group" && params.senderId) {
|
|
16
|
-
return `${base}:thread:${thread}:sender:${params.senderId}`;
|
|
17
|
-
}
|
|
18
|
-
return `${base}:thread:${thread}`;
|
|
19
|
-
}
|
|
1
|
+
export function buildDdchatSessionKey(params: {
|
|
2
|
+
peerKind: "direct" | "group";
|
|
3
|
+
peerId: string;
|
|
4
|
+
senderId?: string;
|
|
5
|
+
threadId?: string | null;
|
|
6
|
+
}): string {
|
|
7
|
+
const base =
|
|
8
|
+
params.peerKind === "group"
|
|
9
|
+
? `ddchat:group:${params.peerId}`
|
|
10
|
+
: `ddchat:direct:${params.peerId}`;
|
|
11
|
+
const thread = params.threadId?.trim();
|
|
12
|
+
if (!thread) {
|
|
13
|
+
return base;
|
|
14
|
+
}
|
|
15
|
+
if (params.peerKind === "group" && params.senderId) {
|
|
16
|
+
return `${base}:thread:${thread}:sender:${params.senderId}`;
|
|
17
|
+
}
|
|
18
|
+
return `${base}:thread:${thread}`;
|
|
19
|
+
}
|