@lotaber_wang/openclaw-dc-plugin 0.1.4 → 0.1.6
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/README.md +4 -1
- package/index.js +0 -4
- package/lib/mcp-bridge.js +119 -7
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -87,8 +87,11 @@ openclaw plugins install @lotaber_wang/openclaw-dc-plugin
|
|
|
87
87
|
- 本机 Node 版本是否 >= 18.14.1
|
|
88
88
|
- OpenClaw 进程是否有临时目录写权限
|
|
89
89
|
|
|
90
|
-
从 `0.1.
|
|
90
|
+
从 `0.1.6` 开始,插件 bridge 会额外做这些兼容处理:
|
|
91
91
|
|
|
92
92
|
- `initialize` 超时后自动切换到无缓冲 / PTY 启动策略重试
|
|
93
|
+
- `initialize` 默认超时提升到 45 秒,避免宿主启动稍慢时过早失败
|
|
93
94
|
- 兼容解析裸 JSON 输出,不再只接受 `Content-Length` 帧
|
|
95
|
+
- 启动时如果 stdout 混入人类可读日志,会先自动丢弃噪音再解析协议消息
|
|
96
|
+
- 如果拿到的是半截 JSON,会先继续等待后续分片,而不是立刻按失败处理
|
|
94
97
|
- 过滤 PTY 场景下被回显到 stdout 的请求消息
|
package/index.js
CHANGED
|
@@ -697,10 +697,6 @@ const plugin = {
|
|
|
697
697
|
for (const definition of toolDefinitions) {
|
|
698
698
|
registerProxyTool(api, bridge, definition);
|
|
699
699
|
}
|
|
700
|
-
|
|
701
|
-
api.logger.info?.(
|
|
702
|
-
`[TapTap DC] OpenClaw plugin v${pluginVersion} initialised with ${toolDefinitions.length} tools`
|
|
703
|
-
);
|
|
704
700
|
},
|
|
705
701
|
};
|
|
706
702
|
|
package/lib/mcp-bridge.js
CHANGED
|
@@ -9,7 +9,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
9
9
|
const require = createRequire(import.meta.url);
|
|
10
10
|
const HEADER_SEPARATOR = '\r\n\r\n';
|
|
11
11
|
const RUNTIME_PACKAGE_NAME = '@mikoto_zero/minigame-open-mcp';
|
|
12
|
-
const DEFAULT_INIT_TIMEOUT_MS =
|
|
12
|
+
const DEFAULT_INIT_TIMEOUT_MS = 45000;
|
|
13
|
+
const ANSI_ESCAPE_REGEX = /\x1B\[[0-?]*[ -/]*[@-~]/g;
|
|
13
14
|
|
|
14
15
|
function readPluginPackageJson() {
|
|
15
16
|
try {
|
|
@@ -75,6 +76,81 @@ function shellEscape(value) {
|
|
|
75
76
|
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
function stripAnsi(text) {
|
|
80
|
+
return text.replace(ANSI_ESCAPE_REGEX, '');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function findLikelyFrameStart(buffer) {
|
|
84
|
+
return buffer.indexOf('Content-Length:', 0, 'utf8');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function findLikelyJsonStart(buffer) {
|
|
88
|
+
const text = buffer.toString('utf8');
|
|
89
|
+
const candidates = ['{"jsonrpc"', '{"result"', '{"method"', '{"error"', '[{"jsonrpc"'];
|
|
90
|
+
let bestIndex = -1;
|
|
91
|
+
|
|
92
|
+
for (const candidate of candidates) {
|
|
93
|
+
const index = text.indexOf(candidate);
|
|
94
|
+
if (index !== -1 && (bestIndex === -1 || index < bestIndex)) {
|
|
95
|
+
bestIndex = index;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return bestIndex;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function trimNonProtocolNoise(buffer, logger) {
|
|
103
|
+
const frameStart = findLikelyFrameStart(buffer);
|
|
104
|
+
const jsonStart = findLikelyJsonStart(buffer);
|
|
105
|
+
|
|
106
|
+
let start = -1;
|
|
107
|
+
if (frameStart !== -1 && jsonStart !== -1) {
|
|
108
|
+
start = Math.min(frameStart, jsonStart);
|
|
109
|
+
} else {
|
|
110
|
+
start = frameStart !== -1 ? frameStart : jsonStart;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (start > 0) {
|
|
114
|
+
const dropped = stripAnsi(buffer.subarray(0, start).toString('utf8')).trim();
|
|
115
|
+
if (dropped) {
|
|
116
|
+
logger?.info?.(`[TapTap DC] Ignoring non-protocol stdout noise: ${dropped.slice(0, 300)}`);
|
|
117
|
+
}
|
|
118
|
+
return buffer.subarray(start);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return buffer;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function looksLikeJsonRpcPayload(text) {
|
|
125
|
+
return /"jsonrpc"\s*:\s*"2\.0"/.test(text) || /"method"\s*:/.test(text) || /"result"\s*:/.test(text);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function safeParseJson(text) {
|
|
129
|
+
try {
|
|
130
|
+
return {
|
|
131
|
+
kind: 'ok',
|
|
132
|
+
value: JSON.parse(text),
|
|
133
|
+
};
|
|
134
|
+
} catch (error) {
|
|
135
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
136
|
+
if (
|
|
137
|
+
message.includes('Unexpected end of JSON input') ||
|
|
138
|
+
message.includes('Unterminated string') ||
|
|
139
|
+
message.includes('Expected double-quoted property name')
|
|
140
|
+
) {
|
|
141
|
+
return {
|
|
142
|
+
kind: 'incomplete',
|
|
143
|
+
error,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
kind: 'invalid',
|
|
149
|
+
error,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
78
154
|
function parseBareJsonMessage(buffer) {
|
|
79
155
|
const text = buffer.toString('utf8');
|
|
80
156
|
let start = 0;
|
|
@@ -137,7 +213,7 @@ function parseBareJsonMessage(buffer) {
|
|
|
137
213
|
|
|
138
214
|
function findNextMessageOffset(buffer) {
|
|
139
215
|
const framedIndex = buffer.indexOf('Content-Length:', 0, 'utf8');
|
|
140
|
-
const jsonIndex = buffer
|
|
216
|
+
const jsonIndex = findLikelyJsonStart(buffer);
|
|
141
217
|
|
|
142
218
|
if (framedIndex === -1) {
|
|
143
219
|
return jsonIndex;
|
|
@@ -150,7 +226,7 @@ function findNextMessageOffset(buffer) {
|
|
|
150
226
|
return Math.min(framedIndex, jsonIndex);
|
|
151
227
|
}
|
|
152
228
|
|
|
153
|
-
function parseMessageBuffer(buffer, onMessage) {
|
|
229
|
+
function parseMessageBuffer(buffer, onMessage, logger) {
|
|
154
230
|
let offset = 0;
|
|
155
231
|
|
|
156
232
|
while (offset < buffer.length) {
|
|
@@ -175,7 +251,20 @@ function parseMessageBuffer(buffer, onMessage) {
|
|
|
175
251
|
}
|
|
176
252
|
|
|
177
253
|
const bodyText = buffer.subarray(bodyStart, bodyEnd).toString('utf8');
|
|
178
|
-
|
|
254
|
+
const parsed = safeParseJson(bodyText);
|
|
255
|
+
if (parsed.kind === 'ok') {
|
|
256
|
+
onMessage(parsed.value);
|
|
257
|
+
offset = bodyEnd;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (parsed.kind === 'incomplete') {
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
logger?.info?.(
|
|
266
|
+
`[TapTap DC] Ignoring invalid framed MCP payload: ${bodyText.slice(0, 300)}`
|
|
267
|
+
);
|
|
179
268
|
offset = bodyEnd;
|
|
180
269
|
continue;
|
|
181
270
|
}
|
|
@@ -183,7 +272,26 @@ function parseMessageBuffer(buffer, onMessage) {
|
|
|
183
272
|
|
|
184
273
|
const bareJson = parseBareJsonMessage(buffer.subarray(offset));
|
|
185
274
|
if (bareJson) {
|
|
186
|
-
|
|
275
|
+
const parsed = safeParseJson(bareJson.raw);
|
|
276
|
+
if (parsed.kind === 'ok') {
|
|
277
|
+
if (looksLikeJsonRpcPayload(bareJson.raw)) {
|
|
278
|
+
onMessage(parsed.value);
|
|
279
|
+
} else {
|
|
280
|
+
logger?.info?.(
|
|
281
|
+
`[TapTap DC] Ignoring bare JSON stdout noise: ${bareJson.raw.slice(0, 300)}`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
offset += bareJson.consumedBytes;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (parsed.kind === 'incomplete') {
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
logger?.info?.(
|
|
293
|
+
`[TapTap DC] Ignoring invalid bare JSON payload: ${bareJson.raw.slice(0, 300)}`
|
|
294
|
+
);
|
|
187
295
|
offset += bareJson.consumedBytes;
|
|
188
296
|
continue;
|
|
189
297
|
}
|
|
@@ -547,8 +655,11 @@ export class TapTapMcpBridge {
|
|
|
547
655
|
this.child.stdout.on('data', (chunk) => {
|
|
548
656
|
try {
|
|
549
657
|
this.stdoutBuffer = Buffer.concat([this.stdoutBuffer, chunk]);
|
|
550
|
-
this.stdoutBuffer =
|
|
551
|
-
|
|
658
|
+
this.stdoutBuffer = trimNonProtocolNoise(this.stdoutBuffer, this.logger);
|
|
659
|
+
this.stdoutBuffer = parseMessageBuffer(
|
|
660
|
+
this.stdoutBuffer,
|
|
661
|
+
(message) => this.handleMessage(message),
|
|
662
|
+
this.logger
|
|
552
663
|
);
|
|
553
664
|
} catch (error) {
|
|
554
665
|
this.logger?.error?.(
|
|
@@ -556,6 +667,7 @@ export class TapTapMcpBridge {
|
|
|
556
667
|
error instanceof Error ? error.message : String(error)
|
|
557
668
|
}`
|
|
558
669
|
);
|
|
670
|
+
this.stdoutBuffer = trimNonProtocolNoise(this.stdoutBuffer, this.logger);
|
|
559
671
|
}
|
|
560
672
|
});
|
|
561
673
|
|
package/openclaw.plugin.json
CHANGED