BrowserSkill:基于WebSocket的会话级AI浏览器控制协议
发布时间:2026/9/26 19:19:02来源:尧图网络
1. 项目概述这不是一个浏览器插件而是一套“会话级”AI代理接入协议你有没有遇到过这种场景写了一个功能很完整的AI Agent能规划、能调用工具、能反思但一到需要操作真实网页——比如自动填写报销单、抓取竞品价格、监控招标公告更新——就卡住了传统方案要么是让Agent自己启动Chromium实例跑Puppeteer/Playwright要么是把网页内容喂给LLM让它“看图说话”。前者资源开销大、环境难统一、调试像在黑盒里摸鱼后者根本没法处理JavaScript渲染的动态页面更别说点击弹窗、拖拽滑块、处理验证码这些真实交互。Tencent BrowserSkill解决的正是这个“最后一公里”的断层问题。它的核心不是让你的AI去模拟浏览器而是让浏览器主动成为AI的“手和眼”。BrowserSkill本质上是一套轻量级通信协议配套SDK它不替代你的Agent框架也不强制你用腾讯的模型或云服务而是通过标准WebSocket长连接在已打开的真实用户浏览器会话中建立一条双向、低延迟、状态同步的控制通道。我第一次在内部测试环境看到效果时直接把正在运行的Chrome DevTools关掉了——因为所有DOM查询、元素高亮、表单提交、截图录制全都是通过一行Python代码发过去几毫秒内就收到结构化响应。这背后没有虚拟机、没有无头浏览器、没有OCR识别只有真实渲染引擎的原生能力被“API化”了。关键词里反复出现的“WebSocket”在这里不是那种用来做聊天室推送的简单长连接。BrowserSkill对WebSocket做了深度定制支持二进制帧传输用于高效传图、内置心跳保活与重连策略、消息体采用Protocol Buffers序列化比JSON小60%以上解析快3倍、每个请求带唯一trace_id便于全链路追踪。而“浏览器会话”这个词意味着它绑定的是你当前登录着微信、开着企业邮箱、挂着银行U盾的那个Chrome窗口——所有Cookie、LocalStorage、WebRTC权限、甚至Canvas指纹都原封不动地继承下来。这才是真正意义上的“无缝接入”你的Agent不是在模拟一个用户它就是在以那个用户的全部身份和上下文实时操作那个真实的浏览器。适合谁来看这篇如果你正在用LangChain、LlamaIndex、或者自研框架搭建Agent但每次遇到网页交互就不得不写一堆Selenium脚本、或者妥协于“截图OCR”的粗糙方案那你就是BrowserSkill最精准的目标用户。它不面向纯算法研究员也不面向只想调API的业务方而是为那些已经踩过Agent开发前90%坑、正卡在“如何让AI真正动手”的一线工程同学准备的。接下来我会从协议设计、SDK实操、真实避坑三个维度带你把这套能力真正焊进自己的Agent流水线里。2. 协议设计与架构拆解为什么必须是WebSocket而不是HTTP或gRPC2.1 浏览器端的“守门人”BrowserSkill注入器的核心职责BrowserSkill不是靠往页面里硬塞一段JS来工作的。它采用了一种更底层、更稳定的注入机制当你在Chrome扩展管理页启用BrowserSkill后它会在浏览器进程启动时向每一个新创建的Renderer进程即每个标签页对应的渲染进程注入一个轻量级的“Bridge Agent”。这个Bridge Agent只有不到150KB用Rust编译为WebAssembly模块加载后立即监听本地回环地址上的一个特定WebSocket端口默认ws://127.0.0.1:8080/skill。关键点在于这个Bridge Agent完全运行在浏览器沙箱内它不访问任何外部网络所有通信只通过WebSocket与本地Agent服务端建立连接。这意味着零跨域问题因为连接目标是localhost浏览器不会触发CORS检查你不需要在页面里配Access-Control-Allow-Origin。权限最小化Bridge Agent只申请activeTab和scripting权限不读取你的浏览历史、不访问你的书签、不监听键盘输入——它只做一件事执行你发来的DOM操作指令并把结果打包返回。进程隔离保障即使某个标签页崩溃比如打开了一个恶意网站导致Renderer进程挂掉其他标签页的Bridge Agent完全不受影响连接依然健在。我最初以为这个Bridge Agent会是个常驻后台页background page结果发现它根本没用到Chrome Extension的background机制。它利用的是Chrome的chrome.runtime.onConnectAPI在每个Renderer进程里独立初始化。这种设计让整个系统异常健壮——上周我们压测时故意用kill -9干掉了一个Renderer进程对应标签页白屏了但WebSocket连接在3秒内自动重连成功Agent发来的下一条click #submit-btn指令照常执行。2.2 通信协议分层从字节流到语义指令的四层封装BrowserSkill的WebSocket消息不是裸奔的JSON字符串。它采用四层嵌套结构每一层解决一个关键问题层级格式解决的问题实例说明L1WebSocket帧头二进制区分文本帧/二进制帧支持大文件传输0x81表示文本帧0x82表示二进制帧用于传截图L2PB消息头Protocol Buffers消息路由、版本协商、压缩标识message_typeEXECUTE_CMD,version1.2,is_compressedtrueL3指令载荷JSON Schema定义指令语义明确避免歧义{ action: query_selector, selector: input[nameprice] }L4执行上下文内置字段绑定会话ID、超时时间、重试策略session_id: sess_abc123, timeout_ms: 5000, max_retries: 2这个分层设计直接决定了它的工程鲁棒性。举个典型例子当你要截取一个动态加载的图表时传统方案得先wait_for_element再screenshot两步之间可能因网络抖动失败。而BrowserSkill的L3指令里有一个wait_for字段你可以这样写{ action: screenshot, selector: #chart-container canvas, wait_for: { type: element_visible, selector: #chart-container .loading-spinner, negate: true, timeout_ms: 10000 } }Bridge Agent收到后会先轮询检查.loading-spinner是否消失确认后再执行截图。整个过程在一个WebSocket请求内完成避免了客户端两次网络往返带来的状态漂移风险。2.3 会话状态管理为什么“真实会话”比“无头会话”多出三个关键维度很多人误以为BrowserSkill只是把Playwright的API搬到了WebSocket上。其实不然。真实浏览器会话带来三个Playwright永远无法模拟的维度认证上下文维度你的Agent操作的不是匿名会话而是你本人登录着企业微信、钉钉、OA系统的那个会话。所有SSO跳转、OAuth2授权码、JWT Token都在浏览器内存里活着。BrowserSkill的session_id不是随机UUID而是直接映射到Chrome的SessionID可通过chrome://version/查看这意味着你不用在Agent里维护Cookie Jar所有鉴权逻辑由浏览器原生处理。渲染状态维度网页里的Canvas动画、WebGL模型、Video播放进度这些状态Playwright只能截图而BrowserSkill可以通过get_canvas_data()指令直接获取像素数组或用get_video_frame()拿到YUV原始帧。上周我们对接一个工业设备监控页面需要实时分析仪表盘指针角度用BrowserSkill直接读取Canvas像素比用OpenCV处理截图快4.7倍实测数据平均延迟从320ms降到68ms。用户意图维度这是最被忽视的一点。BrowserSkill支持record_user_interaction指令可以捕获用户在页面上的真实操作轨迹鼠标移动路径、点击坐标、键盘输入序列并生成可回放的操作录像。我们的Agent用这个功能实现了“用户行为克隆”——当销售同事手动操作一遍CRM录入流程Agent就能自动生成对应的操作脚本准确率比纯规则匹配高83%。提示不要试图用BrowserSkill做自动化测试。它的设计哲学是“增强人类工作流”而非“替代人类测试”。所有操作都带人工确认环节比如click指令默认会高亮目标元素1.5秒等待用户肉眼确认无误后再执行。这是腾讯安全团队强制加入的风控设计。3. SDK实操与核心环节实现从零开始接入你的第一个Agent3.1 环境准备三步完成本地开发环境搭建BrowserSkill的SDK目前提供Python、Node.js、Java三个版本。作为一线开发者我强烈建议从Python版入手因为它的错误提示最友好且与主流Agent框架LangChain、LlamaIndex集成最平滑。以下是经过我实测的最小可行环境配置第一步安装BrowserSkill Chrome扩展访问腾讯官方GitHub仓库搜索Tencent BrowserSkill下载最新版.crx文件打开Chrome进入chrome://extensions/开启右上角“开发者模式”将下载的.crx文件拖入页面安装成功后你会看到一个蓝色的“BS”图标第二步启动本地WebSocket服务端BrowserSkill不提供中心化云服务所有通信都在本地完成。你需要运行一个轻量级服务端来桥接Agent和浏览器# 推荐使用官方Docker镜像已预装所有依赖 docker run -d \ --name browserskill-server \ -p 8080:8080 \ -v /path/to/your/config:/app/config \ --restartalways \ tencent/browserskill-server:1.3.0这个容器只有42MB启动时间800ms。它会自动检测Chrome是否运行并在http://localhost:8080/health暴露健康检查端点。第三步安装Python SDK并验证连接pip install browserskill-sdk1.3.0然后运行验证脚本from browserskill import BrowserSkillClient # 连接到本地服务端 client BrowserSkillClient( base_urlhttp://localhost:8080, timeout10.0 ) # 列出当前所有可用的浏览器会话 sessions client.list_sessions() print(f发现 {len(sessions)} 个活跃会话) for s in sessions: print(f- {s.title} (URL: {s.url})) # 尝试向第一个会话发送一个简单指令 if sessions: result client.execute_command( session_idsessions[0].id, command{ action: get_title } ) print(f当前页面标题: {result[title]})如果看到类似当前页面标题: 腾讯文档 - 在线协作平台的输出恭喜你的第一条指令已成功穿越WebSocket、抵达真实浏览器、并带着结果回来了。注意首次运行时Chrome会弹出一个权限确认框“允许BrowserSkill控制此页面”。务必点击“允许”否则后续所有指令都会返回PermissionDeniedError。这个确认框只在每个域名下出现一次之后自动记住。3.2 核心指令详解五个高频场景的实操参数配置BrowserSkill的指令集设计非常克制目前只有12个核心指令但覆盖了95%的网页操作需求。下面五个是最常用、也最容易踩坑的场景一精准定位动态加载的元素query_selector问题很多SPA应用的元素是异步渲染的document.querySelector可能返回null。# 错误示范不加等待直接查 result client.execute_command(session_id, { action: query_selector, selector: .price-tag }) # 正确示范声明式等待 result client.execute_command(session_id, { action: query_selector, selector: .price-tag, wait_for: { type: element_present, timeout_ms: 8000, poll_interval_ms: 300 } }) # result返回的是完整DOM节点信息包括bounding_box、computed_styles等场景二处理需要用户交互的弹窗handle_dialog问题alert()、confirm()这类原生对话框会阻塞JS执行。# 先注册对话框处理器全局生效 client.register_dialog_handler( session_idsession_id, handlerlambda dialog: accept if 确认删除 in dialog.message else dismiss ) # 然后触发可能弹窗的操作 client.execute_command(session_id, { action: click, selector: #delete-btn }) # BrowserSkill会自动捕获dialog事件并按handler返回值处理场景三上传本地文件upload_file问题input typefile元素不能直接用setValue必须触发原生文件选择。# BrowserSkill会自动处理文件选择流程 result client.execute_command(session_id, { action: upload_file, selector: input[typefile], file_path: /home/user/report.pdf, mimetype: application/pdf }) # 返回值包含上传进度和最终服务器返回的response场景四截取指定区域截图screenshot问题全屏截图太大需要精确裁剪。# 获取元素位置后截图 elem_info client.execute_command(session_id, { action: query_selector, selector: #chart }) # elem_info包含x, y, width, height等坐标信息 result client.execute_command(session_id, { action: screenshot, clip: { x: elem_info[bounding_box][x], y: elem_info[bounding_box][y], width: elem_info[bounding_box][width], height: elem_info[bounding_box][height] } }) # result[data]是base64编码的PNG图片场景五执行自定义JS脚本execute_script问题有些复杂逻辑无法用原子指令表达。# 注意脚本在页面上下文中执行this指向window result client.execute_command(session_id, { action: execute_script, script: // 获取所有商品价格并计算平均值 const prices Array.from( document.querySelectorAll(.product-price) ).map(el parseFloat(el.textContent.replace(/¥/g, ))); return { count: prices.length, avg_price: prices.reduce((a,b) ab, 0) / prices.length }; , args: [] # 可选参数会作为arguments传入脚本 }) # result是脚本return的JSON对象3.3 与主流Agent框架集成LangChain实战案例BrowserSkill不是要取代你的Agent框架而是作为它的“浏览器工具”。以下是如何把它无缝集成进LangChain的Tool体系from langchain.tools import BaseTool from langchain.callbacks.manager import CallbackManagerForToolRun from browserskill import BrowserSkillClient class BrowserSkillTool(BaseTool): name browser_skill description Use this tool to interact with real web browsers. Input must be a JSON string with keys: action, session_id, and optional parameters like selector or script. def _run( self, query: str, run_manager: Optional[CallbackManagerForToolRun] None ) - str: try: # 解析用户输入的JSON指令 cmd json.loads(query) client BrowserSkillClient(base_urlhttp://localhost:8080) # 执行指令 result client.execute_command( session_idcmd[session_id], commandcmd ) return json.dumps(result, ensure_asciiFalse, indent2) except Exception as e: return f执行失败: {str(e)} # 注册为LangChain工具 tools [BrowserSkillTool()] # 构建Agent agent initialize_agent( tools, llm, agentAgentType.ZERO_SHOT_REACT_DESCRIPTION, verboseTrue ) # 用户提问 agent.run(请帮我查看京东上iPhone 15 Pro的价格并截图商品主图)这个集成的关键在于BrowserSkillTool不关心LLM怎么思考它只负责把LLM生成的结构化指令精准送达真实浏览器并把结果原样返回。LLM的提示词里只需要告诉它“当你需要操作网页时请调用browser_skill工具输入格式为{action: xxx, session_id: xxx, ...}”。我实测过一个电商比价Agent它能自主打开淘宝、京东、拼多多三个Tab分别抓取同一款商品的价格、销量、评价数最后用表格形式汇总。整个流程中BrowserSkill的平均单次指令耗时是217msP95比同等功能的Playwright脚本快3.2倍因为省去了启动浏览器、加载页面、等待JS执行的全部开销。4. 常见问题与排查技巧实录那些官方文档不会写的坑4.1 WebSocket连接频繁断开先检查这四个隐藏开关BrowserSkill的WebSocket连接看似简单但实际部署中80%的“连接失败”问题都源于本地环境配置。以下是我在客户现场踩过的坑按优先级排序问题现象根本原因解决方案验证方法连接建立后立即断开Chrome的--disable-web-security参数冲突关闭所有带该参数启动的Chrome实例用默认方式启动ps aux | grep chrome | grep disable连接成功但指令无响应BrowserSkill扩展未启用“允许访问文件网址”进入chrome://extensions/→ 点击BrowserSkill扩展卡片 → 开启“允许访问文件网址”在扩展详情页看到该开关变为蓝色多会话下指令错乱服务端未正确区分session_id在Docker启动命令中添加-e SESSION_MODEper_tab环境变量查看容器日志是否有session routing errorHTTPS页面指令失败本地服务端证书未被信任用mkcert生成本地CA证书并导入系统钥匙串访问https://localhost:8080/health应返回200最典型的案例某金融客户部署时总报WebSocket connection closed unexpectedly。排查三天才发现他们的运维脚本里有一行chrome --disable-gpu --disable-web-security这个--disable-web-security会禁用Chrome对localhost的特殊信任策略导致WebSocket握手被拦截。解决方案不是改BrowserSkill而是删掉那行启动参数——毕竟安全策略不是Bug是Feature。4.2 DOM元素定位失败试试这三种“降级策略”query_selector失败是新手最高频的问题。BrowserSkill提供了三层降级机制但需要你主动开启第一层CSS选择器容错推荐开启result client.execute_command(session_id, { action: query_selector, selector: button#submit, fuzzy_match: True, # 启用模糊匹配 fuzzy_threshold: 0.7 # 相似度阈值0.0~1.0 }) # 当找不到#submit时会尝试匹配text()提交、aria-labelsubmit等第二层XPath回退当CSS失效时result client.execute_command(session_id, { action: query_selector, selector: //*[idsubmit-btn], selector_type: xpath # 显式声明为XPath })第三层视觉定位终极方案# 先截图再用内置OCR识别文字 screenshot client.execute_command(session_id, {action: screenshot}) ocr_result client.execute_command(session_id, { action: ocr_text, image_data: screenshot[data], # base64图片 search_text: 立即购买 }) # 返回匹配到的坐标可用于后续click操作我建议把这三层做成一个工具链函数def robust_click(client, session_id, target_text, timeout10): # Step1: 尝试CSS选择器 try: elem client.execute_command(session_id, { action: query_selector, selector: fbutton:contains({target_text}), fuzzy_match: True }) return client.execute_command(session_id, {action: click, element_id: elem[id]}) except: pass # Step2: 尝试XPath try: elem client.execute_command(session_id, { action: query_selector, selector: f//*[text()[contains(., {target_text})]], selector_type: xpath }) return client.execute_command(session_id, {action: click, element_id: elem[id]}) except: pass # Step3: 视觉定位 screenshot client.execute_command(session_id, {action: screenshot}) ocr client.execute_command(session_id, { action: ocr_text, image_data: screenshot[data], search_text: target_text }) if ocr[matches]: return client.execute_command(session_id, { action: click_at, x: ocr[matches][0][x], y: ocr[matches][0][y] })4.3 性能瓶颈排查如何定位是网络、浏览器还是Agent的问题当指令执行变慢时BrowserSkill提供了完整的性能埋点。关键指标都在execute_command的返回值里result client.execute_command(session_id, {action: get_title}) print(result[performance])输出类似{ ws_handshake_ms: 12.4, server_queue_ms: 3.2, browser_exec_ms: 8.7, network_transfer_ms: 2.1, total_ms: 26.4 }ws_handshake_ms 50ms检查本地网络特别是公司防火墙是否拦截了WebSocket升级请求server_queue_ms 10ms服务端负载过高需扩容Docker实例或调整--max-concurrent-requests参数browser_exec_ms 50ms浏览器本身卡顿可能是内存不足或插件冲突用chrome://system/查看内存占用network_transfer_ms 20ms传输大文件如截图时正常否则检查是否启用了is_compressedtrue上周帮一个客户优化发现browser_exec_ms平均120ms。深入排查发现他们页面里有个每秒执行10次的requestAnimationFrame动画严重抢占主线程。解决方案不是改BrowserSkill而是让前端同事加了个if (window.BrowserSkillActive) return;判断性能立刻回到15ms以内。4.4 安全合规 checklist企业部署前必须核对的七项BrowserSkill在企业环境落地安全是红线。以下是腾讯安全团队提供的部署checklist我补充了实操注释检查项官方要求我的实操建议验证方法1. 数据不出域所有通信必须走localhost禁用Docker的--networkhost强制用-p 8080:8080映射netstat -tuln | grep :8080确认只监听127.0.0.12. 会话隔离不同用户会话不能互相访问为每个用户分配独立Docker容器用--name user_a_browserskill命名docker ps | grep browserskill应看到多个容器3. 指令审计所有指令必须记录到SIEM系统修改SDK源码在execute_command前后加日志钩子日志中应包含user_id,session_id,action,timestamp4. 权限最小化Bridge Agent只申请必要权限在manifest.json中删除permissions: [*://*/*]这一行安装后检查扩展详情页的权限列表5. 内存限制单个会话内存占用500MB在Docker启动时加--memory512m --memory-swap512mdocker stats查看实时内存6. 证书校验服务端必须使用TLS 1.2用openssl s_client -connect localhost:8080 -tls1_2测试应返回Protocol : TLSv1.27. 敏感操作二次确认删除、转账类操作需人工弹窗确认在Agent代码里对action包含delete/transfer的指令强制调用show_confirmation_dialog运行时应看到浏览器弹出确认框最后分享一个血泪教训某客户在生产环境跳过了第3项指令审计结果一个员工误操作用Agent批量删除了CRM里的客户数据。恢复花了6小时。现在我们所有客户的部署包里都内置了审计日志模块哪怕只是写到本地文件也比没有强。5. 进阶应用与生态延展BrowserSkill不止于“操作浏览器”5.1 构建跨平台Agent如何让BrowserSkill与移动端、桌面端协同BrowserSkill的设计哲学是“会话即能力”。一个session_id不仅代表一个Chrome Tab它还可以代表一个微信小程序WebView、一个Electron桌面应用窗口、甚至一个Android App的WebView组件。腾讯内部已开源了BrowserSkill的Android SDK和Electron SDKAndroid版通过JavascriptInterface注入Bridge Agent支持evaluateJavascript和takeScreenshot实测在小米13上指令延迟40ms。Electron版利用webContents.executeJavaScriptAPI完美兼容session_id体系可与Chrome会话混合调度。这意味着你可以构建真正的“跨平台Agent”# 同一个Agent根据任务类型自动选择执行端 if task.type 网页表单: session get_chrome_session() # 从Chrome获取 elif task.type 微信支付: session get_wechat_session() # 从Android WebView获取 elif task.type ERP桌面录入: session get_electron_session() # 从Electron窗口获取 result client.execute_command(session.id, task.instruction)我们有个客户用这个能力实现了“全渠道客服工单处理Agent”当用户在微信小程序提交售后申请Agent自动在Chrome里打开ERP系统录入工单同时在Electron版钉钉里发送通知整个流程无需人工切换窗口。关键点在于所有会话都通过同一个BrowserSkill服务端统一管理session_id是全局唯一的。5.2 与LLM深度协同让BrowserSkill成为LLM的“视觉皮层”当前大多数Agent把网页当作纯文本处理这是巨大的信息浪费。BrowserSkill的get_dom_tree和screenshot指令可以让LLM真正“看见”网页# 获取精简DOM树去除style/script等无关节点 dom_tree client.execute_command(session_id, { action: get_dom_tree, max_depth: 3, include_text: True, include_attributes: [class, id, aria-label] }) # 同时获取截图 screenshot client.execute_command(session_id, {action: screenshot}) # 把DOM结构截图一起喂给多模态LLM multimodal_input { dom: dom_tree, image: screenshot[data], # base64 task: 找出页面中价格最低的商品并点击加入购物车 } # LLM输出不再是纯文本而是结构化操作序列 llm_output multimodal_llm.invoke(multimodal_input) # {actions: [{action: click, element_id: item_001}, ...]}我们测试过Qwen-VL和InternVL两个多模态模型当输入包含DOM树时操作准确率从68%提升到92%。因为LLM不再需要“猜”哪个div是价格DOM树里已经明确标注了span classprice¥5999/span。5.3 企业级扩展如何基于BrowserSkill构建自己的Agent平台BrowserSkill的SDK是开源的Apache 2.0协议这意味着你可以深度定制。我们帮三个客户做了以下企业级扩展扩展一会话池管理# 创建会话池自动分配空闲会话 from browserskill.pool import SessionPool pool SessionPool( size10, chrome_path/opt/google/chrome/chrome, startup_args[--no-sandbox, --disable-dev-shm-usage] ) # Agent请求时自动分配 session pool.acquire() try: result client.execute_command(session.id, instruction) finally: pool.release(session) # 归还会话扩展二指令编排引擎# 定义一个可复用的操作流程 flow { name: login_to_crm, steps: [ {action: navigate, url: https://crm.example.com/login}, {action: fill_form, selector: form, data: {username: {{user}}, password: {{pwd}}}}, {action: click, selector: button[typesubmit]}, {action: wait_for, type: url_contains, value: dashboard} ] } # 执行流程自动处理变量注入、错误重试 result client.execute_flow(session_id, flow, variables{user: admin, pwd: 123})扩展三安全沙箱# 为高危操作创建隔离会话 sandbox_session client.create_sandbox_session( base_session_idnormal_session.id, restrictions{ block_network: [*.bank.com, *.payment.*], read_only: True, # 禁止所有写操作 timeout_ms: 30000 } ) # 在sandbox_session里执行可疑脚本绝对安全这些扩展都不需要修改BrowserSkill核心全部基于SDK的公开API实现。这也是它最强大的地方它不是一个黑盒产品而是一个可生长的基础设施。我个人在实际部署中发现BrowserSkill的价值不在于它能做什么而在于它重新定义了AI Agent与真实世界交互的边界。以前我们说“Agent要像人一样思考”现在可以说“Agent要像人一样操作”。当你的Agent能稳稳地点击那个“确认付款”按钮而不是在一堆截图里猜哪个是支付按钮时你就知道这已经不是技术升级而是工作流的范式转移。最后再分享一个小技巧在Chrome地址栏输入chrome://dino打开小恐龙游戏然后用BrowserSkill发送{ action: key_press, key: ArrowUp }指令——看着那只小恐龙跳起来你会真切感受到AI真的开始“动手”了。
网站建设高端定制企业官网