@lightcone-ai/daemon 0.15.2 → 0.15.3
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/publisher/adapters/douyin.js +167 -46
- package/package.json +1 -1
- package/src/agent-manager.js +155 -20
- package/src/drivers/claude.js +7 -3
- package/src/publish-job-runner.js +108 -50
|
@@ -8,7 +8,6 @@ function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
|
8
8
|
function randomInt(min, max) { return Math.floor(min + Math.random() * (max - min + 1)); }
|
|
9
9
|
|
|
10
10
|
const PACE_SCALE = Math.max(0.2, Number(process.env.PUBLISHER_PACE_SCALE ?? '1') || 1);
|
|
11
|
-
const DRY_RUN = process.env.DOUYIN_PUBLISH_DRY_RUN !== '0';
|
|
12
11
|
|
|
13
12
|
async function humanPause(minMs, maxMs, label = '') {
|
|
14
13
|
const ms = Math.round(randomInt(minMs, maxMs) * PACE_SCALE);
|
|
@@ -22,14 +21,14 @@ const REQUIREMENTS = {
|
|
|
22
21
|
max_images: 35,
|
|
23
22
|
image_formats: ['jpg', 'jpeg', 'png', 'webp'],
|
|
24
23
|
required_fields: ['text', 'images'],
|
|
25
|
-
notes: '图文模式最多 35 张图;正文最多 2200
|
|
24
|
+
notes: '图文模式最多 35 张图;正文最多 2200 字;自动化会填写后点击暂存,不会直接发布',
|
|
26
25
|
},
|
|
27
26
|
short_video: {
|
|
28
27
|
max_text_length: 2200,
|
|
29
28
|
video_max_duration: 900,
|
|
30
29
|
video_formats: ['mp4', 'mov', 'webm'],
|
|
31
30
|
required_fields: ['text', 'video'],
|
|
32
|
-
notes: '视频最长 15 分钟;建议 9:16
|
|
31
|
+
notes: '视频最长 15 分钟;建议 9:16 竖版;自动化会填写后点击暂存,不会直接发布',
|
|
33
32
|
},
|
|
34
33
|
};
|
|
35
34
|
|
|
@@ -70,6 +69,14 @@ const CONTENT_SELECTOR = [
|
|
|
70
69
|
'.ProseMirror[contenteditable="true"]',
|
|
71
70
|
'[contenteditable="true"]',
|
|
72
71
|
].join(', ');
|
|
72
|
+
const CONTENT_EDITOR_SELECTOR = [
|
|
73
|
+
'textarea[placeholder*="描述"]',
|
|
74
|
+
'textarea[placeholder*="作品描述"]',
|
|
75
|
+
'[placeholder*="添加作品描述"]',
|
|
76
|
+
'[contenteditable="true"][data-placeholder*="描述"]',
|
|
77
|
+
'[contenteditable="true"][aria-label*="描述"]',
|
|
78
|
+
'.ProseMirror[contenteditable="true"]',
|
|
79
|
+
].join(', ');
|
|
73
80
|
|
|
74
81
|
const ERROR_SELECTORS = [
|
|
75
82
|
'[class*="error"]',
|
|
@@ -125,18 +132,9 @@ export class DouyinAdapter {
|
|
|
125
132
|
|
|
126
133
|
await humanPause(4500, 9000, 'before-publish-check');
|
|
127
134
|
await this._assertNoBlockingErrors();
|
|
128
|
-
await this.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
console.error('[DouyinAdapter] DOUYIN_PUBLISH_DRY_RUN enabled; skipping final publish click.');
|
|
132
|
-
return { success: true, dry_run: true, post_url: null };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
await humanPause(1200, 3000, 'before-publish-click');
|
|
136
|
-
const clicked = await this._clickByText('发布');
|
|
137
|
-
if (!clicked) throw new Error('PUBLISH_FAILED: 找不到发布按钮,页面结构可能已变化');
|
|
138
|
-
const result = await this._waitForPublishResult(120_000);
|
|
139
|
-
return { success: true, post_url: result.postUrl || result.url };
|
|
135
|
+
await this._assertDraftButtonReady();
|
|
136
|
+
const draftResult = await this._saveAsDraft();
|
|
137
|
+
return { success: true, draft_saved: true, draft_url: draftResult.url, post_url: null };
|
|
140
138
|
}
|
|
141
139
|
|
|
142
140
|
async publishShortVideo({ title, text, tags = [], video, cover }) {
|
|
@@ -163,18 +161,9 @@ export class DouyinAdapter {
|
|
|
163
161
|
|
|
164
162
|
await humanPause(4500, 9000, 'before-publish-check');
|
|
165
163
|
await this._assertNoBlockingErrors();
|
|
166
|
-
await this.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
console.error('[DouyinAdapter] DOUYIN_PUBLISH_DRY_RUN enabled; skipping final publish click.');
|
|
170
|
-
return { success: true, dry_run: true, post_url: null };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
await humanPause(1200, 3000, 'before-publish-click');
|
|
174
|
-
const clicked = await this._clickByText('发布');
|
|
175
|
-
if (!clicked) throw new Error('PUBLISH_FAILED: 找不到发布按钮,页面结构可能已变化');
|
|
176
|
-
const result = await this._waitForPublishResult(120_000);
|
|
177
|
-
return { success: true, post_url: result.postUrl || result.url };
|
|
164
|
+
await this._assertDraftButtonReady();
|
|
165
|
+
const draftResult = await this._saveAsDraft();
|
|
166
|
+
return { success: true, draft_saved: true, draft_url: draftResult.url, post_url: null };
|
|
178
167
|
}
|
|
179
168
|
|
|
180
169
|
async _openPublishComposer(kind) {
|
|
@@ -325,7 +314,7 @@ export class DouyinAdapter {
|
|
|
325
314
|
hasLoginHint: /登录|扫码|验证码|手机号/.test(text) && !/发布|作品管理|创作服务平台/.test(text),
|
|
326
315
|
isCreatorLoggedIn: /创作服务平台|创作者中心|发布作品|作品管理|内容管理/.test(text),
|
|
327
316
|
hasUploader: !!document.querySelector(${JSON.stringify(FILE_INPUT_SELECTOR)}),
|
|
328
|
-
hasPublishEditor: !!document.querySelector(${JSON.stringify(`${TITLE_SELECTOR}, ${
|
|
317
|
+
hasPublishEditor: !!document.querySelector(${JSON.stringify(`${TITLE_SELECTOR}, ${CONTENT_EDITOR_SELECTOR}`)}) && /发布|作品描述|标题|暂存/.test(text),
|
|
329
318
|
postUrl: document.querySelector('a[href*="/video/"]')?.href || document.querySelector('a[href*="/note/"]')?.href || '',
|
|
330
319
|
};
|
|
331
320
|
})()
|
|
@@ -366,34 +355,82 @@ export class DouyinAdapter {
|
|
|
366
355
|
if (blocking) throw new Error(`PUBLISH_BLOCKED: ${blocking}`);
|
|
367
356
|
}
|
|
368
357
|
|
|
369
|
-
async
|
|
358
|
+
async _assertDraftButtonReady() {
|
|
370
359
|
const state = await this._inspectPage();
|
|
371
|
-
const button = (state.buttons ?? []).find(b =>
|
|
372
|
-
|
|
360
|
+
const button = (state.buttons ?? []).find(b =>
|
|
361
|
+
b.text === '暂存'
|
|
362
|
+
|| b.text?.includes('暂存')
|
|
363
|
+
|| b.text?.includes('保存草稿')
|
|
364
|
+
);
|
|
365
|
+
if (!button) throw new Error('PUBLISH_FAILED: 找不到暂存按钮,页面结构可能已变化');
|
|
373
366
|
if (button.disabled) {
|
|
374
|
-
const errors = (state.errors ?? []).join(';') || '
|
|
367
|
+
const errors = (state.errors ?? []).join(';') || '暂存按钮不可点击,可能仍在上传、必填项缺失或账号状态受限';
|
|
375
368
|
throw new Error(`PUBLISH_BLOCKED: ${errors}`);
|
|
376
369
|
}
|
|
377
370
|
}
|
|
378
371
|
|
|
372
|
+
async _saveAsDraft() {
|
|
373
|
+
await humanPause(1200, 3000, 'before-save-draft-click');
|
|
374
|
+
const labels = ['暂存', '暂存离开', '保存草稿'];
|
|
375
|
+
for (const label of labels) {
|
|
376
|
+
const clicked = await this._clickByText(label);
|
|
377
|
+
if (!clicked) continue;
|
|
378
|
+
return this._waitForDraftSaved(60_000);
|
|
379
|
+
}
|
|
380
|
+
throw new Error('PUBLISH_FAILED: 找不到暂存按钮,页面结构可能已变化');
|
|
381
|
+
}
|
|
382
|
+
|
|
379
383
|
async _waitForUploadSettled(expectedCount, timeoutMs) {
|
|
380
384
|
const deadline = Date.now() + timeoutMs;
|
|
385
|
+
const startedAt = Date.now();
|
|
386
|
+
let sawProgress = expectedCount === 0;
|
|
387
|
+
let stableReadyRounds = 0;
|
|
388
|
+
let pollCount = 0;
|
|
389
|
+
let lastHint = 'no_state';
|
|
390
|
+
|
|
381
391
|
while (Date.now() < deadline) {
|
|
382
392
|
await sleep(1000);
|
|
383
|
-
|
|
393
|
+
pollCount += 1;
|
|
394
|
+
|
|
384
395
|
const state = await this._inspectPage();
|
|
396
|
+
const failureHint = this._extractUploadFailureHint(state);
|
|
397
|
+
if (failureHint) throw new Error(`UPLOAD_FAILED: ${failureHint}`);
|
|
398
|
+
|
|
385
399
|
const text = state.text ?? '';
|
|
386
|
-
|
|
387
|
-
|
|
400
|
+
const processing = /上传中|处理中|转码中|正在上传|解析中|合成中|压缩中|扫描中|校验中/.test(text);
|
|
401
|
+
if (processing) sawProgress = true;
|
|
402
|
+
|
|
403
|
+
const composer = await this._probeComposerEditability();
|
|
404
|
+
const editorReady = composer.contentReady;
|
|
405
|
+
const draftReady = composer.draftButtonExists && composer.draftButtonDisabled !== true;
|
|
406
|
+
const uploadedHint = /上传成功|重新上传|更换|替换|已上传|继续编辑|移除/.test(text);
|
|
407
|
+
const completionSignal = sawProgress || uploadedHint || composer.mediaActionVisible;
|
|
408
|
+
|
|
409
|
+
lastHint = this._formatUploadWaitHint(state, composer, {
|
|
410
|
+
processing,
|
|
411
|
+
sawProgress,
|
|
412
|
+
completionSignal,
|
|
413
|
+
stableReadyRounds,
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
if (!processing && editorReady && draftReady && completionSignal) {
|
|
417
|
+
stableReadyRounds += 1;
|
|
418
|
+
if (stableReadyRounds >= 2) return;
|
|
419
|
+
} else if (!processing && editorReady && draftReady && (Date.now() - startedAt) >= 25_000) {
|
|
420
|
+
stableReadyRounds += 1;
|
|
421
|
+
if (stableReadyRounds >= 4) return;
|
|
422
|
+
} else {
|
|
423
|
+
stableReadyRounds = 0;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (pollCount % 10 === 0) {
|
|
427
|
+
console.error(`[DouyinAdapter] wait-upload poll=${pollCount} ${lastHint}`);
|
|
388
428
|
}
|
|
389
|
-
if (/上传中|处理中|转码中|正在上传|解析中|合成中/.test(text)) continue;
|
|
390
|
-
if (state.hasPublishEditor || /上传成功|重新上传|更换|已上传|封面|发布作品/.test(text)) return;
|
|
391
|
-
if (expectedCount === 0) return;
|
|
392
429
|
}
|
|
393
|
-
throw new Error(
|
|
430
|
+
throw new Error(`UPLOAD_TIMEOUT: 等待抖音上传完成超时;${lastHint}`);
|
|
394
431
|
}
|
|
395
432
|
|
|
396
|
-
async
|
|
433
|
+
async _waitForDraftSaved(timeoutMs) {
|
|
397
434
|
const deadline = Date.now() + timeoutMs;
|
|
398
435
|
let lastState = null;
|
|
399
436
|
while (Date.now() < deadline) {
|
|
@@ -401,15 +438,99 @@ export class DouyinAdapter {
|
|
|
401
438
|
const state = await this._inspectPage();
|
|
402
439
|
lastState = state;
|
|
403
440
|
const text = state.text ?? '';
|
|
404
|
-
if (state.
|
|
405
|
-
|
|
406
|
-
return { url: state.url, postUrl: state.postUrl || state.url };
|
|
441
|
+
if (!state.url.includes('/creator-micro/content/upload') && !state.hasLoginHint) {
|
|
442
|
+
return { url: state.url };
|
|
407
443
|
}
|
|
408
|
-
|
|
444
|
+
|
|
445
|
+
if (/暂存成功|草稿已保存|保存成功|已保存草稿/.test(text)) {
|
|
446
|
+
return { url: state.url };
|
|
447
|
+
}
|
|
448
|
+
const blocking = (state.errors ?? []).find(t => /暂存失败|保存失败|提交失败|发布失败|系统异常|网络错误/.test(t));
|
|
409
449
|
if (blocking) throw new Error(`PUBLISH_FAILED: ${blocking}`);
|
|
410
450
|
}
|
|
411
|
-
const hint = (lastState
|
|
412
|
-
throw new Error(`
|
|
451
|
+
const hint = this._formatPageHint(lastState ?? { url: await this._getUrl(), text: '', buttons: [], links: [], uploadish: [] });
|
|
452
|
+
throw new Error(`DRAFT_TIMEOUT: 点击暂存后未检测到成功提示;${hint}`);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async _probeComposerEditability() {
|
|
456
|
+
const result = await this._cdp.send('Runtime.evaluate', {
|
|
457
|
+
expression: `
|
|
458
|
+
(function() {
|
|
459
|
+
const visible = (el) => {
|
|
460
|
+
if (!el) return false;
|
|
461
|
+
const r = el.getBoundingClientRect();
|
|
462
|
+
const s = getComputedStyle(el);
|
|
463
|
+
return r.width > 0 && r.height > 0 && r.left > -1000 && s.display !== 'none' && s.visibility !== 'hidden';
|
|
464
|
+
};
|
|
465
|
+
const editable = (el) => visible(el)
|
|
466
|
+
&& !el.disabled
|
|
467
|
+
&& el.getAttribute('aria-disabled') !== 'true'
|
|
468
|
+
&& el.getAttribute('readonly') !== 'true';
|
|
469
|
+
const textOf = (el) => (el.innerText || el.textContent || el.getAttribute('aria-label') || el.getAttribute('title') || '').trim();
|
|
470
|
+
|
|
471
|
+
const titleNodes = [...document.querySelectorAll(${JSON.stringify(TITLE_SELECTOR)})].filter(editable);
|
|
472
|
+
const strictContentNodes = [...document.querySelectorAll(${JSON.stringify(CONTENT_EDITOR_SELECTOR)})].filter(editable);
|
|
473
|
+
const genericContentNodes = [...document.querySelectorAll(${JSON.stringify(CONTENT_SELECTOR)})].filter(editable);
|
|
474
|
+
|
|
475
|
+
const buttonNodes = [...document.querySelectorAll('button, [role="button"], a, div, span')]
|
|
476
|
+
.filter(visible)
|
|
477
|
+
.map(el => ({
|
|
478
|
+
text: textOf(el),
|
|
479
|
+
disabled: !!el.disabled || el.getAttribute('aria-disabled') === 'true' || (el.className?.toString?.() || '').includes('disabled'),
|
|
480
|
+
}))
|
|
481
|
+
.filter(item => item.text);
|
|
482
|
+
const draftButton = buttonNodes.find(item => /暂存|暂存离开|保存草稿/.test(item.text));
|
|
483
|
+
const publishButton = buttonNodes.find(item => /发布/.test(item.text));
|
|
484
|
+
const mediaActionVisible = buttonNodes.some(item => /重新上传|更换|替换|移除|删除/.test(item.text));
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
titleReady: titleNodes.length > 0,
|
|
488
|
+
contentReady: strictContentNodes.length > 0 || genericContentNodes.length > 0,
|
|
489
|
+
draftButtonExists: !!draftButton,
|
|
490
|
+
draftButtonDisabled: draftButton ? draftButton.disabled : null,
|
|
491
|
+
publishButtonExists: !!publishButton,
|
|
492
|
+
publishButtonDisabled: publishButton ? publishButton.disabled : null,
|
|
493
|
+
mediaActionVisible,
|
|
494
|
+
};
|
|
495
|
+
})()
|
|
496
|
+
`,
|
|
497
|
+
returnByValue: true,
|
|
498
|
+
});
|
|
499
|
+
return result.result?.value ?? {
|
|
500
|
+
titleReady: false,
|
|
501
|
+
contentReady: false,
|
|
502
|
+
draftButtonExists: false,
|
|
503
|
+
draftButtonDisabled: null,
|
|
504
|
+
publishButtonExists: false,
|
|
505
|
+
publishButtonDisabled: null,
|
|
506
|
+
mediaActionVisible: false,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
_extractUploadFailureHint(state) {
|
|
511
|
+
const texts = [
|
|
512
|
+
...(state.errors ?? []),
|
|
513
|
+
state.text ?? '',
|
|
514
|
+
];
|
|
515
|
+
const fatal = texts.find(item => /上传失败|上传出错|转码失败|格式不支持|文件过大|上传异常|上传中断|网络异常/.test(item));
|
|
516
|
+
if (!fatal) return null;
|
|
517
|
+
return String(fatal).replace(/\s+/g, ' ').trim().slice(0, 180);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
_formatUploadWaitHint(state, composer, flags = {}) {
|
|
521
|
+
const errors = (state.errors ?? []).join(';') || 'none';
|
|
522
|
+
return [
|
|
523
|
+
`url=${state.url}`,
|
|
524
|
+
`processing=${flags.processing === true}`,
|
|
525
|
+
`sawProgress=${flags.sawProgress === true}`,
|
|
526
|
+
`completionSignal=${flags.completionSignal === true}`,
|
|
527
|
+
`titleReady=${composer.titleReady === true}`,
|
|
528
|
+
`contentReady=${composer.contentReady === true}`,
|
|
529
|
+
`draftButton=${composer.draftButtonExists === true}`,
|
|
530
|
+
`draftDisabled=${composer.draftButtonDisabled === true}`,
|
|
531
|
+
`stableReadyRounds=${flags.stableReadyRounds ?? 0}`,
|
|
532
|
+
`errors=${errors.slice(0, 180)}`,
|
|
533
|
+
].join('; ');
|
|
413
534
|
}
|
|
414
535
|
|
|
415
536
|
async _fillField(selector, value, kind = 'content') {
|
package/package.json
CHANGED
package/src/agent-manager.js
CHANGED
|
@@ -124,6 +124,24 @@ function normalizeObject(value) {
|
|
|
124
124
|
return isPlainObject(value) ? value : {};
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
function normalizePublishStage(value, fallback = 'unknown') {
|
|
128
|
+
const normalized = String(value ?? '').trim().toLowerCase();
|
|
129
|
+
if (!normalized) return fallback;
|
|
130
|
+
return normalized.replace(/[^a-z0-9:_-]/g, '_');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function buildPublishJobFailureResult(error, fallbackStage = 'unknown') {
|
|
134
|
+
const stage = normalizePublishStage(error?.publish_stage, fallbackStage);
|
|
135
|
+
const message = String(error?.publish_error ?? error?.message ?? error ?? 'unknown_error');
|
|
136
|
+
const context = isPlainObject(error?.publish_context) ? error.publish_context : {};
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
stage,
|
|
140
|
+
error: message,
|
|
141
|
+
context,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
127
145
|
function replacePlaceholders(text, replacements) {
|
|
128
146
|
if (typeof text !== 'string') return text;
|
|
129
147
|
let next = text;
|
|
@@ -330,6 +348,79 @@ export class AgentManager {
|
|
|
330
348
|
return 'system';
|
|
331
349
|
}
|
|
332
350
|
|
|
351
|
+
_normalizeToolName(name) {
|
|
352
|
+
return String(name ?? '').trim().toLowerCase();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
_isSendMessageToolName(name) {
|
|
356
|
+
const normalized = this._normalizeToolName(name);
|
|
357
|
+
return normalized === 'send_message'
|
|
358
|
+
|| normalized.endsWith('__send_message')
|
|
359
|
+
|| normalized.endsWith('.send_message');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
_trackVisibleAssistantText(agent, text) {
|
|
363
|
+
if (!agent) return;
|
|
364
|
+
const snippet = String(text ?? '').trim();
|
|
365
|
+
if (!snippet) return;
|
|
366
|
+
agent.visibleTextSeen = true;
|
|
367
|
+
const previous = typeof agent.visibleTextBuffer === 'string' ? agent.visibleTextBuffer : '';
|
|
368
|
+
const next = previous ? `${previous}\n${snippet}` : snippet;
|
|
369
|
+
// Keep a bounded buffer for fallback delivery.
|
|
370
|
+
agent.visibleTextBuffer = next.length > 6000 ? next.slice(-6000) : next;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
_markSendMessageToolUsed(agent) {
|
|
374
|
+
if (!agent) return;
|
|
375
|
+
agent.sendMessageUsed = true;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
_resetVisibleReplyTracking(agent) {
|
|
379
|
+
if (!agent) return;
|
|
380
|
+
agent.visibleTextSeen = false;
|
|
381
|
+
agent.sendMessageUsed = false;
|
|
382
|
+
agent.visibleTextBuffer = '';
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
_rememberInboundMessage(agent, message) {
|
|
386
|
+
if (!agent || !message || typeof message !== 'object') return;
|
|
387
|
+
agent.lastInboundMessage = message;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async _recoverVisibleTextAsMessage({ agentId, workspaceId, message, content }) {
|
|
391
|
+
const text = String(content ?? '').trim();
|
|
392
|
+
if (!text || !message) return false;
|
|
393
|
+
const target = this._formatDeliveryTarget(message);
|
|
394
|
+
if (!target || target.includes('unknown')) return false;
|
|
395
|
+
const url = `${this.serverUrl}/internal/${encodeURIComponent(agentId)}/send`;
|
|
396
|
+
try {
|
|
397
|
+
const res = await fetch(url, {
|
|
398
|
+
method: 'POST',
|
|
399
|
+
headers: {
|
|
400
|
+
'Content-Type': 'application/json',
|
|
401
|
+
'Authorization': `Bearer ${this.machineApiKey}`,
|
|
402
|
+
},
|
|
403
|
+
body: JSON.stringify({ target, content: text }),
|
|
404
|
+
});
|
|
405
|
+
if (!res.ok) {
|
|
406
|
+
const detail = (await res.text()).slice(0, 300);
|
|
407
|
+
console.warn(
|
|
408
|
+
`[AgentManager][${agentId}][claude] fallback_send_message failed workspace=${workspaceId ?? 'none'} status=${res.status} detail=${detail}`
|
|
409
|
+
);
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
console.warn(
|
|
413
|
+
`[AgentManager][${agentId}][claude] fallback_send_message sent workspace=${workspaceId ?? 'none'} target=${target}`
|
|
414
|
+
);
|
|
415
|
+
return true;
|
|
416
|
+
} catch (err) {
|
|
417
|
+
console.warn(
|
|
418
|
+
`[AgentManager][${agentId}][claude] fallback_send_message error workspace=${workspaceId ?? 'none'} err=${err?.message ?? 'unknown'}`
|
|
419
|
+
);
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
333
424
|
_formatDeliveryHeaderValue(value) {
|
|
334
425
|
const normalized = String(value ?? '').trim();
|
|
335
426
|
if (!normalized) return 'unknown';
|
|
@@ -1123,6 +1214,10 @@ export class AgentManager {
|
|
|
1123
1214
|
directive,
|
|
1124
1215
|
requiredCredentials,
|
|
1125
1216
|
lastStartupMessage: startupMsg?.message ?? null,
|
|
1217
|
+
lastInboundMessage: startupMsg?.message ?? null,
|
|
1218
|
+
visibleTextSeen: false,
|
|
1219
|
+
sendMessageUsed: false,
|
|
1220
|
+
visibleTextBuffer: '',
|
|
1126
1221
|
stopCause: null,
|
|
1127
1222
|
});
|
|
1128
1223
|
this.starting.delete(key);
|
|
@@ -1172,9 +1267,13 @@ export class AgentManager {
|
|
|
1172
1267
|
runtime: 'codex',
|
|
1173
1268
|
codexVisibleTextSeen: false,
|
|
1174
1269
|
codexSendMessageUsed: false,
|
|
1270
|
+
visibleTextSeen: false,
|
|
1271
|
+
sendMessageUsed: false,
|
|
1272
|
+
visibleTextBuffer: '',
|
|
1175
1273
|
directive,
|
|
1176
1274
|
requiredCredentials,
|
|
1177
1275
|
lastStartupMessage: startupMsg?.message ?? null,
|
|
1276
|
+
lastInboundMessage: startupMsg?.message ?? null,
|
|
1178
1277
|
stopCause: null,
|
|
1179
1278
|
});
|
|
1180
1279
|
this.starting.delete(key);
|
|
@@ -1197,9 +1296,13 @@ export class AgentManager {
|
|
|
1197
1296
|
sessionId: config.sessionId ?? null,
|
|
1198
1297
|
proc,
|
|
1199
1298
|
runtime: 'claude',
|
|
1299
|
+
visibleTextSeen: false,
|
|
1300
|
+
sendMessageUsed: false,
|
|
1301
|
+
visibleTextBuffer: '',
|
|
1200
1302
|
directive,
|
|
1201
1303
|
requiredCredentials,
|
|
1202
1304
|
lastStartupMessage: startupMsg?.message ?? null,
|
|
1305
|
+
lastInboundMessage: startupMsg?.message ?? null,
|
|
1203
1306
|
stopCause: null,
|
|
1204
1307
|
});
|
|
1205
1308
|
this.starting.delete(key);
|
|
@@ -1500,6 +1603,12 @@ export class AgentManager {
|
|
|
1500
1603
|
}
|
|
1501
1604
|
if (stage === 'precheck_error') {
|
|
1502
1605
|
console.error(`${logPrefix} precheck-error error=${payload.error ?? 'unknown_error'}`);
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
if (stage === 'publish_action_error') {
|
|
1609
|
+
console.error(
|
|
1610
|
+
`${logPrefix} publish-action-error stage=${payload.stage ?? 'unknown'} error=${payload.error ?? 'unknown_error'}`
|
|
1611
|
+
);
|
|
1503
1612
|
}
|
|
1504
1613
|
},
|
|
1505
1614
|
});
|
|
@@ -1523,13 +1632,14 @@ export class AgentManager {
|
|
|
1523
1632
|
});
|
|
1524
1633
|
} catch (err) {
|
|
1525
1634
|
const errorMessage = err?.message ?? String(err);
|
|
1526
|
-
|
|
1635
|
+
const failureResult = buildPublishJobFailureResult(err, 'publish_job');
|
|
1636
|
+
console.error(`${logPrefix} failed stage=${failureResult.stage} error=${errorMessage}`);
|
|
1527
1637
|
try {
|
|
1528
1638
|
console.log(`${logPrefix} completion-post ok=false`);
|
|
1529
1639
|
await this._postInternalPublishJobComplete({
|
|
1530
1640
|
jobId,
|
|
1531
1641
|
ok: false,
|
|
1532
|
-
result:
|
|
1642
|
+
result: failureResult,
|
|
1533
1643
|
error: errorMessage,
|
|
1534
1644
|
});
|
|
1535
1645
|
console.log(`${logPrefix} completion-post posted ok=false`);
|
|
@@ -1546,6 +1656,7 @@ export class AgentManager {
|
|
|
1546
1656
|
status: 'failed',
|
|
1547
1657
|
completed_at: new Date().toISOString(),
|
|
1548
1658
|
error: errorMessage,
|
|
1659
|
+
result: failureResult,
|
|
1549
1660
|
});
|
|
1550
1661
|
}
|
|
1551
1662
|
}
|
|
@@ -1591,6 +1702,7 @@ export class AgentManager {
|
|
|
1591
1702
|
console.log(`[AgentManager] Queued seq=${seq} for codex next turn pending=${queueSize}`);
|
|
1592
1703
|
return;
|
|
1593
1704
|
}
|
|
1705
|
+
this._rememberInboundMessage(agent, message);
|
|
1594
1706
|
this._write(key, text);
|
|
1595
1707
|
}
|
|
1596
1708
|
|
|
@@ -1612,6 +1724,7 @@ export class AgentManager {
|
|
|
1612
1724
|
const text = this._formatDeliveryText(message);
|
|
1613
1725
|
const agent = this.agents.get(key);
|
|
1614
1726
|
console.log(`[AgentManager] Flushing queued seq=${seq} to agent ${agent?.config?.displayName ?? agentId.slice(0,8)} workspace=${message.workspace_name ?? workspaceId}`);
|
|
1727
|
+
this._rememberInboundMessage(agent, message);
|
|
1615
1728
|
this._write(key, text);
|
|
1616
1729
|
}
|
|
1617
1730
|
}
|
|
@@ -1671,11 +1784,13 @@ export class AgentManager {
|
|
|
1671
1784
|
break;
|
|
1672
1785
|
case 'text':
|
|
1673
1786
|
agent.codexVisibleTextSeen = true;
|
|
1787
|
+
this._trackVisibleAssistantText(agent, evt.text);
|
|
1674
1788
|
console.log(`[AgentManager][${agent.config?.displayName ?? agentId.slice(0, 8)}][codex] <text> ${String(evt.text ?? '').slice(0, 500)}`);
|
|
1675
1789
|
break;
|
|
1676
1790
|
case 'tool_call':
|
|
1677
|
-
if (evt.name
|
|
1791
|
+
if (this._isSendMessageToolName(evt.name)) {
|
|
1678
1792
|
agent.codexSendMessageUsed = true;
|
|
1793
|
+
this._markSendMessageToolUsed(agent);
|
|
1679
1794
|
this._recordSuccessfulOutboundMessage({
|
|
1680
1795
|
key,
|
|
1681
1796
|
agent,
|
|
@@ -1698,6 +1813,7 @@ export class AgentManager {
|
|
|
1698
1813
|
break;
|
|
1699
1814
|
case 'turn_end':
|
|
1700
1815
|
agent.kimiIdle = true;
|
|
1816
|
+
this._resetVisibleReplyTracking(agent);
|
|
1701
1817
|
this._emitLifecycle(connection, {
|
|
1702
1818
|
agentId,
|
|
1703
1819
|
workspaceId,
|
|
@@ -1747,13 +1863,15 @@ export class AgentManager {
|
|
|
1747
1863
|
break;
|
|
1748
1864
|
case 'text':
|
|
1749
1865
|
agent.codexVisibleTextSeen = true;
|
|
1866
|
+
this._trackVisibleAssistantText(agent, evt.text);
|
|
1750
1867
|
console.log(
|
|
1751
1868
|
`[AgentManager][${agent.config?.displayName ?? agentId.slice(0, 8)}][codex][text] ${String(evt.text ?? '').slice(0, 1200)}`
|
|
1752
1869
|
);
|
|
1753
1870
|
break;
|
|
1754
1871
|
case 'tool_call':
|
|
1755
|
-
if (evt.name
|
|
1872
|
+
if (this._isSendMessageToolName(evt.name)) {
|
|
1756
1873
|
agent.codexSendMessageUsed = true;
|
|
1874
|
+
this._markSendMessageToolUsed(agent);
|
|
1757
1875
|
this._recordSuccessfulOutboundMessage({
|
|
1758
1876
|
key,
|
|
1759
1877
|
agent,
|
|
@@ -1787,6 +1905,7 @@ export class AgentManager {
|
|
|
1787
1905
|
}
|
|
1788
1906
|
agent.codexVisibleTextSeen = false;
|
|
1789
1907
|
agent.codexSendMessageUsed = false;
|
|
1908
|
+
this._resetVisibleReplyTracking(agent);
|
|
1790
1909
|
this._emitLifecycle(connection, {
|
|
1791
1910
|
agentId,
|
|
1792
1911
|
workspaceId,
|
|
@@ -1813,10 +1932,10 @@ export class AgentManager {
|
|
|
1813
1932
|
let event;
|
|
1814
1933
|
try { event = JSON.parse(line); }
|
|
1815
1934
|
catch { return; }
|
|
1935
|
+
const agent = this.agents.get(key);
|
|
1816
1936
|
|
|
1817
1937
|
// Capture and sync session ID
|
|
1818
1938
|
if (event.session_id) {
|
|
1819
|
-
const agent = this.agents.get(key);
|
|
1820
1939
|
if (agent && agent.sessionId !== event.session_id) {
|
|
1821
1940
|
agent.sessionId = event.session_id;
|
|
1822
1941
|
connection.send({ type: 'agent:session', agentId, workspaceId, sessionId: event.session_id });
|
|
@@ -1824,27 +1943,26 @@ export class AgentManager {
|
|
|
1824
1943
|
}
|
|
1825
1944
|
|
|
1826
1945
|
// Activity state machine
|
|
1827
|
-
const displayName =
|
|
1946
|
+
const displayName = agent?.config?.displayName ?? agentId.slice(0, 8);
|
|
1828
1947
|
if (event.type === 'assistant') {
|
|
1829
1948
|
const content = event.message?.content ?? [];
|
|
1830
1949
|
for (const block of content) {
|
|
1831
1950
|
if (block.type === 'thinking') {
|
|
1832
1951
|
console.log(`[AgentManager][${displayName}] <thinking> ${block.thinking?.slice(0, 500)}`);
|
|
1833
1952
|
} else if (block.type === 'text') {
|
|
1953
|
+
this._trackVisibleAssistantText(agent, block.text);
|
|
1834
1954
|
console.log(`[AgentManager][${displayName}] <text> ${block.text?.slice(0, 500)}`);
|
|
1835
1955
|
} else if (block.type === 'tool_use') {
|
|
1836
1956
|
console.log(`[AgentManager][${displayName}] <tool_use> ${block.name} params=${JSON.stringify(block.input ?? {}).slice(0, 500)}`);
|
|
1837
|
-
if (block.name
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
});
|
|
1847
|
-
}
|
|
1957
|
+
if (this._isSendMessageToolName(block.name) && agent) {
|
|
1958
|
+
this._markSendMessageToolUsed(agent);
|
|
1959
|
+
this._recordSuccessfulOutboundMessage({
|
|
1960
|
+
key,
|
|
1961
|
+
agent,
|
|
1962
|
+
agentId,
|
|
1963
|
+
workspaceId,
|
|
1964
|
+
connection,
|
|
1965
|
+
});
|
|
1848
1966
|
}
|
|
1849
1967
|
this._emitLifecycle(connection, {
|
|
1850
1968
|
agentId,
|
|
@@ -1854,7 +1972,7 @@ export class AgentManager {
|
|
|
1854
1972
|
availability: 'busy',
|
|
1855
1973
|
runtimeState: 'running',
|
|
1856
1974
|
reason: `tool:${block.name ?? 'unknown_tool'}`,
|
|
1857
|
-
sessionId:
|
|
1975
|
+
sessionId: agent?.sessionId ?? null,
|
|
1858
1976
|
});
|
|
1859
1977
|
connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'working', detail: block.name, entries: [] });
|
|
1860
1978
|
}
|
|
@@ -1868,7 +1986,7 @@ export class AgentManager {
|
|
|
1868
1986
|
availability: 'busy',
|
|
1869
1987
|
runtimeState: 'running',
|
|
1870
1988
|
reason: 'thinking',
|
|
1871
|
-
sessionId:
|
|
1989
|
+
sessionId: agent?.sessionId ?? null,
|
|
1872
1990
|
});
|
|
1873
1991
|
connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'thinking', detail: '', entries: [] });
|
|
1874
1992
|
}
|
|
@@ -1888,6 +2006,23 @@ export class AgentManager {
|
|
|
1888
2006
|
console.log(`[AgentManager][${displayName}] <tool_result> id=${tr.tool_use_id ?? '?'} → ${resultStr}`);
|
|
1889
2007
|
}
|
|
1890
2008
|
} else if (event.type === 'result') {
|
|
2009
|
+
const inboundSenderType = this._normalizeDeliverySenderType(agent?.lastInboundMessage?.sender_type);
|
|
2010
|
+
if (inboundSenderType === 'user' && agent?.visibleTextSeen && !agent?.sendMessageUsed) {
|
|
2011
|
+
agent.stopCause = 'contract_violation';
|
|
2012
|
+
console.warn(
|
|
2013
|
+
`[AgentManager][${displayName}][claude] contract_violation: visible text emitted without send_message workspace=${workspaceId ?? 'none'}`
|
|
2014
|
+
);
|
|
2015
|
+
const fallbackText = String(agent.visibleTextBuffer ?? '').trim();
|
|
2016
|
+
if (fallbackText) {
|
|
2017
|
+
void this._recoverVisibleTextAsMessage({
|
|
2018
|
+
agentId,
|
|
2019
|
+
workspaceId,
|
|
2020
|
+
message: agent.lastInboundMessage,
|
|
2021
|
+
content: fallbackText,
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
this._resetVisibleReplyTracking(agent);
|
|
1891
2026
|
console.log(`[AgentManager][${displayName}] turn done (stop_reason=${event.stop_reason ?? '?'})`);
|
|
1892
2027
|
this._emitLifecycle(connection, {
|
|
1893
2028
|
agentId,
|
|
@@ -1897,7 +2032,7 @@ export class AgentManager {
|
|
|
1897
2032
|
availability: 'available',
|
|
1898
2033
|
runtimeState: 'running',
|
|
1899
2034
|
reason: 'turn_end',
|
|
1900
|
-
sessionId:
|
|
2035
|
+
sessionId: agent?.sessionId ?? null,
|
|
1901
2036
|
});
|
|
1902
2037
|
connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'online', detail: '', entries: [] });
|
|
1903
2038
|
}
|
package/src/drivers/claude.js
CHANGED
|
@@ -46,7 +46,7 @@ CRITICAL RULES:
|
|
|
46
46
|
|
|
47
47
|
## Startup sequence
|
|
48
48
|
|
|
49
|
-
1. If this turn already includes a concrete incoming message with \`from=user\`, first
|
|
49
|
+
1. If this turn already includes a concrete incoming message with \`from=user\`, your first user-visible action MUST be through ${t("send_message")} (or \`${t("create_tasks")}\` immediately followed by ${t("send_message")} for primary-agent execution dispatch).
|
|
50
50
|
2. Read MEMORY.md (via ${t("read_memory")}) and then only the additional memory/notes files you need to handle the current turn well.
|
|
51
51
|
3. If there is no concrete incoming message to handle, stop and wait. New messages will be delivered to you automatically via stdin.
|
|
52
52
|
4. When you receive a message, first apply the audience-aware rules below before deciding whether to call ${t("send_message")}. Do not treat plain narrative output as a substitute for a visible reply.
|
|
@@ -73,7 +73,7 @@ Header fields:
|
|
|
73
73
|
|
|
74
74
|
Each incoming message has a \`from=\` tag and should be handled as follows:
|
|
75
75
|
|
|
76
|
-
- **from=user**: this is a real user request.
|
|
76
|
+
- **from=user**: this is a real user request. Your first user-visible output MUST go through ${t("send_message")} (for execution dispatch as primary agent: \`${t("create_tasks")}\` first, then immediate ${t("send_message")} acknowledgment).
|
|
77
77
|
- **from=peer-agent**: default is **no reply**. Reply only when at least one condition holds:
|
|
78
78
|
1. You were explicitly @mentioned.
|
|
79
79
|
2. You have materially new information that changes execution decisions.
|
|
@@ -136,10 +136,11 @@ Only top-level workspace / DM messages can become tasks. Messages inside threads
|
|
|
136
136
|
5. After approval (e.g. "looks good", "merge it"), set status to \`done\`
|
|
137
137
|
|
|
138
138
|
**Primary-agent dispatch hard rule (fail-closed):**
|
|
139
|
-
- If your role is the workspace primary agent/owner and a user sends an execution request, you MUST call \`${t("create_tasks")}\` first and include an explicit \`scenario_type
|
|
139
|
+
- If your role is the workspace primary agent/owner and a user sends an execution request, you MUST call \`${t("create_tasks")}\` first and include an explicit \`scenario_type\`, then immediately send a visible acknowledgment/update via \`${t("send_message")}\`.
|
|
140
140
|
- Execution requests include requests like content writing, short-video scripting, research, design/asset production, implementation, or any request that requires downstream execution instead of a simple answer.
|
|
141
141
|
- Use \`scenario_type\` values declared by your scenario manifest/dispatch protocol (for example: \`trend_scan\`, \`topic_research\`, \`research\`, \`graphic_writing\`, \`short_video_scripting\`, \`publish\`).
|
|
142
142
|
- Do not route execution work with only \`${t("send_message")}\`: skipping \`${t("create_tasks")}\` can cause downstream \`${t("claim_tasks")}\` failures and deadlock the workflow.
|
|
143
|
+
- If the request is a direct Q&A (no downstream execution dispatch needed), reply directly with \`${t("send_message")}\` and do not force \`${t("create_tasks")}\`.
|
|
143
144
|
|
|
144
145
|
**What \`${t("create_tasks")}\` really means:**
|
|
145
146
|
- Tasks live in the same chat flow as messages. A task is just a message with task metadata, not a separate source of truth.
|
|
@@ -186,6 +187,9 @@ Keep the user informed. They cannot see your internal reasoning, so:
|
|
|
186
187
|
|
|
187
188
|
- **Respect ongoing conversations.** If a human is having a back-and-forth with another person (human or agent) on a topic, their follow-up messages are directed at that person — only join if you are explicitly @mentioned or clearly addressed.
|
|
188
189
|
- **Only respond when relevant.** If a message does not @mention you and is not clearly directed at you or your expertise, do NOT respond. Let the appropriate agent handle it.
|
|
190
|
+
- **No "cannot execute" noise.** Do not send standalone refusal/deflection messages like "I can't execute this", "this is not my job", or "ask another agent". These add noise.
|
|
191
|
+
- **Out-of-scope without @mention = full silence.** If a message is outside your role scope and you were not directly @mentioned, produce no visible output.
|
|
192
|
+
- **Direct @mention exception.** If you are directly @mentioned on out-of-scope work, send one short reroute message and include an @mention of the correct agent.
|
|
189
193
|
- **Only the person doing the work should report on it.** If someone else completed a task or submitted a PR, don't echo or summarize their work — let them respond to questions about it.
|
|
190
194
|
- **Claim before you start.** Always call \`${t("claim_tasks")}\` before doing any work on a task. If the claim fails, stop immediately and pick a different task.
|
|
191
195
|
- **Before stopping, check for concrete blockers you own.** If you still owe a specific handoff, review, decision, or reply that is currently blocking a specific person, send one minimal actionable message to that person or workspace before stopping.
|
|
@@ -512,6 +512,30 @@ function emitPublishJobProgress(onProgress, stage, payload = {}) {
|
|
|
512
512
|
}
|
|
513
513
|
}
|
|
514
514
|
|
|
515
|
+
function errorMessageOf(error) {
|
|
516
|
+
if (error?.message) return String(error.message);
|
|
517
|
+
return String(error);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function attachPublishStage(error, stage, context = {}) {
|
|
521
|
+
const err = error instanceof Error ? error : new Error(errorMessageOf(error));
|
|
522
|
+
const normalizedStage = String(stage ?? '').trim() || 'unknown';
|
|
523
|
+
const existingContext = err.publish_context && typeof err.publish_context === 'object'
|
|
524
|
+
? err.publish_context
|
|
525
|
+
: {};
|
|
526
|
+
|
|
527
|
+
try { err.publish_stage = err.publish_stage || normalizedStage; } catch {}
|
|
528
|
+
try { err.publish_error = err.publish_error || errorMessageOf(error); } catch {}
|
|
529
|
+
try {
|
|
530
|
+
err.publish_context = {
|
|
531
|
+
...existingContext,
|
|
532
|
+
...context,
|
|
533
|
+
stage: normalizedStage,
|
|
534
|
+
};
|
|
535
|
+
} catch {}
|
|
536
|
+
return err;
|
|
537
|
+
}
|
|
538
|
+
|
|
515
539
|
export async function runPublishJob({ serverUrl, machineApiKey, agentId, workspaceDir, job, onProgress }) {
|
|
516
540
|
if (!serverUrl || !machineApiKey || !agentId) {
|
|
517
541
|
throw new Error('runPublishJob requires serverUrl, machineApiKey, and agentId');
|
|
@@ -530,81 +554,107 @@ export async function runPublishJob({ serverUrl, machineApiKey, agentId, workspa
|
|
|
530
554
|
cover,
|
|
531
555
|
} = buildJobInput(job);
|
|
532
556
|
const ownerId = normalizeText(job?.owner_id ?? job?.ownerId);
|
|
557
|
+
const jobId = normalizeText(job?.id);
|
|
558
|
+
const stageContext = {
|
|
559
|
+
platform: platform || null,
|
|
560
|
+
contentType: contentType || null,
|
|
561
|
+
jobId: jobId ?? null,
|
|
562
|
+
ownerId: ownerId ?? null,
|
|
563
|
+
};
|
|
533
564
|
|
|
534
|
-
if (!platform) throw new Error('publish job missing platform');
|
|
535
|
-
if (!contentType) throw new Error('publish job missing content_type');
|
|
536
|
-
if (typeof text !== 'string' || !text.trim())
|
|
565
|
+
if (!platform) throw attachPublishStage(new Error('publish job missing platform'), 'input_validation', stageContext);
|
|
566
|
+
if (!contentType) throw attachPublishStage(new Error('publish job missing content_type'), 'input_validation', stageContext);
|
|
567
|
+
if (typeof text !== 'string' || !text.trim()) {
|
|
568
|
+
throw attachPublishStage(new Error('publish job missing text'), 'input_validation', stageContext);
|
|
569
|
+
}
|
|
537
570
|
|
|
538
571
|
const resolvedWorkspaceDir = workspaceDir ?? process.cwd();
|
|
539
572
|
const workspaceRootDir = path.dirname(resolvedWorkspaceDir);
|
|
573
|
+
stageContext.platform = platform;
|
|
574
|
+
stageContext.contentType = contentType;
|
|
540
575
|
|
|
576
|
+
let currentStage = 'precheck';
|
|
541
577
|
let precheck = null;
|
|
542
578
|
try {
|
|
543
|
-
|
|
579
|
+
currentStage = 'precheck';
|
|
580
|
+
try {
|
|
581
|
+
precheck = await runPublishPrecheck({
|
|
582
|
+
platform,
|
|
583
|
+
title,
|
|
584
|
+
text,
|
|
585
|
+
tags,
|
|
586
|
+
payload,
|
|
587
|
+
callTool: callOfficialTool,
|
|
588
|
+
});
|
|
589
|
+
} catch (error) {
|
|
590
|
+
emitPublishJobProgress(onProgress, 'precheck_error', {
|
|
591
|
+
platform,
|
|
592
|
+
error: errorMessageOf(error),
|
|
593
|
+
});
|
|
594
|
+
throw attachPublishStage(error, currentStage, stageContext);
|
|
595
|
+
}
|
|
596
|
+
emitPublishJobProgress(onProgress, 'precheck_done', {
|
|
544
597
|
platform,
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
payload,
|
|
549
|
-
callTool: callOfficialTool,
|
|
598
|
+
ok: precheck.ok,
|
|
599
|
+
blockerCount: precheck.blockers.length,
|
|
600
|
+
warningCount: precheck.warnings.length,
|
|
550
601
|
});
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
602
|
+
if (!precheck.ok) {
|
|
603
|
+
currentStage = 'precheck_validate';
|
|
604
|
+
const blockerSummary = precheck.blockers.map(item => `${item.code}: ${item.message}`).join(' | ');
|
|
605
|
+
throw attachPublishStage(new Error(`PUBLISH_PRECHECK_BLOCKED:${blockerSummary || 'precheck failed'}`), currentStage, stageContext);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
currentStage = 'materialize_media';
|
|
609
|
+
const localMedia = await materializeMedia({
|
|
610
|
+
images,
|
|
611
|
+
video,
|
|
612
|
+
cover,
|
|
613
|
+
workspaceId,
|
|
614
|
+
workspaceRootDir,
|
|
615
|
+
serverUrl,
|
|
616
|
+
machineApiKey,
|
|
617
|
+
jobId: job?.id,
|
|
555
618
|
});
|
|
556
|
-
throw error;
|
|
557
|
-
}
|
|
558
|
-
emitPublishJobProgress(onProgress, 'precheck_done', {
|
|
559
|
-
platform,
|
|
560
|
-
ok: precheck.ok,
|
|
561
|
-
blockerCount: precheck.blockers.length,
|
|
562
|
-
warningCount: precheck.warnings.length,
|
|
563
|
-
});
|
|
564
|
-
if (!precheck.ok) {
|
|
565
|
-
const blockerSummary = precheck.blockers.map(item => `${item.code}: ${item.message}`).join(' | ');
|
|
566
|
-
throw new Error(`PUBLISH_PRECHECK_BLOCKED:${blockerSummary || 'precheck failed'}`);
|
|
567
|
-
}
|
|
568
619
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
workspaceRootDir,
|
|
575
|
-
serverUrl,
|
|
576
|
-
machineApiKey,
|
|
577
|
-
jobId: job?.id,
|
|
578
|
-
});
|
|
579
|
-
const staticAdapter = createStaticAdapter(platform);
|
|
580
|
-
const req = staticAdapter.getRequirements(contentType);
|
|
581
|
-
if (!req) throw new Error(`INPUT_UNSUPPORTED_CONTENT_TYPE: ${platform} does not support ${contentType}`);
|
|
582
|
-
emitPublishJobProgress(onProgress, 'publish_action_start', { platform, contentType });
|
|
620
|
+
currentStage = 'resolve_requirements';
|
|
621
|
+
const staticAdapter = createStaticAdapter(platform);
|
|
622
|
+
const req = staticAdapter.getRequirements(contentType);
|
|
623
|
+
if (!req) throw new Error(`INPUT_UNSUPPORTED_CONTENT_TYPE: ${platform} does not support ${contentType}`);
|
|
624
|
+
emitPublishJobProgress(onProgress, 'publish_action_start', { platform, contentType });
|
|
583
625
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
626
|
+
currentStage = 'validate_media';
|
|
627
|
+
const media = validateMedia({
|
|
628
|
+
platform,
|
|
629
|
+
contentType,
|
|
630
|
+
...localMedia,
|
|
631
|
+
workspaceDir: resolvedWorkspaceDir,
|
|
632
|
+
workspaceRootDir,
|
|
633
|
+
});
|
|
591
634
|
|
|
592
|
-
|
|
635
|
+
currentStage = 'publish_profile';
|
|
593
636
|
const { publishResult, healthCheck } = await withPublisherProfile(platform, { ownerId }, async () => {
|
|
637
|
+
currentStage = 'adapter_session';
|
|
594
638
|
const adapter = await getAdapter(platform, { ownerId });
|
|
639
|
+
|
|
640
|
+
currentStage = 'pre_publish_login';
|
|
595
641
|
const prePublishLogin = await adapter.checkLoginStatus();
|
|
596
642
|
if (prePublishLogin?.loggedIn === false) {
|
|
597
643
|
throw new Error(`LOGIN_EXPIRED:${platform}`);
|
|
598
644
|
}
|
|
645
|
+
|
|
599
646
|
let publishResult = null;
|
|
600
647
|
if (contentType === 'image_text') {
|
|
648
|
+
currentStage = 'adapter_publish_image_text';
|
|
601
649
|
publishResult = await adapter.publishImageText({ title, text, tags, images: media.images });
|
|
602
650
|
} else {
|
|
651
|
+
currentStage = 'adapter_publish_short_video';
|
|
603
652
|
publishResult = await adapter.publishShortVideo({ title, text, tags, video: media.video, cover: media.cover });
|
|
604
653
|
}
|
|
605
654
|
|
|
606
655
|
let healthCheck = null;
|
|
607
656
|
try {
|
|
657
|
+
currentStage = 'post_publish_health';
|
|
608
658
|
healthCheck = await adapter.checkLoginStatus();
|
|
609
659
|
} catch (error) {
|
|
610
660
|
healthCheck = {
|
|
@@ -631,10 +681,18 @@ export async function runPublishJob({ serverUrl, machineApiKey, agentId, workspa
|
|
|
631
681
|
precheck,
|
|
632
682
|
healthCheck,
|
|
633
683
|
};
|
|
634
|
-
} catch (
|
|
635
|
-
|
|
684
|
+
} catch (error) {
|
|
685
|
+
const stagedError = attachPublishStage(error, currentStage, stageContext);
|
|
686
|
+
const errorMessage = errorMessageOf(stagedError);
|
|
687
|
+
if (errorMessage.startsWith('LOGIN_EXPIRED')) {
|
|
636
688
|
closeSession(platform);
|
|
637
689
|
}
|
|
638
|
-
|
|
690
|
+
emitPublishJobProgress(onProgress, 'publish_action_error', {
|
|
691
|
+
platform,
|
|
692
|
+
contentType,
|
|
693
|
+
stage: stagedError.publish_stage ?? currentStage,
|
|
694
|
+
error: stagedError.publish_error ?? errorMessage,
|
|
695
|
+
});
|
|
696
|
+
throw stagedError;
|
|
639
697
|
}
|
|
640
698
|
}
|