dlinkclaw-openclaw 2026.10.18 → 2026.10.20
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/index.js +1 -1
- package/dist/package.json +2 -2
- package/dist/src/client/dl-client.d.ts +4 -0
- package/dist/src/client/dl-client.js +32 -12
- package/dist/src/client/message.js +2 -2
- package/dist/src/client/types.d.ts +1 -1
- package/dist/src/services/index.js +15 -2
- package/dist/src/services/services.d.ts +3 -1
- package/dist/src/services/services.js +53 -18
- package/dist/src/services/terminal.d.ts +39 -0
- package/dist/src/services/terminal.js +217 -0
- package/dist/src/services/types.d.ts +23 -3
- package/package.json +95 -95
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{emptyPluginConfigSchema as e}from"openclaw/plugin-sdk/core";import{channelMeta as n,dlChannel as t,LOG_TAG as r}from"./src/channel.js";import{setDLLogger as o,setDLRuntime as i,isRunStopped as s}from"./src/runtime.js";const l=n;
|
|
1
|
+
import{emptyPluginConfigSchema as e}from"openclaw/plugin-sdk/core";import{channelMeta as n,dlChannel as t,LOG_TAG as r}from"./src/channel.js";import{setDLLogger as o,setDLRuntime as i,isRunStopped as s}from"./src/runtime.js";const l=n;l.isDebug&&(process.env.NODE_TLS_REJECT_UNAUTHORIZED="0",process.env.NODE_ENV="dev");const a={id:l.id,name:l.label,description:l.blurb,configSchema:e(),register(e){i(e.runtime),o(e.logger,r),e.registerChannel({plugin:t}),e.on("before_tool_call",(e,n)=>{const t=l.id;if(n.channelId===t)return e.runId&&s(e.runId)?{block:!0,blockReason:"用户已点击停止,任务终止执行"}:void 0}),e.on("before_prompt_build",(n,t)=>{const r=l.id;if(e.logger.warn(`ctx.channel=${t.channel}, ctx.channelId=${t.channelId}, channelId=${r}`),t.channel===r||t.channelId===r)return{prependSystemContext:["[AGENT CODE OF CONDUCT - ALWAYS FOLLOW]","1. ASK BEFORE GUESSING: If the task is incomplete or has multiple reasonable interpretations, send a one-line confirmation first. Never guess.",'2. STOP ON FAILURE: If the same problem fails 2-3 times in a row, stop and report: "I am stuck: <reason>. Please advise." Never retry endlessly.','3. ALWAYS REPORT COMPLETION: Every task must end with a completion signal. After sending media/files, always follow up with a short text confirmation (e.g. "✅ Sent 3 files: a.png / b.pdf / c.txt"). The client shows a spinner until it receives the final text reply.'].join("\n")}})}};export default a;
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dlinkclaw-openclaw",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.20",
|
|
4
4
|
"description": "DLinkClaw OpenClaw channel plugin — 朵联 OpenClaw 接入通道",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"DLinkClaw",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
],
|
|
77
77
|
"order": 90,
|
|
78
78
|
"quickstartAllowFrom": true,
|
|
79
|
-
"isDebug":
|
|
79
|
+
"isDebug": false,
|
|
80
80
|
"preferSessionLookupForAnnounceTarget": true
|
|
81
81
|
},
|
|
82
82
|
"install": {
|
|
@@ -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>;
|
|
@@ -231,24 +231,40 @@ export class DLClient extends EventEmitter {
|
|
|
231
231
|
await this.changeConnectStatus('start');
|
|
232
232
|
}
|
|
233
233
|
async stop() {
|
|
234
|
+
// 2026-10-19:关闭阶段必须有界。MQTT end 回调或 HTTP 上报只要有一个不返回,
|
|
235
|
+
// 网关的 drain 就会一直等(systemd TimeoutStopSec 默认 15 分钟),表现为重启卡死。
|
|
234
236
|
if (this.mqttClient) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
237
|
+
await Promise.race([
|
|
238
|
+
new Promise((resolve) => {
|
|
239
|
+
this.mqttClient.end(true, () => {
|
|
240
|
+
this.mqttClient = null;
|
|
241
|
+
this.logger.warn(`DLClient account[${this.ctx.account.accountId}] Disconnected`);
|
|
242
|
+
if (this.cmdHandler) {
|
|
243
|
+
this.cmdHandler.cleanup();
|
|
244
|
+
}
|
|
245
|
+
this.cmdHandler = null;
|
|
246
|
+
this.mqttConfig = null;
|
|
247
|
+
resolve();
|
|
248
|
+
});
|
|
249
|
+
}),
|
|
250
|
+
new Promise((resolve) => setTimeout(() => {
|
|
251
|
+
this.logger.warn(`DLClient account[${this.ctx.account.accountId}] mqtt end timeout(3s), force continue`);
|
|
240
252
|
this.mqttClient = null;
|
|
241
|
-
this.logger.warn(`DLClient account[${this.ctx.account.accountId}] Disconnected`);
|
|
242
|
-
if (this.cmdHandler) {
|
|
243
|
-
this.cmdHandler.cleanup();
|
|
244
|
-
}
|
|
245
253
|
this.cmdHandler = null;
|
|
246
254
|
this.mqttConfig = null;
|
|
247
255
|
resolve();
|
|
248
|
-
})
|
|
249
|
-
|
|
256
|
+
}, 1200)),
|
|
257
|
+
]);
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
await Promise.race([
|
|
261
|
+
this.changeConnectStatus('stop'),
|
|
262
|
+
new Promise((resolve) => setTimeout(resolve, 5000)),
|
|
263
|
+
]);
|
|
264
|
+
}
|
|
265
|
+
catch (e) {
|
|
266
|
+
this.logger.warn(`DLClient changeConnectStatus(stop) failed: ${String(e)}`);
|
|
250
267
|
}
|
|
251
|
-
await this.changeConnectStatus('stop');
|
|
252
268
|
}
|
|
253
269
|
/**
|
|
254
270
|
* 2026-09-15 方案A(石头哥确认):重新拉取 mqtt 配置(含 openUserTopics)。
|
|
@@ -260,6 +276,10 @@ export class DLClient extends EventEmitter {
|
|
|
260
276
|
* 而本方法的目的只是刷新 openUserTopics——它是 publishEventToDLClient(L348)实时读取的,
|
|
261
277
|
* 与 MQTT 订阅目标(clientTopics)、Gateway WS 通道都无关 → 只刷新配置即可,不断连、不重连。
|
|
262
278
|
*/
|
|
279
|
+
/** 2026-10-21:团队创建人(终端等 owner-only 能力使用) */
|
|
280
|
+
getOwnerEmpId() {
|
|
281
|
+
return this.mqttConfig?.ownerEmpId;
|
|
282
|
+
}
|
|
263
283
|
async refreshConfigAndReconnect() {
|
|
264
284
|
try {
|
|
265
285
|
await this.fetchConfig();
|
|
@@ -1094,7 +1094,7 @@ export async function routeAndDispatchMessage(ctx) {
|
|
|
1094
1094
|
streamId,
|
|
1095
1095
|
content,
|
|
1096
1096
|
status: 'read',
|
|
1097
|
-
kind: isTaskListContent(content) ? 'plan' : '
|
|
1097
|
+
kind: isTaskListContent(content) ? 'plan' : 'commentary',
|
|
1098
1098
|
});
|
|
1099
1099
|
}
|
|
1100
1100
|
},
|
|
@@ -1181,7 +1181,7 @@ export async function routeAndDispatchMessage(ctx) {
|
|
|
1181
1181
|
streamId,
|
|
1182
1182
|
content: joined,
|
|
1183
1183
|
status: 'read',
|
|
1184
|
-
kind: isTaskListContent(joined) ? 'plan' : '
|
|
1184
|
+
kind: isTaskListContent(joined) ? 'plan' : 'commentary'
|
|
1185
1185
|
});
|
|
1186
1186
|
}
|
|
1187
1187
|
},
|
|
@@ -96,7 +96,7 @@ export interface DLMixedBody extends DLBaseBody {
|
|
|
96
96
|
msgList: Array<DLTextBody | DLImageBody | DLVoiceBody | DLVideoBody | DLFileBody>;
|
|
97
97
|
quote?: DLQuoteBase;
|
|
98
98
|
}
|
|
99
|
-
export type DLKind = 'block' | 'in_process' | 'tool' | 'plan' | 'patch' | 'command' | 'reasoning' | 'end_reasoning';
|
|
99
|
+
export type DLKind = 'block' | 'in_process' | 'tool' | 'plan' | 'patch' | 'command' | 'reasoning' | 'end_reasoning' | 'commentary';
|
|
100
100
|
export type MessageStatus = 'read' | 'unRead' | undefined;
|
|
101
101
|
export interface ChatUser {
|
|
102
102
|
id: string;
|
|
@@ -18,7 +18,10 @@ export class CmdHandler {
|
|
|
18
18
|
isInitialized = false;
|
|
19
19
|
constructor(config, dlClient, logger) {
|
|
20
20
|
this.dlClient = dlClient;
|
|
21
|
-
|
|
21
|
+
// 2026-10-21:终端输出经 MQTT 事件回推客户端(协议级能力,品牌无关)
|
|
22
|
+
this.services = new ClawServices(config, logger, (sessionId, data) => {
|
|
23
|
+
void this.dlClient.publishEventToDLClient({ sessionId, data }, 'onTerminal');
|
|
24
|
+
});
|
|
22
25
|
}
|
|
23
26
|
log(msg, type) {
|
|
24
27
|
this.services.log(msg, type);
|
|
@@ -154,7 +157,7 @@ export class CmdHandler {
|
|
|
154
157
|
this.dlClient.publishProcessingState({
|
|
155
158
|
streamId,
|
|
156
159
|
content,
|
|
157
|
-
kind: isTaskList ? 'plan' : '
|
|
160
|
+
kind: isTaskList ? 'plan' : 'commentary',
|
|
158
161
|
status: 'read'
|
|
159
162
|
});
|
|
160
163
|
}
|
|
@@ -216,6 +219,16 @@ export class CmdHandler {
|
|
|
216
219
|
else {
|
|
217
220
|
this.log(`🔄CMD 指令:${rawMessage.cmd}, params=${JSON.stringify(rawMessage)}`, 'info');
|
|
218
221
|
}
|
|
222
|
+
// 2026-10-21:终端能力仅限团队创建人(owner)。
|
|
223
|
+
// from 是客户端自己的 chat topic:usr_/<empId>/chat,与 ownerEmpId 比对即可。
|
|
224
|
+
if (typeof rawMessage.cmd === 'string' && rawMessage.cmd.startsWith('terminal.')) {
|
|
225
|
+
const owner = this.dlClient.getOwnerEmpId();
|
|
226
|
+
const expected = owner ? `usr_/${owner}/chat` : '';
|
|
227
|
+
if (!owner || rawMessage.from !== expected) {
|
|
228
|
+
this.log(`terminal denied: from=${rawMessage.from}, owner=${owner}`, 'warn');
|
|
229
|
+
return { success: false, error: 'terminal: forbidden (owner only)', timestamp: Date.now() };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
219
232
|
try {
|
|
220
233
|
// 调用服务层处理
|
|
221
234
|
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,8 @@ export declare class ClawServices extends EventEmitter {
|
|
|
14
15
|
private readonly options;
|
|
15
16
|
private isInitialized;
|
|
16
17
|
private readonly logger;
|
|
17
|
-
|
|
18
|
+
private readonly terminalManager;
|
|
19
|
+
constructor(options: ClawServiceOptions, logger: PluginLogger, terminalSink?: TerminalDataSink);
|
|
18
20
|
log(msg: string, type: 'info' | 'warn' | 'error' | 'debug'): void;
|
|
19
21
|
/**
|
|
20
22
|
* 初始化服务
|
|
@@ -17,6 +17,7 @@ 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 { TerminalManager } from './terminal.js';
|
|
20
21
|
/**
|
|
21
22
|
* Claw-Services 主类
|
|
22
23
|
* 处理 CMD 消息并调用相应的 OpenClaw API
|
|
@@ -26,10 +27,13 @@ export class ClawServices extends EventEmitter {
|
|
|
26
27
|
options;
|
|
27
28
|
isInitialized = false;
|
|
28
29
|
logger;
|
|
29
|
-
|
|
30
|
+
// 2026-10-21:终端会话管理(PTY / 管道)
|
|
31
|
+
terminalManager;
|
|
32
|
+
constructor(options, logger, terminalSink) {
|
|
30
33
|
super();
|
|
31
34
|
this.options = options;
|
|
32
35
|
this.logger = logger;
|
|
36
|
+
this.terminalManager = new TerminalManager(terminalSink ?? (() => { }), (msg, type) => this.log(msg, type ?? 'info'));
|
|
33
37
|
}
|
|
34
38
|
log(msg, type) {
|
|
35
39
|
switch (type) {
|
|
@@ -266,6 +270,19 @@ export class ClawServices extends EventEmitter {
|
|
|
266
270
|
// 新增 cmd 通道 chat.abort , 以上两个/stop, /queue interrupt都不能立即终止执行中的会话
|
|
267
271
|
result = await this.handlChatAbout(cmdMessage.params);
|
|
268
272
|
break;
|
|
273
|
+
// 2026-10-21:终端能力(owner-only,见 CmdHandler 校验)
|
|
274
|
+
case 'terminal.open':
|
|
275
|
+
result = await this.terminalManager.open(cmdMessage.params);
|
|
276
|
+
break;
|
|
277
|
+
case 'terminal.input':
|
|
278
|
+
result = this.terminalManager.write(cmdMessage.params?.sessionId, cmdMessage.params?.data ?? '');
|
|
279
|
+
break;
|
|
280
|
+
case 'terminal.resize':
|
|
281
|
+
result = this.terminalManager.resize(cmdMessage.params?.sessionId, cmdMessage.params?.cols, cmdMessage.params?.rows);
|
|
282
|
+
break;
|
|
283
|
+
case 'terminal.close':
|
|
284
|
+
result = this.terminalManager.close(cmdMessage.params?.sessionId);
|
|
285
|
+
break;
|
|
269
286
|
case 'pairing.approve':
|
|
270
287
|
// 2026-09-06 适配 openclaw 2026.9.1:审批改按 requestId(宿主 RPC),客户端由 onPairingRequest 事件获取;
|
|
271
288
|
// code 保留解析用于旧客户端兼容提示(9.1 起宿主 list 不再暴露 code,无法按 code 审批)
|
|
@@ -330,10 +347,19 @@ export class ClawServices extends EventEmitter {
|
|
|
330
347
|
* @param userId
|
|
331
348
|
*/
|
|
332
349
|
async handleUpdateSessionModel(params) {
|
|
333
|
-
const { sessionKey, provider, model } = params;
|
|
334
|
-
if (!
|
|
350
|
+
const { sessionKey, provider, model, thinkingLevel } = params;
|
|
351
|
+
if (!sessionKey || sessionKey.length === 0) {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
// 2026-10-21:支持「只改推理强度」的 patch;provider/model 与 thinkingLevel 至少给一个
|
|
355
|
+
if ((!provider || provider.length === 0 || !model || model.length === 0) && !thinkingLevel) {
|
|
335
356
|
return null;
|
|
336
357
|
}
|
|
358
|
+
// 推理强度合法值校验(off/minimal/low/medium/high),非法值直接忽略
|
|
359
|
+
const THINK_LEVELS = ['off', 'minimal', 'low', 'medium', 'high'];
|
|
360
|
+
const thinkOverride = thinkingLevel && THINK_LEVELS.includes(thinkingLevel)
|
|
361
|
+
? thinkingLevel
|
|
362
|
+
: undefined;
|
|
337
363
|
const agentId = sessionKey.split(':')[1];
|
|
338
364
|
const { cfg, channelId } = this.options;
|
|
339
365
|
const storePath = resolveStorePath(cfg.session?.store, { agentId });
|
|
@@ -346,19 +372,25 @@ export class ClawServices extends EventEmitter {
|
|
|
346
372
|
storePath,
|
|
347
373
|
sessionKey: realKey,
|
|
348
374
|
update: (entry) => {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
375
|
+
if (provider && model) {
|
|
376
|
+
applyModelOverrideToSessionEntry({
|
|
377
|
+
entry,
|
|
378
|
+
selection: {
|
|
379
|
+
provider: provider,
|
|
380
|
+
model: model,
|
|
381
|
+
},
|
|
382
|
+
selectionSource: 'user',
|
|
383
|
+
markLiveSwitchPending: true, // 让当前 run 立即生效
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
// 2026-10-21:会话级推理强度写入(与模型覆盖同层持久化)
|
|
387
|
+
if (thinkOverride) {
|
|
388
|
+
entry.thinkingLevel = thinkOverride;
|
|
389
|
+
}
|
|
358
390
|
return { ...entry };
|
|
359
391
|
},
|
|
360
392
|
});
|
|
361
|
-
return { provider, model };
|
|
393
|
+
return { provider, model, thinkingLevel: thinkOverride };
|
|
362
394
|
}
|
|
363
395
|
/**
|
|
364
396
|
* 2026-08-28 解析 direct 会话的真实 sessionKey(兼容 chatId 后缀)
|
|
@@ -417,13 +449,16 @@ export class ClawServices extends EventEmitter {
|
|
|
417
449
|
if (!entry)
|
|
418
450
|
return null;
|
|
419
451
|
// 只返回用户主动设置的覆盖,不返回 auto fallback
|
|
452
|
+
const result = {};
|
|
420
453
|
if (entry.modelOverrideSource === 'user' && entry.modelOverride) {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
454
|
+
result.provider = entry.providerOverride ?? '';
|
|
455
|
+
result.model = entry.modelOverride;
|
|
456
|
+
}
|
|
457
|
+
// 2026-10-21:会话级推理强度回显
|
|
458
|
+
if (entry.thinkingLevel) {
|
|
459
|
+
result.thinkingLevel = entry.thinkingLevel;
|
|
425
460
|
}
|
|
426
|
-
return null;
|
|
461
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
427
462
|
}
|
|
428
463
|
//#region ==================== Agent 管理处理器 ====================
|
|
429
464
|
async handleAgentList() {
|
|
@@ -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' | '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,13 @@ 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' | 'onTerminal';
|
|
320
320
|
export type CmdParamsPatchSessionEntry = {
|
|
321
321
|
sessionKey: string;
|
|
322
322
|
provider?: string;
|
|
323
|
-
model
|
|
323
|
+
model?: string;
|
|
324
|
+
/** 2026-10-21:会话级推理强度(off/minimal/low/medium/high),与模型选择同通道 */
|
|
325
|
+
thinkingLevel?: string;
|
|
324
326
|
};
|
|
325
327
|
export interface SkillInfo {
|
|
326
328
|
name: string;
|
|
@@ -390,3 +392,21 @@ export interface AgentFilesGetParams {
|
|
|
390
392
|
agentId: string;
|
|
391
393
|
name: string;
|
|
392
394
|
}
|
|
395
|
+
export type TerminalOpenParams = {
|
|
396
|
+
sessionId: string;
|
|
397
|
+
cwd?: string;
|
|
398
|
+
cols?: number;
|
|
399
|
+
rows?: number;
|
|
400
|
+
};
|
|
401
|
+
export type TerminalInputParams = {
|
|
402
|
+
sessionId: string;
|
|
403
|
+
data: string;
|
|
404
|
+
};
|
|
405
|
+
export type TerminalResizeParams = {
|
|
406
|
+
sessionId: string;
|
|
407
|
+
cols: number;
|
|
408
|
+
rows: number;
|
|
409
|
+
};
|
|
410
|
+
export type TerminalCloseParams = {
|
|
411
|
+
sessionId: string;
|
|
412
|
+
};
|
package/package.json
CHANGED
|
@@ -1,96 +1,96 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dlinkclaw-openclaw",
|
|
3
|
-
"version": "2026.10.
|
|
4
|
-
"description": "DLinkClaw OpenClaw channel plugin — 朵联 OpenClaw 接入通道",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"DLinkClaw",
|
|
7
|
-
"openclaw",
|
|
8
|
-
"朵联",
|
|
9
|
-
"钉钉",
|
|
10
|
-
"QQ",
|
|
11
|
-
"企业微信",
|
|
12
|
-
"飞书"
|
|
13
|
-
],
|
|
14
|
-
"publishConfig": {
|
|
15
|
-
"access": "public"
|
|
16
|
-
},
|
|
17
|
-
"scripts": {
|
|
18
|
-
"build": "tsc && terser dist/index.js --output dist/index.js --compress --mangle --module",
|
|
19
|
-
"dev": "tsc --watch",
|
|
20
|
-
"prepublishOnly": "npm run build"
|
|
21
|
-
},
|
|
22
|
-
"license": "MIT",
|
|
23
|
-
"author": "rock.chen<szygnet@qq.com>",
|
|
24
|
-
"type": "module",
|
|
25
|
-
"types": "./dist/index.d.ts",
|
|
26
|
-
"main": "./index.ts",
|
|
27
|
-
"exports": {
|
|
28
|
-
".": {
|
|
29
|
-
"types": "./dist/index.d.ts",
|
|
30
|
-
"import": "./dist/index.js"
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
"files": [
|
|
34
|
-
"dist",
|
|
35
|
-
"skills",
|
|
36
|
-
"ext_capabilities",
|
|
37
|
-
"openclaw.plugin.json",
|
|
38
|
-
"README.md",
|
|
39
|
-
"docs"
|
|
40
|
-
],
|
|
41
|
-
"dependencies": {
|
|
42
|
-
"axios": "^1.15.2",
|
|
43
|
-
"file-type": "^21.3.1",
|
|
44
|
-
"form-data": "^4.0.5",
|
|
45
|
-
"mime-types": "^3.0.2",
|
|
46
|
-
"mqtt": "^5.10.0",
|
|
47
|
-
"qrcode-terminal": "^0.12.0",
|
|
48
|
-
"uuid": "^14.0.2",
|
|
49
|
-
"ws": "^8.20.0"
|
|
50
|
-
},
|
|
51
|
-
"devDependencies": {
|
|
52
|
-
"@types/mime-types": "^3.0.1",
|
|
53
|
-
"@types/ws": "^8.18.1",
|
|
54
|
-
"terser": "^5.46.1",
|
|
55
|
-
"typescript": "^5.3.3"
|
|
56
|
-
},
|
|
57
|
-
"peerDependencies": {
|
|
58
|
-
"openclaw": ">=2026.10.3"
|
|
59
|
-
},
|
|
60
|
-
"openclaw": {
|
|
61
|
-
"extensions": [
|
|
62
|
-
"./index.ts"
|
|
63
|
-
],
|
|
64
|
-
"installDependencies": true,
|
|
65
|
-
"channel": {
|
|
66
|
-
"id": "dlink",
|
|
67
|
-
"label": "朵联 DLinkClaw",
|
|
68
|
-
"selectionLabel": "朵联(DLinkClaw)",
|
|
69
|
-
"docsPath": "/channels/dlinkclaw-openclaw",
|
|
70
|
-
"docsLabel": "dlinkclaw-openclaw",
|
|
71
|
-
"blurb": "朵联 DLinkClaw OpenClaw 接入通道, 本地或云端部署OpenClaw连接朵联客户端, 支持企业微信、QQ、钉钉、飞书, 无需公网IP.",
|
|
72
|
-
"aliases": [
|
|
73
|
-
"dlink",
|
|
74
|
-
"DLinkClaw",
|
|
75
|
-
"朵联"
|
|
76
|
-
],
|
|
77
|
-
"order": 90,
|
|
78
|
-
"quickstartAllowFrom": true,
|
|
79
|
-
"isDebug":
|
|
80
|
-
"preferSessionLookupForAnnounceTarget": true
|
|
81
|
-
},
|
|
82
|
-
"install": {
|
|
83
|
-
"npmSpec": "dlinkclaw-openclaw",
|
|
84
|
-
"localPath": "extensions/dlinkclaw-openclaw",
|
|
85
|
-
"defaultChoice": "npm"
|
|
86
|
-
}
|
|
87
|
-
},
|
|
88
|
-
"repository": {
|
|
89
|
-
"type": "git",
|
|
90
|
-
"url": "https://github.com/dlinkclaw/dlinkclaw-openclaw-channel.git"
|
|
91
|
-
},
|
|
92
|
-
"homepage": "https://github.com/dlinkclaw/dlinkclaw-openclaw-channel#readme",
|
|
93
|
-
"bugs": {
|
|
94
|
-
"url": "https://github.com/dlinkclaw/dlinkclaw-openclaw-channel/issues"
|
|
95
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "dlinkclaw-openclaw",
|
|
3
|
+
"version": "2026.10.20",
|
|
4
|
+
"description": "DLinkClaw OpenClaw channel plugin — 朵联 OpenClaw 接入通道",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"DLinkClaw",
|
|
7
|
+
"openclaw",
|
|
8
|
+
"朵联",
|
|
9
|
+
"钉钉",
|
|
10
|
+
"QQ",
|
|
11
|
+
"企业微信",
|
|
12
|
+
"飞书"
|
|
13
|
+
],
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc && terser dist/index.js --output dist/index.js --compress --mangle --module",
|
|
19
|
+
"dev": "tsc --watch",
|
|
20
|
+
"prepublishOnly": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"author": "rock.chen<szygnet@qq.com>",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"main": "./index.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"skills",
|
|
36
|
+
"ext_capabilities",
|
|
37
|
+
"openclaw.plugin.json",
|
|
38
|
+
"README.md",
|
|
39
|
+
"docs"
|
|
40
|
+
],
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"axios": "^1.15.2",
|
|
43
|
+
"file-type": "^21.3.1",
|
|
44
|
+
"form-data": "^4.0.5",
|
|
45
|
+
"mime-types": "^3.0.2",
|
|
46
|
+
"mqtt": "^5.10.0",
|
|
47
|
+
"qrcode-terminal": "^0.12.0",
|
|
48
|
+
"uuid": "^14.0.2",
|
|
49
|
+
"ws": "^8.20.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/mime-types": "^3.0.1",
|
|
53
|
+
"@types/ws": "^8.18.1",
|
|
54
|
+
"terser": "^5.46.1",
|
|
55
|
+
"typescript": "^5.3.3"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"openclaw": ">=2026.10.3"
|
|
59
|
+
},
|
|
60
|
+
"openclaw": {
|
|
61
|
+
"extensions": [
|
|
62
|
+
"./index.ts"
|
|
63
|
+
],
|
|
64
|
+
"installDependencies": true,
|
|
65
|
+
"channel": {
|
|
66
|
+
"id": "dlink",
|
|
67
|
+
"label": "朵联 DLinkClaw",
|
|
68
|
+
"selectionLabel": "朵联(DLinkClaw)",
|
|
69
|
+
"docsPath": "/channels/dlinkclaw-openclaw",
|
|
70
|
+
"docsLabel": "dlinkclaw-openclaw",
|
|
71
|
+
"blurb": "朵联 DLinkClaw OpenClaw 接入通道, 本地或云端部署OpenClaw连接朵联客户端, 支持企业微信、QQ、钉钉、飞书, 无需公网IP.",
|
|
72
|
+
"aliases": [
|
|
73
|
+
"dlink",
|
|
74
|
+
"DLinkClaw",
|
|
75
|
+
"朵联"
|
|
76
|
+
],
|
|
77
|
+
"order": 90,
|
|
78
|
+
"quickstartAllowFrom": true,
|
|
79
|
+
"isDebug": false,
|
|
80
|
+
"preferSessionLookupForAnnounceTarget": true
|
|
81
|
+
},
|
|
82
|
+
"install": {
|
|
83
|
+
"npmSpec": "dlinkclaw-openclaw",
|
|
84
|
+
"localPath": "extensions/dlinkclaw-openclaw",
|
|
85
|
+
"defaultChoice": "npm"
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"repository": {
|
|
89
|
+
"type": "git",
|
|
90
|
+
"url": "https://github.com/dlinkclaw/dlinkclaw-openclaw-channel.git"
|
|
91
|
+
},
|
|
92
|
+
"homepage": "https://github.com/dlinkclaw/dlinkclaw-openclaw-channel#readme",
|
|
93
|
+
"bugs": {
|
|
94
|
+
"url": "https://github.com/dlinkclaw/dlinkclaw-openclaw-channel/issues"
|
|
95
|
+
}
|
|
96
96
|
}
|