clawmatrix 0.1.14 → 0.1.15
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/BOOTSTRAP.md +40 -8
- package/package.json +4 -2
- package/src/cluster-service.ts +5 -4
- package/src/config.ts +57 -6
- package/src/connection.ts +20 -3
- package/src/device-info.ts +48 -0
- package/src/handoff.ts +20 -5
- package/src/index.ts +14 -4
- package/src/model-proxy.ts +530 -229
- package/src/peer-manager.ts +11 -2
- package/src/router.ts +31 -23
- package/src/types.ts +24 -0
- package/src/web-ui.ts +227 -20
- package/src/web.ts +55 -1
package/BOOTSTRAP.md
CHANGED
|
@@ -122,7 +122,8 @@ openclaw clawmatrix status
|
|
|
122
122
|
],
|
|
123
123
|
"models": [],
|
|
124
124
|
"proxyModels": [
|
|
125
|
-
{ "id": "
|
|
125
|
+
{ "id": "gpt-5", "nodeId": "office-01", "description": "GPT-5", "input": ["text", "image"], "contextWindow": 400000, "maxTokens": 128000 },
|
|
126
|
+
{ "id": "gemini-2.5-pro", "nodeId": "office-01", "description": "Gemini 2.5 Pro", "input": ["text", "image"], "contextWindow": 1048576, "maxTokens": 65535 }
|
|
126
127
|
],
|
|
127
128
|
"tags": ["home"]
|
|
128
129
|
}
|
|
@@ -165,9 +166,40 @@ openclaw clawmatrix status
|
|
|
165
166
|
| `toolProxy.deny` | array | `[]` | 禁止的工具名(优先于 allow) |
|
|
166
167
|
| `toolProxy.maxOutputBytes` | number | `1048576` | 单次响应最大字节数 |
|
|
167
168
|
|
|
168
|
-
##
|
|
169
|
+
## 启用集群工具
|
|
169
170
|
|
|
170
|
-
|
|
171
|
+
ClawMatrix 的 Agent 工具注册为可选工具(optional),需要在 OpenClaw 配置中显式启用。
|
|
172
|
+
|
|
173
|
+
在 `openclaw.json` 中添加:
|
|
174
|
+
|
|
175
|
+
```json
|
|
176
|
+
{
|
|
177
|
+
"tools": {
|
|
178
|
+
"allow": ["clawmatrix"]
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
或按 Agent 粒度启用:
|
|
184
|
+
|
|
185
|
+
```json
|
|
186
|
+
{
|
|
187
|
+
"agents": {
|
|
188
|
+
"list": [
|
|
189
|
+
{
|
|
190
|
+
"id": "main",
|
|
191
|
+
"tools": {
|
|
192
|
+
"allow": ["clawmatrix"]
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
]
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
> 如果不配置,Agent 的系统提示中会提到集群工具,但实际无法调用。
|
|
201
|
+
|
|
202
|
+
启用后,本节点的 Agent 获得 7 个集群工具:
|
|
171
203
|
|
|
172
204
|
| 工具 | 用途 | 关键参数 |
|
|
173
205
|
|------|------|----------|
|
|
@@ -199,13 +231,13 @@ openclaw clawmatrix status
|
|
|
199
231
|
"api": "openai-completions",
|
|
200
232
|
"models": [
|
|
201
233
|
{
|
|
202
|
-
"id": "
|
|
203
|
-
"name": "
|
|
234
|
+
"id": "gpt-5",
|
|
235
|
+
"name": "GPT-5",
|
|
204
236
|
"reasoning": false,
|
|
205
|
-
"input": ["text"],
|
|
237
|
+
"input": ["text", "image"],
|
|
206
238
|
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
|
|
207
|
-
"contextWindow":
|
|
208
|
-
"maxTokens":
|
|
239
|
+
"contextWindow": 400000,
|
|
240
|
+
"maxTokens": 128000
|
|
209
241
|
}
|
|
210
242
|
]
|
|
211
243
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawmatrix",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "Decentralized mesh cluster plugin for OpenClaw — inter-gateway communication, model proxy, task handoff, and tool proxy.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
"@types/ws": "^8.18.1"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
+
"openclaw": ">=2026.3.7",
|
|
44
45
|
"typescript": "^5"
|
|
45
|
-
}
|
|
46
|
+
},
|
|
47
|
+
"overrides": {}
|
|
46
48
|
}
|
package/src/cluster-service.ts
CHANGED
|
@@ -46,11 +46,11 @@ export class ClusterRuntime {
|
|
|
46
46
|
readonly toolProxy: ToolProxy;
|
|
47
47
|
private logger: PluginLogger;
|
|
48
48
|
|
|
49
|
-
constructor(config: ClawMatrixConfig, logger: PluginLogger, openclawConfig: OpenClawConfig) {
|
|
49
|
+
constructor(config: ClawMatrixConfig, logger: PluginLogger, openclawConfig: OpenClawConfig, openclawVersion?: string) {
|
|
50
50
|
this.config = config;
|
|
51
51
|
this.logger = logger;
|
|
52
52
|
const gatewayInfo = resolveGatewayInfo(openclawConfig);
|
|
53
|
-
this.peerManager = new PeerManager(config);
|
|
53
|
+
this.peerManager = new PeerManager(config, openclawVersion);
|
|
54
54
|
this.handoffManager = new HandoffManager(config, this.peerManager);
|
|
55
55
|
this.modelProxy = new ModelProxy(config, this.peerManager, gatewayInfo, openclawConfig);
|
|
56
56
|
this.toolProxy = new ToolProxy(config, this.peerManager, gatewayInfo);
|
|
@@ -72,7 +72,7 @@ export class ClusterRuntime {
|
|
|
72
72
|
|
|
73
73
|
// Web dashboard (must be set before peerManager.start() creates the HTTP server)
|
|
74
74
|
if (this.config.web?.enabled) {
|
|
75
|
-
const webHandler = new WebHandler(this.config, this.peerManager);
|
|
75
|
+
const webHandler = new WebHandler(this.config, this.peerManager, this.handoffManager);
|
|
76
76
|
this.peerManager.setHttpHandler((req, res) => webHandler.handle(req, res));
|
|
77
77
|
this.logger.info(`[clawmatrix] Web dashboard enabled on listen port`);
|
|
78
78
|
}
|
|
@@ -178,11 +178,12 @@ export function getClusterRuntime(): ClusterRuntime {
|
|
|
178
178
|
export function createClusterService(
|
|
179
179
|
config: ClawMatrixConfig,
|
|
180
180
|
openclawConfig: OpenClawConfig,
|
|
181
|
+
openclawVersion?: string,
|
|
181
182
|
): OpenClawPluginService {
|
|
182
183
|
return {
|
|
183
184
|
id: "clawmatrix",
|
|
184
185
|
start(ctx: OpenClawPluginServiceContext) {
|
|
185
|
-
clusterRuntime = new ClusterRuntime(config, ctx.logger, openclawConfig);
|
|
186
|
+
clusterRuntime = new ClusterRuntime(config, ctx.logger, openclawConfig, openclawVersion);
|
|
186
187
|
clusterRuntime.start();
|
|
187
188
|
},
|
|
188
189
|
async stop() {
|
package/src/config.ts
CHANGED
|
@@ -61,19 +61,28 @@ const ToolProxyConfigSchema = z.object({
|
|
|
61
61
|
maxOutputBytes: z.number().default(1_048_576),
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
// Per-model entry inside a proxy group (minimal — inherits group-level fields)
|
|
65
|
+
const ProxyModelEntrySchema = z.object({
|
|
65
66
|
id: z.string(),
|
|
66
|
-
nodeId: z.string().optional(),
|
|
67
67
|
description: z.string().optional(),
|
|
68
68
|
...ModelParamsSchema,
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
+
// Grouped proxy models: shared nodeId/api/provider, list of models
|
|
72
|
+
const ProxyModelGroupSchema = z.object({
|
|
73
|
+
nodeId: z.string(),
|
|
74
|
+
provider: z.string().optional(),
|
|
75
|
+
description: z.string().optional(),
|
|
76
|
+
...ModelParamsSchema,
|
|
77
|
+
models: z.array(ProxyModelEntrySchema),
|
|
78
|
+
});
|
|
79
|
+
|
|
71
80
|
const WebConfigSchema = z.object({
|
|
72
81
|
enabled: z.boolean().default(false),
|
|
73
82
|
token: z.string().min(8, "web token must be at least 8 characters"),
|
|
74
83
|
}).optional();
|
|
75
84
|
|
|
76
|
-
|
|
85
|
+
const RawClawMatrixConfigSchema = z.object({
|
|
77
86
|
nodeId: z.string(),
|
|
78
87
|
secret: z.string().min(16, "secret must be at least 16 characters"),
|
|
79
88
|
listen: z.boolean().default(false),
|
|
@@ -82,7 +91,7 @@ export const ClawMatrixConfigSchema = z.object({
|
|
|
82
91
|
peers: z.array(PeerConfigSchema).default([]),
|
|
83
92
|
agents: z.array(AgentInfoSchema).default([]),
|
|
84
93
|
models: z.array(ModelInfoSchema).default([]),
|
|
85
|
-
proxyModels: z.array(
|
|
94
|
+
proxyModels: z.array(ProxyModelGroupSchema).default([]),
|
|
86
95
|
tags: z.array(z.string()).default([]),
|
|
87
96
|
proxyPort: z.number().default(19001),
|
|
88
97
|
toolProxy: ToolProxyConfigSchema.optional(),
|
|
@@ -90,10 +99,52 @@ export const ClawMatrixConfigSchema = z.object({
|
|
|
90
99
|
web: WebConfigSchema,
|
|
91
100
|
});
|
|
92
101
|
|
|
93
|
-
|
|
102
|
+
/** Flat proxy model after group expansion (used internally). */
|
|
103
|
+
export interface ProxyModel {
|
|
104
|
+
id: string;
|
|
105
|
+
nodeId: string;
|
|
106
|
+
provider?: string;
|
|
107
|
+
description?: string;
|
|
108
|
+
api?: string;
|
|
109
|
+
contextWindow?: number;
|
|
110
|
+
maxTokens?: number;
|
|
111
|
+
reasoning?: boolean;
|
|
112
|
+
input?: ("text" | "image")[];
|
|
113
|
+
cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
114
|
+
compat?: z.infer<typeof ModelCompatSchema>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export type ClawMatrixConfig = Omit<z.infer<typeof RawClawMatrixConfigSchema>, "proxyModels"> & {
|
|
118
|
+
proxyModels: ProxyModel[];
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export { RawClawMatrixConfigSchema as ClawMatrixConfigSchema };
|
|
94
122
|
export type PeerConfig = z.infer<typeof PeerConfigSchema>;
|
|
95
123
|
export type ToolProxyConfig = z.infer<typeof ToolProxyConfigSchema>;
|
|
96
124
|
|
|
125
|
+
/** Parse and flatten grouped proxyModels into flat array. */
|
|
97
126
|
export function parseConfig(raw: unknown): ClawMatrixConfig {
|
|
98
|
-
|
|
127
|
+
const parsed = RawClawMatrixConfigSchema.parse(raw);
|
|
128
|
+
|
|
129
|
+
// Flatten proxy model groups
|
|
130
|
+
const proxyModels: ProxyModel[] = [];
|
|
131
|
+
for (const group of parsed.proxyModels) {
|
|
132
|
+
for (const m of group.models) {
|
|
133
|
+
proxyModels.push({
|
|
134
|
+
id: m.id,
|
|
135
|
+
nodeId: group.nodeId,
|
|
136
|
+
provider: group.provider,
|
|
137
|
+
description: m.description ?? group.description,
|
|
138
|
+
api: m.api ?? group.api,
|
|
139
|
+
contextWindow: m.contextWindow ?? group.contextWindow,
|
|
140
|
+
maxTokens: m.maxTokens ?? group.maxTokens,
|
|
141
|
+
reasoning: m.reasoning ?? group.reasoning,
|
|
142
|
+
input: m.input ?? group.input,
|
|
143
|
+
cost: m.cost ?? group.cost,
|
|
144
|
+
compat: m.compat ?? group.compat,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { ...parsed, proxyModels };
|
|
99
150
|
}
|
package/src/connection.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
AuthOk,
|
|
7
7
|
AuthFail,
|
|
8
8
|
ClusterFrame,
|
|
9
|
+
DeviceInfo,
|
|
9
10
|
ModelInfo,
|
|
10
11
|
NodeCapabilities,
|
|
11
12
|
} from "./types.ts";
|
|
@@ -28,6 +29,7 @@ export interface WsTransport {
|
|
|
28
29
|
export interface ConnectionEvents {
|
|
29
30
|
message: [frame: AnyClusterFrame];
|
|
30
31
|
authenticated: [capabilities: NodeCapabilities];
|
|
32
|
+
latency: [ms: number];
|
|
31
33
|
close: [code: number, reason: string];
|
|
32
34
|
error: [error: Error];
|
|
33
35
|
}
|
|
@@ -48,6 +50,9 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|
|
48
50
|
private missedPongs = 0;
|
|
49
51
|
private pendingNonce: string | null = null;
|
|
50
52
|
private closed = false;
|
|
53
|
+
private lastPingSentAt = 0;
|
|
54
|
+
/** Exponential moving average of heartbeat RTT in milliseconds. */
|
|
55
|
+
latencyMs = 0;
|
|
51
56
|
|
|
52
57
|
constructor(
|
|
53
58
|
transport: WsTransport,
|
|
@@ -142,6 +147,12 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|
|
142
147
|
}
|
|
143
148
|
if (frame.type === "pong") {
|
|
144
149
|
this.missedPongs = 0;
|
|
150
|
+
if (this.lastPingSentAt > 0) {
|
|
151
|
+
const rtt = Date.now() - this.lastPingSentAt;
|
|
152
|
+
// Exponential moving average (α = 0.3)
|
|
153
|
+
this.latencyMs = this.latencyMs === 0 ? rtt : Math.round(this.latencyMs * 0.7 + rtt * 0.3);
|
|
154
|
+
this.emit("latency", this.latencyMs);
|
|
155
|
+
}
|
|
145
156
|
return;
|
|
146
157
|
}
|
|
147
158
|
|
|
@@ -151,12 +162,13 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|
|
151
162
|
private async handleAuthMessage(frame: AnyClusterFrame) {
|
|
152
163
|
if (this.role === "inbound") {
|
|
153
164
|
if (frame.type !== "auth") return;
|
|
154
|
-
const { nodeId, sig, agents, models, tags } = frame.payload as {
|
|
165
|
+
const { nodeId, sig, agents, models, tags, deviceInfo } = frame.payload as {
|
|
155
166
|
nodeId: string;
|
|
156
167
|
sig: string;
|
|
157
168
|
agents?: AgentInfo[];
|
|
158
169
|
models?: ModelInfo[];
|
|
159
170
|
tags?: string[];
|
|
171
|
+
deviceInfo?: DeviceInfo;
|
|
160
172
|
};
|
|
161
173
|
if (!this.pendingNonce) return;
|
|
162
174
|
|
|
@@ -180,6 +192,7 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|
|
180
192
|
agents: agents ?? [],
|
|
181
193
|
models: models ?? [],
|
|
182
194
|
tags: tags ?? [],
|
|
195
|
+
deviceInfo,
|
|
183
196
|
};
|
|
184
197
|
this.authenticated = true;
|
|
185
198
|
this.clearAuthTimer();
|
|
@@ -193,8 +206,9 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|
|
193
206
|
agents: this.localCapabilities.agents,
|
|
194
207
|
models: this.localCapabilities.models,
|
|
195
208
|
tags: this.localCapabilities.tags,
|
|
209
|
+
deviceInfo: this.localCapabilities.deviceInfo,
|
|
196
210
|
},
|
|
197
|
-
}
|
|
211
|
+
} as AuthOk);
|
|
198
212
|
|
|
199
213
|
this.startHeartbeat();
|
|
200
214
|
this.emit("authenticated", this.remoteCapabilities);
|
|
@@ -212,6 +226,7 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|
|
212
226
|
agents: this.localCapabilities.agents,
|
|
213
227
|
models: this.localCapabilities.models,
|
|
214
228
|
tags: this.localCapabilities.tags,
|
|
229
|
+
deviceInfo: this.localCapabilities.deviceInfo,
|
|
215
230
|
},
|
|
216
231
|
});
|
|
217
232
|
return;
|
|
@@ -225,6 +240,7 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|
|
225
240
|
agents: ok.payload.agents,
|
|
226
241
|
models: ok.payload.models,
|
|
227
242
|
tags: ok.payload.tags,
|
|
243
|
+
deviceInfo: ok.payload.deviceInfo,
|
|
228
244
|
};
|
|
229
245
|
this.authenticated = true;
|
|
230
246
|
this.startHeartbeat();
|
|
@@ -258,10 +274,11 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|
|
258
274
|
this.close(4002, "heartbeat timeout");
|
|
259
275
|
return;
|
|
260
276
|
}
|
|
277
|
+
this.lastPingSentAt = Date.now();
|
|
261
278
|
this.sendRaw({
|
|
262
279
|
type: "ping",
|
|
263
280
|
from: this.nodeId,
|
|
264
|
-
timestamp:
|
|
281
|
+
timestamp: this.lastPingSentAt,
|
|
265
282
|
});
|
|
266
283
|
scheduleNext();
|
|
267
284
|
}, interval);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collect local device/system information using Node.js os module.
|
|
3
|
+
*/
|
|
4
|
+
import { arch, cpus, hostname, totalmem, type, release } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import type { DeviceInfo } from "./types.ts";
|
|
8
|
+
|
|
9
|
+
function readVersionFromPkgJson(dir: string): string | undefined {
|
|
10
|
+
try {
|
|
11
|
+
const raw = readFileSync(join(dir, "package.json"), "utf-8");
|
|
12
|
+
const pkg = JSON.parse(raw) as { name?: string; version?: string };
|
|
13
|
+
if (pkg.name === "openclaw" && pkg.version) return pkg.version;
|
|
14
|
+
} catch { /* not found */ }
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolveOpenclawVersion(hint?: string): string | undefined {
|
|
19
|
+
if (hint && hint !== "unknown") return hint;
|
|
20
|
+
// Walk up from the OpenClaw entry script (process.argv[1]) to find package.json
|
|
21
|
+
try {
|
|
22
|
+
const entry = process.argv[1];
|
|
23
|
+
if (!entry) return undefined;
|
|
24
|
+
let dir = dirname(entry);
|
|
25
|
+
for (let i = 0; i < 10; i++) {
|
|
26
|
+
const v = readVersionFromPkgJson(dir);
|
|
27
|
+
if (v) return v;
|
|
28
|
+
const parent = dirname(dir);
|
|
29
|
+
if (parent === dir) break;
|
|
30
|
+
dir = parent;
|
|
31
|
+
}
|
|
32
|
+
} catch { /* best effort */ }
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Collect device info once at startup. */
|
|
37
|
+
export function collectDeviceInfo(openclawVersion?: string): DeviceInfo {
|
|
38
|
+
const cpuList = cpus();
|
|
39
|
+
return {
|
|
40
|
+
os: `${type()} ${release()}`,
|
|
41
|
+
arch: arch(),
|
|
42
|
+
cpuModel: cpuList[0]?.model?.trim() ?? "unknown",
|
|
43
|
+
cpuCores: cpuList.length,
|
|
44
|
+
totalMemoryMB: Math.round(totalmem() / (1024 * 1024)),
|
|
45
|
+
hostname: hostname(),
|
|
46
|
+
openclawVersion: resolveOpenclawVersion(openclawVersion),
|
|
47
|
+
};
|
|
48
|
+
}
|
package/src/handoff.ts
CHANGED
|
@@ -19,6 +19,7 @@ interface PendingHandoff {
|
|
|
19
19
|
task: string;
|
|
20
20
|
context?: string;
|
|
21
21
|
accumulated: string;
|
|
22
|
+
onStream?: (delta: string) => void;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export class HandoffManager {
|
|
@@ -36,13 +37,19 @@ export class HandoffManager {
|
|
|
36
37
|
target: string,
|
|
37
38
|
task: string,
|
|
38
39
|
context?: string,
|
|
40
|
+
options?: { nodeId?: string; onStream?: (delta: string) => void },
|
|
39
41
|
): Promise<HandoffResponse["payload"]> {
|
|
42
|
+
if (options?.nodeId) {
|
|
43
|
+
// Direct node targeting (e.g. from web UI) — skip router resolution
|
|
44
|
+
return this.sendHandoff(options.nodeId, target, task, context, 0, options.onStream);
|
|
45
|
+
}
|
|
46
|
+
|
|
40
47
|
const route = this.peerManager.router.resolveAgent(target);
|
|
41
48
|
if (!route) {
|
|
42
49
|
throw new Error(`No reachable agent for target "${target}"`);
|
|
43
50
|
}
|
|
44
51
|
|
|
45
|
-
return this.sendHandoff(route.nodeId, target, task, context, MAX_RETRIES);
|
|
52
|
+
return this.sendHandoff(route.nodeId, target, task, context, MAX_RETRIES, options?.onStream);
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
private sendHandoff(
|
|
@@ -51,13 +58,14 @@ export class HandoffManager {
|
|
|
51
58
|
task: string,
|
|
52
59
|
context: string | undefined,
|
|
53
60
|
retriesLeft: number,
|
|
61
|
+
onStream?: (delta: string) => void,
|
|
54
62
|
): Promise<HandoffResponse["payload"]> {
|
|
55
63
|
const id = crypto.randomUUID();
|
|
56
64
|
|
|
57
65
|
return new Promise<HandoffResponse["payload"]>((resolve, reject) => {
|
|
58
|
-
const timer = this.createTimeout(id, targetNodeId, target, task, context, retriesLeft, resolve, reject);
|
|
66
|
+
const timer = this.createTimeout(id, targetNodeId, target, task, context, retriesLeft, resolve, reject, onStream);
|
|
59
67
|
|
|
60
|
-
this.pending.set(id, { resolve, reject, timer, target, retriesLeft, task, context, accumulated: "" });
|
|
68
|
+
this.pending.set(id, { resolve, reject, timer, target, retriesLeft, task, context, accumulated: "", onStream });
|
|
61
69
|
|
|
62
70
|
const frame: HandoffRequest = {
|
|
63
71
|
type: "handoff_req",
|
|
@@ -77,7 +85,7 @@ export class HandoffManager {
|
|
|
77
85
|
if (retriesLeft > 0) {
|
|
78
86
|
const nextRoute = this.peerManager.router.resolveAgent(target);
|
|
79
87
|
if (nextRoute && nextRoute.nodeId !== targetNodeId) {
|
|
80
|
-
this.sendHandoff(nextRoute.nodeId, target, task, context, retriesLeft - 1)
|
|
88
|
+
this.sendHandoff(nextRoute.nodeId, target, task, context, retriesLeft - 1, onStream)
|
|
81
89
|
.then(resolve)
|
|
82
90
|
.catch(reject);
|
|
83
91
|
return;
|
|
@@ -97,6 +105,7 @@ export class HandoffManager {
|
|
|
97
105
|
retriesLeft: number,
|
|
98
106
|
resolve: (result: HandoffResponse["payload"]) => void,
|
|
99
107
|
reject: (error: Error) => void,
|
|
108
|
+
onStream?: (delta: string) => void,
|
|
100
109
|
): ReturnType<typeof setTimeout> {
|
|
101
110
|
return setTimeout(() => {
|
|
102
111
|
this.pending.delete(id);
|
|
@@ -106,7 +115,7 @@ export class HandoffManager {
|
|
|
106
115
|
if (retriesLeft > 0) {
|
|
107
116
|
const nextRoute = this.peerManager.router.resolveAgent(target);
|
|
108
117
|
if (nextRoute && nextRoute.nodeId !== targetNodeId) {
|
|
109
|
-
this.sendHandoff(nextRoute.nodeId, target, task, context, retriesLeft - 1)
|
|
118
|
+
this.sendHandoff(nextRoute.nodeId, target, task, context, retriesLeft - 1, onStream)
|
|
110
119
|
.then(resolve)
|
|
111
120
|
.catch(reject);
|
|
112
121
|
return;
|
|
@@ -125,6 +134,11 @@ export class HandoffManager {
|
|
|
125
134
|
|
|
126
135
|
pending.accumulated += frame.payload.delta;
|
|
127
136
|
|
|
137
|
+
// Notify stream listener
|
|
138
|
+
if (pending.onStream && frame.payload.delta) {
|
|
139
|
+
pending.onStream(frame.payload.delta);
|
|
140
|
+
}
|
|
141
|
+
|
|
128
142
|
// Reset timeout — the remote agent is still working
|
|
129
143
|
clearTimeout(pending.timer);
|
|
130
144
|
pending.timer = this.createTimeout(
|
|
@@ -136,6 +150,7 @@ export class HandoffManager {
|
|
|
136
150
|
pending.retriesLeft,
|
|
137
151
|
pending.resolve,
|
|
138
152
|
pending.reject,
|
|
153
|
+
pending.onStream,
|
|
139
154
|
);
|
|
140
155
|
}
|
|
141
156
|
|
package/src/index.ts
CHANGED
|
@@ -39,7 +39,7 @@ const plugin = {
|
|
|
39
39
|
const config = parseConfig(api.pluginConfig);
|
|
40
40
|
|
|
41
41
|
// Background service: manages mesh connections, WS listener, heartbeat
|
|
42
|
-
api.registerService(createClusterService(config, api.config));
|
|
42
|
+
api.registerService(createClusterService(config, api.config, api.runtime.version));
|
|
43
43
|
|
|
44
44
|
// Model providers: register per-node providers so models are accessed as nodeId/modelId
|
|
45
45
|
const baseUrl = `http://127.0.0.1:${config.proxyPort}/v1`;
|
|
@@ -50,12 +50,21 @@ const plugin = {
|
|
|
50
50
|
// We must patch BOTH api.config (cfgAtStart) AND the runtimeConfigSnapshot
|
|
51
51
|
// (returned by loadConfig()) because activateSecretsRuntimeSnapshot clones the
|
|
52
52
|
// config before plugins load, so api.config and the snapshot are separate objects.
|
|
53
|
+
// Determine per-node API type from proxyModels (all models in a group share the same api)
|
|
54
|
+
const nodeApiType: Record<string, string> = {};
|
|
55
|
+
for (const m of config.proxyModels) {
|
|
56
|
+
if (m.api && !nodeApiType[m.nodeId]) {
|
|
57
|
+
nodeApiType[m.nodeId] = m.api;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
53
61
|
const patchProviders = (cfg: Record<string, unknown>) => {
|
|
54
62
|
const models = ((cfg).models ??= {}) as Record<string, unknown>;
|
|
55
63
|
const providers = (models.providers ??= {}) as Record<string, unknown>;
|
|
56
64
|
for (const [nodeId, nodeModels] of Object.entries(modelsByNode)) {
|
|
57
65
|
if (!providers[nodeId]) {
|
|
58
|
-
|
|
66
|
+
const api = nodeApiType[nodeId] ?? "openai-completions";
|
|
67
|
+
providers[nodeId] = { baseUrl, apiKey: "sk-clawmatrix-proxy", api, models: nodeModels };
|
|
59
68
|
}
|
|
60
69
|
}
|
|
61
70
|
};
|
|
@@ -63,7 +72,8 @@ const plugin = {
|
|
|
63
72
|
patchProviders(api.config as Record<string, unknown>);
|
|
64
73
|
|
|
65
74
|
// Also patch the runtime config snapshot (loadConfig returns it by reference).
|
|
66
|
-
// api.
|
|
75
|
+
// activateSecretsRuntimeSnapshot clones the config, so api.config and the
|
|
76
|
+
// snapshot returned by loadConfig() are separate objects — patch both.
|
|
67
77
|
try {
|
|
68
78
|
const snapshot = api.runtime.config.loadConfig();
|
|
69
79
|
if (snapshot && snapshot !== api.config) {
|
|
@@ -199,7 +209,7 @@ const plugin = {
|
|
|
199
209
|
"This ensures the user always knows when operations leave the local node.",
|
|
200
210
|
);
|
|
201
211
|
|
|
202
|
-
return {
|
|
212
|
+
return { prependSystemContext: lines.join("\n") };
|
|
203
213
|
} catch {
|
|
204
214
|
return;
|
|
205
215
|
}
|