@lightcone-ai/daemon 0.15.1 → 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 +160 -21
- package/src/connection.js +1 -1
- 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';
|
|
@@ -649,12 +740,16 @@ export class AgentManager {
|
|
|
649
740
|
}
|
|
650
741
|
}
|
|
651
742
|
|
|
743
|
+
const resolvedDirectiveEnvVars = normalizeObject(
|
|
744
|
+
this._replaceDirectiveValue(normalizeObject(directive?.env_vars), baseReplacements)
|
|
745
|
+
);
|
|
746
|
+
|
|
652
747
|
const env = {
|
|
653
748
|
...process.env,
|
|
654
749
|
FORCE_COLOR: '0',
|
|
655
750
|
NO_COLOR: '1',
|
|
656
751
|
...Object.fromEntries(
|
|
657
|
-
Object.entries(
|
|
752
|
+
Object.entries(resolvedDirectiveEnvVars).map(([k, v]) => [k, String(v ?? '')])
|
|
658
753
|
),
|
|
659
754
|
};
|
|
660
755
|
|
|
@@ -1119,6 +1214,10 @@ export class AgentManager {
|
|
|
1119
1214
|
directive,
|
|
1120
1215
|
requiredCredentials,
|
|
1121
1216
|
lastStartupMessage: startupMsg?.message ?? null,
|
|
1217
|
+
lastInboundMessage: startupMsg?.message ?? null,
|
|
1218
|
+
visibleTextSeen: false,
|
|
1219
|
+
sendMessageUsed: false,
|
|
1220
|
+
visibleTextBuffer: '',
|
|
1122
1221
|
stopCause: null,
|
|
1123
1222
|
});
|
|
1124
1223
|
this.starting.delete(key);
|
|
@@ -1168,9 +1267,13 @@ export class AgentManager {
|
|
|
1168
1267
|
runtime: 'codex',
|
|
1169
1268
|
codexVisibleTextSeen: false,
|
|
1170
1269
|
codexSendMessageUsed: false,
|
|
1270
|
+
visibleTextSeen: false,
|
|
1271
|
+
sendMessageUsed: false,
|
|
1272
|
+
visibleTextBuffer: '',
|
|
1171
1273
|
directive,
|
|
1172
1274
|
requiredCredentials,
|
|
1173
1275
|
lastStartupMessage: startupMsg?.message ?? null,
|
|
1276
|
+
lastInboundMessage: startupMsg?.message ?? null,
|
|
1174
1277
|
stopCause: null,
|
|
1175
1278
|
});
|
|
1176
1279
|
this.starting.delete(key);
|
|
@@ -1193,9 +1296,13 @@ export class AgentManager {
|
|
|
1193
1296
|
sessionId: config.sessionId ?? null,
|
|
1194
1297
|
proc,
|
|
1195
1298
|
runtime: 'claude',
|
|
1299
|
+
visibleTextSeen: false,
|
|
1300
|
+
sendMessageUsed: false,
|
|
1301
|
+
visibleTextBuffer: '',
|
|
1196
1302
|
directive,
|
|
1197
1303
|
requiredCredentials,
|
|
1198
1304
|
lastStartupMessage: startupMsg?.message ?? null,
|
|
1305
|
+
lastInboundMessage: startupMsg?.message ?? null,
|
|
1199
1306
|
stopCause: null,
|
|
1200
1307
|
});
|
|
1201
1308
|
this.starting.delete(key);
|
|
@@ -1496,6 +1603,12 @@ export class AgentManager {
|
|
|
1496
1603
|
}
|
|
1497
1604
|
if (stage === 'precheck_error') {
|
|
1498
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
|
+
);
|
|
1499
1612
|
}
|
|
1500
1613
|
},
|
|
1501
1614
|
});
|
|
@@ -1519,13 +1632,14 @@ export class AgentManager {
|
|
|
1519
1632
|
});
|
|
1520
1633
|
} catch (err) {
|
|
1521
1634
|
const errorMessage = err?.message ?? String(err);
|
|
1522
|
-
|
|
1635
|
+
const failureResult = buildPublishJobFailureResult(err, 'publish_job');
|
|
1636
|
+
console.error(`${logPrefix} failed stage=${failureResult.stage} error=${errorMessage}`);
|
|
1523
1637
|
try {
|
|
1524
1638
|
console.log(`${logPrefix} completion-post ok=false`);
|
|
1525
1639
|
await this._postInternalPublishJobComplete({
|
|
1526
1640
|
jobId,
|
|
1527
1641
|
ok: false,
|
|
1528
|
-
result:
|
|
1642
|
+
result: failureResult,
|
|
1529
1643
|
error: errorMessage,
|
|
1530
1644
|
});
|
|
1531
1645
|
console.log(`${logPrefix} completion-post posted ok=false`);
|
|
@@ -1542,6 +1656,7 @@ export class AgentManager {
|
|
|
1542
1656
|
status: 'failed',
|
|
1543
1657
|
completed_at: new Date().toISOString(),
|
|
1544
1658
|
error: errorMessage,
|
|
1659
|
+
result: failureResult,
|
|
1545
1660
|
});
|
|
1546
1661
|
}
|
|
1547
1662
|
}
|
|
@@ -1587,6 +1702,7 @@ export class AgentManager {
|
|
|
1587
1702
|
console.log(`[AgentManager] Queued seq=${seq} for codex next turn pending=${queueSize}`);
|
|
1588
1703
|
return;
|
|
1589
1704
|
}
|
|
1705
|
+
this._rememberInboundMessage(agent, message);
|
|
1590
1706
|
this._write(key, text);
|
|
1591
1707
|
}
|
|
1592
1708
|
|
|
@@ -1608,6 +1724,7 @@ export class AgentManager {
|
|
|
1608
1724
|
const text = this._formatDeliveryText(message);
|
|
1609
1725
|
const agent = this.agents.get(key);
|
|
1610
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);
|
|
1611
1728
|
this._write(key, text);
|
|
1612
1729
|
}
|
|
1613
1730
|
}
|
|
@@ -1667,11 +1784,13 @@ export class AgentManager {
|
|
|
1667
1784
|
break;
|
|
1668
1785
|
case 'text':
|
|
1669
1786
|
agent.codexVisibleTextSeen = true;
|
|
1787
|
+
this._trackVisibleAssistantText(agent, evt.text);
|
|
1670
1788
|
console.log(`[AgentManager][${agent.config?.displayName ?? agentId.slice(0, 8)}][codex] <text> ${String(evt.text ?? '').slice(0, 500)}`);
|
|
1671
1789
|
break;
|
|
1672
1790
|
case 'tool_call':
|
|
1673
|
-
if (evt.name
|
|
1791
|
+
if (this._isSendMessageToolName(evt.name)) {
|
|
1674
1792
|
agent.codexSendMessageUsed = true;
|
|
1793
|
+
this._markSendMessageToolUsed(agent);
|
|
1675
1794
|
this._recordSuccessfulOutboundMessage({
|
|
1676
1795
|
key,
|
|
1677
1796
|
agent,
|
|
@@ -1694,6 +1813,7 @@ export class AgentManager {
|
|
|
1694
1813
|
break;
|
|
1695
1814
|
case 'turn_end':
|
|
1696
1815
|
agent.kimiIdle = true;
|
|
1816
|
+
this._resetVisibleReplyTracking(agent);
|
|
1697
1817
|
this._emitLifecycle(connection, {
|
|
1698
1818
|
agentId,
|
|
1699
1819
|
workspaceId,
|
|
@@ -1743,13 +1863,15 @@ export class AgentManager {
|
|
|
1743
1863
|
break;
|
|
1744
1864
|
case 'text':
|
|
1745
1865
|
agent.codexVisibleTextSeen = true;
|
|
1866
|
+
this._trackVisibleAssistantText(agent, evt.text);
|
|
1746
1867
|
console.log(
|
|
1747
1868
|
`[AgentManager][${agent.config?.displayName ?? agentId.slice(0, 8)}][codex][text] ${String(evt.text ?? '').slice(0, 1200)}`
|
|
1748
1869
|
);
|
|
1749
1870
|
break;
|
|
1750
1871
|
case 'tool_call':
|
|
1751
|
-
if (evt.name
|
|
1872
|
+
if (this._isSendMessageToolName(evt.name)) {
|
|
1752
1873
|
agent.codexSendMessageUsed = true;
|
|
1874
|
+
this._markSendMessageToolUsed(agent);
|
|
1753
1875
|
this._recordSuccessfulOutboundMessage({
|
|
1754
1876
|
key,
|
|
1755
1877
|
agent,
|
|
@@ -1783,6 +1905,7 @@ export class AgentManager {
|
|
|
1783
1905
|
}
|
|
1784
1906
|
agent.codexVisibleTextSeen = false;
|
|
1785
1907
|
agent.codexSendMessageUsed = false;
|
|
1908
|
+
this._resetVisibleReplyTracking(agent);
|
|
1786
1909
|
this._emitLifecycle(connection, {
|
|
1787
1910
|
agentId,
|
|
1788
1911
|
workspaceId,
|
|
@@ -1809,10 +1932,10 @@ export class AgentManager {
|
|
|
1809
1932
|
let event;
|
|
1810
1933
|
try { event = JSON.parse(line); }
|
|
1811
1934
|
catch { return; }
|
|
1935
|
+
const agent = this.agents.get(key);
|
|
1812
1936
|
|
|
1813
1937
|
// Capture and sync session ID
|
|
1814
1938
|
if (event.session_id) {
|
|
1815
|
-
const agent = this.agents.get(key);
|
|
1816
1939
|
if (agent && agent.sessionId !== event.session_id) {
|
|
1817
1940
|
agent.sessionId = event.session_id;
|
|
1818
1941
|
connection.send({ type: 'agent:session', agentId, workspaceId, sessionId: event.session_id });
|
|
@@ -1820,27 +1943,26 @@ export class AgentManager {
|
|
|
1820
1943
|
}
|
|
1821
1944
|
|
|
1822
1945
|
// Activity state machine
|
|
1823
|
-
const displayName =
|
|
1946
|
+
const displayName = agent?.config?.displayName ?? agentId.slice(0, 8);
|
|
1824
1947
|
if (event.type === 'assistant') {
|
|
1825
1948
|
const content = event.message?.content ?? [];
|
|
1826
1949
|
for (const block of content) {
|
|
1827
1950
|
if (block.type === 'thinking') {
|
|
1828
1951
|
console.log(`[AgentManager][${displayName}] <thinking> ${block.thinking?.slice(0, 500)}`);
|
|
1829
1952
|
} else if (block.type === 'text') {
|
|
1953
|
+
this._trackVisibleAssistantText(agent, block.text);
|
|
1830
1954
|
console.log(`[AgentManager][${displayName}] <text> ${block.text?.slice(0, 500)}`);
|
|
1831
1955
|
} else if (block.type === 'tool_use') {
|
|
1832
1956
|
console.log(`[AgentManager][${displayName}] <tool_use> ${block.name} params=${JSON.stringify(block.input ?? {}).slice(0, 500)}`);
|
|
1833
|
-
if (block.name
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
});
|
|
1843
|
-
}
|
|
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
|
+
});
|
|
1844
1966
|
}
|
|
1845
1967
|
this._emitLifecycle(connection, {
|
|
1846
1968
|
agentId,
|
|
@@ -1850,7 +1972,7 @@ export class AgentManager {
|
|
|
1850
1972
|
availability: 'busy',
|
|
1851
1973
|
runtimeState: 'running',
|
|
1852
1974
|
reason: `tool:${block.name ?? 'unknown_tool'}`,
|
|
1853
|
-
sessionId:
|
|
1975
|
+
sessionId: agent?.sessionId ?? null,
|
|
1854
1976
|
});
|
|
1855
1977
|
connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'working', detail: block.name, entries: [] });
|
|
1856
1978
|
}
|
|
@@ -1864,7 +1986,7 @@ export class AgentManager {
|
|
|
1864
1986
|
availability: 'busy',
|
|
1865
1987
|
runtimeState: 'running',
|
|
1866
1988
|
reason: 'thinking',
|
|
1867
|
-
sessionId:
|
|
1989
|
+
sessionId: agent?.sessionId ?? null,
|
|
1868
1990
|
});
|
|
1869
1991
|
connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'thinking', detail: '', entries: [] });
|
|
1870
1992
|
}
|
|
@@ -1884,6 +2006,23 @@ export class AgentManager {
|
|
|
1884
2006
|
console.log(`[AgentManager][${displayName}] <tool_result> id=${tr.tool_use_id ?? '?'} → ${resultStr}`);
|
|
1885
2007
|
}
|
|
1886
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);
|
|
1887
2026
|
console.log(`[AgentManager][${displayName}] turn done (stop_reason=${event.stop_reason ?? '?'})`);
|
|
1888
2027
|
this._emitLifecycle(connection, {
|
|
1889
2028
|
agentId,
|
|
@@ -1893,7 +2032,7 @@ export class AgentManager {
|
|
|
1893
2032
|
availability: 'available',
|
|
1894
2033
|
runtimeState: 'running',
|
|
1895
2034
|
reason: 'turn_end',
|
|
1896
|
-
sessionId:
|
|
2035
|
+
sessionId: agent?.sessionId ?? null,
|
|
1897
2036
|
});
|
|
1898
2037
|
connection.send({ type: 'agent:activity', agentId, workspaceId, activity: 'online', detail: '', entries: [] });
|
|
1899
2038
|
}
|
package/src/connection.js
CHANGED
|
@@ -69,7 +69,7 @@ export class DaemonConnection {
|
|
|
69
69
|
let msg;
|
|
70
70
|
try { msg = JSON.parse(raw.toString()); }
|
|
71
71
|
catch { return; }
|
|
72
|
-
if (msg.type !== 'pong') {
|
|
72
|
+
if (msg.type !== 'pong' && msg.type !== 'ping') {
|
|
73
73
|
console.log(`[Connection] ← ${msg.type}${msg.agentId ? ` agent=${msg.agentId.slice(0,8)}` : ''}${msg.workspaceId ? ` workspace=${msg.workspaceId.slice(0,8)}` : ''}${msg.seq != null ? ` seq=${msg.seq}` : ''}`);
|
|
74
74
|
}
|
|
75
75
|
this.onMessage(msg);
|
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
|
}
|