@lightcone-ai/daemon 0.11.0 → 0.13.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/mcp-servers/official/ai-image-gen/index.js +277 -0
- package/mcp-servers/official/ai-image-gen/manifest.json +19 -0
- package/mcp-servers/official/audience-research/index.js +383 -0
- package/mcp-servers/official/audience-research/manifest.json +18 -0
- package/mcp-servers/official/hook-pattern-library/index.js +417 -0
- package/mcp-servers/official/hook-pattern-library/manifest.json +18 -0
- package/mcp-servers/official/image-search-licensed/index.js +309 -0
- package/mcp-servers/official/image-search-licensed/manifest.json +18 -0
- package/mcp-servers/official/keyword-research/index.js +292 -0
- package/mcp-servers/official/keyword-research/manifest.json +19 -0
- package/mcp-servers/official/platform-policy-db/index.js +316 -0
- package/mcp-servers/official/platform-policy-db/manifest.json +18 -0
- package/mcp-servers/official/publish-window-optimizer/index.js +296 -0
- package/mcp-servers/official/publish-window-optimizer/manifest.json +17 -0
- package/mcp-servers/publisher/adapters/publisher-adapter.js +37 -0
- package/mcp-servers/publisher/adapters/xhs.js +103 -17
- package/mcp-servers/publisher/index.js +123 -25
- package/mcp-servers/publisher/official-tool-client.js +171 -0
- package/mcp-servers/publisher/precheck.js +240 -0
- package/package.json +1 -1
- package/src/chat-bridge.js +614 -1
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
const SOURCE_RISK = 'normal';
|
|
7
|
+
|
|
8
|
+
const PEXELS_API_KEY = String(process.env.PEXELS_API_KEY ?? '').trim();
|
|
9
|
+
const PEXELS_BASE_URL = String(process.env.PEXELS_BASE_URL ?? 'https://api.pexels.com/v1').trim();
|
|
10
|
+
|
|
11
|
+
const UNSPLASH_ACCESS_KEY = String(process.env.UNSPLASH_ACCESS_KEY ?? '').trim();
|
|
12
|
+
const UNSPLASH_BASE_URL = String(process.env.UNSPLASH_BASE_URL ?? 'https://api.unsplash.com').trim();
|
|
13
|
+
|
|
14
|
+
const REQUEST_TIMEOUT_MS = clampInt(process.env.IMAGE_SEARCH_TIMEOUT_MS, 1000, 60000, 15000);
|
|
15
|
+
|
|
16
|
+
const PEXELS_LICENSE = Object.freeze({
|
|
17
|
+
name: 'Pexels License',
|
|
18
|
+
url: 'https://www.pexels.com/license/',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const UNSPLASH_LICENSE = Object.freeze({
|
|
22
|
+
name: 'Unsplash License',
|
|
23
|
+
url: 'https://unsplash.com/license',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function clampInt(value, min, max, fallback) {
|
|
27
|
+
const n = Number(value);
|
|
28
|
+
if (!Number.isFinite(n)) return fallback;
|
|
29
|
+
return Math.max(min, Math.min(max, Math.trunc(n)));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function hashString(text) {
|
|
33
|
+
let hash = 2166136261;
|
|
34
|
+
const raw = String(text ?? '');
|
|
35
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
36
|
+
hash ^= raw.charCodeAt(i);
|
|
37
|
+
hash = Math.imul(hash, 16777619);
|
|
38
|
+
}
|
|
39
|
+
return hash >>> 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseJsonMaybe(text) {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(text);
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeOrientation(raw) {
|
|
51
|
+
const value = String(raw ?? 'auto').trim().toLowerCase();
|
|
52
|
+
if (value === 'landscape' || value === 'portrait' || value === 'square') return value;
|
|
53
|
+
return 'auto';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function buildUrl(baseUrl, route, query = {}) {
|
|
57
|
+
const normalizedBase = String(baseUrl ?? '').trim().replace(/\/$/, '');
|
|
58
|
+
const path = route.startsWith('/') ? route : `/${route}`;
|
|
59
|
+
const url = new URL(`${normalizedBase}${path}`);
|
|
60
|
+
|
|
61
|
+
for (const [key, value] of Object.entries(query)) {
|
|
62
|
+
if (value === null || value === undefined) continue;
|
|
63
|
+
const text = String(value).trim();
|
|
64
|
+
if (!text) continue;
|
|
65
|
+
url.searchParams.set(key, text);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return url;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function fetchJson(url, headers = {}) {
|
|
72
|
+
const controller = new AbortController();
|
|
73
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
74
|
+
try {
|
|
75
|
+
const res = await fetch(url.toString(), {
|
|
76
|
+
method: 'GET',
|
|
77
|
+
headers,
|
|
78
|
+
signal: controller.signal,
|
|
79
|
+
});
|
|
80
|
+
const text = await res.text();
|
|
81
|
+
const payload = parseJsonMaybe(text) ?? { raw: text.slice(0, 500) };
|
|
82
|
+
if (!res.ok) {
|
|
83
|
+
throw new Error(`http_${res.status}:${payload?.error ?? payload?.errors?.[0] ?? text.slice(0, 200)}`);
|
|
84
|
+
}
|
|
85
|
+
return payload;
|
|
86
|
+
} finally {
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function searchPexels({ query, limit, page, orientation }) {
|
|
92
|
+
const payload = await fetchJson(
|
|
93
|
+
buildUrl(PEXELS_BASE_URL, '/search', {
|
|
94
|
+
query,
|
|
95
|
+
per_page: limit,
|
|
96
|
+
page,
|
|
97
|
+
orientation: orientation === 'auto' ? null : orientation,
|
|
98
|
+
}),
|
|
99
|
+
{
|
|
100
|
+
Authorization: PEXELS_API_KEY,
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const photos = Array.isArray(payload?.photos) ? payload.photos : [];
|
|
105
|
+
return photos.map((photo, index) => ({
|
|
106
|
+
id: String(photo?.id ?? `pexels-${index + 1}`),
|
|
107
|
+
title: String(photo?.alt ?? '').trim() || String(photo?.photographer ?? '').trim() || query,
|
|
108
|
+
preview_url: String(photo?.src?.large ?? photo?.src?.medium ?? '').trim(),
|
|
109
|
+
full_url: String(photo?.src?.original ?? photo?.src?.large2x ?? '').trim(),
|
|
110
|
+
thumbnail_url: String(photo?.src?.tiny ?? '').trim(),
|
|
111
|
+
source_page_url: String(photo?.url ?? '').trim(),
|
|
112
|
+
author: String(photo?.photographer ?? '').trim() || null,
|
|
113
|
+
author_url: String(photo?.photographer_url ?? '').trim() || null,
|
|
114
|
+
width: Number.isFinite(Number(photo?.width)) ? Number(photo.width) : null,
|
|
115
|
+
height: Number.isFinite(Number(photo?.height)) ? Number(photo.height) : null,
|
|
116
|
+
provider: 'pexels',
|
|
117
|
+
license: PEXELS_LICENSE.name,
|
|
118
|
+
license_url: PEXELS_LICENSE.url,
|
|
119
|
+
license_verified: true,
|
|
120
|
+
source_risk: SOURCE_RISK,
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function searchUnsplash({ query, limit, page, orientation }) {
|
|
125
|
+
const unsplashOrientation = orientation === 'square' ? 'squarish' : orientation;
|
|
126
|
+
const payload = await fetchJson(
|
|
127
|
+
buildUrl(UNSPLASH_BASE_URL, '/search/photos', {
|
|
128
|
+
query,
|
|
129
|
+
per_page: limit,
|
|
130
|
+
page,
|
|
131
|
+
orientation: unsplashOrientation === 'auto' ? null : unsplashOrientation,
|
|
132
|
+
content_filter: 'high',
|
|
133
|
+
}),
|
|
134
|
+
{
|
|
135
|
+
Authorization: `Client-ID ${UNSPLASH_ACCESS_KEY}`,
|
|
136
|
+
'Accept-Version': 'v1',
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const photos = Array.isArray(payload?.results) ? payload.results : [];
|
|
141
|
+
return photos.map((photo, index) => ({
|
|
142
|
+
id: String(photo?.id ?? `unsplash-${index + 1}`),
|
|
143
|
+
title: String(photo?.alt_description ?? photo?.description ?? '').trim() || query,
|
|
144
|
+
preview_url: String(photo?.urls?.small ?? '').trim(),
|
|
145
|
+
full_url: String(photo?.urls?.full ?? photo?.urls?.regular ?? '').trim(),
|
|
146
|
+
thumbnail_url: String(photo?.urls?.thumb ?? '').trim(),
|
|
147
|
+
source_page_url: String(photo?.links?.html ?? '').trim(),
|
|
148
|
+
author: String(photo?.user?.name ?? '').trim() || null,
|
|
149
|
+
author_url: String(photo?.user?.links?.html ?? '').trim() || null,
|
|
150
|
+
width: Number.isFinite(Number(photo?.width)) ? Number(photo.width) : null,
|
|
151
|
+
height: Number.isFinite(Number(photo?.height)) ? Number(photo.height) : null,
|
|
152
|
+
provider: 'unsplash',
|
|
153
|
+
license: UNSPLASH_LICENSE.name,
|
|
154
|
+
license_url: UNSPLASH_LICENSE.url,
|
|
155
|
+
license_verified: true,
|
|
156
|
+
source_risk: SOURCE_RISK,
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function buildMockResults({ query, limit }) {
|
|
161
|
+
const count = clampInt(limit, 1, 20, 8);
|
|
162
|
+
const items = [];
|
|
163
|
+
for (let i = 0; i < count; i += 1) {
|
|
164
|
+
const provider = i % 2 === 0 ? 'pexels' : 'unsplash';
|
|
165
|
+
const seed = hashString(`${query}|${provider}|${i + 1}`);
|
|
166
|
+
const license = provider === 'pexels' ? PEXELS_LICENSE : UNSPLASH_LICENSE;
|
|
167
|
+
items.push({
|
|
168
|
+
id: `mock-${provider}-${i + 1}`,
|
|
169
|
+
title: `${query} ${provider} reference ${i + 1}`,
|
|
170
|
+
preview_url: `https://picsum.photos/seed/licensed-${seed}/720/480`,
|
|
171
|
+
full_url: `https://picsum.photos/seed/licensed-${seed}/1600/1200`,
|
|
172
|
+
thumbnail_url: `https://picsum.photos/seed/licensed-${seed}/320/240`,
|
|
173
|
+
source_page_url: provider === 'pexels'
|
|
174
|
+
? `https://www.pexels.com/search/${encodeURIComponent(query)}/`
|
|
175
|
+
: `https://unsplash.com/s/photos/${encodeURIComponent(query)}`,
|
|
176
|
+
author: null,
|
|
177
|
+
author_url: null,
|
|
178
|
+
width: 1600,
|
|
179
|
+
height: 1200,
|
|
180
|
+
provider,
|
|
181
|
+
license: `${license.name} (fallback)`,
|
|
182
|
+
license_url: license.url,
|
|
183
|
+
license_verified: false,
|
|
184
|
+
source_risk: SOURCE_RISK,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return items;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function dedupeItems(items) {
|
|
191
|
+
const output = [];
|
|
192
|
+
const seen = new Set();
|
|
193
|
+
for (const item of items) {
|
|
194
|
+
const key = String(item?.full_url ?? item?.source_page_url ?? item?.id ?? '').trim();
|
|
195
|
+
if (!key || seen.has(key)) continue;
|
|
196
|
+
seen.add(key);
|
|
197
|
+
output.push(item);
|
|
198
|
+
}
|
|
199
|
+
return output;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function providerAttemptOrder(requestedProvider) {
|
|
203
|
+
const requested = String(requestedProvider ?? 'auto').trim().toLowerCase();
|
|
204
|
+
if (requested === 'pexels') return ['pexels'];
|
|
205
|
+
if (requested === 'unsplash') return ['unsplash'];
|
|
206
|
+
return ['pexels', 'unsplash'];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function searchLicensedImages(input) {
|
|
210
|
+
const query = String(input.query ?? '').trim();
|
|
211
|
+
if (!query) throw new Error('query_required');
|
|
212
|
+
|
|
213
|
+
const limit = clampInt(input.limit, 1, 20, 8);
|
|
214
|
+
const page = clampInt(input.page, 1, 50, 1);
|
|
215
|
+
const orientation = normalizeOrientation(input.orientation);
|
|
216
|
+
const requestedProvider = String(input.provider ?? 'auto').trim().toLowerCase() || 'auto';
|
|
217
|
+
|
|
218
|
+
const providers = providerAttemptOrder(requestedProvider);
|
|
219
|
+
const attempted = [];
|
|
220
|
+
const errors = [];
|
|
221
|
+
let items = [];
|
|
222
|
+
|
|
223
|
+
for (const provider of providers) {
|
|
224
|
+
attempted.push(provider);
|
|
225
|
+
|
|
226
|
+
if (provider === 'pexels' && !PEXELS_API_KEY) {
|
|
227
|
+
errors.push('pexels_missing_api_key');
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (provider === 'unsplash' && !UNSPLASH_ACCESS_KEY) {
|
|
231
|
+
errors.push('unsplash_missing_access_key');
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
const result = provider === 'pexels'
|
|
237
|
+
? await searchPexels({ query, limit, page, orientation })
|
|
238
|
+
: await searchUnsplash({ query, limit, page, orientation });
|
|
239
|
+
items = items.concat(result);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
errors.push(`${provider}_error:${error.message}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
items = dedupeItems(items).slice(0, limit);
|
|
246
|
+
|
|
247
|
+
if (items.length === 0) {
|
|
248
|
+
items = buildMockResults({ query, limit });
|
|
249
|
+
return {
|
|
250
|
+
query,
|
|
251
|
+
provider_requested: requestedProvider,
|
|
252
|
+
provider_used: 'mock',
|
|
253
|
+
providers_attempted: attempted,
|
|
254
|
+
fallback_reason: errors.length > 0 ? errors[0] : 'no_provider_available',
|
|
255
|
+
total: items.length,
|
|
256
|
+
items,
|
|
257
|
+
source_risk: SOURCE_RISK,
|
|
258
|
+
timestamp: new Date().toISOString(),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const providersUsed = [...new Set(items.map(item => item.provider))];
|
|
263
|
+
return {
|
|
264
|
+
query,
|
|
265
|
+
provider_requested: requestedProvider,
|
|
266
|
+
provider_used: providersUsed.join('+'),
|
|
267
|
+
providers_attempted: attempted,
|
|
268
|
+
fallback_reason: errors.length > 0 ? errors[0] : null,
|
|
269
|
+
total: items.length,
|
|
270
|
+
items,
|
|
271
|
+
source_risk: SOURCE_RISK,
|
|
272
|
+
timestamp: new Date().toISOString(),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function toolError(error) {
|
|
277
|
+
return {
|
|
278
|
+
isError: true,
|
|
279
|
+
content: [{ type: 'text', text: `Error: ${error.message}` }],
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const server = new McpServer({ name: 'official-image-search-licensed', version: '0.1.0' });
|
|
284
|
+
|
|
285
|
+
server.tool(
|
|
286
|
+
'image_search_licensed',
|
|
287
|
+
'Search licensed images from Pexels/Unsplash and return license metadata with source_risk=normal.',
|
|
288
|
+
{
|
|
289
|
+
query: z.string().min(1).describe('Search keyword, e.g. 科技蓝色封面。'),
|
|
290
|
+
provider: z.enum(['auto', 'pexels', 'unsplash']).optional().describe('Image provider selection.'),
|
|
291
|
+
limit: z.number().int().min(1).max(20).optional().describe('Max items, default 8.'),
|
|
292
|
+
page: z.number().int().min(1).max(50).optional().describe('Page number for provider APIs.'),
|
|
293
|
+
orientation: z.enum(['auto', 'landscape', 'portrait', 'square']).optional().describe('Optional orientation hint.'),
|
|
294
|
+
},
|
|
295
|
+
async (input) => {
|
|
296
|
+
try {
|
|
297
|
+
const payload = await searchLicensedImages(input);
|
|
298
|
+
return {
|
|
299
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
300
|
+
};
|
|
301
|
+
} catch (error) {
|
|
302
|
+
return toolError(error);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
const transport = new StdioServerTransport();
|
|
308
|
+
await server.connect(transport);
|
|
309
|
+
console.error('[official-image-search-licensed] MCP Server started');
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "image-search-licensed",
|
|
3
|
+
"name": "Official Image Search Licensed MCP",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"runtime": "node",
|
|
6
|
+
"entrypoint": "index.js",
|
|
7
|
+
"tool_declarations": [
|
|
8
|
+
{ "name": "image_search_licensed", "classification": "cacheable" }
|
|
9
|
+
],
|
|
10
|
+
"smoke_test": {
|
|
11
|
+
"tool": "image_search_licensed",
|
|
12
|
+
"arguments": {
|
|
13
|
+
"query": "蓝色科技背景",
|
|
14
|
+
"provider": "auto",
|
|
15
|
+
"limit": 3
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { startFixtureServer } from '../../official-common/server.js';
|
|
4
|
+
|
|
5
|
+
const FIXTURE_META = Object.freeze({
|
|
6
|
+
mode: 'fixture',
|
|
7
|
+
as_of: '2026-04-29',
|
|
8
|
+
capability: 'keyword-research',
|
|
9
|
+
disclaimer: 'Keyword stats are deterministic fixtures for planning rehearsal, not live platform telemetry.',
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const PLATFORM_ALIAS = Object.freeze({
|
|
13
|
+
xiaohongshu: 'xhs',
|
|
14
|
+
redbook: 'xhs',
|
|
15
|
+
douyin: 'douyin',
|
|
16
|
+
tiktok_cn: 'douyin',
|
|
17
|
+
wechat: 'wechat-mp',
|
|
18
|
+
wechat_mp: 'wechat-mp',
|
|
19
|
+
wechatmp: 'wechat-mp',
|
|
20
|
+
gzh: 'wechat-mp',
|
|
21
|
+
'\u516c\u4f17\u53f7': 'wechat-mp',
|
|
22
|
+
all: 'all',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const PLATFORM_HINTS = Object.freeze({
|
|
26
|
+
all: ['\u8d8b\u52bf', '\u6848\u4f8b', '\u6a21\u677f'],
|
|
27
|
+
xhs: ['\u5c0f\u7ea2\u4e66\u7b14\u8bb0', '\u6807\u9898\u5199\u6cd5', '\u5c01\u9762\u7b56\u7565'],
|
|
28
|
+
douyin: ['\u77ed\u89c6\u9891\u811a\u672c', '\u5b8c\u64ad\u7387', '\u9996\u5c4f\u5f00\u573a'],
|
|
29
|
+
'wechat-mp': ['\u516c\u4f17\u53f7\u957f\u6587', '\u8f6c\u5316\u6f0f\u6597', '\u79c1\u57df\u627f\u63a5'],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const INTENT_BRANCHES = Object.freeze([
|
|
33
|
+
{
|
|
34
|
+
id: 'pain_point',
|
|
35
|
+
suffix: '\u75db\u70b9',
|
|
36
|
+
weight: 1.0,
|
|
37
|
+
child_templates: ['{seed} \u5e38\u89c1\u95ee\u9898', '{seed} \u4e3a\u4ec0\u4e48\u6ca1\u6548\u679c', '{seed} \u8bef\u533a'],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'tutorial',
|
|
41
|
+
suffix: '\u6559\u7a0b',
|
|
42
|
+
weight: 0.94,
|
|
43
|
+
child_templates: ['{seed} \u5165\u95e8', '{seed} \u64cd\u4f5c\u6b65\u9aa4', '{seed} \u6267\u884c\u6e05\u5355'],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'template',
|
|
47
|
+
suffix: '\u6a21\u677f',
|
|
48
|
+
weight: 0.9,
|
|
49
|
+
child_templates: ['{seed} \u6587\u6848\u6a21\u677f', '{seed} \u9009\u9898\u6a21\u677f', '{seed} \u8868\u683c\u6a21\u677f'],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: 'case_study',
|
|
53
|
+
suffix: '\u6848\u4f8b',
|
|
54
|
+
weight: 0.88,
|
|
55
|
+
child_templates: ['{seed} \u6210\u529f\u6848\u4f8b', '{seed} \u5931\u8d25\u590d\u76d8', '{seed} A/B \u6d4b\u8bd5'],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'comparison',
|
|
59
|
+
suffix: '\u5bf9\u6bd4',
|
|
60
|
+
weight: 0.84,
|
|
61
|
+
child_templates: ['{seed} \u5de5\u5177\u5bf9\u6bd4', '{seed} \u65b9\u6cd5\u5bf9\u6bd4', '{seed} \u54ea\u4e2a\u66f4\u597d'],
|
|
62
|
+
},
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
function normalizePlatform(value) {
|
|
66
|
+
const raw = String(value ?? '').trim().toLowerCase();
|
|
67
|
+
if (!raw) return 'all';
|
|
68
|
+
return PLATFORM_ALIAS[raw] ?? raw;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function clampInt(value, min, max, fallback) {
|
|
72
|
+
const n = Number(value);
|
|
73
|
+
if (!Number.isFinite(n)) return fallback;
|
|
74
|
+
return Math.max(min, Math.min(max, Math.trunc(n)));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function deterministicRatio(seed, salt) {
|
|
78
|
+
const text = `${seed}#${salt}`;
|
|
79
|
+
let hash = 2166136261;
|
|
80
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
81
|
+
hash ^= text.charCodeAt(i);
|
|
82
|
+
hash = Math.imul(hash, 16777619);
|
|
83
|
+
}
|
|
84
|
+
return (hash >>> 0) / 4294967295;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function computeMetrics({ keyword, seedKeyword, depth, intentWeight }) {
|
|
88
|
+
const base = deterministicRatio(keyword, seedKeyword);
|
|
89
|
+
const searchVolume = Math.round((2200 + base * 8200) * (1 - depth * 0.18) * intentWeight);
|
|
90
|
+
const competitionRaw = 0.28 + deterministicRatio(seedKeyword, keyword) * 0.58 + depth * 0.06;
|
|
91
|
+
const competition = Math.max(0.1, Math.min(0.95, Number(competitionRaw.toFixed(2))));
|
|
92
|
+
const relevanceRaw = 0.62 + deterministicRatio(`${seedKeyword}:${keyword}`, depth) * 0.33 - depth * 0.05;
|
|
93
|
+
const relevance = Math.max(0.4, Math.min(1, Number(relevanceRaw.toFixed(2))));
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
search_volume: Math.max(120, searchVolume),
|
|
97
|
+
competition,
|
|
98
|
+
relevance,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function computePriority(metrics) {
|
|
103
|
+
const volumeScore = Math.min(100, 20 + metrics.search_volume / 120);
|
|
104
|
+
const competitionScore = (1 - metrics.competition) * 100;
|
|
105
|
+
const relevanceScore = metrics.relevance * 100;
|
|
106
|
+
const score = Math.round(volumeScore * 0.45 + competitionScore * 0.35 + relevanceScore * 0.2);
|
|
107
|
+
|
|
108
|
+
let tier = 'P4';
|
|
109
|
+
if (score >= 82) tier = 'P1';
|
|
110
|
+
else if (score >= 68) tier = 'P2';
|
|
111
|
+
else if (score >= 55) tier = 'P3';
|
|
112
|
+
|
|
113
|
+
return { score, tier };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function buildNode({ keyword, seedKeyword, depth, intent, intentWeight }) {
|
|
117
|
+
const metrics = computeMetrics({ keyword, seedKeyword, depth, intentWeight });
|
|
118
|
+
const priority = computePriority(metrics);
|
|
119
|
+
return {
|
|
120
|
+
keyword,
|
|
121
|
+
depth,
|
|
122
|
+
intent,
|
|
123
|
+
metrics,
|
|
124
|
+
priority_score: priority.score,
|
|
125
|
+
priority_tier: priority.tier,
|
|
126
|
+
children: [],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function renderTemplate(template, seedKeyword) {
|
|
131
|
+
return String(template ?? '').replaceAll('{seed}', seedKeyword);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function uniqueKeywords(items) {
|
|
135
|
+
const seen = new Set();
|
|
136
|
+
const output = [];
|
|
137
|
+
for (const item of items) {
|
|
138
|
+
const key = String(item ?? '').trim();
|
|
139
|
+
if (!key || seen.has(key)) continue;
|
|
140
|
+
seen.add(key);
|
|
141
|
+
output.push(key);
|
|
142
|
+
}
|
|
143
|
+
return output;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function flattenTree(root) {
|
|
147
|
+
const output = [];
|
|
148
|
+
const stack = [root];
|
|
149
|
+
while (stack.length > 0) {
|
|
150
|
+
const current = stack.pop();
|
|
151
|
+
output.push(current);
|
|
152
|
+
for (let i = current.children.length - 1; i >= 0; i -= 1) {
|
|
153
|
+
stack.push(current.children[i]);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return output;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function buildKeywordTree({ seedKeyword, platform, maxDepth, branchLimit }) {
|
|
160
|
+
const normalizedSeed = String(seedKeyword).trim();
|
|
161
|
+
const hints = PLATFORM_HINTS[platform] ?? PLATFORM_HINTS.all;
|
|
162
|
+
|
|
163
|
+
const root = buildNode({
|
|
164
|
+
keyword: normalizedSeed,
|
|
165
|
+
seedKeyword: normalizedSeed,
|
|
166
|
+
depth: 0,
|
|
167
|
+
intent: 'seed',
|
|
168
|
+
intentWeight: 1.04,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
if (maxDepth < 1) return root;
|
|
172
|
+
|
|
173
|
+
const selectedBranches = INTENT_BRANCHES.slice(0, branchLimit);
|
|
174
|
+
|
|
175
|
+
for (const branch of selectedBranches) {
|
|
176
|
+
const branchKeyword = `${normalizedSeed} ${branch.suffix}`;
|
|
177
|
+
const branchNode = buildNode({
|
|
178
|
+
keyword: branchKeyword,
|
|
179
|
+
seedKeyword: normalizedSeed,
|
|
180
|
+
depth: 1,
|
|
181
|
+
intent: branch.id,
|
|
182
|
+
intentWeight: branch.weight,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
if (maxDepth >= 2) {
|
|
186
|
+
const baseChildren = branch.child_templates.map((template) => renderTemplate(template, normalizedSeed));
|
|
187
|
+
const hintChildren = hints.map((hint) => `${normalizedSeed} ${hint}`);
|
|
188
|
+
const childKeywords = uniqueKeywords([...baseChildren, ...hintChildren]).slice(0, 3);
|
|
189
|
+
|
|
190
|
+
for (const childKeyword of childKeywords) {
|
|
191
|
+
branchNode.children.push(buildNode({
|
|
192
|
+
keyword: childKeyword,
|
|
193
|
+
seedKeyword: normalizedSeed,
|
|
194
|
+
depth: 2,
|
|
195
|
+
intent: `${branch.id}_longtail`,
|
|
196
|
+
intentWeight: Math.max(0.65, branch.weight - 0.1),
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
root.children.push(branchNode);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return root;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
await startFixtureServer({
|
|
208
|
+
serverId: 'keyword-research',
|
|
209
|
+
serverName: 'official-keyword-research',
|
|
210
|
+
toolName: 'keyword_research',
|
|
211
|
+
toolDescription: 'Generate fixture-mode keyword tree with priority ranking using seed keyword and platform context.',
|
|
212
|
+
inputSchema: {
|
|
213
|
+
seed_keyword: z.string().min(1).describe('Seed keyword/topic, e.g. \"\u5c0f\u7ea2\u4e66\u8d77\u53f7\".'),
|
|
214
|
+
platform: z.enum(['all', 'xhs', 'douyin', 'wechat-mp']).optional()
|
|
215
|
+
.describe('Optional platform hint for long-tail expansion.'),
|
|
216
|
+
locale: z.string().optional().describe('Locale hint, default zh-CN.'),
|
|
217
|
+
max_depth: z.number().int().min(1).max(2).optional()
|
|
218
|
+
.describe('Tree depth, default 2.'),
|
|
219
|
+
branch_limit: z.number().int().min(2).max(5).optional()
|
|
220
|
+
.describe('Max first-level branches, default 4.'),
|
|
221
|
+
top_k: z.number().int().min(3).max(30).optional()
|
|
222
|
+
.describe('Top K keywords in priority ranking, default 12.'),
|
|
223
|
+
},
|
|
224
|
+
handler: ({
|
|
225
|
+
seed_keyword,
|
|
226
|
+
platform = 'all',
|
|
227
|
+
locale = 'zh-CN',
|
|
228
|
+
max_depth = 2,
|
|
229
|
+
branch_limit = 4,
|
|
230
|
+
top_k = 12,
|
|
231
|
+
}) => {
|
|
232
|
+
const seedKeyword = String(seed_keyword ?? '').trim();
|
|
233
|
+
if (!seedKeyword) throw new Error('seed_keyword_required');
|
|
234
|
+
|
|
235
|
+
const normalizedPlatform = normalizePlatform(platform);
|
|
236
|
+
const normalizedLocale = String(locale ?? 'zh-CN').trim() || 'zh-CN';
|
|
237
|
+
const safeDepth = clampInt(max_depth, 1, 2, 2);
|
|
238
|
+
const safeBranchLimit = clampInt(branch_limit, 2, 5, 4);
|
|
239
|
+
const safeTopK = clampInt(top_k, 3, 30, 12);
|
|
240
|
+
|
|
241
|
+
const keywordTree = buildKeywordTree({
|
|
242
|
+
seedKeyword,
|
|
243
|
+
platform: normalizedPlatform,
|
|
244
|
+
maxDepth: safeDepth,
|
|
245
|
+
branchLimit: safeBranchLimit,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const flatNodes = flattenTree(keywordTree);
|
|
249
|
+
const ranked = flatNodes
|
|
250
|
+
.map((node) => ({
|
|
251
|
+
keyword: node.keyword,
|
|
252
|
+
depth: node.depth,
|
|
253
|
+
intent: node.intent,
|
|
254
|
+
priority_score: node.priority_score,
|
|
255
|
+
priority_tier: node.priority_tier,
|
|
256
|
+
search_volume: node.metrics.search_volume,
|
|
257
|
+
competition: node.metrics.competition,
|
|
258
|
+
relevance: node.metrics.relevance,
|
|
259
|
+
}))
|
|
260
|
+
.sort((a, b) => {
|
|
261
|
+
if (b.priority_score !== a.priority_score) return b.priority_score - a.priority_score;
|
|
262
|
+
if (b.search_volume !== a.search_volume) return b.search_volume - a.search_volume;
|
|
263
|
+
return a.keyword.localeCompare(b.keyword, 'zh-Hans-CN');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const priorityRanking = ranked.slice(0, safeTopK);
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
...FIXTURE_META,
|
|
270
|
+
seed_keyword: seedKeyword,
|
|
271
|
+
platform: normalizedPlatform,
|
|
272
|
+
locale: normalizedLocale,
|
|
273
|
+
priority_model: {
|
|
274
|
+
score_formula: '0.45*volume + 0.35*(1-competition) + 0.20*relevance',
|
|
275
|
+
tiers: {
|
|
276
|
+
P1: 'score >= 82',
|
|
277
|
+
P2: '68-81',
|
|
278
|
+
P3: '55-67',
|
|
279
|
+
P4: '< 55',
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
node_count: ranked.length,
|
|
283
|
+
keyword_tree: keywordTree,
|
|
284
|
+
priority_ranking: priorityRanking,
|
|
285
|
+
related_keywords: priorityRanking
|
|
286
|
+
.map((item) => item.keyword)
|
|
287
|
+
.filter((item) => item !== seedKeyword)
|
|
288
|
+
.slice(0, 12),
|
|
289
|
+
generated_at: new Date().toISOString(),
|
|
290
|
+
};
|
|
291
|
+
},
|
|
292
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "keyword-research",
|
|
3
|
+
"name": "Official Keyword Research MCP",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"runtime": "node",
|
|
6
|
+
"entrypoint": "index.js",
|
|
7
|
+
"tool_declarations": [
|
|
8
|
+
{ "name": "keyword_research", "classification": "cacheable" }
|
|
9
|
+
],
|
|
10
|
+
"smoke_test": {
|
|
11
|
+
"tool": "keyword_research",
|
|
12
|
+
"arguments": {
|
|
13
|
+
"seed_keyword": "\u5c0f\u7ea2\u4e66\u8d77\u53f7",
|
|
14
|
+
"platform": "xhs",
|
|
15
|
+
"branch_limit": 3,
|
|
16
|
+
"top_k": 8
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|