@roll-agent/browser-use-agent 0.5.0 → 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
|
@@ -28,9 +28,9 @@ metadata:
|
|
|
28
28
|
- `zhipin_read_messages(limit?, onlyUnread?, sortBy?)` — 读取消息列表中的候选人,返回姓名、消息摘要,以及 `conversationId` / `candidateId`
|
|
29
29
|
- `zhipin_open_chat(candidateName?, index?, preferUnread?)` — 打开指定候选人的聊天窗口(按姓名模糊匹配或列表索引)
|
|
30
30
|
- `zhipin_get_candidate_info(candidateName?, index?, maxMessages?)` — 提取候选人资料、聊天记录,以及当前选中聊天的 `conversationId` / `candidateId`
|
|
31
|
-
- `zhipin_send_reply(signedEnvelope, candidateName?, index?)` — 发送消息。只接受 Reply Authority Service 签发的 `signedEnvelope`;本地会先做 Ed25519
|
|
31
|
+
- `zhipin_send_reply(signedEnvelope, candidateName?, index?)` — 发送消息。只接受 Reply Authority Service 签发的 `signedEnvelope`;本地会先做 Ed25519 验签、过期检查、重放检查、目标绑定校验和 recruiter 绑定校验
|
|
32
32
|
- `zhipin_exchange_wechat(candidateName?, index?)` — 换微信。指定 candidateName 会自动打开对应聊天后执行
|
|
33
|
-
- `zhipin_get_username()` —
|
|
33
|
+
- `zhipin_get_username()` — 获取当前登录的招聘者用户名,返回 `username`(依赖当前 runtime 已跟踪页面;首次使用请先 `open_platform`,已打开但未跟踪页面可先 `list_pages + select_page`,确认登录后如需单独验证 attach,可先调用 `attach_browser_session`)。常用于 recruiter binding 解析和外部通知消息中的账号标识
|
|
34
34
|
|
|
35
35
|
## BOSS直聘 — 推荐列表 Tools
|
|
36
36
|
|
|
@@ -51,8 +51,8 @@ metadata:
|
|
|
51
51
|
2. `zhipin_open_chat(candidateName)` → 打开某人的聊天
|
|
52
52
|
3. `zhipin_get_candidate_info` → 查看候选人资料、聊天记录,并拿到 `conversationId` / `candidateId`
|
|
53
53
|
4. `smart-reply-agent.generate_reply(..., target)` → 获取 `suggestedReply + signedEnvelope`
|
|
54
|
-
5. `zhipin_send_reply(signedEnvelope)` →
|
|
55
|
-
|
|
54
|
+
5. `zhipin_send_reply(signedEnvelope)` → 验签、校验 recruiterBinding 后发送回复
|
|
55
|
+
6. `zhipin_exchange_wechat` → 交换微信(可选)
|
|
56
56
|
|
|
57
57
|
## 支持平台
|
|
58
58
|
|
|
@@ -63,5 +63,5 @@ metadata:
|
|
|
63
63
|
|
|
64
64
|
- `zhipin_send_reply` 不再接受裸文本 `message`
|
|
65
65
|
- 实际发送文本来自验签后的 envelope payload 内部 `reply` 字段
|
|
66
|
-
- envelope 绑定 `conversationId + candidateId
|
|
66
|
+
- envelope 绑定 `conversationId + candidateId + recruiterBinding`,若当前选中聊天或当前登录招聘者与签名目标不一致,会拒绝发送
|
|
67
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 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,d;async function u(e){s||(d=new i(e.sessionsDir),s=new r(e),await s.start(),c=new o(s,d))}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(!d)throw new Error("SessionStore not initialized. Call initRuntime() first.");return d}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),d=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(),d=[];for(const e of c){const t=a.getPageCount(e),o=a.getCurrentUrl(e);let i=null,s="unknown";if(n.shouldRestoreSessionSnapshot()){const[t,n]=await Promise.all([r.loadCookies(e),r.loadLocalStorage(e)]);i=void 0!==t&&t.length>0||void 0!==n&&Object.keys(n).length>0,s=i?"snapshot":"none"}else n.usesPersistentProfile()&&(i=null,s="profile");d.push({platform:e,pagesOpen:t,currentUrl:o,hasLoginState:i,loginStateSource:s})}return{running:o,headless:i,mode:s,activeSessions:d}}});import{defineTool as v}from"@roll-agent/sdk";import{BrowserPageInfoSchema as S,PlatformSchema as k}from"@roll-agent/browser";import{z as A}from"zod";import{PLATFORMS as I}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 N(e,t){try{return new URL(e).host.includes(P(t))}catch{return!1}}function E(e){return I.find(t=>N(e,t))}function q(e,t){const n=E(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:E(n)??null,isSelectedForPlatform:e.isSelectedPageForPlatform(t)}}var z=A.object({platform:k.optional().describe("可选:仅返回指定平台相关的页面")}),M=A.object({pages:A.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=>q(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=E(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=>N(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:q(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:q(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 de(e=.3){return Math.random()<e}import{setTimeout as ue}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 Ae(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 Ie(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!!Ie(t)&&(!!ye(e.url())||await xe(e,2e3))}}async function Pe(e,t){if(!await Ae(t))return!1;if(await xe(t,5e3))return!0;const n=await ve(e,t);return!!n&&await xe(n,5e3)}async function Ne(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 ue(300);const a=await ve(e,t);return!!a&&await xe(a,5e3)}async function Ee(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 qe(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 Ne(e,t))return{found:!1,name:"",index:-1,hasUnread:!1,unreadCount:0,lastMessageTime:"",messagePreview:"",error:"消息列表未加载"};const a=await e.getPage("zhipin"),r=we(await Ee(a),n);if(!r){return{found:!1,name:"",index:-1,hasUnread:!1,unreadCount:0,lastMessageTime:"",messagePreview:"",error:`未找到候选人: ${n.candidateName??`index ${n.index}`}`}}return await qe(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 Ne(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()??"",d=(e.querySelector(".push-text, .chat-last-msg")?.textContent?.trim()??"").slice(0,100);let u=0;const l=e.querySelector(".badge-count");l&&(u=parseInt(l.textContent?.trim()??"0",10)||0);const m=u>0||null!==e.querySelector(".red-dot");t.push({name:i,conversationId:a,candidateId:r,position:s,time:c,preview:d,unreadCount:u,hasUnread:m,index:n})}),t});let s=n?i.filter(e=>e.hasUnread):i;const c=e.sortBy??"time";"time"===c?s.sort((e,t)=>t.time.localeCompare(e.time)):"unreadCount"===c?s.sort((e,t)=>t.unreadCount-e.unreadCount):"name"===c&&s.sort((e,t)=>e.name.localeCompare(t.name)),void 0!==e.limit&&(s=s.slice(0,e.limit));const d={withName:i.filter(e=>e.name.length>0).length,withUnread:i.filter(e=>e.hasUnread).length};return await se(o),t.logger.info(`Found ${s.length} candidates (${d.withUnread} with unread)`),{success:!0,candidates:s,total:i.length,stats:d}}});import{defineTool as $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 Ne(n,a))return{success:!1,candidateName:"",index:-1,hasUnread:!1,unreadCount:0,lastMessageTime:"",messagePreview:"",error:"消息列表未加载"};const e=await n.getPage("zhipin"),t=(await Ee(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+年(?:以上)?|应届生|在校生)/),d=c?.[1]??"",u=o.match(/(初中|高中|中专|大专|本科|硕士|博士)/)?.[1]??"";let l="";const m=document.querySelector(".position-name");if(m){const e=m.cloneNode(!0);e.querySelectorAll(".popover-wrap, .tooltip-job").forEach(e=>e.remove()),l=e.textContent?.trim()??""}let g="",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 d=t.querySelector(".message-card-top-wrap, [class*='d-top-text']");if(d){const e=d.textContent??"";(e.includes("微信")||e.includes("WeChat"))&&(c="wechat-exchange")}const u=t.querySelector(".message-time .time, .message-time"),l=(u?.textContent??"").match(y),m=l?l[0]:"";let g="";if("wechat-exchange"===c&&d){const e=d.textContent??"",t=e.match(/\b(\d{8,15})\b/),n=e.match(/微信[::号]*\s*([a-zA-Z0-9_-]{5,20})/);g=t?`[微信号: ${t[1]}]`:n?`[微信号: ${n[1]}]`:"[交换微信]"}else if(d){const e=t.querySelector(".message-card-top-title"),n=t.querySelector(".dialog-content, .message-card-top-text");g=(e?.textContent?.trim()??n?.textContent?.trim()??"").trim()}else{const e=t.querySelector(".text span, .text-content, .text");e&&(g=(e.textContent?.trim()??"").replace(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:d,education:u,communicationPosition:l,expectedPosition:g,expectedLocation:f,expectedSalary:h,tags:b},messages:x}},n),d=c.messages.filter(e=>"candidate"===e.sender||"recruiter"===e.sender).map(e=>`${"candidate"===e.sender?"求职者":"我"}: ${e.content}`),u={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}: ${u.totalMessages} messages`),{success:!0,conversationId:s?.conversationId??"",candidateId:s?.candidateId??"",candidateInfo:c.candidateInfo,chatMessages:c.messages,formattedHistory:d,stats:u}}});import{defineTool as Je}from"@roll-agent/sdk";import{z as Ye}from"zod";import{z as Ke}from"zod";var Ze="reply-authority-service",Xe="browser-use-agent/zhipin_send_reply",Qe="zhipin",et=60,tt=Ke.object({v:Ke.literal(1),iss:Ke.literal(Ze),kid:Ke.string().min(1),jti:Ke.string().min(1),iat:Ke.number().int(),exp:Ke.number().int(),aud:Ke.literal(Xe),platform:Ke.literal(Qe),tenantId:Ke.string().min(1),conversationId:Ke.string().min(1),candidateId:Ke.string().min(1),reply:Ke.string(),policyVersion:Ke.string().min(1)}),nt=Ke.object({kid:Ke.string().min(1),algorithm:Ke.literal("Ed25519"),publicKey:Ke.string().min(1),validFrom:Ke.string().min(1),validUntil:Ke.string().optional()}),at=Ke.object({keys:Ke.array(nt)}),rt=new Map;function ot(e){for(const[t,n]of rt)n<e-et&&rt.delete(t)}function it(e,t=Math.floor(Date.now()/1e3)){return ot(t),rt.has(e)}function st(e,t,n=Math.floor(Date.now()/1e3)){ot(n),rt.set(e,t)}import{createPublicKey as ct,verify as dt}from"node:crypto";var ut=new Map,lt=null;function mt(){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 gt(e){return ut=new Map(e.map(e=>[e.kid,e]))}async function ft(){if(lt)return lt;lt=(async()=>{const e=await fetch(mt()),t=await e.json();if(!e.ok)throw new Error(`Reply Authority 公钥拉取失败 (${e.status})`);return gt(at.parse(t).keys)})();try{return await lt}finally{lt=null}}async function pt(){await ft()}async function ht(e){const t=ut.get(e);if(t)return t;const n=(await ft()).get(e);if(!n)throw new Error(`Unknown key ID: ${e}`);return n}function bt(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")}const i=tt.safeParse(r);if(!i.success)throw new Error("Envelope payload schema validation failed");return{payload:i.data,payloadJson:o,signatureBase64:a}}function wt(e,t){if(e.exp<=e.iat)throw new Error("Envelope expiry must be after issue time");if(e.exp<t-et)throw new Error("Envelope expired");if(e.iat>t+et)throw new Error("Envelope issued in the future")}async function yt(e,t=Math.floor(Date.now()/1e3)){const n=bt(e),a=await ht(n.payload.kid),r=ct({key:Buffer.from(a.publicKey,"base64url"),format:"der",type:"spki"});if(!dt(null,Buffer.from(n.payloadJson,"utf-8"),r,Buffer.from(n.signatureBase64,"base64url")))throw new Error("Signature verification failed");return wt(n.payload,t),n.payload}var xt=Ye.object({success:Ye.boolean(),sentMessage:Ye.string(),error:Ye.string().optional()}),vt=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:xt,execute:async(e,t)=>{let n="";const a=m(),r=await a.getPage("zhipin");let o=r;try{const i=await yt(e.signedEnvelope);if(n=i.reply,it(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 Ne(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:"发送目标与签名不匹配"};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 u=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(!u.found)return{success:!1,sentMessage:n,error:"未找到发送按钮"};const l=o.locator(u.selector).first();return await l.scrollIntoViewIfNeeded(),await l.hover(),await ie(o),await l.click(),await oe(o,500,1200),st(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 St}from"@roll-agent/sdk";import{z as kt}from"zod";var At=kt.object({success:kt.boolean(),exchanged:kt.boolean(),wechatNumber:kt.string().optional(),error:kt.string().optional()}),It=St({name:"zhipin_exchange_wechat",description:'换微信。可指定 candidateName 自动打开对应聊天后执行,或不传则在当前窗口执行;例如"和鲁倩换微信"应提取 candidateName=鲁倩。',input:kt.object({candidateName:kt.string().optional().describe('候选人姓名。若用户说"和鲁倩换微信",这里应提取为"鲁倩"'),index:kt.number().optional().describe("候选人在列表中的索引(可选)")}),output:At,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 Ct}from"@roll-agent/sdk";import{z as Pt}from"zod";var Nt={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"}},Et=30,qt=new Set(["招聘规范","消息","首页","推荐牛人","看简历","我的客服","面试","招聘数据","账号权益","升级VIP","职位","职位管理","牛人","公司","数据统计","设置","帮助中心","登录","注册","退出登录","退出","BOSS直聘","下载APP","搜索","发布职位"]);function _t(e){const t=e.trim();return!(0===t.length||t.length>Et)&&(!qt.has(t)&&!/登录|注册|退出|下载|帮助|设置|管理/.test(t))}function zt(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 Mt(e){let t=e.priority;return qt.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 Rt(e){const t=e.filter(e=>_t(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=Mt(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 Bt(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 $t(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<=Et){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 Tt(e){const t=[];try{const n=zt(await e.ariaSnapshot({timeout:3e3}));for(const{role:e,name:a}of n)a.length>0&&a.length<=Et&&t.push({text:a,strategy:"aria-snapshot",priority:2,source:`aria:${e}:${a}`})}catch{}return t}async function jt(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},Et);for(const e of n)t.push({text:e,strategy:"leaf-text",priority:3,source:"leaf-text"})}catch{}return t}async function Ut(e){const t=[];try{const n=[Nt.username.primary,...Nt.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<=Et&&t.push({text:n,strategy:"css-fallback",priority:4,source:e})}catch{}return t}async function Ot(e){const t=await Bt(e),n=t?(await Promise.all([$t(t,e,"link","role-link"),$t(t,e,"button","role-button"),Tt(t)])).flat():[],a=Rt(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?jt(t):Promise.resolve([]),Ut(e)])).flat();return[...n,...r]}var Ft=Pt.object({success:Pt.boolean(),userName:Pt.string(),usedSelector:Pt.string().optional(),usedStrategy:Pt.string().optional(),source:Pt.string().optional(),error:Pt.string().optional()}),Lt=Ct({name:"zhipin_get_username",description:"获取当前登录的招聘者用户名",input:Pt.object({}),output:Ft,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=Rt(await Ot(n));return a.found?(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}):{success:!1,userName:"",error:"未找到用户名,请确认当前页面已登录招聘者账号。"}}catch(e){return{success:!1,userName:"",error:e instanceof Error?`获取用户名失败:${e.message}`:"获取用户名失败"}}}});import{defineTool as Dt}from"@roll-agent/sdk";import{z as Wt}from"zod";var Ht=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()}),Vt=Wt.object({success:Wt.boolean(),candidates:Wt.array(Ht),total:Wt.number(),error:Wt.string().optional()}),Gt=Dt({name:"zhipin_get_candidate_list",description:"获取推荐列表页的候选人卡片信息",input:Wt.object({maxResults:Wt.number().optional().describe("最多返回条数")}),output:Vt,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="",d="";const u=e.querySelector(".base-info.join-text-wrap, .base-info");if(u){const e=[];if(u.querySelectorAll(":scope > *").forEach(t=>{const n=t.textContent?.trim();n&&e.push(n)}),e.length<=1&&(e.length=0,u.childNodes.forEach(t=>{if(t.nodeType===Node.TEXT_NODE){const n=t.textContent?.trim();n&&e.push(n)}})),e.length<=1){const t=u.textContent?.trim()??"";e.length=0,t.split(/[丨·|]/).forEach(t=>{const n=t.trim();n&&e.push(n)})}for(const t of e)!i&&t.includes("岁")?i=t:!s&&(t.includes("年")||t.includes("应届")||t.includes("在校"))?s=t:!c&&/(初中|高中|中专|中技|大专|本科|硕士|博士)/.test(t)?c=t:!d&&/(在职|离职|在校)/.test(t)&&(d=t)}const l=e.querySelector(".timeline-wrap.work-exps .content.join-text-wrap")??e.querySelector(".timeline-wrap.work-exps .content"),m=(l?.textContent?.trim()??"").split("·").map(e=>e.trim()),g=m[0]??"",f=m[1]??"";let p="",h="";const b=e.querySelector(".row-flex:not(.geek-desc)");if(b){const e=b.querySelector(".label"),t=b.querySelector(".content"),n=e?.textContent??"";if((n.includes("期望")||n.includes("最近关注"))&&t){const e=(t.textContent?.trim()??"").split("·").map(e=>e.trim());p=e[0]??"",h=e[1]??""}}if(!p){const t=e.querySelector(".timeline-wrap.expect .content.join-text-wrap")??e.querySelector(".timeline-wrap.expect .content");if(t){const e=(t.textContent?.trim()??"").split("·").map(e=>e.trim());p=e[0]??"",h=e[1]??""}}const w=e.querySelector(".salary-wrap")?.textContent?.trim()??"",y=[];e.querySelectorAll(".tags-wrap .tag-item, .tags-wrap .tag, .tags-wrap span").forEach(e=>{const t=e.textContent?.trim();t&&y.push(t)});const x=e.querySelector("button.btn.btn-greet")?.textContent?.trim()??"";a.push({index:t,candidateId:r,name:o,age:i,experience:s,education:c,workStatus:d,company:g,currentPosition:f,expectedLocation:p,expectedPosition:h,expectedSalary:w,tags:y,buttonText:x})}),a},e.maxResults);return t.logger.info(`Found ${o.length} candidates in recommend list`),{success:!0,candidates:o,total:o.length}}});import{defineTool as Jt}from"@roll-agent/sdk";import{z as Yt}from"zod";var Kt=".candidate-card-wrap",Zt="[data-geek], .geek-item",Xt=`${Kt}, ${Zt}`;function Qt(e){return e.evaluate(e=>document.querySelectorAll(e.primarySelector).length>0?e.primarySelector:e.fallbackSelector,{primarySelector:Kt,fallbackSelector:Zt})}function en(e){return e.frame("recommendFrame")??e.frames().find(e=>e.url().includes("recommend"))??e}async function tn(e,t=1e4){try{return await e.waitForSelector(Xt,{timeout:t}),!0}catch{return!1}}async function nn(e,t){const n=await Qt(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 an=Yt.object({index:Yt.number(),candidateName:Yt.string(),candidateId:Yt.string(),success:Yt.boolean(),error:Yt.string().optional()}),rn=Yt.object({success:Yt.boolean(),results:Yt.array(an),summary:Yt.object({total:Yt.number(),succeeded:Yt.number(),failed:Yt.number()})}),on=Jt({name:"zhipin_say_hello",description:"在推荐列表页对候选人点击「打招呼」按钮(支持批量)",input:Yt.object({indices:Yt.array(Yt.number()).describe("要打招呼的候选人索引列表")}),output:rn,execute:async(e,t)=>{t.logger.info(`Saying hello to ${e.indices.length} candidates`);const n=m(),a=await n.getPage("zhipin"),r=en(a);if(!await tn(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 nn(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),de(.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 sn}from"@roll-agent/sdk";import{z as cn}from"zod";var dn=cn.object({success:cn.boolean(),candidateName:cn.string(),candidateId:cn.string(),error:cn.string().optional()}),un=sn({name:"zhipin_open_resume",description:"在推荐列表页点击候选人卡片打开简历详情弹窗",input:cn.object({index:cn.number().describe("候选人在列表中的索引")}),output:dn,execute:async(e,t)=>{t.logger.info(`Opening resume for candidate at index ${e.index}`);const n=m(),a=await n.getPage("zhipin"),r=en(a);if(!await tn(r))return{success:!1,candidateName:"",candidateId:"",error:"推荐列表未加载"};const o=await nn(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 ln}from"@roll-agent/sdk";import{z as mn}from"zod";var gn=mn.object({x:mn.number(),y:mn.number(),width:mn.number(),height:mn.number()}),fn=mn.object({success:mn.boolean(),screenshotArea:gn.optional(),canvasInfo:mn.object({width:mn.number(),height:mn.number()}).optional(),error:mn.string().optional()}),pn=ln({name:"zhipin_locate_resume_canvas",description:"定位简历详情中嵌套 iframe 内的 canvas 元素坐标(用于截图)",input:mn.object({}),output:fn,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),d=(i?.y??0)+(s?.y??0);return t.logger.info(`Canvas located at (${c+o.x}, ${d+o.y})`),{success:!0,screenshotArea:{x:Math.round(c+o.x),y:Math.round(d+o.y),width:Math.round(o.clientWidth),height:Math.round(o.clientHeight)},canvasInfo:{width:o.width,height:o.height}}}catch(e){return{success:!1,error:e instanceof Error?e.message:String(e)}}}});import{defineTool as hn}from"@roll-agent/sdk";import{z as bn}from"zod";var wn=bn.object({success:bn.boolean(),closed:bn.boolean(),error:bn.string().optional()}),yn=[".recommendV2 .boss-popup__close",".dialog-lib-resume .boss-popup__close",".boss-dialog .boss-popup__close",".boss-popup__close",".close-btn",".dialog-close"],xn=[".boss-popup__close",".close-btn",".dialog-close",".modal-close"],vn=hn({name:"zhipin_close_resume",description:"关闭简历详情弹窗",input:bn.object({}),output:wn,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 yn){const t=await r.$(e);if(t&&await t.isVisible())return await t.click(),!0}for(const e of xn){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 Sn}from"@roll-agent/sdk";import{z as kn}from"zod";import{waitForSelector as An}from"@roll-agent/browser";var In={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 Cn,waitForSelector as Pn}from"@roll-agent/browser";var Nn="https://www.yupao.com",En=`${Nn}/chat`,qn=`${Nn}/login`;async function _n(e){e.url().includes("/chat")||await Cn(e,En),await Pn(e,In.messageList.container,{timeout:15e3})}async function zn(e,t){const n=`${Nn}/chat?id=${encodeURIComponent(t)}`;await Cn(e,n),await Pn(e,In.chat.input,{timeout:15e3})}async function Mn(e,t){await _n(e),await An(e,In.messageList.item,{timeout:1e4});const n=In.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 Rn=kn.object({limit:kn.number().optional().describe("最多返回的消息条数")}),Bn=kn.object({conversationId:kn.string(),candidateName:kn.string(),lastMessage:kn.string(),unreadCount:kn.number(),timestamp:kn.string()}),$n=kn.object({messages:kn.array(Bn),total:kn.number()}),Tn=Sn({name:"yupao_read_messages",description:"读取鱼泡未读消息列表",input:Rn,output:$n,execute:async(e,t)=>{t.logger.info(`Reading yupao messages (limit: ${e.limit??"all"})`);const n=m(),a=await n.getPage("yupao"),r=await Mn(a,e.limit);return t.logger.info(`Found ${r.length} messages`),{messages:r.map(e=>({...e})),total:r.length}}});import{defineTool as jn}from"@roll-agent/sdk";import{z as Un}from"zod";import{waitForSelector as On,typeText as Fn,clickElement as Ln}from"@roll-agent/browser";async function Dn(e,t,n){try{return await zn(e,t),await Fn(e,In.chat.input,n),await Ln(e,In.chat.sendButton),await On(e,In.chat.messageItem,{timeout:5e3}),{success:!0}}catch(e){return{success:!1,error:e instanceof Error?e.message:String(e)}}}var Wn=Un.object({conversationId:Un.string().describe("对话 ID"),message:Un.string().describe("要发送的回复消息")}),Hn=Un.object({success:Un.boolean(),conversationId:Un.string(),sentMessage:Un.string(),error:Un.string().optional()}),Vn=jn({name:"yupao_send_reply",description:"向鱼泡指定对话发送回复消息",input:Wn,output:Hn,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 Dn(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 Gn(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 Jn(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 Yn(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 Kn(){return t.parse({mode:process.env.BROWSER_MODE,headless:Gn(process.env.BROWSER_HEADLESS),cdpUrl:process.env.BROWSER_CDP_URL,cdpHost:process.env.BROWSER_CDP_HOST,cdpPort:Jn(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:Yn(process.env.BROWSER_ARGS_JSON),sessionsDir:process.env.BROWSER_SESSIONS_DIR})}var Zn=e({name:"browser-use-agent",tools:[x,R,O,Y,ne,Be,Ue,Ge,vt,It,Lt,Gt,on,un,pn,vn,Tn,Vn,h]},{onShutdown:f});async function Xn(){await u(Kn()),await pt().catch(e=>{console.error(`[browser-use-agent] Failed to preload Reply Authority keys: ${e instanceof Error?e.message:String(e)}`)}),await Zn.listen({transport:{type:"http",port:parseInt(process.env.BROWSER_AGENT_PORT??"3100",10),host:process.env.BROWSER_AGENT_HOST??"127.0.0.1"}})}Xn().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,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 {};
|
|
@@ -3,8 +3,21 @@ export declare const REPLY_AUTHORITY_ISSUER = "reply-authority-service";
|
|
|
3
3
|
export declare const REPLY_AUTHORITY_AUDIENCE = "browser-use-agent/zhipin_send_reply";
|
|
4
4
|
export declare const REPLY_AUTHORITY_PLATFORM = "zhipin";
|
|
5
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
|
+
}>;
|
|
6
19
|
export declare const ReplyAuthorityEnvelopePayloadSchema: z.ZodObject<{
|
|
7
|
-
v: z.ZodLiteral<
|
|
20
|
+
v: z.ZodLiteral<2>;
|
|
8
21
|
iss: z.ZodLiteral<"reply-authority-service">;
|
|
9
22
|
kid: z.ZodString;
|
|
10
23
|
jti: z.ZodString;
|
|
@@ -17,11 +30,24 @@ export declare const ReplyAuthorityEnvelopePayloadSchema: z.ZodObject<{
|
|
|
17
30
|
candidateId: z.ZodString;
|
|
18
31
|
reply: z.ZodString;
|
|
19
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
|
+
}>;
|
|
20
46
|
}, "strip", z.ZodTypeAny, {
|
|
21
47
|
platform: "zhipin";
|
|
22
48
|
conversationId: string;
|
|
23
49
|
candidateId: string;
|
|
24
|
-
v:
|
|
50
|
+
v: 2;
|
|
25
51
|
iss: "reply-authority-service";
|
|
26
52
|
kid: string;
|
|
27
53
|
jti: string;
|
|
@@ -31,11 +57,16 @@ export declare const ReplyAuthorityEnvelopePayloadSchema: z.ZodObject<{
|
|
|
31
57
|
tenantId: string;
|
|
32
58
|
reply: string;
|
|
33
59
|
policyVersion: string;
|
|
60
|
+
recruiterBinding: {
|
|
61
|
+
platform: "zhipin";
|
|
62
|
+
username: string;
|
|
63
|
+
accountId?: string | undefined;
|
|
64
|
+
};
|
|
34
65
|
}, {
|
|
35
66
|
platform: "zhipin";
|
|
36
67
|
conversationId: string;
|
|
37
68
|
candidateId: string;
|
|
38
|
-
v:
|
|
69
|
+
v: 2;
|
|
39
70
|
iss: "reply-authority-service";
|
|
40
71
|
kid: string;
|
|
41
72
|
jti: string;
|
|
@@ -45,6 +76,11 @@ export declare const ReplyAuthorityEnvelopePayloadSchema: z.ZodObject<{
|
|
|
45
76
|
tenantId: string;
|
|
46
77
|
reply: string;
|
|
47
78
|
policyVersion: string;
|
|
79
|
+
recruiterBinding: {
|
|
80
|
+
platform: "zhipin";
|
|
81
|
+
username: string;
|
|
82
|
+
accountId?: string | undefined;
|
|
83
|
+
};
|
|
48
84
|
}>;
|
|
49
85
|
export declare const ReplyAuthorityPublicKeySchema: z.ZodObject<{
|
|
50
86
|
kid: z.ZodString;
|
|
@@ -104,3 +140,4 @@ export declare const ReplyAuthorityPublicKeysResponseSchema: z.ZodObject<{
|
|
|
104
140
|
}>;
|
|
105
141
|
export type ReplyAuthorityEnvelopePayload = z.infer<typeof ReplyAuthorityEnvelopePayloadSchema>;
|
|
106
142
|
export type ReplyAuthorityPublicKey = z.infer<typeof ReplyAuthorityPublicKeySchema>;
|
|
143
|
+
export type RecruiterBinding = z.infer<typeof RecruiterBindingSchema>;
|