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/channel.ts
CHANGED
|
@@ -1,101 +1,101 @@
|
|
|
1
|
-
import { createChatChannelPlugin, type OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
2
|
-
import { patchScopedAccountConfig, prepareScopedSetupConfig } from "openclaw/plugin-sdk/setup";
|
|
3
|
-
import { DDCHAT_CHANNEL_ID } from "./constants.js";
|
|
4
|
-
import { ddchatGateway } from "./gateway.js";
|
|
5
|
-
import { ddchatOutbound } from "./outbound.js";
|
|
6
|
-
import { ddchatPairing } from "./pairing.js";
|
|
7
|
-
import { listDdchatAccountIds, resolveDdchatAccount, type DdchatResolvedAccount } from "./types.js";
|
|
8
|
-
|
|
9
|
-
function inspectDdchatAccount(cfg: OpenClawConfig, accountId?: string | null) {
|
|
10
|
-
const account = resolveDdchatAccount(cfg, accountId);
|
|
11
|
-
return {
|
|
12
|
-
enabled: account.enabled,
|
|
13
|
-
configured: account.configured,
|
|
14
|
-
tokenStatus: account.token ? "available" : "missing",
|
|
15
|
-
connectionMode: account.connectionMode,
|
|
16
|
-
dmPolicy: account.dmPolicy,
|
|
17
|
-
groupPolicy: account.groupPolicy,
|
|
18
|
-
streaming: account.streaming,
|
|
19
|
-
streamingMode: account.streamingMode,
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export const ddchatPlugin = createChatChannelPlugin<DdchatResolvedAccount>({
|
|
24
|
-
base: {
|
|
25
|
-
id: DDCHAT_CHANNEL_ID,
|
|
26
|
-
meta: {
|
|
27
|
-
id: DDCHAT_CHANNEL_ID,
|
|
28
|
-
label: "DDChat",
|
|
29
|
-
selectionLabel: "DDChat (IM)",
|
|
30
|
-
blurb: "DDChat internal IM integration.",
|
|
31
|
-
order: 90,
|
|
32
|
-
},
|
|
33
|
-
capabilities: {
|
|
34
|
-
chatTypes: ["direct", "channel"],
|
|
35
|
-
media: true,
|
|
36
|
-
threads: false,
|
|
37
|
-
polls: false,
|
|
38
|
-
},
|
|
39
|
-
config: {
|
|
40
|
-
listAccountIds: (cfg) => listDdchatAccountIds(cfg),
|
|
41
|
-
resolveAccount: (cfg, accountId) => resolveDdchatAccount(cfg, accountId),
|
|
42
|
-
inspectAccount: inspectDdchatAccount,
|
|
43
|
-
isEnabled: (account) => account.enabled,
|
|
44
|
-
isConfigured: (account) => account.configured,
|
|
45
|
-
},
|
|
46
|
-
setup: {
|
|
47
|
-
resolveAccountId: ({ accountId }) => accountId ?? "default",
|
|
48
|
-
applyAccountName: ({ cfg, accountId, name }) =>
|
|
49
|
-
prepareScopedSetupConfig({
|
|
50
|
-
cfg,
|
|
51
|
-
channelKey: DDCHAT_CHANNEL_ID,
|
|
52
|
-
accountId,
|
|
53
|
-
name,
|
|
54
|
-
alwaysUseAccounts: true,
|
|
55
|
-
}),
|
|
56
|
-
validateInput: ({ input }) => {
|
|
57
|
-
const token = typeof input.token === "string" ? input.token.trim() : "";
|
|
58
|
-
return token ? null : "ddchat requires --token";
|
|
59
|
-
},
|
|
60
|
-
applyAccountConfig: ({ cfg, accountId, input }) => {
|
|
61
|
-
const token = typeof input.token === "string" ? input.token.trim() : "";
|
|
62
|
-
const next = prepareScopedSetupConfig({
|
|
63
|
-
cfg,
|
|
64
|
-
channelKey: DDCHAT_CHANNEL_ID,
|
|
65
|
-
accountId,
|
|
66
|
-
name: input.name,
|
|
67
|
-
alwaysUseAccounts: true,
|
|
68
|
-
});
|
|
69
|
-
const patch: Record<string, unknown> = {};
|
|
70
|
-
if (token) {
|
|
71
|
-
patch.token = token;
|
|
72
|
-
}
|
|
73
|
-
return patchScopedAccountConfig({
|
|
74
|
-
cfg: next,
|
|
75
|
-
channelKey: DDCHAT_CHANNEL_ID,
|
|
76
|
-
accountId,
|
|
77
|
-
patch,
|
|
78
|
-
accountPatch: patch,
|
|
79
|
-
ensureChannelEnabled: true,
|
|
80
|
-
ensureAccountEnabled: true,
|
|
81
|
-
scopeDefaultToAccounts: true,
|
|
82
|
-
});
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
gateway: ddchatGateway,
|
|
86
|
-
},
|
|
87
|
-
pairing: ddchatPairing,
|
|
88
|
-
security: {
|
|
89
|
-
dm: {
|
|
90
|
-
channelKey: DDCHAT_CHANNEL_ID,
|
|
91
|
-
resolvePolicy: (account) => account.dmPolicy,
|
|
92
|
-
resolveAllowFrom: (account) => account.allowFrom,
|
|
93
|
-
defaultPolicy: "pairing",
|
|
94
|
-
normalizeEntry: (raw) => raw.replace(/^ddchat:/i, "").trim(),
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
outbound: ddchatOutbound,
|
|
98
|
-
threading: {
|
|
99
|
-
topLevelReplyToMode: "off",
|
|
100
|
-
},
|
|
101
|
-
});
|
|
1
|
+
import { createChatChannelPlugin, type OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import { patchScopedAccountConfig, prepareScopedSetupConfig } from "openclaw/plugin-sdk/setup";
|
|
3
|
+
import { DDCHAT_CHANNEL_ID } from "./constants.js";
|
|
4
|
+
import { ddchatGateway } from "./gateway.js";
|
|
5
|
+
import { ddchatOutbound } from "./outbound.js";
|
|
6
|
+
import { ddchatPairing } from "./pairing.js";
|
|
7
|
+
import { listDdchatAccountIds, resolveDdchatAccount, type DdchatResolvedAccount } from "./types.js";
|
|
8
|
+
|
|
9
|
+
function inspectDdchatAccount(cfg: OpenClawConfig, accountId?: string | null) {
|
|
10
|
+
const account = resolveDdchatAccount(cfg, accountId);
|
|
11
|
+
return {
|
|
12
|
+
enabled: account.enabled,
|
|
13
|
+
configured: account.configured,
|
|
14
|
+
tokenStatus: account.token ? "available" : "missing",
|
|
15
|
+
connectionMode: account.connectionMode,
|
|
16
|
+
dmPolicy: account.dmPolicy,
|
|
17
|
+
groupPolicy: account.groupPolicy,
|
|
18
|
+
streaming: account.streaming,
|
|
19
|
+
streamingMode: account.streamingMode,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const ddchatPlugin = createChatChannelPlugin<DdchatResolvedAccount>({
|
|
24
|
+
base: {
|
|
25
|
+
id: DDCHAT_CHANNEL_ID,
|
|
26
|
+
meta: {
|
|
27
|
+
id: DDCHAT_CHANNEL_ID,
|
|
28
|
+
label: "DDChat",
|
|
29
|
+
selectionLabel: "DDChat (IM)",
|
|
30
|
+
blurb: "DDChat internal IM integration.",
|
|
31
|
+
order: 90,
|
|
32
|
+
},
|
|
33
|
+
capabilities: {
|
|
34
|
+
chatTypes: ["direct", "channel"],
|
|
35
|
+
media: true,
|
|
36
|
+
threads: false,
|
|
37
|
+
polls: false,
|
|
38
|
+
},
|
|
39
|
+
config: {
|
|
40
|
+
listAccountIds: (cfg) => listDdchatAccountIds(cfg),
|
|
41
|
+
resolveAccount: (cfg, accountId) => resolveDdchatAccount(cfg, accountId),
|
|
42
|
+
inspectAccount: inspectDdchatAccount,
|
|
43
|
+
isEnabled: (account) => account.enabled,
|
|
44
|
+
isConfigured: (account) => account.configured,
|
|
45
|
+
},
|
|
46
|
+
setup: {
|
|
47
|
+
resolveAccountId: ({ accountId }) => accountId ?? "default",
|
|
48
|
+
applyAccountName: ({ cfg, accountId, name }) =>
|
|
49
|
+
prepareScopedSetupConfig({
|
|
50
|
+
cfg,
|
|
51
|
+
channelKey: DDCHAT_CHANNEL_ID,
|
|
52
|
+
accountId,
|
|
53
|
+
name,
|
|
54
|
+
alwaysUseAccounts: true,
|
|
55
|
+
}),
|
|
56
|
+
validateInput: ({ input }) => {
|
|
57
|
+
const token = typeof input.token === "string" ? input.token.trim() : "";
|
|
58
|
+
return token ? null : "ddchat requires --token";
|
|
59
|
+
},
|
|
60
|
+
applyAccountConfig: ({ cfg, accountId, input }) => {
|
|
61
|
+
const token = typeof input.token === "string" ? input.token.trim() : "";
|
|
62
|
+
const next = prepareScopedSetupConfig({
|
|
63
|
+
cfg,
|
|
64
|
+
channelKey: DDCHAT_CHANNEL_ID,
|
|
65
|
+
accountId,
|
|
66
|
+
name: input.name,
|
|
67
|
+
alwaysUseAccounts: true,
|
|
68
|
+
});
|
|
69
|
+
const patch: Record<string, unknown> = {};
|
|
70
|
+
if (token) {
|
|
71
|
+
patch.token = token;
|
|
72
|
+
}
|
|
73
|
+
return patchScopedAccountConfig({
|
|
74
|
+
cfg: next,
|
|
75
|
+
channelKey: DDCHAT_CHANNEL_ID,
|
|
76
|
+
accountId,
|
|
77
|
+
patch,
|
|
78
|
+
accountPatch: patch,
|
|
79
|
+
ensureChannelEnabled: true,
|
|
80
|
+
ensureAccountEnabled: true,
|
|
81
|
+
scopeDefaultToAccounts: true,
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
gateway: ddchatGateway,
|
|
86
|
+
},
|
|
87
|
+
pairing: ddchatPairing,
|
|
88
|
+
security: {
|
|
89
|
+
dm: {
|
|
90
|
+
channelKey: DDCHAT_CHANNEL_ID,
|
|
91
|
+
resolvePolicy: (account) => account.dmPolicy,
|
|
92
|
+
resolveAllowFrom: (account) => account.allowFrom,
|
|
93
|
+
defaultPolicy: "pairing",
|
|
94
|
+
normalizeEntry: (raw) => raw.replace(/^ddchat:/i, "").trim(),
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
outbound: ddchatOutbound,
|
|
98
|
+
threading: {
|
|
99
|
+
topLevelReplyToMode: "off",
|
|
100
|
+
},
|
|
101
|
+
});
|
package/src/constants.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export const DDCHAT_CHANNEL_ID = "ddchat";
|
|
2
|
-
export const DDCHAT_DEFAULT_ACCOUNT_ID = "default";
|
|
3
|
-
|
|
4
|
-
/** Default WebSocket endpoint when `channels.ddchat.wsUrl` / per-account `wsUrl` is unset (`token` query appended at connect). */
|
|
5
|
-
export const DDCHAT_PLUGIN_WS_BASE_URL = "wss://chat.ddjf.info/socket/ai/
|
|
1
|
+
export const DDCHAT_CHANNEL_ID = "ddchat";
|
|
2
|
+
export const DDCHAT_DEFAULT_ACCOUNT_ID = "default";
|
|
3
|
+
|
|
4
|
+
/** Default WebSocket endpoint when `channels.ddchat.wsUrl` / per-account `wsUrl` is unset (`token` query appended at connect). */
|
|
5
|
+
export const DDCHAT_PLUGIN_WS_BASE_URL = "wss://chat.ddjf.info/socket/ai/claw";
|
package/src/dedupe.ts
CHANGED
|
@@ -1,31 +1,51 @@
|
|
|
1
|
-
const DEFAULT_TTL_MS = 48 * 60 * 60 * 1000;
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
1
|
+
const DEFAULT_TTL_MS = 48 * 60 * 60 * 1000;
|
|
2
|
+
const DEFAULT_GC_INTERVAL_MS = 60 * 1000;
|
|
3
|
+
const DEFAULT_GC_CHECK_INTERVAL = 1000;
|
|
4
|
+
|
|
5
|
+
export class DdchatDedupeStore {
|
|
6
|
+
private readonly seen = new Map<string, number>();
|
|
7
|
+
private readonly ttlMs: number;
|
|
8
|
+
private lastGcAt = 0;
|
|
9
|
+
private checksSinceGc = 0;
|
|
10
|
+
|
|
11
|
+
constructor(
|
|
12
|
+
ttlMs = DEFAULT_TTL_MS,
|
|
13
|
+
private readonly gcIntervalMs = DEFAULT_GC_INTERVAL_MS,
|
|
14
|
+
private readonly gcCheckInterval = DEFAULT_GC_CHECK_INTERVAL,
|
|
15
|
+
) {
|
|
16
|
+
this.ttlMs = ttlMs;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
isDuplicate(accountId: string, messageId: string): boolean {
|
|
20
|
+
const key = `${accountId}:${messageId}`;
|
|
21
|
+
const now = Date.now();
|
|
22
|
+
this.gcIfNeeded(now);
|
|
23
|
+
const expiresAt = this.seen.get(key);
|
|
24
|
+
if (expiresAt && expiresAt > now) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
this.seen.set(key, now + this.ttlMs);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private gcIfNeeded(now: number): void {
|
|
32
|
+
this.checksSinceGc += 1;
|
|
33
|
+
if (
|
|
34
|
+
now - this.lastGcAt < this.gcIntervalMs &&
|
|
35
|
+
this.checksSinceGc < this.gcCheckInterval
|
|
36
|
+
) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
this.lastGcAt = now;
|
|
40
|
+
this.checksSinceGc = 0;
|
|
41
|
+
this.gc(now);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private gc(now: number): void {
|
|
45
|
+
for (const [key, expiresAt] of this.seen.entries()) {
|
|
46
|
+
if (expiresAt <= now) {
|
|
47
|
+
this.seen.delete(key);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|