openclaw-linso 1.0.12 → 1.0.16
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 +33 -1
- package/dist/src/monitor.d.ts +4 -0
- package/dist/src/monitor.js +84 -3
- package/dist/src/store.js +16 -0
- package/package.json +1 -1
- package/src/monitor.ts +86 -3
- package/src/store.ts +13 -0
package/dist/index.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// 插件入口:注册 channel + 启动 monitor service
|
|
3
3
|
// 和飞书 index.ts 结构完全一致
|
|
4
4
|
import { linsoPlugin } from "./src/channel.js";
|
|
5
|
-
import { monitorLinsoProvider } from "./src/monitor.js";
|
|
5
|
+
import { monitorLinsoProvider, getActiveDeviceIds, getDeviceRunId } from "./src/monitor.js";
|
|
6
6
|
import { setLinsoRuntime } from "./src/runtime.js";
|
|
7
|
+
import { sendToClient } from "./src/store.js";
|
|
7
8
|
export { monitorLinsoProvider } from "./src/monitor.js";
|
|
8
9
|
export { linsoPlugin } from "./src/channel.js";
|
|
9
10
|
const plugin = {
|
|
@@ -30,6 +31,37 @@ const plugin = {
|
|
|
30
31
|
disconnectFromRelay();
|
|
31
32
|
},
|
|
32
33
|
});
|
|
34
|
+
// 4. 注册 Tool 调用 hooks — 向所有活跃的 iOS 设备广播 tool_call 事件
|
|
35
|
+
api.on("before_tool_call", async (event) => {
|
|
36
|
+
const deviceIds = getActiveDeviceIds();
|
|
37
|
+
for (const deviceId of deviceIds) {
|
|
38
|
+
const pluginRunId = getDeviceRunId(deviceId);
|
|
39
|
+
const runId = pluginRunId ?? event.runId ?? "";
|
|
40
|
+
console.log(`[Linso] 🔧 before_tool_call tool=${event.toolName} pluginRunId=${pluginRunId ?? "none"} ocRunId=${event.runId ?? "-"} → sending runId=${runId}`);
|
|
41
|
+
sendToClient(deviceId, {
|
|
42
|
+
type: "tool_call",
|
|
43
|
+
runId,
|
|
44
|
+
status: "start",
|
|
45
|
+
toolName: event.toolName,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
api.on("after_tool_call", async (event) => {
|
|
50
|
+
const deviceIds = getActiveDeviceIds();
|
|
51
|
+
for (const deviceId of deviceIds) {
|
|
52
|
+
const pluginRunId = getDeviceRunId(deviceId);
|
|
53
|
+
const runId = pluginRunId ?? event.runId ?? "";
|
|
54
|
+
console.log(`[Linso] 🔧 after_tool_call tool=${event.toolName} pluginRunId=${pluginRunId ?? "none"} dur=${event.durationMs ?? "-"}ms → sending runId=${runId}`);
|
|
55
|
+
sendToClient(deviceId, {
|
|
56
|
+
type: "tool_call",
|
|
57
|
+
runId,
|
|
58
|
+
status: "end",
|
|
59
|
+
toolName: event.toolName,
|
|
60
|
+
durationMs: event.durationMs,
|
|
61
|
+
error: event.error,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
});
|
|
33
65
|
},
|
|
34
66
|
};
|
|
35
67
|
export default plugin;
|
package/dist/src/monitor.d.ts
CHANGED
|
@@ -4,4 +4,8 @@ export type MonitorLinsoOpts = {
|
|
|
4
4
|
log?: (...args: unknown[]) => void;
|
|
5
5
|
abortSignal?: AbortSignal;
|
|
6
6
|
};
|
|
7
|
+
/** 返回最近活跃(TTL 内)的所有设备 ID,供 index.ts hook 广播 tool_call 用 */
|
|
8
|
+
export declare function getActiveDeviceIds(): string[];
|
|
9
|
+
/** 获取设备当前活跃的 Plugin runId */
|
|
10
|
+
export declare function getDeviceRunId(deviceId: string): string | undefined;
|
|
7
11
|
export declare function monitorLinsoProvider(opts?: MonitorLinsoOpts): Promise<void>;
|
package/dist/src/monitor.js
CHANGED
|
@@ -26,6 +26,22 @@ const DEVICE_PRUNE_INTERVAL_MS = 60 * 60 * 1000;
|
|
|
26
26
|
function touchDevice(deviceId) {
|
|
27
27
|
deviceLastSeen.set(deviceId, Date.now());
|
|
28
28
|
}
|
|
29
|
+
/** deviceId → 当前正在运行的 Plugin runId,供 index.ts hook 广播 tool_call 用 */
|
|
30
|
+
const deviceActiveRunId = new Map();
|
|
31
|
+
/** 返回最近活跃(TTL 内)的所有设备 ID,供 index.ts hook 广播 tool_call 用 */
|
|
32
|
+
export function getActiveDeviceIds() {
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
const result = [];
|
|
35
|
+
for (const [id, lastSeen] of deviceLastSeen) {
|
|
36
|
+
if (now - lastSeen <= DEVICE_TTL_MS)
|
|
37
|
+
result.push(id);
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
/** 获取设备当前活跃的 Plugin runId */
|
|
42
|
+
export function getDeviceRunId(deviceId) {
|
|
43
|
+
return deviceActiveRunId.get(deviceId);
|
|
44
|
+
}
|
|
29
45
|
function pruneInactiveDevices(log) {
|
|
30
46
|
const now = Date.now();
|
|
31
47
|
let pruned = 0;
|
|
@@ -87,6 +103,9 @@ export async function monitorLinsoProvider(opts = {}) {
|
|
|
87
103
|
log("[Linso] 与 Relay 断开");
|
|
88
104
|
},
|
|
89
105
|
onMessage: (deviceId, msg) => {
|
|
106
|
+
const type = msg.type ?? "unknown";
|
|
107
|
+
const text = msg.text;
|
|
108
|
+
log(`[Linso] ↓ RECV type=${type} deviceId=${String(deviceId).slice(0, 8)}...${text ? ` text="${String(text).slice(0, 50)}"` : ""}`);
|
|
90
109
|
if (msg.type === "send" && typeof msg.text === "string" && msg.text.trim()) {
|
|
91
110
|
touchDevice(deviceId); // [Opt #3] 记录活跃时间
|
|
92
111
|
const text = msg.text.trim();
|
|
@@ -397,6 +416,10 @@ async function handleIncomingMessage(deviceId, text, imageUrls, cfg, log) {
|
|
|
397
416
|
}),
|
|
398
417
|
});
|
|
399
418
|
const runId = `linso-${Date.now()}`;
|
|
419
|
+
deviceActiveRunId.set(deviceId, runId); // 记录当前活跃 runId
|
|
420
|
+
let delivered = false; // 是否已发送过内容
|
|
421
|
+
let deltaCount = 0; // 已发送的 delta 数量
|
|
422
|
+
let lastSentText = ""; // 上次发送的累计文本,用于计算真实增量
|
|
400
423
|
const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({
|
|
401
424
|
deliver: async (payload, info) => {
|
|
402
425
|
// 检查 stop 标志
|
|
@@ -404,27 +427,85 @@ async function handleIncomingMessage(deviceId, text, imageUrls, cfg, log) {
|
|
|
404
427
|
log(`[Linso] stop 标志命中,跳过输出: deviceId=${deviceId}`);
|
|
405
428
|
return;
|
|
406
429
|
}
|
|
407
|
-
if (
|
|
430
|
+
if (payload.isReasoning)
|
|
431
|
+
return;
|
|
432
|
+
if (!payload.text) {
|
|
433
|
+
log(`[Linso] deliver 跳过: kind=${info.kind} text=empty`);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
// 如果已经通过 onPartialReply 发过 delta,则 final 阶段发 stream_end 而非重复文本
|
|
437
|
+
if (info.kind === "final" && deltaCount > 0) {
|
|
438
|
+
log(`[Linso] deliver final skipped (streamed ${deltaCount} deltas): text="${payload.text.slice(0, 40)}"`);
|
|
439
|
+
delivered = true;
|
|
440
|
+
sendToClient(deviceId, { type: "stream_end", runId });
|
|
408
441
|
return;
|
|
442
|
+
}
|
|
443
|
+
log(`[Linso] deliver called: kind=${info.kind} text="${payload.text.slice(0, 40)}"`);
|
|
444
|
+
delivered = true;
|
|
409
445
|
sendToClient(deviceId, {
|
|
410
446
|
type: info.kind === "final" ? "final" : "delta",
|
|
411
447
|
runId,
|
|
412
448
|
text: payload.text,
|
|
413
449
|
});
|
|
414
450
|
},
|
|
451
|
+
onSkip: (payload, info) => {
|
|
452
|
+
log(`[Linso] ⚠️ deliver SKIPPED: kind=${info.kind} reason=${info.reason}`);
|
|
453
|
+
if (!delivered) {
|
|
454
|
+
delivered = true;
|
|
455
|
+
sendToClient(deviceId, { type: "final", runId, text: "😶" });
|
|
456
|
+
}
|
|
457
|
+
},
|
|
458
|
+
onCleanup: () => {
|
|
459
|
+
log(`[Linso] ⚠️ onCleanup (NO_REPLY or cleanup) runId=${runId} delivered=${delivered}`);
|
|
460
|
+
if (!delivered) {
|
|
461
|
+
delivered = true;
|
|
462
|
+
sendToClient(deviceId, { type: "final", runId, text: "😶" });
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
onError: (err, info) => {
|
|
466
|
+
log(`[Linso] ❌ deliver ERROR: kind=${info.kind} err=${String(err)}`);
|
|
467
|
+
},
|
|
415
468
|
});
|
|
416
469
|
sendToClient(deviceId, { type: "run_start", runId });
|
|
417
|
-
await core.channel.reply.withReplyDispatcher({
|
|
470
|
+
const dispatchResult = await core.channel.reply.withReplyDispatcher({
|
|
418
471
|
dispatcher,
|
|
419
472
|
onSettled: () => markDispatchIdle(),
|
|
420
473
|
run: () => core.channel.reply.dispatchReplyFromConfig({
|
|
421
474
|
ctx: inboundCtx,
|
|
422
475
|
cfg: effectiveCfg,
|
|
423
476
|
dispatcher,
|
|
424
|
-
replyOptions
|
|
477
|
+
replyOptions: {
|
|
478
|
+
...replyOptions,
|
|
479
|
+
onPartialReply: (payload) => {
|
|
480
|
+
if (deviceStopFlags.get(deviceId))
|
|
481
|
+
return;
|
|
482
|
+
if (!payload.text)
|
|
483
|
+
return;
|
|
484
|
+
// payload.text 是累积完整文本,提取真实增量部分
|
|
485
|
+
const fullText = payload.text;
|
|
486
|
+
if (fullText.length <= lastSentText.length)
|
|
487
|
+
return; // 没有新内容
|
|
488
|
+
const increment = fullText.slice(lastSentText.length);
|
|
489
|
+
lastSentText = fullText;
|
|
490
|
+
deltaCount++;
|
|
491
|
+
delivered = true;
|
|
492
|
+
sendToClient(deviceId, { type: "delta", runId, text: increment });
|
|
493
|
+
},
|
|
494
|
+
},
|
|
425
495
|
}),
|
|
426
496
|
});
|
|
497
|
+
log(`[Linso] dispatchResult: ${JSON.stringify(dispatchResult)} delivered=${delivered}`);
|
|
498
|
+
// 兜底:如果 dispatcher 没有发送过任何内容
|
|
499
|
+
if (!delivered) {
|
|
500
|
+
log(`[Linso] ⚠️ 没有内容被 deliver,发送兜底回复`);
|
|
501
|
+
sendToClient(deviceId, {
|
|
502
|
+
type: "final",
|
|
503
|
+
runId,
|
|
504
|
+
text: "收到啦,但我暂时无法回复这条消息 🤔",
|
|
505
|
+
});
|
|
506
|
+
}
|
|
427
507
|
sendToClient(deviceId, { type: "done", runId });
|
|
508
|
+
deviceActiveRunId.delete(deviceId); // 清理活跃 runId
|
|
428
509
|
log(`[Linso] 回复完成: deviceId=${deviceId}`);
|
|
429
510
|
}
|
|
430
511
|
finally {
|
package/dist/src/store.js
CHANGED
|
@@ -4,5 +4,21 @@
|
|
|
4
4
|
import { sendToRelay } from "./relay-client.js";
|
|
5
5
|
/** 向指定 iOS 设备发送消息(经由 Relay) */
|
|
6
6
|
export function sendToClient(deviceId, msg) {
|
|
7
|
+
const type = msg.type ?? "unknown";
|
|
8
|
+
console.log(`[Linso] ↑ SEND type=${type} deviceId=${String(deviceId).slice(0, 8)}... ${_preview(msg)}`);
|
|
7
9
|
sendToRelay({ ...msg, deviceId });
|
|
8
10
|
}
|
|
11
|
+
function _preview(msg) {
|
|
12
|
+
const parts = [];
|
|
13
|
+
if (msg.runId)
|
|
14
|
+
parts.push(`runId=${String(msg.runId).slice(-6)}`);
|
|
15
|
+
if (msg.text)
|
|
16
|
+
parts.push(`text="${String(msg.text).slice(0, 40)}"`);
|
|
17
|
+
if (msg.toolName)
|
|
18
|
+
parts.push(`tool=${msg.toolName}`);
|
|
19
|
+
if (msg.status)
|
|
20
|
+
parts.push(`status=${msg.status}`);
|
|
21
|
+
if (msg.error)
|
|
22
|
+
parts.push(`error=${msg.error}`);
|
|
23
|
+
return parts.join(" ");
|
|
24
|
+
}
|
package/package.json
CHANGED
package/src/monitor.ts
CHANGED
|
@@ -43,6 +43,24 @@ function touchDevice(deviceId: string): void {
|
|
|
43
43
|
deviceLastSeen.set(deviceId, Date.now());
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/** deviceId → 当前正在运行的 Plugin runId,供 index.ts hook 广播 tool_call 用 */
|
|
47
|
+
const deviceActiveRunId = new Map<string, string>();
|
|
48
|
+
|
|
49
|
+
/** 返回最近活跃(TTL 内)的所有设备 ID,供 index.ts hook 广播 tool_call 用 */
|
|
50
|
+
export function getActiveDeviceIds(): string[] {
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
const result: string[] = [];
|
|
53
|
+
for (const [id, lastSeen] of deviceLastSeen) {
|
|
54
|
+
if (now - lastSeen <= DEVICE_TTL_MS) result.push(id);
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 获取设备当前活跃的 Plugin runId */
|
|
60
|
+
export function getDeviceRunId(deviceId: string): string | undefined {
|
|
61
|
+
return deviceActiveRunId.get(deviceId);
|
|
62
|
+
}
|
|
63
|
+
|
|
46
64
|
function pruneInactiveDevices(log: (...args: unknown[]) => void): void {
|
|
47
65
|
const now = Date.now();
|
|
48
66
|
let pruned = 0;
|
|
@@ -124,6 +142,10 @@ export async function monitorLinsoProvider(opts: MonitorLinsoOpts = {}): Promise
|
|
|
124
142
|
},
|
|
125
143
|
|
|
126
144
|
onMessage: (deviceId, msg) => {
|
|
145
|
+
const type = (msg as Record<string, unknown>).type ?? "unknown";
|
|
146
|
+
const text = (msg as Record<string, unknown>).text;
|
|
147
|
+
log(`[Linso] ↓ RECV type=${type} deviceId=${String(deviceId).slice(0, 8)}...${text ? ` text="${String(text).slice(0, 50)}"` : ""}`);
|
|
148
|
+
|
|
127
149
|
if (msg.type === "send" && typeof msg.text === "string" && msg.text.trim()) {
|
|
128
150
|
touchDevice(deviceId); // [Opt #3] 记录活跃时间
|
|
129
151
|
const text = msg.text.trim();
|
|
@@ -471,6 +493,10 @@ async function handleIncomingMessage(
|
|
|
471
493
|
});
|
|
472
494
|
|
|
473
495
|
const runId = `linso-${Date.now()}`;
|
|
496
|
+
deviceActiveRunId.set(deviceId, runId); // 记录当前活跃 runId
|
|
497
|
+
let delivered = false; // 是否已发送过内容
|
|
498
|
+
let deltaCount = 0; // 已发送的 delta 数量
|
|
499
|
+
let lastSentText = ""; // 上次发送的累计文本,用于计算真实增量
|
|
474
500
|
|
|
475
501
|
const { dispatcher, replyOptions, markDispatchIdle } =
|
|
476
502
|
core.channel.reply.createReplyDispatcherWithTyping({
|
|
@@ -480,18 +506,48 @@ async function handleIncomingMessage(
|
|
|
480
506
|
log(`[Linso] stop 标志命中,跳过输出: deviceId=${deviceId}`);
|
|
481
507
|
return;
|
|
482
508
|
}
|
|
483
|
-
if (
|
|
509
|
+
if (payload.isReasoning) return;
|
|
510
|
+
if (!payload.text) {
|
|
511
|
+
log(`[Linso] deliver 跳过: kind=${info.kind} text=empty`);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
// 如果已经通过 onPartialReply 发过 delta,则 final 阶段发 stream_end 而非重复文本
|
|
515
|
+
if (info.kind === "final" && deltaCount > 0) {
|
|
516
|
+
log(`[Linso] deliver final skipped (streamed ${deltaCount} deltas): text="${payload.text.slice(0, 40)}"`);
|
|
517
|
+
delivered = true;
|
|
518
|
+
sendToClient(deviceId, { type: "stream_end", runId });
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
log(`[Linso] deliver called: kind=${info.kind} text="${payload.text.slice(0, 40)}"`);
|
|
522
|
+
delivered = true;
|
|
484
523
|
sendToClient(deviceId, {
|
|
485
524
|
type: info.kind === "final" ? "final" : "delta",
|
|
486
525
|
runId,
|
|
487
526
|
text: payload.text,
|
|
488
527
|
});
|
|
489
528
|
},
|
|
529
|
+
onSkip: (payload, info) => {
|
|
530
|
+
log(`[Linso] ⚠️ deliver SKIPPED: kind=${info.kind} reason=${info.reason}`);
|
|
531
|
+
if (!delivered) {
|
|
532
|
+
delivered = true;
|
|
533
|
+
sendToClient(deviceId, { type: "final", runId, text: "😶" });
|
|
534
|
+
}
|
|
535
|
+
},
|
|
536
|
+
onCleanup: () => {
|
|
537
|
+
log(`[Linso] ⚠️ onCleanup (NO_REPLY or cleanup) runId=${runId} delivered=${delivered}`);
|
|
538
|
+
if (!delivered) {
|
|
539
|
+
delivered = true;
|
|
540
|
+
sendToClient(deviceId, { type: "final", runId, text: "😶" });
|
|
541
|
+
}
|
|
542
|
+
},
|
|
543
|
+
onError: (err, info) => {
|
|
544
|
+
log(`[Linso] ❌ deliver ERROR: kind=${info.kind} err=${String(err)}`);
|
|
545
|
+
},
|
|
490
546
|
});
|
|
491
547
|
|
|
492
548
|
sendToClient(deviceId, { type: "run_start", runId });
|
|
493
549
|
|
|
494
|
-
await core.channel.reply.withReplyDispatcher({
|
|
550
|
+
const dispatchResult = await core.channel.reply.withReplyDispatcher({
|
|
495
551
|
dispatcher,
|
|
496
552
|
onSettled: () => markDispatchIdle(),
|
|
497
553
|
run: () =>
|
|
@@ -499,11 +555,38 @@ async function handleIncomingMessage(
|
|
|
499
555
|
ctx: inboundCtx,
|
|
500
556
|
cfg: effectiveCfg,
|
|
501
557
|
dispatcher,
|
|
502
|
-
replyOptions
|
|
558
|
+
replyOptions: {
|
|
559
|
+
...replyOptions,
|
|
560
|
+
onPartialReply: (payload) => {
|
|
561
|
+
if (deviceStopFlags.get(deviceId)) return;
|
|
562
|
+
if (!payload.text) return;
|
|
563
|
+
// payload.text 是累积完整文本,提取真实增量部分
|
|
564
|
+
const fullText = payload.text;
|
|
565
|
+
if (fullText.length <= lastSentText.length) return; // 没有新内容
|
|
566
|
+
const increment = fullText.slice(lastSentText.length);
|
|
567
|
+
lastSentText = fullText;
|
|
568
|
+
deltaCount++;
|
|
569
|
+
delivered = true;
|
|
570
|
+
sendToClient(deviceId, { type: "delta", runId, text: increment });
|
|
571
|
+
},
|
|
572
|
+
},
|
|
503
573
|
}),
|
|
504
574
|
});
|
|
505
575
|
|
|
576
|
+
log(`[Linso] dispatchResult: ${JSON.stringify(dispatchResult)} delivered=${delivered}`);
|
|
577
|
+
|
|
578
|
+
// 兜底:如果 dispatcher 没有发送过任何内容
|
|
579
|
+
if (!delivered) {
|
|
580
|
+
log(`[Linso] ⚠️ 没有内容被 deliver,发送兜底回复`);
|
|
581
|
+
sendToClient(deviceId, {
|
|
582
|
+
type: "final",
|
|
583
|
+
runId,
|
|
584
|
+
text: "收到啦,但我暂时无法回复这条消息 🤔",
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
506
588
|
sendToClient(deviceId, { type: "done", runId });
|
|
589
|
+
deviceActiveRunId.delete(deviceId); // 清理活跃 runId
|
|
507
590
|
log(`[Linso] 回复完成: deviceId=${deviceId}`);
|
|
508
591
|
|
|
509
592
|
} finally {
|
package/src/store.ts
CHANGED
|
@@ -6,5 +6,18 @@ import { sendToRelay } from "./relay-client.js";
|
|
|
6
6
|
|
|
7
7
|
/** 向指定 iOS 设备发送消息(经由 Relay) */
|
|
8
8
|
export function sendToClient(deviceId: string, msg: Record<string, unknown>) {
|
|
9
|
+
const type = msg.type ?? "unknown";
|
|
10
|
+
console.log(`[Linso] ↑ SEND type=${type} deviceId=${String(deviceId).slice(0, 8)}... ${_preview(msg)}`);
|
|
9
11
|
sendToRelay({ ...msg, deviceId });
|
|
10
12
|
}
|
|
13
|
+
|
|
14
|
+
function _preview(msg: Record<string, unknown>): string {
|
|
15
|
+
const parts: string[] = [];
|
|
16
|
+
if (msg.runId) parts.push(`runId=${String(msg.runId).slice(-6)}`);
|
|
17
|
+
if (msg.text) parts.push(`text="${String(msg.text).slice(0, 40)}"`);
|
|
18
|
+
if (msg.toolName) parts.push(`tool=${msg.toolName}`);
|
|
19
|
+
if (msg.status) parts.push(`status=${msg.status}`);
|
|
20
|
+
if (msg.error) parts.push(`error=${msg.error}`);
|
|
21
|
+
return parts.join(" ");
|
|
22
|
+
}
|
|
23
|
+
|