openclaw-linso 1.0.0 → 1.0.2
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/src/monitor.js +58 -4
- package/dist/src/relay-client.js +1 -1
- package/dist/src/types.d.ts +0 -1
- package/dist/src/types.js +3 -2
- package/package.json +4 -1
- package/src/monitor.ts +59 -3
- package/src/relay-client.ts +1 -1
- package/src/types.ts +4 -3
package/dist/src/monitor.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
// src/monitor.ts
|
|
2
2
|
// 核心 monitor:Plugin 主动连接云端 Relay(和飞书连 open.feishu.cn 一致)
|
|
3
3
|
// 收到 iOS 消息后路由到 Agent,流式回复经 Relay 推回 iOS
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import os from "os";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import https from "https";
|
|
8
|
+
import http from "http";
|
|
4
9
|
import { getLinsoRuntime } from "./runtime.js";
|
|
5
10
|
import { connectToRelay, disconnectFromRelay } from "./relay-client.js";
|
|
6
11
|
import { sendToClient } from "./store.js";
|
|
@@ -16,8 +21,8 @@ export async function monitorLinsoProvider(opts = {}) {
|
|
|
16
21
|
log("[Linso] 未启用或未配置,跳过 monitor");
|
|
17
22
|
return;
|
|
18
23
|
}
|
|
19
|
-
if (!account.
|
|
20
|
-
log("[Linso] 未配置
|
|
24
|
+
if (!account.appToken) {
|
|
25
|
+
log("[Linso] 未配置 appToken,跳过 monitor");
|
|
21
26
|
return;
|
|
22
27
|
}
|
|
23
28
|
log(`[Linso] 连接 Relay Server: ${account.relayUrl}`);
|
|
@@ -39,6 +44,9 @@ export async function monitorLinsoProvider(opts = {}) {
|
|
|
39
44
|
onMessage: (deviceId, msg) => {
|
|
40
45
|
if (msg.type === "send" && typeof msg.text === "string" && msg.text.trim()) {
|
|
41
46
|
const text = msg.text.trim();
|
|
47
|
+
const imageUrls = Array.isArray(msg.images)
|
|
48
|
+
? msg.images.filter((u) => typeof u === "string")
|
|
49
|
+
: [];
|
|
42
50
|
// 拦截 slash 命令,直接回复,不走 Agent
|
|
43
51
|
if (text.startsWith("/")) {
|
|
44
52
|
const runId = `linso-${Date.now()}`;
|
|
@@ -47,7 +55,7 @@ export async function monitorLinsoProvider(opts = {}) {
|
|
|
47
55
|
sendToClient(deviceId, { type: "done", runId });
|
|
48
56
|
return;
|
|
49
57
|
}
|
|
50
|
-
void handleIncomingMessage(deviceId, text, cfg, log).catch((err) => {
|
|
58
|
+
void handleIncomingMessage(deviceId, text, imageUrls, cfg, log).catch((err) => {
|
|
51
59
|
log(`[Linso] 处理消息出错: ${String(err)}`);
|
|
52
60
|
sendToClient(deviceId, { type: "error", message: String(err) });
|
|
53
61
|
});
|
|
@@ -65,11 +73,53 @@ export async function monitorLinsoProvider(opts = {}) {
|
|
|
65
73
|
});
|
|
66
74
|
}
|
|
67
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* 下载图片 URL 到本地临时文件,返回本地路径
|
|
78
|
+
* 和 Telegram/Discord 插件处理方式一致
|
|
79
|
+
*/
|
|
80
|
+
async function downloadImageToTemp(url, index) {
|
|
81
|
+
return new Promise((resolve) => {
|
|
82
|
+
try {
|
|
83
|
+
const ext = url.includes(".png") ? ".png" : url.includes(".gif") ? ".gif" : ".jpg";
|
|
84
|
+
const tmpPath = path.join(os.tmpdir(), `linso-img-${Date.now()}-${index}${ext}`);
|
|
85
|
+
const file = fs.createWriteStream(tmpPath);
|
|
86
|
+
const client = url.startsWith("https") ? https : http;
|
|
87
|
+
const req = client.get(url, (res) => {
|
|
88
|
+
if (res.statusCode !== 200) {
|
|
89
|
+
file.close();
|
|
90
|
+
resolve(null);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
res.pipe(file);
|
|
94
|
+
file.on("finish", () => {
|
|
95
|
+
file.close();
|
|
96
|
+
resolve(tmpPath);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
req.on("error", () => { file.close(); resolve(null); });
|
|
100
|
+
req.setTimeout(15000, () => { req.destroy(); file.close(); resolve(null); });
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
resolve(null);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
68
107
|
/**
|
|
69
108
|
* 把 iOS 消息路由到 Agent,流式回复(核心逻辑不变)
|
|
70
109
|
*/
|
|
71
|
-
async function handleIncomingMessage(deviceId, text, cfg, log) {
|
|
110
|
+
async function handleIncomingMessage(deviceId, text, imageUrls, cfg, log) {
|
|
72
111
|
const core = getLinsoRuntime();
|
|
112
|
+
// 下载图片到本地临时文件(和 Telegram/Discord 一致)
|
|
113
|
+
const localPaths = [];
|
|
114
|
+
if (imageUrls.length > 0) {
|
|
115
|
+
log(`[Linso] 下载 ${imageUrls.length} 张图片...`);
|
|
116
|
+
const results = await Promise.all(imageUrls.map((url, i) => downloadImageToTemp(url, i)));
|
|
117
|
+
for (const p of results) {
|
|
118
|
+
if (p)
|
|
119
|
+
localPaths.push(p);
|
|
120
|
+
}
|
|
121
|
+
log(`[Linso] 下载完成: ${localPaths.length}/${imageUrls.length}`);
|
|
122
|
+
}
|
|
73
123
|
const route = core.channel.routing.resolveAgentRoute({
|
|
74
124
|
cfg,
|
|
75
125
|
channel: "linso",
|
|
@@ -104,6 +154,10 @@ async function handleIncomingMessage(deviceId, text, cfg, log) {
|
|
|
104
154
|
WasMentioned: true,
|
|
105
155
|
OriginatingChannel: "linso",
|
|
106
156
|
OriginatingTo: `device:${deviceId}`,
|
|
157
|
+
...(localPaths.length > 0 && {
|
|
158
|
+
MediaPaths: localPaths,
|
|
159
|
+
MediaTypes: localPaths.map(() => "image/jpeg"),
|
|
160
|
+
}),
|
|
107
161
|
});
|
|
108
162
|
const runId = `linso-${Date.now()}`;
|
|
109
163
|
const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({
|
package/dist/src/relay-client.js
CHANGED
|
@@ -20,7 +20,7 @@ export function sendToRelay(msg) {
|
|
|
20
20
|
}
|
|
21
21
|
function _connect(relayUrl, callbacks) {
|
|
22
22
|
const { appToken, onMessage, onReady, onDisconnected, log } = callbacks;
|
|
23
|
-
const url = relayUrl.replace(/\/$/, "") + "/plugin";
|
|
23
|
+
const url = relayUrl.replace(/\/$/, "") + "/ws/plugin";
|
|
24
24
|
log(`[Linso] 连接 Relay: ${url}`);
|
|
25
25
|
ws = new WebSocket(url);
|
|
26
26
|
ws.on("open", () => {
|
package/dist/src/types.d.ts
CHANGED
package/dist/src/types.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const RELAY_URL = "ws://api.linso.ai";
|
|
1
2
|
export function resolveLinsoConfig(cfg) {
|
|
2
3
|
return cfg.channels?.linso ?? {};
|
|
3
4
|
}
|
|
@@ -6,8 +7,8 @@ export function resolveLinsoAccount(cfg, accountId = "default") {
|
|
|
6
7
|
return {
|
|
7
8
|
accountId,
|
|
8
9
|
enabled: c.enabled ?? false,
|
|
9
|
-
configured: !!(c.
|
|
10
|
-
relayUrl:
|
|
10
|
+
configured: !!(c.appToken),
|
|
11
|
+
relayUrl: RELAY_URL,
|
|
11
12
|
appToken: c.appToken ?? "",
|
|
12
13
|
agentId: c.agentId ?? "main",
|
|
13
14
|
dmPolicy: c.dmPolicy ?? "open",
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-linso",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Linso iOS App channel plugin for OpenClaw — connects via cloud Relay Server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
|
+
"openclaw": {
|
|
8
|
+
"extensions": ["./dist/index.js"]
|
|
9
|
+
},
|
|
7
10
|
"files": [
|
|
8
11
|
"dist/",
|
|
9
12
|
"src/",
|
package/src/monitor.ts
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
// 核心 monitor:Plugin 主动连接云端 Relay(和飞书连 open.feishu.cn 一致)
|
|
3
3
|
// 收到 iOS 消息后路由到 Agent,流式回复经 Relay 推回 iOS
|
|
4
4
|
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import os from "os";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import https from "https";
|
|
9
|
+
import http from "http";
|
|
5
10
|
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
6
11
|
import { getLinsoRuntime } from "./runtime.js";
|
|
7
12
|
import { connectToRelay, disconnectFromRelay } from "./relay-client.js";
|
|
@@ -27,8 +32,8 @@ export async function monitorLinsoProvider(opts: MonitorLinsoOpts = {}): Promise
|
|
|
27
32
|
return;
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
if (!account.
|
|
31
|
-
log("[Linso] 未配置
|
|
35
|
+
if (!account.appToken) {
|
|
36
|
+
log("[Linso] 未配置 appToken,跳过 monitor");
|
|
32
37
|
return;
|
|
33
38
|
}
|
|
34
39
|
|
|
@@ -55,6 +60,9 @@ export async function monitorLinsoProvider(opts: MonitorLinsoOpts = {}): Promise
|
|
|
55
60
|
onMessage: (deviceId, msg) => {
|
|
56
61
|
if (msg.type === "send" && typeof msg.text === "string" && msg.text.trim()) {
|
|
57
62
|
const text = msg.text.trim();
|
|
63
|
+
const imageUrls: string[] = Array.isArray(msg.images)
|
|
64
|
+
? (msg.images as unknown[]).filter((u): u is string => typeof u === "string")
|
|
65
|
+
: [];
|
|
58
66
|
|
|
59
67
|
// 拦截 slash 命令,直接回复,不走 Agent
|
|
60
68
|
if (text.startsWith("/")) {
|
|
@@ -65,7 +73,7 @@ export async function monitorLinsoProvider(opts: MonitorLinsoOpts = {}): Promise
|
|
|
65
73
|
return;
|
|
66
74
|
}
|
|
67
75
|
|
|
68
|
-
void handleIncomingMessage(deviceId, text, cfg, log).catch((err) => {
|
|
76
|
+
void handleIncomingMessage(deviceId, text, imageUrls, cfg, log).catch((err) => {
|
|
69
77
|
log(`[Linso] 处理消息出错: ${String(err)}`);
|
|
70
78
|
sendToClient(deviceId, { type: "error", message: String(err) });
|
|
71
79
|
});
|
|
@@ -85,17 +93,61 @@ export async function monitorLinsoProvider(opts: MonitorLinsoOpts = {}): Promise
|
|
|
85
93
|
}
|
|
86
94
|
}
|
|
87
95
|
|
|
96
|
+
/**
|
|
97
|
+
* 下载图片 URL 到本地临时文件,返回本地路径
|
|
98
|
+
* 和 Telegram/Discord 插件处理方式一致
|
|
99
|
+
*/
|
|
100
|
+
async function downloadImageToTemp(url: string, index: number): Promise<string | null> {
|
|
101
|
+
return new Promise((resolve) => {
|
|
102
|
+
try {
|
|
103
|
+
const ext = url.includes(".png") ? ".png" : url.includes(".gif") ? ".gif" : ".jpg";
|
|
104
|
+
const tmpPath = path.join(os.tmpdir(), `linso-img-${Date.now()}-${index}${ext}`);
|
|
105
|
+
const file = fs.createWriteStream(tmpPath);
|
|
106
|
+
const client = url.startsWith("https") ? https : http;
|
|
107
|
+
|
|
108
|
+
const req = client.get(url, (res) => {
|
|
109
|
+
if (res.statusCode !== 200) {
|
|
110
|
+
file.close();
|
|
111
|
+
resolve(null);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
res.pipe(file);
|
|
115
|
+
file.on("finish", () => {
|
|
116
|
+
file.close();
|
|
117
|
+
resolve(tmpPath);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
req.on("error", () => { file.close(); resolve(null); });
|
|
121
|
+
req.setTimeout(15000, () => { req.destroy(); file.close(); resolve(null); });
|
|
122
|
+
} catch {
|
|
123
|
+
resolve(null);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
88
128
|
/**
|
|
89
129
|
* 把 iOS 消息路由到 Agent,流式回复(核心逻辑不变)
|
|
90
130
|
*/
|
|
91
131
|
async function handleIncomingMessage(
|
|
92
132
|
deviceId: string,
|
|
93
133
|
text: string,
|
|
134
|
+
imageUrls: string[],
|
|
94
135
|
cfg: OpenClawConfig,
|
|
95
136
|
log: (...args: unknown[]) => void,
|
|
96
137
|
): Promise<void> {
|
|
97
138
|
const core = getLinsoRuntime();
|
|
98
139
|
|
|
140
|
+
// 下载图片到本地临时文件(和 Telegram/Discord 一致)
|
|
141
|
+
const localPaths: string[] = [];
|
|
142
|
+
if (imageUrls.length > 0) {
|
|
143
|
+
log(`[Linso] 下载 ${imageUrls.length} 张图片...`);
|
|
144
|
+
const results = await Promise.all(imageUrls.map((url, i) => downloadImageToTemp(url, i)));
|
|
145
|
+
for (const p of results) {
|
|
146
|
+
if (p) localPaths.push(p);
|
|
147
|
+
}
|
|
148
|
+
log(`[Linso] 下载完成: ${localPaths.length}/${imageUrls.length}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
99
151
|
const route = core.channel.routing.resolveAgentRoute({
|
|
100
152
|
cfg,
|
|
101
153
|
channel: "linso",
|
|
@@ -133,6 +185,10 @@ async function handleIncomingMessage(
|
|
|
133
185
|
WasMentioned: true,
|
|
134
186
|
OriginatingChannel: "linso" as const,
|
|
135
187
|
OriginatingTo: `device:${deviceId}`,
|
|
188
|
+
...(localPaths.length > 0 && {
|
|
189
|
+
MediaPaths: localPaths,
|
|
190
|
+
MediaTypes: localPaths.map(() => "image/jpeg"),
|
|
191
|
+
}),
|
|
136
192
|
});
|
|
137
193
|
|
|
138
194
|
const runId = `linso-${Date.now()}`;
|
package/src/relay-client.ts
CHANGED
|
@@ -33,7 +33,7 @@ export function sendToRelay(msg: Record<string, unknown>) {
|
|
|
33
33
|
|
|
34
34
|
function _connect(relayUrl: string, callbacks: RelayCallbacks) {
|
|
35
35
|
const { appToken, onMessage, onReady, onDisconnected, log } = callbacks;
|
|
36
|
-
const url = relayUrl.replace(/\/$/, "") + "/plugin";
|
|
36
|
+
const url = relayUrl.replace(/\/$/, "") + "/ws/plugin";
|
|
37
37
|
log(`[Linso] 连接 Relay: ${url}`);
|
|
38
38
|
|
|
39
39
|
ws = new WebSocket(url);
|
package/src/types.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
2
2
|
|
|
3
|
+
const RELAY_URL = "ws://api.linso.ai";
|
|
4
|
+
|
|
3
5
|
export type LinsoChannelConfig = {
|
|
4
6
|
enabled?: boolean;
|
|
5
|
-
relayUrl?: string;
|
|
6
7
|
appToken?: string; // App 注册拿到的 token,Plugin 用它连接 Relay
|
|
7
8
|
agentId?: string;
|
|
8
9
|
dmPolicy?: "open" | "pairing";
|
|
@@ -28,8 +29,8 @@ export function resolveLinsoAccount(cfg: OpenClawConfig, accountId = "default"):
|
|
|
28
29
|
return {
|
|
29
30
|
accountId,
|
|
30
31
|
enabled: c.enabled ?? false,
|
|
31
|
-
configured: !!(c.
|
|
32
|
-
relayUrl:
|
|
32
|
+
configured: !!(c.appToken),
|
|
33
|
+
relayUrl: RELAY_URL,
|
|
33
34
|
appToken: c.appToken ?? "",
|
|
34
35
|
agentId: c.agentId ?? "main",
|
|
35
36
|
dmPolicy: c.dmPolicy ?? "open",
|