@roll-agent/browser-use-agent 0.4.2 → 0.6.0

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
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  name: browser-use-agent
3
3
  description: 浏览器操控 Agent。控制浏览器操作招聘平台——读取消息、打开聊天、发送回复、换微信、查看推荐列表、打招呼、查看简历。
4
+ metadata:
5
+ roll-env-file: references/env.yaml
4
6
  ---
5
7
 
6
8
  # Browser Use Agent
@@ -23,12 +25,12 @@ description: 浏览器操控 Agent。控制浏览器操作招聘平台——读
23
25
 
24
26
  ## BOSS直聘 — 聊天 Tools
25
27
 
26
- - `zhipin_read_messages(limit?, onlyUnread?, sortBy?)` — 读取消息列表中的候选人,支持过滤未读和排序
28
+ - `zhipin_read_messages(limit?, onlyUnread?, sortBy?)` — 读取消息列表中的候选人,返回姓名、消息摘要,以及 `conversationId` / `candidateId`
27
29
  - `zhipin_open_chat(candidateName?, index?, preferUnread?)` — 打开指定候选人的聊天窗口(按姓名模糊匹配或列表索引)
28
- - `zhipin_get_candidate_info(candidateName?, index?, maxMessages?)` — 提取候选人资料和聊天记录。指定 candidateName 会自动打开对应聊天
29
- - `zhipin_send_reply(message, candidateName?, index?)` — 发送消息。指定 candidateName 会自动打开对应聊天后发送
30
+ - `zhipin_get_candidate_info(candidateName?, index?, maxMessages?)` — 提取候选人资料、聊天记录,以及当前选中聊天的 `conversationId` / `candidateId`
31
+ - `zhipin_send_reply(signedEnvelope, candidateName?, index?)` — 发送消息。只接受 Reply Authority Service 签发的 `signedEnvelope`;本地会先做 Ed25519 验签、过期检查、重放检查、目标绑定校验和 recruiter 绑定校验
30
32
  - `zhipin_exchange_wechat(candidateName?, index?)` — 换微信。指定 candidateName 会自动打开对应聊天后执行
31
- - `zhipin_get_username()` — 获取当前登录的招聘者用户名(依赖当前 runtime 已跟踪页面;首次使用请先 `open_platform`,已打开但未跟踪页面可先 `list_pages + select_page`,确认登录后如需单独验证 attach,可先调用 `attach_browser_session`)。常用于外部通知消息中的账号标识,参见 notify-agent 的跨 Agent 工作流
33
+ - `zhipin_get_username()` — 获取当前登录的招聘者用户名,返回 `username`(依赖当前 runtime 已跟踪页面;首次使用请先 `open_platform`,已打开但未跟踪页面可先 `list_pages + select_page`,确认登录后如需单独验证 attach,可先调用 `attach_browser_session`)。常用于 recruiter binding 解析和外部通知消息中的账号标识
32
34
 
33
35
  ## BOSS直聘 — 推荐列表 Tools
34
36
 
@@ -47,11 +49,19 @@ description: 浏览器操控 Agent。控制浏览器操作招聘平台——读
47
49
 
48
50
  1. `zhipin_read_messages` → 获取未读候选人列表
49
51
  2. `zhipin_open_chat(candidateName)` → 打开某人的聊天
50
- 3. `zhipin_get_candidate_info` → 查看候选人资料和聊天记录
51
- 4. `zhipin_send_reply(message)` → 发送回复
52
- 5. `zhipin_exchange_wechat` → 交换微信(可选)
52
+ 3. `zhipin_get_candidate_info` → 查看候选人资料、聊天记录,并拿到 `conversationId` / `candidateId`
53
+ 4. `smart-reply-agent.generate_reply(..., target)` → 获取 `suggestedReply + signedEnvelope`
54
+ 5. `zhipin_send_reply(signedEnvelope)` → 验签、校验 recruiterBinding 后发送回复
55
+ 6. `zhipin_exchange_wechat` → 交换微信(可选)
53
56
 
54
57
  ## 支持平台
55
58
 
56
59
  - BOSS直聘 (zhipin)
57
60
  - 鱼泡 (yupao)
61
+
62
+ ## Reply Authority 集成说明
63
+
64
+ - `zhipin_send_reply` 不再接受裸文本 `message`
65
+ - 实际发送文本来自验签后的 envelope payload 内部 `reply` 字段
66
+ - envelope 绑定 `conversationId + candidateId + recruiterBinding`,若当前选中聊天或当前登录招聘者与签名目标不一致,会拒绝发送
67
+ - Agent 启动时会尝试从 `REPLY_AUTHORITY_KEYS_URL` 预拉公钥;若拉取失败,其他只读工具仍可用,但发送会失败关闭
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{defineAgent as e}from"@roll-agent/sdk";import{BrowserRuntimeConfigSchema as t}from"@roll-agent/browser";import{defineTool as a}from"@roll-agent/sdk";import{z as n}from"zod";import{BrowserRuntime as r,BrowserContextManager as o,SessionStore as i}from"@roll-agent/browser";var s,c,u;async function d(e){s||(u=new i(e.sessionsDir),s=new r(e),await s.start(),c=new o(s,u))}function l(){if(!s)throw new Error("BrowserRuntime not initialized. Call initRuntime() first.");return s}function m(){if(!c)throw new Error("BrowserContextManager not initialized. Call initRuntime() first.");return c}function g(){if(!u)throw new Error("SessionStore not initialized. Call initRuntime() first.");return u}async function f(){c&&(console.error("[browser-use-agent] Closing browser contexts..."),await c.closeAll(),c=void 0),s&&(console.error("[browser-use-agent] Stopping browser process..."),await s.stop(),s=void 0),u=void 0,console.error("[browser-use-agent] Browser runtime shutdown complete")}var p=n.object({success:n.boolean(),mode:n.string(),connected:n.boolean()}),h=a({name:"attach_browser_session",description:"调试工具:显式执行一次 connectOverCDP(),仅建立 Playwright Browser 连接,不做页面导航或 DOM 操作。",input:n.object({}),output:p,execute:async(e,t)=>{const a=l();return t.logger.info("Attaching Playwright browser session over CDP"),await a.getBrowser(),{success:!0,mode:a.mode,connected:!0}}});import{defineTool as b}from"@roll-agent/sdk";import{z as w}from"zod";import{BrowserStatusSchema as y}from"@roll-agent/browser";var x=b({name:"browser_status",description:"查询浏览器运行状态和活跃 session 信息",input:w.object({}),output:y,execute:async(e,t)=>{t.logger.info("Querying browser status");const a=l(),n=m(),r=g(),o=a.isRunning(),{headless:i,mode:s}=a.getConfig(),c=n.getActivePlatforms(),u=[];for(const e of c){const t=n.getPageCount(e),o=n.getCurrentUrl(e);let i=null,s="unknown";if(a.shouldRestoreSessionSnapshot()){const[t,a]=await Promise.all([r.loadCookies(e),r.loadLocalStorage(e)]);i=void 0!==t&&t.length>0||void 0!==a&&Object.keys(a).length>0,s=i?"snapshot":"none"}else a.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}}});import{defineTool as v}from"@roll-agent/sdk";import{BrowserPageInfoSchema as S,PlatformSchema as k}from"@roll-agent/browser";import{z as C}from"zod";import{PLATFORMS as A}from"@roll-agent/browser";var P={zhipin:"https://www.zhipin.com",yupao:"https://www.yupao.com"};function N(e){return new URL(P[e]).host}function q(e,t){try{return new URL(e).host.includes(N(t))}catch{return!1}}function I(e){return A.find(t=>q(e,t))}function _(e,t){const a=I(t.url)??null;return{pageId:t.targetId,url:t.url,title:t.title,boundPlatform:e.getBoundPlatformForNativePage(t.targetId)??null,detectedPlatform:a,isSelectedForPlatform:e.isNativePageSelected(t.targetId)}}async function z(e,t){const a=t.url();return{pageId:e.getPageId(t),url:a,title:await t.title().catch(()=>""),boundPlatform:e.getBoundPlatformForPage(t)??null,detectedPlatform:I(a)??null,isSelectedForPlatform:e.isSelectedPageForPlatform(t)}}var M=C.object({platform:k.optional().describe("可选:仅返回指定平台相关的页面")}),E=C.object({pages:C.array(S)}),R=v({name:"list_pages",description:"通过原生 CDP 列出当前浏览器可见页面及其可选择的 pageId;登录前该值等同于原生 targetId。",input:M,output:E,execute:async(e,t)=>{const a=m();t.logger.info("Listing browser pages");const n=(await a.listNativePages()).map(e=>_(a,e));return{pages:void 0===e.platform?n:n.filter(t=>t.boundPlatform===e.platform||t.detectedPlatform===e.platform)}}});import{defineTool as $}from"@roll-agent/sdk";import{BrowserPageInfoSchema as B}from"@roll-agent/browser";import{z as T}from"zod";var j=T.object({url:T.string().url().describe("要导航到的目标 URL")}),O=T.object({success:T.boolean(),page:B}),U=$({name:"navigate_active_tab",description:"将当前激活的浏览器 tab 导航到指定 URL;若 URL 属于已知平台,会自动绑定该平台当前活跃页。",input:j,output:O,execute:async(e,t)=>{const a=m();t.logger.info(`Navigating active tab to ${e.url}`);const n=await a.getActivePage();if(!n)throw new Error("No active browser tab detected. Use open_platform or select_page first.");await n.bringToFront().catch(()=>{}),await n.goto(e.url,{waitUntil:"domcontentloaded"});const r=I(n.url());return r?await a.selectAttachedPage(r,a.getPageId(n)):a.clearBindingForPage(n),{success:!0,page:await z(a,n)}}});import{defineTool as F}from"@roll-agent/sdk";import{BrowserPageInfoSchema as L,PlatformSchema as W}from"@roll-agent/browser";import{z as D}from"zod";async function V(e,t){return(await e.listNativePages()).find(e=>q(e.url,t))}async function H(e,t){const a=await V(e,t);if(a)return await e.activateNativePage(a.targetId),{page:a,reusedExistingPage:!0};return{page:await e.openNativePage(P[t]),reusedExistingPage:!1}}var G=D.object({platform:W.describe("目标平台:`zhipin` 代表 BOSS直聘,`yupao` 代表鱼泡")}),J=D.object({success:D.boolean(),page:L,reusedExistingTab:D.boolean()}),Z=F({name:"open_platform",description:"打开并聚焦招聘平台主页,供用户手动登录或后续执行站内操作。",input:G,output:J,execute:async(e,t)=>{const{platform:a}=e,n=l(),r=m();t.logger.info(`Opening platform page for ${a}`);const{page:o,reusedExistingPage:i}=await H(n,a);return r.rememberNativePageSelection(a,o),{success:!0,page:_(r,o),reusedExistingTab:i}}});import{defineTool as X}from"@roll-agent/sdk";import{BrowserPageInfoSchema as Q,PlatformSchema as K}from"@roll-agent/browser";import{z as Y}from"zod";var ee=Y.object({platform:K.describe("要将该页面绑定为当前活跃页的平台"),pageId:Y.string().describe("通过 list_pages 返回的 pageId;登录前就是原生 targetId,登录后仍可作为稳定选择句柄")}),te=Y.object({success:Y.boolean(),page:Q}),ae=X({name:"select_page",description:"将指定 pageId 绑定为平台当前活跃页,并切换到前台;登录前走原生 CDP target 激活。",input:ee,output:te,execute:async(e,t)=>{const a=m();t.logger.info(`Selecting page ${e.pageId} for ${e.platform}`);const n=await a.selectNativePage(e.platform,e.pageId);return{success:!0,page:_(a,n)}}});import{defineTool as ne}from"@roll-agent/sdk";import{z as re}from"zod";async function oe(e,t=300,a=800){const n=Math.floor(Math.random()*(a-t))+t;await e.waitForTimeout(n)}async function ie(e){const t=Math.random();let a;a=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(a))}async function se(e,t){const a=t?.minDistance??50,n=t?.maxDistance??200,r=t?.direction??"both",o=Math.floor(Math.random()*(n-a))+a,i="up"===r?-1:"down"===r||Math.random()>.5?1:-1;await e.evaluate(e=>{window.scrollBy({top:e,behavior:"smooth"})},o*i),await oe(e,200,500)}async function ce(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 ie(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 ue(e=.3){return Math.random()<e}import{setTimeout as de}from"node:timers/promises";var le="https://www.zhipin.com/web/geek/chat",me=".chat-list-wrap, .geek-item",ge=new Set(["消息"]),fe="data-roll-chat-entry-target",pe="data-roll-chat-item-target";function he(e){return e.trim().toLocaleLowerCase("zh-CN")}function be(e,t){let a=0;for(const n of e)t.includes(n)&&(a+=1);return a}function we(e,t){if(void 0!==t.index)return e[t.index];const a=t.candidateName;if(!a)return;const n=he(a),r=e.filter(e=>e.name.length>0);let o=r.find(e=>he(e.name)===n);if(o)return o;if(o=r.find(e=>{const t=he(e.name);return t.includes(n)||n.includes(t)}),o)return o;const i=n.length<=2?1:n.length<=4?.75:.6;return r.find(e=>{const t=he(e.name);return be(n,t)>=Math.ceil(Math.min(n.length,t.length)*i)})}function ye(e){return e.includes("/web/geek/chat")||e.includes("/web/chat")}async function xe(e,t=1e4){try{return await e.waitForSelector(me,{timeout:t}),!0}catch{return!1}}async function ve(e,t){const a=(await e.listAttachedPages()).find(e=>e!==t&&ye(e.url()));if(a)return await e.selectAttachedPage("zhipin",e.getPageId(a))}async function Se(e,t){await e.evaluate(e=>{document.querySelectorAll(`[${e}]`).forEach(t=>{t.removeAttribute(e)})},t).catch(()=>{})}async function ke(e,t){const a=e.locator(t).first();await a.scrollIntoViewIfNeeded(),await a.hover(),await oe(e,200,400),await a.click()}async function Ce(e){const t=await e.evaluate(e=>{const t=e=>{const t=e.getBoundingClientRect();return t.width>0&&t.height>0},a=t=>e.messageLabels.some(e=>t===e||t.includes(e));document.querySelectorAll(`[${e.markerAttr}]`).forEach(t=>{t.removeAttribute(e.markerAttr)});const n=Array.from(document.querySelectorAll('a[href*="/web/geek/chat"], a[href*="/web/chat"]'));for(const a of n)if(t(a))return a.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 n of r){if(a(n.textContent?.trim()??"")&&t(n))return n.setAttribute(e.markerAttr,"true"),{found:!0,selector:`[${e.markerAttr}="true"]`}}return{found:!1}},{markerAttr:fe,messageLabels:[...ge]});if(!t.found)return!1;try{return await ke(e,t.selector),!0}finally{await Se(e,fe)}}function Ae(e){return e instanceof Error&&/ERR_ABORTED/i.test(e.message)}async function Pe(e){try{return await e.goto(le,{waitUntil:"domcontentloaded"}),!0}catch(t){return!!Ae(t)&&(!!ye(e.url())||await xe(e,2e3))}}async function Ne(e,t){if(!await Ce(t))return!1;if(await xe(t,5e3))return!0;const a=await ve(e,t);return!!a&&await xe(a,5e3)}async function qe(e,t){if(ye(t.url())&&await xe(t))return!0;const a=await ve(e,t);if(a&&await xe(a))return!0;if(await Ne(e,t))return!0;if(!await Pe(t))return!1;if(await xe(t))return!0;await de(300);const n=await ve(e,t);return!!n&&await xe(n,5e3)}async function Ie(e){return e.evaluate(()=>Array.from(document.querySelectorAll(".geek-item")).map((e,t)=>{const a=e.querySelector('[class*="name"], .nickname, .geek-name, .candidate-name'),n=a?.textContent?.trim()??"",r=e.querySelector(".badge-count"),o=parseInt(r?.textContent?.trim()??"0",10)||0;return{name:n,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 _e(e,t){const a=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:pe,targetIndex:t});if(!a.found)return!1;try{return await ke(e,a.selector),!0}finally{await Se(e,pe)}}async function ze(e,t){if(0===t.length)return void await oe(e,500,900);const a=he(t);try{await e.waitForFunction(e=>{const t=[".name-box",".geek-name",".base-name",".chat-user-name"];for(const a of t){const t=document.querySelector(a)?.textContent?.trim();if(!t)continue;const n=t.trim().toLocaleLowerCase("zh-CN");if(n.includes(e)||e.includes(n))return!0}return!1},a,{timeout:5e3})}catch{await oe(e,800,1200)}}async function Me(e,t,a){if(void 0===a.candidateName&&void 0===a.index)return;if(!await qe(e,t))return{found:!1,name:"",index:-1,hasUnread:!1,unreadCount:0,lastMessageTime:"",messagePreview:"",error:"消息列表未加载"};const n=await e.getPage("zhipin"),r=we(await Ie(n),a);if(!r){return{found:!1,name:"",index:-1,hasUnread:!1,unreadCount:0,lastMessageTime:"",messagePreview:"",error:`未找到候选人: ${a.candidateName??`index ${a.index}`}`}}return await _e(n,r.index)?(await ze(n,r.name),{...r,found:!0}):{...r,found:!1,error:`打开候选人聊天失败: ${r.name||`index ${r.index}`}`}}var Ee=re.object({name:re.string(),position:re.string(),time:re.string(),preview:re.string(),unreadCount:re.number(),hasUnread:re.boolean(),index:re.number()}),Re=re.object({success:re.boolean(),candidates:re.array(Ee),total:re.number(),stats:re.object({withName:re.number(),withUnread:re.number()})}),$e=ne({name:"zhipin_read_messages",description:"读取 BOSS直聘未读候选人列表,支持过滤和排序",input:re.object({limit:re.number().optional().describe("最多返回条数"),onlyUnread:re.boolean().default(!0).describe("是否只返回有未读消息的候选人"),sortBy:re.enum(["time","unreadCount","name"]).default("time")}),output:Re,execute:async(e,t)=>{const a=e.onlyUnread??!0;t.logger.info(`Reading zhipin messages (limit: ${e.limit??"all"}, onlyUnread: ${a})`);const n=m(),r=await n.getPage("zhipin");if(!await qe(n,r))return{success:!1,candidates:[],total:0,stats:{withName:0,withUnread:0}};const o=await n.getPage("zhipin");await ce(o);const i=await o.evaluate(()=>{const e=document.querySelectorAll(".geek-item"),t=[];return e.forEach((e,a)=>{const n=e.querySelector('[class*="name"], .nickname, .geek-name, .candidate-name');let r=n?.textContent?.trim()??"";if(r.length>10){const e=r.match(/[\u4e00-\u9fa5]{2,4}/);e&&(r=e[0])}const o=e.querySelector(".source-job")?.textContent?.trim()??"",i=e.querySelector(".time, .time-shadow")?.textContent?.trim()??"",s=(e.querySelector(".push-text, .chat-last-msg")?.textContent?.trim()??"").slice(0,100);let c=0;const u=e.querySelector(".badge-count");u&&(c=parseInt(u.textContent?.trim()??"0",10)||0);const d=c>0||null!==e.querySelector(".red-dot");t.push({name:r,position:o,time:i,preview:s,unreadCount:c,hasUnread:d,index:a})}),t});let s=a?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 Be}from"@roll-agent/sdk";import{z as Te}from"zod";var je=Te.object({success:Te.boolean(),candidateName:Te.string(),index:Te.number(),hasUnread:Te.boolean(),unreadCount:Te.number(),lastMessageTime:Te.string(),messagePreview:Te.string(),error:Te.string().optional()}),Oe=Be({name:"zhipin_open_chat",description:"打开指定候选人的聊天窗口(按姓名模糊匹配或索引)",input:Te.object({candidateName:Te.string().optional().describe("候选人姓名。若用户说“打开鲁倩的聊天”,这里应提取为“鲁倩”"),index:Te.number().optional().describe("候选人在列表中的索引"),preferUnread:Te.boolean().default(!1).describe("优先选择有未读消息的候选人")}),output:je,execute:async(e,t)=>{t.logger.info(`Opening chat: name=${e.candidateName??"N/A"}, index=${e.index??"N/A"}`);const a=m(),n=await a.getPage("zhipin");let r={candidateName:e.candidateName,index:e.index};if(e.preferUnread&&void 0===e.candidateName&&void 0===e.index){if(!await qe(a,n))return{success:!1,candidateName:"",index:-1,hasUnread:!1,unreadCount:0,lastMessageTime:"",messagePreview:"",error:"消息列表未加载"};const e=await a.getPage("zhipin"),t=(await Ie(e)).find(e=>e.hasUnread);t&&(r={candidateName:t.name,index:t.index})}const o=await Me(a,n,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 Ue}from"@roll-agent/sdk";import{z as Fe}from"zod";var Le=Fe.object({index:Fe.number(),sender:Fe.enum(["candidate","recruiter","system"]),messageType:Fe.enum(["text","system","resume","wechat-exchange"]),content:Fe.string(),time:Fe.string()}),We=Fe.object({name:Fe.string(),age:Fe.string(),experience:Fe.string(),education:Fe.string(),communicationPosition:Fe.string(),expectedPosition:Fe.string(),expectedLocation:Fe.string(),expectedSalary:Fe.string(),tags:Fe.array(Fe.string())}),De=Fe.object({success:Fe.boolean(),candidateInfo:We,chatMessages:Fe.array(Le),formattedHistory:Fe.array(Fe.string()),stats:Fe.object({totalMessages:Fe.number(),candidateMessages:Fe.number(),recruiterMessages:Fe.number(),systemMessages:Fe.number()}),error:Fe.string().optional()}),Ve=Ue({name:"zhipin_get_candidate_info",description:"提取候选人资料和完整聊天记录。可指定 candidateName 自动打开对应聊天,或不传则读取当前窗口;例如“查看鲁倩的聊天详情”应提取 candidateName=鲁倩。",input:Fe.object({candidateName:Fe.string().optional().describe("候选人姓名。若用户说“查看鲁倩的聊天详情”,这里应提取为“鲁倩”"),index:Fe.number().optional().describe("候选人在列表中的索引(可选)"),maxMessages:Fe.number().default(100).describe("最多返回的消息条数")}),output:De,execute:async(e,t)=>{const a=e.maxMessages??100,n=m(),r=await n.getPage("zhipin"),o=await Me(n,r,{candidateName:e.candidateName,index:e.index});if(o&&!o.found){return{success:!1,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 n.getPage("zhipin");try{await i.waitForSelector(".chat-message-list .message-item, .conversation-message .message-item",{timeout:8e3})}catch{}const s=await i.evaluate(e=>{const t=document.querySelector(".base-info-single-detial, .base-info-content"),a=t?.querySelector(".name-box")?.textContent?.trim()??"",n=t?t.querySelectorAll(":scope > div"):document.querySelectorAll(".geek-info-item, .base-info-item"),r=[];n.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+年(?:以上)?|应届生|在校生)/),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 a=null!==t.querySelector(".item-friend"),n=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";a?s="candidate":n?s="recruiter":(r||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/),a=e.match(/微信[::号]*\s*([a-zA-Z0-9_-]{5,20})/);g=t?`[微信号: ${t[1]}]`:a?`[微信号: ${a[1]}]`:"[交换微信]"}else if(u){const e=t.querySelector(".message-card-top-title"),a=t.querySelector(".dialog-content, .message-card-top-text");g=(e?.textContent?.trim()??a?.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:a,age:s,experience:u,education:d,communicationPosition:l,expectedPosition:g,expectedLocation:f,expectedSalary:h,tags:b},messages:x}},a),c=s.messages.filter(e=>"candidate"===e.sender||"recruiter"===e.sender).map(e=>`${"candidate"===e.sender?"求职者":"我"}: ${e.content}`),u={totalMessages:s.messages.length,candidateMessages:s.messages.filter(e=>"candidate"===e.sender).length,recruiterMessages:s.messages.filter(e=>"recruiter"===e.sender).length,systemMessages:s.messages.filter(e=>"system"===e.sender).length};return t.logger.info(`Extracted info for ${s.candidateInfo.name}: ${u.totalMessages} messages`),{success:!0,candidateInfo:s.candidateInfo,chatMessages:s.messages,formattedHistory:c,stats:u}}});import{defineTool as He}from"@roll-agent/sdk";import{z as Ge}from"zod";var Je=Ge.object({success:Ge.boolean(),sentMessage:Ge.string(),error:Ge.string().optional()}),Ze=He({name:"zhipin_send_reply",description:"发送消息。可指定 candidateName 自动打开对应聊天后发送,或不传则发送到当前窗口;例如“回复鲁倩:你好”应提取 candidateName=鲁倩。",input:Ge.object({message:Ge.string().describe("要发送的消息内容"),candidateName:Ge.string().optional().describe("候选人姓名。若用户说“回复鲁倩”,这里应提取为“鲁倩”"),index:Ge.number().optional().describe("候选人在列表中的索引(可选)")}),output:Je,execute:async(e,t)=>{const{message:a}=e,n=m(),r=await n.getPage("zhipin"),o=await Me(n,r,{candidateName:e.candidateName,index:e.index});if(o&&!o.found)return{success:!1,sentMessage:a,error:o.error};t.logger.info(`Sending message (${a.length} chars)${o?` to ${o.name}`:""}`);const i=await n.getPage("zhipin");try{const e="#boss-chat-editor-input, textarea.chat-input, .chat-input";await i.waitForSelector(e,{timeout:5e3});if(await i.evaluate(e=>{const t=document.querySelector(e);return"true"===t?.getAttribute("contenteditable")},e)){const t=i.locator(e).first();await t.focus(),await i.evaluate(e=>{const t=document.querySelector(e.sel);t&&(t.innerHTML=e.msg.split("\n").map(e=>`<p>${e}</p>`).join(""))},{sel:e,msg:a}),await t.dispatchEvent("input",{bubbles:!0})}else await i.fill(e,a);await oe(i,200,500);const n=await i.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(!n.found)return{success:!1,sentMessage:a,error:"未找到发送按钮"};const r=i.locator(n.selector).first();return await r.scrollIntoViewIfNeeded(),await r.hover(),await ie(i),await r.click(),await oe(i,500,1200),t.logger.info("Message sent successfully"),{success:!0,sentMessage:a}}catch(e){return{success:!1,sentMessage:a,error:e instanceof Error?e.message:String(e)}}finally{await i.evaluate(()=>{document.querySelectorAll("[data-roll-send-btn]").forEach(e=>{e.removeAttribute("data-roll-send-btn")})}).catch(()=>{})}}});import{defineTool as Xe}from"@roll-agent/sdk";import{z as Qe}from"zod";var Ke=Qe.object({success:Qe.boolean(),exchanged:Qe.boolean(),wechatNumber:Qe.string().optional(),error:Qe.string().optional()}),Ye=Xe({name:"zhipin_exchange_wechat",description:'换微信。可指定 candidateName 自动打开对应聊天后执行,或不传则在当前窗口执行;例如"和鲁倩换微信"应提取 candidateName=鲁倩。',input:Qe.object({candidateName:Qe.string().optional().describe('候选人姓名。若用户说"和鲁倩换微信",这里应提取为"鲁倩"'),index:Qe.number().optional().describe("候选人在列表中的索引(可选)")}),output:Ke,execute:async(e,t)=>{const a=m(),n=await a.getPage("zhipin"),r=await Me(a,n,{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 a.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 a of t){const t=Array.from(document.querySelectorAll(a));for(const a of t){const t=a.textContent?.trim()??"";if(t.includes("换微信")&&e(a))return a.setAttribute("data-roll-wechat-btn","true"),{found:!0,text:t}}}const a=Array.from(document.querySelectorAll("span"));for(const t of a){const a=t.textContent?.trim()??"";if(a.includes("换微信")&&e(t))return t.setAttribute("data-roll-wechat-btn","true"),{found:!0,text:a}}return{found:!1}})).found)return{success:!1,exchanged:!1,error:"未找到「换微信」按钮"};await oe(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 oe(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 a=document.querySelectorAll("div, section, aside");for(const t of Array.from(a)){if((t.textContent??"").includes("交换微信")&&t.querySelector(".boss-btn-primary, .boss-btn")&&e(t))return!0}return!1})){e=!0;break}await oe(o,400,800)}if(!e)return{success:!1,exchanged:!1,error:"确认对话框未弹出"};await ie(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 a=[".btn-box .boss-btn-primary.boss-btn",".btn-box span.boss-btn-primary","span.boss-btn-primary",".boss-btn-primary"];for(const n of a){const a=t.querySelector(n);if(a&&e(a))return a.setAttribute("data-roll-confirm-btn","true"),{found:!0,text:a.textContent?.trim()??""}}}const a=document.querySelectorAll("div, section, aside");for(const t of Array.from(a)){if(!(t.textContent??"").includes("交换微信"))continue;const a=t.querySelectorAll("span.boss-btn-primary, button.boss-btn-primary, span.boss-btn, button.boss-btn");for(const t of Array.from(a)){const a=t.textContent?.trim()??"";if("确定"===a&&e(t))return t.setAttribute("data-roll-confirm-btn","true"),{found:!0,text:a}}}return{found:!1}})).found)return{success:!1,exchanged:!1,error:"未找到确认按钮"};await oe(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 oe(o,1500,2500);const a=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 a=e[t]?.textContent??"",n=a.match(/\b(\d{8,15})\b/);if(n)return n[1];const r=a.match(/微信[::号]*\s*([a-zA-Z0-9_-]{5,20})/);if(r)return r[1];const o=a.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 a=t[e]?.querySelector('.message-card-top-wrap, [class*="d-top-text"]');if(a){const e=(a.textContent??"").match(/\b(\d{8,15})\b/);if(e)return e[1]}}return null});return t.logger.info("WeChat exchanged"+(a?`, number: ${a}`:"")),{success:!0,exchanged:!0,...null!==a?{wechatNumber:a}:{}}}catch(e){return{success:!1,exchanged:!1,error:e instanceof Error?e.message:String(e)}}}});import{defineTool as et}from"@roll-agent/sdk";import{z as tt}from"zod";var at={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"}},nt=30,rt=new Set(["招聘规范","消息","首页","推荐牛人","看简历","我的客服","面试","招聘数据","账号权益","升级VIP","职位","职位管理","牛人","公司","数据统计","设置","帮助中心","登录","注册","退出登录","退出","BOSS直聘","下载APP","搜索","发布职位"]);function ot(e){const t=e.trim();return!(0===t.length||t.length>nt)&&(!rt.has(t)&&!/登录|注册|退出|下载|帮助|设置|管理/.test(t))}function it(e){const t=[],a=/(link|button|menuitem|img|heading)\s+"([^"]+)"/g;let n;for(;null!==(n=a.exec(e));){const e=n[2]?.trim()??"";e.length>0&&t.push({role:n[1]??"",name:e})}return t}function st(e){let t=e.priority;return rt.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 ct(e){const t=e.filter(e=>ot(e.text));if(0===t.length)return{found:!1};const a=new Map;for(const e of t){const t=e.text.trim(),n=a.get(t)??new Set;n.add(e.strategy),a.set(t,n)}const n=t.map(e=>{let t=st(e);const n=a.get(e.text.trim())?.size??1;return n>1&&(t-=.5*n),{evidence:e,score:t}}).sort((e,t)=>e.score-t.score)[0];return n?{found:!0,userName:n.evidence.text.trim(),strategy:n.evidence.strategy,source:n.evidence.source}:{found:!1}}async function ut(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 dt(e,t,a,n){const r=[],o=t.viewportSize(),i=o?.width??1280;try{const t=await e.getByRole(a).all();for(const e of t)try{if(!await e.isVisible())continue;const t=(await e.textContent()??"").trim();if(t.length>0&&t.length<=nt){let o;try{const t=await e.boundingBox();t&&(o=(t.x+t.width/2)/i)}catch{}r.push({text:t,strategy:n,priority:1,source:`role:${a}`,xRatio:o})}}catch{}}catch{}return r}async function lt(e){const t=[];try{const a=it(await e.ariaSnapshot({timeout:3e3}));for(const{role:e,name:n}of a)n.length>0&&n.length<=nt&&t.push({text:n,strategy:"aria-snapshot",priority:2,source:`aria:${e}:${n}`})}catch{}return t}async function mt(e){const t=[];try{const a=await e.evaluate((e,t)=>{const a=[],n=document.createTreeWalker(e,NodeFilter.SHOW_TEXT);for(;n.nextNode();){const e=n.currentNode.textContent?.trim();e&&e.length>0&&e.length<=t&&a.push(e)}return a},nt);for(const e of a)t.push({text:e,strategy:"leaf-text",priority:3,source:"leaf-text"})}catch{}return t}async function gt(e){const t=[];try{const a=[at.username.primary,...at.username.fallbacks],n=await e.evaluate(e=>e.map(e=>{try{return{selector:e,text:document.querySelector(e)?.textContent?.trim()??""}}catch{return{selector:e,text:""}}}),a);for(const{selector:e,text:a}of n)a.length>0&&a.length<=nt&&t.push({text:a,strategy:"css-fallback",priority:4,source:e})}catch{}return t}async function ft(e){const t=await ut(e),a=t?(await Promise.all([dt(t,e,"link","role-link"),dt(t,e,"button","role-button"),lt(t)])).flat():[],n=ct(a);if(n.found){const e=n.userName;if(new Set(a.filter(t=>t.text.trim()===e).map(e=>e.strategy)).size>=2)return a}const r=(await Promise.all([t?mt(t):Promise.resolve([]),gt(e)])).flat();return[...a,...r]}var pt=tt.object({success:tt.boolean(),userName:tt.string(),usedSelector:tt.string().optional(),usedStrategy:tt.string().optional(),source:tt.string().optional(),error:tt.string().optional()}),ht=et({name:"zhipin_get_username",description:"获取当前登录的招聘者用户名",input:tt.object({}),output:pt,execute:async(e,t)=>{t.logger.info("Getting zhipin username");try{const e=m(),a=await e.getPage("zhipin");await a.bringToFront().catch(()=>{});const n=ct(await ft(a));return n.found?(t.logger.info(`Username: ${n.userName} (strategy: ${n.strategy}, source: ${n.source})`),{success:!0,userName:n.userName,usedSelector:"css-fallback"===n.strategy?n.source:void 0,usedStrategy:n.strategy,source:n.source}):{success:!1,userName:"",error:"未找到用户名,请确认当前页面已登录招聘者账号。"}}catch(e){return{success:!1,userName:"",error:e instanceof Error?`获取用户名失败:${e.message}`:"获取用户名失败"}}}});import{defineTool as bt}from"@roll-agent/sdk";import{z as wt}from"zod";var yt=wt.object({index:wt.number(),candidateId:wt.string(),name:wt.string(),age:wt.string(),experience:wt.string(),education:wt.string(),workStatus:wt.string(),company:wt.string(),currentPosition:wt.string(),expectedLocation:wt.string(),expectedPosition:wt.string(),expectedSalary:wt.string(),tags:wt.array(wt.string()),buttonText:wt.string()}),xt=wt.object({success:wt.boolean(),candidates:wt.array(yt),total:wt.number(),error:wt.string().optional()}),vt=bt({name:"zhipin_get_candidate_list",description:"获取推荐列表页的候选人卡片信息",input:wt.object({maxResults:wt.number().optional().describe("最多返回条数")}),output:xt,execute:async(e,t)=>{t.logger.info("Getting candidate list from recommend page");const a=m(),n=await a.getPage("zhipin"),r=n.frame("recommendFrame")??n.frames().find(e=>e.url().includes("recommend"))??n;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 a=e??t.length,n=[];return t.forEach((e,t)=>{if(t>=a)return;const r=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 a=t.textContent?.trim();a&&e.push(a)}),e.length<=1&&(e.length=0,d.childNodes.forEach(t=>{if(t.nodeType===Node.TEXT_NODE){const a=t.textContent?.trim();a&&e.push(a)}})),e.length<=1){const t=d.textContent?.trim()??"";e.length=0,t.split(/[丨·|]/).forEach(t=>{const a=t.trim();a&&e.push(a)})}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"),a=e?.textContent??"";if((a.includes("期望")||a.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()??"";n.push({index:t,candidateId:r,name:o,age:i,experience:s,education:c,workStatus:u,company:g,currentPosition:f,expectedLocation:p,expectedPosition:h,expectedSalary:w,tags:y,buttonText:x})}),n},e.maxResults);return t.logger.info(`Found ${o.length} candidates in recommend list`),{success:!0,candidates:o,total:o.length}}});import{defineTool as St}from"@roll-agent/sdk";import{z as kt}from"zod";var Ct=".candidate-card-wrap",At="[data-geek], .geek-item",Pt=`${Ct}, ${At}`;function Nt(e){return e.evaluate(e=>document.querySelectorAll(e.primarySelector).length>0?e.primarySelector:e.fallbackSelector,{primarySelector:Ct,fallbackSelector:At})}function qt(e){return e.frame("recommendFrame")??e.frames().find(e=>e.url().includes("recommend"))??e}async function It(e,t=1e4){try{return await e.waitForSelector(Pt,{timeout:t}),!0}catch{return!1}}async function _t(e,t){const a=await Nt(e),n=e.locator(a);if(await n.count()<=t)return{found:!1,cardSelector:a,candidateId:"",name:"",hasGreetButton:!1,error:"索引超出范围"};const r=n.nth(t),o=await r.evaluate(e=>{const t=e.getAttribute("data-geek")??e.querySelector("[data-geek]")?.getAttribute("data-geek")??"",a=e.querySelector(".name")?.textContent?.trim()??"",n=e.querySelector("button.btn.btn-greet");return{candidateId:t,name:a,hasGreetButton:null!==n&&n.offsetWidth>0}});return{found:!0,cardSelector:a,candidateId:o.candidateId,name:o.name,hasGreetButton:o.hasGreetButton}}var zt=kt.object({index:kt.number(),candidateName:kt.string(),candidateId:kt.string(),success:kt.boolean(),error:kt.string().optional()}),Mt=kt.object({success:kt.boolean(),results:kt.array(zt),summary:kt.object({total:kt.number(),succeeded:kt.number(),failed:kt.number()})}),Et=St({name:"zhipin_say_hello",description:"在推荐列表页对候选人点击「打招呼」按钮(支持批量)",input:kt.object({indices:kt.array(kt.number()).describe("要打招呼的候选人索引列表")}),output:Mt,execute:async(e,t)=>{t.logger.info(`Saying hello to ${e.indices.length} candidates`);const a=m(),n=await a.getPage("zhipin"),r=qt(n);if(!await It(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 _t(r,t);if(e.found)if(e.hasGreetButton){const a=r.locator(e.cardSelector).nth(t).locator("button.btn.btn-greet").first();await a.scrollIntoViewIfNeeded(),await a.hover(),await ie(n),await a.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 ie(n),ue(.3)&&await se(n)}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 Rt}from"@roll-agent/sdk";import{z as $t}from"zod";var Bt=$t.object({success:$t.boolean(),candidateName:$t.string(),candidateId:$t.string(),error:$t.string().optional()}),Tt=Rt({name:"zhipin_open_resume",description:"在推荐列表页点击候选人卡片打开简历详情弹窗",input:$t.object({index:$t.number().describe("候选人在列表中的索引")}),output:Bt,execute:async(e,t)=>{t.logger.info(`Opening resume for candidate at index ${e.index}`);const a=m(),n=await a.getPage("zhipin"),r=qt(n);if(!await It(r))return{success:!1,candidateName:"",candidateId:"",error:"推荐列表未加载"};const o=await _t(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 oe(n,200,400),await s.click(),await oe(n,1e3,2e3),t.logger.info(`Opened resume for ${o.name}`),{success:!0,candidateName:o.name,candidateId:o.candidateId}}});import{defineTool as jt}from"@roll-agent/sdk";import{z as Ot}from"zod";var Ut=Ot.object({x:Ot.number(),y:Ot.number(),width:Ot.number(),height:Ot.number()}),Ft=Ot.object({success:Ot.boolean(),screenshotArea:Ut.optional(),canvasInfo:Ot.object({width:Ot.number(),height:Ot.number()}).optional(),error:Ot.string().optional()}),Lt=jt({name:"zhipin_locate_resume_canvas",description:"定位简历详情中嵌套 iframe 内的 canvas 元素坐标(用于截图)",input:Ot.object({}),output:Ft,execute:async(e,t)=>{t.logger.info("Locating resume canvas in nested iframes");const a=m(),n=await a.getPage("zhipin");try{const e=n.frame("recommendFrame")??n.frames().find(e=>e.url().includes("recommend"));if(!e)return{success:!1,error:"未找到推荐页 iframe"};const a=await e.$('iframe[src*="c-resume"]');if(!a)return{success:!1,error:"未找到简历 iframe"};const r=await a.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 n.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 Wt}from"@roll-agent/sdk";import{z as Dt}from"zod";var Vt=Dt.object({success:Dt.boolean(),closed:Dt.boolean(),error:Dt.string().optional()}),Ht=[".recommendV2 .boss-popup__close",".dialog-lib-resume .boss-popup__close",".boss-dialog .boss-popup__close",".boss-popup__close",".close-btn",".dialog-close"],Gt=[".boss-popup__close",".close-btn",".dialog-close",".modal-close"],Jt=Wt({name:"zhipin_close_resume",description:"关闭简历详情弹窗",input:Dt.object({}),output:Vt,execute:async(e,t)=>{t.logger.info("Closing resume detail modal");const a=m(),n=await a.getPage("zhipin"),r=n.frame("recommendFrame")??n.frames().find(e=>e.url().includes("recommend"));if(!await(async()=>{if(r)for(const e of Ht){const t=await r.$(e);if(t&&await t.isVisible())return await t.click(),!0}for(const e of Gt){const t=await n.$(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 n.waitForTimeout(300);const e=r?await r.$(".boss-popup__wrapper, .dialog-lib-resume, .boss-dialog"):await n.$(".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 Zt}from"@roll-agent/sdk";import{z as Xt}from"zod";import{waitForSelector as Qt}from"@roll-agent/browser";var Kt={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 Yt,waitForSelector as ea}from"@roll-agent/browser";var ta="https://www.yupao.com",aa=`${ta}/chat`,na=`${ta}/login`;async function ra(e){e.url().includes("/chat")||await Yt(e,aa),await ea(e,Kt.messageList.container,{timeout:15e3})}async function oa(e,t){const a=`${ta}/chat?id=${encodeURIComponent(t)}`;await Yt(e,a),await ea(e,Kt.chat.input,{timeout:15e3})}async function ia(e,t){await ra(e),await Qt(e,Kt.messageList.item,{timeout:1e4});const a=Kt.messageList;return await e.$$eval(a.item,(e,t)=>{const a=[],n=t.maxItems?e.slice(0,t.maxItems):e;for(const e of n){const n=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]??"";a.push({conversationId:s,candidateName:n?.textContent?.trim()??"",lastMessage:r?.textContent?.trim()??"",unreadCount:parseInt(o?.textContent?.trim()??"0",10)||0,timestamp:i?.textContent?.trim()??""})}return a},{sel:a,maxItems:t})}var sa=Xt.object({limit:Xt.number().optional().describe("最多返回的消息条数")}),ca=Xt.object({conversationId:Xt.string(),candidateName:Xt.string(),lastMessage:Xt.string(),unreadCount:Xt.number(),timestamp:Xt.string()}),ua=Xt.object({messages:Xt.array(ca),total:Xt.number()}),da=Zt({name:"yupao_read_messages",description:"读取鱼泡未读消息列表",input:sa,output:ua,execute:async(e,t)=>{t.logger.info(`Reading yupao messages (limit: ${e.limit??"all"})`);const a=m(),n=await a.getPage("yupao"),r=await ia(n,e.limit);return t.logger.info(`Found ${r.length} messages`),{messages:r.map(e=>({...e})),total:r.length}}});import{defineTool as la}from"@roll-agent/sdk";import{z as ma}from"zod";import{waitForSelector as ga,typeText as fa,clickElement as pa}from"@roll-agent/browser";async function ha(e,t,a){try{return await oa(e,t),await fa(e,Kt.chat.input,a),await pa(e,Kt.chat.sendButton),await ga(e,Kt.chat.messageItem,{timeout:5e3}),{success:!0}}catch(e){return{success:!1,error:e instanceof Error?e.message:String(e)}}}var ba=ma.object({conversationId:ma.string().describe("对话 ID"),message:ma.string().describe("要发送的回复消息")}),wa=ma.object({success:ma.boolean(),conversationId:ma.string(),sentMessage:ma.string(),error:ma.string().optional()}),ya=la({name:"yupao_send_reply",description:"向鱼泡指定对话发送回复消息",input:ba,output:wa,execute:async(e,t)=>{const{conversationId:a,message:n}=e;t.logger.info(`Sending reply to yupao conversation ${a}`);const r=m(),o=await r.getPage("yupao"),i=await ha(o,a,n);return i.success?t.logger.info("Reply sent successfully"):t.logger.error(`Failed to send reply: ${i.error}`),{success:i.success,conversationId:a,sentMessage:n,error:i.error}}});function xa(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 va(e,t){if(void 0===e)return;const a=Number.parseInt(e,10);if(!Number.isInteger(a))throw new Error(`${t} must be an integer, received "${e}".`);return a}function Sa(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 ka(){return t.parse({mode:process.env.BROWSER_MODE,headless:xa(process.env.BROWSER_HEADLESS),cdpUrl:process.env.BROWSER_CDP_URL,cdpHost:process.env.BROWSER_CDP_HOST,cdpPort:va(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:Sa(process.env.BROWSER_ARGS_JSON),sessionsDir:process.env.BROWSER_SESSIONS_DIR})}var Ca=e({name:"browser-use-agent",tools:[x,R,U,Z,ae,$e,Oe,Ve,Ze,Ye,ht,vt,Et,Tt,Lt,Jt,da,ya,h]},{onShutdown:f});async function Aa(){await d(ka()),await Ca.listen({transport:{type:"http",port:parseInt(process.env.BROWSER_AGENT_PORT??"3100",10),host:process.env.BROWSER_AGENT_HOST??"127.0.0.1"}})}Aa().catch(async e=>{console.error("Fatal error:",e),await f().catch(()=>{}),process.exit(1)});
1
+ import{defineAgent as e}from"@roll-agent/sdk";import{BrowserRuntimeConfigSchema as t}from"@roll-agent/browser";import{defineTool as n}from"@roll-agent/sdk";import{z as a}from"zod";import{BrowserRuntime as r,BrowserContextManager as o,SessionStore as i}from"@roll-agent/browser";var s,c,u;async function d(e){s||(u=new i(e.sessionsDir),s=new r(e),await s.start(),c=new o(s,u))}function l(){if(!s)throw new Error("BrowserRuntime not initialized. Call initRuntime() first.");return s}function m(){if(!c)throw new Error("BrowserContextManager not initialized. Call initRuntime() first.");return c}function g(){if(!u)throw new Error("SessionStore not initialized. Call initRuntime() first.");return u}async function f(){c&&(console.error("[browser-use-agent] Closing browser contexts..."),await c.closeAll(),c=void 0),s&&(console.error("[browser-use-agent] Stopping browser process..."),await s.stop(),s=void 0),u=void 0,console.error("[browser-use-agent] Browser runtime shutdown complete")}var p=a.object({success:a.boolean(),mode:a.string(),connected:a.boolean()}),h=n({name:"attach_browser_session",description:"调试工具:显式执行一次 connectOverCDP(),仅建立 Playwright Browser 连接,不做页面导航或 DOM 操作。",input:a.object({}),output:p,execute:async(e,t)=>{const n=l();return t.logger.info("Attaching Playwright browser session over CDP"),await n.getBrowser(),{success:!0,mode:n.mode,connected:!0}}});import{defineTool as b}from"@roll-agent/sdk";import{z as w}from"zod";import{BrowserStatusSchema as y}from"@roll-agent/browser";var x=b({name:"browser_status",description:"查询浏览器运行状态和活跃 session 信息",input:w.object({}),output:y,execute:async(e,t)=>{t.logger.info("Querying browser status");const n=l(),a=m(),r=g(),o=n.isRunning(),{headless:i,mode:s}=n.getConfig(),c=a.getActivePlatforms(),u=[];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");u.push({platform:e,pagesOpen:t,currentUrl:o,hasLoginState:i,loginStateSource:s})}return{running:o,headless:i,mode:s,activeSessions:u}}});import{defineTool as v}from"@roll-agent/sdk";import{BrowserPageInfoSchema as S,PlatformSchema as k}from"@roll-agent/browser";import{z as I}from"zod";import{PLATFORMS as A}from"@roll-agent/browser";var C={zhipin:"https://www.zhipin.com",yupao:"https://www.yupao.com"};function P(e){return new URL(C[e]).host}function E(e,t){try{return new URL(e).host.includes(P(t))}catch{return!1}}function q(e){return A.find(t=>E(e,t))}function N(e,t){const n=q(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 _(e,t){const n=t.url();return{pageId:e.getPageId(t),url:n,title:await t.title().catch(()=>""),boundPlatform:e.getBoundPlatformForPage(t)??null,detectedPlatform:q(n)??null,isSelectedForPlatform:e.isSelectedPageForPlatform(t)}}var z=I.object({platform:k.optional().describe("可选:仅返回指定平台相关的页面")}),M=I.object({pages:I.array(S)}),R=v({name:"list_pages",description:"通过原生 CDP 列出当前浏览器可见页面及其可选择的 pageId;登录前该值等同于原生 targetId。",input:z,output:M,execute:async(e,t)=>{const n=m();t.logger.info("Listing browser pages");const a=(await n.listNativePages()).map(e=>N(n,e));return{pages:void 0===e.platform?a:a.filter(t=>t.boundPlatform===e.platform||t.detectedPlatform===e.platform)}}});import{defineTool as B}from"@roll-agent/sdk";import{BrowserPageInfoSchema as $}from"@roll-agent/browser";import{z as T}from"zod";var j=T.object({url:T.string().url().describe("要导航到的目标 URL")}),U=T.object({success:T.boolean(),page:$}),O=B({name:"navigate_active_tab",description:"将当前激活的浏览器 tab 导航到指定 URL;若 URL 属于已知平台,会自动绑定该平台当前活跃页。",input:j,output:U,execute:async(e,t)=>{const n=m();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=q(a.url());return r?await n.selectAttachedPage(r,n.getPageId(a)):n.clearBindingForPage(a),{success:!0,page:await _(n,a)}}});import{defineTool as F}from"@roll-agent/sdk";import{BrowserPageInfoSchema as L,PlatformSchema as D}from"@roll-agent/browser";import{z as W}from"zod";async function H(e,t){return(await e.listNativePages()).find(e=>E(e.url,t))}async function V(e,t){const n=await H(e,t);if(n)return await e.activateNativePage(n.targetId),{page:n,reusedExistingPage:!0};return{page:await e.openNativePage(C[t]),reusedExistingPage:!1}}var G=W.object({platform:D.describe("目标平台:`zhipin` 代表 BOSS直聘,`yupao` 代表鱼泡")}),J=W.object({success:W.boolean(),page:L,reusedExistingTab:W.boolean()}),Y=F({name:"open_platform",description:"打开并聚焦招聘平台主页,供用户手动登录或后续执行站内操作。",input:G,output:J,execute:async(e,t)=>{const{platform:n}=e,a=l(),r=m();t.logger.info(`Opening platform page for ${n}`);const{page:o,reusedExistingPage:i}=await V(a,n);return r.rememberNativePageSelection(n,o),{success:!0,page:N(r,o),reusedExistingTab:i}}});import{defineTool as K}from"@roll-agent/sdk";import{BrowserPageInfoSchema as Z,PlatformSchema as X}from"@roll-agent/browser";import{z as Q}from"zod";var ee=Q.object({platform:X.describe("要将该页面绑定为当前活跃页的平台"),pageId:Q.string().describe("通过 list_pages 返回的 pageId;登录前就是原生 targetId,登录后仍可作为稳定选择句柄")}),te=Q.object({success:Q.boolean(),page:Z}),ne=K({name:"select_page",description:"将指定 pageId 绑定为平台当前活跃页,并切换到前台;登录前走原生 CDP target 激活。",input:ee,output:te,execute:async(e,t)=>{const n=m();t.logger.info(`Selecting page ${e.pageId} for ${e.platform}`);const a=await n.selectNativePage(e.platform,e.pageId);return{success:!0,page:N(n,a)}}});import{defineTool as ae}from"@roll-agent/sdk";import{z as re}from"zod";async function oe(e,t=300,n=800){const a=Math.floor(Math.random()*(n-t))+t;await e.waitForTimeout(a)}async function ie(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 oe(e,200,500)}async function ce(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 ie(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 ue(e=.3){return Math.random()<e}import{setTimeout as de}from"node:timers/promises";var le="https://www.zhipin.com/web/geek/chat",me=".chat-list-wrap, .geek-item",ge=new Set(["消息"]),fe="data-roll-chat-entry-target",pe="data-roll-chat-item-target";function he(e){return e.trim().toLocaleLowerCase("zh-CN")}function be(e,t){let n=0;for(const a of e)t.includes(a)&&(n+=1);return n}function we(e,t){if(void 0!==t.index)return e[t.index];const n=t.candidateName;if(!n)return;const a=he(n),r=e.filter(e=>e.name.length>0);let o=r.find(e=>he(e.name)===a);if(o)return o;if(o=r.find(e=>{const t=he(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=he(e.name);return be(a,t)>=Math.ceil(Math.min(a.length,t.length)*i)})}function ye(e){return e.includes("/web/geek/chat")||e.includes("/web/chat")}async function xe(e,t=1e4){try{return await e.waitForSelector(me,{timeout:t}),!0}catch{return!1}}async function ve(e,t){const n=(await e.listAttachedPages()).find(e=>e!==t&&ye(e.url()));if(n)return await e.selectAttachedPage("zhipin",e.getPageId(n))}async function Se(e,t){await e.evaluate(e=>{document.querySelectorAll(`[${e}]`).forEach(t=>{t.removeAttribute(e)})},t).catch(()=>{})}async function ke(e,t){const n=e.locator(t).first();await n.scrollIntoViewIfNeeded(),await n.hover(),await oe(e,200,400),await n.click()}async function Ie(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:fe,messageLabels:[...ge]});if(!t.found)return!1;try{return await ke(e,t.selector),!0}finally{await Se(e,fe)}}function Ae(e){return e instanceof Error&&/ERR_ABORTED/i.test(e.message)}async function Ce(e){try{return await e.goto(le,{waitUntil:"domcontentloaded"}),!0}catch(t){return!!Ae(t)&&(!!ye(e.url())||await xe(e,2e3))}}async function Pe(e,t){if(!await Ie(t))return!1;if(await xe(t,5e3))return!0;const n=await ve(e,t);return!!n&&await xe(n,5e3)}async function Ee(e,t){if(ye(t.url())&&await xe(t))return!0;const n=await ve(e,t);if(n&&await xe(n))return!0;if(await Pe(e,t))return!0;if(!await Ce(t))return!1;if(await xe(t))return!0;await de(300);const a=await ve(e,t);return!!a&&await xe(a,5e3)}async function qe(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 Ne(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:pe,targetIndex:t});if(!n.found)return!1;try{return await ke(e,n.selector),!0}finally{await Se(e,pe)}}async function _e(e,t){if(0===t.length)return void await oe(e,500,900);const n=he(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 oe(e,800,1200)}}async function ze(e,t,n){if(void 0===n.candidateName&&void 0===n.index)return;if(!await Ee(e,t))return{found:!1,name:"",index:-1,hasUnread:!1,unreadCount:0,lastMessageTime:"",messagePreview:"",error:"消息列表未加载"};const a=await e.getPage("zhipin"),r=we(await qe(a),n);if(!r){return{found:!1,name:"",index:-1,hasUnread:!1,unreadCount:0,lastMessageTime:"",messagePreview:"",error:`未找到候选人: ${n.candidateName??`index ${n.index}`}`}}return await Ne(a,r.index)?(await _e(a,r.name),{...r,found:!0}):{...r,found:!1,error:`打开候选人聊天失败: ${r.name||`index ${r.index}`}`}}var Me=re.object({name:re.string(),conversationId:re.string(),candidateId:re.string(),position:re.string(),time:re.string(),preview:re.string(),unreadCount:re.number(),hasUnread:re.boolean(),index:re.number()}),Re=re.object({success:re.boolean(),candidates:re.array(Me),total:re.number(),stats:re.object({withName:re.number(),withUnread:re.number()})}),Be=ae({name:"zhipin_read_messages",description:"读取 BOSS直聘未读候选人列表,支持过滤和排序",input:re.object({limit:re.number().optional().describe("最多返回条数"),onlyUnread:re.boolean().default(!0).describe("是否只返回有未读消息的候选人"),sortBy:re.enum(["time","unreadCount","name"]).default("time")}),output:Re,execute:async(e,t)=>{const n=e.onlyUnread??!0;t.logger.info(`Reading zhipin messages (limit: ${e.limit??"all"}, onlyUnread: ${n})`);const a=m(),r=await a.getPage("zhipin");if(!await Ee(a,r))return{success:!1,candidates:[],total:0,stats:{withName:0,withUnread:0}};const o=await a.getPage("zhipin");await ce(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()??"",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:a,candidateId:r,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 $e}from"@roll-agent/sdk";import{z as Te}from"zod";var je=Te.object({success:Te.boolean(),candidateName:Te.string(),index:Te.number(),hasUnread:Te.boolean(),unreadCount:Te.number(),lastMessageTime:Te.string(),messagePreview:Te.string(),error:Te.string().optional()}),Ue=$e({name:"zhipin_open_chat",description:"打开指定候选人的聊天窗口(按姓名模糊匹配或索引)",input:Te.object({candidateName:Te.string().optional().describe("候选人姓名。若用户说“打开鲁倩的聊天”,这里应提取为“鲁倩”"),index:Te.number().optional().describe("候选人在列表中的索引"),preferUnread:Te.boolean().default(!1).describe("优先选择有未读消息的候选人")}),output:je,execute:async(e,t)=>{t.logger.info(`Opening chat: name=${e.candidateName??"N/A"}, index=${e.index??"N/A"}`);const n=m(),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 Ee(n,a))return{success:!1,candidateName:"",index:-1,hasUnread:!1,unreadCount:0,lastMessageTime:"",messagePreview:"",error:"消息列表未加载"};const e=await n.getPage("zhipin"),t=(await qe(e)).find(e=>e.hasUnread);t&&(r={candidateName:t.name,index:t.index})}const o=await ze(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 Oe}from"@roll-agent/sdk";import{z as Fe}from"zod";var Le=".geek-item.selected";async function De(e){try{await e.waitForSelector(Le,{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 We=Fe.object({index:Fe.number(),sender:Fe.enum(["candidate","recruiter","system"]),messageType:Fe.enum(["text","system","resume","wechat-exchange"]),content:Fe.string(),time:Fe.string()}),He=Fe.object({name:Fe.string(),age:Fe.string(),experience:Fe.string(),education:Fe.string(),communicationPosition:Fe.string(),expectedPosition:Fe.string(),expectedLocation:Fe.string(),expectedSalary:Fe.string(),tags:Fe.array(Fe.string())}),Ve=Fe.object({success:Fe.boolean(),conversationId:Fe.string(),candidateId:Fe.string(),candidateInfo:He,chatMessages:Fe.array(We),formattedHistory:Fe.array(Fe.string()),stats:Fe.object({totalMessages:Fe.number(),candidateMessages:Fe.number(),recruiterMessages:Fe.number(),systemMessages:Fe.number()}),error:Fe.string().optional()}),Ge=Oe({name:"zhipin_get_candidate_info",description:"提取候选人资料和完整聊天记录。可指定 candidateName 自动打开对应聊天,或不传则读取当前窗口;例如“查看鲁倩的聊天详情”应提取 candidateName=鲁倩。",input:Fe.object({candidateName:Fe.string().optional().describe("候选人姓名。若用户说“查看鲁倩的聊天详情”,这里应提取为“鲁倩”"),index:Fe.number().optional().describe("候选人在列表中的索引(可选)"),maxMessages:Fe.number().default(100).describe("最多返回的消息条数")}),output:Ve,execute:async(e,t)=>{const n=e.maxMessages??100,a=m(),r=await a.getPage("zhipin"),o=await ze(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 De(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+年(?:以上)?|应届生|在校生)/),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"),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 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 Je}from"@roll-agent/sdk";import{z as Ye}from"zod";var Ke={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"}},Ze=30,Xe=new Set(["招聘规范","消息","首页","推荐牛人","看简历","我的客服","面试","招聘数据","账号权益","升级VIP","职位","职位管理","牛人","公司","数据统计","设置","帮助中心","登录","注册","退出登录","退出","BOSS直聘","下载APP","搜索","发布职位"]);function Qe(e){const t=e.trim();return!(0===t.length||t.length>Ze)&&(!Xe.has(t)&&!/登录|注册|退出|下载|帮助|设置|管理/.test(t))}function et(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 tt(e){let t=e.priority;return Xe.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 nt(e){const t=e.filter(e=>Qe(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=tt(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 at(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 rt(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<=Ze){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 ot(e){const t=[];try{const n=et(await e.ariaSnapshot({timeout:3e3}));for(const{role:e,name:a}of n)a.length>0&&a.length<=Ze&&t.push({text:a,strategy:"aria-snapshot",priority:2,source:`aria:${e}:${a}`})}catch{}return t}async function it(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},Ze);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=[Ke.username.primary,...Ke.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<=Ze&&t.push({text:n,strategy:"css-fallback",priority:4,source:e})}catch{}return t}async function ct(e){const t=await at(e),n=t?(await Promise.all([rt(t,e,"link","role-link"),rt(t,e,"button","role-button"),ot(t)])).flat():[],a=nt(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?it(t):Promise.resolve([]),st(e)])).flat();return[...n,...r]}async function ut(e,t=ct){const n=nt(await t(e));if(!n.found)throw new Error("未找到用户名,请确认当前页面已登录招聘者账号。");return{platform:"zhipin",username:n.username,strategy:n.strategy,source:n.source}}function dt(e,t){return void 0!==t.accountId?e.accountId===t.accountId:e.username===t.username}import{z as lt}from"zod";var mt="reply-authority-service",gt="browser-use-agent/zhipin_send_reply",ft="zhipin",pt=60,ht=lt.object({platform:lt.literal(ft),username:lt.string().min(1),accountId:lt.string().min(1).optional()}),bt=lt.object({v:lt.literal(2),iss:lt.literal(mt),kid:lt.string().min(1),jti:lt.string().min(1),iat:lt.number().int(),exp:lt.number().int(),aud:lt.literal(gt),platform:lt.literal(ft),tenantId:lt.string().min(1),conversationId:lt.string().min(1),candidateId:lt.string().min(1),reply:lt.string(),policyVersion:lt.string().min(1),recruiterBinding:ht}),wt=lt.object({kid:lt.string().min(1),algorithm:lt.literal("Ed25519"),publicKey:lt.string().min(1),validFrom:lt.string().min(1),validUntil:lt.string().optional()}),yt=lt.object({keys:lt.array(wt)}),xt=new Map;function vt(e){for(const[t,n]of xt)n<e-pt&&xt.delete(t)}function St(e,t=Math.floor(Date.now()/1e3)){return vt(t),xt.has(e)}function kt(e,t,n=Math.floor(Date.now()/1e3)){vt(n),xt.set(e,t)}import{createPublicKey as It,verify as At}from"node:crypto";var Ct=new Map,Pt=null;function Et(){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 qt(e){return Ct=new Map(e.map(e=>[e.kid,e]))}async function Nt(){if(Pt)return Pt;Pt=(async()=>{const e=await fetch(Et()),t=await e.json();if(!e.ok)throw new Error(`Reply Authority 公钥拉取失败 (${e.status})`);return qt(yt.parse(t).keys)})();try{return await Pt}finally{Pt=null}}async function _t(){await Nt()}async function zt(e){const t=Ct.get(e);if(t)return t;const n=(await Nt()).get(e);if(!n)throw new Error(`Unknown key ID: ${e}`);return n}function Mt(e){if("object"==typeof e&&null!==e&&"v"in e&&"number"==typeof e.v&&2!==e.v)throw new Error("unexpected envelope version")}function Rt(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")}Mt(r);const i=bt.safeParse(r);if(!i.success)throw new Error("Envelope payload schema validation failed");return{payload:i.data,payloadJson:o,signatureBase64:a}}function Bt(e,t){if(e.exp<=e.iat)throw new Error("Envelope expiry must be after issue time");if(e.exp<t-pt)throw new Error("Envelope expired");if(e.iat>t+pt)throw new Error("Envelope issued in the future")}async function $t(e,t=Math.floor(Date.now()/1e3)){const n=Rt(e),a=await zt(n.payload.kid),r=It({key:Buffer.from(a.publicKey,"base64url"),format:"der",type:"spki"});if(!At(null,Buffer.from(n.payloadJson,"utf-8"),r,Buffer.from(n.signatureBase64,"base64url")))throw new Error("Signature verification failed");return Bt(n.payload,t),n.payload}var Tt=Ye.object({success:Ye.boolean(),sentMessage:Ye.string(),error:Ye.string().optional()}),jt=Je({name:"zhipin_send_reply",description:"发送消息。只接受由 Reply Authority Service 签发的 signedEnvelope;可指定 candidateName 自动打开对应聊天后发送,或不传则发送到当前选中的聊天窗口。",input:Ye.object({signedEnvelope:Ye.string().describe("Reply Authority Service 返回的紧凑签名信封"),candidateName:Ye.string().optional().describe("候选人姓名。若用户说“回复鲁倩”,这里应提取为“鲁倩”"),index:Ye.number().optional().describe("候选人在列表中的索引(可选)")}),output:Tt,execute:async(e,t)=>{let n="";const a=m(),r=await a.getPage("zhipin");let o=r;try{const i=await $t(e.signedEnvelope);if(n=i.reply,St(i.jti))return{success:!1,sentMessage:n,error:"token 已消费,禁止重放"};const s=await ze(a,r,{candidateName:e.candidateName,index:e.index});if(s&&!s.found)return{success:!1,sentMessage:n,error:s.error};if(!s){if(!await Ee(a,r))return{success:!1,sentMessage:n,error:"消息列表未加载"}}o=await a.getPage("zhipin");const c=await De(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 ut(o);if(!dt(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 oe(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 ie(o),await m.click(),await oe(o,500,1200),kt(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 Ut}from"@roll-agent/sdk";import{z as Ot}from"zod";var Ft=Ot.object({success:Ot.boolean(),exchanged:Ot.boolean(),wechatNumber:Ot.string().optional(),error:Ot.string().optional()}),Lt=Ut({name:"zhipin_exchange_wechat",description:'换微信。可指定 candidateName 自动打开对应聊天后执行,或不传则在当前窗口执行;例如"和鲁倩换微信"应提取 candidateName=鲁倩。',input:Ot.object({candidateName:Ot.string().optional().describe('候选人姓名。若用户说"和鲁倩换微信",这里应提取为"鲁倩"'),index:Ot.number().optional().describe("候选人在列表中的索引(可选)")}),output:Ft,execute:async(e,t)=>{const n=m(),a=await n.getPage("zhipin"),r=await ze(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 oe(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 oe(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 oe(o,400,800)}if(!e)return{success:!1,exchanged:!1,error:"确认对话框未弹出"};await ie(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 oe(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 oe(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 Dt}from"@roll-agent/sdk";import{z as Wt}from"zod";var Ht=Wt.object({success:Wt.boolean(),username:Wt.string(),usedSelector:Wt.string().optional(),usedStrategy:Wt.string().optional(),source:Wt.string().optional(),error:Wt.string().optional()}),Vt=Dt({name:"zhipin_get_username",description:"获取当前登录的招聘者用户名",input:Wt.object({}),output:Ht,execute:async(e,t)=>{t.logger.info("Getting zhipin username");try{const e=m(),n=await e.getPage("zhipin");await n.bringToFront().catch(()=>{});const a=await ut(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 Gt}from"@roll-agent/sdk";import{z as Jt}from"zod";var Yt=Jt.object({index:Jt.number(),candidateId:Jt.string(),name:Jt.string(),age:Jt.string(),experience:Jt.string(),education:Jt.string(),workStatus:Jt.string(),company:Jt.string(),currentPosition:Jt.string(),expectedLocation:Jt.string(),expectedPosition:Jt.string(),expectedSalary:Jt.string(),tags:Jt.array(Jt.string()),buttonText:Jt.string()}),Kt=Jt.object({success:Jt.boolean(),candidates:Jt.array(Yt),total:Jt.number(),error:Jt.string().optional()}),Zt=Gt({name:"zhipin_get_candidate_list",description:"获取推荐列表页的候选人卡片信息",input:Jt.object({maxResults:Jt.number().optional().describe("最多返回条数")}),output:Kt,execute:async(e,t)=>{t.logger.info("Getting candidate list from recommend page");const n=m(),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="",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()??"";a.push({index:t,candidateId:r,name:o,age:i,experience:s,education:c,workStatus:u,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 Xt}from"@roll-agent/sdk";import{z as Qt}from"zod";var en=".candidate-card-wrap",tn="[data-geek], .geek-item",nn=`${en}, ${tn}`;function an(e){return e.evaluate(e=>document.querySelectorAll(e.primarySelector).length>0?e.primarySelector:e.fallbackSelector,{primarySelector:en,fallbackSelector:tn})}function rn(e){return e.frame("recommendFrame")??e.frames().find(e=>e.url().includes("recommend"))??e}async function on(e,t=1e4){try{return await e.waitForSelector(nn,{timeout:t}),!0}catch{return!1}}async function sn(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 cn=Qt.object({index:Qt.number(),candidateName:Qt.string(),candidateId:Qt.string(),success:Qt.boolean(),error:Qt.string().optional()}),un=Qt.object({success:Qt.boolean(),results:Qt.array(cn),summary:Qt.object({total:Qt.number(),succeeded:Qt.number(),failed:Qt.number()})}),dn=Xt({name:"zhipin_say_hello",description:"在推荐列表页对候选人点击「打招呼」按钮(支持批量)",input:Qt.object({indices:Qt.array(Qt.number()).describe("要打招呼的候选人索引列表")}),output:un,execute:async(e,t)=>{t.logger.info(`Saying hello to ${e.indices.length} candidates`);const n=m(),a=await n.getPage("zhipin"),r=rn(a);if(!await on(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 sn(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 ie(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 ie(a),ue(.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 ln}from"@roll-agent/sdk";import{z as mn}from"zod";var gn=mn.object({success:mn.boolean(),candidateName:mn.string(),candidateId:mn.string(),error:mn.string().optional()}),fn=ln({name:"zhipin_open_resume",description:"在推荐列表页点击候选人卡片打开简历详情弹窗",input:mn.object({index:mn.number().describe("候选人在列表中的索引")}),output:gn,execute:async(e,t)=>{t.logger.info(`Opening resume for candidate at index ${e.index}`);const n=m(),a=await n.getPage("zhipin"),r=rn(a);if(!await on(r))return{success:!1,candidateName:"",candidateId:"",error:"推荐列表未加载"};const o=await sn(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 oe(a,200,400),await s.click(),await oe(a,1e3,2e3),t.logger.info(`Opened resume for ${o.name}`),{success:!0,candidateName:o.name,candidateId:o.candidateId}}});import{defineTool as pn}from"@roll-agent/sdk";import{z as hn}from"zod";var bn=hn.object({x:hn.number(),y:hn.number(),width:hn.number(),height:hn.number()}),wn=hn.object({success:hn.boolean(),screenshotArea:bn.optional(),canvasInfo:hn.object({width:hn.number(),height:hn.number()}).optional(),error:hn.string().optional()}),yn=pn({name:"zhipin_locate_resume_canvas",description:"定位简历详情中嵌套 iframe 内的 canvas 元素坐标(用于截图)",input:hn.object({}),output:wn,execute:async(e,t)=>{t.logger.info("Locating resume canvas in nested iframes");const n=m(),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),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 xn}from"@roll-agent/sdk";import{z as vn}from"zod";var Sn=vn.object({success:vn.boolean(),closed:vn.boolean(),error:vn.string().optional()}),kn=[".recommendV2 .boss-popup__close",".dialog-lib-resume .boss-popup__close",".boss-dialog .boss-popup__close",".boss-popup__close",".close-btn",".dialog-close"],In=[".boss-popup__close",".close-btn",".dialog-close",".modal-close"],An=xn({name:"zhipin_close_resume",description:"关闭简历详情弹窗",input:vn.object({}),output:Sn,execute:async(e,t)=>{t.logger.info("Closing resume detail modal");const n=m(),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 kn){const t=await r.$(e);if(t&&await t.isVisible())return await t.click(),!0}for(const e of In){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 Cn}from"@roll-agent/sdk";import{z as Pn}from"zod";import{waitForSelector as En}from"@roll-agent/browser";var qn={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 Nn,waitForSelector as _n}from"@roll-agent/browser";var zn="https://www.yupao.com",Mn=`${zn}/chat`,Rn=`${zn}/login`;async function Bn(e){e.url().includes("/chat")||await Nn(e,Mn),await _n(e,qn.messageList.container,{timeout:15e3})}async function $n(e,t){const n=`${zn}/chat?id=${encodeURIComponent(t)}`;await Nn(e,n),await _n(e,qn.chat.input,{timeout:15e3})}async function Tn(e,t){await Bn(e),await En(e,qn.messageList.item,{timeout:1e4});const n=qn.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 jn=Pn.object({limit:Pn.number().optional().describe("最多返回的消息条数")}),Un=Pn.object({conversationId:Pn.string(),candidateName:Pn.string(),lastMessage:Pn.string(),unreadCount:Pn.number(),timestamp:Pn.string()}),On=Pn.object({messages:Pn.array(Un),total:Pn.number()}),Fn=Cn({name:"yupao_read_messages",description:"读取鱼泡未读消息列表",input:jn,output:On,execute:async(e,t)=>{t.logger.info(`Reading yupao messages (limit: ${e.limit??"all"})`);const n=m(),a=await n.getPage("yupao"),r=await Tn(a,e.limit);return t.logger.info(`Found ${r.length} messages`),{messages:r.map(e=>({...e})),total:r.length}}});import{defineTool as Ln}from"@roll-agent/sdk";import{z as Dn}from"zod";import{waitForSelector as Wn,typeText as Hn,clickElement as Vn}from"@roll-agent/browser";async function Gn(e,t,n){try{return await $n(e,t),await Hn(e,qn.chat.input,n),await Vn(e,qn.chat.sendButton),await Wn(e,qn.chat.messageItem,{timeout:5e3}),{success:!0}}catch(e){return{success:!1,error:e instanceof Error?e.message:String(e)}}}var Jn=Dn.object({conversationId:Dn.string().describe("对话 ID"),message:Dn.string().describe("要发送的回复消息")}),Yn=Dn.object({success:Dn.boolean(),conversationId:Dn.string(),sentMessage:Dn.string(),error:Dn.string().optional()}),Kn=Ln({name:"yupao_send_reply",description:"向鱼泡指定对话发送回复消息",input:Jn,output:Yn,execute:async(e,t)=>{const{conversationId:n,message:a}=e;t.logger.info(`Sending reply to yupao conversation ${n}`);const r=m(),o=await r.getPage("yupao"),i=await Gn(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}}});function Zn(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 Xn(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 Qn(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 ea(){return t.parse({mode:process.env.BROWSER_MODE,headless:Zn(process.env.BROWSER_HEADLESS),cdpUrl:process.env.BROWSER_CDP_URL,cdpHost:process.env.BROWSER_CDP_HOST,cdpPort:Xn(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:Qn(process.env.BROWSER_ARGS_JSON),sessionsDir:process.env.BROWSER_SESSIONS_DIR})}var ta=e({name:"browser-use-agent",tools:[x,R,O,Y,ne,Be,Ue,Ge,jt,Lt,Vt,Zt,dn,fn,yn,An,Fn,Kn,h]},{onShutdown:f});async function na(){await d(ea()),await _t().catch(e=>{console.error(`[browser-use-agent] Failed to preload Reply Authority keys: ${e instanceof Error?e.message:String(e)}`)}),await ta.listen({transport:{type:"http",port:parseInt(process.env.BROWSER_AGENT_PORT??"3100",10),host:process.env.BROWSER_AGENT_HOST??"127.0.0.1"}})}na().catch(async e=>{console.error("Fatal error:",e),await f().catch(()=>{}),process.exit(1)});
@@ -0,0 +1,7 @@
1
+ import type { Page } from "@roll-agent/browser";
2
+ export interface ZhipinChatTarget {
3
+ readonly conversationId: string;
4
+ readonly candidateId: string;
5
+ readonly candidateName: string;
6
+ }
7
+ export declare function getSelectedChatTarget(page: Page): Promise<ZhipinChatTarget | null>;
@@ -0,0 +1,14 @@
1
+ import type { Page } from "@roll-agent/browser";
2
+ import { type UsernameEvidence, type UsernameStrategy } from "./username.ts";
3
+ import type { RecruiterBinding } from "../../reply-authority/schemas.ts";
4
+ export interface ZhipinRecruiterIdentity {
5
+ readonly platform: "zhipin";
6
+ readonly username: string;
7
+ readonly accountId?: string;
8
+ readonly strategy: UsernameStrategy;
9
+ readonly source: string;
10
+ }
11
+ type UsernameEvidenceCollector = (page: Page) => Promise<ReadonlyArray<UsernameEvidence>>;
12
+ export declare function getCurrentZhipinRecruiterIdentity(page: Page, collectEvidence?: UsernameEvidenceCollector): Promise<ZhipinRecruiterIdentity>;
13
+ export declare function matchesRecruiterBinding(current: Pick<ZhipinRecruiterIdentity, "username" | "accountId">, binding: RecruiterBinding): boolean;
14
+ export {};
@@ -14,7 +14,7 @@ export type UsernameEvidence = {
14
14
  };
15
15
  export type UsernameLookupResult = {
16
16
  found: true;
17
- userName: string;
17
+ username: string;
18
18
  strategy: UsernameStrategy;
19
19
  source: string;
20
20
  } | {
@@ -0,0 +1,6 @@
1
+ import { type ReplyAuthorityPublicKey } from "./schemas.ts";
2
+ export declare function refreshReplyAuthorityKeys(): Promise<ReadonlyMap<string, ReplyAuthorityPublicKey>>;
3
+ export declare function preloadReplyAuthorityKeys(): Promise<void>;
4
+ export declare function resolveReplyAuthorityPublicKey(kid: string): Promise<ReplyAuthorityPublicKey>;
5
+ export declare function setReplyAuthorityKeysForTests(keys: ReadonlyArray<ReplyAuthorityPublicKey>): void;
6
+ export declare function resetReplyAuthorityKeyStoreForTests(): void;
@@ -0,0 +1,3 @@
1
+ export declare function isReplyEnvelopeConsumed(jti: string, nowSeconds?: number): boolean;
2
+ export declare function markReplyEnvelopeConsumed(jti: string, exp: number, nowSeconds?: number): void;
3
+ export declare function resetReplyEnvelopeReplayStoreForTests(): void;
@@ -0,0 +1,143 @@
1
+ import { z } from "zod";
2
+ export declare const REPLY_AUTHORITY_ISSUER = "reply-authority-service";
3
+ export declare const REPLY_AUTHORITY_AUDIENCE = "browser-use-agent/zhipin_send_reply";
4
+ export declare const REPLY_AUTHORITY_PLATFORM = "zhipin";
5
+ export declare const REPLY_AUTHORITY_CLOCK_SKEW_SECONDS = 60;
6
+ export declare const RecruiterBindingSchema: z.ZodObject<{
7
+ platform: z.ZodLiteral<"zhipin">;
8
+ username: z.ZodString;
9
+ accountId: z.ZodOptional<z.ZodString>;
10
+ }, "strip", z.ZodTypeAny, {
11
+ platform: "zhipin";
12
+ username: string;
13
+ accountId?: string | undefined;
14
+ }, {
15
+ platform: "zhipin";
16
+ username: string;
17
+ accountId?: string | undefined;
18
+ }>;
19
+ export declare const ReplyAuthorityEnvelopePayloadSchema: z.ZodObject<{
20
+ v: z.ZodLiteral<2>;
21
+ iss: z.ZodLiteral<"reply-authority-service">;
22
+ kid: z.ZodString;
23
+ jti: z.ZodString;
24
+ iat: z.ZodNumber;
25
+ exp: z.ZodNumber;
26
+ aud: z.ZodLiteral<"browser-use-agent/zhipin_send_reply">;
27
+ platform: z.ZodLiteral<"zhipin">;
28
+ tenantId: z.ZodString;
29
+ conversationId: z.ZodString;
30
+ candidateId: z.ZodString;
31
+ reply: z.ZodString;
32
+ policyVersion: z.ZodString;
33
+ recruiterBinding: z.ZodObject<{
34
+ platform: z.ZodLiteral<"zhipin">;
35
+ username: z.ZodString;
36
+ accountId: z.ZodOptional<z.ZodString>;
37
+ }, "strip", z.ZodTypeAny, {
38
+ platform: "zhipin";
39
+ username: string;
40
+ accountId?: string | undefined;
41
+ }, {
42
+ platform: "zhipin";
43
+ username: string;
44
+ accountId?: string | undefined;
45
+ }>;
46
+ }, "strip", z.ZodTypeAny, {
47
+ platform: "zhipin";
48
+ conversationId: string;
49
+ candidateId: string;
50
+ v: 2;
51
+ iss: "reply-authority-service";
52
+ kid: string;
53
+ jti: string;
54
+ iat: number;
55
+ exp: number;
56
+ aud: "browser-use-agent/zhipin_send_reply";
57
+ tenantId: string;
58
+ reply: string;
59
+ policyVersion: string;
60
+ recruiterBinding: {
61
+ platform: "zhipin";
62
+ username: string;
63
+ accountId?: string | undefined;
64
+ };
65
+ }, {
66
+ platform: "zhipin";
67
+ conversationId: string;
68
+ candidateId: string;
69
+ v: 2;
70
+ iss: "reply-authority-service";
71
+ kid: string;
72
+ jti: string;
73
+ iat: number;
74
+ exp: number;
75
+ aud: "browser-use-agent/zhipin_send_reply";
76
+ tenantId: string;
77
+ reply: string;
78
+ policyVersion: string;
79
+ recruiterBinding: {
80
+ platform: "zhipin";
81
+ username: string;
82
+ accountId?: string | undefined;
83
+ };
84
+ }>;
85
+ export declare const ReplyAuthorityPublicKeySchema: z.ZodObject<{
86
+ kid: z.ZodString;
87
+ algorithm: z.ZodLiteral<"Ed25519">;
88
+ publicKey: z.ZodString;
89
+ validFrom: z.ZodString;
90
+ validUntil: z.ZodOptional<z.ZodString>;
91
+ }, "strip", z.ZodTypeAny, {
92
+ kid: string;
93
+ algorithm: "Ed25519";
94
+ publicKey: string;
95
+ validFrom: string;
96
+ validUntil?: string | undefined;
97
+ }, {
98
+ kid: string;
99
+ algorithm: "Ed25519";
100
+ publicKey: string;
101
+ validFrom: string;
102
+ validUntil?: string | undefined;
103
+ }>;
104
+ export declare const ReplyAuthorityPublicKeysResponseSchema: z.ZodObject<{
105
+ keys: z.ZodArray<z.ZodObject<{
106
+ kid: z.ZodString;
107
+ algorithm: z.ZodLiteral<"Ed25519">;
108
+ publicKey: z.ZodString;
109
+ validFrom: z.ZodString;
110
+ validUntil: z.ZodOptional<z.ZodString>;
111
+ }, "strip", z.ZodTypeAny, {
112
+ kid: string;
113
+ algorithm: "Ed25519";
114
+ publicKey: string;
115
+ validFrom: string;
116
+ validUntil?: string | undefined;
117
+ }, {
118
+ kid: string;
119
+ algorithm: "Ed25519";
120
+ publicKey: string;
121
+ validFrom: string;
122
+ validUntil?: string | undefined;
123
+ }>, "many">;
124
+ }, "strip", z.ZodTypeAny, {
125
+ keys: {
126
+ kid: string;
127
+ algorithm: "Ed25519";
128
+ publicKey: string;
129
+ validFrom: string;
130
+ validUntil?: string | undefined;
131
+ }[];
132
+ }, {
133
+ keys: {
134
+ kid: string;
135
+ algorithm: "Ed25519";
136
+ publicKey: string;
137
+ validFrom: string;
138
+ validUntil?: string | undefined;
139
+ }[];
140
+ }>;
141
+ export type ReplyAuthorityEnvelopePayload = z.infer<typeof ReplyAuthorityEnvelopePayloadSchema>;
142
+ export type ReplyAuthorityPublicKey = z.infer<typeof ReplyAuthorityPublicKeySchema>;
143
+ export type RecruiterBinding = z.infer<typeof RecruiterBindingSchema>;
@@ -0,0 +1,2 @@
1
+ import { type ReplyAuthorityEnvelopePayload } from "./schemas.ts";
2
+ export declare function verifySignedReplyEnvelope(signedEnvelope: string, nowSeconds?: number): Promise<ReplyAuthorityEnvelopePayload>;
@@ -3,7 +3,7 @@ export declare const yupaoSendReply: import("@roll-agent/sdk").ToolDefinition<{
3
3
  conversationId: string;
4
4
  }, {
5
5
  success: boolean;
6
- sentMessage: string;
7
6
  conversationId: string;
7
+ sentMessage: string;
8
8
  error?: string | undefined;
9
9
  }>;
@@ -4,6 +4,8 @@ export declare const zhipinGetCandidateInfo: import("@roll-agent/sdk").ToolDefin
4
4
  maxMessages?: number | undefined;
5
5
  }, {
6
6
  success: boolean;
7
+ conversationId: string;
8
+ candidateId: string;
7
9
  stats: {
8
10
  totalMessages: number;
9
11
  candidateMessages: number;
@@ -5,6 +5,7 @@ export declare const zhipinGetCandidateList: import("@roll-agent/sdk").ToolDefin
5
5
  candidates: {
6
6
  name: string;
7
7
  index: number;
8
+ candidateId: string;
8
9
  age: string;
9
10
  experience: string;
10
11
  education: string;
@@ -12,7 +13,6 @@ export declare const zhipinGetCandidateList: import("@roll-agent/sdk").ToolDefin
12
13
  expectedLocation: string;
13
14
  expectedSalary: string;
14
15
  tags: string[];
15
- candidateId: string;
16
16
  workStatus: string;
17
17
  company: string;
18
18
  currentPosition: string;
@@ -1,6 +1,6 @@
1
1
  export declare const zhipinGetUsername: import("@roll-agent/sdk").ToolDefinition<{}, {
2
2
  success: boolean;
3
- userName: string;
3
+ username: string;
4
4
  error?: string | undefined;
5
5
  source?: string | undefined;
6
6
  usedSelector?: string | undefined;
@@ -2,7 +2,7 @@ export declare const zhipinOpenResume: import("@roll-agent/sdk").ToolDefinition<
2
2
  index: number;
3
3
  }, {
4
4
  success: boolean;
5
- candidateName: string;
6
5
  candidateId: string;
6
+ candidateName: string;
7
7
  error?: string | undefined;
8
8
  }>;
@@ -6,6 +6,8 @@ export declare const zhipinReadMessages: import("@roll-agent/sdk").ToolDefinitio
6
6
  success: boolean;
7
7
  candidates: {
8
8
  name: string;
9
+ conversationId: string;
10
+ candidateId: string;
9
11
  position: string;
10
12
  time: string;
11
13
  preview: string;
@@ -10,8 +10,8 @@ export declare const zhipinSayHello: import("@roll-agent/sdk").ToolDefinition<{
10
10
  results: {
11
11
  success: boolean;
12
12
  index: number;
13
- candidateName: string;
14
13
  candidateId: string;
14
+ candidateName: string;
15
15
  error?: string | undefined;
16
16
  }[];
17
17
  }>;
@@ -1,5 +1,5 @@
1
1
  export declare const zhipinSendReply: import("@roll-agent/sdk").ToolDefinition<{
2
- message: string;
2
+ signedEnvelope: string;
3
3
  index?: number | undefined;
4
4
  candidateName?: string | undefined;
5
5
  }, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roll-agent/browser-use-agent",
3
- "version": "0.4.2",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -12,7 +12,8 @@
12
12
  },
13
13
  "files": [
14
14
  "dist",
15
- "SKILL.md"
15
+ "SKILL.md",
16
+ "references"
16
17
  ],
17
18
  "engines": {
18
19
  "node": ">=22.6.0"
@@ -0,0 +1,4 @@
1
+ required:
2
+ - name: REPLY_AUTHORITY_KEYS_URL
3
+ purpose: Reply Authority Service 公钥分发端点;`zhipin_send_reply` 启动预热和本地 Ed25519 验签都依赖它
4
+ example: https://reply-authority.duliday.com/.well-known/reply-authority-keys