ddchat 0.4.1 → 0.4.2
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 -0
- package/OPTIMIZATION.md +105 -0
- package/README.md +22 -0
- package/{index.js → index.ts} +7 -5
- package/openclaw.plugin.json +3 -137
- package/package.json +6 -6
- package/setup-entry.ts +4 -0
- package/src/channel.ts +101 -0
- package/src/{constants.js → constants.ts} +2 -1
- package/src/dedupe.ts +31 -0
- package/src/gateway.ts +237 -0
- package/src/inbound.ts +394 -0
- package/src/outbound.ts +183 -0
- package/src/pairing.ts +9 -0
- package/src/runtime.ts +27 -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/setup-entry.js +0 -8
- package/src/channel.js +0 -99
- package/src/dedupe.js +0 -44
- package/src/gateway.js +0 -211
- package/src/inbound.js +0 -363
- package/src/outbound.js +0 -150
- package/src/pairing.js +0 -8
- package/src/runtime.js +0 -20
- package/src/session.js +0 -13
- package/src/types.js +0 -73
package/src/outbound.ts
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { loadWebMedia } from "openclaw/plugin-sdk/web-media";
|
|
2
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
3
|
+
import { DDCHAT_CHANNEL_ID } from "./constants.js";
|
|
4
|
+
import { getDdchatState } from "./runtime.js";
|
|
5
|
+
import { pathToFileURL } from "url";
|
|
6
|
+
|
|
7
|
+
function resolveMediaMaxBytes(cfg: OpenClawConfig): number | undefined {
|
|
8
|
+
const mb = cfg.agents?.defaults?.mediaMaxMb;
|
|
9
|
+
if (!mb || mb <= 0) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
return mb * 1024 * 1024;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Check if URL is a local file path */
|
|
16
|
+
function isLocalFilePath(url: string): boolean {
|
|
17
|
+
if (url.startsWith("file://")) return true;
|
|
18
|
+
if (/^[a-zA-Z]:[\\/]/.test(url)) return true; // Windows path
|
|
19
|
+
if (url.startsWith("/") && !url.startsWith("//")) return true; // Unix absolute path
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Convert local path to file:// URL */
|
|
24
|
+
function toFileUrl(path: string): string {
|
|
25
|
+
if (path.startsWith("file://")) return path;
|
|
26
|
+
return pathToFileURL(path).href;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Load media for WS outbound; same limits as sendMedia. URL-only fallback on failure. */
|
|
30
|
+
export async function resolveDdchatOutboundMediaFields(
|
|
31
|
+
cfg: OpenClawConfig,
|
|
32
|
+
mediaUrl: string,
|
|
33
|
+
): Promise<{ mediaBase64?: string; mediaType?: string; mediaName?: string; error?: string }> {
|
|
34
|
+
try {
|
|
35
|
+
// Handle local file paths
|
|
36
|
+
let resolvedUrl = mediaUrl;
|
|
37
|
+
if (isLocalFilePath(mediaUrl)) {
|
|
38
|
+
resolvedUrl = toFileUrl(mediaUrl);
|
|
39
|
+
}
|
|
40
|
+
console.log(`[ddchat] Resolved media URL: ${resolvedUrl}`);
|
|
41
|
+
|
|
42
|
+
const media = await loadWebMedia(resolvedUrl, {
|
|
43
|
+
maxBytes: resolveMediaMaxBytes(cfg),
|
|
44
|
+
});
|
|
45
|
+
console.log(`[ddchat] Media loaded successfully: ${media.fileName}, type: ${media.contentType}, size: ${media.buffer.length}`);
|
|
46
|
+
return {
|
|
47
|
+
mediaBase64: media.buffer.toString("base64"),
|
|
48
|
+
mediaType: media.contentType,
|
|
49
|
+
mediaName: media.fileName,
|
|
50
|
+
};
|
|
51
|
+
} catch (error) {
|
|
52
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
53
|
+
console.error(`[ddchat] Failed to load media from ${mediaUrl}: ${errorMsg}`);
|
|
54
|
+
return { error: errorMsg };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolveTarget(to: string): { targetType: "group" | "direct"; targetId: string } {
|
|
59
|
+
const normalized = to.trim();
|
|
60
|
+
if (normalized.startsWith("group:")) {
|
|
61
|
+
return { targetType: "group", targetId: normalized.slice("group:".length) };
|
|
62
|
+
}
|
|
63
|
+
if (normalized.startsWith("chat:")) {
|
|
64
|
+
return { targetType: "group", targetId: normalized.slice("chat:".length) };
|
|
65
|
+
}
|
|
66
|
+
if (normalized.startsWith("user:")) {
|
|
67
|
+
return { targetType: "direct", targetId: normalized.slice("user:".length) };
|
|
68
|
+
}
|
|
69
|
+
return { targetType: "direct", targetId: normalized };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const ddchatOutbound = {
|
|
73
|
+
deliveryMode: "direct",
|
|
74
|
+
textChunkLimit: 4000,
|
|
75
|
+
chunkerMode: "markdown",
|
|
76
|
+
sendText: async ({ to, text, accountId }) => {
|
|
77
|
+
const messageId = `ddchat-text-${Date.now()}`;
|
|
78
|
+
const target = resolveTarget(to);
|
|
79
|
+
|
|
80
|
+
// Check WebSocket connection
|
|
81
|
+
const state = getDdchatState();
|
|
82
|
+
console.log(`[ddchat] sendText called - wsConnected: ${state.wsConnected}, wsSend available: ${!!state.wsSend}`);
|
|
83
|
+
|
|
84
|
+
if (!state.wsSend || !state.wsConnected) {
|
|
85
|
+
console.error(`[ddchat] WebSocket not connected, cannot send text message`);
|
|
86
|
+
throw new Error("DDChat WebSocket not connected");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const payload = {
|
|
90
|
+
type: "outbound_message",
|
|
91
|
+
from: "plugin",
|
|
92
|
+
channel: DDCHAT_CHANNEL_ID,
|
|
93
|
+
accountId,
|
|
94
|
+
messageId,
|
|
95
|
+
targetType: target.targetType,
|
|
96
|
+
targetId: target.targetId,
|
|
97
|
+
text,
|
|
98
|
+
};
|
|
99
|
+
console.log(`[ddchat] Sending text payload:`, JSON.stringify(payload, null, 2));
|
|
100
|
+
const sent = state.wsSend(payload);
|
|
101
|
+
if (!sent) {
|
|
102
|
+
throw new Error("Failed to send text message via WebSocket");
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
messageId,
|
|
106
|
+
to,
|
|
107
|
+
channel: DDCHAT_CHANNEL_ID,
|
|
108
|
+
text,
|
|
109
|
+
transport: "ws",
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
sendMedia: async ({ cfg, to, text, mediaUrl, accountId }) => {
|
|
113
|
+
const messageId = `ddchat-media-${Date.now()}`;
|
|
114
|
+
const target = resolveTarget(to);
|
|
115
|
+
|
|
116
|
+
// Check WebSocket connection
|
|
117
|
+
const state = getDdchatState();
|
|
118
|
+
console.log(`[ddchat] sendMedia called - wsConnected: ${state.wsConnected}, wsSend available: ${!!state.wsSend}`);
|
|
119
|
+
|
|
120
|
+
if (!state.wsSend || !state.wsConnected) {
|
|
121
|
+
console.error(`[ddchat] WebSocket not connected, cannot send media`);
|
|
122
|
+
throw new Error("DDChat WebSocket not connected");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Load media
|
|
126
|
+
console.log(`[ddchat] Loading media from: ${mediaUrl}`);
|
|
127
|
+
const { mediaBase64, mediaType, mediaName, error } = await resolveDdchatOutboundMediaFields(
|
|
128
|
+
cfg,
|
|
129
|
+
mediaUrl,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
if (error) {
|
|
133
|
+
console.error(`[ddchat] Media loading failed: ${error}`);
|
|
134
|
+
throw new Error(`Failed to load media: ${error}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Validate media data
|
|
138
|
+
if (!mediaBase64) {
|
|
139
|
+
console.error(`[ddchat] No media data available for ${mediaUrl}`);
|
|
140
|
+
throw new Error("No media data available");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const payload = {
|
|
144
|
+
type: "outbound_message",
|
|
145
|
+
from: "plugin",
|
|
146
|
+
channel: DDCHAT_CHANNEL_ID,
|
|
147
|
+
accountId,
|
|
148
|
+
messageId,
|
|
149
|
+
targetType: target.targetType,
|
|
150
|
+
targetId: target.targetId,
|
|
151
|
+
text,
|
|
152
|
+
mediaUrl,
|
|
153
|
+
mediaBase64,
|
|
154
|
+
mediaType,
|
|
155
|
+
mediaName,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
console.log(`[ddchat] Sending media message:`, {
|
|
159
|
+
messageId,
|
|
160
|
+
targetType: target.targetType,
|
|
161
|
+
targetId: target.targetId,
|
|
162
|
+
mediaType,
|
|
163
|
+
mediaName,
|
|
164
|
+
mediaSize: mediaBase64?.length,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const sent = state.wsSend(payload);
|
|
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
|
+
};
|
package/src/pairing.ts
ADDED
|
@@ -0,0 +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
|
+
};
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { DdchatDedupeStore } from "./dedupe.js";
|
|
2
|
+
|
|
3
|
+
export type DdchatPluginRuntimeState = {
|
|
4
|
+
dedupe: DdchatDedupeStore;
|
|
5
|
+
wsSend?: (payload: Record<string, unknown>) => boolean;
|
|
6
|
+
wsConnected: boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const state: DdchatPluginRuntimeState = {
|
|
10
|
+
dedupe: new DdchatDedupeStore(),
|
|
11
|
+
wsConnected: false,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function getDdchatState(): DdchatPluginRuntimeState {
|
|
15
|
+
// console.log(`[ddchat] getDdchatState called - wsConnected: ${state.wsConnected}, wsSend available: ${!!state.wsSend}`);
|
|
16
|
+
return state;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function setDdchatWsRuntime(params: {
|
|
20
|
+
send?: (payload: Record<string, unknown>) => boolean;
|
|
21
|
+
connected: boolean;
|
|
22
|
+
}): void {
|
|
23
|
+
// console.log(`[ddchat] setDdchatWsRuntime called - connected: ${params.connected}, send available: ${!!params.send}`);
|
|
24
|
+
state.wsSend = params.send;
|
|
25
|
+
state.wsConnected = params.connected;
|
|
26
|
+
// console.log(`[ddchat] State updated - wsConnected: ${state.wsConnected}, wsSend available: ${!!state.wsSend}`);
|
|
27
|
+
}
|
package/src/session.ts
ADDED
|
@@ -0,0 +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
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
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
|
+
function toStringList(input: Array<string | number> | undefined): string[] {
|
|
50
|
+
if (!input) {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
return input.map((entry) => String(entry).trim()).filter(Boolean);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeConnectionMode(mode: string | undefined): DdchatConnectionMode {
|
|
57
|
+
return mode === "webhook" ? "webhook" : "websocket";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeStreamingMode(mode: string | undefined): DdchatStreamingMode {
|
|
61
|
+
return mode === "token" ? "token" : "chunk";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readChannelConfig(cfg: OpenClawConfig): DdchatChannelConfig {
|
|
65
|
+
return ((cfg.channels as Record<string, unknown> | undefined)?.[DDCHAT_CHANNEL_ID] ?? {}) as DdchatChannelConfig;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function mergeAccountConfig(params: {
|
|
69
|
+
root: DdchatChannelConfig;
|
|
70
|
+
account?: DdchatAccountConfig;
|
|
71
|
+
}): DdchatAccountConfig {
|
|
72
|
+
const { root, account } = params;
|
|
73
|
+
return {
|
|
74
|
+
...root,
|
|
75
|
+
...account,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function listDdchatAccountIds(cfg: OpenClawConfig): string[] {
|
|
80
|
+
const channelCfg = readChannelConfig(cfg);
|
|
81
|
+
const keys = Object.keys(channelCfg.accounts ?? {}).filter((key) => key.trim().length > 0);
|
|
82
|
+
return keys.length > 0 ? keys : [DDCHAT_DEFAULT_ACCOUNT_ID];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function resolveDefaultDdchatAccountId(cfg: OpenClawConfig): string {
|
|
86
|
+
const channelCfg = readChannelConfig(cfg);
|
|
87
|
+
const ids = listDdchatAccountIds(cfg);
|
|
88
|
+
const configured = channelCfg.defaultAccount?.trim();
|
|
89
|
+
return configured && ids.includes(configured) ? configured : ids[0];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function resolveDdchatAccount(
|
|
93
|
+
cfg: OpenClawConfig,
|
|
94
|
+
accountId?: string | null,
|
|
95
|
+
): DdchatResolvedAccount {
|
|
96
|
+
const channelCfg = readChannelConfig(cfg);
|
|
97
|
+
const resolvedAccountId = accountId?.trim() || resolveDefaultDdchatAccountId(cfg);
|
|
98
|
+
const named = channelCfg.accounts?.[resolvedAccountId];
|
|
99
|
+
const merged = mergeAccountConfig({ root: channelCfg, account: named });
|
|
100
|
+
const token = typeof merged.token === "string" ? merged.token.trim() : "";
|
|
101
|
+
const wsUrl = typeof merged.wsUrl === "string" ? merged.wsUrl.trim() : "";
|
|
102
|
+
return {
|
|
103
|
+
accountId: resolvedAccountId,
|
|
104
|
+
enabled: merged.enabled !== false,
|
|
105
|
+
configured: Boolean(token),
|
|
106
|
+
token: token || undefined,
|
|
107
|
+
wsUrl: wsUrl || undefined,
|
|
108
|
+
webhookPath:
|
|
109
|
+
typeof merged.webhookPath === "string" && merged.webhookPath.trim()
|
|
110
|
+
? merged.webhookPath.trim()
|
|
111
|
+
: "/ddchat/webhook",
|
|
112
|
+
webhookPort:
|
|
113
|
+
typeof merged.webhookPort === "number" && Number.isFinite(merged.webhookPort)
|
|
114
|
+
? merged.webhookPort
|
|
115
|
+
: 3010,
|
|
116
|
+
connectionMode: normalizeConnectionMode(merged.connectionMode),
|
|
117
|
+
dmPolicy: merged.dmPolicy ?? "pairing",
|
|
118
|
+
groupPolicy: merged.groupPolicy ?? "allowlist",
|
|
119
|
+
requireMention: merged.requireMention === true,
|
|
120
|
+
streaming: merged.streaming !== false,
|
|
121
|
+
streamingMode: normalizeStreamingMode(merged.streamingMode),
|
|
122
|
+
allowFrom: toStringList(merged.allowFrom),
|
|
123
|
+
groupAllowFrom: toStringList(merged.groupAllowFrom),
|
|
124
|
+
heartbeatSec: Math.max(15, Number(merged.heartbeatSec ?? 60) || 60),
|
|
125
|
+
};
|
|
126
|
+
}
|
package/task/BLOCKERS.md
ADDED
package/task/DOING.md
ADDED
package/task/DONE.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
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
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.
|