@roll-agent/browser-use-agent 0.7.0 → 0.7.1

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/SKILL.md CHANGED
@@ -29,7 +29,7 @@ metadata:
29
29
 
30
30
  - `zhipin_read_messages(limit?, onlyUnread?, sortBy?)` — 读取消息列表中的候选人,返回姓名、消息摘要,以及 `conversationId` / `candidateId`
31
31
  - `zhipin_open_chat(candidateName?, index?, preferUnread?)` — 打开指定候选人的聊天窗口(按姓名模糊匹配或列表索引)
32
- - `zhipin_get_candidate_info(candidateName?, index?, maxMessages?)` — 提取候选人资料、聊天记录,以及当前选中聊天的 `conversationId` / `candidateId`
32
+ - `zhipin_get_candidate_info(candidateName?, index?, maxMessages?)` — 提取候选人资料、聊天记录,以及当前选中聊天的 `conversationId` / `candidateId`。输出里的 `candidateInfo.communicationPosition`、`candidateInfo.expectedLocation`、`candidateInfo.expectedPosition` 已按“沟通职位 + 最近关注”结构化解析;若 `communicationPosition` 含连字符类分隔符(`-` / `-` / `—` / `–`),则取第一段作为可选 `preferredBrand`,否则不输出该字段
33
33
  - `zhipin_send_reply(signedEnvelope, candidateName?, index?)` — 发送消息。只接受 Reply Authority Service 签发的 `signedEnvelope`;本地会先做 Ed25519 验签、过期检查、重放检查、目标绑定校验和 recruiter 绑定校验。启动期公钥预加载失败时直接前置拒绝,错误指向 `browser_status.replyAuthorityKeysLoaded`
34
34
  - `zhipin_exchange_wechat(candidateName?, index?)` — 换微信。指定 candidateName 会自动打开对应聊天后执行
35
35
  - `zhipin_get_username()` — 获取当前登录的招聘者用户名,返回 `username`(依赖当前 runtime 已跟踪页面;首次使用请先 `open_platform`,已打开但未跟踪页面可先 `list_pages + select_page`,确认登录后如需单独验证 attach,可先调用 `attach_browser_session`)。常用于 recruiter binding 解析和外部通知消息中的账号标识
@@ -52,9 +52,14 @@ metadata:
52
52
  1. `zhipin_read_messages` → 获取未读候选人列表
53
53
  2. `zhipin_open_chat(candidateName)` → 打开某人的聊天
54
54
  3. `zhipin_get_candidate_info` → 查看候选人资料、聊天记录,并拿到 `conversationId` / `candidateId`
55
- 4. `smart-reply-agent.generate_reply(..., target)` → 获取 `suggestedReply + signedEnvelope`
56
- 5. `zhipin_send_reply(signedEnvelope)` → 验签、校验 recruiterBinding 后发送回复
57
- 6. `zhipin_exchange_wechat` → 交换微信(可选)
55
+ 4. `smart-reply-agent.generate_reply` 前,先尝试透传以下信号:
56
+ - 能读到就传:`candidateInfo.communicationPosition`、`candidateInfo.expectedLocation`、`candidateInfo.expectedPosition`
57
+ - 读不到就如实不传
58
+ - `preferredBrand`:仅当 `communicationPosition` 含连字符类分隔符(`-` / `-` / `—` / `–`)时,取第一段透传;没有分隔符就不传
59
+ - 严禁把通用岗位名(如“餐饮兼职服务员”“门店服务员”)或 `zhipin_get_candidate_list.company`(候选人现/前雇主)伪装成 `preferredBrand`
60
+ 5. `smart-reply-agent.generate_reply(..., target)` → 获取 `suggestedReply + signedEnvelope`
61
+ 6. `zhipin_send_reply(signedEnvelope)` → 验签、校验 recruiterBinding 后发送回复
62
+ 7. `zhipin_exchange_wechat` → 交换微信(可选)
58
63
 
59
64
  ## 支持平台
60
65
 
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{defineAgent as e,createAgentLogger as t}from"@roll-agent/sdk";import{BrowserRuntimeConfigSchema as n}from"@roll-agent/browser";import{defineTool as r}from"@roll-agent/sdk";import{z as a}from"zod";import{createAgentLogger as o}from"@roll-agent/sdk";import{BrowserRuntime as i,BrowserContextManager as s,SessionStore as c}from"@roll-agent/browser";var u,d,l,m=!1,g=o("browser-use-agent");async function f(e){u||(m=!1,l=new c(e.sessionsDir),u=new i(e),await u.start(),d=new s(u,l))}function p(){if(!u)throw new Error("BrowserRuntime not initialized. Call initRuntime() first.");return u}function h(){if(!d)throw new Error("BrowserContextManager not initialized. Call initRuntime() first.");return d}function b(){if(!l)throw new Error("SessionStore not initialized. Call initRuntime() first.");return l}function w(e){m=e}function y(){return m}async function x(){const e=d,t=u,n=[];if(d=void 0,u=void 0,l=void 0,m=!1,e){g.info("Closing browser contexts...");try{await e.closeAll()}catch(e){n.push(new Error("Failed to close browser contexts",{cause:e})),g.error(`Failed to close browser contexts: ${e instanceof Error?e.stack??e.message:String(e)}`)}}if(t){g.info("Stopping browser process...");try{await t.stop()}catch(e){n.push(new Error("Failed to stop browser runtime",{cause:e})),g.error(`Failed to stop browser runtime: ${e instanceof Error?e.stack??e.message:String(e)}`)}}if(g.info("Browser runtime shutdown complete"),n.length>0)throw new AggregateError(n,"Browser runtime shutdown failed")}var v=a.object({success:a.boolean(),mode:a.string(),connected:a.boolean()}),S=r({name:"attach_browser_session",description:"调试工具:显式执行一次 connectOverCDP(),仅建立 Playwright Browser 连接,不做页面导航或 DOM 操作。",input:a.object({}),output:v,execute:async(e,t)=>{const n=p();return t.logger.info("Attaching Playwright browser session over CDP"),await n.getBrowser(),{success:!0,mode:n.mode,connected:!0}}});import{defineTool as k}from"@roll-agent/sdk";import{z as A}from"zod";import{BrowserStatusSchema as I}from"@roll-agent/browser";import{createHash as C}from"node:crypto";import{z as E}from"zod";var P=["REPLY_AUTHORITY_KEYS_URL"],_=/^[0-9a-f]{8}$/,q=E.object({present:E.boolean(),fingerprint:E.string().regex(_).optional()}),N=E.record(q);function R(e,t=process.env){return Object.fromEntries(e.map(e=>{const n=t[e];return"string"==typeof n&&n.length>0?[e,{present:!0,fingerprint:z(n)}]:[e,{present:!1}]}))}function z(e){return C("sha256").update(e).digest("hex").slice(0,8)}var M=I.extend({replyAuthorityKeysLoaded:A.boolean(),effectiveEnvSources:N}),B=k({name:"browser_status",description:"查询浏览器运行状态和活跃 session 信息",input:A.object({}),output:M,execute:async(e,t)=>{t.logger.info("Querying browser status");const n=p(),r=h(),a=b(),o=n.isRunning(),{headless:i,mode:s}=n.getConfig(),c=r.getActivePlatforms(),u=[];for(const e of c){const t=r.getPageCount(e),o=r.getCurrentUrl(e);let i=null,s="unknown";if(n.shouldRestoreSessionSnapshot()){const[t,n]=await Promise.all([a.loadCookies(e),a.loadLocalStorage(e)]);i=void 0!==t&&t.length>0||void 0!==n&&Object.keys(n).length>0,s=i?"snapshot":"none"}else n.usesPersistentProfile()&&(i=null,s="profile");u.push({platform:e,pagesOpen:t,currentUrl:o,hasLoginState:i,loginStateSource:s})}return{running:o,headless:i,mode:s,activeSessions:u,replyAuthorityKeysLoaded:y(),effectiveEnvSources:R(P)}}});import{defineTool as $}from"@roll-agent/sdk";import{BrowserPageInfoSchema as T,PlatformSchema as j}from"@roll-agent/browser";import{z as U}from"zod";import{PLATFORMS as O}from"@roll-agent/browser";var L={zhipin:"https://www.zhipin.com",yupao:"https://www.yupao.com"};function F(e){return new URL(L[e]).host}function D(e,t){try{return new URL(e).host.includes(F(t))}catch{return!1}}function W(e){return O.find(t=>D(e,t))}function H(e,t){const n=W(t.url)??null;return{pageId:t.targetId,url:t.url,title:t.title,boundPlatform:e.getBoundPlatformForNativePage(t.targetId)??null,detectedPlatform:n,isSelectedForPlatform:e.isNativePageSelected(t.targetId)}}async function V(e,t){const n=t.url();return{pageId:e.getPageId(t),url:n,title:await t.title().catch(()=>""),boundPlatform:e.getBoundPlatformForPage(t)??null,detectedPlatform:W(n)??null,isSelectedForPlatform:e.isSelectedPageForPlatform(t)}}var Y=U.object({platform:j.optional().describe("可选:仅返回指定平台相关的页面")}),G=U.object({pages:U.array(T)}),K=$({name:"list_pages",description:"通过原生 CDP 列出当前浏览器可见页面及其可选择的 pageId;登录前该值等同于原生 targetId。",input:Y,output:G,execute:async(e,t)=>{const n=h();t.logger.info("Listing browser pages");const r=(await n.listNativePages()).map(e=>H(n,e));return{pages:void 0===e.platform?r:r.filter(t=>t.boundPlatform===e.platform||t.detectedPlatform===e.platform)}}});import{defineTool as J}from"@roll-agent/sdk";import{BrowserPageInfoSchema as Z}from"@roll-agent/browser";import{z as X}from"zod";var Q=X.object({url:X.string().url().describe("要导航到的目标 URL")}),ee=X.object({success:X.boolean(),page:Z}),te=J({name:"navigate_active_tab",description:"将当前激活的浏览器 tab 导航到指定 URL;若 URL 属于已知平台,会自动绑定该平台当前活跃页。",input:Q,output:ee,execute:async(e,t)=>{const n=h();t.logger.info(`Navigating active tab to ${e.url}`);const r=await n.getActivePage();if(!r)throw new Error("No active browser tab detected. Use open_platform or select_page first.");await r.bringToFront().catch(()=>{}),await r.goto(e.url,{waitUntil:"domcontentloaded"});const a=W(r.url());return a?await n.selectAttachedPage(a,n.getPageId(r)):n.clearBindingForPage(r),{success:!0,page:await V(n,r)}}});import{defineTool as ne}from"@roll-agent/sdk";import{BrowserPageInfoSchema as re,PlatformSchema as ae}from"@roll-agent/browser";import{z as oe}from"zod";async function ie(e,t){return(await e.listNativePages()).find(e=>D(e.url,t))}async function se(e,t){const n=await ie(e,t);if(n)return await e.activateNativePage(n.targetId),{page:n,reusedExistingPage:!0};return{page:await e.openNativePage(L[t]),reusedExistingPage:!1}}var ce=oe.object({platform:ae.describe("目标平台:`zhipin` 代表 BOSS直聘,`yupao` 代表鱼泡")}),ue=oe.object({success:oe.boolean(),page:re,reusedExistingTab:oe.boolean()}),de=ne({name:"open_platform",description:"打开并聚焦招聘平台主页,供用户手动登录或后续执行站内操作。",input:ce,output:ue,execute:async(e,t)=>{const{platform:n}=e,r=p(),a=h();t.logger.info(`Opening platform page for ${n}`);const{page:o,reusedExistingPage:i}=await se(r,n);return a.rememberNativePageSelection(n,o),{success:!0,page:H(a,o),reusedExistingTab:i}}});import{defineTool as le}from"@roll-agent/sdk";import{BrowserPageInfoSchema as me,PlatformSchema as ge}from"@roll-agent/browser";import{z as fe}from"zod";var pe=fe.object({platform:ge.describe("要将该页面绑定为当前活跃页的平台"),pageId:fe.string().describe("通过 list_pages 返回的 pageId;登录前就是原生 targetId,登录后仍可作为稳定选择句柄")}),he=fe.object({success:fe.boolean(),page:me}),be=le({name:"select_page",description:"将指定 pageId 绑定为平台当前活跃页,并切换到前台;登录前走原生 CDP target 激活。",input:pe,output:he,execute:async(e,t)=>{const n=h();t.logger.info(`Selecting page ${e.pageId} for ${e.platform}`);const r=await n.selectNativePage(e.platform,e.pageId);return{success:!0,page:H(n,r)}}});import{defineTool as we}from"@roll-agent/sdk";import{z as ye}from"zod";async function xe(e,t=300,n=800){const r=Math.floor(Math.random()*(n-t))+t;await e.waitForTimeout(r)}async function ve(e){const t=Math.random();let n;n=t<.5?800+1200*Math.random():t<.8?500+300*Math.random():t<.95?2e3+2e3*Math.random():4e3+2e3*Math.random(),await e.waitForTimeout(Math.floor(n))}async function Se(e,t){const n=t?.minDistance??50,r=t?.maxDistance??200,a=t?.direction??"both",o=Math.floor(Math.random()*(r-n))+n,i="up"===a?-1:"down"===a||Math.random()>.5?1:-1;await e.evaluate(e=>{window.scrollBy({top:e,behavior:"smooth"})},o*i),await xe(e,200,500)}async function ke(e){if(Math.random()<.8){const t=100+Math.floor(100*Math.random());await e.evaluate(e=>{window.scrollBy({top:e,behavior:"smooth"})},t),await ve(e)}if(Math.random()<.5){const t=(50+Math.floor(100*Math.random()))*(Math.random()>.5?1:-1);await e.evaluate(e=>{window.scrollBy({top:e,behavior:"smooth"})},t)}}function Ae(e=.3){return Math.random()<e}import{setTimeout as Ie}from"node:timers/promises";var Ce="https://www.zhipin.com/web/geek/chat",Ee=".chat-list-wrap, .geek-item",Pe=new Set(["消息"]),_e="data-roll-chat-entry-target",qe="data-roll-chat-item-target";function Ne(e){return e.trim().toLocaleLowerCase("zh-CN")}function Re(e,t){let n=0;for(const r of e)t.includes(r)&&(n+=1);return n}function ze(e,t){if(void 0!==t.index)return e[t.index];const n=t.candidateName;if(!n)return;const r=Ne(n),a=e.filter(e=>e.name.length>0);let o=a.find(e=>Ne(e.name)===r);if(o)return o;if(o=a.find(e=>{const t=Ne(e.name);return t.includes(r)||r.includes(t)}),o)return o;const i=r.length<=2?1:r.length<=4?.75:.6;return a.find(e=>{const t=Ne(e.name);return Re(r,t)>=Math.ceil(Math.min(r.length,t.length)*i)})}function Me(e){return e.includes("/web/geek/chat")||e.includes("/web/chat")}async function Be(e,t=1e4){try{return await e.waitForSelector(Ee,{timeout:t}),!0}catch{return!1}}async function $e(e,t){const n=(await e.listAttachedPages()).find(e=>e!==t&&Me(e.url()));if(n)return await e.selectAttachedPage("zhipin",e.getPageId(n))}async function Te(e,t){await e.evaluate(e=>{document.querySelectorAll(`[${e}]`).forEach(t=>{t.removeAttribute(e)})},t).catch(()=>{})}async function je(e,t){const n=e.locator(t).first();await n.scrollIntoViewIfNeeded(),await n.hover(),await xe(e,200,400),await n.click()}async function Ue(e){const t=await e.evaluate(e=>{const t=e=>{const t=e.getBoundingClientRect();return t.width>0&&t.height>0},n=t=>e.messageLabels.some(e=>t===e||t.includes(e));document.querySelectorAll(`[${e.markerAttr}]`).forEach(t=>{t.removeAttribute(e.markerAttr)});const r=Array.from(document.querySelectorAll('a[href*="/web/geek/chat"], a[href*="/web/chat"]'));for(const n of r)if(t(n))return n.setAttribute(e.markerAttr,"true"),{found:!0,selector:`[${e.markerAttr}="true"]`};const a=Array.from(document.querySelectorAll('a, button, [role="link"], [role="button"], span, div'));for(const r of a){if(n(r.textContent?.trim()??"")&&t(r))return r.setAttribute(e.markerAttr,"true"),{found:!0,selector:`[${e.markerAttr}="true"]`}}return{found:!1}},{markerAttr:_e,messageLabels:[...Pe]});if(!t.found)return!1;try{return await je(e,t.selector),!0}finally{await Te(e,_e)}}function Oe(e){return e instanceof Error&&/ERR_ABORTED/i.test(e.message)}async function Le(e){try{return await e.goto(Ce,{waitUntil:"domcontentloaded"}),!0}catch(t){return!!Oe(t)&&(!!Me(e.url())||await Be(e,2e3))}}async function Fe(e,t){if(!await Ue(t))return!1;if(await Be(t,5e3))return!0;const n=await $e(e,t);return!!n&&await Be(n,5e3)}async function De(e,t){if(Me(t.url())&&await Be(t))return!0;const n=await $e(e,t);if(n&&await Be(n))return!0;if(await Fe(e,t))return!0;if(!await Le(t))return!1;if(await Be(t))return!0;await Ie(300);const r=await $e(e,t);return!!r&&await Be(r,5e3)}async function We(e){return e.evaluate(()=>Array.from(document.querySelectorAll(".geek-item")).map((e,t)=>{const n=e.querySelector('[class*="name"], .nickname, .geek-name, .candidate-name'),r=n?.textContent?.trim()??"",a=e.querySelector(".badge-count"),o=parseInt(a?.textContent?.trim()??"0",10)||0;return{name:r,index:t,hasUnread:o>0||null!==e.querySelector(".red-dot"),unreadCount:o,lastMessageTime:e.querySelector(".time, .time-shadow")?.textContent?.trim()??"",messagePreview:(e.querySelector(".push-text, .chat-last-msg")?.textContent?.trim()??"").slice(0,100)}}))}async function He(e,t){const n=await e.evaluate(e=>{document.querySelectorAll(`[${e.markerAttr}]`).forEach(t=>{t.removeAttribute(e.markerAttr)});const t=Array.from(document.querySelectorAll(".geek-item"))[e.targetIndex];if(!t)return{found:!1};return(t.querySelector(".chat-item-content")??t).setAttribute(e.markerAttr,"true"),{found:!0,selector:`[${e.markerAttr}="true"]`}},{markerAttr:qe,targetIndex:t});if(!n.found)return!1;try{return await je(e,n.selector),!0}finally{await Te(e,qe)}}async function Ve(e,t){if(0===t.length)return void await xe(e,500,900);const n=Ne(t);try{await e.waitForFunction(e=>{const t=[".name-box",".geek-name",".base-name",".chat-user-name"];for(const n of t){const t=document.querySelector(n)?.textContent?.trim();if(!t)continue;const r=t.trim().toLocaleLowerCase("zh-CN");if(r.includes(e)||e.includes(r))return!0}return!1},n,{timeout:5e3})}catch{await xe(e,800,1200)}}async function Ye(e,t,n){if(void 0===n.candidateName&&void 0===n.index)return;if(!await De(e,t))return{found:!1,name:"",index:-1,hasUnread:!1,unreadCount:0,lastMessageTime:"",messagePreview:"",error:"消息列表未加载"};const r=await e.getPage("zhipin"),a=ze(await We(r),n);if(!a){return{found:!1,name:"",index:-1,hasUnread:!1,unreadCount:0,lastMessageTime:"",messagePreview:"",error:`未找到候选人: ${n.candidateName??`index ${n.index}`}`}}return await He(r,a.index)?(await Ve(r,a.name),{...a,found:!0}):{...a,found:!1,error:`打开候选人聊天失败: ${a.name||`index ${a.index}`}`}}var Ge=ye.object({name:ye.string(),conversationId:ye.string(),candidateId:ye.string(),position:ye.string(),time:ye.string(),preview:ye.string(),unreadCount:ye.number(),hasUnread:ye.boolean(),index:ye.number()}),Ke=ye.object({success:ye.boolean(),candidates:ye.array(Ge),total:ye.number(),stats:ye.object({withName:ye.number(),withUnread:ye.number()})}),Je=we({name:"zhipin_read_messages",description:"读取 BOSS直聘未读候选人列表,支持过滤和排序",input:ye.object({limit:ye.number().optional().describe("最多返回条数"),onlyUnread:ye.boolean().default(!0).describe("是否只返回有未读消息的候选人"),sortBy:ye.enum(["time","unreadCount","name"]).default("time")}),output:Ke,execute:async(e,t)=>{const n=e.onlyUnread??!0;t.logger.info(`Reading zhipin messages (limit: ${e.limit??"all"}, onlyUnread: ${n})`);const r=h(),a=await r.getPage("zhipin");if(!await De(r,a))return{success:!1,candidates:[],total:0,stats:{withName:0,withUnread:0}};const o=await r.getPage("zhipin");await ke(o);const i=await o.evaluate(()=>{const e=document.querySelectorAll(".geek-item"),t=[];return e.forEach((e,n)=>{const r=e.getAttribute("data-id")??e.closest('[role="listitem"]')?.getAttribute("key")??"",a=e.getAttribute("data-geek")??e.querySelector("[data-geek]")?.getAttribute("data-geek")??r,o=e.querySelector('[class*="name"], .nickname, .geek-name, .candidate-name');let i=o?.textContent?.trim()??"";if(i.length>10){const e=i.match(/[\u4e00-\u9fa5]{2,4}/);e&&(i=e[0])}const s=e.querySelector(".source-job")?.textContent?.trim()??"",c=e.querySelector(".time, .time-shadow")?.textContent?.trim()??"",u=(e.querySelector(".push-text, .chat-last-msg")?.textContent?.trim()??"").slice(0,100);let d=0;const l=e.querySelector(".badge-count");l&&(d=parseInt(l.textContent?.trim()??"0",10)||0);const m=d>0||null!==e.querySelector(".red-dot");t.push({name:i,conversationId:r,candidateId:a,position:s,time:c,preview:u,unreadCount:d,hasUnread:m,index:n})}),t});let s=n?i.filter(e=>e.hasUnread):i;const c=e.sortBy??"time";"time"===c?s.sort((e,t)=>t.time.localeCompare(e.time)):"unreadCount"===c?s.sort((e,t)=>t.unreadCount-e.unreadCount):"name"===c&&s.sort((e,t)=>e.name.localeCompare(t.name)),void 0!==e.limit&&(s=s.slice(0,e.limit));const u={withName:i.filter(e=>e.name.length>0).length,withUnread:i.filter(e=>e.hasUnread).length};return await Se(o),t.logger.info(`Found ${s.length} candidates (${u.withUnread} with unread)`),{success:!0,candidates:s,total:i.length,stats:u}}});import{defineTool as Ze}from"@roll-agent/sdk";import{z as Xe}from"zod";var Qe=Xe.object({success:Xe.boolean(),candidateName:Xe.string(),index:Xe.number(),hasUnread:Xe.boolean(),unreadCount:Xe.number(),lastMessageTime:Xe.string(),messagePreview:Xe.string(),error:Xe.string().optional()}),et=Ze({name:"zhipin_open_chat",description:"打开指定候选人的聊天窗口(按姓名模糊匹配或索引)",input:Xe.object({candidateName:Xe.string().optional().describe("候选人姓名。若用户说“打开鲁倩的聊天”,这里应提取为“鲁倩”"),index:Xe.number().optional().describe("候选人在列表中的索引"),preferUnread:Xe.boolean().default(!1).describe("优先选择有未读消息的候选人")}),output:Qe,execute:async(e,t)=>{t.logger.info(`Opening chat: name=${e.candidateName??"N/A"}, index=${e.index??"N/A"}`);const n=h(),r=await n.getPage("zhipin");let a={candidateName:e.candidateName,index:e.index};if(e.preferUnread&&void 0===e.candidateName&&void 0===e.index){if(!await De(n,r))return{success:!1,candidateName:"",index:-1,hasUnread:!1,unreadCount:0,lastMessageTime:"",messagePreview:"",error:"消息列表未加载"};const e=await n.getPage("zhipin"),t=(await We(e)).find(e=>e.hasUnread);t&&(a={candidateName:t.name,index:t.index})}const o=await Ye(n,r,a);return o&&o.found?(t.logger.info(`Opened chat with ${o.name} (index: ${o.index})`),{success:!0,candidateName:o.name,index:o.index,hasUnread:o.hasUnread,unreadCount:o.unreadCount,lastMessageTime:o.lastMessageTime,messagePreview:o.messagePreview}):{success:!1,candidateName:e.candidateName??"",index:e.index??-1,hasUnread:!1,unreadCount:0,lastMessageTime:"",messagePreview:"",error:o?.error??`未找到候选人: ${e.candidateName??`index ${e.index}`}`}}});import{defineTool as tt}from"@roll-agent/sdk";import{z as nt}from"zod";var rt=".geek-item.selected";async function at(e){try{await e.waitForSelector(rt,{timeout:5e3})}catch{return null}const t=await e.evaluate(()=>{const e=document.querySelector(".geek-item.selected");if(!e)return null;const t=e.getAttribute("data-id")??e.closest('[role="listitem"]')?.getAttribute("key")??"";return{conversationId:t,candidateId:e.getAttribute("data-geek")??e.querySelector("[data-geek]")?.getAttribute("data-geek")??t,candidateName:e.querySelector('[class*="name"], .nickname, .geek-name, .candidate-name')?.textContent?.trim()??""}});return t&&"string"==typeof t.conversationId&&"string"==typeof t.candidateId&&0!==t.conversationId.length&&0!==t.candidateId.length?t:null}var ot=nt.object({index:nt.number(),sender:nt.enum(["candidate","recruiter","system"]),messageType:nt.enum(["text","system","resume","wechat-exchange"]),content:nt.string(),time:nt.string()}),it=nt.object({name:nt.string(),age:nt.string(),experience:nt.string(),education:nt.string(),communicationPosition:nt.string(),expectedPosition:nt.string(),expectedLocation:nt.string(),expectedSalary:nt.string(),tags:nt.array(nt.string())}),st=nt.object({success:nt.boolean(),conversationId:nt.string(),candidateId:nt.string(),candidateInfo:it,chatMessages:nt.array(ot),formattedHistory:nt.array(nt.string()),stats:nt.object({totalMessages:nt.number(),candidateMessages:nt.number(),recruiterMessages:nt.number(),systemMessages:nt.number()}),error:nt.string().optional()}),ct=tt({name:"zhipin_get_candidate_info",description:"提取候选人资料和完整聊天记录。可指定 candidateName 自动打开对应聊天,或不传则读取当前窗口;例如“查看鲁倩的聊天详情”应提取 candidateName=鲁倩。",input:nt.object({candidateName:nt.string().optional().describe("候选人姓名。若用户说“查看鲁倩的聊天详情”,这里应提取为“鲁倩”"),index:nt.number().optional().describe("候选人在列表中的索引(可选)"),maxMessages:nt.number().default(100).describe("最多返回的消息条数")}),output:st,execute:async(e,t)=>{const n=e.maxMessages??100,r=h(),a=await r.getPage("zhipin"),o=await Ye(r,a,{candidateName:e.candidateName,index:e.index});if(o&&!o.found){return{success:!1,conversationId:"",candidateId:"",candidateInfo:{name:"",age:"",experience:"",education:"",communicationPosition:"",expectedPosition:"",expectedLocation:"",expectedSalary:"",tags:[]},chatMessages:[],formattedHistory:[],stats:{totalMessages:0,candidateMessages:0,recruiterMessages:0,systemMessages:0},error:o.error}}t.logger.info("Extracting candidate info"+(o?` for ${o.name}`:" (current window)"));const i=await r.getPage("zhipin"),s=await at(i);try{await i.waitForSelector(".chat-message-list .message-item, .conversation-message .message-item",{timeout:8e3})}catch{}const c=await i.evaluate(e=>{const t=document.querySelector(".base-info-single-detial, .base-info-content"),n=t?.querySelector(".name-box")?.textContent?.trim()??"",r=t?t.querySelectorAll(":scope > div"):document.querySelectorAll(".geek-info-item, .base-info-item"),a=[];r.forEach(e=>{const t=e.textContent?.trim();t&&a.push(t)});const o=a.join(" "),i=o.match(/(\d{2,3})岁/),s=i?i[1]+"岁":"",c=o.match(/(\d+年(?:以上)?|应届生|在校生)/),u=c?.[1]??"",d=o.match(/(初中|高中|中专|大专|本科|硕士|博士)/)?.[1]??"";let l="";const m=document.querySelector(".position-name");if(m){const e=m.cloneNode(!0);e.querySelectorAll(".popover-wrap, .tooltip-job").forEach(e=>e.remove()),l=e.textContent?.trim()??""}let g="",f="";const p=document.querySelector(".position-item.expect .value.job");if(p){const e=(p.textContent?.trim()??"").split("·").map(e=>e.trim());f=e[0]??"",g=e[1]??""}const h=document.querySelector(".position-item.expect .high-light-orange")?.textContent?.trim()??"",b=[];t&&t.querySelectorAll(".geek-tag, .base-info-item .high-light-boss").forEach(e=>{const t=e.textContent?.trim();t&&!t.includes("更换职位")&&t.length<20&&b.push(t)});const w=document.querySelectorAll(".chat-message-list > .message-item"),y=/\d{1,2}:\d{2}(?::\d{2})?|\d{4}-\d{2}-\d{2}/,x=[];let v=0;return w.forEach(t=>{if(v>=e)return;const n=null!==t.querySelector(".item-friend"),r=null!==t.querySelector(".item-myself"),a=null!==t.querySelector(".item-system"),o=null!==t.querySelector(".item-resume"),i=null!==t.querySelector(".message-dialog-center");let s="system",c="text";n?s="candidate":r?s="recruiter":(a||i)&&(s="system",c="system"),o&&(c="resume");const u=t.querySelector(".message-card-top-wrap, [class*='d-top-text']");if(u){const e=u.textContent??"";(e.includes("微信")||e.includes("WeChat"))&&(c="wechat-exchange")}const d=t.querySelector(".message-time .time, .message-time"),l=(d?.textContent??"").match(y),m=l?l[0]:"";let g="";if("wechat-exchange"===c&&u){const e=u.textContent??"",t=e.match(/\b(\d{8,15})\b/),n=e.match(/微信[::号]*\s*([a-zA-Z0-9_-]{5,20})/);g=t?`[微信号: ${t[1]}]`:n?`[微信号: ${n[1]}]`:"[交换微信]"}else if(u){const e=t.querySelector(".message-card-top-title"),n=t.querySelector(".dialog-content, .message-card-top-text");g=(e?.textContent?.trim()??n?.textContent?.trim()??"").trim()}else{const e=t.querySelector(".text span, .text-content, .text");e&&(g=(e.textContent?.trim()??"").replace(y,"").replace("已读","").trim())}(g||"text"!==c)&&(x.push({index:v,sender:s,messageType:c,content:g,time:m}),v++)}),{candidateInfo:{name:n,age:s,experience:u,education:d,communicationPosition:l,expectedPosition:g,expectedLocation:f,expectedSalary:h,tags:b},messages:x}},n),u=c.messages.filter(e=>"candidate"===e.sender||"recruiter"===e.sender).map(e=>`${"candidate"===e.sender?"求职者":"我"}: ${e.content}`),d={totalMessages:c.messages.length,candidateMessages:c.messages.filter(e=>"candidate"===e.sender).length,recruiterMessages:c.messages.filter(e=>"recruiter"===e.sender).length,systemMessages:c.messages.filter(e=>"system"===e.sender).length};return t.logger.info(`Extracted info for ${c.candidateInfo.name}: ${d.totalMessages} messages`),{success:!0,conversationId:s?.conversationId??"",candidateId:s?.candidateId??"",candidateInfo:c.candidateInfo,chatMessages:c.messages,formattedHistory:u,stats:d}}});import{defineTool as ut}from"@roll-agent/sdk";import{z as dt}from"zod";var lt={login:{notLoggedIn:".header-login-btn"},unread:{container:".chat-list-wrap",listItem:'[role="listitem"]',geekItemWrap:".geek-item-wrap",item:".chat-item",unreadBadge:".badge-count",unreadBadgeNew:".badge-count.badge-count-common-less",unreadBadgeSpan:".badge-count span",unreadDot:".red-dot",figure:".figure",badge:".badge",candidateName:".candidate-name",candidateNameAlt:".chat-item-name",candidateNameSelectors:'[class*="name"], .nickname, .geek-name',candidateNameNew:".geek-name",jobTitle:".source-job",lastMessage:".push-text",lastMessageAlt:".chat-last-msg",messageTime:".time, .time-shadow",clickArea:".chat-item-content",unreadCandidates:".geek-item"},chat:{chatContainer:".chat-container",messageList:".message-list",messageItem:".message-item",messageContent:".message-content",messageText:".text-content",userMessage:".message-right",candidateMessage:".message-left",messageTime:".message-time",senderName:".sender-name",inputBox:".chat-input",inputTextarea:"textarea.chat-input",inputEditorId:"#boss-chat-editor-input",sendButton:".btn-send",conversationEditor:".conversation-editor",submitButton:".submit-content .submit",submitButtonActive:".submit-content .submit.active",submitContent:".submit-content",sendButtonAlt:".conversation-editor .submit-content",sendIcon:".submit-content .icon-send",systemMessage:".system-msg"},chatDetails:{candidateInfoContainer:".base-info-single-container, .base-info-content",candidateName:".name-box, .geek-name, .base-name",candidateInfoItem:".geek-info-item, .base-info-item, .base-info-single-detial > div",candidateTag:".geek-tag, .high-light-boss",communicationPosition:".position-name",communicationPositionAlt:".position-item:not(.expect) .value.high-light-boss",candidateExpectContainer:".position-item.expect",candidateExpectValue:".position-item.expect .value.job",candidateExpectSalary:".position-item.expect .high-light-orange",candidatePosition:".geek-position, .position-name",candidatePositionAlt:".position-content .value, .position-item .value",chatMessageContainer:".conversation-message, .message-list",messageItem:".message-item",messageTime:".message-time .time",messageTextSpan:".text span",systemMessage:".item-system",friendMessage:".item-friend",myMessage:".item-myself",resumeMessage:".item-resume",readStatus:".status-read"},exchangeWechat:{exchangeButtonPath:"#container > div:nth-child(1) > div > div.chat-box > div.chat-container > div.chat-conversation > div.conversation-box > div.conversation-operate > div.toolbar-box > div.toolbar-box-right > div.operate-exchange-left > div:nth-child(3) > span.operate-btn",exchangeButtonFallback:".operate-exchange-left .operate-btn",confirmDialog:".exchange-tooltip",confirmButton:".exchange-tooltip .btn-box .boss-btn-primary.boss-btn",confirmButtonPath:"#container > div:nth-child(1) > div > div.chat-box > div.chat-container > div.chat-conversation > div.conversation-box > div.conversation-operate > div.toolbar-box > div.toolbar-box-right > div.operate-exchange-left > div:nth-child(3) > div > div > span.boss-btn-primary.boss-btn",cancelButton:".exchange-tooltip .btn-box .boss-btn-outline.boss-btn",wechatCard:".message-card-top-wrap",wechatCardAlt:'[class*="d-top-text"]'},username:{primary:".nav-item.nav-logout .user-name",fallbacks:["#header > div > div > div.nav-item.nav-logout > div.top-profile-logout.ui-dropmenu.ui-dropmenu-drop-arrow > div.ui-dropmenu-label > div > span.user-name",".ui-dropmenu-label .user-name",".nav-logout .user-name","#header .user-name",".top-profile .user-name",".nav-user .user-name",".user-name",'[class*="user-name"]','[class*="username"]','[data-qa="user-name"]',".header-user-name","#header .label-name"]},recommend:{iframe:"#recommendFrame",resumeIframe:'iframe[src*="c-resume"]',candidateItem:"[data-geek], .geek-item",candidateName:".name",candidateBaseInfo:".base-info",workExps:".timeline-wrap.work-exps",expectInfo:".row-flex, .timeline-wrap.expect",salaryWrap:".salary-wrap",tagsWrap:".tags-wrap",greetButton:".btn-greet, .op-btn",resumeCanvas:"div#resume > canvas#resume, canvas#resume",closeResumeBtn:".close-btn, .dialog-close",closeResumeBtnV2:".recommendV2 .close-btn"},candidateProfile:{panel:".candidate-info, .resume-info, .geek-info",name:".candidate-info .name, .geek-info .name",age:".candidate-info .age, .geek-info .age",gender:".candidate-info .gender, .geek-info .gender",experience:".candidate-info .experience, .geek-info .work-exp",education:".candidate-info .education, .geek-info .edu",expectedSalary:".candidate-info .salary, .geek-info .expect-salary",expectedPosition:".candidate-info .position, .geek-info .expect-position",activeTime:".candidate-info .active-time, .geek-info .active"}},mt=30,gt=new Set(["招聘规范","消息","首页","推荐牛人","看简历","我的客服","面试","招聘数据","账号权益","升级VIP","职位","职位管理","牛人","公司","数据统计","设置","帮助中心","登录","注册","退出登录","退出","BOSS直聘","下载APP","搜索","发布职位"]);function ft(e){const t=e.trim();return!(0===t.length||t.length>mt)&&(!gt.has(t)&&!/登录|注册|退出|下载|帮助|设置|管理/.test(t))}function pt(e){const t=[],n=/(link|button|menuitem|img|heading)\s+"([^"]+)"/g;let r;for(;null!==(r=n.exec(e));){const e=r[2]?.trim()??"";e.length>0&&t.push({role:r[1]??"",name:e})}return t}function ht(e){let t=e.priority;return gt.has(e.text.trim())&&(t+=10),e.text.trim().length>10&&(t+=5),/^[\u4e00-\u9fff]{2,4}$/.test(e.text.trim())&&(t-=.5),void 0!==e.xRatio&&(t-=4*(e.xRatio-.5)),t}function bt(e){const t=e.filter(e=>ft(e.text));if(0===t.length)return{found:!1};const n=new Map;for(const e of t){const t=e.text.trim(),r=n.get(t)??new Set;r.add(e.strategy),n.set(t,r)}const r=t.map(e=>{let t=ht(e);const r=n.get(e.text.trim())?.size??1;return r>1&&(t-=.5*r),{evidence:e,score:t}}).sort((e,t)=>e.score-t.score)[0];return r?{found:!0,username:r.evidence.text.trim(),strategy:r.evidence.strategy,source:r.evidence.source}:{found:!1}}async function wt(e){const t=[e.getByRole("banner"),e.locator("header").first(),e.getByRole("navigation").first(),e.locator("#header")];for(const e of t)try{if(await e.count()>0&&await e.first().isVisible())return e.first()}catch{}return null}async function yt(e,t,n,r){const a=[],o=t.viewportSize(),i=o?.width??1280;try{const t=await e.getByRole(n).all();for(const e of t)try{if(!await e.isVisible())continue;const t=(await e.textContent()??"").trim();if(t.length>0&&t.length<=mt){let o;try{const t=await e.boundingBox();t&&(o=(t.x+t.width/2)/i)}catch{}a.push({text:t,strategy:r,priority:1,source:`role:${n}`,xRatio:o})}}catch{}}catch{}return a}async function xt(e){const t=[];try{const n=pt(await e.ariaSnapshot({timeout:3e3}));for(const{role:e,name:r}of n)r.length>0&&r.length<=mt&&t.push({text:r,strategy:"aria-snapshot",priority:2,source:`aria:${e}:${r}`})}catch{}return t}async function vt(e){const t=[];try{const n=await e.evaluate((e,t)=>{const n=[],r=document.createTreeWalker(e,NodeFilter.SHOW_TEXT);for(;r.nextNode();){const e=r.currentNode.textContent?.trim();e&&e.length>0&&e.length<=t&&n.push(e)}return n},mt);for(const e of n)t.push({text:e,strategy:"leaf-text",priority:3,source:"leaf-text"})}catch{}return t}async function St(e){const t=[];try{const n=[lt.username.primary,...lt.username.fallbacks],r=await e.evaluate(e=>e.map(e=>{try{return{selector:e,text:document.querySelector(e)?.textContent?.trim()??""}}catch{return{selector:e,text:""}}}),n);for(const{selector:e,text:n}of r)n.length>0&&n.length<=mt&&t.push({text:n,strategy:"css-fallback",priority:4,source:e})}catch{}return t}async function kt(e){const t=await wt(e),n=t?(await Promise.all([yt(t,e,"link","role-link"),yt(t,e,"button","role-button"),xt(t)])).flat():[],r=bt(n);if(r.found){const e=r.username;if(new Set(n.filter(t=>t.text.trim()===e).map(e=>e.strategy)).size>=2)return n}const a=(await Promise.all([t?vt(t):Promise.resolve([]),St(e)])).flat();return[...n,...a]}async function At(e,t=kt){const n=bt(await t(e));if(!n.found)throw new Error("未找到用户名,请确认当前页面已登录招聘者账号。");return{platform:"zhipin",username:n.username,strategy:n.strategy,source:n.source}}function It(e,t){return void 0!==t.accountId?e.accountId===t.accountId:e.username===t.username}import{z as Ct}from"zod";var Et="reply-authority-service",Pt="browser-use-agent/zhipin_send_reply",_t="zhipin",qt=60,Nt=Ct.object({platform:Ct.literal(_t),username:Ct.string().min(1),accountId:Ct.string().min(1).optional()}),Rt=Ct.object({v:Ct.literal(2),iss:Ct.literal(Et),kid:Ct.string().min(1),jti:Ct.string().min(1),iat:Ct.number().int(),exp:Ct.number().int(),aud:Ct.literal(Pt),platform:Ct.literal(_t),tenantId:Ct.string().min(1),conversationId:Ct.string().min(1),candidateId:Ct.string().min(1),reply:Ct.string(),policyVersion:Ct.string().min(1),recruiterBinding:Nt}),zt=Ct.object({kid:Ct.string().min(1),algorithm:Ct.literal("Ed25519"),publicKey:Ct.string().min(1),validFrom:Ct.string().min(1),validUntil:Ct.string().optional()}),Mt=Ct.object({keys:Ct.array(zt)}),Bt=new Map;function $t(e){for(const[t,n]of Bt)n<e-qt&&Bt.delete(t)}function Tt(e,t=Math.floor(Date.now()/1e3)){return $t(t),Bt.has(e)}function jt(e,t,n=Math.floor(Date.now()/1e3)){$t(n),Bt.set(e,t)}import{createPublicKey as Ut,verify as Ot}from"node:crypto";var Lt=new Map,Ft=null;function Dt(){const e=process.env.REPLY_AUTHORITY_KEYS_URL?.trim();if(!e)throw new Error("REPLY_AUTHORITY_KEYS_URL 未配置,browser-use-agent 无法拉取 Reply Authority 公钥。");return e}function Wt(e){return Lt=new Map(e.map(e=>[e.kid,e]))}async function Ht(){if(Ft)return Ft;Ft=(async()=>{const e=await fetch(Dt()),t=await e.json();if(!e.ok)throw new Error(`Reply Authority 公钥拉取失败 (${e.status})`);return Wt(Mt.parse(t).keys)})();try{return await Ft}finally{Ft=null}}async function Vt(){await Ht()}async function Yt(e){const t=Lt.get(e);if(t)return t;const n=(await Ht()).get(e);if(!n)throw new Error(`Unknown key ID: ${e}`);return n}function Gt(e){if("object"==typeof e&&null!==e&&"v"in e&&"number"==typeof e.v&&2!==e.v)throw new Error("unexpected envelope version")}function Kt(e){const t=e.split("."),n=t[0],r=t[1];if(2!==t.length||void 0===n||void 0===r||0===n.length||0===r.length)throw new Error("Invalid signed envelope format");let a,o="";try{o=Buffer.from(n,"base64url").toString("utf-8")}catch{throw new Error("Invalid signed envelope format")}try{a=JSON.parse(o)}catch{throw new Error("Envelope payload schema validation failed")}Gt(a);const i=Rt.safeParse(a);if(!i.success)throw new Error("Envelope payload schema validation failed");return{payload:i.data,payloadJson:o,signatureBase64:r}}function Jt(e,t){if(e.exp<=e.iat)throw new Error("Envelope expiry must be after issue time");if(e.exp<t-qt)throw new Error("Envelope expired");if(e.iat>t+qt)throw new Error("Envelope issued in the future")}async function Zt(e,t=Math.floor(Date.now()/1e3)){const n=Kt(e),r=await Yt(n.payload.kid),a=Ut({key:Buffer.from(r.publicKey,"base64url"),format:"der",type:"spki"});if(!Ot(null,Buffer.from(n.payloadJson,"utf-8"),a,Buffer.from(n.signatureBase64,"base64url")))throw new Error("Signature verification failed");return Jt(n.payload,t),n.payload}var Xt=dt.object({success:dt.boolean(),sentMessage:dt.string(),error:dt.string().optional()}),Qt=ut({name:"zhipin_send_reply",description:"发送消息。只接受由 Reply Authority Service 签发的 signedEnvelope;可指定 candidateName 自动打开对应聊天后发送,或不传则发送到当前选中的聊天窗口。",input:dt.object({signedEnvelope:dt.string().describe("Reply Authority Service 返回的紧凑签名信封"),candidateName:dt.string().optional().describe("候选人姓名。若用户说“回复鲁倩”,这里应提取为“鲁倩”"),index:dt.number().optional().describe("候选人在列表中的索引(可选)")}),output:Xt,execute:async(e,t)=>{let n="";if(!y()){const e="Reply Authority 公钥尚未成功预加载,当前无法发送签名回复。请检查启动日志、`REPLY_AUTHORITY_KEYS_URL` 配置,以及 `browser_status.replyAuthorityKeysLoaded`。";return t.logger.error(e),{success:!1,sentMessage:n,error:e}}const r=h(),a=await r.getPage("zhipin");let o=a;try{const i=await Zt(e.signedEnvelope);if(n=i.reply,Tt(i.jti))return{success:!1,sentMessage:n,error:"token 已消费,禁止重放"};const s=await Ye(r,a,{candidateName:e.candidateName,index:e.index});if(s&&!s.found)return{success:!1,sentMessage:n,error:s.error};if(!s){if(!await De(r,a))return{success:!1,sentMessage:n,error:"消息列表未加载"}}o=await r.getPage("zhipin");const c=await at(o);if(!c)return{success:!1,sentMessage:n,error:"未能提取当前聊天的 conversationId/candidateId"};if(c.conversationId!==i.conversationId||c.candidateId!==i.candidateId)return{success:!1,sentMessage:n,error:"发送目标与签名不匹配"};const u=await At(o);if(!It(u,i.recruiterBinding))return{success:!1,sentMessage:n,error:`recruiter 绑定不匹配:当前账号 ${u.username} 与签发时 ${i.recruiterBinding.username} 不一致`};t.logger.info(`Sending message (${n.length} chars) to ${c.candidateName||c.candidateId}`);const d="#boss-chat-editor-input, textarea.chat-input, .chat-input";await o.waitForSelector(d,{timeout:5e3});if(await o.evaluate(e=>{const t=document.querySelector(e);return"true"===t?.getAttribute("contenteditable")},d)){const e=o.locator(d).first();await e.focus(),await o.evaluate(e=>{const t=document.querySelector(e.sel);t&&(t.innerHTML=e.msg.split("\n").map(e=>`<p>${e}</p>`).join(""))},{sel:d,msg:n}),await e.dispatchEvent("input",{bubbles:!0})}else await o.fill(d,n);await xe(o,200,500);const l=await o.evaluate(()=>{document.querySelectorAll("[data-roll-send-btn]").forEach(e=>{e.removeAttribute("data-roll-send-btn")});const e=[".submit-content .submit.active",".submit-content .submit",".submit-content",".btn-send"];for(const t of e){const e=document.querySelector(t);if(e&&e.offsetWidth>0)return{found:!0,selector:t}}const t=Array.from(document.querySelectorAll("span"));for(const e of t)if("发送"===e.textContent?.trim()&&e.offsetWidth>0)return e.setAttribute("data-roll-send-btn","true"),{found:!0,selector:'[data-roll-send-btn="true"]'};return{found:!1}});if(!l.found)return{success:!1,sentMessage:n,error:"未找到发送按钮"};const m=o.locator(l.selector).first();return await m.scrollIntoViewIfNeeded(),await m.hover(),await ve(o),await m.click(),await xe(o,500,1200),jt(i.jti,i.exp),t.logger.info("Message sent successfully"),{success:!0,sentMessage:n}}catch(e){return{success:!1,sentMessage:n,error:e instanceof Error?e.message:String(e)}}finally{await o.evaluate(()=>{document.querySelectorAll("[data-roll-send-btn]").forEach(e=>{e.removeAttribute("data-roll-send-btn")})}).catch(()=>{})}}});import{defineTool as en}from"@roll-agent/sdk";import{z as tn}from"zod";var nn=tn.object({success:tn.boolean(),exchanged:tn.boolean(),wechatNumber:tn.string().optional(),error:tn.string().optional()}),rn=en({name:"zhipin_exchange_wechat",description:'换微信。可指定 candidateName 自动打开对应聊天后执行,或不传则在当前窗口执行;例如"和鲁倩换微信"应提取 candidateName=鲁倩。',input:tn.object({candidateName:tn.string().optional().describe('候选人姓名。若用户说"和鲁倩换微信",这里应提取为"鲁倩"'),index:tn.number().optional().describe("候选人在列表中的索引(可选)")}),output:nn,execute:async(e,t)=>{const n=h(),r=await n.getPage("zhipin"),a=await Ye(n,r,{candidateName:e.candidateName,index:e.index});if(a&&!a.found)return{success:!1,exchanged:!1,error:a.error};t.logger.info("Starting WeChat exchange"+(a?` with ${a.name}`:""));const o=await n.getPage("zhipin");try{if(!(await o.evaluate(()=>{const e=e=>{const t=e.getBoundingClientRect();return t.width>0&&t.height>0},t=[".operate-exchange-left .operate-btn","span.operate-btn"];for(const n of t){const t=Array.from(document.querySelectorAll(n));for(const n of t){const t=n.textContent?.trim()??"";if(t.includes("换微信")&&e(n))return n.setAttribute("data-roll-wechat-btn","true"),{found:!0,text:t}}}const n=Array.from(document.querySelectorAll("span"));for(const t of n){const n=t.textContent?.trim()??"";if(n.includes("换微信")&&e(t))return t.setAttribute("data-roll-wechat-btn","true"),{found:!0,text:n}}return{found:!1}})).found)return{success:!1,exchanged:!1,error:"未找到「换微信」按钮"};await xe(o,200,400),await o.click('[data-roll-wechat-btn="true"]'),await o.evaluate(()=>{document.querySelector("[data-roll-wechat-btn]")?.removeAttribute("data-roll-wechat-btn")}),await xe(o,400,800);let e=!1;for(let t=0;t<8;t++){if(await o.evaluate(()=>{const e=e=>{const t=e.getBoundingClientRect();return t.width>0&&t.height>0},t=document.querySelector(".exchange-tooltip");if(t&&e(t))return!0;const n=document.querySelectorAll("div, section, aside");for(const t of Array.from(n)){if((t.textContent??"").includes("交换微信")&&t.querySelector(".boss-btn-primary, .boss-btn")&&e(t))return!0}return!1})){e=!0;break}await xe(o,400,800)}if(!e)return{success:!1,exchanged:!1,error:"确认对话框未弹出"};await ve(o);if(!(await o.evaluate(()=>{const e=e=>{const t=e.getBoundingClientRect();return t.width>0&&t.height>0},t=document.querySelector(".exchange-tooltip");if(t){const n=[".btn-box .boss-btn-primary.boss-btn",".btn-box span.boss-btn-primary","span.boss-btn-primary",".boss-btn-primary"];for(const r of n){const n=t.querySelector(r);if(n&&e(n))return n.setAttribute("data-roll-confirm-btn","true"),{found:!0,text:n.textContent?.trim()??""}}}const n=document.querySelectorAll("div, section, aside");for(const t of Array.from(n)){if(!(t.textContent??"").includes("交换微信"))continue;const n=t.querySelectorAll("span.boss-btn-primary, button.boss-btn-primary, span.boss-btn, button.boss-btn");for(const t of Array.from(n)){const n=t.textContent?.trim()??"";if("确定"===n&&e(t))return t.setAttribute("data-roll-confirm-btn","true"),{found:!0,text:n}}}return{found:!1}})).found)return{success:!1,exchanged:!1,error:"未找到确认按钮"};await xe(o,200,400),await o.click('[data-roll-confirm-btn="true"]'),await o.evaluate(()=>{document.querySelector("[data-roll-confirm-btn]")?.removeAttribute("data-roll-confirm-btn")}),await xe(o,1500,2500);const n=await o.evaluate(()=>{const e=[".message-card-top-wrap",'[class*="d-top-text"]',".message-card-top-title"];for(const t of e){const e=Array.from(document.querySelectorAll(t));for(let t=e.length-1;t>=0;t--){const n=e[t]?.textContent??"",r=n.match(/\b(\d{8,15})\b/);if(r)return r[1];const a=n.match(/微信[::号]*\s*([a-zA-Z0-9_-]{5,20})/);if(a)return a[1];const o=n.match(/\b([a-zA-Z][a-zA-Z0-9_-]{5,19})\b/);if(o&&!["微信","WeChat"].includes(o[1]))return o[1]}}const t=Array.from(document.querySelectorAll(".message-item"));for(let e=t.length-1;e>=0;e--){const n=t[e]?.querySelector('.message-card-top-wrap, [class*="d-top-text"]');if(n){const e=(n.textContent??"").match(/\b(\d{8,15})\b/);if(e)return e[1]}}return null});return t.logger.info("WeChat exchanged"+(n?`, number: ${n}`:"")),{success:!0,exchanged:!0,...null!==n?{wechatNumber:n}:{}}}catch(e){return{success:!1,exchanged:!1,error:e instanceof Error?e.message:String(e)}}}});import{defineTool as an}from"@roll-agent/sdk";import{z as on}from"zod";var sn=on.object({success:on.boolean(),username:on.string(),usedSelector:on.string().optional(),usedStrategy:on.string().optional(),source:on.string().optional(),error:on.string().optional()}),cn=an({name:"zhipin_get_username",description:"获取当前登录的招聘者用户名",input:on.object({}),output:sn,execute:async(e,t)=>{t.logger.info("Getting zhipin username");try{const e=h(),n=await e.getPage("zhipin");await n.bringToFront().catch(()=>{});const r=await At(n);return t.logger.info(`Username: ${r.username} (strategy: ${r.strategy}, source: ${r.source})`),{success:!0,username:r.username,usedSelector:"css-fallback"===r.strategy?r.source:void 0,usedStrategy:r.strategy,source:r.source}}catch(e){return{success:!1,username:"",error:e instanceof Error?`获取用户名失败:${e.message}`:"获取用户名失败"}}}});import{defineTool as un}from"@roll-agent/sdk";import{z as dn}from"zod";var ln=dn.object({index:dn.number(),candidateId:dn.string(),name:dn.string(),age:dn.string(),experience:dn.string(),education:dn.string(),workStatus:dn.string(),company:dn.string(),currentPosition:dn.string(),expectedLocation:dn.string(),expectedPosition:dn.string(),expectedSalary:dn.string(),tags:dn.array(dn.string()),buttonText:dn.string()}),mn=dn.object({success:dn.boolean(),candidates:dn.array(ln),total:dn.number(),error:dn.string().optional()}),gn=un({name:"zhipin_get_candidate_list",description:"获取推荐列表页的候选人卡片信息",input:dn.object({maxResults:dn.number().optional().describe("最多返回条数")}),output:mn,execute:async(e,t)=>{t.logger.info("Getting candidate list from recommend page");const n=h(),r=await n.getPage("zhipin"),a=r.frame("recommendFrame")??r.frames().find(e=>e.url().includes("recommend"))??r;try{await a.waitForSelector("[data-geek], .geek-item",{timeout:1e4})}catch{return{success:!1,candidates:[],total:0,error:"推荐列表未加载"}}const o=await a.evaluate(e=>{let t=Array.from(document.querySelectorAll(".candidate-card-wrap"));0===t.length&&(t=Array.from(document.querySelectorAll("[data-geek], .geek-item")));const n=e??t.length,r=[];return t.forEach((e,t)=>{if(t>=n)return;const a=e.getAttribute("data-geek")??e.querySelector("[data-geek]")?.getAttribute("data-geek")??"",o=e.querySelector(".name")?.textContent?.trim()??"";let i="",s="",c="",u="";const d=e.querySelector(".base-info.join-text-wrap, .base-info");if(d){const e=[];if(d.querySelectorAll(":scope > *").forEach(t=>{const n=t.textContent?.trim();n&&e.push(n)}),e.length<=1&&(e.length=0,d.childNodes.forEach(t=>{if(t.nodeType===Node.TEXT_NODE){const n=t.textContent?.trim();n&&e.push(n)}})),e.length<=1){const t=d.textContent?.trim()??"";e.length=0,t.split(/[丨·|]/).forEach(t=>{const n=t.trim();n&&e.push(n)})}for(const t of e)!i&&t.includes("岁")?i=t:!s&&(t.includes("年")||t.includes("应届")||t.includes("在校"))?s=t:!c&&/(初中|高中|中专|中技|大专|本科|硕士|博士)/.test(t)?c=t:!u&&/(在职|离职|在校)/.test(t)&&(u=t)}const l=e.querySelector(".timeline-wrap.work-exps .content.join-text-wrap")??e.querySelector(".timeline-wrap.work-exps .content"),m=(l?.textContent?.trim()??"").split("·").map(e=>e.trim()),g=m[0]??"",f=m[1]??"";let p="",h="";const b=e.querySelector(".row-flex:not(.geek-desc)");if(b){const e=b.querySelector(".label"),t=b.querySelector(".content"),n=e?.textContent??"";if((n.includes("期望")||n.includes("最近关注"))&&t){const e=(t.textContent?.trim()??"").split("·").map(e=>e.trim());p=e[0]??"",h=e[1]??""}}if(!p){const t=e.querySelector(".timeline-wrap.expect .content.join-text-wrap")??e.querySelector(".timeline-wrap.expect .content");if(t){const e=(t.textContent?.trim()??"").split("·").map(e=>e.trim());p=e[0]??"",h=e[1]??""}}const w=e.querySelector(".salary-wrap")?.textContent?.trim()??"",y=[];e.querySelectorAll(".tags-wrap .tag-item, .tags-wrap .tag, .tags-wrap span").forEach(e=>{const t=e.textContent?.trim();t&&y.push(t)});const x=e.querySelector("button.btn.btn-greet")?.textContent?.trim()??"";r.push({index:t,candidateId:a,name:o,age:i,experience:s,education:c,workStatus:u,company:g,currentPosition:f,expectedLocation:p,expectedPosition:h,expectedSalary:w,tags:y,buttonText:x})}),r},e.maxResults);return t.logger.info(`Found ${o.length} candidates in recommend list`),{success:!0,candidates:o,total:o.length}}});import{defineTool as fn}from"@roll-agent/sdk";import{z as pn}from"zod";var hn=".candidate-card-wrap",bn="[data-geek], .geek-item",wn=`${hn}, ${bn}`;function yn(e){return e.evaluate(e=>document.querySelectorAll(e.primarySelector).length>0?e.primarySelector:e.fallbackSelector,{primarySelector:hn,fallbackSelector:bn})}function xn(e){return e.frame("recommendFrame")??e.frames().find(e=>e.url().includes("recommend"))??e}async function vn(e,t=1e4){try{return await e.waitForSelector(wn,{timeout:t}),!0}catch{return!1}}async function Sn(e,t){const n=await yn(e),r=e.locator(n);if(await r.count()<=t)return{found:!1,cardSelector:n,candidateId:"",name:"",hasGreetButton:!1,error:"索引超出范围"};const a=r.nth(t),o=await a.evaluate(e=>{const t=e.getAttribute("data-geek")??e.querySelector("[data-geek]")?.getAttribute("data-geek")??"",n=e.querySelector(".name")?.textContent?.trim()??"",r=e.querySelector("button.btn.btn-greet");return{candidateId:t,name:n,hasGreetButton:null!==r&&r.offsetWidth>0}});return{found:!0,cardSelector:n,candidateId:o.candidateId,name:o.name,hasGreetButton:o.hasGreetButton}}var kn=pn.object({index:pn.number(),candidateName:pn.string(),candidateId:pn.string(),success:pn.boolean(),error:pn.string().optional()}),An=pn.object({success:pn.boolean(),results:pn.array(kn),summary:pn.object({total:pn.number(),succeeded:pn.number(),failed:pn.number()})}),In=fn({name:"zhipin_say_hello",description:"在推荐列表页对候选人点击「打招呼」按钮(支持批量)",input:pn.object({indices:pn.array(pn.number()).describe("要打招呼的候选人索引列表")}),output:An,execute:async(e,t)=>{t.logger.info(`Saying hello to ${e.indices.length} candidates`);const n=h(),r=await n.getPage("zhipin"),a=xn(r);if(!await vn(a)){const t=e.indices.map(e=>({index:e,candidateName:"",candidateId:"",success:!1,error:"推荐列表未加载"}));return{success:!1,results:t,summary:{total:t.length,succeeded:0,failed:t.length}}}const o=[];for(const t of e.indices)try{const e=await Sn(a,t);if(e.found)if(e.hasGreetButton){const n=a.locator(e.cardSelector).nth(t).locator("button.btn.btn-greet").first();await n.scrollIntoViewIfNeeded(),await n.hover(),await ve(r),await n.click(),o.push({index:t,candidateName:e.name,candidateId:e.candidateId,success:!0})}else o.push({index:t,candidateName:e.name,candidateId:e.candidateId,success:!1,error:"未找到打招呼按钮"});else o.push({index:t,candidateName:"",candidateId:"",success:!1,...void 0!==e.error?{error:e.error}:{}});await ve(r),Ae(.3)&&await Se(r)}catch(e){o.push({index:t,candidateName:"",candidateId:"",success:!1,error:e instanceof Error?e.message:String(e)})}const i={total:o.length,succeeded:o.filter(e=>e.success).length,failed:o.filter(e=>!e.success).length};return t.logger.info(`Say hello: ${i.succeeded}/${i.total} succeeded`),{success:0===i.failed,results:o,summary:i}}});import{defineTool as Cn}from"@roll-agent/sdk";import{z as En}from"zod";var Pn=En.object({success:En.boolean(),candidateName:En.string(),candidateId:En.string(),error:En.string().optional()}),_n=Cn({name:"zhipin_open_resume",description:"在推荐列表页点击候选人卡片打开简历详情弹窗",input:En.object({index:En.number().describe("候选人在列表中的索引")}),output:Pn,execute:async(e,t)=>{t.logger.info(`Opening resume for candidate at index ${e.index}`);const n=h(),r=await n.getPage("zhipin"),a=xn(r);if(!await vn(a))return{success:!1,candidateName:"",candidateId:"",error:"推荐列表未加载"};const o=await Sn(a,e.index);if(!o.found)return{success:!1,candidateName:"",candidateId:"",error:o.error??`索引 ${e.index} 超出范围`};const i=a.locator(o.cardSelector).nth(e.index),s=await i.locator("[data-geek], .card-inner, .geek-item").count()>0?i.locator("[data-geek], .card-inner, .geek-item").first():i;return await s.scrollIntoViewIfNeeded(),await s.hover(),await xe(r,200,400),await s.click(),await xe(r,1e3,2e3),t.logger.info(`Opened resume for ${o.name}`),{success:!0,candidateName:o.name,candidateId:o.candidateId}}});import{defineTool as qn}from"@roll-agent/sdk";import{z as Nn}from"zod";var Rn=Nn.object({x:Nn.number(),y:Nn.number(),width:Nn.number(),height:Nn.number()}),zn=Nn.object({success:Nn.boolean(),screenshotArea:Rn.optional(),canvasInfo:Nn.object({width:Nn.number(),height:Nn.number()}).optional(),error:Nn.string().optional()}),Mn=qn({name:"zhipin_locate_resume_canvas",description:"定位简历详情中嵌套 iframe 内的 canvas 元素坐标(用于截图)",input:Nn.object({}),output:zn,execute:async(e,t)=>{t.logger.info("Locating resume canvas in nested iframes");const n=h(),r=await n.getPage("zhipin");try{const e=r.frame("recommendFrame")??r.frames().find(e=>e.url().includes("recommend"));if(!e)return{success:!1,error:"未找到推荐页 iframe"};const n=await e.$('iframe[src*="c-resume"]');if(!n)return{success:!1,error:"未找到简历 iframe"};const a=await n.contentFrame();if(!a)return{success:!1,error:"无法访问简历 iframe 内容"};try{await a.waitForSelector("canvas#resume, div#resume canvas",{timeout:5e3})}catch{return{success:!1,error:"简历 canvas 未加载"}}const o=await a.evaluate(()=>{const e=document.querySelector("canvas#resume, div#resume canvas");if(!e)return null;const t=e.getBoundingClientRect();return{width:e.width,height:e.height,clientWidth:t.width,clientHeight:t.height,x:t.x,y:t.y}});if(!o)return{success:!1,error:"无法获取 canvas 信息"};const i=await r.evaluate(()=>{const e=document.querySelector("#recommendFrame");if(!e)return null;const t=e.getBoundingClientRect();return{x:t.x,y:t.y}}),s=await e.evaluate(()=>{const e=document.querySelector('iframe[src*="c-resume"]');if(!e)return null;const t=e.getBoundingClientRect();return{x:t.x,y:t.y}}),c=(i?.x??0)+(s?.x??0),u=(i?.y??0)+(s?.y??0);return t.logger.info(`Canvas located at (${c+o.x}, ${u+o.y})`),{success:!0,screenshotArea:{x:Math.round(c+o.x),y:Math.round(u+o.y),width:Math.round(o.clientWidth),height:Math.round(o.clientHeight)},canvasInfo:{width:o.width,height:o.height}}}catch(e){return{success:!1,error:e instanceof Error?e.message:String(e)}}}});import{defineTool as Bn}from"@roll-agent/sdk";import{z as $n}from"zod";var Tn=$n.object({success:$n.boolean(),closed:$n.boolean(),error:$n.string().optional()}),jn=[".recommendV2 .boss-popup__close",".dialog-lib-resume .boss-popup__close",".boss-dialog .boss-popup__close",".boss-popup__close",".close-btn",".dialog-close"],Un=[".boss-popup__close",".close-btn",".dialog-close",".modal-close"],On=Bn({name:"zhipin_close_resume",description:"关闭简历详情弹窗",input:$n.object({}),output:Tn,execute:async(e,t)=>{t.logger.info("Closing resume detail modal");const n=h(),r=await n.getPage("zhipin"),a=r.frame("recommendFrame")??r.frames().find(e=>e.url().includes("recommend"));if(!await(async()=>{if(a)for(const e of jn){const t=await a.$(e);if(t&&await t.isVisible())return await t.click(),!0}for(const e of Un){const t=await r.$(e);if(t&&await t.isVisible())return await t.click(),!0}return!1})())return{success:!1,closed:!1,error:"未找到关闭按钮"};let o=!1;for(let e=0;e<5;e++){await r.waitForTimeout(300);const e=a?await a.$(".boss-popup__wrapper, .dialog-lib-resume, .boss-dialog"):await r.$(".boss-popup__wrapper");if(!e||!await e.isVisible()){o=!0;break}}return t.logger.info(o?"Resume modal closed and verified":"Resume modal close unverified"),{success:!0,closed:!0}}});import{defineTool as Ln}from"@roll-agent/sdk";import{z as Fn}from"zod";import{waitForSelector as Dn}from"@roll-agent/browser";var Wn={login:{qrCode:".login-qr img, .qr-code img",loginSuccess:".user-info, .header-user"},messageList:{container:".chat-list, .msg-list",item:".chat-item, .msg-item",candidateName:".chat-item .name, .msg-item .name",lastMessage:".chat-item .msg, .msg-item .content",unreadBadge:".chat-item .unread, .msg-item .badge",timestamp:".chat-item .time, .msg-item .time"},chat:{input:".chat-input textarea, .msg-input textarea",sendButton:".btn-send, .send-btn",messageItem:".chat-msg, .msg-bubble",messageText:".chat-msg .text, .msg-bubble .text"}};import{navigateTo as Hn,waitForSelector as Vn}from"@roll-agent/browser";var Yn="https://www.yupao.com",Gn=`${Yn}/chat`,Kn=`${Yn}/login`;async function Jn(e){e.url().includes("/chat")||await Hn(e,Gn),await Vn(e,Wn.messageList.container,{timeout:15e3})}async function Zn(e,t){const n=`${Yn}/chat?id=${encodeURIComponent(t)}`;await Hn(e,n),await Vn(e,Wn.chat.input,{timeout:15e3})}async function Xn(e,t){await Jn(e),await Dn(e,Wn.messageList.item,{timeout:1e4});const n=Wn.messageList;return await e.$$eval(n.item,(e,t)=>{const n=[],r=t.maxItems?e.slice(0,t.maxItems):e;for(const e of r){const r=e.querySelector(t.sel.candidateName),a=e.querySelector(t.sel.lastMessage),o=e.querySelector(t.sel.unreadBadge),i=e.querySelector(t.sel.timestamp),s=e.getAttribute("data-id")??e.getAttribute("data-conversation-id")??e.querySelector("a")?.getAttribute("href")?.match(/id=([^&]+)/)?.[1]??"";n.push({conversationId:s,candidateName:r?.textContent?.trim()??"",lastMessage:a?.textContent?.trim()??"",unreadCount:parseInt(o?.textContent?.trim()??"0",10)||0,timestamp:i?.textContent?.trim()??""})}return n},{sel:n,maxItems:t})}var Qn=Fn.object({limit:Fn.number().optional().describe("最多返回的消息条数")}),er=Fn.object({conversationId:Fn.string(),candidateName:Fn.string(),lastMessage:Fn.string(),unreadCount:Fn.number(),timestamp:Fn.string()}),tr=Fn.object({messages:Fn.array(er),total:Fn.number()}),nr=Ln({name:"yupao_read_messages",description:"读取鱼泡未读消息列表",input:Qn,output:tr,execute:async(e,t)=>{t.logger.info(`Reading yupao messages (limit: ${e.limit??"all"})`);const n=h(),r=await n.getPage("yupao"),a=await Xn(r,e.limit);return t.logger.info(`Found ${a.length} messages`),{messages:a.map(e=>({...e})),total:a.length}}});import{defineTool as rr}from"@roll-agent/sdk";import{z as ar}from"zod";import{waitForSelector as or,typeText as ir,clickElement as sr}from"@roll-agent/browser";async function cr(e,t,n){try{return await Zn(e,t),await ir(e,Wn.chat.input,n),await sr(e,Wn.chat.sendButton),await or(e,Wn.chat.messageItem,{timeout:5e3}),{success:!0}}catch(e){return{success:!1,error:e instanceof Error?e.message:String(e)}}}var ur=ar.object({conversationId:ar.string().describe("对话 ID"),message:ar.string().describe("要发送的回复消息")}),dr=ar.object({success:ar.boolean(),conversationId:ar.string(),sentMessage:ar.string(),error:ar.string().optional()}),lr=rr({name:"yupao_send_reply",description:"向鱼泡指定对话发送回复消息",input:ur,output:dr,execute:async(e,t)=>{const{conversationId:n,message:r}=e;t.logger.info(`Sending reply to yupao conversation ${n}`);const a=h(),o=await a.getPage("yupao"),i=await cr(o,n,r);return i.success?t.logger.info("Reply sent successfully"):t.logger.error(`Failed to send reply: ${i.error}`),{success:i.success,conversationId:n,sentMessage:r,error:i.error}}}),mr=t("browser-use-agent");function gr(e){if(void 0!==e){if("true"===e)return!0;if("false"===e)return!1;throw new Error(`Expected boolean env value "true" or "false", received "${e}".`)}}function fr(e,t){if(void 0===e)return;const n=Number.parseInt(e,10);if(!Number.isInteger(n))throw new Error(`${t} must be an integer, received "${e}".`);return n}function pr(e){if(void 0===e)return;const t=JSON.parse(e);if(!Array.isArray(t)||!t.every(e=>"string"==typeof e))throw new Error("BROWSER_ARGS_JSON must be a JSON string array.");return t}function hr(){return n.parse({mode:process.env.BROWSER_MODE,headless:gr(process.env.BROWSER_HEADLESS),cdpUrl:process.env.BROWSER_CDP_URL,cdpHost:process.env.BROWSER_CDP_HOST,cdpPort:fr(process.env.BROWSER_CDP_PORT,"BROWSER_CDP_PORT"),channel:process.env.BROWSER_CHANNEL,executablePath:process.env.BROWSER_EXECUTABLE_PATH,userDataDir:process.env.BROWSER_USER_DATA_DIR,args:pr(process.env.BROWSER_ARGS_JSON),sessionsDir:process.env.BROWSER_SESSIONS_DIR})}var br=e({name:"browser-use-agent",tools:[B,K,te,de,be,Je,et,ct,Qt,rn,cn,gn,In,_n,Mn,On,nr,lr,S]},{onShutdown:x});async function wr(){await f(hr());try{await Vt(),w(!0)}catch(e){w(!1),mr.error(`Failed to preload Reply Authority keys during startup; browser_status.replyAuthorityKeysLoaded=false. ${e instanceof Error?e.stack??e.message:String(e)}`)}await br.listen({transport:{type:"http",port:parseInt(process.env.BROWSER_AGENT_PORT??"3100",10),host:process.env.BROWSER_AGENT_HOST??"127.0.0.1"}})}wr().catch(async e=>{mr.error(`Fatal error: ${e instanceof Error?e.stack??e.message:String(e)}`),await x().catch(()=>{}),process.exit(1)});
1
+ import{defineAgent as e,createAgentLogger as t}from"@roll-agent/sdk";import{BrowserRuntimeConfigSchema as n}from"@roll-agent/browser";import{defineTool as a}from"@roll-agent/sdk";import{z as r}from"zod";import{createAgentLogger as o}from"@roll-agent/sdk";import{BrowserRuntime as i,BrowserContextManager as s,SessionStore as c}from"@roll-agent/browser";var d,u,l,m=!1,g=o("browser-use-agent");async function f(e){d||(m=!1,l=new c(e.sessionsDir),d=new i(e),await d.start(),u=new s(d,l))}function p(){if(!d)throw new Error("BrowserRuntime not initialized. Call initRuntime() first.");return d}function h(){if(!u)throw new Error("BrowserContextManager not initialized. Call initRuntime() first.");return u}function b(){if(!l)throw new Error("SessionStore not initialized. Call initRuntime() first.");return l}function w(e){m=e}function y(){return m}async function x(){const e=u,t=d,n=[];if(u=void 0,d=void 0,l=void 0,m=!1,e){g.info("Closing browser contexts...");try{await e.closeAll()}catch(e){n.push(new Error("Failed to close browser contexts",{cause:e})),g.error(`Failed to close browser contexts: ${e instanceof Error?e.stack??e.message:String(e)}`)}}if(t){g.info("Stopping browser process...");try{await t.stop()}catch(e){n.push(new Error("Failed to stop browser runtime",{cause:e})),g.error(`Failed to stop browser runtime: ${e instanceof Error?e.stack??e.message:String(e)}`)}}if(g.info("Browser runtime shutdown complete"),n.length>0)throw new AggregateError(n,"Browser runtime shutdown failed")}var v=r.object({success:r.boolean(),mode:r.string(),connected:r.boolean()}),S=a({name:"attach_browser_session",description:"调试工具:显式执行一次 connectOverCDP(),仅建立 Playwright Browser 连接,不做页面导航或 DOM 操作。",input:r.object({}),output:v,execute:async(e,t)=>{const n=p();return t.logger.info("Attaching Playwright browser session over CDP"),await n.getBrowser(),{success:!0,mode:n.mode,connected:!0}}});import{defineTool as k}from"@roll-agent/sdk";import{z as I}from"zod";import{BrowserStatusSchema as A}from"@roll-agent/browser";import{createHash as P}from"node:crypto";import{z as C}from"zod";var E=["REPLY_AUTHORITY_KEYS_URL"],_=/^[0-9a-f]{8}$/,q=C.object({present:C.boolean(),fingerprint:C.string().regex(_).optional()}),N=C.record(q);function R(e,t=process.env){return Object.fromEntries(e.map(e=>{const n=t[e];return"string"==typeof n&&n.length>0?[e,{present:!0,fingerprint:z(n)}]:[e,{present:!1}]}))}function z(e){return P("sha256").update(e).digest("hex").slice(0,8)}var M=A.extend({replyAuthorityKeysLoaded:I.boolean(),effectiveEnvSources:N}),B=k({name:"browser_status",description:"查询浏览器运行状态和活跃 session 信息",input:I.object({}),output:M,execute:async(e,t)=>{t.logger.info("Querying browser status");const n=p(),a=h(),r=b(),o=n.isRunning(),{headless:i,mode:s}=n.getConfig(),c=a.getActivePlatforms(),d=[];for(const e of c){const t=a.getPageCount(e),o=a.getCurrentUrl(e);let i=null,s="unknown";if(n.shouldRestoreSessionSnapshot()){const[t,n]=await Promise.all([r.loadCookies(e),r.loadLocalStorage(e)]);i=void 0!==t&&t.length>0||void 0!==n&&Object.keys(n).length>0,s=i?"snapshot":"none"}else n.usesPersistentProfile()&&(i=null,s="profile");d.push({platform:e,pagesOpen:t,currentUrl:o,hasLoginState:i,loginStateSource:s})}return{running:o,headless:i,mode:s,activeSessions:d,replyAuthorityKeysLoaded:y(),effectiveEnvSources:R(E)}}});import{defineTool as $}from"@roll-agent/sdk";import{BrowserPageInfoSchema as T,PlatformSchema as j}from"@roll-agent/browser";import{z as L}from"zod";import{PLATFORMS as U}from"@roll-agent/browser";var O={zhipin:"https://www.zhipin.com",yupao:"https://www.yupao.com"};function F(e){return new URL(O[e]).host}function D(e,t){try{return new URL(e).host.includes(F(t))}catch{return!1}}function W(e){return U.find(t=>D(e,t))}function H(e,t){const n=W(t.url)??null;return{pageId:t.targetId,url:t.url,title:t.title,boundPlatform:e.getBoundPlatformForNativePage(t.targetId)??null,detectedPlatform:n,isSelectedForPlatform:e.isNativePageSelected(t.targetId)}}async function V(e,t){const n=t.url();return{pageId:e.getPageId(t),url:n,title:await t.title().catch(()=>""),boundPlatform:e.getBoundPlatformForPage(t)??null,detectedPlatform:W(n)??null,isSelectedForPlatform:e.isSelectedPageForPlatform(t)}}var Y=L.object({platform:j.optional().describe("可选:仅返回指定平台相关的页面")}),G=L.object({pages:L.array(T)}),J=$({name:"list_pages",description:"通过原生 CDP 列出当前浏览器可见页面及其可选择的 pageId;登录前该值等同于原生 targetId。",input:Y,output:G,execute:async(e,t)=>{const n=h();t.logger.info("Listing browser pages");const a=(await n.listNativePages()).map(e=>H(n,e));return{pages:void 0===e.platform?a:a.filter(t=>t.boundPlatform===e.platform||t.detectedPlatform===e.platform)}}});import{defineTool as K}from"@roll-agent/sdk";import{BrowserPageInfoSchema as Z}from"@roll-agent/browser";import{z as X}from"zod";var Q=X.object({url:X.string().url().describe("要导航到的目标 URL")}),ee=X.object({success:X.boolean(),page:Z}),te=K({name:"navigate_active_tab",description:"将当前激活的浏览器 tab 导航到指定 URL;若 URL 属于已知平台,会自动绑定该平台当前活跃页。",input:Q,output:ee,execute:async(e,t)=>{const n=h();t.logger.info(`Navigating active tab to ${e.url}`);const a=await n.getActivePage();if(!a)throw new Error("No active browser tab detected. Use open_platform or select_page first.");await a.bringToFront().catch(()=>{}),await a.goto(e.url,{waitUntil:"domcontentloaded"});const r=W(a.url());return r?await n.selectAttachedPage(r,n.getPageId(a)):n.clearBindingForPage(a),{success:!0,page:await V(n,a)}}});import{defineTool as ne}from"@roll-agent/sdk";import{BrowserPageInfoSchema as ae,PlatformSchema as re}from"@roll-agent/browser";import{z as oe}from"zod";async function ie(e,t){return(await e.listNativePages()).find(e=>D(e.url,t))}async function se(e,t){const n=await ie(e,t);if(n)return await e.activateNativePage(n.targetId),{page:n,reusedExistingPage:!0};return{page:await e.openNativePage(O[t]),reusedExistingPage:!1}}var ce=oe.object({platform:re.describe("目标平台:`zhipin` 代表 BOSS直聘,`yupao` 代表鱼泡")}),de=oe.object({success:oe.boolean(),page:ae,reusedExistingTab:oe.boolean()}),ue=ne({name:"open_platform",description:"打开并聚焦招聘平台主页,供用户手动登录或后续执行站内操作。",input:ce,output:de,execute:async(e,t)=>{const{platform:n}=e,a=p(),r=h();t.logger.info(`Opening platform page for ${n}`);const{page:o,reusedExistingPage:i}=await se(a,n);return r.rememberNativePageSelection(n,o),{success:!0,page:H(r,o),reusedExistingTab:i}}});import{defineTool as le}from"@roll-agent/sdk";import{BrowserPageInfoSchema as me,PlatformSchema as ge}from"@roll-agent/browser";import{z as fe}from"zod";var pe=fe.object({platform:ge.describe("要将该页面绑定为当前活跃页的平台"),pageId:fe.string().describe("通过 list_pages 返回的 pageId;登录前就是原生 targetId,登录后仍可作为稳定选择句柄")}),he=fe.object({success:fe.boolean(),page:me}),be=le({name:"select_page",description:"将指定 pageId 绑定为平台当前活跃页,并切换到前台;登录前走原生 CDP target 激活。",input:pe,output:he,execute:async(e,t)=>{const n=h();t.logger.info(`Selecting page ${e.pageId} for ${e.platform}`);const a=await n.selectNativePage(e.platform,e.pageId);return{success:!0,page:H(n,a)}}});import{defineTool as we}from"@roll-agent/sdk";import{z as ye}from"zod";async function xe(e,t=300,n=800){const a=Math.floor(Math.random()*(n-t))+t;await e.waitForTimeout(a)}async function ve(e){const t=Math.random();let n;n=t<.5?800+1200*Math.random():t<.8?500+300*Math.random():t<.95?2e3+2e3*Math.random():4e3+2e3*Math.random(),await e.waitForTimeout(Math.floor(n))}async function Se(e,t){const n=t?.minDistance??50,a=t?.maxDistance??200,r=t?.direction??"both",o=Math.floor(Math.random()*(a-n))+n,i="up"===r?-1:"down"===r||Math.random()>.5?1:-1;await e.evaluate(e=>{window.scrollBy({top:e,behavior:"smooth"})},o*i),await xe(e,200,500)}async function ke(e){if(Math.random()<.8){const t=100+Math.floor(100*Math.random());await e.evaluate(e=>{window.scrollBy({top:e,behavior:"smooth"})},t),await ve(e)}if(Math.random()<.5){const t=(50+Math.floor(100*Math.random()))*(Math.random()>.5?1:-1);await e.evaluate(e=>{window.scrollBy({top:e,behavior:"smooth"})},t)}}function Ie(e=.3){return Math.random()<e}import{setTimeout as Ae}from"node:timers/promises";var Pe="https://www.zhipin.com/web/geek/chat",Ce=".chat-list-wrap, .geek-item",Ee=new Set(["消息"]),_e="data-roll-chat-entry-target",qe="data-roll-chat-item-target";function Ne(e){return e.trim().toLocaleLowerCase("zh-CN")}function Re(e,t){let n=0;for(const a of e)t.includes(a)&&(n+=1);return n}function ze(e,t){if(void 0!==t.index)return e[t.index];const n=t.candidateName;if(!n)return;const a=Ne(n),r=e.filter(e=>e.name.length>0);let o=r.find(e=>Ne(e.name)===a);if(o)return o;if(o=r.find(e=>{const t=Ne(e.name);return t.includes(a)||a.includes(t)}),o)return o;const i=a.length<=2?1:a.length<=4?.75:.6;return r.find(e=>{const t=Ne(e.name);return Re(a,t)>=Math.ceil(Math.min(a.length,t.length)*i)})}function Me(e){return e.includes("/web/geek/chat")||e.includes("/web/chat")}async function Be(e,t=1e4){try{return await e.waitForSelector(Ce,{timeout:t}),!0}catch{return!1}}async function $e(e,t){const n=(await e.listAttachedPages()).find(e=>e!==t&&Me(e.url()));if(n)return await e.selectAttachedPage("zhipin",e.getPageId(n))}async function Te(e,t){await e.evaluate(e=>{document.querySelectorAll(`[${e}]`).forEach(t=>{t.removeAttribute(e)})},t).catch(()=>{})}async function je(e,t){const n=e.locator(t).first();await n.scrollIntoViewIfNeeded(),await n.hover(),await xe(e,200,400),await n.click()}async function Le(e){const t=await e.evaluate(e=>{const t=e=>{const t=e.getBoundingClientRect();return t.width>0&&t.height>0},n=t=>e.messageLabels.some(e=>t===e||t.includes(e));document.querySelectorAll(`[${e.markerAttr}]`).forEach(t=>{t.removeAttribute(e.markerAttr)});const a=Array.from(document.querySelectorAll('a[href*="/web/geek/chat"], a[href*="/web/chat"]'));for(const n of a)if(t(n))return n.setAttribute(e.markerAttr,"true"),{found:!0,selector:`[${e.markerAttr}="true"]`};const r=Array.from(document.querySelectorAll('a, button, [role="link"], [role="button"], span, div'));for(const a of r){if(n(a.textContent?.trim()??"")&&t(a))return a.setAttribute(e.markerAttr,"true"),{found:!0,selector:`[${e.markerAttr}="true"]`}}return{found:!1}},{markerAttr:_e,messageLabels:[...Ee]});if(!t.found)return!1;try{return await je(e,t.selector),!0}finally{await Te(e,_e)}}function Ue(e){return e instanceof Error&&/ERR_ABORTED/i.test(e.message)}async function Oe(e){try{return await e.goto(Pe,{waitUntil:"domcontentloaded"}),!0}catch(t){return!!Ue(t)&&(!!Me(e.url())||await Be(e,2e3))}}async function Fe(e,t){if(!await Le(t))return!1;if(await Be(t,5e3))return!0;const n=await $e(e,t);return!!n&&await Be(n,5e3)}async function De(e,t){if(Me(t.url())&&await Be(t))return!0;const n=await $e(e,t);if(n&&await Be(n))return!0;if(await Fe(e,t))return!0;if(!await Oe(t))return!1;if(await Be(t))return!0;await Ae(300);const a=await $e(e,t);return!!a&&await Be(a,5e3)}async function We(e){return e.evaluate(()=>Array.from(document.querySelectorAll(".geek-item")).map((e,t)=>{const n=e.querySelector('[class*="name"], .nickname, .geek-name, .candidate-name'),a=n?.textContent?.trim()??"",r=e.querySelector(".badge-count"),o=parseInt(r?.textContent?.trim()??"0",10)||0;return{name:a,index:t,hasUnread:o>0||null!==e.querySelector(".red-dot"),unreadCount:o,lastMessageTime:e.querySelector(".time, .time-shadow")?.textContent?.trim()??"",messagePreview:(e.querySelector(".push-text, .chat-last-msg")?.textContent?.trim()??"").slice(0,100)}}))}async function He(e,t){const n=await e.evaluate(e=>{document.querySelectorAll(`[${e.markerAttr}]`).forEach(t=>{t.removeAttribute(e.markerAttr)});const t=Array.from(document.querySelectorAll(".geek-item"))[e.targetIndex];if(!t)return{found:!1};return(t.querySelector(".chat-item-content")??t).setAttribute(e.markerAttr,"true"),{found:!0,selector:`[${e.markerAttr}="true"]`}},{markerAttr:qe,targetIndex:t});if(!n.found)return!1;try{return await je(e,n.selector),!0}finally{await Te(e,qe)}}async function Ve(e,t){if(0===t.length)return void await xe(e,500,900);const n=Ne(t);try{await e.waitForFunction(e=>{const t=[".name-box",".geek-name",".base-name",".chat-user-name"];for(const n of t){const t=document.querySelector(n)?.textContent?.trim();if(!t)continue;const a=t.trim().toLocaleLowerCase("zh-CN");if(a.includes(e)||e.includes(a))return!0}return!1},n,{timeout:5e3})}catch{await xe(e,800,1200)}}async function Ye(e,t,n){if(void 0===n.candidateName&&void 0===n.index)return;if(!await De(e,t))return{found:!1,name:"",index:-1,hasUnread:!1,unreadCount:0,lastMessageTime:"",messagePreview:"",error:"消息列表未加载"};const a=await e.getPage("zhipin"),r=ze(await We(a),n);if(!r){return{found:!1,name:"",index:-1,hasUnread:!1,unreadCount:0,lastMessageTime:"",messagePreview:"",error:`未找到候选人: ${n.candidateName??`index ${n.index}`}`}}return await He(a,r.index)?(await Ve(a,r.name),{...r,found:!0}):{...r,found:!1,error:`打开候选人聊天失败: ${r.name||`index ${r.index}`}`}}var Ge=ye.object({name:ye.string(),conversationId:ye.string(),candidateId:ye.string(),position:ye.string(),time:ye.string(),preview:ye.string(),unreadCount:ye.number(),hasUnread:ye.boolean(),index:ye.number()}),Je=ye.object({success:ye.boolean(),candidates:ye.array(Ge),total:ye.number(),stats:ye.object({withName:ye.number(),withUnread:ye.number()})}),Ke=we({name:"zhipin_read_messages",description:"读取 BOSS直聘未读候选人列表,支持过滤和排序",input:ye.object({limit:ye.number().optional().describe("最多返回条数"),onlyUnread:ye.boolean().default(!0).describe("是否只返回有未读消息的候选人"),sortBy:ye.enum(["time","unreadCount","name"]).default("time")}),output:Je,execute:async(e,t)=>{const n=e.onlyUnread??!0;t.logger.info(`Reading zhipin messages (limit: ${e.limit??"all"}, onlyUnread: ${n})`);const a=h(),r=await a.getPage("zhipin");if(!await De(a,r))return{success:!1,candidates:[],total:0,stats:{withName:0,withUnread:0}};const o=await a.getPage("zhipin");await ke(o);const i=await o.evaluate(()=>{const e=document.querySelectorAll(".geek-item"),t=[];return e.forEach((e,n)=>{const a=e.getAttribute("data-id")??e.closest('[role="listitem"]')?.getAttribute("key")??"",r=e.getAttribute("data-geek")??e.querySelector("[data-geek]")?.getAttribute("data-geek")??a,o=e.querySelector('[class*="name"], .nickname, .geek-name, .candidate-name');let i=o?.textContent?.trim()??"";if(i.length>10){const e=i.match(/[\u4e00-\u9fa5]{2,4}/);e&&(i=e[0])}const s=e.querySelector(".source-job")?.textContent?.trim()??"",c=e.querySelector(".time, .time-shadow")?.textContent?.trim()??"",d=(e.querySelector(".push-text, .chat-last-msg")?.textContent?.trim()??"").slice(0,100);let u=0;const l=e.querySelector(".badge-count");l&&(u=parseInt(l.textContent?.trim()??"0",10)||0);const m=u>0||null!==e.querySelector(".red-dot");t.push({name:i,conversationId:a,candidateId:r,position:s,time:c,preview:d,unreadCount:u,hasUnread:m,index:n})}),t});let s=n?i.filter(e=>e.hasUnread):i;const c=e.sortBy??"time";"time"===c?s.sort((e,t)=>t.time.localeCompare(e.time)):"unreadCount"===c?s.sort((e,t)=>t.unreadCount-e.unreadCount):"name"===c&&s.sort((e,t)=>e.name.localeCompare(t.name)),void 0!==e.limit&&(s=s.slice(0,e.limit));const d={withName:i.filter(e=>e.name.length>0).length,withUnread:i.filter(e=>e.hasUnread).length};return await Se(o),t.logger.info(`Found ${s.length} candidates (${d.withUnread} with unread)`),{success:!0,candidates:s,total:i.length,stats:d}}});import{defineTool as Ze}from"@roll-agent/sdk";import{z as Xe}from"zod";var Qe=Xe.object({success:Xe.boolean(),candidateName:Xe.string(),index:Xe.number(),hasUnread:Xe.boolean(),unreadCount:Xe.number(),lastMessageTime:Xe.string(),messagePreview:Xe.string(),error:Xe.string().optional()}),et=Ze({name:"zhipin_open_chat",description:"打开指定候选人的聊天窗口(按姓名模糊匹配或索引)",input:Xe.object({candidateName:Xe.string().optional().describe("候选人姓名。若用户说“打开鲁倩的聊天”,这里应提取为“鲁倩”"),index:Xe.number().optional().describe("候选人在列表中的索引"),preferUnread:Xe.boolean().default(!1).describe("优先选择有未读消息的候选人")}),output:Qe,execute:async(e,t)=>{t.logger.info(`Opening chat: name=${e.candidateName??"N/A"}, index=${e.index??"N/A"}`);const n=h(),a=await n.getPage("zhipin");let r={candidateName:e.candidateName,index:e.index};if(e.preferUnread&&void 0===e.candidateName&&void 0===e.index){if(!await De(n,a))return{success:!1,candidateName:"",index:-1,hasUnread:!1,unreadCount:0,lastMessageTime:"",messagePreview:"",error:"消息列表未加载"};const e=await n.getPage("zhipin"),t=(await We(e)).find(e=>e.hasUnread);t&&(r={candidateName:t.name,index:t.index})}const o=await Ye(n,a,r);return o&&o.found?(t.logger.info(`Opened chat with ${o.name} (index: ${o.index})`),{success:!0,candidateName:o.name,index:o.index,hasUnread:o.hasUnread,unreadCount:o.unreadCount,lastMessageTime:o.lastMessageTime,messagePreview:o.messagePreview}):{success:!1,candidateName:e.candidateName??"",index:e.index??-1,hasUnread:!1,unreadCount:0,lastMessageTime:"",messagePreview:"",error:o?.error??`未找到候选人: ${e.candidateName??`index ${e.index}`}`}}});import{defineTool as tt}from"@roll-agent/sdk";import{z as nt}from"zod";var at=".geek-item.selected";async function rt(e){try{await e.waitForSelector(at,{timeout:5e3})}catch{return null}const t=await e.evaluate(()=>{const e=document.querySelector(".geek-item.selected");if(!e)return null;const t=e.getAttribute("data-id")??e.closest('[role="listitem"]')?.getAttribute("key")??"";return{conversationId:t,candidateId:e.getAttribute("data-geek")??e.querySelector("[data-geek]")?.getAttribute("data-geek")??t,candidateName:e.querySelector('[class*="name"], .nickname, .geek-name, .candidate-name')?.textContent?.trim()??""}});return t&&"string"==typeof t.conversationId&&"string"==typeof t.candidateId&&0!==t.conversationId.length&&0!==t.candidateId.length?t:null}var ot="·",it=/[--—–]/;function st(e){return(e??"").trim()}function ct(e){const t=st(e);if(!t||!it.test(t))return;const[n=""]=t.split(it);return st(n)||void 0}function dt(e){const t=st(e);if(!t)return{expectedLocation:"",expectedPosition:""};const[n="",a=""]=t.split(ot).map(e=>st(e));return{expectedLocation:n,expectedPosition:a}}function ut(e){const t=st(e.communicationPosition),{expectedLocation:n,expectedPosition:a}=dt(e.expectedJobText),r=ct(t);return{communicationPosition:t,expectedLocation:n,expectedPosition:a,...void 0!==r?{preferredBrand:r}:{}}}var lt=nt.object({index:nt.number(),sender:nt.enum(["candidate","recruiter","system"]),messageType:nt.enum(["text","system","resume","wechat-exchange"]),content:nt.string(),time:nt.string()}),mt=nt.object({name:nt.string(),age:nt.string(),experience:nt.string(),education:nt.string(),communicationPosition:nt.string(),expectedPosition:nt.string(),expectedLocation:nt.string(),expectedSalary:nt.string(),tags:nt.array(nt.string())}),gt=nt.object({success:nt.boolean(),conversationId:nt.string(),candidateId:nt.string(),candidateInfo:mt,preferredBrand:nt.string().optional(),chatMessages:nt.array(lt),formattedHistory:nt.array(nt.string()),stats:nt.object({totalMessages:nt.number(),candidateMessages:nt.number(),recruiterMessages:nt.number(),systemMessages:nt.number()}),error:nt.string().optional()}),ft=tt({name:"zhipin_get_candidate_info",description:"提取候选人资料和完整聊天记录。可指定 candidateName 自动打开对应聊天,或不传则读取当前窗口;例如“查看鲁倩的聊天详情”应提取 candidateName=鲁倩。",input:nt.object({candidateName:nt.string().optional().describe("候选人姓名。若用户说“查看鲁倩的聊天详情”,这里应提取为“鲁倩”"),index:nt.number().optional().describe("候选人在列表中的索引(可选)"),maxMessages:nt.number().default(100).describe("最多返回的消息条数")}),output:gt,execute:async(e,t)=>{const n=e.maxMessages??100,a=h(),r=await a.getPage("zhipin"),o=await Ye(a,r,{candidateName:e.candidateName,index:e.index});if(o&&!o.found){return{success:!1,conversationId:"",candidateId:"",candidateInfo:{name:"",age:"",experience:"",education:"",communicationPosition:"",expectedPosition:"",expectedLocation:"",expectedSalary:"",tags:[]},chatMessages:[],formattedHistory:[],stats:{totalMessages:0,candidateMessages:0,recruiterMessages:0,systemMessages:0},error:o.error}}t.logger.info("Extracting candidate info"+(o?` for ${o.name}`:" (current window)"));const i=await a.getPage("zhipin"),s=await rt(i);try{await i.waitForSelector(".chat-message-list .message-item, .conversation-message .message-item",{timeout:8e3})}catch{}const c=await i.evaluate(e=>{const t=document.querySelector(".base-info-single-detial, .base-info-content"),n=t?.querySelector(".name-box")?.textContent?.trim()??"",a=t?t.querySelectorAll(":scope > div"):document.querySelectorAll(".geek-info-item, .base-info-item"),r=[];a.forEach(e=>{const t=e.textContent?.trim();t&&r.push(t)});const o=r.join(" "),i=o.match(/(\d{2,3})岁/),s=i?i[1]+"岁":"",c=o.match(/(\d+年(?:以上)?|应届生|在校生)/),d=c?.[1]??"",u=o.match(/(初中|高中|中专|大专|本科|硕士|博士)/)?.[1]??"";let l="";const m=document.querySelector(".position-name");if(m){const e=m.cloneNode(!0);e.querySelectorAll(".popover-wrap, .tooltip-job").forEach(e=>e.remove()),l=e.textContent?.trim()??""}let g="";const f=document.querySelector(".position-item.expect .value.job");f&&(g=f.textContent?.trim()??"");const p=document.querySelector(".position-item.expect .high-light-orange")?.textContent?.trim()??"",h=[];t&&t.querySelectorAll(".geek-tag, .base-info-item .high-light-boss").forEach(e=>{const t=e.textContent?.trim();t&&!t.includes("更换职位")&&t.length<20&&h.push(t)});const b=document.querySelectorAll(".chat-message-list > .message-item"),w=/\d{1,2}:\d{2}(?::\d{2})?|\d{4}-\d{2}-\d{2}/,y=[];let x=0;return b.forEach(t=>{if(x>=e)return;const n=null!==t.querySelector(".item-friend"),a=null!==t.querySelector(".item-myself"),r=null!==t.querySelector(".item-system"),o=null!==t.querySelector(".item-resume"),i=null!==t.querySelector(".message-dialog-center");let s="system",c="text";n?s="candidate":a?s="recruiter":(r||i)&&(s="system",c="system"),o&&(c="resume");const d=t.querySelector(".message-card-top-wrap, [class*='d-top-text']");if(d){const e=d.textContent??"";(e.includes("微信")||e.includes("WeChat"))&&(c="wechat-exchange")}const u=t.querySelector(".message-time .time, .message-time"),l=(u?.textContent??"").match(w),m=l?l[0]:"";let g="";if("wechat-exchange"===c&&d){const e=d.textContent??"",t=e.match(/\b(\d{8,15})\b/),n=e.match(/微信[::号]*\s*([a-zA-Z0-9_-]{5,20})/);g=t?`[微信号: ${t[1]}]`:n?`[微信号: ${n[1]}]`:"[交换微信]"}else if(d){const e=t.querySelector(".message-card-top-title"),n=t.querySelector(".dialog-content, .message-card-top-text");g=(e?.textContent?.trim()??n?.textContent?.trim()??"").trim()}else{const e=t.querySelector(".text span, .text-content, .text");e&&(g=(e.textContent?.trim()??"").replace(w,"").replace("已读","").trim())}(g||"text"!==c)&&(y.push({index:x,sender:s,messageType:c,content:g,time:m}),x++)}),{candidateInfo:{name:n,age:s,experience:d,education:u,communicationPosition:l,expectedJobText:g,expectedSalary:p,tags:h},messages:y}},n),d=ut({communicationPosition:c.candidateInfo.communicationPosition,expectedJobText:c.candidateInfo.expectedJobText}),u=c.messages.filter(e=>"candidate"===e.sender||"recruiter"===e.sender).map(e=>`${"candidate"===e.sender?"求职者":"我"}: ${e.content}`),l={totalMessages:c.messages.length,candidateMessages:c.messages.filter(e=>"candidate"===e.sender).length,recruiterMessages:c.messages.filter(e=>"recruiter"===e.sender).length,systemMessages:c.messages.filter(e=>"system"===e.sender).length};return t.logger.info(`Extracted info for ${c.candidateInfo.name}: ${l.totalMessages} messages`),{success:!0,conversationId:s?.conversationId??"",candidateId:s?.candidateId??"",candidateInfo:{name:c.candidateInfo.name,age:c.candidateInfo.age,experience:c.candidateInfo.experience,education:c.candidateInfo.education,communicationPosition:d.communicationPosition,expectedPosition:d.expectedPosition,expectedLocation:d.expectedLocation,expectedSalary:c.candidateInfo.expectedSalary,tags:c.candidateInfo.tags},...void 0!==d.preferredBrand?{preferredBrand:d.preferredBrand}:{},chatMessages:c.messages,formattedHistory:u,stats:l}}});import{defineTool as pt}from"@roll-agent/sdk";import{z as ht}from"zod";var bt={login:{notLoggedIn:".header-login-btn"},unread:{container:".chat-list-wrap",listItem:'[role="listitem"]',geekItemWrap:".geek-item-wrap",item:".chat-item",unreadBadge:".badge-count",unreadBadgeNew:".badge-count.badge-count-common-less",unreadBadgeSpan:".badge-count span",unreadDot:".red-dot",figure:".figure",badge:".badge",candidateName:".candidate-name",candidateNameAlt:".chat-item-name",candidateNameSelectors:'[class*="name"], .nickname, .geek-name',candidateNameNew:".geek-name",jobTitle:".source-job",lastMessage:".push-text",lastMessageAlt:".chat-last-msg",messageTime:".time, .time-shadow",clickArea:".chat-item-content",unreadCandidates:".geek-item"},chat:{chatContainer:".chat-container",messageList:".message-list",messageItem:".message-item",messageContent:".message-content",messageText:".text-content",userMessage:".message-right",candidateMessage:".message-left",messageTime:".message-time",senderName:".sender-name",inputBox:".chat-input",inputTextarea:"textarea.chat-input",inputEditorId:"#boss-chat-editor-input",sendButton:".btn-send",conversationEditor:".conversation-editor",submitButton:".submit-content .submit",submitButtonActive:".submit-content .submit.active",submitContent:".submit-content",sendButtonAlt:".conversation-editor .submit-content",sendIcon:".submit-content .icon-send",systemMessage:".system-msg"},chatDetails:{candidateInfoContainer:".base-info-single-container, .base-info-content",candidateName:".name-box, .geek-name, .base-name",candidateInfoItem:".geek-info-item, .base-info-item, .base-info-single-detial > div",candidateTag:".geek-tag, .high-light-boss",communicationPosition:".position-name",communicationPositionAlt:".position-item:not(.expect) .value.high-light-boss",candidateExpectContainer:".position-item.expect",candidateExpectValue:".position-item.expect .value.job",candidateExpectSalary:".position-item.expect .high-light-orange",candidatePosition:".geek-position, .position-name",candidatePositionAlt:".position-content .value, .position-item .value",chatMessageContainer:".conversation-message, .message-list",messageItem:".message-item",messageTime:".message-time .time",messageTextSpan:".text span",systemMessage:".item-system",friendMessage:".item-friend",myMessage:".item-myself",resumeMessage:".item-resume",readStatus:".status-read"},exchangeWechat:{exchangeButtonPath:"#container > div:nth-child(1) > div > div.chat-box > div.chat-container > div.chat-conversation > div.conversation-box > div.conversation-operate > div.toolbar-box > div.toolbar-box-right > div.operate-exchange-left > div:nth-child(3) > span.operate-btn",exchangeButtonFallback:".operate-exchange-left .operate-btn",confirmDialog:".exchange-tooltip",confirmButton:".exchange-tooltip .btn-box .boss-btn-primary.boss-btn",confirmButtonPath:"#container > div:nth-child(1) > div > div.chat-box > div.chat-container > div.chat-conversation > div.conversation-box > div.conversation-operate > div.toolbar-box > div.toolbar-box-right > div.operate-exchange-left > div:nth-child(3) > div > div > span.boss-btn-primary.boss-btn",cancelButton:".exchange-tooltip .btn-box .boss-btn-outline.boss-btn",wechatCard:".message-card-top-wrap",wechatCardAlt:'[class*="d-top-text"]'},username:{primary:".nav-item.nav-logout .user-name",fallbacks:["#header > div > div > div.nav-item.nav-logout > div.top-profile-logout.ui-dropmenu.ui-dropmenu-drop-arrow > div.ui-dropmenu-label > div > span.user-name",".ui-dropmenu-label .user-name",".nav-logout .user-name","#header .user-name",".top-profile .user-name",".nav-user .user-name",".user-name",'[class*="user-name"]','[class*="username"]','[data-qa="user-name"]',".header-user-name","#header .label-name"]},recommend:{iframe:"#recommendFrame",resumeIframe:'iframe[src*="c-resume"]',candidateItem:"[data-geek], .geek-item",candidateName:".name",candidateBaseInfo:".base-info",workExps:".timeline-wrap.work-exps",expectInfo:".row-flex, .timeline-wrap.expect",salaryWrap:".salary-wrap",tagsWrap:".tags-wrap",greetButton:".btn-greet, .op-btn",resumeCanvas:"div#resume > canvas#resume, canvas#resume",closeResumeBtn:".close-btn, .dialog-close",closeResumeBtnV2:".recommendV2 .close-btn"},candidateProfile:{panel:".candidate-info, .resume-info, .geek-info",name:".candidate-info .name, .geek-info .name",age:".candidate-info .age, .geek-info .age",gender:".candidate-info .gender, .geek-info .gender",experience:".candidate-info .experience, .geek-info .work-exp",education:".candidate-info .education, .geek-info .edu",expectedSalary:".candidate-info .salary, .geek-info .expect-salary",expectedPosition:".candidate-info .position, .geek-info .expect-position",activeTime:".candidate-info .active-time, .geek-info .active"}},wt=30,yt=new Set(["招聘规范","消息","首页","推荐牛人","看简历","我的客服","面试","招聘数据","账号权益","升级VIP","职位","职位管理","牛人","公司","数据统计","设置","帮助中心","登录","注册","退出登录","退出","BOSS直聘","下载APP","搜索","发布职位"]);function xt(e){const t=e.trim();return!(0===t.length||t.length>wt)&&(!yt.has(t)&&!/登录|注册|退出|下载|帮助|设置|管理/.test(t))}function vt(e){const t=[],n=/(link|button|menuitem|img|heading)\s+"([^"]+)"/g;let a;for(;null!==(a=n.exec(e));){const e=a[2]?.trim()??"";e.length>0&&t.push({role:a[1]??"",name:e})}return t}function St(e){let t=e.priority;return yt.has(e.text.trim())&&(t+=10),e.text.trim().length>10&&(t+=5),/^[\u4e00-\u9fff]{2,4}$/.test(e.text.trim())&&(t-=.5),void 0!==e.xRatio&&(t-=4*(e.xRatio-.5)),t}function kt(e){const t=e.filter(e=>xt(e.text));if(0===t.length)return{found:!1};const n=new Map;for(const e of t){const t=e.text.trim(),a=n.get(t)??new Set;a.add(e.strategy),n.set(t,a)}const a=t.map(e=>{let t=St(e);const a=n.get(e.text.trim())?.size??1;return a>1&&(t-=.5*a),{evidence:e,score:t}}).sort((e,t)=>e.score-t.score)[0];return a?{found:!0,username:a.evidence.text.trim(),strategy:a.evidence.strategy,source:a.evidence.source}:{found:!1}}async function It(e){const t=[e.getByRole("banner"),e.locator("header").first(),e.getByRole("navigation").first(),e.locator("#header")];for(const e of t)try{if(await e.count()>0&&await e.first().isVisible())return e.first()}catch{}return null}async function At(e,t,n,a){const r=[],o=t.viewportSize(),i=o?.width??1280;try{const t=await e.getByRole(n).all();for(const e of t)try{if(!await e.isVisible())continue;const t=(await e.textContent()??"").trim();if(t.length>0&&t.length<=wt){let o;try{const t=await e.boundingBox();t&&(o=(t.x+t.width/2)/i)}catch{}r.push({text:t,strategy:a,priority:1,source:`role:${n}`,xRatio:o})}}catch{}}catch{}return r}async function Pt(e){const t=[];try{const n=vt(await e.ariaSnapshot({timeout:3e3}));for(const{role:e,name:a}of n)a.length>0&&a.length<=wt&&t.push({text:a,strategy:"aria-snapshot",priority:2,source:`aria:${e}:${a}`})}catch{}return t}async function Ct(e){const t=[];try{const n=await e.evaluate((e,t)=>{const n=[],a=document.createTreeWalker(e,NodeFilter.SHOW_TEXT);for(;a.nextNode();){const e=a.currentNode.textContent?.trim();e&&e.length>0&&e.length<=t&&n.push(e)}return n},wt);for(const e of n)t.push({text:e,strategy:"leaf-text",priority:3,source:"leaf-text"})}catch{}return t}async function Et(e){const t=[];try{const n=[bt.username.primary,...bt.username.fallbacks],a=await e.evaluate(e=>e.map(e=>{try{return{selector:e,text:document.querySelector(e)?.textContent?.trim()??""}}catch{return{selector:e,text:""}}}),n);for(const{selector:e,text:n}of a)n.length>0&&n.length<=wt&&t.push({text:n,strategy:"css-fallback",priority:4,source:e})}catch{}return t}async function _t(e){const t=await It(e),n=t?(await Promise.all([At(t,e,"link","role-link"),At(t,e,"button","role-button"),Pt(t)])).flat():[],a=kt(n);if(a.found){const e=a.username;if(new Set(n.filter(t=>t.text.trim()===e).map(e=>e.strategy)).size>=2)return n}const r=(await Promise.all([t?Ct(t):Promise.resolve([]),Et(e)])).flat();return[...n,...r]}async function qt(e,t=_t){const n=kt(await t(e));if(!n.found)throw new Error("未找到用户名,请确认当前页面已登录招聘者账号。");return{platform:"zhipin",username:n.username,strategy:n.strategy,source:n.source}}function Nt(e,t){return void 0!==t.accountId?e.accountId===t.accountId:e.username===t.username}import{z as Rt}from"zod";var zt="reply-authority-service",Mt="browser-use-agent/zhipin_send_reply",Bt="zhipin",$t=60,Tt=Rt.object({platform:Rt.literal(Bt),username:Rt.string().min(1),accountId:Rt.string().min(1).optional()}),jt=Rt.object({v:Rt.literal(2),iss:Rt.literal(zt),kid:Rt.string().min(1),jti:Rt.string().min(1),iat:Rt.number().int(),exp:Rt.number().int(),aud:Rt.literal(Mt),platform:Rt.literal(Bt),tenantId:Rt.string().min(1),conversationId:Rt.string().min(1),candidateId:Rt.string().min(1),reply:Rt.string(),policyVersion:Rt.string().min(1),recruiterBinding:Tt}),Lt=Rt.object({kid:Rt.string().min(1),algorithm:Rt.literal("Ed25519"),publicKey:Rt.string().min(1),validFrom:Rt.string().min(1),validUntil:Rt.string().optional()}),Ut=Rt.object({keys:Rt.array(Lt)}),Ot=new Map;function Ft(e){for(const[t,n]of Ot)n<e-$t&&Ot.delete(t)}function Dt(e,t=Math.floor(Date.now()/1e3)){return Ft(t),Ot.has(e)}function Wt(e,t,n=Math.floor(Date.now()/1e3)){Ft(n),Ot.set(e,t)}import{createPublicKey as Ht,verify as Vt}from"node:crypto";var Yt=new Map,Gt=null;function Jt(){const e=process.env.REPLY_AUTHORITY_KEYS_URL?.trim();if(!e)throw new Error("REPLY_AUTHORITY_KEYS_URL 未配置,browser-use-agent 无法拉取 Reply Authority 公钥。");return e}function Kt(e){return Yt=new Map(e.map(e=>[e.kid,e]))}async function Zt(){if(Gt)return Gt;Gt=(async()=>{const e=await fetch(Jt()),t=await e.json();if(!e.ok)throw new Error(`Reply Authority 公钥拉取失败 (${e.status})`);return Kt(Ut.parse(t).keys)})();try{return await Gt}finally{Gt=null}}async function Xt(){await Zt()}async function Qt(e){const t=Yt.get(e);if(t)return t;const n=(await Zt()).get(e);if(!n)throw new Error(`Unknown key ID: ${e}`);return n}function en(e){if("object"==typeof e&&null!==e&&"v"in e&&"number"==typeof e.v&&2!==e.v)throw new Error("unexpected envelope version")}function tn(e){const t=e.split("."),n=t[0],a=t[1];if(2!==t.length||void 0===n||void 0===a||0===n.length||0===a.length)throw new Error("Invalid signed envelope format");let r,o="";try{o=Buffer.from(n,"base64url").toString("utf-8")}catch{throw new Error("Invalid signed envelope format")}try{r=JSON.parse(o)}catch{throw new Error("Envelope payload schema validation failed")}en(r);const i=jt.safeParse(r);if(!i.success)throw new Error("Envelope payload schema validation failed");return{payload:i.data,payloadJson:o,signatureBase64:a}}function nn(e,t){if(e.exp<=e.iat)throw new Error("Envelope expiry must be after issue time");if(e.exp<t-$t)throw new Error("Envelope expired");if(e.iat>t+$t)throw new Error("Envelope issued in the future")}async function an(e,t=Math.floor(Date.now()/1e3)){const n=tn(e),a=await Qt(n.payload.kid),r=Ht({key:Buffer.from(a.publicKey,"base64url"),format:"der",type:"spki"});if(!Vt(null,Buffer.from(n.payloadJson,"utf-8"),r,Buffer.from(n.signatureBase64,"base64url")))throw new Error("Signature verification failed");return nn(n.payload,t),n.payload}var rn=ht.object({success:ht.boolean(),sentMessage:ht.string(),error:ht.string().optional()}),on=pt({name:"zhipin_send_reply",description:"发送消息。只接受由 Reply Authority Service 签发的 signedEnvelope;可指定 candidateName 自动打开对应聊天后发送,或不传则发送到当前选中的聊天窗口。",input:ht.object({signedEnvelope:ht.string().describe("Reply Authority Service 返回的紧凑签名信封"),candidateName:ht.string().optional().describe("候选人姓名。若用户说“回复鲁倩”,这里应提取为“鲁倩”"),index:ht.number().optional().describe("候选人在列表中的索引(可选)")}),output:rn,execute:async(e,t)=>{let n="";if(!y()){const e="Reply Authority 公钥尚未成功预加载,当前无法发送签名回复。请检查启动日志、`REPLY_AUTHORITY_KEYS_URL` 配置,以及 `browser_status.replyAuthorityKeysLoaded`。";return t.logger.error(e),{success:!1,sentMessage:n,error:e}}const a=h(),r=await a.getPage("zhipin");let o=r;try{const i=await an(e.signedEnvelope);if(n=i.reply,Dt(i.jti))return{success:!1,sentMessage:n,error:"token 已消费,禁止重放"};const s=await Ye(a,r,{candidateName:e.candidateName,index:e.index});if(s&&!s.found)return{success:!1,sentMessage:n,error:s.error};if(!s){if(!await De(a,r))return{success:!1,sentMessage:n,error:"消息列表未加载"}}o=await a.getPage("zhipin");const c=await rt(o);if(!c)return{success:!1,sentMessage:n,error:"未能提取当前聊天的 conversationId/candidateId"};if(c.conversationId!==i.conversationId||c.candidateId!==i.candidateId)return{success:!1,sentMessage:n,error:"发送目标与签名不匹配"};const d=await qt(o);if(!Nt(d,i.recruiterBinding))return{success:!1,sentMessage:n,error:`recruiter 绑定不匹配:当前账号 ${d.username} 与签发时 ${i.recruiterBinding.username} 不一致`};t.logger.info(`Sending message (${n.length} chars) to ${c.candidateName||c.candidateId}`);const u="#boss-chat-editor-input, textarea.chat-input, .chat-input";await o.waitForSelector(u,{timeout:5e3});if(await o.evaluate(e=>{const t=document.querySelector(e);return"true"===t?.getAttribute("contenteditable")},u)){const e=o.locator(u).first();await e.focus(),await o.evaluate(e=>{const t=document.querySelector(e.sel);t&&(t.innerHTML=e.msg.split("\n").map(e=>`<p>${e}</p>`).join(""))},{sel:u,msg:n}),await e.dispatchEvent("input",{bubbles:!0})}else await o.fill(u,n);await xe(o,200,500);const l=await o.evaluate(()=>{document.querySelectorAll("[data-roll-send-btn]").forEach(e=>{e.removeAttribute("data-roll-send-btn")});const e=[".submit-content .submit.active",".submit-content .submit",".submit-content",".btn-send"];for(const t of e){const e=document.querySelector(t);if(e&&e.offsetWidth>0)return{found:!0,selector:t}}const t=Array.from(document.querySelectorAll("span"));for(const e of t)if("发送"===e.textContent?.trim()&&e.offsetWidth>0)return e.setAttribute("data-roll-send-btn","true"),{found:!0,selector:'[data-roll-send-btn="true"]'};return{found:!1}});if(!l.found)return{success:!1,sentMessage:n,error:"未找到发送按钮"};const m=o.locator(l.selector).first();return await m.scrollIntoViewIfNeeded(),await m.hover(),await ve(o),await m.click(),await xe(o,500,1200),Wt(i.jti,i.exp),t.logger.info("Message sent successfully"),{success:!0,sentMessage:n}}catch(e){return{success:!1,sentMessage:n,error:e instanceof Error?e.message:String(e)}}finally{await o.evaluate(()=>{document.querySelectorAll("[data-roll-send-btn]").forEach(e=>{e.removeAttribute("data-roll-send-btn")})}).catch(()=>{})}}});import{defineTool as sn}from"@roll-agent/sdk";import{z as cn}from"zod";var dn=cn.object({success:cn.boolean(),exchanged:cn.boolean(),wechatNumber:cn.string().optional(),error:cn.string().optional()}),un=sn({name:"zhipin_exchange_wechat",description:'换微信。可指定 candidateName 自动打开对应聊天后执行,或不传则在当前窗口执行;例如"和鲁倩换微信"应提取 candidateName=鲁倩。',input:cn.object({candidateName:cn.string().optional().describe('候选人姓名。若用户说"和鲁倩换微信",这里应提取为"鲁倩"'),index:cn.number().optional().describe("候选人在列表中的索引(可选)")}),output:dn,execute:async(e,t)=>{const n=h(),a=await n.getPage("zhipin"),r=await Ye(n,a,{candidateName:e.candidateName,index:e.index});if(r&&!r.found)return{success:!1,exchanged:!1,error:r.error};t.logger.info("Starting WeChat exchange"+(r?` with ${r.name}`:""));const o=await n.getPage("zhipin");try{if(!(await o.evaluate(()=>{const e=e=>{const t=e.getBoundingClientRect();return t.width>0&&t.height>0},t=[".operate-exchange-left .operate-btn","span.operate-btn"];for(const n of t){const t=Array.from(document.querySelectorAll(n));for(const n of t){const t=n.textContent?.trim()??"";if(t.includes("换微信")&&e(n))return n.setAttribute("data-roll-wechat-btn","true"),{found:!0,text:t}}}const n=Array.from(document.querySelectorAll("span"));for(const t of n){const n=t.textContent?.trim()??"";if(n.includes("换微信")&&e(t))return t.setAttribute("data-roll-wechat-btn","true"),{found:!0,text:n}}return{found:!1}})).found)return{success:!1,exchanged:!1,error:"未找到「换微信」按钮"};await xe(o,200,400),await o.click('[data-roll-wechat-btn="true"]'),await o.evaluate(()=>{document.querySelector("[data-roll-wechat-btn]")?.removeAttribute("data-roll-wechat-btn")}),await xe(o,400,800);let e=!1;for(let t=0;t<8;t++){if(await o.evaluate(()=>{const e=e=>{const t=e.getBoundingClientRect();return t.width>0&&t.height>0},t=document.querySelector(".exchange-tooltip");if(t&&e(t))return!0;const n=document.querySelectorAll("div, section, aside");for(const t of Array.from(n)){if((t.textContent??"").includes("交换微信")&&t.querySelector(".boss-btn-primary, .boss-btn")&&e(t))return!0}return!1})){e=!0;break}await xe(o,400,800)}if(!e)return{success:!1,exchanged:!1,error:"确认对话框未弹出"};await ve(o);if(!(await o.evaluate(()=>{const e=e=>{const t=e.getBoundingClientRect();return t.width>0&&t.height>0},t=document.querySelector(".exchange-tooltip");if(t){const n=[".btn-box .boss-btn-primary.boss-btn",".btn-box span.boss-btn-primary","span.boss-btn-primary",".boss-btn-primary"];for(const a of n){const n=t.querySelector(a);if(n&&e(n))return n.setAttribute("data-roll-confirm-btn","true"),{found:!0,text:n.textContent?.trim()??""}}}const n=document.querySelectorAll("div, section, aside");for(const t of Array.from(n)){if(!(t.textContent??"").includes("交换微信"))continue;const n=t.querySelectorAll("span.boss-btn-primary, button.boss-btn-primary, span.boss-btn, button.boss-btn");for(const t of Array.from(n)){const n=t.textContent?.trim()??"";if("确定"===n&&e(t))return t.setAttribute("data-roll-confirm-btn","true"),{found:!0,text:n}}}return{found:!1}})).found)return{success:!1,exchanged:!1,error:"未找到确认按钮"};await xe(o,200,400),await o.click('[data-roll-confirm-btn="true"]'),await o.evaluate(()=>{document.querySelector("[data-roll-confirm-btn]")?.removeAttribute("data-roll-confirm-btn")}),await xe(o,1500,2500);const n=await o.evaluate(()=>{const e=[".message-card-top-wrap",'[class*="d-top-text"]',".message-card-top-title"];for(const t of e){const e=Array.from(document.querySelectorAll(t));for(let t=e.length-1;t>=0;t--){const n=e[t]?.textContent??"",a=n.match(/\b(\d{8,15})\b/);if(a)return a[1];const r=n.match(/微信[::号]*\s*([a-zA-Z0-9_-]{5,20})/);if(r)return r[1];const o=n.match(/\b([a-zA-Z][a-zA-Z0-9_-]{5,19})\b/);if(o&&!["微信","WeChat"].includes(o[1]))return o[1]}}const t=Array.from(document.querySelectorAll(".message-item"));for(let e=t.length-1;e>=0;e--){const n=t[e]?.querySelector('.message-card-top-wrap, [class*="d-top-text"]');if(n){const e=(n.textContent??"").match(/\b(\d{8,15})\b/);if(e)return e[1]}}return null});return t.logger.info("WeChat exchanged"+(n?`, number: ${n}`:"")),{success:!0,exchanged:!0,...null!==n?{wechatNumber:n}:{}}}catch(e){return{success:!1,exchanged:!1,error:e instanceof Error?e.message:String(e)}}}});import{defineTool as ln}from"@roll-agent/sdk";import{z as mn}from"zod";var gn=mn.object({success:mn.boolean(),username:mn.string(),usedSelector:mn.string().optional(),usedStrategy:mn.string().optional(),source:mn.string().optional(),error:mn.string().optional()}),fn=ln({name:"zhipin_get_username",description:"获取当前登录的招聘者用户名",input:mn.object({}),output:gn,execute:async(e,t)=>{t.logger.info("Getting zhipin username");try{const e=h(),n=await e.getPage("zhipin");await n.bringToFront().catch(()=>{});const a=await qt(n);return t.logger.info(`Username: ${a.username} (strategy: ${a.strategy}, source: ${a.source})`),{success:!0,username:a.username,usedSelector:"css-fallback"===a.strategy?a.source:void 0,usedStrategy:a.strategy,source:a.source}}catch(e){return{success:!1,username:"",error:e instanceof Error?`获取用户名失败:${e.message}`:"获取用户名失败"}}}});import{defineTool as pn}from"@roll-agent/sdk";import{z as hn}from"zod";var bn=hn.object({index:hn.number(),candidateId:hn.string(),name:hn.string(),age:hn.string(),experience:hn.string(),education:hn.string(),workStatus:hn.string(),company:hn.string(),currentPosition:hn.string(),expectedLocation:hn.string(),expectedPosition:hn.string(),expectedSalary:hn.string(),tags:hn.array(hn.string()),buttonText:hn.string()}),wn=hn.object({success:hn.boolean(),candidates:hn.array(bn),total:hn.number(),error:hn.string().optional()}),yn=pn({name:"zhipin_get_candidate_list",description:"获取推荐列表页的候选人卡片信息",input:hn.object({maxResults:hn.number().optional().describe("最多返回条数")}),output:wn,execute:async(e,t)=>{t.logger.info("Getting candidate list from recommend page");const n=h(),a=await n.getPage("zhipin"),r=a.frame("recommendFrame")??a.frames().find(e=>e.url().includes("recommend"))??a;try{await r.waitForSelector("[data-geek], .geek-item",{timeout:1e4})}catch{return{success:!1,candidates:[],total:0,error:"推荐列表未加载"}}const o=await r.evaluate(e=>{let t=Array.from(document.querySelectorAll(".candidate-card-wrap"));0===t.length&&(t=Array.from(document.querySelectorAll("[data-geek], .geek-item")));const n=e??t.length,a=[];return t.forEach((e,t)=>{if(t>=n)return;const r=e.getAttribute("data-geek")??e.querySelector("[data-geek]")?.getAttribute("data-geek")??"",o=e.querySelector(".name")?.textContent?.trim()??"";let i="",s="",c="",d="";const u=e.querySelector(".base-info.join-text-wrap, .base-info");if(u){const e=[];if(u.querySelectorAll(":scope > *").forEach(t=>{const n=t.textContent?.trim();n&&e.push(n)}),e.length<=1&&(e.length=0,u.childNodes.forEach(t=>{if(t.nodeType===Node.TEXT_NODE){const n=t.textContent?.trim();n&&e.push(n)}})),e.length<=1){const t=u.textContent?.trim()??"";e.length=0,t.split(/[丨·|]/).forEach(t=>{const n=t.trim();n&&e.push(n)})}for(const t of e)!i&&t.includes("岁")?i=t:!s&&(t.includes("年")||t.includes("应届")||t.includes("在校"))?s=t:!c&&/(初中|高中|中专|中技|大专|本科|硕士|博士)/.test(t)?c=t:!d&&/(在职|离职|在校)/.test(t)&&(d=t)}const l=e.querySelector(".timeline-wrap.work-exps .content.join-text-wrap")??e.querySelector(".timeline-wrap.work-exps .content"),m=(l?.textContent?.trim()??"").split("·").map(e=>e.trim()),g=m[0]??"",f=m[1]??"";let p="",h="";const b=e.querySelector(".row-flex:not(.geek-desc)");if(b){const e=b.querySelector(".label"),t=b.querySelector(".content"),n=e?.textContent??"";if((n.includes("期望")||n.includes("最近关注"))&&t){const e=(t.textContent?.trim()??"").split("·").map(e=>e.trim());p=e[0]??"",h=e[1]??""}}if(!p){const t=e.querySelector(".timeline-wrap.expect .content.join-text-wrap")??e.querySelector(".timeline-wrap.expect .content");if(t){const e=(t.textContent?.trim()??"").split("·").map(e=>e.trim());p=e[0]??"",h=e[1]??""}}const w=e.querySelector(".salary-wrap")?.textContent?.trim()??"",y=[];e.querySelectorAll(".tags-wrap .tag-item, .tags-wrap .tag, .tags-wrap span").forEach(e=>{const t=e.textContent?.trim();t&&y.push(t)});const x=e.querySelector("button.btn.btn-greet")?.textContent?.trim()??"";a.push({index:t,candidateId:r,name:o,age:i,experience:s,education:c,workStatus:d,company:g,currentPosition:f,expectedLocation:p,expectedPosition:h,expectedSalary:w,tags:y,buttonText:x})}),a},e.maxResults);return t.logger.info(`Found ${o.length} candidates in recommend list`),{success:!0,candidates:o,total:o.length}}});import{defineTool as xn}from"@roll-agent/sdk";import{z as vn}from"zod";var Sn=".candidate-card-wrap",kn="[data-geek], .geek-item",In=`${Sn}, ${kn}`;function An(e){return e.evaluate(e=>document.querySelectorAll(e.primarySelector).length>0?e.primarySelector:e.fallbackSelector,{primarySelector:Sn,fallbackSelector:kn})}function Pn(e){return e.frame("recommendFrame")??e.frames().find(e=>e.url().includes("recommend"))??e}async function Cn(e,t=1e4){try{return await e.waitForSelector(In,{timeout:t}),!0}catch{return!1}}async function En(e,t){const n=await An(e),a=e.locator(n);if(await a.count()<=t)return{found:!1,cardSelector:n,candidateId:"",name:"",hasGreetButton:!1,error:"索引超出范围"};const r=a.nth(t),o=await r.evaluate(e=>{const t=e.getAttribute("data-geek")??e.querySelector("[data-geek]")?.getAttribute("data-geek")??"",n=e.querySelector(".name")?.textContent?.trim()??"",a=e.querySelector("button.btn.btn-greet");return{candidateId:t,name:n,hasGreetButton:null!==a&&a.offsetWidth>0}});return{found:!0,cardSelector:n,candidateId:o.candidateId,name:o.name,hasGreetButton:o.hasGreetButton}}var _n=vn.object({index:vn.number(),candidateName:vn.string(),candidateId:vn.string(),success:vn.boolean(),error:vn.string().optional()}),qn=vn.object({success:vn.boolean(),results:vn.array(_n),summary:vn.object({total:vn.number(),succeeded:vn.number(),failed:vn.number()})}),Nn=xn({name:"zhipin_say_hello",description:"在推荐列表页对候选人点击「打招呼」按钮(支持批量)",input:vn.object({indices:vn.array(vn.number()).describe("要打招呼的候选人索引列表")}),output:qn,execute:async(e,t)=>{t.logger.info(`Saying hello to ${e.indices.length} candidates`);const n=h(),a=await n.getPage("zhipin"),r=Pn(a);if(!await Cn(r)){const t=e.indices.map(e=>({index:e,candidateName:"",candidateId:"",success:!1,error:"推荐列表未加载"}));return{success:!1,results:t,summary:{total:t.length,succeeded:0,failed:t.length}}}const o=[];for(const t of e.indices)try{const e=await En(r,t);if(e.found)if(e.hasGreetButton){const n=r.locator(e.cardSelector).nth(t).locator("button.btn.btn-greet").first();await n.scrollIntoViewIfNeeded(),await n.hover(),await ve(a),await n.click(),o.push({index:t,candidateName:e.name,candidateId:e.candidateId,success:!0})}else o.push({index:t,candidateName:e.name,candidateId:e.candidateId,success:!1,error:"未找到打招呼按钮"});else o.push({index:t,candidateName:"",candidateId:"",success:!1,...void 0!==e.error?{error:e.error}:{}});await ve(a),Ie(.3)&&await Se(a)}catch(e){o.push({index:t,candidateName:"",candidateId:"",success:!1,error:e instanceof Error?e.message:String(e)})}const i={total:o.length,succeeded:o.filter(e=>e.success).length,failed:o.filter(e=>!e.success).length};return t.logger.info(`Say hello: ${i.succeeded}/${i.total} succeeded`),{success:0===i.failed,results:o,summary:i}}});import{defineTool as Rn}from"@roll-agent/sdk";import{z as zn}from"zod";var Mn=zn.object({success:zn.boolean(),candidateName:zn.string(),candidateId:zn.string(),error:zn.string().optional()}),Bn=Rn({name:"zhipin_open_resume",description:"在推荐列表页点击候选人卡片打开简历详情弹窗",input:zn.object({index:zn.number().describe("候选人在列表中的索引")}),output:Mn,execute:async(e,t)=>{t.logger.info(`Opening resume for candidate at index ${e.index}`);const n=h(),a=await n.getPage("zhipin"),r=Pn(a);if(!await Cn(r))return{success:!1,candidateName:"",candidateId:"",error:"推荐列表未加载"};const o=await En(r,e.index);if(!o.found)return{success:!1,candidateName:"",candidateId:"",error:o.error??`索引 ${e.index} 超出范围`};const i=r.locator(o.cardSelector).nth(e.index),s=await i.locator("[data-geek], .card-inner, .geek-item").count()>0?i.locator("[data-geek], .card-inner, .geek-item").first():i;return await s.scrollIntoViewIfNeeded(),await s.hover(),await xe(a,200,400),await s.click(),await xe(a,1e3,2e3),t.logger.info(`Opened resume for ${o.name}`),{success:!0,candidateName:o.name,candidateId:o.candidateId}}});import{defineTool as $n}from"@roll-agent/sdk";import{z as Tn}from"zod";var jn=Tn.object({x:Tn.number(),y:Tn.number(),width:Tn.number(),height:Tn.number()}),Ln=Tn.object({success:Tn.boolean(),screenshotArea:jn.optional(),canvasInfo:Tn.object({width:Tn.number(),height:Tn.number()}).optional(),error:Tn.string().optional()}),Un=$n({name:"zhipin_locate_resume_canvas",description:"定位简历详情中嵌套 iframe 内的 canvas 元素坐标(用于截图)",input:Tn.object({}),output:Ln,execute:async(e,t)=>{t.logger.info("Locating resume canvas in nested iframes");const n=h(),a=await n.getPage("zhipin");try{const e=a.frame("recommendFrame")??a.frames().find(e=>e.url().includes("recommend"));if(!e)return{success:!1,error:"未找到推荐页 iframe"};const n=await e.$('iframe[src*="c-resume"]');if(!n)return{success:!1,error:"未找到简历 iframe"};const r=await n.contentFrame();if(!r)return{success:!1,error:"无法访问简历 iframe 内容"};try{await r.waitForSelector("canvas#resume, div#resume canvas",{timeout:5e3})}catch{return{success:!1,error:"简历 canvas 未加载"}}const o=await r.evaluate(()=>{const e=document.querySelector("canvas#resume, div#resume canvas");if(!e)return null;const t=e.getBoundingClientRect();return{width:e.width,height:e.height,clientWidth:t.width,clientHeight:t.height,x:t.x,y:t.y}});if(!o)return{success:!1,error:"无法获取 canvas 信息"};const i=await a.evaluate(()=>{const e=document.querySelector("#recommendFrame");if(!e)return null;const t=e.getBoundingClientRect();return{x:t.x,y:t.y}}),s=await e.evaluate(()=>{const e=document.querySelector('iframe[src*="c-resume"]');if(!e)return null;const t=e.getBoundingClientRect();return{x:t.x,y:t.y}}),c=(i?.x??0)+(s?.x??0),d=(i?.y??0)+(s?.y??0);return t.logger.info(`Canvas located at (${c+o.x}, ${d+o.y})`),{success:!0,screenshotArea:{x:Math.round(c+o.x),y:Math.round(d+o.y),width:Math.round(o.clientWidth),height:Math.round(o.clientHeight)},canvasInfo:{width:o.width,height:o.height}}}catch(e){return{success:!1,error:e instanceof Error?e.message:String(e)}}}});import{defineTool as On}from"@roll-agent/sdk";import{z as Fn}from"zod";var Dn=Fn.object({success:Fn.boolean(),closed:Fn.boolean(),error:Fn.string().optional()}),Wn=[".recommendV2 .boss-popup__close",".dialog-lib-resume .boss-popup__close",".boss-dialog .boss-popup__close",".boss-popup__close",".close-btn",".dialog-close"],Hn=[".boss-popup__close",".close-btn",".dialog-close",".modal-close"],Vn=On({name:"zhipin_close_resume",description:"关闭简历详情弹窗",input:Fn.object({}),output:Dn,execute:async(e,t)=>{t.logger.info("Closing resume detail modal");const n=h(),a=await n.getPage("zhipin"),r=a.frame("recommendFrame")??a.frames().find(e=>e.url().includes("recommend"));if(!await(async()=>{if(r)for(const e of Wn){const t=await r.$(e);if(t&&await t.isVisible())return await t.click(),!0}for(const e of Hn){const t=await a.$(e);if(t&&await t.isVisible())return await t.click(),!0}return!1})())return{success:!1,closed:!1,error:"未找到关闭按钮"};let o=!1;for(let e=0;e<5;e++){await a.waitForTimeout(300);const e=r?await r.$(".boss-popup__wrapper, .dialog-lib-resume, .boss-dialog"):await a.$(".boss-popup__wrapper");if(!e||!await e.isVisible()){o=!0;break}}return t.logger.info(o?"Resume modal closed and verified":"Resume modal close unverified"),{success:!0,closed:!0}}});import{defineTool as Yn}from"@roll-agent/sdk";import{z as Gn}from"zod";import{waitForSelector as Jn}from"@roll-agent/browser";var Kn={login:{qrCode:".login-qr img, .qr-code img",loginSuccess:".user-info, .header-user"},messageList:{container:".chat-list, .msg-list",item:".chat-item, .msg-item",candidateName:".chat-item .name, .msg-item .name",lastMessage:".chat-item .msg, .msg-item .content",unreadBadge:".chat-item .unread, .msg-item .badge",timestamp:".chat-item .time, .msg-item .time"},chat:{input:".chat-input textarea, .msg-input textarea",sendButton:".btn-send, .send-btn",messageItem:".chat-msg, .msg-bubble",messageText:".chat-msg .text, .msg-bubble .text"}};import{navigateTo as Zn,waitForSelector as Xn}from"@roll-agent/browser";var Qn="https://www.yupao.com",ea=`${Qn}/chat`,ta=`${Qn}/login`;async function na(e){e.url().includes("/chat")||await Zn(e,ea),await Xn(e,Kn.messageList.container,{timeout:15e3})}async function aa(e,t){const n=`${Qn}/chat?id=${encodeURIComponent(t)}`;await Zn(e,n),await Xn(e,Kn.chat.input,{timeout:15e3})}async function ra(e,t){await na(e),await Jn(e,Kn.messageList.item,{timeout:1e4});const n=Kn.messageList;return await e.$$eval(n.item,(e,t)=>{const n=[],a=t.maxItems?e.slice(0,t.maxItems):e;for(const e of a){const a=e.querySelector(t.sel.candidateName),r=e.querySelector(t.sel.lastMessage),o=e.querySelector(t.sel.unreadBadge),i=e.querySelector(t.sel.timestamp),s=e.getAttribute("data-id")??e.getAttribute("data-conversation-id")??e.querySelector("a")?.getAttribute("href")?.match(/id=([^&]+)/)?.[1]??"";n.push({conversationId:s,candidateName:a?.textContent?.trim()??"",lastMessage:r?.textContent?.trim()??"",unreadCount:parseInt(o?.textContent?.trim()??"0",10)||0,timestamp:i?.textContent?.trim()??""})}return n},{sel:n,maxItems:t})}var oa=Gn.object({limit:Gn.number().optional().describe("最多返回的消息条数")}),ia=Gn.object({conversationId:Gn.string(),candidateName:Gn.string(),lastMessage:Gn.string(),unreadCount:Gn.number(),timestamp:Gn.string()}),sa=Gn.object({messages:Gn.array(ia),total:Gn.number()}),ca=Yn({name:"yupao_read_messages",description:"读取鱼泡未读消息列表",input:oa,output:sa,execute:async(e,t)=>{t.logger.info(`Reading yupao messages (limit: ${e.limit??"all"})`);const n=h(),a=await n.getPage("yupao"),r=await ra(a,e.limit);return t.logger.info(`Found ${r.length} messages`),{messages:r.map(e=>({...e})),total:r.length}}});import{defineTool as da}from"@roll-agent/sdk";import{z as ua}from"zod";import{waitForSelector as la,typeText as ma,clickElement as ga}from"@roll-agent/browser";async function fa(e,t,n){try{return await aa(e,t),await ma(e,Kn.chat.input,n),await ga(e,Kn.chat.sendButton),await la(e,Kn.chat.messageItem,{timeout:5e3}),{success:!0}}catch(e){return{success:!1,error:e instanceof Error?e.message:String(e)}}}var pa=ua.object({conversationId:ua.string().describe("对话 ID"),message:ua.string().describe("要发送的回复消息")}),ha=ua.object({success:ua.boolean(),conversationId:ua.string(),sentMessage:ua.string(),error:ua.string().optional()}),ba=da({name:"yupao_send_reply",description:"向鱼泡指定对话发送回复消息",input:pa,output:ha,execute:async(e,t)=>{const{conversationId:n,message:a}=e;t.logger.info(`Sending reply to yupao conversation ${n}`);const r=h(),o=await r.getPage("yupao"),i=await fa(o,n,a);return i.success?t.logger.info("Reply sent successfully"):t.logger.error(`Failed to send reply: ${i.error}`),{success:i.success,conversationId:n,sentMessage:a,error:i.error}}}),wa=t("browser-use-agent");function ya(e){if(void 0!==e){if("true"===e)return!0;if("false"===e)return!1;throw new Error(`Expected boolean env value "true" or "false", received "${e}".`)}}function xa(e,t){if(void 0===e)return;const n=Number.parseInt(e,10);if(!Number.isInteger(n))throw new Error(`${t} must be an integer, received "${e}".`);return n}function va(e){if(void 0===e)return;const t=JSON.parse(e);if(!Array.isArray(t)||!t.every(e=>"string"==typeof e))throw new Error("BROWSER_ARGS_JSON must be a JSON string array.");return t}function Sa(){return n.parse({mode:process.env.BROWSER_MODE,headless:ya(process.env.BROWSER_HEADLESS),cdpUrl:process.env.BROWSER_CDP_URL,cdpHost:process.env.BROWSER_CDP_HOST,cdpPort:xa(process.env.BROWSER_CDP_PORT,"BROWSER_CDP_PORT"),channel:process.env.BROWSER_CHANNEL,executablePath:process.env.BROWSER_EXECUTABLE_PATH,userDataDir:process.env.BROWSER_USER_DATA_DIR,args:va(process.env.BROWSER_ARGS_JSON),sessionsDir:process.env.BROWSER_SESSIONS_DIR})}var ka=e({name:"browser-use-agent",tools:[B,J,te,ue,be,Ke,et,ft,on,un,fn,yn,Nn,Bn,Un,Vn,ca,ba,S]},{onShutdown:x});async function Ia(){await f(Sa());try{await Xt(),w(!0)}catch(e){w(!1),wa.error(`Failed to preload Reply Authority keys during startup; browser_status.replyAuthorityKeysLoaded=false. ${e instanceof Error?e.stack??e.message:String(e)}`)}await ka.listen({transport:{type:"http",port:parseInt(process.env.BROWSER_AGENT_PORT??"3100",10),host:process.env.BROWSER_AGENT_HOST??"127.0.0.1"}})}Ia().catch(async e=>{wa.error(`Fatal error: ${e instanceof Error?e.stack??e.message:String(e)}`),await x().catch(()=>{}),process.exit(1)});
@@ -0,0 +1,14 @@
1
+ export declare function resolvePreferredBrand(communicationPosition: string): string | undefined;
2
+ export declare function resolveExpectedSignals(expectedJobText: string): {
3
+ expectedLocation: string;
4
+ expectedPosition: string;
5
+ };
6
+ export declare function resolveConversationSignals(input: {
7
+ communicationPosition: string;
8
+ expectedJobText: string;
9
+ }): {
10
+ communicationPosition: string;
11
+ expectedLocation: string;
12
+ expectedPosition: string;
13
+ preferredBrand?: string;
14
+ };
@@ -14,12 +14,12 @@ export declare const zhipinGetCandidateInfo: import("@roll-agent/sdk").ToolDefin
14
14
  };
15
15
  candidateInfo: {
16
16
  name: string;
17
+ expectedLocation: string;
18
+ expectedPosition: string;
17
19
  age: string;
18
20
  experience: string;
19
21
  education: string;
20
22
  communicationPosition: string;
21
- expectedPosition: string;
22
- expectedLocation: string;
23
23
  expectedSalary: string;
24
24
  tags: string[];
25
25
  };
@@ -32,4 +32,5 @@ export declare const zhipinGetCandidateInfo: import("@roll-agent/sdk").ToolDefin
32
32
  }[];
33
33
  formattedHistory: string[];
34
34
  error?: string | undefined;
35
+ preferredBrand?: string | undefined;
35
36
  }>;
@@ -6,11 +6,11 @@ export declare const zhipinGetCandidateList: import("@roll-agent/sdk").ToolDefin
6
6
  name: string;
7
7
  index: number;
8
8
  candidateId: string;
9
+ expectedLocation: string;
10
+ expectedPosition: string;
9
11
  age: string;
10
12
  experience: string;
11
13
  education: string;
12
- expectedPosition: string;
13
- expectedLocation: string;
14
14
  expectedSalary: string;
15
15
  tags: string[];
16
16
  workStatus: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roll-agent/browser-use-agent",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",