@lotaber_wang/openclaw-dc-plugin 0.1.0
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 +46 -0
- package/index.js +374 -0
- package/lib/mcp-bridge.js +251 -0
- package/openclaw.plugin.json +40 -0
- package/package.json +47 -0
- package/skills/taptap-dc-ops-brief/SKILL.md +55 -0
- package/skills/taptap-dc-ops-brief/references/actions_rubric.md +37 -0
- package/skills/taptap-dc-ops-brief/references/metrics.md +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# TapTap DC OpenClaw Plugin
|
|
2
|
+
|
|
3
|
+
OpenClaw plugin that exposes raw TapTap DC tools and bundles a TapTap DC ops-brief skill.
|
|
4
|
+
|
|
5
|
+
## What It Does
|
|
6
|
+
|
|
7
|
+
- installs as a native OpenClaw plugin
|
|
8
|
+
- internally boots the published `@mikoto_zero/minigame-open-mcp` runtime
|
|
9
|
+
- exposes raw JSON-oriented TapTap tools for:
|
|
10
|
+
- authorization
|
|
11
|
+
- app selection
|
|
12
|
+
- store/review/community overview
|
|
13
|
+
- store snapshot
|
|
14
|
+
- forum contents
|
|
15
|
+
- reviews
|
|
16
|
+
- like/reply review actions
|
|
17
|
+
- bundles a `taptap-dc-ops-brief` skill that turns those raw responses into a concise ops brief
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
openclaw plugins install @lotaber_wang/openclaw-dc-plugin
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Typical Flow
|
|
26
|
+
|
|
27
|
+
1. Call `taptap_dc_check_environment`
|
|
28
|
+
2. If not authorized, call `taptap_dc_start_authorization`
|
|
29
|
+
3. Ask the user to open `auth_url` or scan `qrcode_url`
|
|
30
|
+
4. Call `taptap_dc_complete_authorization`
|
|
31
|
+
5. Call `taptap_dc_list_apps`
|
|
32
|
+
6. Call `taptap_dc_select_app`
|
|
33
|
+
7. Call overview tools and let the bundled skill produce the brief
|
|
34
|
+
|
|
35
|
+
## Configuration
|
|
36
|
+
|
|
37
|
+
Optional plugin config:
|
|
38
|
+
|
|
39
|
+
- `environment`: `production` or `rnd`
|
|
40
|
+
- `workspaceRoot`
|
|
41
|
+
- `cacheDir`
|
|
42
|
+
- `tempDir`
|
|
43
|
+
- `logRoot`
|
|
44
|
+
- `verbose`
|
|
45
|
+
|
|
46
|
+
Production use should not need `client_id` / `client_secret` overrides when the embedded TapTap package already contains them.
|
package/index.js
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { TapTapMcpBridge } from './lib/mcp-bridge.js';
|
|
3
|
+
|
|
4
|
+
function toolResult(text, details = {}) {
|
|
5
|
+
return {
|
|
6
|
+
content: [{ type: 'text', text }],
|
|
7
|
+
details,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function tryParseJson(text) {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(text);
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizeJsonText(text) {
|
|
20
|
+
const parsed = tryParseJson(text);
|
|
21
|
+
if (parsed !== null) {
|
|
22
|
+
return JSON.stringify(parsed, null, 2);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const fencedJson = text.match(/```json\s*([\s\S]*?)```/i);
|
|
26
|
+
if (fencedJson?.[1]) {
|
|
27
|
+
const parsedFence = tryParseJson(fencedJson[1].trim());
|
|
28
|
+
if (parsedFence !== null) {
|
|
29
|
+
return JSON.stringify(parsedFence, null, 2);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return JSON.stringify(
|
|
34
|
+
{
|
|
35
|
+
ok: false,
|
|
36
|
+
error: 'UNPARSEABLE_RESPONSE',
|
|
37
|
+
raw: text,
|
|
38
|
+
},
|
|
39
|
+
null,
|
|
40
|
+
2
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function createSchema(properties = {}, required = []) {
|
|
45
|
+
return {
|
|
46
|
+
type: 'object',
|
|
47
|
+
properties,
|
|
48
|
+
...(required.length > 0 ? { required } : {}),
|
|
49
|
+
additionalProperties: false,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function registerProxyTool(api, bridge, definition) {
|
|
54
|
+
api.registerTool(
|
|
55
|
+
() => ({
|
|
56
|
+
name: definition.name,
|
|
57
|
+
label: definition.label,
|
|
58
|
+
description: definition.description,
|
|
59
|
+
parameters: definition.parameters,
|
|
60
|
+
async execute(_id, params) {
|
|
61
|
+
try {
|
|
62
|
+
const text = await bridge.callTool(definition.mcpToolName, params || {});
|
|
63
|
+
const normalized = normalizeJsonText(text);
|
|
64
|
+
return toolResult(normalized, {
|
|
65
|
+
mcpToolName: definition.mcpToolName,
|
|
66
|
+
parsed: tryParseJson(normalized),
|
|
67
|
+
});
|
|
68
|
+
} catch (error) {
|
|
69
|
+
return toolResult(
|
|
70
|
+
JSON.stringify(
|
|
71
|
+
{
|
|
72
|
+
ok: false,
|
|
73
|
+
error: 'PLUGIN_PROXY_ERROR',
|
|
74
|
+
message: error instanceof Error ? error.message : String(error),
|
|
75
|
+
},
|
|
76
|
+
null,
|
|
77
|
+
2
|
|
78
|
+
)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
}),
|
|
83
|
+
{ name: definition.name }
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const toolDefinitions = [
|
|
88
|
+
{
|
|
89
|
+
name: 'taptap_dc_check_environment',
|
|
90
|
+
label: 'TapTap Env',
|
|
91
|
+
description: 'Check embedded TapTap runtime, signer, auth status, and directories as raw JSON.',
|
|
92
|
+
mcpToolName: 'check_environment_raw',
|
|
93
|
+
parameters: createSchema(),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'taptap_dc_start_authorization',
|
|
97
|
+
label: 'TapTap Auth Start',
|
|
98
|
+
description:
|
|
99
|
+
'Start TapTap OAuth device flow and return auth_url/qrcode_url/device_code as raw JSON.',
|
|
100
|
+
mcpToolName: 'start_oauth_authorization_raw',
|
|
101
|
+
parameters: createSchema(),
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'taptap_dc_complete_authorization',
|
|
105
|
+
label: 'TapTap Auth Complete',
|
|
106
|
+
description: 'Complete TapTap OAuth device flow after the user scans and approves the QR code.',
|
|
107
|
+
mcpToolName: 'complete_oauth_authorization_raw',
|
|
108
|
+
parameters: createSchema(),
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'taptap_dc_clear_auth',
|
|
112
|
+
label: 'TapTap Clear Auth',
|
|
113
|
+
description: 'Clear TapTap cached token and/or selected app cache.',
|
|
114
|
+
mcpToolName: 'clear_auth_data_raw',
|
|
115
|
+
parameters: createSchema({
|
|
116
|
+
clear_token: {
|
|
117
|
+
type: 'boolean',
|
|
118
|
+
description: 'Clear OAuth token cache. Defaults to true.',
|
|
119
|
+
},
|
|
120
|
+
clear_cache: {
|
|
121
|
+
type: 'boolean',
|
|
122
|
+
description: 'Clear selected app cache. Defaults to true.',
|
|
123
|
+
},
|
|
124
|
+
}),
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: 'taptap_dc_list_apps',
|
|
128
|
+
label: 'TapTap Apps',
|
|
129
|
+
description: 'List all accessible developers and apps as raw JSON.',
|
|
130
|
+
mcpToolName: 'list_developers_and_apps_raw',
|
|
131
|
+
parameters: createSchema(),
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: 'taptap_dc_select_app',
|
|
135
|
+
label: 'TapTap Select App',
|
|
136
|
+
description: 'Select a developer/app pair for subsequent TapTap DC calls.',
|
|
137
|
+
mcpToolName: 'select_app_raw',
|
|
138
|
+
parameters: createSchema(
|
|
139
|
+
{
|
|
140
|
+
developer_id: {
|
|
141
|
+
type: 'number',
|
|
142
|
+
description: 'Developer ID to select.',
|
|
143
|
+
},
|
|
144
|
+
app_id: {
|
|
145
|
+
type: 'number',
|
|
146
|
+
description: 'App ID to select.',
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
['developer_id', 'app_id']
|
|
150
|
+
),
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'taptap_dc_get_current_app',
|
|
154
|
+
label: 'TapTap Current App',
|
|
155
|
+
description: 'Get the currently selected app/cache payload as raw JSON.',
|
|
156
|
+
mcpToolName: 'get_current_app_info_raw',
|
|
157
|
+
parameters: createSchema({
|
|
158
|
+
ignore_cache: {
|
|
159
|
+
type: 'boolean',
|
|
160
|
+
description: 'Force refresh from server when true.',
|
|
161
|
+
},
|
|
162
|
+
}),
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: 'taptap_dc_get_store_overview',
|
|
166
|
+
label: 'TapTap Store Overview',
|
|
167
|
+
description: 'Get current-app store overview raw JSON.',
|
|
168
|
+
mcpToolName: 'get_current_app_store_overview_raw',
|
|
169
|
+
parameters: createSchema({
|
|
170
|
+
start_date: {
|
|
171
|
+
type: 'string',
|
|
172
|
+
description: 'Optional start date in YYYY-MM-DD format.',
|
|
173
|
+
},
|
|
174
|
+
end_date: {
|
|
175
|
+
type: 'string',
|
|
176
|
+
description: 'Optional end date in YYYY-MM-DD format.',
|
|
177
|
+
},
|
|
178
|
+
}),
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: 'taptap_dc_get_review_overview',
|
|
182
|
+
label: 'TapTap Review Overview',
|
|
183
|
+
description: 'Get current-app review overview raw JSON.',
|
|
184
|
+
mcpToolName: 'get_current_app_review_overview_raw',
|
|
185
|
+
parameters: createSchema({
|
|
186
|
+
start_date: {
|
|
187
|
+
type: 'string',
|
|
188
|
+
description: 'Optional start date in YYYY-MM-DD format.',
|
|
189
|
+
},
|
|
190
|
+
end_date: {
|
|
191
|
+
type: 'string',
|
|
192
|
+
description: 'Optional end date in YYYY-MM-DD format.',
|
|
193
|
+
},
|
|
194
|
+
}),
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: 'taptap_dc_get_community_overview',
|
|
198
|
+
label: 'TapTap Community Overview',
|
|
199
|
+
description: 'Get current-app community overview raw JSON.',
|
|
200
|
+
mcpToolName: 'get_current_app_community_overview_raw',
|
|
201
|
+
parameters: createSchema({
|
|
202
|
+
start_date: {
|
|
203
|
+
type: 'string',
|
|
204
|
+
description: 'Optional start date in YYYY-MM-DD format.',
|
|
205
|
+
},
|
|
206
|
+
end_date: {
|
|
207
|
+
type: 'string',
|
|
208
|
+
description: 'Optional end date in YYYY-MM-DD format.',
|
|
209
|
+
},
|
|
210
|
+
}),
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: 'taptap_dc_get_store_snapshot',
|
|
214
|
+
label: 'TapTap Store Snapshot',
|
|
215
|
+
description: 'Get current-app store snapshot raw JSON.',
|
|
216
|
+
mcpToolName: 'get_current_app_store_snapshot_raw',
|
|
217
|
+
parameters: createSchema(),
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: 'taptap_dc_get_forum_contents',
|
|
221
|
+
label: 'TapTap Forum Contents',
|
|
222
|
+
description: 'Get current-app forum contents raw JSON.',
|
|
223
|
+
mcpToolName: 'get_current_app_forum_contents_raw',
|
|
224
|
+
parameters: createSchema({
|
|
225
|
+
type: {
|
|
226
|
+
type: 'string',
|
|
227
|
+
description: 'Forum flow type. Default: feed.',
|
|
228
|
+
},
|
|
229
|
+
sort: {
|
|
230
|
+
type: 'string',
|
|
231
|
+
description: 'Sort mode. Default: default.',
|
|
232
|
+
},
|
|
233
|
+
from: {
|
|
234
|
+
type: 'number',
|
|
235
|
+
description: 'Pagination start offset.',
|
|
236
|
+
},
|
|
237
|
+
limit: {
|
|
238
|
+
type: 'number',
|
|
239
|
+
description: 'Page size. Default 10, max 20.',
|
|
240
|
+
},
|
|
241
|
+
group_label_id: {
|
|
242
|
+
type: 'number',
|
|
243
|
+
description: 'Optional sub-group label ID.',
|
|
244
|
+
},
|
|
245
|
+
}),
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: 'taptap_dc_get_reviews',
|
|
249
|
+
label: 'TapTap Reviews',
|
|
250
|
+
description: 'Get current-app reviews raw JSON.',
|
|
251
|
+
mcpToolName: 'get_current_app_reviews_raw',
|
|
252
|
+
parameters: createSchema({
|
|
253
|
+
sort: {
|
|
254
|
+
type: 'string',
|
|
255
|
+
description: 'new / hot / spent',
|
|
256
|
+
},
|
|
257
|
+
from: {
|
|
258
|
+
type: 'number',
|
|
259
|
+
description: 'Pagination start offset.',
|
|
260
|
+
},
|
|
261
|
+
limit: {
|
|
262
|
+
type: 'number',
|
|
263
|
+
description: 'Page size. Default 10, max 10.',
|
|
264
|
+
},
|
|
265
|
+
is_collapsed: {
|
|
266
|
+
type: 'boolean',
|
|
267
|
+
description: 'Whether to query collapsed reviews.',
|
|
268
|
+
},
|
|
269
|
+
filter_platform: {
|
|
270
|
+
type: 'string',
|
|
271
|
+
description: 'mobile / pc / web',
|
|
272
|
+
},
|
|
273
|
+
}),
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
name: 'taptap_dc_like_review',
|
|
277
|
+
label: 'TapTap Like Review',
|
|
278
|
+
description: 'Like a current-app review and return the raw upstream response.',
|
|
279
|
+
mcpToolName: 'like_current_app_review_raw',
|
|
280
|
+
parameters: createSchema(
|
|
281
|
+
{
|
|
282
|
+
review_id: {
|
|
283
|
+
type: 'number',
|
|
284
|
+
description: 'Target review ID to like.',
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
['review_id']
|
|
288
|
+
),
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
name: 'taptap_dc_reply_review',
|
|
292
|
+
label: 'TapTap Reply Review',
|
|
293
|
+
description: 'Reply to a current-app review and return the raw upstream response.',
|
|
294
|
+
mcpToolName: 'reply_current_app_review_raw',
|
|
295
|
+
parameters: createSchema(
|
|
296
|
+
{
|
|
297
|
+
review_id: {
|
|
298
|
+
type: 'number',
|
|
299
|
+
description: 'Target review ID to reply to.',
|
|
300
|
+
},
|
|
301
|
+
contents: {
|
|
302
|
+
type: 'string',
|
|
303
|
+
description: 'Reply content to send.',
|
|
304
|
+
},
|
|
305
|
+
reply_comment_id: {
|
|
306
|
+
type: 'number',
|
|
307
|
+
description: 'Optional comment ID when replying to a child comment.',
|
|
308
|
+
},
|
|
309
|
+
confirm_high_risk: {
|
|
310
|
+
type: 'boolean',
|
|
311
|
+
description: 'Only set after explicit human approval.',
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
['review_id', 'contents']
|
|
315
|
+
),
|
|
316
|
+
},
|
|
317
|
+
];
|
|
318
|
+
|
|
319
|
+
let pluginVersion = 'unknown';
|
|
320
|
+
try {
|
|
321
|
+
const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
|
|
322
|
+
pluginVersion = pkg.version || 'unknown';
|
|
323
|
+
} catch {
|
|
324
|
+
// Best-effort version only
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const plugin = {
|
|
328
|
+
id: 'taptap-dc-plugin',
|
|
329
|
+
name: 'TapTap DC',
|
|
330
|
+
description: 'Raw TapTap DC tools for OpenClaw, bundled with an ops brief skill.',
|
|
331
|
+
configSchema: {
|
|
332
|
+
type: 'object',
|
|
333
|
+
properties: {
|
|
334
|
+
environment: {
|
|
335
|
+
type: 'string',
|
|
336
|
+
enum: ['production', 'rnd'],
|
|
337
|
+
default: 'production',
|
|
338
|
+
},
|
|
339
|
+
workspaceRoot: {
|
|
340
|
+
type: 'string',
|
|
341
|
+
},
|
|
342
|
+
cacheDir: {
|
|
343
|
+
type: 'string',
|
|
344
|
+
},
|
|
345
|
+
tempDir: {
|
|
346
|
+
type: 'string',
|
|
347
|
+
},
|
|
348
|
+
logRoot: {
|
|
349
|
+
type: 'string',
|
|
350
|
+
},
|
|
351
|
+
verbose: {
|
|
352
|
+
type: 'boolean',
|
|
353
|
+
default: false,
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
additionalProperties: false,
|
|
357
|
+
},
|
|
358
|
+
register(api) {
|
|
359
|
+
const bridge = new TapTapMcpBridge({
|
|
360
|
+
logger: api.logger,
|
|
361
|
+
config: api.pluginConfig || {},
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
for (const definition of toolDefinitions) {
|
|
365
|
+
registerProxyTool(api, bridge, definition);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
api.logger.info?.(
|
|
369
|
+
`[TapTap DC] OpenClaw plugin v${pluginVersion} initialised with ${toolDefinitions.length} tools`
|
|
370
|
+
);
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
export default plugin;
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const HEADER_SEPARATOR = '\r\n\r\n';
|
|
10
|
+
|
|
11
|
+
function resolveLocalServerPath() {
|
|
12
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
return path.resolve(currentDir, '../../../dist/server.js');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function resolveEmbeddedServerPath() {
|
|
17
|
+
try {
|
|
18
|
+
const packageJsonPath = require.resolve('@mikoto_zero/minigame-open-mcp/package.json');
|
|
19
|
+
const packageRoot = path.dirname(packageJsonPath);
|
|
20
|
+
const serverPath = path.join(packageRoot, 'dist', 'server.js');
|
|
21
|
+
if (existsSync(serverPath)) {
|
|
22
|
+
return serverPath;
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
// Fall through to local dev path
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const localServerPath = resolveLocalServerPath();
|
|
29
|
+
if (existsSync(localServerPath)) {
|
|
30
|
+
return localServerPath;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
throw new Error(
|
|
34
|
+
'Unable to resolve the embedded TapTap MCP server. Make sure @mikoto_zero/minigame-open-mcp is installed.'
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseMessageBuffer(buffer, onMessage) {
|
|
39
|
+
let offset = 0;
|
|
40
|
+
|
|
41
|
+
while (offset < buffer.length) {
|
|
42
|
+
const headerEnd = buffer.indexOf(HEADER_SEPARATOR, offset, 'utf8');
|
|
43
|
+
if (headerEnd === -1) {
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const headerText = buffer.subarray(offset, headerEnd).toString('utf8');
|
|
48
|
+
const contentLengthMatch = headerText.match(/content-length:\s*(\d+)/i);
|
|
49
|
+
if (!contentLengthMatch) {
|
|
50
|
+
throw new Error(`Missing Content-Length header in MCP frame: ${headerText}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const contentLength = Number(contentLengthMatch[1]);
|
|
54
|
+
const bodyStart = headerEnd + HEADER_SEPARATOR.length;
|
|
55
|
+
const bodyEnd = bodyStart + contentLength;
|
|
56
|
+
if (buffer.length < bodyEnd) {
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const bodyText = buffer.subarray(bodyStart, bodyEnd).toString('utf8');
|
|
61
|
+
onMessage(JSON.parse(bodyText));
|
|
62
|
+
offset = bodyEnd;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return buffer.subarray(offset);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function extractTextFromToolResult(result) {
|
|
69
|
+
if (!result || !Array.isArray(result.content)) {
|
|
70
|
+
return JSON.stringify(result, null, 2);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const parts = result.content
|
|
74
|
+
.filter((item) => item && item.type === 'text' && typeof item.text === 'string')
|
|
75
|
+
.map((item) => item.text);
|
|
76
|
+
|
|
77
|
+
if (parts.length === 0) {
|
|
78
|
+
return JSON.stringify(result, null, 2);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return parts.join('\n\n');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export class TapTapMcpBridge {
|
|
85
|
+
constructor(options = {}) {
|
|
86
|
+
this.logger = options.logger;
|
|
87
|
+
this.config = options.config || {};
|
|
88
|
+
this.child = null;
|
|
89
|
+
this.readyPromise = null;
|
|
90
|
+
this.pending = new Map();
|
|
91
|
+
this.nextId = 1;
|
|
92
|
+
this.stdoutBuffer = Buffer.alloc(0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
buildEnv() {
|
|
96
|
+
const env = {
|
|
97
|
+
...process.env,
|
|
98
|
+
TAPTAP_MCP_TRANSPORT: 'stdio',
|
|
99
|
+
TAPTAP_MCP_ENV: this.config.environment || 'production',
|
|
100
|
+
TAPTAP_MCP_ENABLE_RAW_TOOLS: 'true',
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
if (this.config.workspaceRoot) {
|
|
104
|
+
env.TAPTAP_MCP_WORKSPACE_ROOT = this.config.workspaceRoot;
|
|
105
|
+
}
|
|
106
|
+
if (this.config.cacheDir) {
|
|
107
|
+
env.TAPTAP_MCP_CACHE_DIR = this.config.cacheDir;
|
|
108
|
+
}
|
|
109
|
+
if (this.config.tempDir) {
|
|
110
|
+
env.TAPTAP_MCP_TEMP_DIR = this.config.tempDir;
|
|
111
|
+
}
|
|
112
|
+
if (this.config.logRoot) {
|
|
113
|
+
env.TAPTAP_MCP_LOG_ROOT = this.config.logRoot;
|
|
114
|
+
}
|
|
115
|
+
if (this.config.verbose) {
|
|
116
|
+
env.TAPTAP_MCP_VERBOSE = 'true';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return env;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async ensureReady() {
|
|
123
|
+
if (this.readyPromise) {
|
|
124
|
+
return this.readyPromise;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this.readyPromise = this.start();
|
|
128
|
+
return this.readyPromise;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async start() {
|
|
132
|
+
const serverPath = resolveEmbeddedServerPath();
|
|
133
|
+
this.child = spawn(process.execPath, [serverPath], {
|
|
134
|
+
env: this.buildEnv(),
|
|
135
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
this.child.stdout.on('data', (chunk) => {
|
|
139
|
+
try {
|
|
140
|
+
this.stdoutBuffer = Buffer.concat([this.stdoutBuffer, chunk]);
|
|
141
|
+
this.stdoutBuffer = parseMessageBuffer(this.stdoutBuffer, (message) =>
|
|
142
|
+
this.handleMessage(message)
|
|
143
|
+
);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
this.logger?.error?.(
|
|
146
|
+
`[TapTap DC] Failed to parse MCP stdout: ${
|
|
147
|
+
error instanceof Error ? error.message : String(error)
|
|
148
|
+
}`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
this.child.stderr.on('data', (chunk) => {
|
|
154
|
+
const text = chunk.toString('utf8').trim();
|
|
155
|
+
if (text) {
|
|
156
|
+
this.logger?.info?.(`[TapTap MCP] ${text}`);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
this.child.on('exit', (code, signal) => {
|
|
161
|
+
const error = new Error(
|
|
162
|
+
`Embedded TapTap MCP server exited unexpectedly (code=${code ?? 'null'}, signal=${signal ?? 'null'}).`
|
|
163
|
+
);
|
|
164
|
+
for (const pending of this.pending.values()) {
|
|
165
|
+
pending.reject(error);
|
|
166
|
+
}
|
|
167
|
+
this.pending.clear();
|
|
168
|
+
this.child = null;
|
|
169
|
+
this.readyPromise = null;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await this.request('initialize', {
|
|
173
|
+
protocolVersion: '2025-11-25',
|
|
174
|
+
capabilities: {
|
|
175
|
+
tools: {},
|
|
176
|
+
resources: {},
|
|
177
|
+
logging: {},
|
|
178
|
+
},
|
|
179
|
+
clientInfo: {
|
|
180
|
+
name: 'taptap-openclaw-dc-plugin',
|
|
181
|
+
version: '0.1.0',
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
this.notify('notifications/initialized', {});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
handleMessage(message) {
|
|
189
|
+
if (message.id !== undefined && this.pending.has(message.id)) {
|
|
190
|
+
const pending = this.pending.get(message.id);
|
|
191
|
+
this.pending.delete(message.id);
|
|
192
|
+
|
|
193
|
+
if (message.error) {
|
|
194
|
+
pending.reject(new Error(message.error.message || JSON.stringify(message.error)));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
pending.resolve(message.result);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
send(message) {
|
|
204
|
+
if (!this.child?.stdin) {
|
|
205
|
+
throw new Error('Embedded TapTap MCP server is not running.');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const body = JSON.stringify(message);
|
|
209
|
+
const frame = `Content-Length: ${Buffer.byteLength(body, 'utf8')}\r\n\r\n${body}`;
|
|
210
|
+
this.child.stdin.write(frame, 'utf8');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
request(method, params) {
|
|
214
|
+
const id = this.nextId++;
|
|
215
|
+
|
|
216
|
+
return new Promise((resolve, reject) => {
|
|
217
|
+
this.pending.set(id, { resolve, reject });
|
|
218
|
+
this.send({
|
|
219
|
+
jsonrpc: '2.0',
|
|
220
|
+
id,
|
|
221
|
+
method,
|
|
222
|
+
params,
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
notify(method, params) {
|
|
228
|
+
this.send({
|
|
229
|
+
jsonrpc: '2.0',
|
|
230
|
+
method,
|
|
231
|
+
params,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async callTool(name, args = {}) {
|
|
236
|
+
await this.ensureReady();
|
|
237
|
+
const result = await this.request('tools/call', {
|
|
238
|
+
name,
|
|
239
|
+
arguments: args,
|
|
240
|
+
});
|
|
241
|
+
return extractTextFromToolResult(result);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async close() {
|
|
245
|
+
if (this.child) {
|
|
246
|
+
this.child.kill();
|
|
247
|
+
this.child = null;
|
|
248
|
+
this.readyPromise = null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "taptap-dc-plugin",
|
|
3
|
+
"name": "TapTap DC",
|
|
4
|
+
"description": "Raw TapTap DC tools for OpenClaw, bundled with an ops brief skill.",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"skills": ["./skills"],
|
|
7
|
+
"configSchema": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"properties": {
|
|
10
|
+
"environment": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"description": "TapTap environment to use.",
|
|
13
|
+
"enum": ["production", "rnd"],
|
|
14
|
+
"default": "production"
|
|
15
|
+
},
|
|
16
|
+
"workspaceRoot": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"description": "Optional workspace root passed to the embedded TapTap runtime."
|
|
19
|
+
},
|
|
20
|
+
"cacheDir": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "Optional cache directory override for the embedded TapTap runtime."
|
|
23
|
+
},
|
|
24
|
+
"tempDir": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "Optional temp directory override for the embedded TapTap runtime."
|
|
27
|
+
},
|
|
28
|
+
"logRoot": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"description": "Optional log directory override for the embedded TapTap runtime."
|
|
31
|
+
},
|
|
32
|
+
"verbose": {
|
|
33
|
+
"type": "boolean",
|
|
34
|
+
"description": "Enable verbose logging for the embedded TapTap runtime.",
|
|
35
|
+
"default": false
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"additionalProperties": false
|
|
39
|
+
}
|
|
40
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lotaber_wang/openclaw-dc-plugin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "OpenClaw plugin for TapTap DC raw data tools with a bundled ops-brief skill.",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./package.json": "./package.json"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"index.js",
|
|
16
|
+
"lib/",
|
|
17
|
+
"openclaw.plugin.json",
|
|
18
|
+
"skills/",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"keywords": [
|
|
22
|
+
"openclaw",
|
|
23
|
+
"openclaw-plugin",
|
|
24
|
+
"taptap",
|
|
25
|
+
"dc",
|
|
26
|
+
"skill"
|
|
27
|
+
],
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18.14.1"
|
|
30
|
+
},
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"openclaw": ">=2026.1.29"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@mikoto_zero/minigame-open-mcp": "^1.19.0"
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/taptap/taptap_minigame_open_mcp.git",
|
|
41
|
+
"directory": "packages/openclaw-dc-plugin"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/taptap/taptap_minigame_open_mcp/tree/main/packages/openclaw-dc-plugin",
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/taptap/taptap_minigame_open_mcp/issues"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: taptap-dc-ops-brief
|
|
3
|
+
description: 生成 TapTap 当前游戏 DC 运营简报与结论解读(商店/评价/社区)。在 OpenClaw 中使用 bundled raw-data tools 拉原始 JSON,再由 skill 负责解读与行动建议。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# TapTap DC 运营简报(OpenClaw Plugin)
|
|
7
|
+
|
|
8
|
+
## 适用范围
|
|
9
|
+
|
|
10
|
+
- 面向人群:运营同学、工作室开发者
|
|
11
|
+
- 输出目标:用最少认知成本,把 TapTap DC 原始数据变成“结论 + 重点解读 + 下一步建议”
|
|
12
|
+
- 数据来源:当前 OpenClaw plugin 提供的 raw tools
|
|
13
|
+
- 重要约束:任何写操作(点赞/官方回复)都必须先征得用户许可
|
|
14
|
+
|
|
15
|
+
## 默认工作流
|
|
16
|
+
|
|
17
|
+
1. 自检插件环境
|
|
18
|
+
- 调用 `taptap_dc_check_environment`
|
|
19
|
+
2. 如果未授权
|
|
20
|
+
- 调用 `taptap_dc_start_authorization`
|
|
21
|
+
- 让用户打开 `auth_url` 或扫描 `qrcode_url`
|
|
22
|
+
- 用户确认后调用 `taptap_dc_complete_authorization`
|
|
23
|
+
3. 选择游戏
|
|
24
|
+
- 调用 `taptap_dc_list_apps`
|
|
25
|
+
- 展示列表并让用户指定
|
|
26
|
+
- 调用 `taptap_dc_select_app`
|
|
27
|
+
4. 拉取只读数据
|
|
28
|
+
- `taptap_dc_get_store_overview`
|
|
29
|
+
- `taptap_dc_get_review_overview`
|
|
30
|
+
- `taptap_dc_get_community_overview`
|
|
31
|
+
- 需要结果快照时调用 `taptap_dc_get_store_snapshot`
|
|
32
|
+
- 需要具体内容时调用 `taptap_dc_get_reviews` / `taptap_dc_get_forum_contents`
|
|
33
|
+
5. 输出“30 秒可读”的简报
|
|
34
|
+
6. 若需要动作建议,可给出“是否建议点赞/回复”的候选,但不要直接执行
|
|
35
|
+
|
|
36
|
+
## 输出要求
|
|
37
|
+
|
|
38
|
+
输出尽量短,一屏内读完:
|
|
39
|
+
|
|
40
|
+
1. 结论(3 行内)
|
|
41
|
+
2. 关键指标(仅列 5-8 个最相关)
|
|
42
|
+
3. 变化与解读(最多 3 点)
|
|
43
|
+
4. 建议动作(最多 3 条,且需用户确认后才执行)
|
|
44
|
+
|
|
45
|
+
## 关键规则
|
|
46
|
+
|
|
47
|
+
- 这些 plugin tools 返回的是 **raw JSON**,你要自己完成解读,不要把 JSON 原样长篇贴回给用户
|
|
48
|
+
- `page_view_count` 应写成“详情页访问量(PV)”,不要偷换成别的口径
|
|
49
|
+
- `taptap_dc_like_review` / `taptap_dc_reply_review` 只能在用户明确确认后调用
|
|
50
|
+
- 如果回复结果里出现 `need_confirmation=true`,必须先把草稿给用户确认,再决定是否带 `confirm_high_risk=true` 重试
|
|
51
|
+
|
|
52
|
+
## 口径参考
|
|
53
|
+
|
|
54
|
+
- 指标口径:见 `references/metrics.md`
|
|
55
|
+
- 动作判断:见 `references/actions_rubric.md`
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# 点赞/回复动作建议 Rubric(简化版)
|
|
2
|
+
|
|
3
|
+
本文件用于在“只读简报”之外,给出可控的运营动作建议。任何写操作都必须先征得用户许可。
|
|
4
|
+
|
|
5
|
+
## 点赞(like)建议
|
|
6
|
+
|
|
7
|
+
优先点赞的评价类型:
|
|
8
|
+
|
|
9
|
+
- 信息密度高:包含明确问题描述、复现步骤、机型/版本信息
|
|
10
|
+
- 具有代表性:反映当前版本最常见的 1-2 个核心问题
|
|
11
|
+
- 正向长评:能帮其他用户做决策,且语气友好
|
|
12
|
+
|
|
13
|
+
不建议点赞的情况:
|
|
14
|
+
|
|
15
|
+
- 明显引战、辱骂、人身攻击
|
|
16
|
+
- 涉及敏感/合规风险
|
|
17
|
+
|
|
18
|
+
## 官方回复(reply)建议
|
|
19
|
+
|
|
20
|
+
建议回复的触发条件:
|
|
21
|
+
|
|
22
|
+
- 负向或中立,且包含可行动点
|
|
23
|
+
- 互动较高或对转化影响大
|
|
24
|
+
- 同类问题集中出现
|
|
25
|
+
|
|
26
|
+
不建议直接回复的情况:
|
|
27
|
+
|
|
28
|
+
- 涉及账号/支付隐私,需要引导走客服渠道
|
|
29
|
+
- 需要承诺时间点或赔付
|
|
30
|
+
|
|
31
|
+
## 执行前确认
|
|
32
|
+
|
|
33
|
+
调用写操作前,必须向用户展示:
|
|
34
|
+
|
|
35
|
+
- 目标评价:`review_id` + 内容摘要
|
|
36
|
+
- 拟执行动作:点赞/回复
|
|
37
|
+
- 拟回复文本(如为回复)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# TapTap 店铺运营指标口径速查(简化版)
|
|
2
|
+
|
|
3
|
+
本文件用于把 TapTap raw tools 返回的字段,映射为运营可理解的“指标名 + 口径解释”。
|
|
4
|
+
|
|
5
|
+
## 使用原则
|
|
6
|
+
|
|
7
|
+
- 报告正文优先用中文短名,首次出现时解释一次
|
|
8
|
+
- 不确定口径时,标注“口径待确认”,并把字段原名一并展示
|
|
9
|
+
- `page_view_count` 指 **详情页浏览量(PV)**
|
|
10
|
+
- 不要把 `page_view_count` 解读成推荐曝光或其他渠道曝光
|
|
11
|
+
|
|
12
|
+
## 常用指标
|
|
13
|
+
|
|
14
|
+
| 简报用名 | 字段名 | 说明 |
|
|
15
|
+
| ------------------ | ---------------------------------- | --------------------- |
|
|
16
|
+
| 详情页访问量(PV) | `page_view_count` | 商店详情页浏览量 |
|
|
17
|
+
| 下载请求量 | `download_request_count` | 点击下载/触发下载次数 |
|
|
18
|
+
| 下载完成量 | `download_count` | 实际完成下载的次数 |
|
|
19
|
+
| 预约量 | `reserve_count` | 预约行为次数 |
|
|
20
|
+
| 广告下载&预约量 | `ad_download_reserve_count` | 广告带来的下载/预约量 |
|
|
21
|
+
| PC 下载请求量 | `pc_download_request_count` | PC 端触发下载的请求数 |
|
|
22
|
+
| 社区页面浏览量 | `topic_page_view_count` | 社区页面 PV |
|
|
23
|
+
| 评价总数 | `rating_summary.stat.review_count` | 历史累计评价数 |
|