ddchat 0.1.0 → 0.2.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 -0
- package/OPTIMIZATION.md +105 -0
- package/README.md +14 -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 +31 -31
- package/src/gateway.ts +237 -237
- package/src/inbound.ts +394 -379
- package/src/outbound.ts +183 -95
- package/src/pairing.ts +9 -9
- package/src/runtime.ts +27 -24
- package/src/session.ts +19 -19
- package/src/types.ts +126 -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/CLAUDE.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
本文件为 Claude Code (claude.ai/code) 在本仓库中工作时提供指导。
|
|
4
|
+
|
|
5
|
+
## 项目概述
|
|
6
|
+
|
|
7
|
+
DDChat 频道插件 (`@ddchat/openclaw-ddchat`) — OpenClaw AI 智能体平台的 DDChat(内部即时通讯)频道集成插件。通过 WebSocket 或 Webhook 连接,将 DDChat 的私聊和群聊消息桥接到 OpenClaw 智能体。
|
|
8
|
+
|
|
9
|
+
## 技术栈
|
|
10
|
+
|
|
11
|
+
- TypeScript,ES 模块 (`"type": "module"`)
|
|
12
|
+
- OpenClaw monorepo 的一部分(使用 `workspace:*` 依赖)
|
|
13
|
+
- OpenClaw 插件 SDK:`openclaw/plugin-sdk/core`、`openclaw/plugin-sdk/setup`、`openclaw/plugin-sdk/channel-pairing`、`openclaw/plugin-sdk/media-runtime`
|
|
14
|
+
|
|
15
|
+
## 开发与测试
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# 启动模拟 DDChat 服务器(WebSocket 端口 :9001,HTTP 界面端口 :9020)
|
|
19
|
+
node ddchat/test/server.mjs
|
|
20
|
+
|
|
21
|
+
# 将插件安装到 OpenClaw
|
|
22
|
+
openclaw channels add --channel ddchat --token "appId:appSecret"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
无独立的构建或测试脚本 — 构建由 OpenClaw 工作区统一处理。
|
|
26
|
+
|
|
27
|
+
## 架构
|
|
28
|
+
|
|
29
|
+
插件遵循 OpenClaw 的 `ChatChannelPlugin` 模式,包含以下核心模块:
|
|
30
|
+
|
|
31
|
+
- **`index.ts`** — 插件入口,导出 extensions 和 setup entry
|
|
32
|
+
- **`src/channel.ts`** — 插件定义:能力声明、配置 schema、安装流程、网关、安全策略
|
|
33
|
+
- **`src/gateway.ts`** — WebSocket 生命周期:连接、认证(token 作为查询参数)、心跳 ping/pong、指数退避重连(1s→2s→5s→10s→30s)
|
|
34
|
+
- **`src/inbound.ts`** — 接收消息(WebSocket 或 webhook),去重,解析媒体/文件,路由到 AI 智能体,处理流式响应
|
|
35
|
+
- **`src/outbound.ts`** — 将智能体回复发送回 DDChat;按 4000 字符使用 markdown 感知方式分块;处理媒体附件(base64 或 URL)
|
|
36
|
+
- **`src/types.ts`** — `DdchatResolvedAccount` 配置类型、账户解析/合并逻辑、策略类型
|
|
37
|
+
- **`src/runtime.ts`** — 全局插件状态:去重存储、WebSocket 发送函数
|
|
38
|
+
- **`src/dedupe.ts`** — 基于 TTL 的消息去重(默认 48 小时),带 GC 回收
|
|
39
|
+
- **`src/pairing.ts`** — 用户配对审批流程,ID 标准化(去除前缀)
|
|
40
|
+
- **`src/session.ts`** — 会话 key 构造,用于私聊/群聊路由
|
|
41
|
+
|
|
42
|
+
**数据流:** DDChat → WebSocket/Webhook → inbound.ts(去重、解析、媒体处理)→ OpenClaw 智能体 → outbound.ts(分块、发送)→ DDChat
|
|
43
|
+
|
|
44
|
+
## 关键约定
|
|
45
|
+
|
|
46
|
+
- 出站消息包含 `from: "plugin"` 字段
|
|
47
|
+
- 标识符:From 使用 `ddchat:${userId}`,To 使用 `user:${userId}` / `group:${groupId}`
|
|
48
|
+
- 流式 ID:`ddchat-stream-${messageId}`
|
|
49
|
+
- 连接模式:`websocket`(默认)| `webhook`
|
|
50
|
+
- 访问策略:私聊使用 `open | pairing | allowlist`(默认:pairing);群聊使用 `open | allowlist | disabled`(默认:allowlist)
|
|
51
|
+
- 流式模式:`chunk` | `token`
|
package/OPTIMIZATION.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# DDChat 插件代码优化建议
|
|
2
|
+
|
|
3
|
+
## 1. 去重 GC 性能问题
|
|
4
|
+
|
|
5
|
+
**文件:** `src/dedupe.ts:23-29`
|
|
6
|
+
|
|
7
|
+
`gc()` 在每次 `isDuplicate()` 调用时都全量遍历整个 Map。高频消息场景下性能会线性退化。
|
|
8
|
+
|
|
9
|
+
**建议:** 改为按频率限流 GC(如每 N 次调用或每 M 秒执行一次),或改用双桶/时间轮策略。
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 2. 全局单例状态不支持多账户并发(功能缺陷)
|
|
14
|
+
|
|
15
|
+
**文件:** `src/runtime.ts`
|
|
16
|
+
|
|
17
|
+
`wsSend` 和 `wsConnected` 是全局唯一的,但插件支持多账户。如果两个账户同时启动 WebSocket,后启动的会覆盖前一个的 `wsSend`,导致第一个账户的出站消息实际发到第二个连接上。
|
|
18
|
+
|
|
19
|
+
**建议:** 将 `wsSend` 改为 `Map<accountId, sendFn>` 结构,出站时按 `accountId` 查找对应的发送函数。
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 3. messageId 使用 Date.now() 存在碰撞风险
|
|
24
|
+
|
|
25
|
+
**文件:** `src/outbound.ts:33,55`
|
|
26
|
+
|
|
27
|
+
`ddchat-text-${Date.now()}` 在高并发下可能产生重复 ID。
|
|
28
|
+
|
|
29
|
+
**建议:** 加入自增计数器或使用 `crypto.randomUUID()`。
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 4. Webhook 模式下的轮询等待(资源浪费)
|
|
34
|
+
|
|
35
|
+
**文件:** `src/gateway.ts:29-36`
|
|
36
|
+
|
|
37
|
+
Webhook 模式用 `setInterval` 每秒轮询 `abortSignal.aborted`,浪费资源。
|
|
38
|
+
|
|
39
|
+
**建议:** 改为直接监听 `abortSignal` 的 `abort` 事件:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
await new Promise<void>(resolve => {
|
|
43
|
+
ctx.abortSignal.addEventListener("abort", () => resolve(), { once: true });
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 5. resolveMediaMaxBytes 重复定义
|
|
50
|
+
|
|
51
|
+
**文件:** `src/inbound.ts:68-76` 和 `src/outbound.ts:6-12`
|
|
52
|
+
|
|
53
|
+
`inbound.ts` 的 `resolveMediaFetchMaxBytes` 和 `outbound.ts` 的 `resolveMediaMaxBytes` 逻辑完全相同。
|
|
54
|
+
|
|
55
|
+
**建议:** 提取到 `types.ts` 或新建一个共享工具函数。
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 6. WebSocket 构造器类型处理冗余
|
|
60
|
+
|
|
61
|
+
**文件:** `src/gateway.ts:122-135`
|
|
62
|
+
|
|
63
|
+
通过 `globalThis as unknown` 手动声明 WebSocket 类型,既冗长又不安全。
|
|
64
|
+
|
|
65
|
+
**建议:** 使用 TypeScript 内置的 `WebSocket` 类型(TS 4.4+ 全局可用),或在文件顶部用 `/// <reference lib="dom" />` 引入,去掉手动类型断言。
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 7. gateway.ts 中 ctx 类型未复用
|
|
70
|
+
|
|
71
|
+
**文件:** `src/gateway.ts:102-117`
|
|
72
|
+
|
|
73
|
+
`runWsSession` 的 `ctx` 参数手写了大量类型结构,���这些应该可以从 SDK 中导入。
|
|
74
|
+
|
|
75
|
+
**建议:** 从 `openclaw/plugin-sdk/core` 导入网关上下文类型,避免与 SDK 类型不同步。
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 8. 流式响应在 WebSocket 断开时静默丢失
|
|
80
|
+
|
|
81
|
+
**文件:** `src/inbound.ts:320-334`
|
|
82
|
+
|
|
83
|
+
`emitStream` 中调用 `getDdchatState().wsSend?.(...)` 使用了可选链,连接断开时流式数据无声丢失,客户端无法感知。
|
|
84
|
+
|
|
85
|
+
**建议:** 至少记录一条日志,或在 `wsSend` 返回 `false` 时尝试缓冲/通知调用方。
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 9. buildDdchatSessionKey 未被使用
|
|
90
|
+
|
|
91
|
+
**文件:** `src/session.ts`
|
|
92
|
+
|
|
93
|
+
整个 `session.ts` 导出了 `buildDdchatSessionKey` 函数,但在项目中没有任何地方引用它(路由使用的是 SDK 的 `resolveAgentRoute`)。
|
|
94
|
+
|
|
95
|
+
**建议:** 确认是否为遗留代码,如果不需要则移除。
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 10. Webhook 路径配置未生效
|
|
100
|
+
|
|
101
|
+
**文件:** `src/inbound.ts:212` 和 `src/types.ts:108-111`
|
|
102
|
+
|
|
103
|
+
`types.ts` 中解析了 `webhookPath` 和 `webhookPort` 配置,但 `registerDdchatWebhook` 硬编码了 `/ddchat/webhook` 路径,并未使用账户配置中的值。
|
|
104
|
+
|
|
105
|
+
**建议:** 使用 `account.webhookPath` 注册路由,或移除 `types.ts` 中未使用的配置字段。
|
package/README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
# 本地安装ddchat插件
|
|
2
|
-
```shell
|
|
3
|
-
openclaw plugins install "安装包路径"
|
|
4
|
-
```
|
|
5
|
-
|
|
6
|
-
# 默认default账号
|
|
7
|
-
```shell
|
|
8
|
-
openclaw channels add --channel ddchat --token "appId:appSecret"
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
# 多个账号时指定账户名 避免覆盖
|
|
12
|
-
```shell
|
|
13
|
-
openclaw channels add --channel ddchat --account xxx --token "appId:appSecret"
|
|
14
|
-
```
|
|
1
|
+
# 本地安装ddchat插件
|
|
2
|
+
```shell
|
|
3
|
+
openclaw plugins install "安装包路径"
|
|
4
|
+
```
|
|
5
|
+
|
|
6
|
+
# 默认default账号
|
|
7
|
+
```shell
|
|
8
|
+
openclaw channels add --channel ddchat --token "appId:appSecret"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
# 多个账号时指定账户名 避免覆盖
|
|
12
|
+
```shell
|
|
13
|
+
openclaw channels add --channel ddchat --account xxx --token "appId:appSecret"
|
|
14
|
+
```
|
package/index.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
|
|
2
|
-
import { ddchatPlugin } from "./src/channel.js";
|
|
3
|
-
import { registerDdchatWebhook } from "./src/inbound.js";
|
|
4
|
-
|
|
5
|
-
export { ddchatPlugin } from "./src/channel.js";
|
|
6
|
-
|
|
7
|
-
export default defineChannelPluginEntry({
|
|
8
|
-
id: "ddchat",
|
|
9
|
-
name: "DDChat",
|
|
10
|
-
description: "DDChat channel plugin",
|
|
11
|
-
plugin: ddchatPlugin,
|
|
12
|
-
registerFull: registerDdchatWebhook,
|
|
13
|
-
});
|
|
1
|
+
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import { ddchatPlugin } from "./src/channel.js";
|
|
3
|
+
import { registerDdchatWebhook } from "./src/inbound.js";
|
|
4
|
+
|
|
5
|
+
export { ddchatPlugin } from "./src/channel.js";
|
|
6
|
+
|
|
7
|
+
export default defineChannelPluginEntry({
|
|
8
|
+
id: "ddchat",
|
|
9
|
+
name: "DDChat",
|
|
10
|
+
description: "DDChat channel plugin",
|
|
11
|
+
plugin: ddchatPlugin,
|
|
12
|
+
registerFull: registerDdchatWebhook,
|
|
13
|
+
});
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "ddchat",
|
|
3
|
-
"kind": "channel",
|
|
4
|
-
"channels": ["ddchat"],
|
|
5
|
-
"configSchema": {
|
|
6
|
-
"type": "object",
|
|
7
|
-
"additionalProperties": false,
|
|
8
|
-
"properties": {
|
|
9
|
-
"channels": {
|
|
10
|
-
"type": "object",
|
|
11
|
-
"additionalProperties": true
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"id": "ddchat",
|
|
3
|
+
"kind": "channel",
|
|
4
|
+
"channels": ["ddchat"],
|
|
5
|
+
"configSchema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"channels": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"additionalProperties": true
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
package/package.json
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "ddchat",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "DDChat channel plugin for OpenClaw",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"devDependencies": {
|
|
7
|
-
"openclaw": "workspace:*"
|
|
8
|
-
},
|
|
9
|
-
"peerDependencies": {
|
|
10
|
-
"openclaw": ">=2026.2.0"
|
|
11
|
-
},
|
|
12
|
-
"peerDependenciesMeta": {
|
|
13
|
-
"openclaw": {
|
|
14
|
-
"optional": true
|
|
15
|
-
}
|
|
16
|
-
},
|
|
17
|
-
"openclaw": {
|
|
18
|
-
"extensions": [
|
|
19
|
-
"./index.ts"
|
|
20
|
-
],
|
|
21
|
-
"setupEntry": "./setup-entry.ts",
|
|
22
|
-
"channel": {
|
|
23
|
-
"id": "ddchat",
|
|
24
|
-
"label": "DDChat",
|
|
25
|
-
"selectionLabel": "DDChat (IM)",
|
|
26
|
-
"detailLabel": "DDChat IM",
|
|
27
|
-
"blurb": "DDChat internal IM integration.",
|
|
28
|
-
"order": 90
|
|
29
|
-
},
|
|
30
|
-
"install": {
|
|
31
|
-
"npmSpec": "ddchat",
|
|
32
|
-
"localPath": "ddchat",
|
|
33
|
-
"defaultChoice": "local"
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "ddchat",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "DDChat channel plugin for OpenClaw",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"devDependencies": {
|
|
7
|
+
"openclaw": "workspace:*"
|
|
8
|
+
},
|
|
9
|
+
"peerDependencies": {
|
|
10
|
+
"openclaw": ">=2026.2.0"
|
|
11
|
+
},
|
|
12
|
+
"peerDependenciesMeta": {
|
|
13
|
+
"openclaw": {
|
|
14
|
+
"optional": true
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"openclaw": {
|
|
18
|
+
"extensions": [
|
|
19
|
+
"./index.ts"
|
|
20
|
+
],
|
|
21
|
+
"setupEntry": "./setup-entry.ts",
|
|
22
|
+
"channel": {
|
|
23
|
+
"id": "ddchat",
|
|
24
|
+
"label": "DDChat",
|
|
25
|
+
"selectionLabel": "DDChat (IM)",
|
|
26
|
+
"detailLabel": "DDChat IM",
|
|
27
|
+
"blurb": "DDChat internal IM integration.",
|
|
28
|
+
"order": 90
|
|
29
|
+
},
|
|
30
|
+
"install": {
|
|
31
|
+
"npmSpec": "ddchat",
|
|
32
|
+
"localPath": "ddchat",
|
|
33
|
+
"defaultChoice": "local"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
package/setup-entry.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core";
|
|
2
|
-
import { ddchatPlugin } from "./src/channel.js";
|
|
3
|
-
|
|
4
|
-
export default defineSetupPluginEntry(ddchatPlugin);
|
|
1
|
+
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import { ddchatPlugin } from "./src/channel.js";
|
|
3
|
+
|
|
4
|
+
export default defineSetupPluginEntry(ddchatPlugin);
|
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/plugin";
|
|
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/plugin";
|
package/src/dedupe.ts
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
const DEFAULT_TTL_MS = 48 * 60 * 60 * 1000;
|
|
2
|
-
|
|
3
|
-
export class DdchatDedupeStore {
|
|
4
|
-
private readonly seen = new Map<string, number>();
|
|
5
|
-
private readonly ttlMs: number;
|
|
6
|
-
|
|
7
|
-
constructor(ttlMs = DEFAULT_TTL_MS) {
|
|
8
|
-
this.ttlMs = ttlMs;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
isDuplicate(accountId: string, messageId: string): boolean {
|
|
12
|
-
this.gc();
|
|
13
|
-
const key = `${accountId}:${messageId}`;
|
|
14
|
-
const now = Date.now();
|
|
15
|
-
const expiresAt = this.seen.get(key);
|
|
16
|
-
if (expiresAt && expiresAt > now) {
|
|
17
|
-
return true;
|
|
18
|
-
}
|
|
19
|
-
this.seen.set(key, now + this.ttlMs);
|
|
20
|
-
return false;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
private gc(): void {
|
|
24
|
-
const now = Date.now();
|
|
25
|
-
for (const [key, expiresAt] of this.seen.entries()) {
|
|
26
|
-
if (expiresAt <= now) {
|
|
27
|
-
this.seen.delete(key);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
1
|
+
const DEFAULT_TTL_MS = 48 * 60 * 60 * 1000;
|
|
2
|
+
|
|
3
|
+
export class DdchatDedupeStore {
|
|
4
|
+
private readonly seen = new Map<string, number>();
|
|
5
|
+
private readonly ttlMs: number;
|
|
6
|
+
|
|
7
|
+
constructor(ttlMs = DEFAULT_TTL_MS) {
|
|
8
|
+
this.ttlMs = ttlMs;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
isDuplicate(accountId: string, messageId: string): boolean {
|
|
12
|
+
this.gc();
|
|
13
|
+
const key = `${accountId}:${messageId}`;
|
|
14
|
+
const now = Date.now();
|
|
15
|
+
const expiresAt = this.seen.get(key);
|
|
16
|
+
if (expiresAt && expiresAt > now) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
this.seen.set(key, now + this.ttlMs);
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private gc(): void {
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
for (const [key, expiresAt] of this.seen.entries()) {
|
|
26
|
+
if (expiresAt <= now) {
|
|
27
|
+
this.seen.delete(key);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|