@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 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` 会直接返回 `auth_url` / `qrcode_url`,便于用户立即扫码
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. 完成扫码后调用 `taptap_dc_complete_authorization`
29
- 4. 再次调用 `taptap_dc_quick_brief`
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
- `授权链接:${authPayload?.auth_url || '-'}`,
194
- `二维码链接:${authPayload?.qrcode_url || '-'}`,
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
- return toolResult(normalized, {
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: tryParseJson(normalized),
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 auth_url/qrcode_url/device_code as raw JSON.',
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
- const headerEnd = buffer.indexOf(HEADER_SEPARATOR, offset, 'utf8');
70
- if (headerEnd === -1) {
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 headerText = buffer.subarray(offset, headerEnd).toString('utf8');
75
- const contentLengthMatch = headerText.match(/content-length:\s*(\d+)/i);
76
- if (!contentLengthMatch) {
77
- throw new Error(`Missing Content-Length header in MCP frame: ${headerText}`);
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
- const contentLength = Number(contentLengthMatch[1]);
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 bodyText = buffer.subarray(bodyStart, bodyEnd).toString('utf8');
88
- onMessage(JSON.parse(bodyText));
89
- offset = bodyEnd;
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.logger?.info?.(
286
- `[TapTap DC] Starting embedded TapTap MCP runtime from ${runtime.source}: ${runtime.serverPath}`
287
- );
442
+ const launchAttempts = this.getLaunchAttempts(runtime.serverPath);
443
+ const errors = [];
288
444
 
289
- this.child = spawn(process.execPath, [runtime.serverPath], {
290
- env: this.buildEnv(),
291
- stdio: ['pipe', 'pipe', 'pipe'],
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.stdoutBuffer = Buffer.concat([this.stdoutBuffer, chunk]);
297
- this.stdoutBuffer = parseMessageBuffer(this.stdoutBuffer, (message) =>
298
- this.handleMessage(message)
299
- );
453
+ await this.initializeServer();
454
+ return;
300
455
  } catch (error) {
301
- this.logger?.error?.(
302
- `[TapTap DC] Failed to parse MCP stdout: ${
303
- error instanceof Error ? error.message : String(error)
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
- this.notify('notifications/initialized', {});
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
  }
@@ -2,7 +2,7 @@
2
2
  "id": "taptap-dc-plugin",
3
3
  "name": "TapTap DC",
4
4
  "description": "面向 OpenClaw 的 TapTap DC 插件,内置原始数据工具与运营简报 skill。",
5
- "version": "0.1.3",
5
+ "version": "0.1.5",
6
6
  "skills": ["./skills"],
7
7
  "configSchema": {
8
8
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotaber_wang/openclaw-dc-plugin",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "description": "TapTap DC 的 OpenClaw 插件,内置原始数据工具与运营简报 skill。",
6
6
  "main": "index.js",
@@ -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` 会直接返回 `auth_url` / `qrcode_url`
23
- - 让用户打开 `auth_url` 或扫描 `qrcode_url`
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` 重试