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,78 @@
1
+ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/core";
2
+ import { getDLLogger, getDLRuntime } from "./runtime.js";
3
+ import { TEXT_CHUNK_LIMIT } from "./client/const.js";
4
+ import { getDLClient } from "./client/client-manager.js";
5
+ const log_prefix = "[Outbound.ts]";
6
+ const getClient = (accountId) => {
7
+ const dlClient = getDLClient(accountId);
8
+ if (!dlClient) {
9
+ throw new Error(`${log_prefix} DLClient account[${accountId}] is null!`);
10
+ }
11
+ return dlClient;
12
+ };
13
+ export const dlOutbound = () => {
14
+ const logger = getDLLogger();
15
+ return {
16
+ deliveryMode: "direct",
17
+ chunker: (text, limit) => getDLRuntime().channel.text.chunkMarkdownText(text, limit),
18
+ chunkerMode: "markdown",
19
+ textChunkLimit: TEXT_CHUNK_LIMIT,
20
+ async sendMedia(ctx) {
21
+ const { to, text, accountId, replyToId, threadId, mediaUrl } = ctx;
22
+ const accId = accountId || DEFAULT_ACCOUNT_ID;
23
+ logger.info(`${log_prefix} Notify[sendMedia] to[${to}], accountId=${accId}, replyToId=${replyToId}, threadId=${threadId}, text=${text}`);
24
+ logger.info(`${log_prefix} Media files = [${mediaUrl}]..`);
25
+ return await getClient(accId).autoReply({
26
+ to,
27
+ text,
28
+ mediaFiles: mediaUrl && mediaUrl.length > 0 ? [mediaUrl] : []
29
+ });
30
+ },
31
+ async sendPayload(ctx) {
32
+ //主动出站(文本与媒体文件)
33
+ const { to, accountId, replyToId, threadId, payload } = ctx;
34
+ const accId = accountId || DEFAULT_ACCOUNT_ID;
35
+ logger.info(`${log_prefix} Notify[sendPayload] to[${to}], accountId=${accId}, replyToId=${replyToId}, threadId=${threadId}, text=${payload.text}`);
36
+ // 2026-09-01 修复:mediaUrl 与 mediaUrls 可能同时携带同一文件(语音场景),去重避免重复发送导致空语音
37
+ const mediaFile = [...new Set([...(payload.mediaUrl ? [payload.mediaUrl] : []), ...(payload.mediaUrls ?? [])])];
38
+ const text = payload.text ?? '';
39
+ const dlClient = getClient(accId);
40
+ return await dlClient.autoReply({
41
+ to,
42
+ text,
43
+ mediaFiles: [...mediaFile]
44
+ });
45
+ },
46
+ async sendText(ctx) {
47
+ //主动出站(文本)
48
+ const { cfg, to, text, accountId, replyToId, threadId } = ctx;
49
+ const accId = accountId || DEFAULT_ACCOUNT_ID;
50
+ logger.info(`${log_prefix} Notify[sendText] to[${to}], accountId=${accId}, replyToId=${replyToId}, threadId=${threadId}, text=${text}`);
51
+ const dlClient = getClient(accId);
52
+ return await dlClient.autoReply({
53
+ to,
54
+ text
55
+ });
56
+ }
57
+ };
58
+ };
59
+ export const dlPairing = (channelId) => {
60
+ const logger = getDLLogger();
61
+ return {
62
+ idLabel: channelId,
63
+ normalizeAllowEntry: (entry) => {
64
+ const trimmed = entry?.trim();
65
+ logger.info(`${log_prefix} NormalizeAllowEntry: ${trimmed}`);
66
+ return trimmed.replace(new RegExp(`^(${channelId}|user):`, "i"), "").trim();
67
+ },
68
+ notifyApproval: async ({ cfg, accountId }) => {
69
+ const accId = accountId || DEFAULT_ACCOUNT_ID;
70
+ logger.info('对码成功回调通知...');
71
+ const dlClient = getDLClient(accId);
72
+ if (dlClient) {
73
+ await dlClient.approved(true);
74
+ }
75
+ throw new Error(`${channelId} account[${accId}] DLClient is null!`);
76
+ }
77
+ };
78
+ };
@@ -0,0 +1,42 @@
1
+ import { DLConfig, DLResolvedAccount } from "./client/types.js";
2
+ import { DLClient } from "./client/dl-client.js";
3
+ import { OpenClawConfig, PluginLogger, PluginRuntime } from "openclaw/plugin-sdk/core";
4
+ export type DLGroupPolicyCheckResult = {
5
+ allowed: boolean;
6
+ };
7
+ export type DmPolicyCheckResult = {
8
+ allowed: boolean;
9
+ allowFrom?: Array<string | number>;
10
+ };
11
+ export declare function checkGroupPolicy(params: {
12
+ chatId: string;
13
+ senderId: string;
14
+ account: DLResolvedAccount;
15
+ channelId: string;
16
+ logger: PluginLogger;
17
+ }): DLGroupPolicyCheckResult;
18
+ /**
19
+ * 检查发送者是否在允许列表中(通用)
20
+ */
21
+ export declare function isSenderAllowed(senderId: string, allowFrom: string[], channelId: string): boolean;
22
+ /**
23
+ * 检查 DM Policy 访问控制
24
+ * @returns 检查结果,包含是否允许继续处理
25
+ */
26
+ export declare function checkDmPolicy(params: {
27
+ core: PluginRuntime;
28
+ senderId: string;
29
+ account: DLResolvedAccount;
30
+ client: DLClient;
31
+ channelId: string;
32
+ streamId: string;
33
+ logger: PluginLogger;
34
+ }): Promise<DmPolicyCheckResult>;
35
+ /**
36
+ * 设置 dmPolicy
37
+ */
38
+ export declare function setDmPolicy(cfg: OpenClawConfig, dmPolicy: "pairing" | "allowlist" | "open" | "disabled", channelId: string): OpenClawConfig;
39
+ /**
40
+ * 设置企业微信账户配置
41
+ */
42
+ export declare function setAccount(cfg: OpenClawConfig, account: Partial<DLConfig>, channelId: string): OpenClawConfig;
@@ -0,0 +1,216 @@
1
+ // 2026-09-06 适配 openclaw 2026.9.1:审批改按 requestId,经宿主 RPC list 获取(9.1 起按 code 审批 API 不再公开)
2
+ import { listPairingRequests } from "./services/gateway-rpc.js";
3
+ import { resolveDLAccount } from "./account.js";
4
+ import { resolveAllowlistMatchSimple } from "openclaw/plugin-sdk/allow-from";
5
+ import { addWildcardAllowFrom } from "openclaw/plugin-sdk/setup";
6
+ /**
7
+ * 检查群组是否在允许列表中
8
+ */
9
+ function isInGroupAllowed(params) {
10
+ const { channelId, groupPolicy } = params;
11
+ if (groupPolicy === "disabled") {
12
+ return false;
13
+ }
14
+ if (groupPolicy === "open") {
15
+ return true;
16
+ }
17
+ // allowlist 模式:检查群组是否在允许列表中
18
+ const normalizedAllowFrom = params.allowFrom.map((entry) => String(entry).replace(new RegExp(`^${channelId}:`, "i"), "").trim());
19
+ if (normalizedAllowFrom.includes("*")) {
20
+ return true;
21
+ }
22
+ const normalizedGroupId = params.groupId.trim();
23
+ return normalizedAllowFrom.some((entry) => entry === normalizedGroupId || entry.toLowerCase() === normalizedGroupId.toLowerCase());
24
+ }
25
+ /**
26
+ * 检查群组内发送者是否在允许列表中
27
+ */
28
+ function isGroupSenderAllowed(params) {
29
+ const { senderId, groupId, channelId, config } = params;
30
+ const groups = config.groups;
31
+ //不配置默认允许
32
+ if (!groups) {
33
+ return true;
34
+ }
35
+ const groupConfig = groups[groupId];
36
+ if (!groupConfig) {
37
+ return true;
38
+ }
39
+ const perGroupSenderAllowFrom = (groupConfig.allowFrom ?? []).map((v) => String(v));
40
+ //不配置默认允许
41
+ if (perGroupSenderAllowFrom.length === 0) {
42
+ return true;
43
+ }
44
+ if (perGroupSenderAllowFrom.includes("*") || perGroupSenderAllowFrom.includes(senderId)) {
45
+ return true;
46
+ }
47
+ return perGroupSenderAllowFrom.some((entry) => {
48
+ const normalized = entry.replace(new RegExp(`^${channelId}:`, "i"), "").trim();
49
+ return normalized === senderId || normalized === `user:${senderId}`;
50
+ });
51
+ }
52
+ export function checkGroupPolicy(params) {
53
+ const { channelId, chatId, senderId, account, logger } = params;
54
+ const { accountId } = account;
55
+ const groupPolicy = account.config.groupPolicy ?? "open";
56
+ const groupAllowFrom = account.config.groupAllowFrom ?? [];
57
+ const groupAllowed = isInGroupAllowed({
58
+ channelId,
59
+ groupPolicy,
60
+ allowFrom: groupAllowFrom,
61
+ groupId: chatId,
62
+ });
63
+ if (!groupAllowed) {
64
+ logger.warn(`account[${accountId}] Group ${chatId} not allowed (groupPolicy=${groupPolicy})`);
65
+ return { allowed: false };
66
+ }
67
+ const senderAllowed = isGroupSenderAllowed({
68
+ channelId,
69
+ senderId,
70
+ groupId: chatId,
71
+ config: account.config,
72
+ });
73
+ if (!senderAllowed) {
74
+ logger.warn(`account[${accountId}] Sender ${senderId} not in group ${chatId} sender allowlist`);
75
+ return { allowed: false };
76
+ }
77
+ return { allowed: true };
78
+ }
79
+ /**
80
+ * 检查发送者是否在允许列表中(通用)
81
+ */
82
+ export function isSenderAllowed(senderId, allowFrom, channelId) {
83
+ if (allowFrom.includes("*")) {
84
+ return true;
85
+ }
86
+ return allowFrom.some((entry) => {
87
+ const normalized = entry.replace(new RegExp(`^${channelId}:`, "i"), "").trim();
88
+ return normalized === senderId || normalized === `user:${senderId}`;
89
+ });
90
+ }
91
+ /**
92
+ * 检查 DM Policy 访问控制
93
+ * @returns 检查结果,包含是否允许继续处理
94
+ */
95
+ export async function checkDmPolicy(params) {
96
+ const { core, senderId, account, client, logger, channelId, streamId } = params;
97
+ const logPreTag = `account[${account.accountId}],Policy.ts[checkDmPolicy],`;
98
+ const dmPolicy = account.config.dmPolicy ?? "pairing"; //默认需要配对
99
+ logger.warn(`${logPreTag}Blocked DM from [${senderId}] (dmPolicy=${dmPolicy})`);
100
+ const configAllowFrom = (account.config.allowFrom ?? []).map((v) => String(v));
101
+ // 如果 dmPolicy 是 disabled直接拒绝
102
+ if (dmPolicy === "disabled") {
103
+ logger.warn(`未配对,直接拒绝...`);
104
+ return { allowed: false };
105
+ }
106
+ // 如果是 open 模式,允许所有人
107
+ if (dmPolicy === "open") {
108
+ return { allowed: true, allowFrom: [...configAllowFrom] };
109
+ }
110
+ // Compatibility fallback for newer OpenClaw implementations.
111
+ const storeAllowFrom = await core.channel.pairing
112
+ .readAllowFromStore({ channel: channelId, accountId: account.accountId })
113
+ .catch((e) => {
114
+ logger.error(`${logPreTag}get allow from store, error=[${String(e)}]`);
115
+ return [];
116
+ });
117
+ const effectiveAllowFrom = [...configAllowFrom, ...storeAllowFrom];
118
+ const senderAllowedResult = resolveAllowlistMatchSimple({
119
+ allowFrom: effectiveAllowFrom,
120
+ senderId: senderId,
121
+ allowNameMatching: false
122
+ });
123
+ logger.info(`${logPreTag}senderAllowedResult = ${JSON.stringify(senderAllowedResult)}, effectiveAllowFrom=[${effectiveAllowFrom.join(',')}]`);
124
+ if (senderAllowedResult && senderAllowedResult.allowed) {
125
+ return { allowed: true, allowFrom: [...effectiveAllowFrom] };
126
+ }
127
+ // 处理未授权用户
128
+ if (dmPolicy === "pairing") {
129
+ const { code, created } = await core.channel.pairing.upsertPairingRequest({
130
+ channel: channelId,
131
+ id: senderId,
132
+ accountId: account.accountId,
133
+ meta: { name: senderId },
134
+ });
135
+ if (created) {
136
+ logger.info(`${logPreTag}pairing request created for sender=${senderId}`);
137
+ const replayContent = core.channel.pairing.buildPairingReply({
138
+ channel: channelId,
139
+ idLine: `Your account[${account.accountId}] sedner=${senderId}`,
140
+ code
141
+ });
142
+ await client.publishToServer({
143
+ streamId,
144
+ content: replayContent,
145
+ kind: 'block',
146
+ finish: true
147
+ });
148
+ }
149
+ else {
150
+ logger.error(`${logPreTag}blocked unauthorized sender=${senderId} (dmPolicy=${dmPolicy})`);
151
+ }
152
+ // 2026-09-06 适配 openclaw 2026.9.1:审批改按 requestId(宿主 RPC),
153
+ // 通过 channels.pairing.list 获取本次 requestId 随事件下发;
154
+ // 同 sender 幂等 upsert(id=senderId),pending 中按 senderId 唯一定位
155
+ let pairingRequestId;
156
+ try {
157
+ const pending = await listPairingRequests(channelId, account.accountId);
158
+ pairingRequestId = pending.find(r => r.senderId === senderId)?.requestId;
159
+ if (!pairingRequestId) {
160
+ logger.warn(`${logPreTag}list 未匹配到 sender=${senderId} 的配对请求`);
161
+ }
162
+ }
163
+ catch (e) {
164
+ logger.error(`${logPreTag}获取 requestId 失败:${String(e)}`);
165
+ }
166
+ // 对码请求已创建/已存在,通过MQTT通知客户端可一键对码(带 requestId 供 9.1 审批使用)
167
+ const pairingEvent = {
168
+ code, // 对码验证码
169
+ requestId: pairingRequestId, // 2026-09-06 新增:宿主配对请求 id(9.1 起审批必需)
170
+ channelId, // 通道ID
171
+ senderId, // 发送者ID,实质是relayBotId
172
+ relayType: "OpenClaw"
173
+ };
174
+ client.publishEventToDLClient(pairingEvent, 'onPairingRequest', '0').catch(err => logger.error(`${logPreTag}发送对码事件失败:${String(err)}`));
175
+ }
176
+ logger.warn(`account[${account.accountId}] sender=${senderId} access denied`);
177
+ return { allowed: false };
178
+ }
179
+ /**
180
+ * 设置 dmPolicy
181
+ */
182
+ export function setDmPolicy(cfg, dmPolicy, channelId) {
183
+ const account = resolveDLAccount({ cfg }, channelId);
184
+ const existingAllowFrom = account.config.allowFrom ?? [];
185
+ const allowFrom = dmPolicy === "open"
186
+ ? addWildcardAllowFrom(existingAllowFrom.map((x) => String(x)))
187
+ : existingAllowFrom.map((x) => String(x));
188
+ return setAccount(cfg, {
189
+ dmPolicy,
190
+ allowFrom,
191
+ }, channelId);
192
+ }
193
+ /**
194
+ * 设置企业微信账户配置
195
+ */
196
+ export function setAccount(cfg, account, channelId) {
197
+ const existing = (cfg.channels?.[channelId] ?? {});
198
+ const merged = {
199
+ enabled: account.enabled ?? existing?.enabled ?? true,
200
+ apiKey: account.apiKey ?? existing?.apiKey ?? "",
201
+ secret: account.secret ?? existing?.secret ?? "",
202
+ baseUrl: account.baseUrl ?? existing?.baseUrl ?? "",
203
+ allowFrom: account.allowFrom ?? existing?.allowFrom,
204
+ dmPolicy: account.dmPolicy ?? existing?.dmPolicy,
205
+ ...(account.accountName || existing?.accountName
206
+ ? { name: account.accountName ?? existing?.accountName }
207
+ : {})
208
+ };
209
+ return {
210
+ ...cfg,
211
+ channels: {
212
+ ...cfg.channels,
213
+ [channelId]: merged,
214
+ },
215
+ };
216
+ }
@@ -0,0 +1,9 @@
1
+ import type { PluginLogger, PluginRuntime } from "openclaw/plugin-sdk/core";
2
+ export declare function setDLRuntime(next: PluginRuntime): void;
3
+ export declare function getDLRuntime(): PluginRuntime;
4
+ export declare function setDLLogger(coreLogger: PluginLogger, msgPrefix: string): void;
5
+ export declare function getDLLogger(): PluginLogger;
6
+ /** 记录一个已停止的 runId(客户端点停止后由 services.ts 调用) */
7
+ export declare function markRunStopped(runId: string): void;
8
+ /** 检查 runId 是否处于已停止状态(before_tool_call 钩子调用) */
9
+ export declare function isRunStopped(runId?: string): boolean;
@@ -0,0 +1,65 @@
1
+ let runtime = null;
2
+ let logger = null;
3
+ export function setDLRuntime(next) {
4
+ runtime = next;
5
+ }
6
+ export function getDLRuntime() {
7
+ if (!runtime) {
8
+ throw new Error("DLink runtime not initialized");
9
+ }
10
+ return runtime;
11
+ }
12
+ export function setDLLogger(coreLogger, msgPrefix) {
13
+ const log = {
14
+ info: (msg) => {
15
+ coreLogger.info(`${msgPrefix} ${msg}`);
16
+ },
17
+ warn: (msg) => {
18
+ coreLogger.warn(`${msgPrefix} ${msg}`);
19
+ },
20
+ error: (msg) => {
21
+ coreLogger.error(`${msgPrefix} ${msg}`);
22
+ },
23
+ debug: (msg) => {
24
+ if (coreLogger.debug) {
25
+ coreLogger.debug(`${msgPrefix} ${msg}`);
26
+ return;
27
+ }
28
+ if (process.env.NODE_ENV === 'dev') {
29
+ coreLogger.info(`DEBUG-${msgPrefix} ${msg}`);
30
+ }
31
+ }
32
+ };
33
+ logger = log;
34
+ }
35
+ export function getDLLogger() {
36
+ if (!logger) {
37
+ return console;
38
+ }
39
+ return logger;
40
+ }
41
+ // 2026-08-24 停止标记集合:客户端点“停止”触发 chat.abort 命令时记录 runId,
42
+ // before_tool_call 钩子据此 block 该 run 的后续工具调用,实现“立即终止”。
43
+ // 用 TTL 过期清理,防止 runId 无限堆积(runId 为网关唯一 ID,过期后不可能再命中)。
44
+ const stoppedRuns = new Map();
45
+ const STOPPED_RUN_TTL_MS = 10 * 60 * 1000; // 10 分钟过期
46
+ /** 记录一个已停止的 runId(客户端点停止后由 services.ts 调用) */
47
+ export function markRunStopped(runId) {
48
+ if (!runId)
49
+ return;
50
+ stoppedRuns.set(runId, Date.now());
51
+ getDLLogger().info(`markRunStopped: ${runId}`);
52
+ }
53
+ /** 检查 runId 是否处于已停止状态(before_tool_call 钩子调用) */
54
+ export function isRunStopped(runId) {
55
+ if (!runId)
56
+ return false;
57
+ const ts = stoppedRuns.get(runId);
58
+ if (!ts)
59
+ return false;
60
+ if (Date.now() - ts > STOPPED_RUN_TTL_MS) {
61
+ stoppedRuns.delete(runId); // 过期自动清理
62
+ return false;
63
+ }
64
+ return true;
65
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 2026-09-06 适配 openclaw 2026.9.1:宿主 Gateway RPC 调用工具(配对审批等)
3
+ *
4
+ * 背景:9.1 起 approveChannelPairingCode 等 pairing-store 函数不再从 plugin-sdk 导出,
5
+ * 官方改为宿主 Gateway RPC(channels.pairing.list / channels.pairing.approve),
6
+ * 且审批按 requestId(不透明 id)进行,不再暴露配对码 code。
7
+ * 本模块通过插件自有的 WSClient(已认证的 Gateway WebSocket)调用宿主 RPC,
8
+ * 实例由 ClawServices 初始化后注入(setGatewayClient)。
9
+ */
10
+ import type WSClient from './ws-client.js';
11
+ /** 注入 Gateway WebSocket 客户端(ClawServices.initialize 成功后调用) */
12
+ export declare function setGatewayClient(client: WSClient | null): void;
13
+ export declare function getGatewayClient(): WSClient | null;
14
+ /** channels.pairing.list 返回的单个配对请求(宿主 publicRequest 结构) */
15
+ export interface PairingRequestItem {
16
+ requestId: string;
17
+ channel: string;
18
+ channelLabel?: string;
19
+ accountId?: string;
20
+ senderId?: string;
21
+ senderLabel?: string;
22
+ metadata?: Record<string, string>;
23
+ createdAt?: string;
24
+ }
25
+ /** channels.pairing.list:列出指定通道/账号下 pending 的配对请求 */
26
+ export declare function listPairingRequests(channel: string, accountId: string): Promise<PairingRequestItem[]>;
27
+ /**
28
+ * channels.pairing.approve:审批配对请求
29
+ * notify=true 时宿主回调本通道 pairing.notifyApproval(→ dlClient.approved(true))
30
+ * 返回宿主响应 { requestId, senderId, notification, commandOwnerBootstrap }
31
+ */
32
+ export declare function approvePairingRequest(channel: string, accountId: string, requestId: string, notify?: boolean): Promise<{
33
+ requestId: string;
34
+ senderId?: string;
35
+ notification?: string;
36
+ commandOwnerBootstrap?: string;
37
+ }>;
@@ -0,0 +1,37 @@
1
+ let gwClient = null;
2
+ /** 注入 Gateway WebSocket 客户端(ClawServices.initialize 成功后调用) */
3
+ export function setGatewayClient(client) {
4
+ gwClient = client;
5
+ }
6
+ export function getGatewayClient() {
7
+ return gwClient;
8
+ }
9
+ /** channels.pairing.list:列出指定通道/账号下 pending 的配对请求 */
10
+ export async function listPairingRequests(channel, accountId) {
11
+ if (!gwClient) {
12
+ throw new Error('Gateway RPC 客户端未初始化(setGatewayClient 未调用)');
13
+ }
14
+ const res = await gwClient.callGatewayMethod('channels.pairing.list', {
15
+ channel,
16
+ accountId,
17
+ });
18
+ // 宿主返回 { accounts: [...], requests: [...] },requestId 在 requests 中
19
+ const requests = res?.requests ?? [];
20
+ return requests.filter((r) => r.accountId === accountId || !r.accountId);
21
+ }
22
+ /**
23
+ * channels.pairing.approve:审批配对请求
24
+ * notify=true 时宿主回调本通道 pairing.notifyApproval(→ dlClient.approved(true))
25
+ * 返回宿主响应 { requestId, senderId, notification, commandOwnerBootstrap }
26
+ */
27
+ export async function approvePairingRequest(channel, accountId, requestId, notify = true) {
28
+ if (!gwClient) {
29
+ throw new Error('Gateway RPC 客户端未初始化(setGatewayClient 未调用)');
30
+ }
31
+ return await gwClient.callGatewayMethod('channels.pairing.approve', {
32
+ channel,
33
+ accountId,
34
+ requestId,
35
+ notify,
36
+ });
37
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * CMD 消息处理器
3
+ * 集成到 duolian 的消息处理流程中
4
+ */
5
+ import { DLClient } from "../client/dl-client.js";
6
+ import { ClawServiceOptions, CmdMessage, CmdResponse } from "./types.js";
7
+ import { PluginLogger } from "openclaw/plugin-sdk/core";
8
+ /**
9
+ * CMD 消息处理器
10
+ */
11
+ export declare class CmdHandler {
12
+ private readonly dlClient;
13
+ private services;
14
+ private isInitialized;
15
+ constructor(config: ClawServiceOptions, dlClient: DLClient, logger: PluginLogger);
16
+ private log;
17
+ /**
18
+ * 初始化处理器
19
+ */
20
+ initialize(): Promise<boolean>;
21
+ /**
22
+ * 处理 CMD 消息
23
+ */
24
+ handleCmdMessage(rawMessage: CmdMessage): Promise<CmdResponse>;
25
+ /**
26
+ * 生成 CMD 响应消息(用于MQTT回复)
27
+ */
28
+ createCmdResponse(response: CmdResponse): any;
29
+ /**
30
+ * 清理资源
31
+ */
32
+ cleanup(): void;
33
+ /**
34
+ * 检查处理器是否就绪
35
+ */
36
+ isReady(): boolean;
37
+ }
38
+ /**
39
+ * 创建 CMD 处理器
40
+ */
41
+ export declare function createCmdHandler(config: ClawServiceOptions, dlClient: DLClient, logger: PluginLogger): CmdHandler;