dlinkclaw-openclaw 1.0.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.
Files changed (85) hide show
  1. package/LICENSE +201 -0
  2. package/README.en.md +46 -0
  3. package/README.md +15 -0
  4. package/dist/index.d.ts +9 -0
  5. package/dist/index.js +1 -0
  6. package/dist/package.json +95 -0
  7. package/dist/src/account.d.ts +54 -0
  8. package/dist/src/account.js +394 -0
  9. package/dist/src/channel.d.ts +17 -0
  10. package/dist/src/channel.js +153 -0
  11. package/dist/src/client/client-manager.d.ts +4 -0
  12. package/dist/src/client/client-manager.js +10 -0
  13. package/dist/src/client/const.d.ts +37 -0
  14. package/dist/src/client/const.js +155 -0
  15. package/dist/src/client/crypto.d.ts +47 -0
  16. package/dist/src/client/crypto.js +260 -0
  17. package/dist/src/client/dl-client.d.ts +117 -0
  18. package/dist/src/client/dl-client.js +588 -0
  19. package/dist/src/client/dl-http.d.ts +27 -0
  20. package/dist/src/client/dl-http.js +223 -0
  21. package/dist/src/client/media-handler.d.ts +21 -0
  22. package/dist/src/client/media-handler.js +196 -0
  23. package/dist/src/client/message.d.ts +96 -0
  24. package/dist/src/client/message.js +1211 -0
  25. package/dist/src/client/timeout.d.ts +20 -0
  26. package/dist/src/client/timeout.js +36 -0
  27. package/dist/src/client/types.d.ts +226 -0
  28. package/dist/src/client/types.js +1 -0
  29. package/dist/src/monitor.d.ts +18 -0
  30. package/dist/src/monitor.js +322 -0
  31. package/dist/src/outbound.d.ts +6 -0
  32. package/dist/src/outbound.js +78 -0
  33. package/dist/src/policy.d.ts +42 -0
  34. package/dist/src/policy.js +216 -0
  35. package/dist/src/runtime.d.ts +9 -0
  36. package/dist/src/runtime.js +65 -0
  37. package/dist/src/services/gateway-rpc.d.ts +37 -0
  38. package/dist/src/services/gateway-rpc.js +37 -0
  39. package/dist/src/services/index.d.ts +41 -0
  40. package/dist/src/services/index.js +245 -0
  41. package/dist/src/services/services.d.ts +169 -0
  42. package/dist/src/services/services.js +1160 -0
  43. package/dist/src/services/types.d.ts +391 -0
  44. package/dist/src/services/types.js +5 -0
  45. package/dist/src/services/ws-client.d.ts +186 -0
  46. package/dist/src/services/ws-client.js +896 -0
  47. package/dist/src/utils.d.ts +20 -0
  48. package/dist/src/utils.js +128 -0
  49. package/ext_capabilities/workrelay-speak/CLONE_GUIDE.md +129 -0
  50. package/ext_capabilities/workrelay-speak/HARDWARE.md +84 -0
  51. package/ext_capabilities/workrelay-speak/QWEN_GUIDE.md +151 -0
  52. package/ext_capabilities/workrelay-speak/README.md +79 -0
  53. package/ext_capabilities/workrelay-speak/__pycache__/patch-kokoro-en.cpython-312.pyc +0 -0
  54. package/ext_capabilities/workrelay-speak/install.sh +329 -0
  55. package/ext_capabilities/workrelay-speak/install.sh.bak-20260814 +376 -0
  56. package/ext_capabilities/workrelay-speak/manifest.json +63 -0
  57. package/ext_capabilities/workrelay-speak/patch-kokoro-en.py +85 -0
  58. package/ext_capabilities/workrelay-speak/status.sh +111 -0
  59. package/ext_capabilities/workrelay-speak/workrelay-tts.sh +118 -0
  60. package/ext_capabilities/workrelay-speak-luxtts/README.md +44 -0
  61. package/ext_capabilities/workrelay-speak-luxtts/install.sh +330 -0
  62. package/ext_capabilities/workrelay-speak-luxtts/manifest.json +63 -0
  63. package/ext_capabilities/workrelay-speak-luxtts/samples/45ea2e2c-0863-4966-a739-8bff8817f4c0.wav +0 -0
  64. package/ext_capabilities/workrelay-speak-luxtts/samples/800e5af4-cba5-441e-8003-6d36f35409e6.wav +0 -0
  65. package/ext_capabilities/workrelay-speak-luxtts/samples/85fafe01-d4d6-4a48-890e-9ae113f21905.wav +0 -0
  66. package/ext_capabilities/workrelay-speak-luxtts/samples/reference_text.txt +4 -0
  67. package/ext_capabilities/workrelay-speak-luxtts/samples/stone-sample-6.4s.wav +0 -0
  68. package/ext_capabilities/workrelay-speak-luxtts/status.sh +115 -0
  69. package/ext_capabilities/workrelay-speak-luxtts/workrelay-tts.sh +98 -0
  70. package/ext_capabilities/workrelay-speak-qwen/README.md +62 -0
  71. package/ext_capabilities/workrelay-speak-qwen/install.sh +349 -0
  72. package/ext_capabilities/workrelay-speak-qwen/manifest.json +63 -0
  73. package/ext_capabilities/workrelay-speak-qwen/status.sh +106 -0
  74. package/ext_capabilities/workrelay-speak-qwen/workrelay-tts.sh +98 -0
  75. package/ext_capabilities/workrelay-transcribe/README.md +76 -0
  76. package/ext_capabilities/workrelay-transcribe/__pycache__/transcribe.cpython-312.pyc +0 -0
  77. package/ext_capabilities/workrelay-transcribe/__pycache__/transcribe_server.cpython-312.pyc +0 -0
  78. package/ext_capabilities/workrelay-transcribe/install.sh +227 -0
  79. package/ext_capabilities/workrelay-transcribe/manifest.json +42 -0
  80. package/ext_capabilities/workrelay-transcribe/status.sh +63 -0
  81. package/ext_capabilities/workrelay-transcribe/transcribe.py +56 -0
  82. package/ext_capabilities/workrelay-transcribe/transcribe_server.py +116 -0
  83. package/ext_capabilities/workrelay-transcribe/voicebox-transcribe.sh +88 -0
  84. package/openclaw.plugin.json +142 -0
  85. package/package.json +95 -0
@@ -0,0 +1,394 @@
1
+ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/core";
2
+ import { createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/channel-status";
3
+ import { getDLClient } from "./client/client-manager.js";
4
+ import { getDLLogger } from "./runtime.js";
5
+ // 2026-08-23 安装默认媒体上限:写入 openclaw.json 供 OpenClaw 核心出站媒体校验读取(默认仅 5MB,导致 ⚠️ Media failed.)
6
+ import { DEFAULT_INSTALL_MEDIA_MAX_MB } from "./client/const.js";
7
+ const log_prefix = "[Account.ts]";
8
+ function resolveDLConfig(cfg, channelId) {
9
+ const channels = cfg.channels;
10
+ const channelConfig = channels?.[channelId];
11
+ if (channelConfig) {
12
+ channelConfig.accountId = channelConfig.accountId || DEFAULT_ACCOUNT_ID;
13
+ return channelConfig;
14
+ }
15
+ // 首次配置时 cfg.channels 或 channels[channelId] 尚未创建,返回安全默认值
16
+ return {
17
+ enabled: false,
18
+ apiKey: '',
19
+ secret: '',
20
+ baseUrl: '',
21
+ accountId: DEFAULT_ACCOUNT_ID,
22
+ };
23
+ }
24
+ /**
25
+ * List all configured account IDs.
26
+ */
27
+ export function listDLConfiguredAccountIds(cfg, channelId) {
28
+ const config = resolveDLConfig(cfg, channelId);
29
+ return [config.accountId];
30
+ }
31
+ /**
32
+ * Resolve a complete account.
33
+ */
34
+ export function resolveDLAccount(params, channelId) {
35
+ const accountId = normalizeAccountId(params.accountId);
36
+ const config = resolveDLConfig(params.cfg, channelId);
37
+ return {
38
+ accountId: accountId,
39
+ accountName: config.accountName || 'DEFAULT',
40
+ enabled: config.enabled,
41
+ config: config
42
+ };
43
+ }
44
+ export function listDuolinkConfiguredAccounts(cfg, channelId) {
45
+ const config = resolveDLConfig(cfg, channelId);
46
+ const accounts = new Set();
47
+ accounts.add({
48
+ accountId: config.accountId,
49
+ accountName: config.accountName || config.accountId,
50
+ enabled: config.enabled,
51
+ config: config
52
+ });
53
+ return accounts;
54
+ }
55
+ export function resolveDefaultDLAccountId(cfg, channelId) {
56
+ const ids = listDLConfiguredAccountIds(cfg, channelId);
57
+ if (ids.includes(DEFAULT_ACCOUNT_ID)) {
58
+ return DEFAULT_ACCOUNT_ID;
59
+ }
60
+ return ids[0] ?? DEFAULT_ACCOUNT_ID;
61
+ }
62
+ export function setDLChannelAccountEnabled(params, channelId) {
63
+ const { cfg, accountId, enabled } = params;
64
+ const account = resolveDLAccount({ cfg, accountId }, channelId);
65
+ account.enabled = enabled;
66
+ const config = account.config;
67
+ return {
68
+ ...cfg,
69
+ channels: {
70
+ ...cfg.channels,
71
+ [channelId]: {
72
+ ...config,
73
+ enabled: enabled,
74
+ },
75
+ },
76
+ };
77
+ }
78
+ export function setDLChannelAccountParamsValue(params, channelId) {
79
+ const { cfg, param, value } = params;
80
+ const account = resolveDLAccount({ cfg, accountId: params.accountId }, channelId);
81
+ const config = account.config;
82
+ // 自动启用:当三个必填项都已填写时
83
+ const hasAll = !!((param === 'apiKey' ? value : config.apiKey) &&
84
+ (param === 'secret' ? value : config.secret) &&
85
+ (param === 'baseUrl' ? value : config.baseUrl));
86
+ // 首次配置且三个必填项全齐时,同时创建默认 main agent
87
+ const existingList = cfg.agents?.list;
88
+ const needsDefaultAgent = (!existingList || (Array.isArray(existingList) && existingList.length === 0)) && hasAll;
89
+ const result = {
90
+ ...cfg,
91
+ channels: {
92
+ ...cfg.channels,
93
+ [channelId]: {
94
+ ...config,
95
+ [param]: value,
96
+ enabled: hasAll ? true : config.enabled,
97
+ // 2026-08-23 安装/配置时自动写入默认媒体上限(MB),供 OpenClaw 核心出站媒体校验读取(默认仅 5MB,会导致 ⚠️ Media failed.)
98
+ // 不覆盖用户已设置的值
99
+ mediaMaxMb: config.mediaMaxMb ?? DEFAULT_INSTALL_MEDIA_MAX_MB,
100
+ },
101
+ },
102
+ };
103
+ if (needsDefaultAgent) {
104
+ result.agents = {
105
+ ...(cfg.agents ?? {}),
106
+ list: [
107
+ ...(Array.isArray(existingList) ? existingList : []),
108
+ { id: "main", name: "Main Agent" }
109
+ ]
110
+ };
111
+ }
112
+ return result;
113
+ }
114
+ export function deleteDLAccount({ cfg, accountId }, channelId) {
115
+ const next = { ...cfg };
116
+ const nextChannels = { ...cfg.channels };
117
+ delete nextChannels[channelId];
118
+ if (Object.keys(nextChannels).length > 0) {
119
+ next.channels = nextChannels;
120
+ }
121
+ else {
122
+ delete next.channels;
123
+ }
124
+ return next;
125
+ }
126
+ export function checkConfigured(config) {
127
+ const apiKey = config.apiKey;
128
+ const secret = config.secret;
129
+ const baeUrl = config.baseUrl;
130
+ // 三个必填项齐全、且 enabled 明确为 true,才算"已完成配置"
131
+ if (config.enabled !== true)
132
+ return false;
133
+ if (!apiKey || apiKey.length === 0)
134
+ return false;
135
+ if (!secret || secret.length === 0)
136
+ return false;
137
+ if (!baeUrl || baeUrl.length === 0)
138
+ return false;
139
+ if (!baeUrl.startsWith("https://"))
140
+ return false;
141
+ return true;
142
+ }
143
+ /**
144
+ * 生成设置向导引导信息(含二维码预留)
145
+ *
146
+ * 发版步骤:
147
+ * 1. npm install qrcode-terminal
148
+ * 2. 取消顶部 import qrcode 的注释
149
+ * 3. 取消底部 qrcode.generate 的注释
150
+ * 4. 替换 registerUrl 为沃联实际注册地址
151
+ */
152
+ function getDLIntroLines(registerUrl) {
153
+ const lines = [
154
+ "请选择以下任一方式完成配置:",
155
+ "",
156
+ "━━━ 方式一:我要注册新 APIKEY ━━━",
157
+ ];
158
+ // TODO: 发版时取消注释以生成终端二维码
159
+ // try {
160
+ // qrcode.generate(registerUrl, { small: true }, (qr: string) => {
161
+ // lines.push(...qr.split('\n'));
162
+ // });
163
+ // } catch (_) {
164
+ // // 二维码生成失败,降级显示纯文本链接
165
+ // }
166
+ lines.push("📱 扫码或在浏览器打开以下链接注册沃联账号:", registerUrl, "", "━━━ 方式二:已有 APIKEY ━━━", "直接按提示输入即可。");
167
+ return lines;
168
+ }
169
+ export function dlSetupWizard(channelId) {
170
+ return {
171
+ channel: channelId,
172
+ status: {
173
+ configuredLabel: "已配置 ✓",
174
+ unconfiguredLabel: "需要 baseUrl, APIKey 和 Secret",
175
+ configuredHint: "已配置",
176
+ unconfiguredHint: "需要设置",
177
+ resolveConfigured: ({ cfg, accountId }) => {
178
+ const account = resolveDLAccount({ cfg, accountId }, channelId);
179
+ const configured = checkConfigured(account.config);
180
+ return configured;
181
+ },
182
+ resolveStatusLines: ({ configured }) => {
183
+ return [`沃联 WorkRelay: ${configured ? "已配置" : "需要 baseUrl, APIKey 和 Secret"}`];
184
+ },
185
+ },
186
+ introNote: {
187
+ title: "🔧 沃联 WorkRelay 设置向导",
188
+ lines: getDLIntroLines(process.env.DL_REGISTER_URL || "https://WorkRelay.cn/register"),
189
+ shouldShow: ({ cfg, accountId }) => {
190
+ const account = resolveDLAccount({ cfg, accountId }, channelId);
191
+ return !checkConfigured(account.config);
192
+ },
193
+ },
194
+ credentials: [
195
+ {
196
+ inputKey: "url",
197
+ providerHint: "沃联回调地址 (BaseUrl)",
198
+ credentialLabel: "BaseUrl",
199
+ envPrompt: "使用环境变量中的 DL_BASEURL?",
200
+ keepPrompt: "BaseUrl 已配置,保留当前值?",
201
+ inputPrompt: "请输入沃联回调地址 (BaseUrl):",
202
+ inspect: ({ cfg, accountId }) => {
203
+ const account = resolveDLAccount({ cfg, accountId }, channelId);
204
+ const hasValue = Boolean(account.config.baseUrl?.trim());
205
+ return {
206
+ accountConfigured: hasValue,
207
+ hasConfiguredValue: hasValue,
208
+ resolvedValue: account.config.baseUrl || undefined,
209
+ };
210
+ },
211
+ applySet: ({ cfg, accountId, resolvedValue }) => setDLChannelAccountParamsValue({ cfg, accountId, 'param': 'baseUrl', value: resolvedValue }, channelId)
212
+ },
213
+ {
214
+ inputKey: "token",
215
+ providerHint: "沃联 API Key",
216
+ credentialLabel: "API Key",
217
+ envPrompt: "使用环境变量中的 DL_APIKEY?",
218
+ keepPrompt: "API Key 已配置,保留当前值?",
219
+ inputPrompt: "请输入沃联 API Key:",
220
+ inspect: ({ cfg, accountId }) => {
221
+ const account = resolveDLAccount({ cfg, accountId }, channelId);
222
+ const hasValue = Boolean(account.config.apiKey?.trim());
223
+ return {
224
+ accountConfigured: hasValue,
225
+ hasConfiguredValue: hasValue,
226
+ resolvedValue: account.config.apiKey || undefined,
227
+ };
228
+ },
229
+ applySet: ({ cfg, accountId, resolvedValue }) => setDLChannelAccountParamsValue({ cfg, accountId, 'param': 'apiKey', value: resolvedValue }, channelId)
230
+ },
231
+ {
232
+ inputKey: "privateKey",
233
+ providerHint: "沃联签名密钥 (Secret)",
234
+ credentialLabel: "Secret",
235
+ envPrompt: "使用环境变量中的 DL_SECRET?",
236
+ keepPrompt: "Secret 已配置,保留当前值?",
237
+ inputPrompt: "请输入沃联签名密钥 (Secret):",
238
+ inspect: ({ cfg, accountId }) => {
239
+ const account = resolveDLAccount({ cfg, accountId }, channelId);
240
+ const hasValue = Boolean(account.config.secret?.trim());
241
+ return {
242
+ accountConfigured: hasValue,
243
+ hasConfiguredValue: hasValue,
244
+ resolvedValue: account.config.secret || undefined,
245
+ };
246
+ },
247
+ applySet: ({ cfg, accountId, resolvedValue }) => setDLChannelAccountParamsValue({ cfg, accountId, 'param': 'secret', value: resolvedValue }, channelId)
248
+ },
249
+ ]
250
+ };
251
+ }
252
+ export function dlSetupAdapter(channelId) {
253
+ return {
254
+ resolveAccountId: ({ cfg, accountId }) => {
255
+ const account = resolveDLAccount({ cfg, accountId }, channelId);
256
+ return account.accountId;
257
+ },
258
+ applyAccountConfig: ({ cfg, accountId, input }) => {
259
+ const patch = {};
260
+ if (input.url !== undefined) {
261
+ patch.baseUrl = String(input.url).trim();
262
+ }
263
+ if (input.token !== undefined) {
264
+ patch.apiKey = String(input.token).trim();
265
+ }
266
+ if (input.privateKey !== undefined) {
267
+ patch.secret = String(input.privateKey).trim();
268
+ }
269
+ // 2026-08-23 安装时自动写入默认媒体上限(MB),供 OpenClaw 核心出站媒体校验读取(默认仅 5MB,会导致 ⚠️ Media failed.)
270
+ // 不覆盖用户已设置的值
271
+ const accountForPatch = resolveDLAccount({ cfg, accountId }, channelId);
272
+ if (accountForPatch.config.mediaMaxMb === undefined || accountForPatch.config.mediaMaxMb === null) {
273
+ patch.mediaMaxMb = DEFAULT_INSTALL_MEDIA_MAX_MB;
274
+ }
275
+ // 只要三个必填项全部提供,自动启用
276
+ const account = resolveDLAccount({ cfg, accountId }, channelId);
277
+ const hasAll = !!((patch.baseUrl ?? account.config.baseUrl) &&
278
+ (patch.apiKey ?? account.config.apiKey) &&
279
+ (patch.secret ?? account.config.secret));
280
+ if (hasAll) {
281
+ patch.enabled = true;
282
+ }
283
+ // 首次配置时,如果 agents.list 不存在,自动创建默认 main agent
284
+ const existingList = cfg.agents?.list;
285
+ const needsDefaultAgent = !existingList || (Array.isArray(existingList) && existingList.length === 0);
286
+ const result = {
287
+ ...cfg,
288
+ channels: {
289
+ ...cfg.channels,
290
+ [channelId]: {
291
+ ...account.config,
292
+ ...patch
293
+ }
294
+ }
295
+ };
296
+ if (needsDefaultAgent) {
297
+ result.agents = {
298
+ ...(cfg.agents ?? {}),
299
+ list: [
300
+ ...(Array.isArray(existingList) ? existingList : []),
301
+ { id: "main", name: "Main Agent" }
302
+ ]
303
+ };
304
+ }
305
+ return result;
306
+ },
307
+ };
308
+ }
309
+ ;
310
+ export function dlAccountStatus(channelId) {
311
+ return {
312
+ defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, { port: null }),
313
+ collectStatusIssues: (accounts) => accounts.flatMap((entry) => {
314
+ const accountId = String(entry.accountId ?? DEFAULT_ACCOUNT_ID);
315
+ const enabled = entry.enabled !== false;
316
+ const configured = entry.configured === true;
317
+ if (!enabled) {
318
+ return [];
319
+ }
320
+ const issues = [];
321
+ if (!configured) {
322
+ issues.push({
323
+ channel: channelId,
324
+ accountId,
325
+ kind: "config",
326
+ message: "沃联 baseUrl, apiKey, secret 未配置",
327
+ fix: `Run: openclaw channels add ${channelId} --base-url <baseUrl> --apiKey <apiKey> --secret <secret>`,
328
+ });
329
+ }
330
+ return issues;
331
+ }),
332
+ buildChannelSummary: ({ snapshot }) => ({
333
+ configured: snapshot.configured ?? false,
334
+ running: snapshot.running ?? false,
335
+ lastStartAt: snapshot.lastStartAt ?? null,
336
+ lastStopAt: snapshot.lastStopAt ?? null,
337
+ lastError: snapshot.lastError ?? null,
338
+ }),
339
+ buildAccountSnapshot: ({ account, runtime }) => {
340
+ const config = account.config;
341
+ const configured = Boolean(config.baseUrl?.trim() &&
342
+ config.apiKey?.trim() &&
343
+ config.secret?.trim());
344
+ return {
345
+ accountId: account.accountId,
346
+ name: account.accountName,
347
+ enabled: account.enabled,
348
+ configured,
349
+ running: runtime?.running ?? false,
350
+ lastStartAt: runtime?.lastStartAt ?? null,
351
+ lastStopAt: runtime?.lastStopAt ?? null,
352
+ lastError: runtime?.lastError ?? null,
353
+ };
354
+ },
355
+ probeAccount: async ({ account }) => {
356
+ const accountId = account.accountId;
357
+ const client = getDLClient(accountId);
358
+ if (!client || !client.isConnected) {
359
+ return { ok: false, error: `DLClient is not connected` };
360
+ }
361
+ return { ok: true };
362
+ },
363
+ };
364
+ }
365
+ /**
366
+ * 解释Account
367
+ * 打开WebUI -> 频道时 回调
368
+ * @param params
369
+ * @returns
370
+ */
371
+ export function dlDescribeAccount(params) {
372
+ const { account } = params;
373
+ const accountId = account.accountId || DEFAULT_ACCOUNT_ID;
374
+ //const logger = getDLLogger();
375
+ //logger.info(`${log_prefix} DescribeAccount account[${accountId}]...`)
376
+ const config = account.config;
377
+ const configured = checkConfigured(config);
378
+ const dlClient = getDLClient(accountId);
379
+ return {
380
+ "accountId": accountId,
381
+ "name": account.accountName || accountId,
382
+ "enabled": account.enabled,
383
+ "configured": configured,
384
+ "connected": dlClient?.isConnected(),
385
+ "lastInboundAt": dlClient?.getLastInboundAt(),
386
+ "busy": true
387
+ };
388
+ }
389
+ export function dlInspectAccount(params) {
390
+ const { accountId } = params;
391
+ const accId = accountId || DEFAULT_ACCOUNT_ID;
392
+ const logger = getDLLogger();
393
+ logger.info(`${log_prefix}InspectAccount account[${accId}]..`);
394
+ }
@@ -0,0 +1,17 @@
1
+ import { ChannelPlugin } from "openclaw/plugin-sdk/core";
2
+ import { DLResolvedAccount } from "./client/types.js";
3
+ export declare const channelMeta: {
4
+ id: string;
5
+ label: string;
6
+ selectionLabel: string;
7
+ docsPath: string;
8
+ docsLabel: string;
9
+ blurb: string;
10
+ aliases: string[];
11
+ order: number;
12
+ quickstartAllowFrom: boolean;
13
+ isDebug: boolean;
14
+ preferSessionLookupForAnnounceTarget: boolean;
15
+ };
16
+ export declare const LOG_TAG: string;
17
+ export declare const dlChannel: ChannelPlugin<DLResolvedAccount>;
@@ -0,0 +1,153 @@
1
+ import pkg from '../package.json' with { type: 'json' };
2
+ import { monitorDuolinkProvider } from "./monitor.js";
3
+ import { checkConfigured, deleteDLAccount, listDLConfiguredAccountIds, resolveDefaultDLAccountId, resolveDLAccount, setDLChannelAccountEnabled, dlSetupWizard, dlSetupAdapter, dlAccountStatus, dlDescribeAccount } from "./account.js";
4
+ import { formatPairingApproveHint } from "openclaw/plugin-sdk/core";
5
+ import { getDLLogger } from "./runtime.js";
6
+ import { dlOutbound, dlPairing } from "./outbound.js";
7
+ export const channelMeta = pkg.openclaw.channel;
8
+ export const LOG_TAG = channelMeta.selectionLabel;
9
+ const channelId = channelMeta.id;
10
+ export const dlChannel = {
11
+ id: channelId,
12
+ meta: {
13
+ ...channelMeta,
14
+ },
15
+ configSchema: {
16
+ schema: {
17
+ type: "object",
18
+ additionalProperties: true,
19
+ properties: {
20
+ enabled: { type: "boolean" },
21
+ apiKey: { type: "string" },
22
+ secret: { type: "string" },
23
+ baseUrl: { type: "string" }
24
+ }
25
+ }
26
+ },
27
+ capabilities: {
28
+ chatTypes: ['direct', 'group'],
29
+ reactions: true,
30
+ polls: false,
31
+ edit: false,
32
+ groupManagement: false,
33
+ threads: true,
34
+ reply: true,
35
+ media: true,
36
+ nativeCommands: true,
37
+ blockStreaming: true,
38
+ },
39
+ setupWizard: dlSetupWizard(channelId),
40
+ setup: dlSetupAdapter(channelId),
41
+ status: dlAccountStatus(channelId),
42
+ agentPrompt: {
43
+ messageToolHints: () => {
44
+ return [
45
+ // 2026-08-24 优化:target 格式补齐第 4 段 relayId(与 message.ts fromLabel 4 段格式保持一致,
46
+ // 否则 dl-client.ts autoReply 解析不到 relayId,出站消息无法路由到正确中继)
47
+ `- ${channelId} targeting: omit 'target' to reply to the current conversation (auto-inferred). Explicit targets: 'user:<userId>-<botId>-<botType>-<relayId>' or 'group:<chatId>-<botId>-<botType>-<relayId>'.`,
48
+ `- Example for user: user:I27484094711599104-I27484094749347841-{ewx|qq|dd|feishu|dLink}-<relayId> (botType varies by client).`,
49
+ `- Example for group: group:I27484094711599104-I27484094749347841-{ewx|qq|dd|feishu|dLink}-<relayId> (botType varies by client).`,
50
+ `- ${channelId} outbound with markdown text or media file upload can be realized.`,
51
+ `- **Outbound delivery**: All messages MUST route through ${channelId} channel. Never use webchat.`,
52
+ `- When creating cron jobs: always set delivery.channel: "${channelId}" and delivery.to: "${channelId}:group:<chatId>-<botId>-{dLink|ewx|qq|dd|feishu}-<relayId>" — botType depends on the client. Extract it from the current incoming message address. Omitting channel silently falls back to webchat.`,
53
+ `- When using the message tool: always channel: "${channelId}"`,
54
+ // 2026-08-24 新增:发送媒体/文件后必须补发一条文本确认,禁止静默(NO_REPLY)。
55
+ // 否则客户端 UI 一直显示"工作/转圈"状态(等不到完成信号),石头哥已明确要求。
56
+ `- **After sending media/files via the message tool, ALWAYS follow up with a short text confirmation (e.g. "✅ 已发出 3 个文件:a.png / b.pdf / c.txt"). Never reply NO_REPLY or stay silent after a media send — the client keeps showing a spinner/working state until it receives a final text reply.`,
57
+ ];
58
+ }
59
+ },
60
+ reload: {
61
+ configPrefixes: [
62
+ `channels.${channelId}`
63
+ ]
64
+ },
65
+ config: {
66
+ listAccountIds: (cfg) => listDLConfiguredAccountIds(cfg, channelId),
67
+ resolveAccount: (cfg, accountId) => resolveDLAccount({ cfg, accountId }, channelId),
68
+ defaultAccountId: (cfg) => resolveDefaultDLAccountId(cfg, channelId),
69
+ setAccountEnabled: (params) => setDLChannelAccountEnabled(params, channelId),
70
+ deleteAccount: (params) => deleteDLAccount(params, channelId),
71
+ isConfigured: (account) => {
72
+ const configured = checkConfigured(account.config);
73
+ account.config.configured = configured;
74
+ return configured;
75
+ },
76
+ describeAccount: (account, cfg) => dlDescribeAccount({ account, cfg }),
77
+ //暂时关闭inspectAccount: (cfg, accountId) => dlInspectAccount({ cfg, accountId, channelId })
78
+ },
79
+ resolver: {
80
+ //未了解其作用!,测试以观察日志
81
+ resolveTargets: (params) => {
82
+ const { accountId, inputs, kind, runtime } = params;
83
+ const logger = getDLLogger();
84
+ logger.info(`ResolveTargets account[${accountId}], kind=${kind}, input=[${inputs.join('')}]`);
85
+ const result = inputs.map(item => {
86
+ return {
87
+ input: item,
88
+ id: accountId || '',
89
+ resolved: true
90
+ };
91
+ });
92
+ return new Promise((ok) => {
93
+ ok(result);
94
+ });
95
+ }
96
+ },
97
+ messaging: {
98
+ normalizeTarget(raw) {
99
+ const trimmed = raw.trim();
100
+ const logger = getDLLogger();
101
+ logger.info(`Messaging[normalizeTarget] target normalized >>> ${trimmed}`);
102
+ if (!trimmed)
103
+ return undefined;
104
+ return trimmed;
105
+ },
106
+ targetResolver: {
107
+ looksLikeId: (id) => {
108
+ const trimmed = id?.trim();
109
+ return Boolean(trimmed);
110
+ },
111
+ hint: "<userId|groupId>",
112
+ },
113
+ formatTargetDisplay(params) {
114
+ getDLLogger().info('Messaging[formatTargetDisplay]:' + JSON.stringify(params));
115
+ return `FormatTargetDisplay: ${params.target}`;
116
+ }
117
+ },
118
+ security: {
119
+ resolveDmPolicy: ({ account }) => {
120
+ const basePath = `channels.${channelId}.`;
121
+ return {
122
+ policy: account.config.dmPolicy ?? "pairing",
123
+ allowFrom: account.config.allowFrom ?? [],
124
+ policyPath: `${basePath}dmPolicy`,
125
+ allowFromPath: basePath,
126
+ approveHint: formatPairingApproveHint(channelId),
127
+ normalizeEntry: (raw) => {
128
+ getDLLogger().info('Security[resolveDmPolicy]:' + raw);
129
+ const result = raw.replace(new RegExp(`^${channelId}:`, "i"), "").trim();
130
+ getDLLogger().info('Security[resolveDmPolicy]:' + result);
131
+ return result;
132
+ },
133
+ };
134
+ }
135
+ },
136
+ pairing: dlPairing(channelId),
137
+ outbound: dlOutbound(),
138
+ gateway: {
139
+ startAccount: async (ctx) => {
140
+ ctx.setStatus({
141
+ accountId: ctx.accountId,
142
+ lastStartAt: Date.now()
143
+ });
144
+ return monitorDuolinkProvider({
145
+ config: ctx.cfg,
146
+ runtime: ctx.runtime,
147
+ abortSignal: ctx.abortSignal,
148
+ accountId: ctx.accountId,
149
+ channelId
150
+ });
151
+ },
152
+ }
153
+ };
@@ -0,0 +1,4 @@
1
+ import { DLClient } from "./dl-client.js";
2
+ export declare const getDLClient: (accountId: string) => DLClient | null;
3
+ export declare const setDLClient: (accountId: string, client: DLClient) => void;
4
+ export declare const removeDLClient: (accountId: string) => void;
@@ -0,0 +1,10 @@
1
+ const dlClients = new Map();
2
+ export const getDLClient = (accountId) => {
3
+ return dlClients.get(accountId) ?? null;
4
+ };
5
+ export const setDLClient = (accountId, client) => {
6
+ dlClients.set(accountId, client);
7
+ };
8
+ export const removeDLClient = (accountId) => {
9
+ dlClients.delete(accountId);
10
+ };
@@ -0,0 +1,37 @@
1
+ /** 仅包含图片时的消息占位符 */
2
+ export declare const MEDIA_IMAGE_PLACEHOLDER = "<media:image>";
3
+ export declare const MEDIA_VOICE_PLACEHOLDER = "<media:audio>";
4
+ export declare const MEDIA_VIDEO_PLACEHOLDER = "<media:video>";
5
+ /** 仅包含文件时的消息占位符 */
6
+ export declare const MEDIA_DOCUMENT_PLACEHOLDER = "<media:document>";
7
+ /** 默认媒体大小上限(MB)——插件↔服务端链路(下载/上传)的兜底限制 */
8
+ export declare const DEFAULT_MEDIA_MAX_MB = 50;
9
+ /**
10
+ * 2026-08-23 安装时写入 openclaw.json 的默认媒体上限(MB)
11
+ * 注意:OpenClaw 核心出站媒体校验读取的是 channels.<id>.mediaMaxMb(默认仅 5MB),
12
+ * 与 DEFAULT_MEDIA_MAX_MB(插件↔服务端)无关。安装向导写入本值后,核心出站即放行。
13
+ */
14
+ export declare const DEFAULT_INSTALL_MEDIA_MAX_MB = 100;
15
+ /** 文本分块大小上限 */
16
+ export declare const TEXT_CHUNK_LIMIT = 4000;
17
+ /** 图片下载超时时间(毫秒) */
18
+ export declare const IMAGE_DOWNLOAD_TIMEOUT_MS: number;
19
+ /** 文件下载超时时间(毫秒) */
20
+ export declare const FILE_DOWNLOAD_TIMEOUT_MS: number;
21
+ /** 消息发送超时时间(毫秒) */
22
+ export declare const REPLY_SEND_TIMEOUT_MS = 15000;
23
+ /**
24
+ * 主动回复API
25
+ */
26
+ export declare const AUTO_RELAY_API = "/guest/relayBot/sendNotification";
27
+ export declare const AUTO_RELAY_UPLOAD = "/guest/relayBot/uploadMediaFiles";
28
+ export declare const FILE_LIMITS: {
29
+ image: {
30
+ maxSize: number;
31
+ };
32
+ attachment: {
33
+ maxSize: number;
34
+ };
35
+ };
36
+ export declare const IMAGE_EXTS: Set<string>;
37
+ export type ATTACHMENT_KINK = "image" | "video" | "audio" | "document" | "unknown" | undefined;