@lightcone-ai/daemon 0.15.52 → 0.15.54
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/_thin-proxy/forward.js +80 -0
- package/mcp-servers/official/audience-research/index.js +24 -376
- package/mcp-servers/official/hook-pattern-library/index.js +17 -410
- package/mcp-servers/official/keyword-research/index.js +17 -324
- package/mcp-servers/official/page-understanding/index.js +17 -96
- package/mcp-servers/official/platform-policy-db/index.js +19 -264
- package/mcp-servers/official/video-narration-planner/index.js +30 -130
- package/package.json +1 -1
- package/mcp-servers/official/keyword-research/keyword-fixtures.json +0 -58
- package/mcp-servers/official/platform-policy-db/policy-fixtures.json +0 -257
- package/mcp-servers/official/video-narration-planner/core.js +0 -1403
- package/mcp-servers/official/video-narration-planner/planner-config.json +0 -112
- package/src/_vendor/video/understanding/analyze-page.js +0 -737
- package/src/_vendor/video/understanding/heuristics.js +0 -826
- package/src/_vendor/video/understanding/index.js +0 -11
- package/src/_vendor/video/understanding/llm-client.js +0 -261
- package/src/_vendor/video/understanding/schema.js +0 -254
- package/src/_vendor/video/understanding/site-selectors.js +0 -47
|
@@ -1,331 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
// Thin-proxy. Real impl on lightcone server.
|
|
3
|
+
// Source of truth: src/mcp-services/keyword-research.js
|
|
3
4
|
import { z } from 'zod';
|
|
4
|
-
import {
|
|
5
|
+
import { startThinProxy } from '../../_thin-proxy/forward.js';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
const FIXTURE_META = KEYWORD_FIXTURES.fixtureMeta;
|
|
8
|
-
const PLATFORM_ALIAS = KEYWORD_FIXTURES.platformAliases;
|
|
9
|
-
const PLATFORM_HINTS = KEYWORD_FIXTURES.platformHints;
|
|
10
|
-
const INTENT_BRANCHES = KEYWORD_FIXTURES.intentBranches;
|
|
11
|
-
|
|
12
|
-
function isPlainObject(value) {
|
|
13
|
-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function deepFreeze(value) {
|
|
17
|
-
if (!value || typeof value !== 'object') return value;
|
|
18
|
-
if (Object.isFrozen(value)) return value;
|
|
19
|
-
if (Array.isArray(value)) {
|
|
20
|
-
value.forEach(item => deepFreeze(item));
|
|
21
|
-
return Object.freeze(value);
|
|
22
|
-
}
|
|
23
|
-
for (const key of Object.keys(value)) {
|
|
24
|
-
deepFreeze(value[key]);
|
|
25
|
-
}
|
|
26
|
-
return Object.freeze(value);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function toToken(value) {
|
|
30
|
-
return String(value ?? '').trim();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function normalizeAliasMap(rawAliases) {
|
|
34
|
-
if (!isPlainObject(rawAliases)) return { all: 'all' };
|
|
35
|
-
const aliases = {};
|
|
36
|
-
for (const [key, value] of Object.entries(rawAliases)) {
|
|
37
|
-
const alias = toToken(key).toLowerCase();
|
|
38
|
-
const target = toToken(value).toLowerCase();
|
|
39
|
-
if (!alias || !target) continue;
|
|
40
|
-
aliases[alias] = target;
|
|
41
|
-
}
|
|
42
|
-
if (!Object.hasOwn(aliases, 'all')) aliases.all = 'all';
|
|
43
|
-
return aliases;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function normalizePlatformHints(rawHints) {
|
|
47
|
-
if (!isPlainObject(rawHints)) return { all: [] };
|
|
48
|
-
const hints = {};
|
|
49
|
-
for (const [key, value] of Object.entries(rawHints)) {
|
|
50
|
-
const platform = toToken(key).toLowerCase();
|
|
51
|
-
if (!platform || !Array.isArray(value)) continue;
|
|
52
|
-
hints[platform] = value.map(item => toToken(item)).filter(Boolean);
|
|
53
|
-
}
|
|
54
|
-
if (!Array.isArray(hints.all)) hints.all = [];
|
|
55
|
-
return hints;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function normalizeIntentBranches(rawBranches) {
|
|
59
|
-
if (!Array.isArray(rawBranches)) return [];
|
|
60
|
-
return rawBranches
|
|
61
|
-
.map((branch) => {
|
|
62
|
-
if (!isPlainObject(branch)) return null;
|
|
63
|
-
const id = toToken(branch.id);
|
|
64
|
-
const suffix = toToken(branch.suffix);
|
|
65
|
-
const weight = Number(branch.weight);
|
|
66
|
-
const childTemplates = Array.isArray(branch.child_templates)
|
|
67
|
-
? branch.child_templates.map(item => toToken(item)).filter(Boolean)
|
|
68
|
-
: [];
|
|
69
|
-
if (!id || !suffix || !Number.isFinite(weight) || childTemplates.length === 0) return null;
|
|
70
|
-
return {
|
|
71
|
-
id,
|
|
72
|
-
suffix,
|
|
73
|
-
weight,
|
|
74
|
-
child_templates: childTemplates,
|
|
75
|
-
};
|
|
76
|
-
})
|
|
77
|
-
.filter(Boolean);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function loadKeywordFixtures() {
|
|
81
|
-
let parsed = null;
|
|
82
|
-
try {
|
|
83
|
-
parsed = JSON.parse(
|
|
84
|
-
readFileSync(new URL('./keyword-fixtures.json', import.meta.url), 'utf8')
|
|
85
|
-
);
|
|
86
|
-
} catch (error) {
|
|
87
|
-
throw new Error(`keyword_fixture_load_failed:${error.message}`);
|
|
88
|
-
}
|
|
89
|
-
if (!isPlainObject(parsed)) {
|
|
90
|
-
throw new Error('keyword_fixture_invalid:root_object_required');
|
|
91
|
-
}
|
|
92
|
-
const intentBranches = normalizeIntentBranches(parsed.intent_branches);
|
|
93
|
-
if (intentBranches.length === 0) {
|
|
94
|
-
throw new Error('keyword_fixture_invalid:intent_branches_required');
|
|
95
|
-
}
|
|
96
|
-
return deepFreeze({
|
|
97
|
-
fixtureMeta: isPlainObject(parsed.fixture_meta) ? parsed.fixture_meta : {},
|
|
98
|
-
platformAliases: normalizeAliasMap(parsed.platform_aliases),
|
|
99
|
-
platformHints: normalizePlatformHints(parsed.platform_hints),
|
|
100
|
-
intentBranches,
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function normalizePlatform(value) {
|
|
105
|
-
const raw = String(value ?? '').trim().toLowerCase();
|
|
106
|
-
if (!raw) return 'all';
|
|
107
|
-
return PLATFORM_ALIAS[raw] ?? raw;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function clampInt(value, min, max, fallback) {
|
|
111
|
-
const n = Number(value);
|
|
112
|
-
if (!Number.isFinite(n)) return fallback;
|
|
113
|
-
return Math.max(min, Math.min(max, Math.trunc(n)));
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function deterministicRatio(seed, salt) {
|
|
117
|
-
const text = `${seed}#${salt}`;
|
|
118
|
-
let hash = 2166136261;
|
|
119
|
-
for (let i = 0; i < text.length; i += 1) {
|
|
120
|
-
hash ^= text.charCodeAt(i);
|
|
121
|
-
hash = Math.imul(hash, 16777619);
|
|
122
|
-
}
|
|
123
|
-
return (hash >>> 0) / 4294967295;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function computeMetrics({ keyword, seedKeyword, depth, intentWeight }) {
|
|
127
|
-
const base = deterministicRatio(keyword, seedKeyword);
|
|
128
|
-
const searchVolume = Math.round((2200 + base * 8200) * (1 - depth * 0.18) * intentWeight);
|
|
129
|
-
const competitionRaw = 0.28 + deterministicRatio(seedKeyword, keyword) * 0.58 + depth * 0.06;
|
|
130
|
-
const competition = Math.max(0.1, Math.min(0.95, Number(competitionRaw.toFixed(2))));
|
|
131
|
-
const relevanceRaw = 0.62 + deterministicRatio(`${seedKeyword}:${keyword}`, depth) * 0.33 - depth * 0.05;
|
|
132
|
-
const relevance = Math.max(0.4, Math.min(1, Number(relevanceRaw.toFixed(2))));
|
|
133
|
-
|
|
134
|
-
return {
|
|
135
|
-
search_volume: Math.max(120, searchVolume),
|
|
136
|
-
competition,
|
|
137
|
-
relevance,
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function computePriority(metrics) {
|
|
142
|
-
const volumeScore = Math.min(100, 20 + metrics.search_volume / 120);
|
|
143
|
-
const competitionScore = (1 - metrics.competition) * 100;
|
|
144
|
-
const relevanceScore = metrics.relevance * 100;
|
|
145
|
-
const score = Math.round(volumeScore * 0.45 + competitionScore * 0.35 + relevanceScore * 0.2);
|
|
146
|
-
|
|
147
|
-
let tier = 'P4';
|
|
148
|
-
if (score >= 82) tier = 'P1';
|
|
149
|
-
else if (score >= 68) tier = 'P2';
|
|
150
|
-
else if (score >= 55) tier = 'P3';
|
|
151
|
-
|
|
152
|
-
return { score, tier };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function buildNode({ keyword, seedKeyword, depth, intent, intentWeight }) {
|
|
156
|
-
const metrics = computeMetrics({ keyword, seedKeyword, depth, intentWeight });
|
|
157
|
-
const priority = computePriority(metrics);
|
|
158
|
-
return {
|
|
159
|
-
keyword,
|
|
160
|
-
depth,
|
|
161
|
-
intent,
|
|
162
|
-
metrics,
|
|
163
|
-
priority_score: priority.score,
|
|
164
|
-
priority_tier: priority.tier,
|
|
165
|
-
children: [],
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function renderTemplate(template, seedKeyword) {
|
|
170
|
-
return String(template ?? '').replaceAll('{seed}', seedKeyword);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function uniqueKeywords(items) {
|
|
174
|
-
const seen = new Set();
|
|
175
|
-
const output = [];
|
|
176
|
-
for (const item of items) {
|
|
177
|
-
const key = String(item ?? '').trim();
|
|
178
|
-
if (!key || seen.has(key)) continue;
|
|
179
|
-
seen.add(key);
|
|
180
|
-
output.push(key);
|
|
181
|
-
}
|
|
182
|
-
return output;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function flattenTree(root) {
|
|
186
|
-
const output = [];
|
|
187
|
-
const stack = [root];
|
|
188
|
-
while (stack.length > 0) {
|
|
189
|
-
const current = stack.pop();
|
|
190
|
-
output.push(current);
|
|
191
|
-
for (let i = current.children.length - 1; i >= 0; i -= 1) {
|
|
192
|
-
stack.push(current.children[i]);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
return output;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function buildKeywordTree({ seedKeyword, platform, maxDepth, branchLimit }) {
|
|
199
|
-
const normalizedSeed = String(seedKeyword).trim();
|
|
200
|
-
const hints = PLATFORM_HINTS[platform] ?? PLATFORM_HINTS.all;
|
|
201
|
-
|
|
202
|
-
const root = buildNode({
|
|
203
|
-
keyword: normalizedSeed,
|
|
204
|
-
seedKeyword: normalizedSeed,
|
|
205
|
-
depth: 0,
|
|
206
|
-
intent: 'seed',
|
|
207
|
-
intentWeight: 1.04,
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
if (maxDepth < 1) return root;
|
|
211
|
-
|
|
212
|
-
const selectedBranches = INTENT_BRANCHES.slice(0, branchLimit);
|
|
213
|
-
|
|
214
|
-
for (const branch of selectedBranches) {
|
|
215
|
-
const branchKeyword = `${normalizedSeed} ${branch.suffix}`;
|
|
216
|
-
const branchNode = buildNode({
|
|
217
|
-
keyword: branchKeyword,
|
|
218
|
-
seedKeyword: normalizedSeed,
|
|
219
|
-
depth: 1,
|
|
220
|
-
intent: branch.id,
|
|
221
|
-
intentWeight: branch.weight,
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
if (maxDepth >= 2) {
|
|
225
|
-
const baseChildren = branch.child_templates.map((template) => renderTemplate(template, normalizedSeed));
|
|
226
|
-
const hintChildren = hints.map((hint) => `${normalizedSeed} ${hint}`);
|
|
227
|
-
const childKeywords = uniqueKeywords([...baseChildren, ...hintChildren]).slice(0, 3);
|
|
228
|
-
|
|
229
|
-
for (const childKeyword of childKeywords) {
|
|
230
|
-
branchNode.children.push(buildNode({
|
|
231
|
-
keyword: childKeyword,
|
|
232
|
-
seedKeyword: normalizedSeed,
|
|
233
|
-
depth: 2,
|
|
234
|
-
intent: `${branch.id}_longtail`,
|
|
235
|
-
intentWeight: Math.max(0.65, branch.weight - 0.1),
|
|
236
|
-
}));
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
root.children.push(branchNode);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return root;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
await startFixtureServer({
|
|
7
|
+
await startThinProxy({
|
|
247
8
|
serverId: 'keyword-research',
|
|
248
9
|
serverName: 'official-keyword-research',
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
top_k: z.number().int().min(3).max(30).optional()
|
|
261
|
-
.describe('Top K keywords in priority ranking, default 12.'),
|
|
262
|
-
},
|
|
263
|
-
handler: ({
|
|
264
|
-
seed_keyword,
|
|
265
|
-
platform = 'all',
|
|
266
|
-
locale = 'zh-CN',
|
|
267
|
-
max_depth = 2,
|
|
268
|
-
branch_limit = 4,
|
|
269
|
-
top_k = 12,
|
|
270
|
-
}) => {
|
|
271
|
-
const seedKeyword = String(seed_keyword ?? '').trim();
|
|
272
|
-
if (!seedKeyword) throw new Error('seed_keyword_required');
|
|
273
|
-
|
|
274
|
-
const normalizedPlatform = normalizePlatform(platform);
|
|
275
|
-
const normalizedLocale = String(locale ?? 'zh-CN').trim() || 'zh-CN';
|
|
276
|
-
const safeDepth = clampInt(max_depth, 1, 2, 2);
|
|
277
|
-
const safeBranchLimit = clampInt(branch_limit, 2, 5, 4);
|
|
278
|
-
const safeTopK = clampInt(top_k, 3, 30, 12);
|
|
279
|
-
|
|
280
|
-
const keywordTree = buildKeywordTree({
|
|
281
|
-
seedKeyword,
|
|
282
|
-
platform: normalizedPlatform,
|
|
283
|
-
maxDepth: safeDepth,
|
|
284
|
-
branchLimit: safeBranchLimit,
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
const flatNodes = flattenTree(keywordTree);
|
|
288
|
-
const ranked = flatNodes
|
|
289
|
-
.map((node) => ({
|
|
290
|
-
keyword: node.keyword,
|
|
291
|
-
depth: node.depth,
|
|
292
|
-
intent: node.intent,
|
|
293
|
-
priority_score: node.priority_score,
|
|
294
|
-
priority_tier: node.priority_tier,
|
|
295
|
-
search_volume: node.metrics.search_volume,
|
|
296
|
-
competition: node.metrics.competition,
|
|
297
|
-
relevance: node.metrics.relevance,
|
|
298
|
-
}))
|
|
299
|
-
.sort((a, b) => {
|
|
300
|
-
if (b.priority_score !== a.priority_score) return b.priority_score - a.priority_score;
|
|
301
|
-
if (b.search_volume !== a.search_volume) return b.search_volume - a.search_volume;
|
|
302
|
-
return a.keyword.localeCompare(b.keyword, 'zh-Hans-CN');
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
const priorityRanking = ranked.slice(0, safeTopK);
|
|
306
|
-
|
|
307
|
-
return {
|
|
308
|
-
...FIXTURE_META,
|
|
309
|
-
seed_keyword: seedKeyword,
|
|
310
|
-
platform: normalizedPlatform,
|
|
311
|
-
locale: normalizedLocale,
|
|
312
|
-
priority_model: {
|
|
313
|
-
score_formula: '0.45*volume + 0.35*(1-competition) + 0.20*relevance',
|
|
314
|
-
tiers: {
|
|
315
|
-
P1: 'score >= 82',
|
|
316
|
-
P2: '68-81',
|
|
317
|
-
P3: '55-67',
|
|
318
|
-
P4: '< 55',
|
|
319
|
-
},
|
|
10
|
+
tools: [
|
|
11
|
+
{
|
|
12
|
+
name: 'keyword_research',
|
|
13
|
+
description: 'Generate fixture-mode keyword tree with priority ranking using seed keyword and platform context.',
|
|
14
|
+
inputSchema: {
|
|
15
|
+
seed_keyword: z.string().min(1),
|
|
16
|
+
platform: z.enum(['all', 'xhs', 'douyin', 'wechat-mp']).optional(),
|
|
17
|
+
locale: z.string().optional(),
|
|
18
|
+
max_depth: z.number().int().min(1).max(2).optional(),
|
|
19
|
+
branch_limit: z.number().int().min(2).max(5).optional(),
|
|
20
|
+
top_k: z.number().int().min(3).max(30).optional(),
|
|
320
21
|
},
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
priority_ranking: priorityRanking,
|
|
324
|
-
related_keywords: priorityRanking
|
|
325
|
-
.map((item) => item.keyword)
|
|
326
|
-
.filter((item) => item !== seedKeyword)
|
|
327
|
-
.slice(0, 12),
|
|
328
|
-
generated_at: new Date().toISOString(),
|
|
329
|
-
};
|
|
330
|
-
},
|
|
22
|
+
},
|
|
23
|
+
],
|
|
331
24
|
});
|
|
@@ -1,41 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
// Thin-proxy. Real impl on lightcone server.
|
|
3
|
+
// Source of truth: src/mcp-services/page-understanding.js (wraps src/video/understanding/).
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
analyzePage,
|
|
8
|
-
analyzePageFromHtmlFixture,
|
|
9
|
-
validatePageUnderstanding,
|
|
10
|
-
} from '../../../src/_vendor/video/understanding/index.js';
|
|
11
|
-
import {
|
|
12
|
-
findToolBlockRule,
|
|
13
|
-
loadToolBlockRulesFromManifest,
|
|
14
|
-
} from '../../official-common/tool-access-policy.js';
|
|
15
|
-
|
|
16
|
-
const TOOL_BLOCK_RULES = loadToolBlockRulesFromManifest(new URL('./manifest.json', import.meta.url));
|
|
17
|
-
|
|
18
|
-
function resolveAnalyzePageBlockMessage(env = process.env) {
|
|
19
|
-
const matchedRule = findToolBlockRule(TOOL_BLOCK_RULES, {
|
|
20
|
-
toolName: 'analyze_page',
|
|
21
|
-
workspaceId: env?.WORKSPACE_ID,
|
|
22
|
-
agentId: env?.AGENT_ID,
|
|
23
|
-
});
|
|
24
|
-
return matchedRule?.message ?? null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function toText(payload) {
|
|
28
|
-
return {
|
|
29
|
-
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function toError(message) {
|
|
34
|
-
return {
|
|
35
|
-
isError: true,
|
|
36
|
-
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
37
|
-
};
|
|
38
|
-
}
|
|
5
|
+
import { startThinProxy } from '../../_thin-proxy/forward.js';
|
|
39
6
|
|
|
40
7
|
const AnalyzePageOptionsSchema = z.object({
|
|
41
8
|
settleMs: z.number().int().min(500).max(30000).optional(),
|
|
@@ -49,64 +16,18 @@ const AnalyzePageOptionsSchema = z.object({
|
|
|
49
16
|
fixture_mode: z.boolean().optional(),
|
|
50
17
|
}).passthrough();
|
|
51
18
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
19
|
+
await startThinProxy({
|
|
20
|
+
serverId: 'page-understanding',
|
|
21
|
+
serverName: 'official-page-understanding',
|
|
22
|
+
tools: [
|
|
23
|
+
{
|
|
24
|
+
name: 'analyze_page',
|
|
25
|
+
description: 'Analyze webpage structure for short-video narration planning. Returns page_understanding schema.',
|
|
26
|
+
inputSchema: {
|
|
27
|
+
url: z.string().url(),
|
|
28
|
+
persona: z.string().optional(),
|
|
29
|
+
options: AnalyzePageOptionsSchema.optional(),
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
],
|
|
55
33
|
});
|
|
56
|
-
|
|
57
|
-
server.tool(
|
|
58
|
-
'analyze_page',
|
|
59
|
-
'Analyze webpage structure for short-video narration planning. Returns page_understanding schema.',
|
|
60
|
-
{
|
|
61
|
-
url: z.string().url().describe('Target page URL.'),
|
|
62
|
-
persona: z.string().optional().describe('Audience persona, e.g. "校招求职学生".'),
|
|
63
|
-
options: AnalyzePageOptionsSchema.optional(),
|
|
64
|
-
},
|
|
65
|
-
async ({ url, persona = '', options = {} }) => {
|
|
66
|
-
try {
|
|
67
|
-
const blockedMessage = resolveAnalyzePageBlockMessage();
|
|
68
|
-
if (blockedMessage) {
|
|
69
|
-
return toError(blockedMessage);
|
|
70
|
-
}
|
|
71
|
-
let payload;
|
|
72
|
-
if (options?.fixture_mode) {
|
|
73
|
-
payload = await analyzePageFromHtmlFixture({
|
|
74
|
-
url,
|
|
75
|
-
hostname: new URL(url).hostname,
|
|
76
|
-
persona,
|
|
77
|
-
bins: [
|
|
78
|
-
{ y_center: 380, text: '这是一个用于 smoke test 的结构化 bin 内容。' },
|
|
79
|
-
{ y_center: 980, text: '该页面包含若干重点段落,用于验证 schema 输出。' },
|
|
80
|
-
],
|
|
81
|
-
hotspots: [
|
|
82
|
-
{ y: 420, reason: '标题区域', weight: 9, type: 'h1' },
|
|
83
|
-
{ y: 1100, reason: '正文重点', weight: 8, type: 'h2' },
|
|
84
|
-
],
|
|
85
|
-
meta: {
|
|
86
|
-
title: 'Fixture Page',
|
|
87
|
-
og_title: 'Fixture Page',
|
|
88
|
-
og_description: 'Fixture mode output for analyze_page smoke test.',
|
|
89
|
-
},
|
|
90
|
-
});
|
|
91
|
-
} else {
|
|
92
|
-
payload = await analyzePage({
|
|
93
|
-
url,
|
|
94
|
-
persona,
|
|
95
|
-
options,
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const validation = validatePageUnderstanding(payload);
|
|
100
|
-
if (!validation.ok) {
|
|
101
|
-
return toError(`page_understanding_validation_failed: ${validation.errors.join('; ')}`);
|
|
102
|
-
}
|
|
103
|
-
return toText(payload);
|
|
104
|
-
} catch (error) {
|
|
105
|
-
return toError(error?.message ?? 'analyze_page_failed');
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
const transport = new StdioServerTransport();
|
|
111
|
-
await server.connect(transport);
|
|
112
|
-
console.error('[page-understanding] MCP Server started');
|