@lightcone-ai/daemon 0.10.3 → 0.12.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/agent-manager.js +41 -1
- package/src/chat-bridge.js +163 -3
- package/src/drivers/claude.js +66 -10
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
* Uses deterministic CDP operations — no AI vision required.
|
|
4
4
|
*/
|
|
5
5
|
import { formatTextWithTags } from '../text.js';
|
|
6
|
+
import { PublisherAdapter } from './publisher-adapter.js';
|
|
6
7
|
|
|
7
8
|
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
8
9
|
function randomInt(min, max) { return Math.floor(min + Math.random() * (max - min + 1)); }
|
|
9
10
|
|
|
10
11
|
const PACE_SCALE = Math.max(0.2, Number(process.env.PUBLISHER_PACE_SCALE ?? '1') || 1);
|
|
11
12
|
const DRY_RUN = process.env.XHS_PUBLISH_DRY_RUN !== '0';
|
|
13
|
+
const TITLE_MIN_LENGTH = 2;
|
|
14
|
+
const TITLE_MAX_LENGTH = 20;
|
|
12
15
|
async function humanPause(minMs, maxMs, label = '') {
|
|
13
16
|
const ms = Math.round(randomInt(minMs, maxMs) * PACE_SCALE);
|
|
14
17
|
if (label) console.error(`[XhsAdapter] pause ${label}: ${ms}ms`);
|
|
@@ -17,22 +20,74 @@ async function humanPause(minMs, maxMs, label = '') {
|
|
|
17
20
|
|
|
18
21
|
const REQUIREMENTS = {
|
|
19
22
|
image_text: {
|
|
23
|
+
platform: 'xhs',
|
|
24
|
+
content_type: 'image_text',
|
|
20
25
|
max_text_length: 1000,
|
|
21
26
|
max_images: 9,
|
|
22
27
|
image_formats: ['jpg', 'jpeg', 'png', 'webp'],
|
|
23
|
-
required_fields: ['text', 'images'],
|
|
24
|
-
|
|
28
|
+
required_fields: ['title', 'text', 'images'],
|
|
29
|
+
optional_fields: ['tags'],
|
|
30
|
+
notes: [
|
|
31
|
+
'标题需 2-20 字',
|
|
32
|
+
'必须提供至少 1 张本地图片;图片建议 3:4 竖版',
|
|
33
|
+
'话题标签以 tags 数组传入,系统会自动拼接到正文',
|
|
34
|
+
'广告/合作内容必须带平台广告标识;命中策略会在 precheck 阻断',
|
|
35
|
+
],
|
|
25
36
|
},
|
|
26
37
|
short_video: {
|
|
38
|
+
platform: 'xhs',
|
|
39
|
+
content_type: 'short_video',
|
|
27
40
|
max_text_length: 1000,
|
|
28
|
-
|
|
29
|
-
video_max_duration: 600,
|
|
41
|
+
video_max_duration_sec: 600,
|
|
30
42
|
video_formats: ['mp4', 'mov'],
|
|
31
|
-
required_fields: ['text', 'video'],
|
|
32
|
-
|
|
43
|
+
required_fields: ['title', 'text', 'video'],
|
|
44
|
+
optional_fields: ['tags', 'cover'],
|
|
45
|
+
notes: [
|
|
46
|
+
'标题需 2-20 字',
|
|
47
|
+
'视频时长不超过 10 分钟',
|
|
48
|
+
'封面图建议 3:4;当前封面上传依赖页面能力',
|
|
49
|
+
'广告/合作内容必须带平台广告标识;命中策略会在 precheck 阻断',
|
|
50
|
+
],
|
|
33
51
|
},
|
|
34
52
|
};
|
|
35
53
|
|
|
54
|
+
const XHS_CAPABILITIES = Object.freeze({
|
|
55
|
+
platform: 'xhs',
|
|
56
|
+
display_name: '小红书',
|
|
57
|
+
spec_version: '0.1.0',
|
|
58
|
+
supportedDrivers: ['cdp'],
|
|
59
|
+
supportedContentTypes: ['image_text', 'short_video'],
|
|
60
|
+
max_image: 9,
|
|
61
|
+
max_duration: 600,
|
|
62
|
+
short_video_max_duration: 600,
|
|
63
|
+
image_formats: ['jpg', 'jpeg', 'png', 'webp'],
|
|
64
|
+
video_formats: ['mp4', 'mov'],
|
|
65
|
+
ai_label_required: true,
|
|
66
|
+
ad_disclosure_required: true,
|
|
67
|
+
sensitive_word_db_ref: {
|
|
68
|
+
skill: 'platform-policy-db',
|
|
69
|
+
platform: 'xhs',
|
|
70
|
+
version: 'xhs-2026.04',
|
|
71
|
+
},
|
|
72
|
+
cover_required_for_short_video: false,
|
|
73
|
+
cover_required_for_long_video: false,
|
|
74
|
+
dry_run_supported: true,
|
|
75
|
+
schedule_supported: false,
|
|
76
|
+
default_publish_rate: {
|
|
77
|
+
per_account_per_day_max: 4,
|
|
78
|
+
notes: '建议保持保守发布频率,避免触发平台风控',
|
|
79
|
+
},
|
|
80
|
+
platform_extras: {
|
|
81
|
+
platform: 'xhs',
|
|
82
|
+
topics_max: 10,
|
|
83
|
+
hashtag_max: 10,
|
|
84
|
+
poi_supported: true,
|
|
85
|
+
brand_partnership_required_for_ads: true,
|
|
86
|
+
cover_aspect_ratios: ['3:4', '1:1'],
|
|
87
|
+
},
|
|
88
|
+
evidence: 'verified',
|
|
89
|
+
});
|
|
90
|
+
|
|
36
91
|
const PUBLISH_URL = 'https://creator.xiaohongshu.com/publish/publish';
|
|
37
92
|
const IMAGE_PUBLISH_URL = 'https://creator.xiaohongshu.com/publish/publish?target=image';
|
|
38
93
|
const VIDEO_PUBLISH_URL = 'https://creator.xiaohongshu.com/publish/publish?source=video';
|
|
@@ -55,9 +110,13 @@ const ERROR_SELECTORS = [
|
|
|
55
110
|
'[role="alert"]',
|
|
56
111
|
];
|
|
57
112
|
|
|
58
|
-
export class XhsAdapter {
|
|
113
|
+
export class XhsAdapter extends PublisherAdapter {
|
|
59
114
|
constructor(cdp) {
|
|
60
|
-
|
|
115
|
+
super(cdp);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
getCapabilities() {
|
|
119
|
+
return XHS_CAPABILITIES;
|
|
61
120
|
}
|
|
62
121
|
|
|
63
122
|
getRequirements(contentType) {
|
|
@@ -74,10 +133,23 @@ export class XhsAdapter {
|
|
|
74
133
|
const url = state.url;
|
|
75
134
|
const loggedIn = state.isCreatorLoggedIn && !state.hasLoginHint;
|
|
76
135
|
console.error(`[XhsAdapter] checkLoginStatus: loggedIn=${loggedIn} url=${url}`);
|
|
77
|
-
return {
|
|
136
|
+
return {
|
|
137
|
+
loggedIn,
|
|
138
|
+
url,
|
|
139
|
+
profileDir,
|
|
140
|
+
userId: null,
|
|
141
|
+
nickname: null,
|
|
142
|
+
authMode: 'cdp',
|
|
143
|
+
lastVerifiedAt: new Date().toISOString(),
|
|
144
|
+
};
|
|
78
145
|
}
|
|
79
146
|
|
|
80
147
|
async publishImageText({ title, text, tags = [], images = [] }) {
|
|
148
|
+
const normalizedTitle = this._normalizeAndValidateTitle(title);
|
|
149
|
+
if (images.length > XHS_CAPABILITIES.max_image) {
|
|
150
|
+
throw new Error(`INPUT_MEDIA_REJECTED: 小红书图文最多 ${XHS_CAPABILITIES.max_image} 张图`);
|
|
151
|
+
}
|
|
152
|
+
|
|
81
153
|
await this._openPublishComposer('image');
|
|
82
154
|
await this._assertReadyForPublish();
|
|
83
155
|
await humanPause(2500, 5500, 'composer-ready');
|
|
@@ -93,10 +165,8 @@ export class XhsAdapter {
|
|
|
93
165
|
}
|
|
94
166
|
|
|
95
167
|
// Fill title
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
await humanPause(1200, 2800, 'after-title');
|
|
99
|
-
}
|
|
168
|
+
await this._fillField(TITLE_SELECTOR, normalizedTitle);
|
|
169
|
+
await humanPause(1200, 2800, 'after-title');
|
|
100
170
|
|
|
101
171
|
// Fill content text and only append tags that are not already present.
|
|
102
172
|
const fullText = formatTextWithTags(text, tags);
|
|
@@ -119,6 +189,8 @@ export class XhsAdapter {
|
|
|
119
189
|
}
|
|
120
190
|
|
|
121
191
|
async publishShortVideo({ title, text, tags = [], video, cover }) {
|
|
192
|
+
const normalizedTitle = this._normalizeAndValidateTitle(title);
|
|
193
|
+
|
|
122
194
|
await this._openPublishComposer('video');
|
|
123
195
|
await this._assertReadyForPublish();
|
|
124
196
|
await humanPause(2500, 5500, 'composer-ready');
|
|
@@ -138,10 +210,8 @@ export class XhsAdapter {
|
|
|
138
210
|
await humanPause(1800, 4200, 'after-cover-upload');
|
|
139
211
|
}
|
|
140
212
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
await humanPause(1200, 2800, 'after-title');
|
|
144
|
-
}
|
|
213
|
+
await this._fillField(TITLE_SELECTOR, normalizedTitle);
|
|
214
|
+
await humanPause(1200, 2800, 'after-title');
|
|
145
215
|
|
|
146
216
|
const fullText = formatTextWithTags(text, tags);
|
|
147
217
|
await this._fillField(CONTENT_SELECTOR, fullText);
|
|
@@ -162,6 +232,22 @@ export class XhsAdapter {
|
|
|
162
232
|
return { success: true, post_url: result.postUrl || result.url };
|
|
163
233
|
}
|
|
164
234
|
|
|
235
|
+
async withdrawPost() {
|
|
236
|
+
throw new Error('PUBLISH_UNSUPPORTED_FEATURE: 小红书当前自动化未提供撤稿能力');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
_normalizeAndValidateTitle(title) {
|
|
240
|
+
const normalized = String(title ?? '').trim();
|
|
241
|
+
if (!normalized) {
|
|
242
|
+
throw new Error(`INPUT_REQUIRED_FIELD_MISSING: 小红书标题不能为空(${TITLE_MIN_LENGTH}-${TITLE_MAX_LENGTH} 字)`);
|
|
243
|
+
}
|
|
244
|
+
const length = [...normalized].length;
|
|
245
|
+
if (length < TITLE_MIN_LENGTH || length > TITLE_MAX_LENGTH) {
|
|
246
|
+
throw new Error(`INPUT_TEXT_TOO_LONG: 小红书标题需 ${TITLE_MIN_LENGTH}-${TITLE_MAX_LENGTH} 字,当前 ${length} 字`);
|
|
247
|
+
}
|
|
248
|
+
return normalized;
|
|
249
|
+
}
|
|
250
|
+
|
|
165
251
|
// ── CDP helpers ─────────────────────────────────────────────────────────────
|
|
166
252
|
|
|
167
253
|
async _openPublishComposer(kind) {
|
|
@@ -18,6 +18,8 @@ import { getSession, closeSession } from './chrome-pool.js';
|
|
|
18
18
|
import { XhsAdapter } from './adapters/xhs.js';
|
|
19
19
|
import { DouyinAdapter } from './adapters/douyin.js';
|
|
20
20
|
import { KuaishouAdapter } from './adapters/kuaishou.js';
|
|
21
|
+
import { callOfficialTool } from './official-tool-client.js';
|
|
22
|
+
import { runPublishPrecheck } from './precheck.js';
|
|
21
23
|
import { withProfileLock } from '../../src/profile-lock.js';
|
|
22
24
|
|
|
23
25
|
const SERVER_URL = process.env.SERVER_URL ?? '';
|
|
@@ -41,24 +43,40 @@ const PLATFORM_LABELS = {
|
|
|
41
43
|
kuaishou: '快手',
|
|
42
44
|
};
|
|
43
45
|
|
|
46
|
+
const ADAPTER_REGISTRY = Object.freeze({
|
|
47
|
+
xhs: XhsAdapter,
|
|
48
|
+
douyin: DouyinAdapter,
|
|
49
|
+
kuaishou: KuaishouAdapter,
|
|
50
|
+
});
|
|
51
|
+
|
|
44
52
|
function getProfileDir(platform) {
|
|
45
53
|
const key = PLATFORM_ENV_KEYS[platform];
|
|
46
54
|
if (!key) return null;
|
|
47
55
|
return process.env[key] ?? null;
|
|
48
56
|
}
|
|
49
57
|
|
|
58
|
+
function getAdapterClass(platform) {
|
|
59
|
+
const AdapterClass = ADAPTER_REGISTRY[platform];
|
|
60
|
+
if (!AdapterClass) throw new Error(`Unknown platform: ${platform}`);
|
|
61
|
+
return AdapterClass;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function createAdapter(platform, cdp) {
|
|
65
|
+
const AdapterClass = getAdapterClass(platform);
|
|
66
|
+
return new AdapterClass(cdp);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function createStaticAdapter(platform) {
|
|
70
|
+
return createAdapter(platform, null);
|
|
71
|
+
}
|
|
72
|
+
|
|
50
73
|
async function getAdapter(platform) {
|
|
51
74
|
const profileDir = getProfileDir(platform);
|
|
52
75
|
if (!profileDir) {
|
|
53
76
|
throw new Error(`No profile dir for platform="${platform}". Has the user logged in and authorized this agent?`);
|
|
54
77
|
}
|
|
55
78
|
const cdp = await getSession(platform, profileDir);
|
|
56
|
-
|
|
57
|
-
case 'xhs': return new XhsAdapter(cdp);
|
|
58
|
-
case 'douyin': return new DouyinAdapter(cdp);
|
|
59
|
-
case 'kuaishou': return new KuaishouAdapter(cdp);
|
|
60
|
-
default: throw new Error(`Unknown platform: ${platform}`);
|
|
61
|
-
}
|
|
79
|
+
return createAdapter(platform, cdp);
|
|
62
80
|
}
|
|
63
81
|
|
|
64
82
|
async function withPublisherProfile(platform, fn) {
|
|
@@ -111,7 +129,7 @@ async function completeApproval(actionId, ok, result, error) {
|
|
|
111
129
|
}
|
|
112
130
|
}
|
|
113
131
|
|
|
114
|
-
const
|
|
132
|
+
const DEFAULT_MEDIA_LIMITS = {
|
|
115
133
|
xhs: { maxImages: 9, imageExts: ['.jpg', '.jpeg', '.png', '.webp'], videoExts: ['.mp4', '.mov'] },
|
|
116
134
|
douyin: { maxImages: 35, imageExts: ['.jpg', '.jpeg', '.png', '.webp'], videoExts: ['.mp4', '.mov', '.webm'] },
|
|
117
135
|
kuaishou: { maxImages: 12, imageExts: ['.jpg', '.jpeg', '.png'], videoExts: ['.mp4', '.mov'] },
|
|
@@ -218,8 +236,37 @@ function validateLocalFile(filePath, { kind, required = false, allowedExts = []
|
|
|
218
236
|
return realFile;
|
|
219
237
|
}
|
|
220
238
|
|
|
239
|
+
function normalizeFormatExts(formats = [], fallback = []) {
|
|
240
|
+
if (!Array.isArray(formats) || formats.length === 0) return fallback;
|
|
241
|
+
return formats
|
|
242
|
+
.map(item => String(item ?? '').trim().toLowerCase())
|
|
243
|
+
.filter(Boolean)
|
|
244
|
+
.map(item => (item.startsWith('.') ? item : `.${item}`));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function resolveMediaLimits(platform, contentType) {
|
|
248
|
+
const fallback = DEFAULT_MEDIA_LIMITS[platform] ?? DEFAULT_MEDIA_LIMITS.xhs;
|
|
249
|
+
const adapter = createStaticAdapter(platform);
|
|
250
|
+
const requirements = adapter.getRequirements(contentType) ?? {};
|
|
251
|
+
const capabilities = typeof adapter.getCapabilities === 'function'
|
|
252
|
+
? (adapter.getCapabilities() ?? {})
|
|
253
|
+
: {};
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
maxImages: Number(requirements.max_images ?? capabilities.max_image ?? fallback.maxImages),
|
|
257
|
+
imageExts: normalizeFormatExts(
|
|
258
|
+
requirements.image_formats ?? capabilities.image_formats,
|
|
259
|
+
fallback.imageExts
|
|
260
|
+
),
|
|
261
|
+
videoExts: normalizeFormatExts(
|
|
262
|
+
requirements.video_formats ?? capabilities.video_formats,
|
|
263
|
+
fallback.videoExts
|
|
264
|
+
),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
221
268
|
function validateMedia({ platform, contentType, images = [], video, cover }) {
|
|
222
|
-
const limits =
|
|
269
|
+
const limits = resolveMediaLimits(platform, contentType);
|
|
223
270
|
if (contentType === 'image_text') {
|
|
224
271
|
if (images.length === 0) {
|
|
225
272
|
throw new Error(`image_text requires at least 1 image on ${platform}. Provide absolute image paths inside the agent workspace artifacts directory.`);
|
|
@@ -267,28 +314,77 @@ images/video 字段填写本地绝对路径(在 agent workspace 目录下)
|
|
|
267
314
|
const label = PLATFORM_LABELS[platform] ?? platform;
|
|
268
315
|
try {
|
|
269
316
|
const approvalData = await validateApproval(approval_action_id, platform);
|
|
317
|
+
const approvalPayload = approvalData?.payload && typeof approvalData.payload === 'object' && !Array.isArray(approvalData.payload)
|
|
318
|
+
? approvalData.payload
|
|
319
|
+
: {};
|
|
320
|
+
const precheck = await runPublishPrecheck({
|
|
321
|
+
platform,
|
|
322
|
+
title,
|
|
323
|
+
text,
|
|
324
|
+
tags: tags ?? [],
|
|
325
|
+
payload: approvalPayload,
|
|
326
|
+
callTool: callOfficialTool,
|
|
327
|
+
});
|
|
328
|
+
if (!precheck.ok) {
|
|
329
|
+
const blockerSummary = precheck.blockers.map(item => `${item.code}: ${item.message}`).join(' | ');
|
|
330
|
+
throw new Error(`PUBLISH_PRECHECK_BLOCKED:${blockerSummary || 'precheck failed'}`);
|
|
331
|
+
}
|
|
332
|
+
|
|
270
333
|
const localMedia = await materializeMedia({ images: images ?? [], video, cover }, approvalData);
|
|
334
|
+
const staticAdapter = createStaticAdapter(platform);
|
|
335
|
+
const req = staticAdapter.getRequirements(content_type);
|
|
336
|
+
if (!req) throw new Error(`INPUT_UNSUPPORTED_CONTENT_TYPE: ${platform} does not support ${content_type}`);
|
|
271
337
|
const media = validateMedia({ platform, contentType: content_type, ...localMedia });
|
|
272
|
-
const
|
|
338
|
+
const { publishResult, healthCheck } = await withPublisherProfile(platform, async () => {
|
|
273
339
|
const adapter = await getAdapter(platform);
|
|
340
|
+
let publishResult = null;
|
|
274
341
|
if (content_type === 'image_text') {
|
|
275
|
-
|
|
342
|
+
publishResult = await adapter.publishImageText({ title, text, tags: tags ?? [], images: media.images });
|
|
343
|
+
} else if (content_type === 'short_video') {
|
|
344
|
+
publishResult = await adapter.publishShortVideo({ title, text, tags: tags ?? [], video: media.video, cover: media.cover });
|
|
345
|
+
} else {
|
|
346
|
+
throw new Error(`Unsupported content_type: ${content_type}`);
|
|
276
347
|
}
|
|
277
|
-
|
|
278
|
-
|
|
348
|
+
|
|
349
|
+
let healthCheck = null;
|
|
350
|
+
try {
|
|
351
|
+
healthCheck = await adapter.checkLoginStatus();
|
|
352
|
+
} catch (error) {
|
|
353
|
+
healthCheck = {
|
|
354
|
+
loggedIn: null,
|
|
355
|
+
error: error.message,
|
|
356
|
+
};
|
|
279
357
|
}
|
|
280
|
-
|
|
358
|
+
return { publishResult, healthCheck };
|
|
281
359
|
});
|
|
282
360
|
|
|
283
|
-
|
|
284
|
-
|
|
361
|
+
const completionResult = {
|
|
362
|
+
precheck: {
|
|
363
|
+
blockers: precheck.blockers,
|
|
364
|
+
warnings: precheck.warnings,
|
|
365
|
+
policy_version: precheck?.policyScan?.policy_version ?? null,
|
|
366
|
+
advisory: precheck?.advisory?.advisory ?? null,
|
|
367
|
+
},
|
|
368
|
+
publish_result: publishResult,
|
|
369
|
+
post_publish_health: healthCheck,
|
|
370
|
+
};
|
|
371
|
+
await completeApproval(approval_action_id, true, completionResult, null);
|
|
372
|
+
|
|
373
|
+
const advisoryMessage = precheck?.advisory?.advisory?.level === 'suggest_delay'
|
|
374
|
+
? `\n发布建议:${precheck.advisory.advisory.message}`
|
|
375
|
+
: '';
|
|
376
|
+
const healthMessage = healthCheck?.loggedIn === false
|
|
377
|
+
? '\n发布后巡检:账号登录状态异常,请尽快复核凭据。'
|
|
378
|
+
: '';
|
|
379
|
+
|
|
380
|
+
if (publishResult?.dry_run) {
|
|
285
381
|
return {
|
|
286
|
-
content: [{ type: 'text', text: `✓ ${label}
|
|
382
|
+
content: [{ type: 'text', text: `✓ ${label}发布流程已完成到发布前一步,已跳过最终“发布”点击。${advisoryMessage}${healthMessage}` }],
|
|
287
383
|
};
|
|
288
384
|
}
|
|
289
|
-
const postUrl =
|
|
385
|
+
const postUrl = publishResult?.post_url ? `\n发布链接: ${publishResult.post_url}` : '';
|
|
290
386
|
return {
|
|
291
|
-
content: [{ type: 'text', text: `✓ 已成功发布到${label}。${postUrl}` }],
|
|
387
|
+
content: [{ type: 'text', text: `✓ 已成功发布到${label}。${postUrl}${advisoryMessage}${healthMessage}` }],
|
|
292
388
|
};
|
|
293
389
|
} catch (err) {
|
|
294
390
|
if (err.message?.startsWith('LOGIN_EXPIRED')) {
|
|
@@ -299,6 +395,14 @@ images/video 字段填写本地绝对路径(在 agent workspace 目录下)
|
|
|
299
395
|
isError: true,
|
|
300
396
|
};
|
|
301
397
|
}
|
|
398
|
+
if (err.message?.startsWith('PUBLISH_PRECHECK_BLOCKED:')) {
|
|
399
|
+
const message = err.message.replace('PUBLISH_PRECHECK_BLOCKED:', '').trim();
|
|
400
|
+
if (approval_action_id) await completeApproval(approval_action_id, false, null, err.message);
|
|
401
|
+
return {
|
|
402
|
+
content: [{ type: 'text', text: `发布前预检未通过:${message}` }],
|
|
403
|
+
isError: true,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
302
406
|
if (approval_action_id) await completeApproval(approval_action_id, false, null, err.message);
|
|
303
407
|
return {
|
|
304
408
|
content: [{ type: 'text', text: `发布失败: ${err.message}` }],
|
|
@@ -319,13 +423,7 @@ server.tool(
|
|
|
319
423
|
},
|
|
320
424
|
async ({ platform, content_type }) => {
|
|
321
425
|
try {
|
|
322
|
-
|
|
323
|
-
switch (platform) {
|
|
324
|
-
case 'xhs': adapter = new XhsAdapter(null); break;
|
|
325
|
-
case 'douyin': adapter = new DouyinAdapter(null); break;
|
|
326
|
-
case 'kuaishou': adapter = new KuaishouAdapter(null); break;
|
|
327
|
-
default: throw new Error(`Unknown platform: ${platform}`);
|
|
328
|
-
}
|
|
426
|
+
const adapter = createStaticAdapter(platform);
|
|
329
427
|
const req = adapter.getRequirements(content_type);
|
|
330
428
|
if (!req) return { content: [{ type: 'text', text: `该平台不支持 ${content_type} 类型` }] };
|
|
331
429
|
return { content: [{ type: 'text', text: JSON.stringify(req, null, 2) }] };
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { resolveMcpServerEntrypoint } from '../../../src/mcp/registry.js';
|
|
3
|
+
|
|
4
|
+
function extractErrorText(result) {
|
|
5
|
+
const chunks = Array.isArray(result?.content) ? result.content : [];
|
|
6
|
+
const texts = chunks
|
|
7
|
+
.map(chunk => (typeof chunk?.text === 'string' ? chunk.text.trim() : ''))
|
|
8
|
+
.filter(Boolean);
|
|
9
|
+
if (texts.length > 0) return texts.join('\n');
|
|
10
|
+
return 'unknown_mcp_error';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parseJsonContent(result) {
|
|
14
|
+
const chunks = Array.isArray(result?.content) ? result.content : [];
|
|
15
|
+
const text = chunks
|
|
16
|
+
.map(chunk => (typeof chunk?.text === 'string' ? chunk.text : ''))
|
|
17
|
+
.join('\n')
|
|
18
|
+
.trim();
|
|
19
|
+
if (!text) return {};
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(text);
|
|
22
|
+
} catch {
|
|
23
|
+
return { raw_text: text };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class StdioJsonRpcClient {
|
|
28
|
+
constructor(command, args = [], env = {}) {
|
|
29
|
+
this.command = command;
|
|
30
|
+
this.args = args;
|
|
31
|
+
this.env = env;
|
|
32
|
+
this.proc = null;
|
|
33
|
+
this.stdoutBuffer = '';
|
|
34
|
+
this.nextId = 1;
|
|
35
|
+
this.pending = new Map();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async start() {
|
|
39
|
+
if (this.proc) return;
|
|
40
|
+
this.proc = spawn(this.command, this.args, {
|
|
41
|
+
cwd: process.cwd(),
|
|
42
|
+
env: {
|
|
43
|
+
...process.env,
|
|
44
|
+
...this.env,
|
|
45
|
+
},
|
|
46
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
this.proc.stdout.setEncoding('utf8');
|
|
50
|
+
this.proc.stdout.on('data', chunk => this._handleStdout(chunk));
|
|
51
|
+
this.proc.on('exit', (code) => {
|
|
52
|
+
const error = new Error(`process_exited:${code}`);
|
|
53
|
+
for (const pending of this.pending.values()) {
|
|
54
|
+
clearTimeout(pending.timer);
|
|
55
|
+
pending.reject(error);
|
|
56
|
+
}
|
|
57
|
+
this.pending.clear();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
_handleStdout(chunk) {
|
|
62
|
+
this.stdoutBuffer += chunk;
|
|
63
|
+
const lines = this.stdoutBuffer.split('\n');
|
|
64
|
+
this.stdoutBuffer = lines.pop() ?? '';
|
|
65
|
+
|
|
66
|
+
for (const line of lines) {
|
|
67
|
+
const text = line.trim();
|
|
68
|
+
if (!text) continue;
|
|
69
|
+
|
|
70
|
+
let payload;
|
|
71
|
+
try {
|
|
72
|
+
payload = JSON.parse(text);
|
|
73
|
+
} catch {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (payload?.id == null) continue;
|
|
77
|
+
|
|
78
|
+
const pending = this.pending.get(payload.id);
|
|
79
|
+
if (!pending) continue;
|
|
80
|
+
|
|
81
|
+
clearTimeout(pending.timer);
|
|
82
|
+
this.pending.delete(payload.id);
|
|
83
|
+
|
|
84
|
+
if (payload.error) {
|
|
85
|
+
pending.reject(new Error(payload.error.message ?? 'jsonrpc_error'));
|
|
86
|
+
} else {
|
|
87
|
+
pending.resolve(payload.result);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
request(method, params = {}, timeoutMs = 10000) {
|
|
93
|
+
if (!this.proc) {
|
|
94
|
+
return Promise.reject(new Error('client_not_started'));
|
|
95
|
+
}
|
|
96
|
+
const id = this.nextId++;
|
|
97
|
+
const payload = {
|
|
98
|
+
jsonrpc: '2.0',
|
|
99
|
+
id,
|
|
100
|
+
method,
|
|
101
|
+
params,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const timer = setTimeout(() => {
|
|
106
|
+
this.pending.delete(id);
|
|
107
|
+
reject(new Error(`timeout:${method}`));
|
|
108
|
+
}, timeoutMs);
|
|
109
|
+
|
|
110
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
111
|
+
this.proc.stdin.write(`${JSON.stringify(payload)}\n`);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
notify(method, params = {}) {
|
|
116
|
+
if (!this.proc) return;
|
|
117
|
+
const payload = {
|
|
118
|
+
jsonrpc: '2.0',
|
|
119
|
+
method,
|
|
120
|
+
params,
|
|
121
|
+
};
|
|
122
|
+
this.proc.stdin.write(`${JSON.stringify(payload)}\n`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async close() {
|
|
126
|
+
if (!this.proc) return;
|
|
127
|
+
if (!this.proc.killed) {
|
|
128
|
+
this.proc.kill('SIGTERM');
|
|
129
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
130
|
+
if (!this.proc.killed) this.proc.kill('SIGKILL');
|
|
131
|
+
}
|
|
132
|
+
this.proc = null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function callOfficialTool({
|
|
137
|
+
serverId,
|
|
138
|
+
toolName,
|
|
139
|
+
argumentsPayload = {},
|
|
140
|
+
timeoutMs = 10000,
|
|
141
|
+
}) {
|
|
142
|
+
const entrypointPath = resolveMcpServerEntrypoint(serverId, { strict: true });
|
|
143
|
+
if (!entrypointPath) {
|
|
144
|
+
throw new Error(`official_server_not_found:${serverId}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const client = new StdioJsonRpcClient(process.execPath, [entrypointPath]);
|
|
148
|
+
try {
|
|
149
|
+
await client.start();
|
|
150
|
+
await client.request('initialize', {
|
|
151
|
+
protocolVersion: '2024-11-05',
|
|
152
|
+
capabilities: {},
|
|
153
|
+
clientInfo: {
|
|
154
|
+
name: 'publisher-official-tool-client',
|
|
155
|
+
version: '0.1.0',
|
|
156
|
+
},
|
|
157
|
+
}, timeoutMs);
|
|
158
|
+
client.notify('notifications/initialized', {});
|
|
159
|
+
|
|
160
|
+
const result = await client.request('tools/call', {
|
|
161
|
+
name: toolName,
|
|
162
|
+
arguments: argumentsPayload ?? {},
|
|
163
|
+
}, timeoutMs);
|
|
164
|
+
if (result?.isError === true) {
|
|
165
|
+
throw new Error(extractErrorText(result));
|
|
166
|
+
}
|
|
167
|
+
return parseJsonContent(result);
|
|
168
|
+
} finally {
|
|
169
|
+
await client.close();
|
|
170
|
+
}
|
|
171
|
+
}
|