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/types.ts
CHANGED
|
@@ -1,126 +1,136 @@
|
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function
|
|
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
|
-
export function
|
|
93
|
-
cfg
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
return
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
+
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
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
# BLOCKERS
|
|
2
|
-
|
|
3
|
-
- None currently.
|
|
1
|
+
# BLOCKERS
|
|
2
|
+
|
|
3
|
+
- None currently.
|
package/task/DOING.md
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
# DOING
|
|
2
|
-
|
|
3
|
-
- [ ] Implement websocket-first inbound and chunk streaming for multi-account `ddchat`
|
|
1
|
+
# DOING
|
|
2
|
+
|
|
3
|
+
- [ ] Implement websocket-first inbound and chunk streaming for multi-account `ddchat`
|
package/task/DONE.md
CHANGED
|
@@ -1,8 +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
|
|
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
CHANGED
|
@@ -1,17 +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.
|
|
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
CHANGED
|
@@ -1,10 +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
|
|
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
CHANGED
|
@@ -1,48 +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.
|
|
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.
|