ddchat 0.4.2 → 0.4.4

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/README.md CHANGED
@@ -13,10 +13,19 @@ openclaw plugins uninstall ddchat # 卸载插件
13
13
 
14
14
  # 默认default账号
15
15
  ```shell
16
- openclaw channels add --channel ddchat --token "appId:appSecret"
17
- ```
18
-
19
- # 多个账号时指定账户名 避免覆盖
20
- ```shell
21
- openclaw channels add --channel ddchat --account xxx --token "appId:appSecret"
16
+ openclaw channels add --channel ddchat --token "appId:appSecret"
17
+ ```
18
+
19
+ # 登录命令
20
+ ```shell
21
+ openclaw channels login --channel ddchat
22
+
23
+ # 非交互环境可以通过环境变量传入 token。
24
+ # PowerShell:
25
+ $env:OPENCLAW_DDCHAT_TOKEN="appId:appSecret"; openclaw channels login --channel ddchat
26
+ ```
27
+
28
+ # 多个账号时指定账户名 避免覆盖
29
+ ```shell
30
+ openclaw channels add --channel ddchat --account xxx --token "appId:appSecret"
22
31
  ```
@@ -1,13 +1,11 @@
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
+ export { ddchatPlugin } from "./src/channel.js";
5
+ export default defineChannelPluginEntry({
6
+ id: "ddchat",
7
+ name: "DDChat",
8
+ description: "DDChat channel plugin",
9
+ plugin: ddchatPlugin,
10
+ registerFull: registerDdchatWebhook,
11
+ });
@@ -1,15 +1,149 @@
1
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
- }
2
+ "id": "ddchat",
3
+ "kind": "channel",
4
+ "channels": ["ddchat"],
5
+ "configSchema": {
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "properties": {}
9
+ },
10
+ "channelConfigs": {
11
+ "ddchat": {
12
+ "label": "DDChat",
13
+ "description": "DDChat internal IM integration.",
14
+ "schema": {
15
+ "$schema": "http://json-schema.org/draft-07/schema#",
16
+ "type": "object",
17
+ "additionalProperties": false,
18
+ "properties": {
19
+ "name": { "type": "string" },
20
+ "enabled": { "type": "boolean" },
21
+ "token": { "type": "string" },
22
+ "wsUrl": { "type": "string" },
23
+ "webhookPath": { "type": "string" },
24
+ "webhookPort": {
25
+ "type": "integer",
26
+ "minimum": 0,
27
+ "maximum": 9007199254740991
28
+ },
29
+ "connectionMode": {
30
+ "type": "string",
31
+ "enum": ["websocket", "webhook"]
32
+ },
33
+ "dmPolicy": {
34
+ "type": "string",
35
+ "enum": ["open", "pairing", "allowlist"]
36
+ },
37
+ "groupPolicy": {
38
+ "type": "string",
39
+ "enum": ["open", "allowlist", "disabled"]
40
+ },
41
+ "requireMention": { "type": "boolean" },
42
+ "streaming": { "type": "boolean" },
43
+ "streamingMode": {
44
+ "type": "string",
45
+ "enum": ["chunk", "token"]
46
+ },
47
+ "allowFrom": {
48
+ "type": "array",
49
+ "items": {
50
+ "anyOf": [{ "type": "string" }, { "type": "number" }]
51
+ }
52
+ },
53
+ "groupAllowFrom": {
54
+ "type": "array",
55
+ "items": {
56
+ "anyOf": [{ "type": "string" }, { "type": "number" }]
57
+ }
58
+ },
59
+ "heartbeatSec": {
60
+ "type": "number",
61
+ "minimum": 15
62
+ },
63
+ "defaultAccount": { "type": "string" },
64
+ "accounts": {
65
+ "type": "object",
66
+ "propertyNames": { "type": "string" },
67
+ "additionalProperties": {
68
+ "type": "object",
69
+ "additionalProperties": false,
70
+ "properties": {
71
+ "name": { "type": "string" },
72
+ "enabled": { "type": "boolean" },
73
+ "token": { "type": "string" },
74
+ "wsUrl": { "type": "string" },
75
+ "webhookPath": { "type": "string" },
76
+ "webhookPort": {
77
+ "type": "integer",
78
+ "minimum": 0,
79
+ "maximum": 9007199254740991
80
+ },
81
+ "connectionMode": {
82
+ "type": "string",
83
+ "enum": ["websocket", "webhook"]
84
+ },
85
+ "dmPolicy": {
86
+ "type": "string",
87
+ "enum": ["open", "pairing", "allowlist"]
88
+ },
89
+ "groupPolicy": {
90
+ "type": "string",
91
+ "enum": ["open", "allowlist", "disabled"]
92
+ },
93
+ "requireMention": { "type": "boolean" },
94
+ "streaming": { "type": "boolean" },
95
+ "streamingMode": {
96
+ "type": "string",
97
+ "enum": ["chunk", "token"]
98
+ },
99
+ "allowFrom": {
100
+ "type": "array",
101
+ "items": {
102
+ "anyOf": [{ "type": "string" }, { "type": "number" }]
103
+ }
104
+ },
105
+ "groupAllowFrom": {
106
+ "type": "array",
107
+ "items": {
108
+ "anyOf": [{ "type": "string" }, { "type": "number" }]
109
+ }
110
+ },
111
+ "heartbeatSec": {
112
+ "type": "number",
113
+ "minimum": 15
114
+ }
115
+ }
116
+ }
117
+ }
118
+ }
119
+ },
120
+ "uiHints": {
121
+ "": {
122
+ "label": "DDChat",
123
+ "help": "DDChat channel provider configuration for WebSocket/Webhook transport, account credentials, and access policies."
124
+ },
125
+ "token": {
126
+ "label": "DDChat Token",
127
+ "sensitive": true
128
+ },
129
+ "accounts.*.token": {
130
+ "label": "DDChat Token",
131
+ "sensitive": true
132
+ },
133
+ "connectionMode": {
134
+ "label": "DDChat Connection Mode"
135
+ },
136
+ "dmPolicy": {
137
+ "label": "DDChat DM Policy"
138
+ },
139
+ "groupPolicy": {
140
+ "label": "DDChat Group Policy"
141
+ }
142
+ },
143
+ "commands": {
144
+ "nativeCommandsAutoEnabled": false,
145
+ "nativeSkillsAutoEnabled": false
146
+ }
147
+ }
148
+ }
149
+ }
package/package.json CHANGED
@@ -1,36 +1,36 @@
1
- {
2
- "name": "ddchat",
3
- "version": "0.4.2",
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.4.4",
4
+ "description": "DDChat channel plugin for OpenClaw",
5
+ "type": "module",
6
+ "main": "./index.js",
7
+ "peerDependencies": {
8
+ "openclaw": ">=2026.2.0"
9
+ },
10
+ "peerDependenciesMeta": {
11
+ "openclaw": {
12
+ "optional": true
13
+ }
14
+ },
15
+ "openclaw": {
16
+ "extensions": [
17
+ "./index.js"
18
+ ],
19
+ "setupEntry": "./setup-entry.js",
20
+ "channel": {
21
+ "id": "ddchat",
22
+ "label": "DDChat",
23
+ "selectionLabel": "DDChat (IM)",
24
+ "detailLabel": "DDChat IM",
25
+ "docsPath": "/channels/ddchat",
26
+ "docsLabel": "ddchat",
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.js ADDED
@@ -0,0 +1,8 @@
1
+ import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
2
+ import { ddchatPlugin } from "./src/channel.js";
3
+ export default defineChannelPluginEntry({
4
+ id: "ddchat",
5
+ name: "DDChat",
6
+ description: "DDChat channel setup plugin",
7
+ plugin: ddchatPlugin,
8
+ });
package/src/channel.js ADDED
@@ -0,0 +1,151 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { createChatChannelPlugin } from "openclaw/plugin-sdk/core";
3
+ import { mutateConfigFile } from "openclaw/plugin-sdk/config-mutation";
4
+ import { patchScopedAccountConfig, prepareScopedSetupConfig } from "openclaw/plugin-sdk/setup";
5
+ import { DDCHAT_CHANNEL_ID } from "./constants.js";
6
+ import { ddchatGateway } from "./gateway.js";
7
+ import { ddchatOutbound } from "./outbound.js";
8
+ import { ddchatPairing } from "./pairing.js";
9
+ import { listDdchatAccountIds, resolveDdchatAccount } from "./types.js";
10
+ function inspectDdchatAccount(cfg, accountId) {
11
+ const account = resolveDdchatAccount(cfg, accountId);
12
+ return {
13
+ enabled: account.enabled,
14
+ configured: account.configured,
15
+ tokenStatus: account.token ? "available" : "missing",
16
+ connectionMode: account.connectionMode,
17
+ dmPolicy: account.dmPolicy,
18
+ groupPolicy: account.groupPolicy,
19
+ streaming: account.streaming,
20
+ streamingMode: account.streamingMode,
21
+ };
22
+ }
23
+ function replaceConfigContents(target, next) {
24
+ for (const key of Object.keys(target)) {
25
+ delete target[key];
26
+ }
27
+ Object.assign(target, next);
28
+ }
29
+ async function promptDdchatToken() {
30
+ const envToken = process.env.OPENCLAW_DDCHAT_TOKEN?.trim() || process.env.DDCHAT_TOKEN?.trim();
31
+ if (envToken) {
32
+ return envToken;
33
+ }
34
+ if (!process.stdin.isTTY) {
35
+ throw new Error("ddchat login requires OPENCLAW_DDCHAT_TOKEN, DDCHAT_TOKEN, or an interactive TTY");
36
+ }
37
+ const rl = createInterface({
38
+ input: process.stdin,
39
+ output: process.stderr,
40
+ });
41
+ try {
42
+ return (await rl.question("DDChat token (appId:appSecret): ")).trim();
43
+ }
44
+ finally {
45
+ rl.close();
46
+ }
47
+ }
48
+ export const ddchatPlugin = createChatChannelPlugin({
49
+ base: {
50
+ id: DDCHAT_CHANNEL_ID,
51
+ meta: {
52
+ id: DDCHAT_CHANNEL_ID,
53
+ label: "DDChat",
54
+ selectionLabel: "DDChat (IM)",
55
+ docsPath: "/channels/ddchat",
56
+ blurb: "DDChat internal IM integration.",
57
+ order: 90,
58
+ },
59
+ capabilities: {
60
+ chatTypes: ["direct", "channel"],
61
+ media: true,
62
+ threads: false,
63
+ polls: false,
64
+ },
65
+ config: {
66
+ listAccountIds: (cfg) => listDdchatAccountIds(cfg),
67
+ resolveAccount: (cfg, accountId) => resolveDdchatAccount(cfg, accountId),
68
+ inspectAccount: inspectDdchatAccount,
69
+ isEnabled: (account) => account.enabled,
70
+ isConfigured: (account) => account.configured,
71
+ },
72
+ setup: {
73
+ resolveAccountId: ({ accountId }) => accountId ?? "default",
74
+ applyAccountName: ({ cfg, accountId, name }) => prepareScopedSetupConfig({
75
+ cfg,
76
+ channelKey: DDCHAT_CHANNEL_ID,
77
+ accountId,
78
+ name,
79
+ alwaysUseAccounts: true,
80
+ }),
81
+ validateInput: ({ input }) => {
82
+ const token = typeof input.token === "string" ? input.token.trim() : "";
83
+ return token ? null : "ddchat requires --token";
84
+ },
85
+ applyAccountConfig: ({ cfg, accountId, input }) => {
86
+ const token = typeof input.token === "string" ? input.token.trim() : "";
87
+ const next = prepareScopedSetupConfig({
88
+ cfg,
89
+ channelKey: DDCHAT_CHANNEL_ID,
90
+ accountId,
91
+ name: input.name,
92
+ alwaysUseAccounts: true,
93
+ });
94
+ const patch = {};
95
+ if (token) {
96
+ patch.token = token;
97
+ }
98
+ return patchScopedAccountConfig({
99
+ cfg: next,
100
+ channelKey: DDCHAT_CHANNEL_ID,
101
+ accountId,
102
+ patch,
103
+ accountPatch: patch,
104
+ ensureChannelEnabled: true,
105
+ ensureAccountEnabled: true,
106
+ scopeDefaultToAccounts: true,
107
+ });
108
+ },
109
+ },
110
+ gateway: ddchatGateway,
111
+ auth: {
112
+ login: async ({ cfg, accountId, runtime }) => {
113
+ const resolvedAccountId = accountId?.trim() || "default";
114
+ const token = await promptDdchatToken();
115
+ if (!token) {
116
+ throw new Error("ddchat login requires a non-empty token");
117
+ }
118
+ await mutateConfigFile({
119
+ base: "source",
120
+ afterWrite: { mode: "restart", reason: "ddchat login updated channel credentials" },
121
+ mutate: (draft) => {
122
+ const next = ddchatPlugin.setup?.applyAccountConfig({
123
+ cfg: draft,
124
+ accountId: resolvedAccountId,
125
+ input: { token },
126
+ });
127
+ if (!next) {
128
+ throw new Error("ddchat setup adapter is unavailable");
129
+ }
130
+ replaceConfigContents(draft, next);
131
+ },
132
+ });
133
+ runtime.log(`DDChat credentials saved for account ${resolvedAccountId}.`);
134
+ },
135
+ },
136
+ },
137
+ pairing: ddchatPairing,
138
+ security: {
139
+ dm: {
140
+ channelKey: DDCHAT_CHANNEL_ID,
141
+ resolvePolicy: (account) => account.dmPolicy,
142
+ resolveAllowFrom: (account) => account.allowFrom,
143
+ defaultPolicy: "pairing",
144
+ normalizeEntry: (raw) => raw.replace(/^ddchat:/i, "").trim(),
145
+ },
146
+ },
147
+ outbound: ddchatOutbound,
148
+ threading: {
149
+ topLevelReplyToMode: "off",
150
+ },
151
+ });
@@ -1,5 +1,4 @@
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
+ /** Default WebSocket endpoint when `channels.ddchat.wsUrl` / per-account `wsUrl` is unset (`token` query appended at connect). */
4
+ export const DDCHAT_PLUGIN_WS_BASE_URL = "wss://chat.ddjf.info/socket/ai/claw";
package/src/dedupe.js ADDED
@@ -0,0 +1,44 @@
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
+ export class DdchatDedupeStore {
5
+ gcIntervalMs;
6
+ gcCheckInterval;
7
+ seen = new Map();
8
+ ttlMs;
9
+ lastGcAt = 0;
10
+ checksSinceGc = 0;
11
+ constructor(ttlMs = DEFAULT_TTL_MS, gcIntervalMs = DEFAULT_GC_INTERVAL_MS, gcCheckInterval = DEFAULT_GC_CHECK_INTERVAL) {
12
+ this.gcIntervalMs = gcIntervalMs;
13
+ this.gcCheckInterval = gcCheckInterval;
14
+ this.ttlMs = ttlMs;
15
+ }
16
+ isDuplicate(accountId, messageId) {
17
+ const key = `${accountId}:${messageId}`;
18
+ const now = Date.now();
19
+ this.gcIfNeeded(now);
20
+ const expiresAt = this.seen.get(key);
21
+ if (expiresAt && expiresAt > now) {
22
+ return true;
23
+ }
24
+ this.seen.set(key, now + this.ttlMs);
25
+ return false;
26
+ }
27
+ gcIfNeeded(now) {
28
+ this.checksSinceGc += 1;
29
+ if (now - this.lastGcAt < this.gcIntervalMs &&
30
+ this.checksSinceGc < this.gcCheckInterval) {
31
+ return;
32
+ }
33
+ this.lastGcAt = now;
34
+ this.checksSinceGc = 0;
35
+ this.gc(now);
36
+ }
37
+ gc(now) {
38
+ for (const [key, expiresAt] of this.seen.entries()) {
39
+ if (expiresAt <= now) {
40
+ this.seen.delete(key);
41
+ }
42
+ }
43
+ }
44
+ }