@lightcone-ai/daemon 0.15.24 → 0.15.26

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.
@@ -781,7 +781,7 @@ function pickSlotKeysForMode({ mode, slots }) {
781
781
  return ordered.filter(key => slotHasValue(slots[key])).slice(0, 3);
782
782
  }
783
783
  if (mode === 'job_alert') {
784
- const ordered = ['job_directions', 'locations', 'cohort', 'entry_or_cta'];
784
+ const ordered = ['job_directions', 'locations', 'cohort'];
785
785
  return ordered.filter(key => slotHasValue(slots[key])).slice(0, 1);
786
786
  }
787
787
  if (mode === 'info_summary') {
@@ -1013,7 +1013,7 @@ export function planVideo({
1013
1013
  semantic_slot: ctaSlotKey,
1014
1014
  focus_region: ctaAnchor?.y_range ?? null,
1015
1015
  confidence: ctaSlot ? Number(clampNumber(ctaSlot.confidence, 0, 1, 0).toFixed(2)) : null,
1016
- guidance: '收尾给出下一步动作,避免空泛口号',
1016
+ guidance: '收尾自然收口(如"感兴趣去原文查看"),禁止提及任何 URL、网址、二维码或投递入口',
1017
1017
  });
1018
1018
 
1019
1019
  const cappedPlan = phasePlan.slice(0, 5);
@@ -1139,24 +1139,24 @@ function buildPhaseSentence({ phase, strategy, phaseIndex }) {
1139
1139
  if (role === 'cta') {
1140
1140
  if (mode === 'job_intel_broadcast') {
1141
1141
  return ensureSentenceLength(
1142
- `最后给行动建议:如果你对这次机会感兴趣,优先去${entry}核对截止时间、岗位要求和投递方式,再决定是否马上投递;如果与你目标不匹配,就按同一口径快速跳过。`
1142
+ `感兴趣的同学去原文查看完整岗位要求和截止时间,对照自己情况再决定要不要投。`
1143
1143
  );
1144
1144
  }
1145
1145
 
1146
1146
  if (mode === 'job_alert') {
1147
1147
  return ensureSentenceLength(
1148
- `建议先把这条提醒收藏,然后去${entry}逐项核对岗位方向、城市和流程细节。确认信息完整后再投递,会比凭单条资讯直接判断更稳妥。`
1148
+ `细节不完整,建议收藏原文,等正式公告发布后再核对岗位方向、城市和流程再决定。`
1149
1149
  );
1150
1150
  }
1151
1151
 
1152
1152
  if (mode === 'refuse_auto_broadcast') {
1153
1153
  return ensureSentenceLength(
1154
- '建议补充官方招聘链接,或提供包含公司、岗位、发布时间的清晰页面截图后再自动播报。信息完整度上来后,系统才能给出可执行的投递判断。'
1154
+ '页面信息不完整,暂不播报。建议直接去官方渠道确认公司、岗位和发布时间后再做判断。'
1155
1155
  );
1156
1156
  }
1157
1157
 
1158
1158
  return ensureSentenceLength(
1159
- `把它当作招聘资讯线索更合适:最终请以${entry}的原始岗位说明为准,尤其是城市、要求和截止时间,避免被二次转述或摘要信息带偏判断。`
1159
+ '具体要求和截止时间见原文,建议直接查看官方公告,避免依赖摘要信息做投递判断。'
1160
1160
  );
1161
1161
  }
1162
1162
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightcone-ai/daemon",
3
- "version": "0.15.24",
3
+ "version": "0.15.26",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -157,6 +157,26 @@ async function scrollToTop(page) {
157
157
  });
158
158
  }
159
159
 
160
+ function scalePhaseY(phase, zoom) {
161
+ if (!phase || typeof phase !== 'object') return phase;
162
+ const scale = (v) => (Number.isFinite(Number(v)) ? Math.round(Number(v) * zoom) : v);
163
+ const scaledVisualAction = phase.visual_action && typeof phase.visual_action === 'object'
164
+ ? {
165
+ ...phase.visual_action,
166
+ ...(phase.visual_action.target_y != null ? { target_y: scale(phase.visual_action.target_y) } : {}),
167
+ ...(phase.visual_action.to_y != null ? { to_y: scale(phase.visual_action.to_y) } : {}),
168
+ ...(phase.visual_action.from_y != null ? { from_y: scale(phase.visual_action.from_y) } : {}),
169
+ }
170
+ : phase.visual_action;
171
+ return {
172
+ ...phase,
173
+ ...(phase.target_y != null ? { target_y: scale(phase.target_y) } : {}),
174
+ ...(phase.to_y != null ? { to_y: scale(phase.to_y) } : {}),
175
+ ...(phase.from_y != null ? { from_y: scale(phase.from_y) } : {}),
176
+ ...(scaledVisualAction !== phase.visual_action ? { visual_action: scaledVisualAction } : {}),
177
+ };
178
+ }
179
+
160
180
  export async function recordUrlNarration({
161
181
  plan,
162
182
  output_path,
@@ -167,6 +187,7 @@ export async function recordUrlNarration({
167
187
  viewport = DEFAULT_VIEWPORT,
168
188
  fps = DEFAULT_FPS,
169
189
  settle_ms = 4000,
190
+ page_zoom = 1.1,
170
191
  displayPool = defaultDisplayPool,
171
192
  ffmpegDurationBufferSec = 8,
172
193
  startupProbeMs = 1200,
@@ -174,7 +195,9 @@ export async function recordUrlNarration({
174
195
  xvfbStopTimeoutMs = 5000,
175
196
  postPlanTailMs = 600,
176
197
  } = {}) {
177
- const phases = normalizePlanPhases(plan);
198
+ const zoom = Number.isFinite(Number(page_zoom)) && Number(page_zoom) > 0 ? Number(page_zoom) : 1.0;
199
+ const rawPhases = normalizePlanPhases(plan);
200
+ const phases = zoom !== 1.0 ? rawPhases.map(p => scalePhaseY(p, zoom)) : rawPhases;
178
201
  const executablePlan = {
179
202
  ...(plan && typeof plan === 'object' ? plan : {}),
180
203
  phases,
@@ -220,6 +243,13 @@ export async function recordUrlNarration({
220
243
  settleMs: settle_ms,
221
244
  });
222
245
 
246
+ if (zoom !== 1.0) {
247
+ await browserSession.page.evaluate((z) => {
248
+ document.documentElement.style.zoom = String(z);
249
+ }, zoom);
250
+ await browserSession.page.waitForTimeout(300);
251
+ }
252
+
223
253
  const estimatedDurationMs = estimatePlanDurationMs(executablePlan);
224
254
  const estimatedDurationSec = Math.max(
225
255
  5,
@@ -204,6 +204,55 @@ async function extractStructure(page, { binHeight, minBinChars }) {
204
204
  return rangeFromMatches(matches, root.scrollHeight);
205
205
  }
206
206
 
207
+ function trimNoiseFromBottom(range, pageHeight, detectionStrategy) {
208
+ // Site-specific selectors already point to the exact content element — trust them.
209
+ if (detectionStrategy && detectionStrategy.startsWith('site:')) return range;
210
+
211
+ const [coreTop, rawBottom] = range;
212
+ const minContentHeight = 600; // always keep at least 600px of core content
213
+
214
+ // Selectors that reliably identify non-article noise below the body text.
215
+ const noiseSelectors = [
216
+ // Comment sections
217
+ '[class*="comment-list"]', '[class*="comment-wrap"]', '[class*="commentList"]',
218
+ '[id*="comment"]', '[class*="discuss"]', '[class*="reply-list"]',
219
+ // Related / recommended articles
220
+ '[class*="related-read"]', '[class*="related-article"]', '[class*="relatedArticle"]',
221
+ '[class*="recommend"]', '[class*="more-article"]', '[class*="further-reading"]',
222
+ // Article footer / bottom toolbars
223
+ '[class*="article-footer"]', '[class*="post-footer"]', '[class*="article-bottom"]',
224
+ // Social sharing bars that sit below the body
225
+ '[class*="share-bar"]', '[class*="share-wrap"]', '[class*="article-share"]',
226
+ // Generic page footer inside the core element
227
+ 'footer',
228
+ ];
229
+
230
+ const root = document.scrollingElement || document.documentElement;
231
+ let trimBottom = rawBottom;
232
+
233
+ for (const sel of noiseSelectors) {
234
+ try {
235
+ const elements = document.querySelectorAll(sel);
236
+ for (const el of elements) {
237
+ if (!isVisibleEnough(el, 40)) continue;
238
+ const rect = el.getBoundingClientRect();
239
+ const elTop = Math.round(rect.top + root.scrollTop);
240
+ if (
241
+ elTop > coreTop + minContentHeight
242
+ && elTop < trimBottom
243
+ && rect.height > 60
244
+ ) {
245
+ trimBottom = elTop;
246
+ }
247
+ }
248
+ } catch {
249
+ // ignore selector errors for individual sites
250
+ }
251
+ }
252
+
253
+ return [coreTop, trimBottom];
254
+ }
255
+
207
256
  function collectTextBins({ range, coreElement }) {
208
257
  const [rangeTop, rangeBottom] = range;
209
258
  const root = document.scrollingElement || document.documentElement;
@@ -353,6 +402,10 @@ async function extractStructure(page, { binHeight, minBinChars }) {
353
402
  strategy = `${strategy}:range_fixed`;
354
403
  }
355
404
 
405
+ // Trim coreBottom to exclude footer noise (comments, related articles, recommendations)
406
+ // that commonly appear below the article body.
407
+ coreRange = trimNoiseFromBottom(coreRange, totalHeight, strategy);
408
+
356
409
  const bins = collectTextBins({ range: coreRange, coreElement });
357
410
  const focusRange = (() => {
358
411
  const span = coreRange[1] - coreRange[0];
@@ -1480,6 +1480,7 @@ server.tool('record_url_narration',
1480
1480
  }).optional().describe('Default 1080x1920 (mobile portrait). Override only if the plan requires a different shape.'),
1481
1481
  fps: z.number().optional().describe('Default 30. Do not change unless needed.'),
1482
1482
  settle_ms: z.number().optional().describe('Default 4000. Settle wait after navigation before recording starts.'),
1483
+ 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.'),
1483
1484
  },
1484
1485
  async (args) => {
1485
1486
  if (isBlockedCvmaxEditorVideoTool('record_url_narration')) {
@@ -141,6 +141,7 @@ export async function runRecordUrlNarrationTool({
141
141
  viewport: finalInput.viewport,
142
142
  fps: finalInput.fps,
143
143
  settle_ms: finalInput.settle_ms,
144
+ page_zoom: finalInput.page_zoom,
144
145
  });
145
146
 
146
147
  return {