@lotaber_wang/openclaw-dc-plugin 0.1.3 → 0.1.5
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 +32 -4
- package/index.js +36 -5
- package/lib/mcp-bridge.js +327 -64
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/taptap-dc-ops-brief/SKILL.md +4 -2
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
- 运行时解析顺序为:插件内 bundled runtime -> 本地已安装 runtime -> 本地缓存 runtime
|
|
10
10
|
- 暴露适合 agent / skill 消费的 TapTap DC 原始 JSON 工具
|
|
11
11
|
- 提供高层工具 `taptap_dc_quick_brief`,可直接按游戏名 / app_id 生成 TapTap DC 快速简报
|
|
12
|
-
- 如果当前未授权,`taptap_dc_quick_brief`
|
|
12
|
+
- 如果当前未授权,`taptap_dc_quick_brief` 和 `taptap_dc_start_authorization` 都会直接返回可点击的授权超链接与二维码链接
|
|
13
13
|
- 覆盖授权、选游戏、商店/评价/社区概览、商店快照、论坛内容、评价列表、点赞与官方回复
|
|
14
14
|
- 内置 `taptap-dc-ops-brief`,可把原始数据整理成简洁的运营简报
|
|
15
15
|
|
|
@@ -24,9 +24,10 @@ openclaw plugins install @lotaber_wang/openclaw-dc-plugin
|
|
|
24
24
|
优先推荐直接使用:
|
|
25
25
|
|
|
26
26
|
1. 直接调用 `taptap_dc_quick_brief`
|
|
27
|
-
2.
|
|
28
|
-
3.
|
|
29
|
-
4.
|
|
27
|
+
2. 如果还没授权,工具会直接返回可点击授权链接
|
|
28
|
+
3. 手机端优先直接点击授权链接;桌面端优先打开二维码后扫码
|
|
29
|
+
4. 完成扫码后调用 `taptap_dc_complete_authorization`
|
|
30
|
+
5. 再次调用 `taptap_dc_quick_brief`
|
|
30
31
|
|
|
31
32
|
示例:
|
|
32
33
|
|
|
@@ -57,6 +58,25 @@ openclaw plugins install @lotaber_wang/openclaw-dc-plugin
|
|
|
57
58
|
|
|
58
59
|
正常情况下,如果内嵌的 TapTap MCP 主包已经带了可用凭据,生产环境不需要额外配置 `client_id` / `client_secret`。
|
|
59
60
|
|
|
61
|
+
## 运行时环境变量
|
|
62
|
+
|
|
63
|
+
插件内部会继续向 bundled TapTap MCP runtime 透传或设置这些环境变量:
|
|
64
|
+
|
|
65
|
+
- `TAPTAP_MCP_TRANSPORT=stdio`
|
|
66
|
+
- `TAPTAP_MCP_ENV`
|
|
67
|
+
- `TAPTAP_MCP_ENABLE_RAW_TOOLS=true`
|
|
68
|
+
- `TAPTAP_MCP_WORKSPACE_ROOT`
|
|
69
|
+
- `TAPTAP_MCP_CACHE_DIR`
|
|
70
|
+
- `TAPTAP_MCP_TEMP_DIR`
|
|
71
|
+
- `TAPTAP_MCP_LOG_ROOT`
|
|
72
|
+
- `TAPTAP_MCP_VERBOSE`
|
|
73
|
+
|
|
74
|
+
如果你是在 OpenClaw / 容器环境中排查启动问题,优先关注:
|
|
75
|
+
|
|
76
|
+
- `TAPTAP_MCP_ENV`
|
|
77
|
+
- `TAPTAP_MCP_VERBOSE`
|
|
78
|
+
- `TAPTAP_MCP_LOG_ROOT`
|
|
79
|
+
|
|
60
80
|
## 安装排障
|
|
61
81
|
|
|
62
82
|
如果 OpenClaw 能安装插件,但第一次调用工具仍然偏慢,通常是在启动内嵌 TapTap 运行时,而不是在现场重新下载依赖。
|
|
@@ -66,3 +86,11 @@ openclaw plugins install @lotaber_wang/openclaw-dc-plugin
|
|
|
66
86
|
- 当前环境是否能访问 npm registry
|
|
67
87
|
- 本机 Node 版本是否 >= 18.14.1
|
|
68
88
|
- OpenClaw 进程是否有临时目录写权限
|
|
89
|
+
|
|
90
|
+
从 `0.1.5` 开始,插件 bridge 会额外做这些兼容处理:
|
|
91
|
+
|
|
92
|
+
- `initialize` 超时后自动切换到无缓冲 / PTY 启动策略重试
|
|
93
|
+
- `initialize` 默认超时提升到 20 秒,避免宿主启动稍慢时过早失败
|
|
94
|
+
- 兼容解析裸 JSON 输出,不再只接受 `Content-Length` 帧
|
|
95
|
+
- 启动时如果 stdout 混入人类可读日志,会先自动丢弃噪音再解析协议消息
|
|
96
|
+
- 过滤 PTY 场景下被回显到 stdout 的请求消息
|
package/index.js
CHANGED
|
@@ -50,6 +50,14 @@ function createSchema(properties = {}, required = []) {
|
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
function buildMarkdownLink(label, url) {
|
|
54
|
+
if (!url) {
|
|
55
|
+
return '-';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return `[${label}](${url})`;
|
|
59
|
+
}
|
|
60
|
+
|
|
53
61
|
function normalizeText(value) {
|
|
54
62
|
return String(value || '')
|
|
55
63
|
.trim()
|
|
@@ -187,11 +195,16 @@ function buildBriefText(appTitle, sections, meta = {}) {
|
|
|
187
195
|
}
|
|
188
196
|
|
|
189
197
|
function buildAuthGuideText(authPayload) {
|
|
198
|
+
const authUrl = authPayload?.auth_url;
|
|
199
|
+
const qrcodeUrl = authPayload?.qrcode_url;
|
|
190
200
|
const lines = [
|
|
191
201
|
'当前还没有完成 TapTap 授权,请先扫码授权。',
|
|
192
202
|
'',
|
|
193
|
-
|
|
194
|
-
|
|
203
|
+
'如果你现在是在手机上对话,优先直接点击下面这个授权链接:',
|
|
204
|
+
buildMarkdownLink('立即打开授权页面', authUrl),
|
|
205
|
+
'',
|
|
206
|
+
'如果你现在是在桌面端对话,可以打开二维码后用 TapTap 扫码授权:',
|
|
207
|
+
buildMarkdownLink('打开二维码图片', qrcodeUrl),
|
|
195
208
|
'',
|
|
196
209
|
'完成扫码后,请继续调用 `taptap_dc_complete_authorization`,然后再次调用 `taptap_dc_quick_brief`。',
|
|
197
210
|
];
|
|
@@ -200,6 +213,14 @@ function buildAuthGuideText(authPayload) {
|
|
|
200
213
|
lines.push('', `device_code:${authPayload.device_code}`);
|
|
201
214
|
}
|
|
202
215
|
|
|
216
|
+
if (authUrl) {
|
|
217
|
+
lines.push('', `授权直链:${authUrl}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (qrcodeUrl) {
|
|
221
|
+
lines.push(`二维码直链:${qrcodeUrl}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
203
224
|
return lines.join('\n');
|
|
204
225
|
}
|
|
205
226
|
|
|
@@ -276,9 +297,13 @@ function registerProxyTool(api, bridge, definition) {
|
|
|
276
297
|
try {
|
|
277
298
|
const text = await bridge.callTool(definition.mcpToolName, params || {});
|
|
278
299
|
const normalized = normalizeJsonText(text);
|
|
279
|
-
|
|
300
|
+
const parsed = tryParseJson(normalized);
|
|
301
|
+
const renderedText = definition.resultFormatter
|
|
302
|
+
? definition.resultFormatter(parsed, normalized)
|
|
303
|
+
: normalized;
|
|
304
|
+
return toolResult(renderedText, {
|
|
280
305
|
mcpToolName: definition.mcpToolName,
|
|
281
|
-
parsed
|
|
306
|
+
parsed,
|
|
282
307
|
});
|
|
283
308
|
} catch (error) {
|
|
284
309
|
return toolResult(
|
|
@@ -397,9 +422,15 @@ const toolDefinitions = [
|
|
|
397
422
|
name: 'taptap_dc_start_authorization',
|
|
398
423
|
label: 'TapTap Auth Start',
|
|
399
424
|
description:
|
|
400
|
-
'Start TapTap OAuth device flow and return
|
|
425
|
+
'Start TapTap OAuth device flow and return a mobile-friendly clickable auth link plus qrcode link.',
|
|
401
426
|
mcpToolName: 'start_oauth_authorization_raw',
|
|
402
427
|
parameters: createSchema(),
|
|
428
|
+
resultFormatter(parsed, normalized) {
|
|
429
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
430
|
+
return normalized;
|
|
431
|
+
}
|
|
432
|
+
return buildAuthGuideText(parsed);
|
|
433
|
+
},
|
|
403
434
|
},
|
|
404
435
|
{
|
|
405
436
|
name: 'taptap_dc_complete_authorization',
|
package/lib/mcp-bridge.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
@@ -9,6 +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 = 20000;
|
|
13
|
+
const ANSI_ESCAPE_REGEX = /\x1B\[[0-?]*[ -/]*[@-~]/g;
|
|
12
14
|
|
|
13
15
|
function readPluginPackageJson() {
|
|
14
16
|
try {
|
|
@@ -62,31 +64,185 @@ function getNpmExecutable() {
|
|
|
62
64
|
return process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
function hasCommand(command) {
|
|
68
|
+
const lookupCommand = process.platform === 'win32' ? 'where' : 'which';
|
|
69
|
+
const result = spawnSync(lookupCommand, [command], {
|
|
70
|
+
stdio: 'ignore',
|
|
71
|
+
});
|
|
72
|
+
return result.status === 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function shellEscape(value) {
|
|
76
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
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 parseBareJsonMessage(buffer) {
|
|
125
|
+
const text = buffer.toString('utf8');
|
|
126
|
+
let start = 0;
|
|
127
|
+
|
|
128
|
+
while (start < text.length && /\s/.test(text[start])) {
|
|
129
|
+
start += 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const firstChar = text[start];
|
|
133
|
+
if (firstChar !== '{' && firstChar !== '[') {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let depth = 0;
|
|
138
|
+
let inString = false;
|
|
139
|
+
let escaped = false;
|
|
140
|
+
|
|
141
|
+
for (let index = start; index < text.length; index += 1) {
|
|
142
|
+
const char = text[index];
|
|
143
|
+
|
|
144
|
+
if (inString) {
|
|
145
|
+
if (escaped) {
|
|
146
|
+
escaped = false;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (char === '\\') {
|
|
150
|
+
escaped = true;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (char === '"') {
|
|
154
|
+
inString = false;
|
|
155
|
+
}
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (char === '"') {
|
|
160
|
+
inString = true;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (char === '{' || char === '[') {
|
|
165
|
+
depth += 1;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (char === '}' || char === ']') {
|
|
170
|
+
depth -= 1;
|
|
171
|
+
if (depth === 0) {
|
|
172
|
+
const raw = text.slice(start, index + 1);
|
|
173
|
+
return {
|
|
174
|
+
raw,
|
|
175
|
+
consumedBytes: Buffer.byteLength(text.slice(0, index + 1), 'utf8'),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function findNextMessageOffset(buffer) {
|
|
185
|
+
const framedIndex = buffer.indexOf('Content-Length:', 0, 'utf8');
|
|
186
|
+
const jsonIndex = findLikelyJsonStart(buffer);
|
|
187
|
+
|
|
188
|
+
if (framedIndex === -1) {
|
|
189
|
+
return jsonIndex;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (jsonIndex === -1) {
|
|
193
|
+
return framedIndex;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return Math.min(framedIndex, jsonIndex);
|
|
197
|
+
}
|
|
198
|
+
|
|
65
199
|
function parseMessageBuffer(buffer, onMessage) {
|
|
66
200
|
let offset = 0;
|
|
67
201
|
|
|
68
202
|
while (offset < buffer.length) {
|
|
69
|
-
|
|
70
|
-
|
|
203
|
+
while (offset < buffer.length && /\s/.test(String.fromCharCode(buffer[offset]))) {
|
|
204
|
+
offset += 1;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (offset >= buffer.length) {
|
|
71
208
|
break;
|
|
72
209
|
}
|
|
73
210
|
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
211
|
+
const headerEnd = buffer.indexOf(HEADER_SEPARATOR, offset, 'utf8');
|
|
212
|
+
if (headerEnd !== -1) {
|
|
213
|
+
const headerText = buffer.subarray(offset, headerEnd).toString('utf8');
|
|
214
|
+
const contentLengthMatch = headerText.match(/content-length:\s*(\d+)/i);
|
|
215
|
+
if (contentLengthMatch) {
|
|
216
|
+
const contentLength = Number(contentLengthMatch[1]);
|
|
217
|
+
const bodyStart = headerEnd + HEADER_SEPARATOR.length;
|
|
218
|
+
const bodyEnd = bodyStart + contentLength;
|
|
219
|
+
if (buffer.length < bodyEnd) {
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const bodyText = buffer.subarray(bodyStart, bodyEnd).toString('utf8');
|
|
224
|
+
onMessage(JSON.parse(bodyText));
|
|
225
|
+
offset = bodyEnd;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const bareJson = parseBareJsonMessage(buffer.subarray(offset));
|
|
231
|
+
if (bareJson) {
|
|
232
|
+
onMessage(JSON.parse(bareJson.raw));
|
|
233
|
+
offset += bareJson.consumedBytes;
|
|
234
|
+
continue;
|
|
78
235
|
}
|
|
79
236
|
|
|
80
|
-
|
|
81
|
-
const bodyStart = headerEnd + HEADER_SEPARATOR.length;
|
|
82
|
-
const bodyEnd = bodyStart + contentLength;
|
|
83
|
-
if (buffer.length < bodyEnd) {
|
|
237
|
+
if (bareJson === undefined) {
|
|
84
238
|
break;
|
|
85
239
|
}
|
|
86
240
|
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
241
|
+
const nextOffset = findNextMessageOffset(buffer.subarray(offset + 1));
|
|
242
|
+
if (nextOffset === -1) {
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
offset += nextOffset + 1;
|
|
90
246
|
}
|
|
91
247
|
|
|
92
248
|
return buffer.subarray(offset);
|
|
@@ -116,6 +272,7 @@ export class TapTapMcpBridge {
|
|
|
116
272
|
this.readyPromise = null;
|
|
117
273
|
this.installPromise = null;
|
|
118
274
|
this.pending = new Map();
|
|
275
|
+
this.outboundMessages = [];
|
|
119
276
|
this.nextId = 1;
|
|
120
277
|
this.stdoutBuffer = Buffer.alloc(0);
|
|
121
278
|
}
|
|
@@ -282,66 +439,39 @@ export class TapTapMcpBridge {
|
|
|
282
439
|
|
|
283
440
|
async start() {
|
|
284
441
|
const runtime = await this.resolveRuntimeServer();
|
|
285
|
-
this.
|
|
286
|
-
|
|
287
|
-
);
|
|
442
|
+
const launchAttempts = this.getLaunchAttempts(runtime.serverPath);
|
|
443
|
+
const errors = [];
|
|
288
444
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
445
|
+
for (const attempt of launchAttempts) {
|
|
446
|
+
this.logger?.info?.(
|
|
447
|
+
`[TapTap DC] Starting embedded TapTap MCP runtime from ${runtime.source} using ${attempt.label}: ${runtime.serverPath}`
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
this.spawnChild(attempt.command, attempt.args);
|
|
293
451
|
|
|
294
|
-
this.child.stdout.on('data', (chunk) => {
|
|
295
452
|
try {
|
|
296
|
-
this.
|
|
297
|
-
|
|
298
|
-
this.handleMessage(message)
|
|
299
|
-
);
|
|
453
|
+
await this.initializeServer();
|
|
454
|
+
return;
|
|
300
455
|
} catch (error) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}`
|
|
456
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
457
|
+
errors.push(`${attempt.label}: ${message}`);
|
|
458
|
+
this.logger?.info?.(
|
|
459
|
+
`[TapTap DC] Runtime start attempt failed via ${attempt.label}, retrying if possible: ${message}`
|
|
305
460
|
);
|
|
461
|
+
await this.close();
|
|
306
462
|
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
this.child.stderr.on('data', (chunk) => {
|
|
310
|
-
const text = chunk.toString('utf8').trim();
|
|
311
|
-
if (text) {
|
|
312
|
-
this.logger?.info?.(`[TapTap MCP] ${text}`);
|
|
313
|
-
}
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
this.child.on('exit', (code, signal) => {
|
|
317
|
-
const error = new Error(
|
|
318
|
-
`Embedded TapTap MCP server exited unexpectedly (code=${code ?? 'null'}, signal=${signal ?? 'null'}).`
|
|
319
|
-
);
|
|
320
|
-
for (const pending of this.pending.values()) {
|
|
321
|
-
pending.reject(error);
|
|
322
|
-
}
|
|
323
|
-
this.pending.clear();
|
|
324
|
-
this.child = null;
|
|
325
|
-
this.readyPromise = null;
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
await this.request('initialize', {
|
|
329
|
-
protocolVersion: '2025-11-25',
|
|
330
|
-
capabilities: {
|
|
331
|
-
tools: {},
|
|
332
|
-
resources: {},
|
|
333
|
-
logging: {},
|
|
334
|
-
},
|
|
335
|
-
clientInfo: {
|
|
336
|
-
name: 'taptap-openclaw-dc-plugin',
|
|
337
|
-
version: '0.1.0',
|
|
338
|
-
},
|
|
339
|
-
});
|
|
463
|
+
}
|
|
340
464
|
|
|
341
|
-
|
|
465
|
+
throw new Error(
|
|
466
|
+
`Failed to start embedded TapTap MCP runtime. Attempts: ${errors.join(' | ')}`
|
|
467
|
+
);
|
|
342
468
|
}
|
|
343
469
|
|
|
344
470
|
handleMessage(message) {
|
|
471
|
+
if (this.isEchoedOutboundMessage(message)) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
345
475
|
if (message.id !== undefined && this.pending.has(message.id)) {
|
|
346
476
|
const pending = this.pending.get(message.id);
|
|
347
477
|
this.pending.delete(message.id);
|
|
@@ -361,6 +491,15 @@ export class TapTapMcpBridge {
|
|
|
361
491
|
throw new Error('Embedded TapTap MCP server is not running.');
|
|
362
492
|
}
|
|
363
493
|
|
|
494
|
+
this.outboundMessages.push({
|
|
495
|
+
id: message.id,
|
|
496
|
+
method: message.method,
|
|
497
|
+
payload: JSON.stringify(message),
|
|
498
|
+
});
|
|
499
|
+
if (this.outboundMessages.length > 20) {
|
|
500
|
+
this.outboundMessages.shift();
|
|
501
|
+
}
|
|
502
|
+
|
|
364
503
|
const body = JSON.stringify(message);
|
|
365
504
|
const frame = `Content-Length: ${Buffer.byteLength(body, 'utf8')}\r\n\r\n${body}`;
|
|
366
505
|
this.child.stdin.write(frame, 'utf8');
|
|
@@ -403,5 +542,129 @@ export class TapTapMcpBridge {
|
|
|
403
542
|
this.child = null;
|
|
404
543
|
this.readyPromise = null;
|
|
405
544
|
}
|
|
545
|
+
this.pending.clear();
|
|
546
|
+
this.outboundMessages = [];
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
getLaunchAttempts(serverPath) {
|
|
550
|
+
const attempts = [
|
|
551
|
+
{
|
|
552
|
+
label: 'direct stdio',
|
|
553
|
+
command: process.execPath,
|
|
554
|
+
args: [serverPath],
|
|
555
|
+
},
|
|
556
|
+
];
|
|
557
|
+
|
|
558
|
+
if (hasCommand('stdbuf')) {
|
|
559
|
+
attempts.push({
|
|
560
|
+
label: 'stdbuf unbuffered stdio',
|
|
561
|
+
command: 'stdbuf',
|
|
562
|
+
args: ['-i0', '-o0', '-e0', process.execPath, serverPath],
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (process.platform === 'darwin' && hasCommand('script')) {
|
|
567
|
+
attempts.push({
|
|
568
|
+
label: 'script pty wrapper',
|
|
569
|
+
command: 'script',
|
|
570
|
+
args: ['-q', '/dev/null', process.execPath, serverPath],
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (process.platform === 'linux' && hasCommand('script')) {
|
|
575
|
+
attempts.push({
|
|
576
|
+
label: 'script pty wrapper',
|
|
577
|
+
command: 'script',
|
|
578
|
+
args: ['-q', '-e', '-c', `${shellEscape(process.execPath)} ${shellEscape(serverPath)}`, '/dev/null'],
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return attempts;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
spawnChild(command, args) {
|
|
586
|
+
this.child = spawn(command, args, {
|
|
587
|
+
env: this.buildEnv(),
|
|
588
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
this.stdoutBuffer = Buffer.alloc(0);
|
|
592
|
+
|
|
593
|
+
this.child.stdout.on('data', (chunk) => {
|
|
594
|
+
try {
|
|
595
|
+
this.stdoutBuffer = Buffer.concat([this.stdoutBuffer, chunk]);
|
|
596
|
+
this.stdoutBuffer = trimNonProtocolNoise(this.stdoutBuffer, this.logger);
|
|
597
|
+
this.stdoutBuffer = parseMessageBuffer(this.stdoutBuffer, (message) =>
|
|
598
|
+
this.handleMessage(message)
|
|
599
|
+
);
|
|
600
|
+
} catch (error) {
|
|
601
|
+
this.logger?.error?.(
|
|
602
|
+
`[TapTap DC] Failed to parse MCP stdout: ${
|
|
603
|
+
error instanceof Error ? error.message : String(error)
|
|
604
|
+
}`
|
|
605
|
+
);
|
|
606
|
+
this.stdoutBuffer = trimNonProtocolNoise(this.stdoutBuffer, this.logger);
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
this.child.stderr.on('data', (chunk) => {
|
|
611
|
+
const text = chunk.toString('utf8').trim();
|
|
612
|
+
if (text) {
|
|
613
|
+
this.logger?.info?.(`[TapTap MCP] ${text}`);
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
this.child.on('exit', (code, signal) => {
|
|
618
|
+
const error = new Error(
|
|
619
|
+
`Embedded TapTap MCP server exited unexpectedly (code=${code ?? 'null'}, signal=${signal ?? 'null'}).`
|
|
620
|
+
);
|
|
621
|
+
for (const pending of this.pending.values()) {
|
|
622
|
+
pending.reject(error);
|
|
623
|
+
}
|
|
624
|
+
this.pending.clear();
|
|
625
|
+
this.child = null;
|
|
626
|
+
this.readyPromise = null;
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
async initializeServer() {
|
|
631
|
+
await Promise.race([
|
|
632
|
+
this.request('initialize', {
|
|
633
|
+
protocolVersion: '2025-11-25',
|
|
634
|
+
capabilities: {
|
|
635
|
+
tools: {},
|
|
636
|
+
resources: {},
|
|
637
|
+
logging: {},
|
|
638
|
+
},
|
|
639
|
+
clientInfo: {
|
|
640
|
+
name: 'taptap-openclaw-dc-plugin',
|
|
641
|
+
version: '0.1.0',
|
|
642
|
+
},
|
|
643
|
+
}),
|
|
644
|
+
new Promise((_, reject) => {
|
|
645
|
+
setTimeout(() => {
|
|
646
|
+
reject(
|
|
647
|
+
new Error(
|
|
648
|
+
`Initialize handshake timed out after ${DEFAULT_INIT_TIMEOUT_MS}ms`
|
|
649
|
+
)
|
|
650
|
+
);
|
|
651
|
+
}, DEFAULT_INIT_TIMEOUT_MS);
|
|
652
|
+
}),
|
|
653
|
+
]);
|
|
654
|
+
|
|
655
|
+
this.notify('notifications/initialized', {});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
isEchoedOutboundMessage(message) {
|
|
659
|
+
if (!message || typeof message !== 'object' || !message.method) {
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const payload = JSON.stringify(message);
|
|
664
|
+
return this.outboundMessages.some(
|
|
665
|
+
(entry) =>
|
|
666
|
+
entry.payload === payload ||
|
|
667
|
+
(entry.id !== undefined && entry.id === message.id && entry.method === message.method)
|
|
668
|
+
);
|
|
406
669
|
}
|
|
407
670
|
}
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -19,8 +19,9 @@ description: 生成 TapTap 当前游戏 DC 运营简报与结论解读(商店/
|
|
|
19
19
|
- 如果用户给了游戏名,直接把 `app_name` 传进去
|
|
20
20
|
- 如果用户给了 `app_id`,直接把 `app_id` 传进去
|
|
21
21
|
2. 如果未授权
|
|
22
|
-
- `taptap_dc_quick_brief` 会直接返回
|
|
23
|
-
-
|
|
22
|
+
- `taptap_dc_quick_brief` 会直接返回 markdown 授权链接和二维码链接
|
|
23
|
+
- 如果用户当前在手机上对话,优先引导用户直接点击授权链接
|
|
24
|
+
- 如果用户当前在桌面端对话,再引导用户打开二维码并扫码
|
|
24
25
|
- 用户确认后调用 `taptap_dc_complete_authorization`
|
|
25
26
|
- 然后再次调用 `taptap_dc_quick_brief`
|
|
26
27
|
3. 只有在用户明确要求更细的内容时,才退回到底层工具链
|
|
@@ -44,6 +45,7 @@ description: 生成 TapTap 当前游戏 DC 运营简报与结论解读(商店/
|
|
|
44
45
|
## 关键规则
|
|
45
46
|
|
|
46
47
|
- 这些 plugin tools 返回的是 **raw JSON**,你要自己完成解读,不要把 JSON 原样长篇贴回给用户
|
|
48
|
+
- 当授权工具已经返回可点击 markdown 链接时,优先直接复用它,不要再把链接改写成纯文本说明
|
|
47
49
|
- `page_view_count` 应写成“详情页访问量(PV)”,不要偷换成别的口径
|
|
48
50
|
- `taptap_dc_like_review` / `taptap_dc_reply_review` 只能在用户明确确认后调用
|
|
49
51
|
- 如果回复结果里出现 `need_confirmation=true`,必须先把草稿给用户确认,再决定是否带 `confirm_high_risk=true` 重试
|