dlinkclaw-openclaw 2026.10.19 → 2026.10.21

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/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dlinkclaw-openclaw",
3
- "version": "2026.10.19",
3
+ "version": "2026.10.21",
4
4
  "description": "DLinkClaw OpenClaw channel plugin — 朵联 OpenClaw 接入通道",
5
5
  "keywords": [
6
6
  "DLinkClaw",
@@ -147,7 +147,7 @@ export function checkConfigured(config) {
147
147
  * 1. npm install qrcode-terminal
148
148
  * 2. 取消顶部 import qrcode 的注释
149
149
  * 3. 取消底部 qrcode.generate 的注释
150
- * 4. 替换 registerUrl 为沃联实际注册地址
150
+ * 4. 替换 registerUrl 为朵联实际注册地址
151
151
  */
152
152
  function getDLIntroLines(registerUrl) {
153
153
  const lines = [
@@ -163,12 +163,14 @@ function getDLIntroLines(registerUrl) {
163
163
  // } catch (_) {
164
164
  // // 二维码生成失败,降级显示纯文本链接
165
165
  // }
166
- lines.push("📱 扫码或在浏览器打开以下链接注册沃联账号:", registerUrl, "", "━━━ 方式二:已有 APIKEY ━━━", "直接按提示输入即可。");
166
+ lines.push("📱 扫码或在浏览器打开以下链接注册朵联账号:", registerUrl, "", "━━━ 方式二:已有 APIKEY ━━━", "直接按提示输入即可。");
167
167
  return lines;
168
168
  }
169
169
  export function dlSetupWizard(channelId) {
170
170
  return {
171
171
  channel: channelId,
172
+ // 2026-10-22:先输入 BaseUrl(textInputs),再输入 API Key / Secret(credentials)
173
+ stepOrder: "text-first",
172
174
  status: {
173
175
  configuredLabel: "已配置 ✓",
174
176
  unconfiguredLabel: "需要 baseUrl, APIKey 和 Secret",
@@ -180,11 +182,11 @@ export function dlSetupWizard(channelId) {
180
182
  return configured;
181
183
  },
182
184
  resolveStatusLines: ({ configured }) => {
183
- return [`沃联 WorkRelay: ${configured ? "已配置" : "需要 baseUrl, APIKey 和 Secret"}`];
185
+ return [`朵联 WorkRelay: ${configured ? "已配置" : "需要 baseUrl, APIKey 和 Secret"}`];
184
186
  },
185
187
  },
186
188
  introNote: {
187
- title: "🔧 沃联 WorkRelay 设置向导",
189
+ title: "🔧 朵联 WorkRelay 设置向导",
188
190
  lines: getDLIntroLines(process.env.DL_REGISTER_URL || "https://WorkRelay.cn/register"),
189
191
  shouldShow: ({ cfg, accountId }) => {
190
192
  const account = resolveDLAccount({ cfg, accountId }, channelId);
@@ -192,31 +194,13 @@ export function dlSetupWizard(channelId) {
192
194
  },
193
195
  },
194
196
  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
197
  {
214
198
  inputKey: "token",
215
- providerHint: "沃联 API Key",
199
+ providerHint: "朵联 API Key",
216
200
  credentialLabel: "API Key",
217
201
  envPrompt: "使用环境变量中的 DL_APIKEY?",
218
202
  keepPrompt: "API Key 已配置,保留当前值?",
219
- inputPrompt: "请输入沃联 API Key:",
203
+ inputPrompt: "请输入朵联 API Key:",
220
204
  inspect: ({ cfg, accountId }) => {
221
205
  const account = resolveDLAccount({ cfg, accountId }, channelId);
222
206
  const hasValue = Boolean(account.config.apiKey?.trim());
@@ -230,11 +214,11 @@ export function dlSetupWizard(channelId) {
230
214
  },
231
215
  {
232
216
  inputKey: "privateKey",
233
- providerHint: "沃联签名密钥 (Secret)",
217
+ providerHint: "朵联签名密钥 (Secret)",
234
218
  credentialLabel: "Secret",
235
219
  envPrompt: "使用环境变量中的 DL_SECRET?",
236
220
  keepPrompt: "Secret 已配置,保留当前值?",
237
- inputPrompt: "请输入沃联签名密钥 (Secret):",
221
+ inputPrompt: "请输入朵联签名密钥 (Secret):",
238
222
  inspect: ({ cfg, accountId }) => {
239
223
  const account = resolveDLAccount({ cfg, accountId }, channelId);
240
224
  const hasValue = Boolean(account.config.secret?.trim());
@@ -246,7 +230,21 @@ export function dlSetupWizard(channelId) {
246
230
  },
247
231
  applySet: ({ cfg, accountId, resolvedValue }) => setDLChannelAccountParamsValue({ cfg, accountId, 'param': 'secret', value: resolvedValue }, channelId)
248
232
  },
249
- ]
233
+ ],
234
+ // 2026-10-22:BaseUrl 不是密钥,必须放 textInputs;放 credentials 会被向导掩码成 ***
235
+ textInputs: [
236
+ {
237
+ inputKey: "url",
238
+ message: "请输入朵联回调地址 (BaseUrl):",
239
+ placeholder: "https://your-dlink-server.example.com",
240
+ required: true,
241
+ currentValue: ({ cfg, accountId }) => {
242
+ const account = resolveDLAccount({ cfg, accountId }, channelId);
243
+ return account.config.baseUrl?.trim() || undefined;
244
+ },
245
+ applySet: ({ cfg, accountId, value }) => setDLChannelAccountParamsValue({ cfg, accountId, 'param': 'baseUrl', value }, channelId),
246
+ },
247
+ ],
250
248
  };
251
249
  }
252
250
  export function dlSetupAdapter(channelId) {
@@ -323,7 +321,7 @@ export function dlAccountStatus(channelId) {
323
321
  channel: channelId,
324
322
  accountId,
325
323
  kind: "config",
326
- message: "沃联 baseUrl, apiKey, secret 未配置",
324
+ message: "朵联 baseUrl, apiKey, secret 未配置",
327
325
  fix: `Run: openclaw channels add ${channelId} --base-url <baseUrl> --apiKey <apiKey> --secret <secret>`,
328
326
  });
329
327
  }
@@ -28,6 +28,8 @@ export interface MqttConfig {
28
28
  clientTopics: Array<string>;
29
29
  openUserTopics: Array<string>;
30
30
  openUserNotifyTopics: Array<string>;
31
+ /** 2026-10-21:团队创建人 empId(owner-only 能力校验) */
32
+ ownerEmpId?: string;
31
33
  }
32
34
  export declare class DLClient extends EventEmitter {
33
35
  private ctx;
@@ -66,6 +68,8 @@ export declare class DLClient extends EventEmitter {
66
68
  * 而本方法的目的只是刷新 openUserTopics——它是 publishEventToDLClient(L348)实时读取的,
67
69
  * 与 MQTT 订阅目标(clientTopics)、Gateway WS 通道都无关 → 只刷新配置即可,不断连、不重连。
68
70
  */
71
+ /** 2026-10-21:团队创建人(终端等 owner-only 能力使用) */
72
+ getOwnerEmpId(): string | undefined;
69
73
  refreshConfigAndReconnect(): Promise<void>;
70
74
  exeCmd(cmdMessage: CmdMessage): Promise<CmdResponse | undefined>;
71
75
  publishEventToDLClient(payload: any, event: EventType, sqos?: '0' | '1' | '2', to?: string): Promise<void>;
@@ -276,6 +276,10 @@ export class DLClient extends EventEmitter {
276
276
  * 而本方法的目的只是刷新 openUserTopics——它是 publishEventToDLClient(L348)实时读取的,
277
277
  * 与 MQTT 订阅目标(clientTopics)、Gateway WS 通道都无关 → 只刷新配置即可,不断连、不重连。
278
278
  */
279
+ /** 2026-10-21:团队创建人(终端等 owner-only 能力使用) */
280
+ getOwnerEmpId() {
281
+ return this.mqttConfig?.ownerEmpId;
282
+ }
279
283
  async refreshConfigAndReconnect() {
280
284
  try {
281
285
  await this.fetchConfig();
@@ -2,6 +2,7 @@ import { listDuolinkConfiguredAccounts, resolveDLAccount } from "./account.js";
2
2
  import { getDLLogger, getDLRuntime } from "./runtime.js";
3
3
  import { DLClient } from "./client/dl-client.js";
4
4
  import { checkDmPolicy, checkGroupPolicy } from "./policy.js";
5
+ import { bufferPendingMessage } from "./pending-pairing.js";
5
6
  import { buildMessageContext, parseMessageContent, routeAndDispatchMessage } from "./client/message.js";
6
7
  import { withTimeout } from "./client/timeout.js";
7
8
  import { getDLClient, removeDLClient, setDLClient } from "./client/client-manager.js";
@@ -165,6 +166,12 @@ async function duolinkMessageHandler(dlCtx, client) {
165
166
  logger
166
167
  });
167
168
  if (!dmPolicy.allowed) {
169
+ // 2026-10-22:首次未配对时原消息会被配对码回复取代而丢失,这里暂存,
170
+ // 由 pairing.approve 审批通过后补发(见 services.ts handleApprove)。
171
+ if (dmPolicy.pendingPairing) {
172
+ bufferPendingMessage(relayId, () => duolinkMessageHandler(dlCtx, client));
173
+ logger.info(`${accountLogInfo} message buffered until pairing approved, relayId=${relayId}`);
174
+ }
168
175
  client.publishToServer({
169
176
  streamId: msg.streamId,
170
177
  content: '[tag_error_policy_refuse]',
@@ -0,0 +1,7 @@
1
+ export declare function bufferPendingMessage(key: string, replay: () => Promise<void>): void;
2
+ /**
3
+ * 补发暂存消息,返回补发条数。
4
+ * key 为空时补发所有(兜底场景)。
5
+ */
6
+ export declare function flushPendingMessages(key?: string): number;
7
+ export declare function pendingMessageCount(key?: string): number;
@@ -0,0 +1,70 @@
1
+ const MAX_PER_KEY = 20;
2
+ const MAX_KEYS = 100;
3
+ const TTL_MS = 10 * 60 * 1000;
4
+ const pending = new Map();
5
+ function purgeExpired(now = Date.now()) {
6
+ for (const [key, list] of pending) {
7
+ const alive = list.filter((entry) => now - entry.at <= TTL_MS);
8
+ if (alive.length === 0) {
9
+ pending.delete(key);
10
+ }
11
+ else {
12
+ pending.set(key, alive);
13
+ }
14
+ }
15
+ }
16
+ export function bufferPendingMessage(key, replay) {
17
+ if (!key) {
18
+ return;
19
+ }
20
+ const now = Date.now();
21
+ purgeExpired(now);
22
+ const list = pending.get(key) ?? [];
23
+ list.push({ replay, at: now });
24
+ while (list.length > MAX_PER_KEY) {
25
+ list.shift();
26
+ }
27
+ pending.set(key, list);
28
+ while (pending.size > MAX_KEYS) {
29
+ const oldest = pending.keys().next().value;
30
+ if (oldest === undefined) {
31
+ break;
32
+ }
33
+ pending.delete(oldest);
34
+ }
35
+ }
36
+ /**
37
+ * 补发暂存消息,返回补发条数。
38
+ * key 为空时补发所有(兜底场景)。
39
+ */
40
+ export function flushPendingMessages(key) {
41
+ purgeExpired();
42
+ const keys = key ? [key] : Array.from(pending.keys());
43
+ let count = 0;
44
+ for (const k of keys) {
45
+ const list = pending.get(k);
46
+ if (!list || list.length === 0) {
47
+ continue;
48
+ }
49
+ pending.delete(k);
50
+ for (const entry of list) {
51
+ count += 1;
52
+ // 串行补发,避免同时打爆消息链路
53
+ Promise.resolve()
54
+ .then(() => entry.replay())
55
+ .catch(() => { });
56
+ }
57
+ }
58
+ return count;
59
+ }
60
+ export function pendingMessageCount(key) {
61
+ purgeExpired();
62
+ if (key) {
63
+ return pending.get(key)?.length ?? 0;
64
+ }
65
+ let total = 0;
66
+ for (const list of pending.values()) {
67
+ total += list.length;
68
+ }
69
+ return total;
70
+ }
@@ -7,6 +7,8 @@ export type DLGroupPolicyCheckResult = {
7
7
  export type DmPolicyCheckResult = {
8
8
  allowed: boolean;
9
9
  allowFrom?: Array<string | number>;
10
+ /** 2026-10-22:因未配对被拦截(消息可暂存,配对后补发) */
11
+ pendingPairing?: boolean;
10
12
  };
11
13
  export declare function checkGroupPolicy(params: {
12
14
  chatId: string;
@@ -174,7 +174,8 @@ export async function checkDmPolicy(params) {
174
174
  client.publishEventToDLClient(pairingEvent, 'onPairingRequest', '0').catch(err => logger.error(`${logPreTag}发送对码事件失败:${String(err)}`));
175
175
  }
176
176
  logger.warn(`account[${account.accountId}] sender=${senderId} access denied`);
177
- return { allowed: false };
177
+ // 2026-10-22:pairing 模式下拦截的消息可暂存,审批通过后补发
178
+ return { allowed: false, pendingPairing: dmPolicy === "pairing" };
178
179
  }
179
180
  /**
180
181
  * 设置 dmPolicy
@@ -18,7 +18,13 @@ export class CmdHandler {
18
18
  isInitialized = false;
19
19
  constructor(config, dlClient, logger) {
20
20
  this.dlClient = dlClient;
21
- this.services = new ClawServices(config, logger);
21
+ // 2026-10-21:终端输出经 MQTT 事件回推客户端(协议级能力,品牌无关)
22
+ this.services = new ClawServices(config, logger, (sessionId, data) => {
23
+ void this.dlClient.publishEventToDLClient({ sessionId, data }, 'onTerminal');
24
+ }, (payload) => {
25
+ // 2026-10-22:用量上报走服务端事件,由中继落库
26
+ void this.dlClient.publishEventToServer(payload, 'onUsage', '1');
27
+ });
22
28
  }
23
29
  log(msg, type) {
24
30
  this.services.log(msg, type);
@@ -104,13 +110,21 @@ export class CmdHandler {
104
110
  try {
105
111
  const sessionKey = payload.sessionKey;
106
112
  const message = payload.message;
113
+ // 2026-10-23:message 可能缺失
114
+ if (!message) {
115
+ return;
116
+ }
107
117
  const role = message.role;
108
118
  if (role !== 'assistant') {
109
119
  return;
110
120
  }
111
121
  //this.log(`onSessionMessage >>> ${JSON.stringify(payload)}`, 'warn');
112
122
  const stopReason = message.stopReason;
113
- const content = message.content ?? [];
123
+ // 2026-10-23:content 可能是字符串(非数组),直接 .map 会 TypeError
124
+ const rawContent = message.content;
125
+ const content = Array.isArray(rawContent)
126
+ ? rawContent
127
+ : (typeof rawContent === 'string' ? [{ type: 'text', text: rawContent }] : []);
114
128
  const result_contents = content.map(item => {
115
129
  if (item.type === 'thinking') {
116
130
  return item.thinking;
@@ -125,7 +139,8 @@ export class CmdHandler {
125
139
  if (content.length === 0) {
126
140
  return;
127
141
  }
128
- const label = payload.origin.label || undefined;
142
+ // 2026-10-23:origin 可能为空,直接取 .label 会 TypeError
143
+ const label = payload.origin?.label || undefined;
129
144
  if (!label) {
130
145
  this.log(`不能StreamId...${label}`, 'error');
131
146
  this.dlClient.publishEventToServer({
@@ -216,6 +231,16 @@ export class CmdHandler {
216
231
  else {
217
232
  this.log(`🔄CMD 指令:${rawMessage.cmd}, params=${JSON.stringify(rawMessage)}`, 'info');
218
233
  }
234
+ // 2026-10-21:终端能力仅限团队创建人(owner)。
235
+ // from 是客户端自己的 chat topic:usr_/<empId>/chat,与 ownerEmpId 比对即可。
236
+ if (typeof rawMessage.cmd === 'string' && rawMessage.cmd.startsWith('terminal.')) {
237
+ const owner = this.dlClient.getOwnerEmpId();
238
+ const expected = owner ? `usr_/${owner}/chat` : '';
239
+ if (!owner || rawMessage.from !== expected) {
240
+ this.log(`terminal denied: from=${rawMessage.from}, owner=${owner}`, 'warn');
241
+ return { success: false, error: 'terminal: forbidden (owner only)', timestamp: Date.now() };
242
+ }
243
+ }
219
244
  try {
220
245
  // 调用服务层处理
221
246
  return await this.services.handleCmdMessage(rawMessage);
@@ -5,6 +5,7 @@
5
5
  import EventEmitter from 'events';
6
6
  import { ClawServiceOptions, CmdMessage, CmdResponse } from './types.js';
7
7
  import { PluginLogger } from 'openclaw/plugin-sdk/core';
8
+ import { TerminalDataSink } from './terminal.js';
8
9
  /**
9
10
  * Claw-Services 主类
10
11
  * 处理 CMD 消息并调用相应的 OpenClaw API
@@ -14,7 +15,13 @@ export declare class ClawServices extends EventEmitter {
14
15
  private readonly options;
15
16
  private isInitialized;
16
17
  private readonly logger;
17
- constructor(options: ClawServiceOptions, logger: PluginLogger);
18
+ private readonly terminalManager;
19
+ /** 2026-10-22:token 用量上报(中继落库,按同事聚合) */
20
+ private readonly usagePublisher?;
21
+ private usageTimer;
22
+ constructor(options: ClawServiceOptions, logger: PluginLogger, terminalSink?: TerminalDataSink, usagePublisher?: (payload: Record<string, unknown>) => void);
23
+ /** 2026-10-22:每 60 秒把最近一天的 token 用量上报中继(幂等 upsert) */
24
+ private startUsageReporter;
18
25
  log(msg: string, type: 'info' | 'warn' | 'error' | 'debug'): void;
19
26
  /**
20
27
  * 初始化服务
@@ -50,6 +57,14 @@ export declare class ClawServices extends EventEmitter {
50
57
  */
51
58
  private handleCurrentSessionModel;
52
59
  private handleAgentList;
60
+ /**
61
+ * 2026-10-22:网关在 agents.create/update 后会做配置热加载,RPC 响应帧可能丢失,
62
+ * 插件侧 ws-client 默认要干等 30s,导致中继先超时。
63
+ * 这里对 RPC 加短超时,超时后由调用方回查 listAgents 确认是否已生效。
64
+ */
65
+ private rpcWithTimeout;
66
+ /** 回查 agent 是否已写入(热加载完成后可见) */
67
+ private findAgentConfirmed;
53
68
  private handleAgentCreate;
54
69
  private handleAgentUpdate;
55
70
  private handleAgentDelete;
@@ -112,6 +127,7 @@ export declare class ClawServices extends EventEmitter {
112
127
  * 新增 cmd 通道 agent.stop 指令
113
128
  */
114
129
  private handleStopAgent;
130
+ private handleSessionsAbort;
115
131
  private handlChatAbout;
116
132
  /**
117
133
  * 2026-09-06 适配 openclaw 2026.9.1:对码审批改走宿主 RPC channels.pairing.approve(按 requestId)
@@ -17,6 +17,8 @@ import { getSessionEntry, patchSessionEntry, resolveStorePath, listSessionEntrie
17
17
  import { setGatewayClient, approvePairingRequest } from './gateway-rpc.js';
18
18
  import { DEFAULT_ACCOUNT_ID } from 'openclaw/plugin-sdk/core';
19
19
  import { markRunStopped } from '../runtime.js';
20
+ import { flushPendingMessages } from '../pending-pairing.js';
21
+ import { TerminalManager } from './terminal.js';
20
22
  /**
21
23
  * Claw-Services 主类
22
24
  * 处理 CMD 消息并调用相应的 OpenClaw API
@@ -26,10 +28,50 @@ export class ClawServices extends EventEmitter {
26
28
  options;
27
29
  isInitialized = false;
28
30
  logger;
29
- constructor(options, logger) {
31
+ // 2026-10-21:终端会话管理(PTY / 管道)
32
+ terminalManager;
33
+ /** 2026-10-22:token 用量上报(中继落库,按同事聚合) */
34
+ usagePublisher;
35
+ usageTimer = null;
36
+ constructor(options, logger, terminalSink, usagePublisher) {
30
37
  super();
31
38
  this.options = options;
32
39
  this.logger = logger;
40
+ this.usagePublisher = usagePublisher;
41
+ this.terminalManager = new TerminalManager(terminalSink ?? (() => { }), (msg, type) => this.log(msg, type ?? 'info'));
42
+ }
43
+ /** 2026-10-22:每 60 秒把最近一天的 token 用量上报中继(幂等 upsert) */
44
+ startUsageReporter() {
45
+ if (this.usageTimer || !this.usagePublisher) {
46
+ return;
47
+ }
48
+ const tick = async () => {
49
+ try {
50
+ // 2026-10-23:上送绝对时间 + 本机时区,业务日由中继按部署时区归并;
51
+ // 查询窗口取近 48 小时,避免跨日/跨时区边界漏会话(窗口仅用于取数)
52
+ const capturedAt = Date.now();
53
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
54
+ const endDate = new Date().toISOString().slice(0, 10);
55
+ const startDate = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
56
+ const res = await this.rpcWithTimeout(this.client.getSessionsUsage({ agentScope: 'all', startDate, endDate, limit: 1000 }), 15000, 'sessions.usage');
57
+ const root = res?.data ?? res;
58
+ const sessions = Array.isArray(root?.sessions)
59
+ ? root.sessions
60
+ : Array.isArray(root?.aggregates)
61
+ ? root.aggregates
62
+ : [];
63
+ if (sessions.length === 0) {
64
+ return;
65
+ }
66
+ // relayId 由中继按 apiKey 补齐,这里不传
67
+ this.usagePublisher({ usageDate: endDate, capturedAt, timeZone, sessions });
68
+ }
69
+ catch (e) {
70
+ this.log(`usage reporter: ${String(e)}`, 'debug');
71
+ }
72
+ };
73
+ this.usageTimer = setInterval(() => { void tick(); }, 60_000);
74
+ void tick();
33
75
  }
34
76
  log(msg, type) {
35
77
  switch (type) {
@@ -104,6 +146,8 @@ export class ClawServices extends EventEmitter {
104
146
  });
105
147
  await this.client.connect();
106
148
  this.isInitialized = true;
149
+ // 2026-10-22:启动后开始周期上报用量
150
+ this.startUsageReporter();
107
151
  return true;
108
152
  }
109
153
  catch (error) {
@@ -266,12 +310,29 @@ export class ClawServices extends EventEmitter {
266
310
  // 新增 cmd 通道 chat.abort , 以上两个/stop, /queue interrupt都不能立即终止执行中的会话
267
311
  result = await this.handlChatAbout(cmdMessage.params);
268
312
  break;
313
+ case 'sessions.abort':
314
+ // 2026-10-24:会话级中止,无 runId 时也能立即停止,并丢弃排队后续
315
+ result = await this.handleSessionsAbort(cmdMessage.params);
316
+ break;
317
+ // 2026-10-21:终端能力(owner-only,见 CmdHandler 校验)
318
+ case 'terminal.open':
319
+ result = await this.terminalManager.open(cmdMessage.params);
320
+ break;
321
+ case 'terminal.input':
322
+ result = this.terminalManager.write(cmdMessage.params?.sessionId, cmdMessage.params?.data ?? '');
323
+ break;
324
+ case 'terminal.resize':
325
+ result = this.terminalManager.resize(cmdMessage.params?.sessionId, cmdMessage.params?.cols, cmdMessage.params?.rows);
326
+ break;
327
+ case 'terminal.close':
328
+ result = this.terminalManager.close(cmdMessage.params?.sessionId);
329
+ break;
269
330
  case 'pairing.approve':
270
331
  // 2026-09-06 适配 openclaw 2026.9.1:审批改按 requestId(宿主 RPC),客户端由 onPairingRequest 事件获取;
271
332
  // code 保留解析用于旧客户端兼容提示(9.1 起宿主 list 不再暴露 code,无法按 code 审批)
272
- const { requestId, code, empId, accountId } = cmdMessage.params ?? {};
333
+ const { requestId, code, empId, accountId, relayId } = cmdMessage.params ?? {};
273
334
  //注意:accountId目前只能为 DEFAULT_ACCOUNT_ID, 本插件不需要使用多账户,服务端提供多帐户服务(openId提供AGENT路由),在安装插件时需提醒使用"default"
274
- result = await this.handleApprove({ requestId, code, empId, accountId: accountId || DEFAULT_ACCOUNT_ID });
335
+ result = await this.handleApprove({ requestId, code, empId, relayId, accountId: accountId || DEFAULT_ACCOUNT_ID });
275
336
  break;
276
337
  // 工作区 / Artifact
277
338
  case 'artifacts.list':
@@ -330,10 +391,19 @@ export class ClawServices extends EventEmitter {
330
391
  * @param userId
331
392
  */
332
393
  async handleUpdateSessionModel(params) {
333
- const { sessionKey, provider, model } = params;
334
- if (!provider || provider.length === 0 || !sessionKey || sessionKey.length === 0) {
394
+ const { sessionKey, provider, model, thinkingLevel } = params;
395
+ if (!sessionKey || sessionKey.length === 0) {
335
396
  return null;
336
397
  }
398
+ // 2026-10-21:支持「只改推理强度」的 patch;provider/model 与 thinkingLevel 至少给一个
399
+ if ((!provider || provider.length === 0 || !model || model.length === 0) && !thinkingLevel) {
400
+ return null;
401
+ }
402
+ // 推理强度合法值校验(off/minimal/low/medium/high),非法值直接忽略
403
+ const THINK_LEVELS = ['off', 'minimal', 'low', 'medium', 'high'];
404
+ const thinkOverride = thinkingLevel && THINK_LEVELS.includes(thinkingLevel)
405
+ ? thinkingLevel
406
+ : undefined;
337
407
  const agentId = sessionKey.split(':')[1];
338
408
  const { cfg, channelId } = this.options;
339
409
  const storePath = resolveStorePath(cfg.session?.store, { agentId });
@@ -346,19 +416,25 @@ export class ClawServices extends EventEmitter {
346
416
  storePath,
347
417
  sessionKey: realKey,
348
418
  update: (entry) => {
349
- applyModelOverrideToSessionEntry({
350
- entry,
351
- selection: {
352
- provider: provider,
353
- model: model,
354
- },
355
- selectionSource: 'user',
356
- markLiveSwitchPending: true, // 让当前 run 立即生效
357
- });
419
+ if (provider && model) {
420
+ applyModelOverrideToSessionEntry({
421
+ entry,
422
+ selection: {
423
+ provider: provider,
424
+ model: model,
425
+ },
426
+ selectionSource: 'user',
427
+ markLiveSwitchPending: true, // 让当前 run 立即生效
428
+ });
429
+ }
430
+ // 2026-10-21:会话级推理强度写入(与模型覆盖同层持久化)
431
+ if (thinkOverride) {
432
+ entry.thinkingLevel = thinkOverride;
433
+ }
358
434
  return { ...entry };
359
435
  },
360
436
  });
361
- return { provider, model };
437
+ return { provider, model, thinkingLevel: thinkOverride };
362
438
  }
363
439
  /**
364
440
  * 2026-08-28 解析 direct 会话的真实 sessionKey(兼容 chatId 后缀)
@@ -369,11 +445,12 @@ export class ClawServices extends EventEmitter {
369
445
  * 策略:先精确匹配传入 key;查不到 entry 时,用前缀匹配 store 中 updatedAt 最新的 direct 会话。
370
446
  */
371
447
  resolveDirectSessionKey(storePath, sessionKey, agentId, channelId) {
372
- // 1. 传入 key 直接命中 entry → 直接用
448
+ // 1. 精确匹配先记下来,但不直接返回:旧的无后缀 key 可能仍存在,
449
+ // 直接写入会命中过期会话(表现为「切了模型但下一条消息仍用旧模型」)。
373
450
  const exact = getSessionEntry({ storePath, sessionKey });
374
- if (exact) {
375
- return sessionKey;
376
- }
451
+ const exactUpdated = exact
452
+ ? Number(exact.updatedAt ?? exact.lastActivityAt ?? 0)
453
+ : -1;
377
454
  // 2. 前缀匹配:agent:<agentId>:<channelId>:direct:<...> 开头的最新会话
378
455
  const prefix = `agent:${agentId}:${channelId}:direct:`;
379
456
  try {
@@ -389,8 +466,9 @@ export class ClawServices extends EventEmitter {
389
466
  bestKey = s.sessionKey;
390
467
  }
391
468
  }
392
- if (bestKey) {
393
- this.log(`resolveDirectSessionKey: ${sessionKey} -> ${bestKey} (前缀匹配最新会话)`, 'info');
469
+ // 3. 谁更新得更晚就用谁:避免旧 exact 会话把模型覆盖写丢
470
+ if (bestKey && bestUpdated > exactUpdated) {
471
+ this.log(`resolveDirectSessionKey: ${sessionKey} -> ${bestKey} (更晚更新的会话)`, 'info');
394
472
  return bestKey;
395
473
  }
396
474
  }
@@ -417,13 +495,16 @@ export class ClawServices extends EventEmitter {
417
495
  if (!entry)
418
496
  return null;
419
497
  // 只返回用户主动设置的覆盖,不返回 auto fallback
498
+ const result = {};
420
499
  if (entry.modelOverrideSource === 'user' && entry.modelOverride) {
421
- return {
422
- provider: entry.providerOverride ?? '',
423
- model: entry.modelOverride,
424
- };
500
+ result.provider = entry.providerOverride ?? '';
501
+ result.model = entry.modelOverride;
425
502
  }
426
- return null;
503
+ // 2026-10-21:会话级推理强度回显
504
+ if (entry.thinkingLevel) {
505
+ result.thinkingLevel = entry.thinkingLevel;
506
+ }
507
+ return Object.keys(result).length > 0 ? result : null;
427
508
  }
428
509
  //#region ==================== Agent 管理处理器 ====================
429
510
  async handleAgentList() {
@@ -439,6 +520,43 @@ export class ClawServices extends EventEmitter {
439
520
  sessionCount: 0 // 需要额外API调用获取
440
521
  }));
441
522
  }
523
+ /**
524
+ * 2026-10-22:网关在 agents.create/update 后会做配置热加载,RPC 响应帧可能丢失,
525
+ * 插件侧 ws-client 默认要干等 30s,导致中继先超时。
526
+ * 这里对 RPC 加短超时,超时后由调用方回查 listAgents 确认是否已生效。
527
+ */
528
+ rpcWithTimeout(p, ms, label) {
529
+ // 避免落败 promise 产生 unhandledRejection
530
+ void p.catch(() => { });
531
+ return new Promise((resolve, reject) => {
532
+ const timer = setTimeout(() => reject(new Error(`${label} rpc timeout after ${ms}ms`)), ms);
533
+ p.then((v) => { clearTimeout(timer); resolve(v); }, (e) => { clearTimeout(timer); reject(e); });
534
+ });
535
+ }
536
+ /** 回查 agent 是否已写入(热加载完成后可见) */
537
+ async findAgentConfirmed(params) {
538
+ const wantedId = (params.id ?? '').toLowerCase();
539
+ const wantedName = (params.name ?? '').toLowerCase();
540
+ for (let i = 0; i < 6; i++) {
541
+ try {
542
+ const agents = await this.rpcWithTimeout(this.client.listAgents(), 5000, 'agents.list');
543
+ const found = agents.agents.find((a) => {
544
+ const id = (a.id ?? '').toLowerCase();
545
+ const nm = (a.name ?? '').toLowerCase();
546
+ return (wantedId.length > 0 && (id === wantedId || nm === wantedId))
547
+ || (wantedName.length > 0 && (nm === wantedName || id === wantedName));
548
+ });
549
+ if (found) {
550
+ return found;
551
+ }
552
+ }
553
+ catch {
554
+ // 热加载期间 list 可能失败,稍后重试
555
+ }
556
+ await new Promise((r) => setTimeout(r, 800));
557
+ }
558
+ return undefined;
559
+ }
442
560
  async handleAgentCreate(params) {
443
561
  const workspace_path = getOpenclawPath('workspace', params.id.toLowerCase().replace(/\s+/g, '-'));
444
562
  const provider = params.provider ?? '';
@@ -450,23 +568,36 @@ export class ClawServices extends EventEmitter {
450
568
  avatar: (params.avatar && params.avatar.length > 0) ? this.options.baseUrl + params.avatar : undefined,
451
569
  model: provider.length > 0 ? `${provider}/${params.model}` : params.model,
452
570
  };
453
- const result = await this.client.createAgent(createParams);
571
+ let result;
572
+ try {
573
+ result = await this.rpcWithTimeout(this.client.createAgent(createParams), 8000, 'agents.create');
574
+ }
575
+ catch (e) {
576
+ // 响应可能因热加载丢失,但配置已写入 → 回查确认
577
+ const confirmed = await this.findAgentConfirmed(params);
578
+ if (!confirmed) {
579
+ throw e;
580
+ }
581
+ this.log(`agents.create 响应超时但已生效:${confirmed.id}`, 'warn');
582
+ result = { agentId: confirmed.id };
583
+ }
454
584
  // 2026-09-18 修复(石头哥报:首次点击创建失败,且 name 未写入 openclaw.json):
455
585
  // agents.create 后网关是「异步热加载配置」的(日志:create 186ms 成功,紧跟的
456
586
  // agents.update 报 agent "xiaoha" not found,1 秒后 config hot reload applied)。
457
587
  // 这里改用「update 快速失败重试」:agents.update 对未就绪 agent 是 2ms 快速返回 not found
458
588
  // (ws-client.send 在 WS 未连接时也快速抛错),不会挂起;热加载完成后下一次即成功。
459
589
  // 避免原 waitAgentReady 用 listAgents 轮询导致客户端 loading 超时的问题。
460
- const MAX_UPDATE_RETRY = 10;
590
+ // 2026-10-22:单次 update 已加 4s 超时,重试次数收紧,避免最坏情况超过中继 30s
591
+ const MAX_UPDATE_RETRY = 3;
461
592
  const UPDATE_RETRY_MS = 500;
462
593
  let nameWritten = false;
463
594
  for (let i = 0; i < MAX_UPDATE_RETRY && !nameWritten; i++) {
464
595
  try {
465
- const up = await this.client.updateAgent({
596
+ const up = await this.rpcWithTimeout(this.client.updateAgent({
466
597
  agentId: result.agentId,
467
598
  name: params.name,
468
599
  workspace: workspace_path,
469
- });
600
+ }), 4000, 'agents.update');
470
601
  if (up?.ok) {
471
602
  nameWritten = true;
472
603
  }
@@ -493,25 +624,45 @@ export class ClawServices extends EventEmitter {
493
624
  };
494
625
  }
495
626
  async handleAgentUpdate(params) {
496
- const agents = await this.client.listAgents();
627
+ let agents;
628
+ try {
629
+ agents = await this.rpcWithTimeout(this.client.listAgents(), 5000, 'agents.list');
630
+ }
631
+ catch {
632
+ agents = { agents: [] };
633
+ }
497
634
  const agent = agents.agents.find(a => a.id === params.agentId);
498
635
  const provider = params.provider ?? '';
499
636
  if (agent) {
500
637
  //agent存在直接更新
501
- const result = await this.client.updateAgent({
502
- agentId: params.agentId,
503
- name: params.name,
504
- workspace: params.workspace,
505
- model: provider.length > 0 ? `${provider}/${params.model}` : params.model,
506
- avatar: params.avatar
507
- });
638
+ let result;
639
+ try {
640
+ result = await this.rpcWithTimeout(this.client.updateAgent({
641
+ agentId: params.agentId,
642
+ name: params.name,
643
+ workspace: params.workspace,
644
+ model: provider.length > 0 ? `${provider}/${params.model}` : params.model,
645
+ avatar: params.avatar
646
+ }), 8000, 'agents.update');
647
+ }
648
+ catch (e) {
649
+ // 响应可能因热加载丢失,但配置已写入 → 回查确认
650
+ const confirmed = await this.findAgentConfirmed({ id: params.agentId, name: params.name });
651
+ if (!confirmed) {
652
+ throw e;
653
+ }
654
+ this.log(`agents.update 响应超时但已生效:${confirmed.id}`, 'warn');
655
+ result = { agentId: confirmed.id };
656
+ }
657
+ // 兼容旧结构:原来的 updateAgent 调用以 result 收尾,这里保持后续逻辑
658
+ const legacyResult = result;
508
659
  const sessionKey = `agent:${params.agentId}:${this.options.channelId}:direct:${params.userId.toLowerCase()}`;
509
660
  await this.handleUpdateSessionModel({
510
661
  sessionKey,
511
662
  provider: provider,
512
663
  model: params.model
513
664
  });
514
- return { id: result.agentId };
665
+ return { id: legacyResult.agentId };
515
666
  }
516
667
  //不存在直接创建一个
517
668
  const p = {
@@ -821,6 +972,13 @@ export class ClawServices extends EventEmitter {
821
972
  sessionKey
822
973
  });
823
974
  }
975
+ async handleSessionsAbort(params) {
976
+ const { sessionKey } = params;
977
+ if (!sessionKey) {
978
+ throw new Error('sessions.abort 缺少 sessionKey 参数');
979
+ }
980
+ return await this.client?.sessionsAbort({ sessionKey });
981
+ }
824
982
  async handlChatAbout(params) {
825
983
  const { sessionKey, runId } = params;
826
984
  if (!sessionKey) {
@@ -843,7 +1001,7 @@ export class ClawServices extends EventEmitter {
843
1001
  * 服务端暂无该端点会 404 噪音);服务端回调沿用下方 onPairingResult MQTT 事件(7.1 同款链路)
844
1002
  */
845
1003
  async handleApprove(params) {
846
- const { requestId, code, empId, accountId } = params;
1004
+ const { requestId, code, empId, accountId, relayId } = params;
847
1005
  if (!requestId) {
848
1006
  // 旧客户端只传 code:9.1 无公开按 code 审批 API,提示升级(重新对码后事件会带 requestId)
849
1007
  throw new Error(`审批失败:缺少 requestId${code ? `(code=${code} 已不被支持)` : ''},请重新触发对码后重试`);
@@ -856,10 +1014,13 @@ export class ClawServices extends EventEmitter {
856
1014
  empId,
857
1015
  requestId,
858
1016
  });
1017
+ // 2026-10-22:补发该中继在未配对期间被拦截的消息;返回条数供客户端决定是否兜底重发
1018
+ const replayed = relayId ? flushPendingMessages(relayId) : 0;
859
1019
  return {
860
1020
  success: true,
861
1021
  data: {
862
- ...res
1022
+ ...res,
1023
+ replayed
863
1024
  },
864
1025
  timestamp: Date.now()
865
1026
  };
@@ -0,0 +1,39 @@
1
+ export type TerminalDataSink = (sessionId: string, data: string) => void;
2
+ export type TerminalLogger = (msg: string, type?: 'info' | 'warn' | 'error' | 'debug') => void;
3
+ export declare class TerminalManager {
4
+ private readonly sink;
5
+ private readonly log;
6
+ private readonly sessions;
7
+ private ptyModule;
8
+ constructor(sink: TerminalDataSink, log: TerminalLogger);
9
+ private defaultCwd;
10
+ private append;
11
+ private touch;
12
+ open(params: {
13
+ sessionId?: string;
14
+ cwd?: string;
15
+ cols?: number;
16
+ rows?: number;
17
+ }): Promise<{
18
+ sessionId: string;
19
+ mode: string;
20
+ reused: boolean;
21
+ replay: string;
22
+ } | {
23
+ sessionId: string;
24
+ mode: string;
25
+ reused?: undefined;
26
+ replay?: undefined;
27
+ }>;
28
+ write(sessionId: string, data: string): {
29
+ sessionId: string;
30
+ };
31
+ resize(sessionId: string, cols: number, rows: number): {
32
+ sessionId: string;
33
+ };
34
+ close(sessionId: string, alreadyExited?: boolean): {
35
+ sessionId: string;
36
+ closed: boolean;
37
+ };
38
+ disposeAll(): void;
39
+ }
@@ -0,0 +1,217 @@
1
+ /**
2
+ * 2026-10-21:终端能力(协议级,非 OpenClaw 专属)。
3
+ * 通过 PTY(可用时)或 child_process 运行登录 shell,输出经 sink 回推客户端。
4
+ * 其他品牌插件(如 dsh)实现同一套 terminal.* 契约即可。
5
+ */
6
+ import { spawn } from 'child_process';
7
+ import os from 'os';
8
+ import path from 'path';
9
+ const FLUSH_MS = 16;
10
+ const IDLE_MS = 30 * 60 * 1000;
11
+ /** 重新 attach 时回放的最大输出量 */
12
+ const HISTORY_LIMIT = 64 * 1024;
13
+ /** 优先用 PTY(交互式 shell),不可用时退回管道模式 */
14
+ async function loadPty() {
15
+ for (const name of ['node-pty', '@lydell/node-pty']) {
16
+ try {
17
+ const mod = await import(name);
18
+ if (mod) {
19
+ return mod.default ?? mod;
20
+ }
21
+ }
22
+ catch {
23
+ // 继续尝试下一个
24
+ }
25
+ }
26
+ return null;
27
+ }
28
+ export class TerminalManager {
29
+ sink;
30
+ log;
31
+ sessions = new Map();
32
+ ptyModule;
33
+ constructor(sink, log) {
34
+ this.sink = sink;
35
+ this.log = log;
36
+ }
37
+ defaultCwd() {
38
+ return path.join(os.homedir(), '.openclaw', 'workspace');
39
+ }
40
+ append(session, chunk) {
41
+ if (session.closed || !chunk) {
42
+ return;
43
+ }
44
+ session.buffer += chunk;
45
+ if (session.history.length < HISTORY_LIMIT) {
46
+ session.history += chunk;
47
+ if (session.history.length > HISTORY_LIMIT) {
48
+ session.history = session.history.slice(-HISTORY_LIMIT);
49
+ }
50
+ }
51
+ if (session.flushTimer) {
52
+ return;
53
+ }
54
+ // 16ms 合并,避免高频输出打爆 MQTT
55
+ session.flushTimer = setTimeout(() => {
56
+ session.flushTimer = null;
57
+ const data = session.buffer;
58
+ session.buffer = '';
59
+ if (data) {
60
+ try {
61
+ this.sink(session.id, data);
62
+ }
63
+ catch (e) {
64
+ this.log(`terminal sink failed: ${String(e)}`, 'warn');
65
+ }
66
+ }
67
+ }, FLUSH_MS);
68
+ }
69
+ touch(session) {
70
+ if (session.idleTimer) {
71
+ clearTimeout(session.idleTimer);
72
+ }
73
+ session.idleTimer = setTimeout(() => {
74
+ this.log(`terminal session idle timeout: ${session.id}`, 'info');
75
+ this.close(session.id);
76
+ }, IDLE_MS);
77
+ }
78
+ async open(params) {
79
+ const sessionId = params.sessionId;
80
+ if (!sessionId) {
81
+ throw new Error('terminal.open: sessionId required');
82
+ }
83
+ const existing = this.sessions.get(sessionId);
84
+ if (existing) {
85
+ // 2026-10-21:客户端切 Tab 后重新 attach,回放最近输出,保持会话连续
86
+ this.touch(existing);
87
+ return {
88
+ sessionId,
89
+ mode: existing.isPty ? 'pty' : 'pipe',
90
+ reused: true,
91
+ replay: existing.history,
92
+ };
93
+ }
94
+ if (this.ptyModule === undefined) {
95
+ this.ptyModule = await loadPty();
96
+ if (!this.ptyModule) {
97
+ this.log('node-pty unavailable, fallback to pipe mode (no TTY)', 'warn');
98
+ }
99
+ }
100
+ const cwd = params.cwd && params.cwd.length > 0 ? params.cwd : this.defaultCwd();
101
+ const cols = params.cols && params.cols > 0 ? params.cols : 120;
102
+ const rows = params.rows && params.rows > 0 ? params.rows : 30;
103
+ const env = { ...process.env, TERM: 'xterm-256color', LANG: process.env.LANG ?? 'C.UTF-8' };
104
+ let proc;
105
+ let isPty = false;
106
+ if (this.ptyModule?.spawn) {
107
+ proc = this.ptyModule.spawn('bash', ['-il'], {
108
+ name: 'xterm-256color',
109
+ cols,
110
+ rows,
111
+ cwd,
112
+ env,
113
+ });
114
+ isPty = true;
115
+ }
116
+ else {
117
+ proc = spawn('bash', ['-il'], { cwd, env, stdio: 'pipe' });
118
+ isPty = false;
119
+ }
120
+ const session = {
121
+ id: sessionId,
122
+ proc,
123
+ isPty,
124
+ buffer: '',
125
+ history: '',
126
+ flushTimer: null,
127
+ idleTimer: null,
128
+ closed: false,
129
+ };
130
+ this.sessions.set(sessionId, session);
131
+ this.touch(session);
132
+ const onData = (data) => {
133
+ this.touch(session);
134
+ this.append(session, typeof data === 'string' ? data : data.toString('utf8'));
135
+ };
136
+ if (isPty) {
137
+ proc.onData(onData);
138
+ proc.onExit((e) => {
139
+ this.log(`terminal exited: ${sessionId}, code=${e?.exitCode}`, 'info');
140
+ this.close(sessionId, true);
141
+ });
142
+ }
143
+ else {
144
+ proc.stdout?.on('data', onData);
145
+ proc.stderr?.on('data', onData);
146
+ proc.on('exit', (code) => {
147
+ this.log(`terminal exited: ${sessionId}, code=${code}`, 'info');
148
+ this.close(sessionId, true);
149
+ });
150
+ proc.on('error', (err) => {
151
+ this.log(`terminal error: ${sessionId}, ${err.message}`, 'error');
152
+ this.append(session, `\r\n[terminal error] ${err.message}\r\n`);
153
+ });
154
+ }
155
+ this.log(`terminal opened: ${sessionId}, mode=${isPty ? 'pty' : 'pipe'}, cwd=${cwd}`, 'info');
156
+ return { sessionId, mode: isPty ? 'pty' : 'pipe' };
157
+ }
158
+ write(sessionId, data) {
159
+ const session = this.sessions.get(sessionId);
160
+ if (!session || session.closed) {
161
+ throw new Error('terminal session not found');
162
+ }
163
+ this.touch(session);
164
+ if (session.isPty) {
165
+ session.proc.write(data);
166
+ }
167
+ else {
168
+ session.proc.stdin?.write(data);
169
+ }
170
+ return { sessionId };
171
+ }
172
+ resize(sessionId, cols, rows) {
173
+ const session = this.sessions.get(sessionId);
174
+ if (!session || session.closed) {
175
+ throw new Error('terminal session not found');
176
+ }
177
+ if (session.isPty && session.proc.resize) {
178
+ session.proc.resize(cols, rows);
179
+ }
180
+ return { sessionId };
181
+ }
182
+ close(sessionId, alreadyExited = false) {
183
+ const session = this.sessions.get(sessionId);
184
+ if (!session) {
185
+ return { sessionId, closed: false };
186
+ }
187
+ session.closed = true;
188
+ if (session.flushTimer) {
189
+ clearTimeout(session.flushTimer);
190
+ session.flushTimer = null;
191
+ }
192
+ if (session.idleTimer) {
193
+ clearTimeout(session.idleTimer);
194
+ session.idleTimer = null;
195
+ }
196
+ if (!alreadyExited) {
197
+ try {
198
+ if (session.isPty) {
199
+ session.proc.kill();
200
+ }
201
+ else {
202
+ session.proc.kill('SIGHUP');
203
+ }
204
+ }
205
+ catch {
206
+ // 忽略
207
+ }
208
+ }
209
+ this.sessions.delete(sessionId);
210
+ return { sessionId, closed: true };
211
+ }
212
+ disposeAll() {
213
+ for (const id of Array.from(this.sessions.keys())) {
214
+ this.close(id);
215
+ }
216
+ }
217
+ }
@@ -306,7 +306,7 @@ export interface CmdResult {
306
306
  timestamp: string | number;
307
307
  timezone: string;
308
308
  }
309
- export type CmdType = 'agent.ask' | 'agent.list' | 'agent.create' | 'agent.update' | 'agent.delete' | 'agent.get' | 'agent.stop' | 'chat.abort' | 'task.list' | 'task.create' | 'task.update' | 'task.delete' | 'task.run' | 'status.health' | 'status.system' | 'status.usage' | 'models.list' | 'sessions.list' | 'sessions.get' | 'sessions.describe' | 'sessions.resolve' | 'sessions.create' | 'sessions.archive' | 'sessions.restore' | 'sessions.delete' | 'sessions.reset' | 'sessions.patch' | 'sessions.currentModel' | 'chat.history' | 'chat.trajectory' | 'skills.status' | 'skills.search' | 'skills.detail' | 'skills.install' | 'skills.update' | 'skills.securityVerdicts' | 'cancelMessage' | 'usage.cost' | 'sessions.usage' | 'sessions.usage.logs' | 'pairing.approve' | 'artifacts.list' | 'artifacts.get' | 'artifacts.download' | 'agents.files.list' | 'agents.files.get' | 'extCapabilities.status' | 'extCapabilities.check' | 'extCapabilities.install';
309
+ export type CmdType = 'agent.ask' | 'agent.list' | 'agent.create' | 'agent.update' | 'agent.delete' | 'agent.get' | 'agent.stop' | 'chat.abort' | 'task.list' | 'task.create' | 'task.update' | 'task.delete' | 'task.run' | 'status.health' | 'status.system' | 'status.usage' | 'models.list' | 'sessions.list' | 'sessions.get' | 'sessions.describe' | 'sessions.resolve' | 'sessions.create' | 'sessions.archive' | 'sessions.restore' | 'sessions.delete' | 'sessions.reset' | 'sessions.patch' | 'sessions.currentModel' | 'sessions.abort' | 'terminal.open' | 'terminal.input' | 'terminal.resize' | 'terminal.close' | 'chat.history' | 'chat.trajectory' | 'skills.status' | 'skills.search' | 'skills.detail' | 'skills.install' | 'skills.update' | 'skills.securityVerdicts' | 'cancelMessage' | 'usage.cost' | 'sessions.usage' | 'sessions.usage.logs' | 'pairing.approve' | 'artifacts.list' | 'artifacts.get' | 'artifacts.download' | 'agents.files.list' | 'agents.files.get' | 'extCapabilities.status' | 'extCapabilities.check' | 'extCapabilities.install';
310
310
  export type ReadlineType = Extract<CmdType, 'chat.trajectory'>;
311
311
  export interface ClawMessage {
312
312
  type: 'res' | 'event';
@@ -316,11 +316,15 @@ export interface ClawMessage {
316
316
  payload: any;
317
317
  error?: any;
318
318
  }
319
- export type EventType = 'onTick' | 'onHealth' | 'onHandshaked' | 'onConnectState' | 'onError' | 'onAgentReply' | 'onAgentChat' | 'onChatTrajectory' | 'onCron' | 'onShutdown' | 'onSessionsChanged' | 'onSkillInstalled' | 'onSkillInstalling' | 'onSkillError' | 'onPairingRequest' | 'onPairingResult';
319
+ export type EventType = 'onTick' | 'onHealth' | 'onHandshaked' | 'onConnectState' | 'onError' | 'onAgentReply' | 'onAgentChat' | 'onChatTrajectory' | 'onCron' | 'onShutdown' | 'onSessionsChanged' | 'onSkillInstalled' | 'onSkillInstalling' | 'onSkillError' | 'onPairingRequest' | 'onPairingResult'
320
+ /** 2026-10-22:周期上报 token 用量 */
321
+ | 'onUsage' | 'onTerminal';
320
322
  export type CmdParamsPatchSessionEntry = {
321
323
  sessionKey: string;
322
324
  provider?: string;
323
- model: string;
325
+ model?: string;
326
+ /** 2026-10-21:会话级推理强度(off/minimal/low/medium/high),与模型选择同通道 */
327
+ thinkingLevel?: string;
324
328
  };
325
329
  export interface SkillInfo {
326
330
  name: string;
@@ -390,3 +394,21 @@ export interface AgentFilesGetParams {
390
394
  agentId: string;
391
395
  name: string;
392
396
  }
397
+ export type TerminalOpenParams = {
398
+ sessionId: string;
399
+ cwd?: string;
400
+ cols?: number;
401
+ rows?: number;
402
+ };
403
+ export type TerminalInputParams = {
404
+ sessionId: string;
405
+ data: string;
406
+ };
407
+ export type TerminalResizeParams = {
408
+ sessionId: string;
409
+ cols: number;
410
+ rows: number;
411
+ };
412
+ export type TerminalCloseParams = {
413
+ sessionId: string;
414
+ };
@@ -141,7 +141,11 @@ export declare class WSClient extends EventEmitter {
141
141
  }): Promise<any>;
142
142
  chatAbort(params: {
143
143
  sessionKey: string;
144
- runId: string;
144
+ runId?: string;
145
+ }): Promise<any>;
146
+ /** 2026-10-24:会话级中止(无 runId 时使用),同时丢弃排队后续 */
147
+ sessionsAbort(params: {
148
+ sessionKey: string;
145
149
  }): Promise<any>;
146
150
  getSkillSecurityVerdicts(agentId?: string): Promise<any>;
147
151
  getUsageCost(params: Record<string, unknown>): Promise<any>;
@@ -460,10 +460,19 @@ export class WSClient extends EventEmitter {
460
460
  method: 'chat.abort',
461
461
  params: {
462
462
  sessionKey,
463
- runId
463
+ ...(runId ? { runId } : {})
464
464
  }
465
465
  });
466
466
  }
467
+ /** 2026-10-24:会话级中止(无 runId 时使用),同时丢弃排队后续 */
468
+ async sessionsAbort(params) {
469
+ const { sessionKey } = params;
470
+ return this.sendRequest({
471
+ type: 'req',
472
+ method: 'sessions.abort',
473
+ params: { sessionKey }
474
+ });
475
+ }
467
476
  async getSkillSecurityVerdicts(agentId) {
468
477
  const params = {};
469
478
  if (agentId)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dlinkclaw-openclaw",
3
- "version": "2026.10.19",
3
+ "version": "2026.10.21",
4
4
  "description": "DLinkClaw OpenClaw channel plugin — 朵联 OpenClaw 接入通道",
5
5
  "keywords": [
6
6
  "DLinkClaw",