@lightcone-ai/daemon 0.15.27 → 0.15.29

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.
@@ -858,7 +858,7 @@ export function planVideo({
858
858
  const coreMessage = toSafeString(understanding?.core_message || understanding?.coreMessage || understanding?.title) || '本页价值点概览';
859
859
 
860
860
  const hotspots = collectHotspots(understanding);
861
- const skipZones = collectSkipZones(understanding);
861
+ const baseSkipZones = collectSkipZones(understanding);
862
862
  const coreRange = resolveCoreRange(understanding, hotspots);
863
863
  const semanticSlots = collectSemanticSlots(understanding);
864
864
  const recruitmentFailClosedEnabled = shouldApplyRecruitmentFailClosed(understanding);
@@ -878,6 +878,16 @@ export function planVideo({
878
878
  const modeInfo = resolveNarrationMode({ understanding, slots: semanticSlots });
879
879
  const semanticSummary = buildSemanticSummary(semanticSlots);
880
880
 
881
+ const isRecruitmentMode = modeInfo.mode === 'job_intel_broadcast' || modeInfo.mode === 'job_alert';
882
+ // For recruitment pages, treat the entry/CTA region as a hard skip zone so the
883
+ // camera never scrolls into the QR-code / application-link area.
884
+ const entrySkipRange = isRecruitmentMode
885
+ ? normalizeYRange(semanticSlots.entry_or_cta?.focus_region)
886
+ : null;
887
+ const skipZones = entrySkipRange
888
+ ? [...baseSkipZones, entrySkipRange]
889
+ : baseSkipZones;
890
+
881
891
  const hotspotHighlights = pickHighlightHotspots({
882
892
  hotspots,
883
893
  skipZones,
@@ -986,7 +996,11 @@ export function planVideo({
986
996
  previousY = highlight.target_y;
987
997
  }
988
998
 
989
- const ctaSlotKey = pickFirstAvailableSlot(semanticSlots, ['entry_or_cta', 'process', 'company']);
999
+ // Recruitment pages: never navigate to entry/QR-code area for CTA.
1000
+ const ctaSlotCandidates = isRecruitmentMode
1001
+ ? ['process', 'company']
1002
+ : ['entry_or_cta', 'process', 'company'];
1003
+ const ctaSlotKey = pickFirstAvailableSlot(semanticSlots, ctaSlotCandidates);
990
1004
  const ctaSlot = ctaSlotKey ? semanticSlots[ctaSlotKey] : null;
991
1005
  const ctaAnchor = focusRegionToHighlight({
992
1006
  focusRegion: ctaSlot?.focus_region,
@@ -995,15 +1009,18 @@ export function planVideo({
995
1009
  text: ctaSlotKey ? (slotValueToText(ctaSlot) || slotLabel(ctaSlotKey)) : '收尾行动',
996
1010
  confidence: ctaSlot?.confidence ?? 0.38,
997
1011
  });
998
- const ctaAction = ctaAnchor && modeInfo.mode !== 'refuse_auto_broadcast'
999
- ? {
1000
- type: 'scroll_to_dwell',
1001
- target_hotspot: ctaAnchor.id,
1002
- target_y: ctaAnchor.target_y,
1003
- transition_ms: 900,
1004
- reason: '收尾聚焦投递入口或行动提示',
1005
- }
1006
- : buildCtaAction({ coreRange });
1012
+ // Recruitment mode: always scroll back to core start — never scroll to entry/QR area.
1013
+ const ctaAction = isRecruitmentMode
1014
+ ? buildCtaAction({ coreRange })
1015
+ : (ctaAnchor && modeInfo.mode !== 'refuse_auto_broadcast'
1016
+ ? {
1017
+ type: 'scroll_to_dwell',
1018
+ target_hotspot: ctaAnchor.id,
1019
+ target_y: ctaAnchor.target_y,
1020
+ transition_ms: 900,
1021
+ reason: '收尾聚焦行动提示',
1022
+ }
1023
+ : buildCtaAction({ coreRange }));
1007
1024
 
1008
1025
  phasePlan.push({
1009
1026
  phase_id: 'cta',
@@ -1013,7 +1030,7 @@ export function planVideo({
1013
1030
  semantic_slot: ctaSlotKey,
1014
1031
  focus_region: ctaAnchor?.y_range ?? null,
1015
1032
  confidence: ctaSlot ? Number(clampNumber(ctaSlot.confidence, 0, 1, 0).toFixed(2)) : null,
1016
- guidance: '收尾自然收口(如"感兴趣去原文查看"),禁止提及任何 URL、网址、二维码或投递入口',
1033
+ guidance: '介绍完最后一个信息点直接结束,禁止加任何收口语(不说"感兴趣去原文查看""截止见正文"等),禁止提及 URL、网址、二维码或投递入口',
1017
1034
  });
1018
1035
 
1019
1036
  const cappedPlan = phasePlan.slice(0, 5);
@@ -1137,27 +1154,7 @@ function buildPhaseSentence({ phase, strategy, phaseIndex }) {
1137
1154
  }
1138
1155
 
1139
1156
  if (role === 'cta') {
1140
- if (mode === 'job_intel_broadcast') {
1141
- return ensureSentenceLength(
1142
- `感兴趣的同学去原文查看完整岗位要求和截止时间,对照自己情况再决定要不要投。`
1143
- );
1144
- }
1145
-
1146
- if (mode === 'job_alert') {
1147
- return ensureSentenceLength(
1148
- `细节不完整,建议收藏原文,等正式公告发布后再核对岗位方向、城市和流程再决定。`
1149
- );
1150
- }
1151
-
1152
- if (mode === 'refuse_auto_broadcast') {
1153
- return ensureSentenceLength(
1154
- '页面信息不完整,暂不播报。建议直接去官方渠道确认公司、岗位和发布时间后再做判断。'
1155
- );
1156
- }
1157
-
1158
- return ensureSentenceLength(
1159
- '具体要求和截止时间见原文,建议直接查看官方公告,避免依赖摘要信息做投递判断。'
1160
- );
1157
+ return null;
1161
1158
  }
1162
1159
 
1163
1160
  if (mode === 'job_intel_broadcast') {
@@ -1369,6 +1366,8 @@ export async function detailSections({
1369
1366
  phaseIndex: i,
1370
1367
  });
1371
1368
 
1369
+ if (sentence == null) continue;
1370
+
1372
1371
  const voice = resolveVoiceForPhase({ profile, role, index: i });
1373
1372
  const phaseBudgetMs = clampInt((toFiniteNumber(phase?.duration_s) ?? 6) * 1000, 1000, 180000, 6000);
1374
1373
 
@@ -1444,6 +1443,7 @@ export async function detailSections({
1444
1443
  const totalDurationMs = sections.reduce((sum, item) => sum + item.dwell_ms, 0);
1445
1444
 
1446
1445
  return {
1446
+ detail_sections_version: 1,
1447
1447
  sections,
1448
1448
  outro_video_id: toSafeString(strategy?.outro_video_id || strategy?.outroVideoId) || DEFAULT_OUTRO_VIDEO_ID,
1449
1449
  total_duration_ms: totalDurationMs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightcone-ai/daemon",
3
- "version": "0.15.27",
3
+ "version": "0.15.29",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -38,7 +38,14 @@ export async function launchChromiumMobile({
38
38
  hasTouch = true,
39
39
  headless = false,
40
40
  channel = 'chrome',
41
- launchArgs = ['--no-sandbox', '--disable-dev-shm-usage'],
41
+ launchArgs = [
42
+ '--no-sandbox',
43
+ '--disable-dev-shm-usage',
44
+ '--kiosk',
45
+ '--disable-infobars',
46
+ '--no-first-run',
47
+ '--no-default-browser-check',
48
+ ],
42
49
  playwrightModule = 'playwright',
43
50
  launchOptions = {},
44
51
  contextOptions = {},
@@ -195,7 +195,7 @@ export async function recordUrlNarration({
195
195
  xvfbStopTimeoutMs = 5000,
196
196
  postPlanTailMs = 600,
197
197
  } = {}) {
198
- const zoom = Number.isFinite(Number(page_zoom)) && Number(page_zoom) > 0 ? Number(page_zoom) : 1.0;
198
+ const zoom = Number.isFinite(Number(page_zoom)) && Number(page_zoom) > 0 ? Number(page_zoom) : 1.1;
199
199
  const rawPhases = normalizePlanPhases(plan);
200
200
  const phases = zoom !== 1.0 ? rawPhases.map(p => scalePhaseY(p, zoom)) : rawPhases;
201
201
  const executablePlan = {
@@ -1481,7 +1481,6 @@ server.tool('record_url_narration',
1481
1481
  }).optional().describe('Default 1080x1920 (mobile portrait). Override only if the plan requires a different shape.'),
1482
1482
  fps: z.number().optional().describe('Default 30. Do not change unless needed.'),
1483
1483
  settle_ms: z.number().optional().describe('Default 4000. Settle wait after navigation before recording starts.'),
1484
- page_zoom: z.number().optional().describe('Browser zoom factor applied before recording. Default 1.1 (10% zoom in). Set to 1.0 to disable. Plan Y coordinates are automatically scaled by this factor.'),
1485
1484
  },
1486
1485
  async (args) => {
1487
1486
  if (isBlockedCvmaxEditorVideoTool('record_url_narration')) {
@@ -48,6 +48,19 @@ function derivePhaseCount({ plan, recorderOutput }) {
48
48
  return null;
49
49
  }
50
50
 
51
+ const PIPELINE_SENTINEL_KEY = 'detail_sections_version';
52
+
53
+ function assertPipelineCompliance(plan) {
54
+ if (!isPlainObject(plan)) return;
55
+ if (!plan[PIPELINE_SENTINEL_KEY]) {
56
+ throw new Error(
57
+ 'pipeline_violation: plan must come from detail_sections output. '
58
+ + 'Required pipeline: analyze_page → plan_video → detail_sections → generate_voiceover → record_url_narration → compose_video → submit_to_library. '
59
+ + 'Do not hand-write phases or bypass detail_sections.'
60
+ );
61
+ }
62
+ }
63
+
51
64
  export function validateRecordUrlNarrationArgs(args = {}) {
52
65
  const normalizedUrl = normalizeText(args.url);
53
66
  if (!normalizedUrl) {
@@ -113,6 +126,12 @@ export async function runRecordUrlNarrationTool({
113
126
  return toolError(`Error: ${error.message}`);
114
127
  }
115
128
 
129
+ try {
130
+ assertPipelineCompliance(validatedInput.plan);
131
+ } catch (error) {
132
+ return toolError(`Error: ${error.message}`);
133
+ }
134
+
116
135
  try {
117
136
  const result = await runMandatoryLocalToolFn({
118
137
  toolName: 'record_url_narration',
@@ -141,7 +160,6 @@ export async function runRecordUrlNarrationTool({
141
160
  viewport: finalInput.viewport,
142
161
  fps: finalInput.fps,
143
162
  settle_ms: finalInput.settle_ms,
144
- page_zoom: finalInput.page_zoom,
145
163
  });
146
164
 
147
165
  return {