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