Tio Boot DocsTio Boot Docs
Home
  • java-db
  • api-table
  • jooq
  • mysql
  • postgresql
  • oceanbase
  • Enjoy
  • Tio Boot Admin
  • java-openai
  • ai_agent
  • knowlege_base
  • voice-agent
  • ai-search
  • ai-coding
  • ai-browser
  • 案例
Abount
AI 检索
  • Github
  • Gitee
Home
  • java-db
  • api-table
  • jooq
  • mysql
  • postgresql
  • oceanbase
  • Enjoy
  • Tio Boot Admin
  • java-openai
  • ai_agent
  • knowlege_base
  • voice-agent
  • ai-search
  • ai-coding
  • ai-browser
  • 案例
Abount
AI 检索
  • Github
  • Gitee
  • 01_tio-boot 简介

    • 01_tio-boot 简介
    • tio-boot:新一代高性能 Java Web 开发框架
    • tio-boot 入门示例
    • Tio-Boot 配置 : 现代化的配置方案
    • tio-boot 整合 Logback
    • tio-boot 整合 hotswap-classloader 实现热加载
    • 自行编译 tio-boot
    • 最新版本
    • 开发规范
  • 02_部署

    • 02_部署
    • 使用 Maven Profile 实现分环境打包 tio-boot 项目
    • Maven 项目配置详解:依赖与 Profiles 配置
    • tio-boot 打包成 FatJar
    • 使用 GraalVM 构建 tio-boot Native 程序
    • 使用 Docker 部署 tio-boot
    • 部署到 Fly.io
    • 部署到 AWS Lambda
    • 到阿里云云函数
    • 使用 Deploy 工具部署
    • 使用Systemctl启动项目
    • 使用 Jenkins 部署 Tio-Boot 项目
    • 使用 Nginx 反向代理 Tio-Boot
    • 使用 Supervisor 管理 Java 应用
    • 历史部署页与替代方案
    • 胖包与瘦包的打包与部署
  • 03_配置

    • 03_配置
    • 配置参数
    • 服务器监听器
    • 内置缓存系统 AbsCache
    • 使用 Redis 作为内部 Cache
    • 静态文件处理器
    • 基于域名的静态资源隔离
    • DecodeExceptionHandler
    • 开启虚拟线程(Virtual Thread)
    • 框架级错误通知
  • 04_原理

    • 04_原理
    • 生命周期
    • 请求处理流程
    • 重要的类
  • 05_json

    • 05_json
    • Json
    • 接受 JSON 和响应 JSON
    • 响应实体类
  • 06_web

    • 06_web
    • 概述
    • 接收请求参数
    • 接收日期参数
    • 接收数组参数
    • 返回字符串
    • 返回文本数据
    • 返回网页
    • 请求和响应字节
    • 文件上传
    • 文件下载
    • 返回视频文件并支持断点续传
    • http Session
    • Cookie
    • 重定向和转发
    • HttpRequest
    • HttpResponse
    • Resps
    • RespBodyVo
    • Controller拦截器
    • 请求拦截器
    • LoggingInterceptor
    • 全局异常处理器
    • 异步处理
    • 动态 返回 CSS 实现
    • 返回图片
    • 跨域
    • 添加 Controller
    • Transfer-Encoding: chunked 实时音频播放
    • Server-Sent Events (SSE)
    • handler入门
    • 返回 multipart
    • Handler 的请求方法与错误响应
    • 自定义 Handler 转发请求
    • 使用 HttpForwardHandler 转发所有请求
    • 常用工具类
    • HTTP Basic 认证
    • Http响应加密
    • 使用零拷贝发送大文件
    • 分片上传
    • 接口访问统计
    • 接口请求和响应数据记录
    • WebJars
    • JProtobuf
    • 测速
    • Gzip Bomb:使用压缩炸弹防御恶意爬虫
  • 07_validate

    • 07_validate
    • 数据紧校验规范
    • 参数校验
  • 08_websocket

    • 08_websocket
    • 使用 tio-boot 搭建 WebSocket 服务
    • WebSocket 聊天室项目示例
  • 09_java-db

    • 09_java-db
    • Db 工具类
    • java‑db
    • 操作数据库入门示例
    • SQL 模板 (SqlTemplates)
    • 数据源配置与使用
    • ActiveRecord
    • Db 工具类
    • 批量操作
    • Model
    • Model生成器
    • 注解
    • 异常处理
    • 数据库事务处理
    • Cache 缓存
    • Dialect 多数据库支持
    • 表关联操作
    • 复合主键
    • Oracle 支持
    • Enjoy SQL 模板
    • 整合 Enjoy 模板最佳实践
    • 多数据源支持
    • 独立使用 ActiveRecord
    • 调用存储过程
    • java-db 整合 Guava 的 Striped 锁优化
    • 生成 SQL
    • 通过实体类操作数据库
    • java-db 读写分离
    • Spring Boot 整合 Java-DB
    • like 查询
    • 常用操作示例
    • Druid 监控集成指南
    • SQL 统计
    • Db 与 PostgreSQL 业务实践
  • 10_api-table

    • 10_api-table
    • ApiTable 概述
    • 使用 ApiTable 连接 SQLite
    • 使用 ApiTable 连接 Mysql
    • 使用 ApiTable 连接 Postgres
    • 使用 ApiTable 连接 TDEngine
    • 使用 api-table 连接 oracle
    • 使用 api-table 连接 mysql and tdengine 多数据源
    • EasyExcel 导出
    • EasyExcel 导入
    • ApiTable 的权限与业务边界
    • ApiTable 联调与故障定位
    • ApiTable 实现增删改查
    • 数组类型
    • 单独使用 ApiTable
    • TQL(Table SQL)前端输入规范
  • 11_aop

    • 11_aop
    • JFinal-aop
    • Aop 工具类
    • 配置
    • 独立使用 JFinal Aop
    • @AImport
    • 自定义注解拦截器
    • 原理解析
  • 12_cache

    • 12_cache
    • Caffeine
    • CacheUtils 工具类
    • 使用 java-db 整合 ehcache
  • 13_认证和权限

    • 13_认证
    • FixedTokenInterceptor
    • TokenManager
    • 数据表
    • 匿名登录
    • 个人中心
    • 权限校验注解
    • Sa-Token
    • sa-token 登录注册
    • StpUtil.isLogin() 源码解析
  • 14_i18n

    • 14_i18n
    • i18n
  • 15_enjoy

    • 15_enjoy
    • tio-boot 整合 Enjoy 模版引擎文档
    • Tio-Boot 整合 Java-DB 与 Enjoy 模板引擎示例
    • 引擎配置
    • 表达式
    • 指令
    • 注释
    • 原样输出
    • Shared Method 扩展
    • Shared Object 扩展
    • Extension Method 扩展
    • Spring boot 整合
    • 独立使用 Enjoy
    • tio-boot enjoy 自定义指令 localeDate
    • PromptEngine
    • Enjoy 入门示例-擎渲染大模型请求体
    • Tio Boot + Enjoy:分页与 SEO 实战指南
    • TioBoot + Enjoy 生成 robots.txt 与 sitemap.xml:实战与SEO指南
    • Enjoy 使用示例
  • 16_定时任务

    • 16_定时任务
    • Quartz 定时任务集成指南
    • 分布式定时任务 xxl-jb
    • cron4j 使用指南
  • 17_tests

    • 17_tests
    • TioBootTest:环境与 AOP 初始化
    • 真实 HTTP 集成测试
    • 数据库集成测试与隔离
  • 18_mybatis

    • 18_mybatis
    • Tio-Boot 整合 MyBatis
    • 使用配置类方式整合 MyBatis
    • 整合数据源
    • 使用 mybatis-plus 整合 tdengine
    • 整合 mybatis-plus
  • 19_redis

    • 19_redis
    • 使用 Docker 安装 Redis
    • 使用 java-db 整合 Redis
    • Java DB Redis 相关 Api
    • redis 使用示例
    • 和 RedisTemplate 协作
    • 使用 Jedis 连接池接入 Redis
    • hutool RedisDS
    • Redisson
    • Caffeine 与 Redis 两级缓存
    • 使用 CacheUtils 整合 caffeine 和 redis 实现的两级缓存
  • 20_mongodb

    • 20_mongodb
    • tio-boot 使用 mongo-java-driver 操作 mongodb
  • 21_elastic-search

    • 21_elastic-search
    • Elasticsearch
    • JavaDB 整合 ElasticSearch
    • Elastic 工具类使用指南
    • Elastic-search 注意事项
    • ES 课程示例文档
  • 22_mq

    • 22_mq
    • Mica-mqtt
    • EMQX
    • Disruptor
  • 23_kafka

    • 23_kafka
    • Kafka
    • AWS MSK
  • 24_postgresql

    • 24_postgresql
    • PostgreSQL 安装
    • PostgreSQL 主键自增
    • PostgreSQL 日期类型
    • Postgresql 金融类型
    • PostgreSQL 数组类型
    • 索引
    • PostgreSQL 查询优化
    • 获取字段类型
    • PostgreSQL 全文检索
    • PostgreSQL 向量
    • PostgreSQL 优化向量查询
    • PostgreSQL 其他
  • 25_mysql

    • 25_mysql
    • 使用 Docker 运行 MySQL
    • 常见问题
  • 26_oceanbase

    • 26_oceanbase
    • 快速体验 OceanBase 社区版
    • 快速上手 OceanBase 数据库单机部署与管理
    • 诊断集群性能
    • 优化 SQL 性能指南
    • 待定
  • 27_oracle

    • 27_oracle
    • Oracle
  • 28_sqlserver

    • 28_sqlserver
    • SQL Server
  • 29_sqlite

    • 29_sqlite
    • SQLite
  • 30_integration_thirty_party

    • 30_integration_thirty_party
    • 整合 okhttp
    • 整合 GrpahQL
    • 集成 Mailjet
    • 整合 ip2region
    • 整合 GeoLite 离线库
    • 整合 Lark 机器人指南
    • 集成 Lark Mail 实现邮件发送
    • Thymeleaf
    • Swagger
    • Clerk 验证
    • 集成datadog
  • 31_tio

    • 31_tio
    • TioBootServer
    • 独立端口启动 TCP 服务器
    • 内置 TCP 处理器
    • 独立启动 UDPServer
    • 使用内置 UDPServer
    • t-io 消息处理流程
    • tio-运行原理详解
    • TioConfig
    • ChannelContext
    • Tio 工具类
    • 业务数据绑定
    • 业务数据解绑
    • 发送数据
    • 关闭连接
    • Packet
    • 监控: 心跳
    • 监控: 客户端的流量数据
    • 监控: 单条 TCP 连接的流量数据
    • 监控: 端口的流量数据
    • 单条通道统计: ChannelStat
    • 所有通道统计: GroupStat
    • 资源共享
    • 成员排序
    • SSL
    • DecodeRunnable
    • 使用 AsynchronousSocketChannel 响应数据
    • 拉黑 IP
    • 深入解析 Tio 源码:构建高性能 Java 网络应用
    • Https建立连接过程
  • 32_tio-utils

    • 32_tio-utils
    • tio-utils
    • EnvUtils 配置工具
    • Notification
    • Email
    • JSON
    • File
    • Base64
    • 上传和下载
    • Http
    • Telegram
    • RsaUtils
    • HttpUtils
    • ByteBufferUtils
    • 系统监控
    • 线程
    • 虚拟线程
    • 毫秒并发 ID (MCID) 生成方案
  • 33_tio-http-server

    • 33_tio-http-server
    • 使用 Tio-Http-Server 搭建简单的 HTTP 服务
    • tio-boot 添加 HttpRequestHandler
    • 在 Android 上使用 tio-boot 运行 HTTP 服务
    • tio-http-server-native
    • handler 常用操作
    • tio-http-server 与 tio-boot 的使用边界
  • 34_tio-websocket

    • 34_tio-websocket
    • WebSocket 服务器
    • WebSocket Client
    • TCP数据转发
  • 35_magic-script

    • 35_magic-script
    • tio-boot 与 magic-script 集成指南
  • 36_groovy

    • 36_groovy
    • tio-boot 整合 Groovy
    • 调试常用脚本
  • 37_spider

    • 37_spider
    • jsoup
    • 爬取 z-lib.io 数据
    • 整合 WebMagic
    • WebMagic 示例:爬取学校课程数据
    • Playwright
    • Flexmark (Markdown 处理器)
    • tio-boot 整合 Playwright
    • 缓存网页数据
  • 38_dubbo

    • 38_dubbo
    • 概述
    • dubbo 2.6.0
    • dubbo 2.6.0 调用过程
    • dubbo 3.2.0
  • 39_spring

    • 39_spring
    • Spring Boot Web 整合 Tio Boot
    • spring-boot-starter-webflux 整合 tio-boot
    • tio-boot 整合 spring-boot-starter
    • Tio Boot 整合 Spring Boot Starter db
    • Tio Boot 整合 Spring Boot Starter Data Redis 指南
  • 40_spring-cloud

    • 40_spring-cloud
    • tio-boot spring-cloud
  • 41_aio

    • 41_aio
    • ByteBuffer
    • AIO HTTP 服务器
    • 自定义和线程池和池化 ByteBuffer
    • AioHttpServer 应用示例 IP 属地查询
    • 手写 AIO Http 服务器
    • Java 21 中的虚拟线程与 AIO
  • 42_netty

    • 42_netty
    • Netty TCP Server
    • Netty Web Socket Server
    • 使用 protoc 生成 Java 包文件
    • Netty WebSocket Server 二进制数据传输
    • Netty 组件详解
  • 43_netty-boot

    • 43_netty-boot
    • Netty-Boot
    • 原理解析
    • 整合 Hot Reload
    • 整合 数据库
    • 整合 Redis
    • 整合 Elasticsearch
    • 整合 Dubbo
    • Listener
    • 文件上传
    • 拦截器
    • Spring Boot 整合 Netty-Boot
    • SSL 配置指南
    • ChannelInitializer
    • Reserve
  • 44_quarkus

    • 44_quarkus
    • Quarkus(无 HTTP)整合 tio-boot(有 HTTP)
    • tio-boot + Quarkus + Hibernate ORM Panache
    • tio-boot + Quarkus + Hibernate ORM Panache + jOOQ 整合方案
  • 45_jooq

    • 45_jooq
    • 使用配置类方式整合 jOOQ
    • tio-boot + jOOQ 事务管理
    • 批量操作与性能优化
    • 整合agroal
    • 代码生成与类型安全
    • 基于 Record / POJO 增删改查
    • UPSERT、批量更新、返回主键与高级 SQL
    • 的多表关联查询、DTO 投影、聚合统计与视图封装
    • 的窗口函数、CTE、JSON 查询与 PostgreSQL 高级 SQL 实战
    • tio-boot + jOOQ 的审计字段、乐观锁、数据权限与企业级 Repository 设计
    • 测试策略、SQL 日志、性能诊断与生产排障
    • 多租户、读写分离与多数据源设计
    • 代码生成治理、数据库迁移与团队协作规范实战
  • 46_media

    • 46_media
    • JAVE 提取视频中的声音
    • Jave 提取视频中的图片
    • 待定
  • 47_asr

    • 47_asr
    • Whisper-JNI
  • 48_tts

    • 48_tts
  • 49_ocr

    • 49_ocr
  • 50_native-media

    • 50_native-media
    • java-native-media
    • JNI 入门示例
    • mp3 拆分
    • mp4 转 mp3
    • 使用 libmp3lame 实现高质量 MP3 编码
    • Linux 编译
    • macOS 编译
    • 从 JAR 包中加载本地库文件
    • 支持的音频和视频格式
    • 任意格式转为 mp3
    • 通用格式转换
    • 通用格式拆分
    • 视频合并
    • VideoToHLS
    • split_video_to_hls 支持其他语言
    • 持久化 HLS 会话
    • 获取视频长度
    • 保存视频的最后一帧
    • 添加水印
    • linux版本
  • 51_cv

    • 51_cv
    • 使用 Java 运行 YOLOv8 ONNX 模型进行目标检测
    • tio-boot整合yolo
    • ONNX Runtime 推理说明
    • Paddle Structure
    • tio-boot 整合 Paddle Structure
    • tio-boot整合Paddle Structure 提取图片
    • U2Net 图片去背景原理
    • tio-boot 整合 U2Net 实现图片去背景
  • 52_telegram4j

    • 52_telegram4j
    • 数据库设计
    • 基于 HTTP 协议开发 Telegram 翻译机器人
    • 基于 MTProto 协议开发 Telegram 翻译机器人
    • 过滤旧消息
    • 保存机器人消息
    • 定时推送
    • 增加命令菜单
    • 使用 telegram-Client
    • 使用自定义 StoreLayout
    • 延迟测试
    • Reactor 错误处理
    • Telegram4J 常见错误处理指南
    • 处理回调查询
    • Reactor
    • 文档翻译
    • 使用 Tio-Boot 整合 tdlight
    • tio-boot 整合 TelegramBots
    • tio-boot 整合 Telegram-Bot-Utils
    • Telegram-Bot-Utils 使用指南
  • 53_telegram-bots

    • 53_telegram-bots
    • TelegramBots 入门指南
    • 使用工具库 telegram-bot-base 开发翻译机器人
  • 54_java-openai

    • 54_java-openai
    • 简介
    • 流式生成
    • 图片多模态输入
    • Google Gemini接入
    • google Vertex AI 接入
    • Perplexity API
    • WhisperClient 语音识别
    • SupadataClient 获取视频字幕
    • GiteeClient 文档解析与图片 OCR
    • DeepSeekClient 官方模型查询
    • BailianTTSClient 语音合成
  • 55_ai_agent

    • 55_ai_agent
    • 数据库设计
    • 示例问题管理
    • 会话管理
    • 历史记录
    • 意图识别
    • 智能问答
    • 文件上传与解析文档
    • 翻译
    • 名人搜索功能实现
    • Ai studio gemini youbue 问答使用说明
    • 自建 YouTube 字幕问答系统
    • 自建 获取 youtube 字幕服务
    • 使用 OpenAI ASR 实现语音识别接口(Java 后端示例)
    • 定向搜索
    • 16
    • 17
    • 18
    • 在 tio-boot 应用中整合 ai-agent
    • 接口文档
    • 自定义 ChatAskService
    • 请求记录
    • 限流和错误处理
    • 增强检索(RAG)
    • 结构化数据检索
    • AI 问答
    • 连接代码执行器
    • 待定
    • 模型编程能力评测
    • 音频会话 SDP 示例
  • 56_knowlege_base

    • 56_knowlege_base
    • 学术论文
    • 数据库设计
    • 用户登录实现
    • 模型管理
    • 知识库管理
    • 文档拆分
    • 片段向量
    • 命中测试
    • 文档管理
    • 片段管理
    • 问题管理
    • 应用管理
    • 向量检索
    • 推理问答
    • 问答模块
    • 统计分析
    • 用户管理
    • api 管理
    • 存储文件到 S3
    • 文档解析优化
    • 片段汇总
    • 段落分块与检索
    • 多文档解析
    • 对话日志
    • 检索性能优化
    • Milvus
    • 文档解析方案和费用对比
    • 豫自然资办发〔2021〕18号文档解析实测
    • 离线运行向量模型
    • 爬取网页数据
  • 57_voice-agent

    • 57_voice-agent
    • 整合Gemini realtime模型
    • Voice Agent 前端接入接口文档
    • 整合千问realtime模型
    • 打断支持
    • 主动介入
    • eleven labs
    • 基于 tio-boot + ElevenLabs 构建实时语音 Agent(支持打断与主动介入)
  • 58_ai-search

    • 58_ai-search
    • ai-search 项目简介
    • ai-search 数据库文档
    • ai-search SearxNG 搜索引擎
    • ai-search Jina Reader API
    • ai-search Jina Search API
    • ai-search 搜索、重排与读取内容
    • ai-search PDF 文件处理
    • ai-search 推理问答
    • Google Custom Search JSON API
    • ai-search 意图识别
    • ai-search 问题重写
    • ai-search 系统 API 接口 WebSocket 版本
    • ai-search 搜索代码实现 WebSocket 版本
    • ai-search 生成建议问
    • ai-search 生成问题标题
    • ai-search 历史记录
    • Discover API
    • 翻译
    • Tavily Search API 文档
    • 对接 Tavily Search
    • 火山引擎 DeepSeek
    • 对接 火山引擎 DeepSeek
    • ai-search 搜索代码实现 SSE 版本
    • jar 包部署
    • Docker 部署
    • 爬取一个静态网站的所有数据
    • 网页数据预处理
    • 网页数据检索与问答流程整合
  • 59_ai-coding

    • 59_ai-coding
    • Cline 提示词
    • Cline 提示词-中文版本
  • 60_ai-browser

    • deepseek-browser-use:从入门到源码
    • deepseek-browser-use:概念与学习路线
    • 安装、启动与健康检查
    • 第一个任务:打开页面、读取结果与关闭
    • 客户端:dsb 命令行、Python 与 PowerShell
    • 统一命令接口与人机协作
    • 浏览器、profile 与登录态
    • 窗口尺寸与页面视口
    • 接入智能体:观察、执行与验证
    • 表单与多层弹窗排障:防止重复提交
    • 调用追踪、页面留档与文件上传
    • 站点配方、技能与异步作业
    • Windows OCR:本地图片与页面文字识别
    • 配置项与运维自省
    • 命令清单
    • 源码教程:从 HTTP 请求到命令执行
    • DOM 原理:前端如何生成可交互快照
    • DOM 原理:Java 模型与跨 Frame 索引
    • 正文提取与上层结构化处理
    • 源码教程:生命周期、导航与页签
    • 源码教程:DOM、页面状态与元素读取
    • 源码教程:点击、输入、键盘与鼠标
    • 源码教程:等待条件与 JavaScript 执行
    • 源码教程:文件上传、截图、PDF 与 OCR
    • 源码教程:Cookie、存储与页面设置
    • 源码教程:网络记录、请求拦截与控制台
    • 源码教程:原生对话框、DOM 弹窗与人机协作
    • 源码教程:批量、配方、后台作业与维护
  • 61_java-uni-ai-server

    • 61_java-uni-ai-server
    • 语音合成系统
    • Fish.audio TTS 接口说明文档与 Java 客户端封装
    • 整合 fishaudio 到 java-uni-ai-server 项目
    • 待定
  • 62_java-llm-proxy

    • 62_java-llm-proxy
    • 使用tio-boot搭建多模型LLM代理服务
  • 63_java-kit-server

    • 63_java-kit-server
    • Java 执行 python 代码
    • 通过大模型执行 Python 代码
    • 执行 Python (Manim) 代码
    • 待定
    • 待定
    • 待定
    • 视频下载增加水印说明文档
  • 64_文件存储

    • 64_文件存储
    • 文件上传数据表
    • 本地存储
    • 存储到 亚马逊 S3
    • 存储到 Cloudflare R2
    • 存储到 腾讯 COS
    • 上传文件到阿里云 OSS
  • 65_tio-boot-admin

    • 65_tio-boot-admin
    • 入门指南:使用框架内置配置
    • 手动初始化数据库
    • 配置职责、生效条件与扩展边界
    • 整合数据库
    • 与前端集成
    • 文件上传
    • 网络请求
    • 单图片管理(只读模式)
    • 多图片管理
    • 布尔值管理
    • 字段联动
    • Word 管理
    • PDF 管理
    • 文章管理
    • 富文本编辑器
    • 整合 Enjoy 模版引擎
    • 历史可选方案:Token 存储与 Sa-Token
    • 业务 API 与 H5 / 小程序联调
    • 方法路由与业务鉴权
    • 整合 Redis
    • 整合 Elasticsearch
    • 后端开发规范:tio-boot、java-db 与 Kv
    • 多表实现文件数据存储
  • 66_第三方登录注册

    • 66_第三方登录注册
    • 邮箱登录和注册
    • 邮箱重置密码
    • 腾讯云短信登录注册
    • 腾讯云短信重置密码
    • 阿里云短信登录和注册
    • 阿里云短信重置密码
    • 微信登录与绑定手机号
    • 支付宝登录与绑定手机号
    • 微信小程序手机号快捷登录
    • Google登录
    • 阿里云邮件推送验证邮箱
  • 67_pay

    • 支付集成
    • 微信小程序支付:普通支付
    • 微信支付:Native 扫码支付(PC 网页扫码)
    • 支付宝:电脑网站支付接入指南
  • 68_firebase

    • 68_firebase
    • 整合 google firebase
    • Firebase Storage
    • Firebase Authentication
    • 使用 Firebase Admin SDK 进行匿名用户管理与自定义状态标记
    • 导出用户
    • 登录注册
    • 注册回调
  • 69_clerk

    • 69_clerk
    • Clerk
  • 70_tio-im

    • 70_tio-im
    • 通讯协议文档
    • ChatPacket.proto 文档
    • java protobuf
    • 数据表设计
    • 创建工程
    • 登录
    • 历史消息
    • 发消息
  • 71_tio-mail-wing

    • 71_tio-mail-wing
    • tio-mail-wing简介
    • 任务1:实现POP3系统
    • 使用 getmail 验证 tio-mail-wing POP3 服务
    • 任务2:实现 SMTP 服务
    • 数据库初始化文档
    • 用户管理
    • 邮件管理
    • 任务3:实现 SMTP 服务 数据库版本
    • 任务4:实现 POP3 服务(数据库版本)
    • IMAP 协议
    • 拉取多封邮件
    • 任务5:实现 IMAP 服务(数据库版本)
    • IMAP实现讲解
    • IMAP 手动测试脚本
    • IMAP 认证机制
    • 主动推送
    • namesapce
    • CONDSTORE and QRESYNC
  • 72_tio-mcp-server

    • 72_tio-mcp-server
    • 实现 MCP Server 开发指南
    • MCP 协议
    • /zh/72_tio-mcp-server/11.html
  • 73_tio-sip

    • 73_tio-sip
    • SIP Server 第一版原理说明
    • SIP Server 第一版实战
    • 一、Windows 平台测试
    • SIP Server 第二版实战
    • SIP Server 第三版实战
    • 性能优化
    • 基于 MediaProcessor 对接 Realtime 模型说明
    • 对接大语言模型
    • 支持 G722 宽带语音
    • G722编码和解码
    • 会话级采样率转换
    • 增加 9196 回声测试分机
    • 语音系统链路说明
    • 一、Gemini Realtime 的打断机制
  • 74_tio-log-server

    • 74_tio-log-server
    • 简介
    • 收集 docker 日志
    • 入库
  • 75_性能测试

    • 75_性能测试
    • 压力测试 - tio-http-serer
    • 压力测试 - tio-boot
    • 压力测试 - tio-boot-native
    • 压力测试 - netty-boot
    • 性能测试对比
    • TechEmpower FrameworkBenchmarks
    • 压力测试 - tio-boot 12 C 32G
    • HTTP/1.1 Pipelining 性能测试报告
    • tio-boot vs Quarkus 性能对比测试报告
  • 76_tio-boot

    • 76_tio-boot
    • 源码阅读入口
    • Swagger 整合到 Tio-Boot 中的指南
    • 启动与关闭生命周期
    • HTTP 请求分发与路由优先级
    • 高性能网络编程中的 ByteBuffer 分配与回收策略
    • TioBootServerHandler 源码解析
  • 77_案例

    • 77_案例
    • 封装 IP 查询服务
    • tio-boot 案例 - 全局异常捕获与企业微信群通知
    • tio-boot 案例 - 文件上传和下载
    • tio-boot 案例 - 整合 ant design pro 增删改查
    • tio-boot 案例 - 流失响应
    • tio-boot 案例 - 增强检索
    • tio-boot 案例 - 整合 function call
    • tio-boot 案例 - 定时任务 监控 PostgreSQL、Redis 和 Elasticsearch
    • Tio-Boot 案例:使用 SQLite 整合到登录注册系统
    • tio-boot 案例 - 执行 shell 命令
    • /zh/77_%E6%A1%88%E4%BE%8B/11.html
    • /zh/77_%E6%A1%88%E4%BE%8B/12.html
    • /zh/77_%E6%A1%88%E4%BE%8B/13.html

客户端:dsb 命令行、Python 与 PowerShell

完成 第一个任务 后,可以选择命令行、Python 库或 PowerShell 客户端接入同一个 HTTP 服务。客户端负责组装请求、处理响应和本地留档;真正的浏览器操作仍由 Java 服务执行。

1. 客户端入口与运行目录

入口适用场景
client/dsb.py跨平台命令行和 Python 程序,使用标准库
client/dsb.cmdWindows 包装,委托给 Python 客户端
client/dsbLinux/macOS 包装
scripts/trace/browse.ps1PowerShell 中发送 JSON、筛选结果与留档

下面的命令都从项目根目录执行。Windows PowerShell 调用包装脚本时使用 .\client\dsb.cmd;参数含 &、^ 或 % 等字符时,优先通过 JSON 文件传递,避免包装层解释特殊字符。

2. 常用命令行调用

python client/dsb.py --port 10049 health
python client/dsb.py methods click
python client/dsb.py --id 1001 start --headful
python client/dsb.py --id 1001 run go_to_url -p url=https://example.com
python client/dsb.py --id 1001 state --full
python client/dsb.py --id 1001 run get_form_state --select data.fields
python client/dsb.py --id 1001 close

1001 是教学任务 ID,使用前先确认未被占用。--base-url、--host、--port 指定服务位置;命令行配置优先于客户端环境配置,再使用默认值。环境配置包含 DSB_BASE_URL、DSB_HOST、DSB_PORT、DSB_TASK_ID、DSB_SESSION 等。

参数较复杂时使用文件

保存 params.json:

{"selector":"#display-name","text":"示例用户","mode":"type"}

在已有任务中调用:

python client/dsb.py --id 1001 run input_text_by_selector --params @params.json

-p k=v 由客户端解析常见数字、布尔和 JSON 值。复杂对象、中文脚本或多行文本适合放在文件中。

三种“精简”不是一回事

选项行为
--summary / --compact精简本地终端输出,不会改变服务端请求
--response-mode compact在请求信封设置 responseMode,让服务裁剪响应
--select data.fields在客户端按字段路径投影结果

--diagnostics 与 responseMode 同属请求信封,不放在 params 内。需要完整回执时使用 --json,并按任务需要选择字段。

3. 批量执行与异步查询

保存 commands.json:

[
  {"get_title": {}},
  {"get_url": {}}
]
python client/dsb.py --id 1001 batch commands.json
python client/dsb.py --id 1001 batch commands.json --async --wait

CLI 默认遇到动作失败停止,--keep-going 允许继续;断言停止由 --stop-on-expect-failure 单独控制。Python Client.batch 的 stop_on_error 默认值是 false,业务代码应显式传入,不能把 CLI 默认值套用到库调用。

异步提交返回 jobId,后续用 job 查询。HTTP 超时不等于动作未执行,尤其不要自动重新发送提交类批次。完整语义见 批量与作业。

4. 在 Python 程序中使用

下面示例可保存为项目根目录的 browser_client_example.py。只有 start 成功后才进入 finally 关闭自己的任务,避免启动失败时关闭已被其他任务占用的教学 ID。

import sys
sys.path.insert(0, "client")
from dsb import Client, TransportError

def require_ok(response):
    if not 200 <= response.status < 300 or not response.ok:
        raise RuntimeError(response.msg or str(response.data))
    return response.data

def main():
    client = Client(port=10049, task_id=1001, session="tutorial")
    require_ok(client.start(headless=True))
    try:
        require_ok(client.command("go_to_url", {"url": "https://example.com"}))
        state = require_ok(client.command("get_browser_state", {}))
        print(state.get("text", ""))
        result = client.batch(
            [{"get_title": {}}, {"get_url": {}}],
            stop_on_error=True)
        print(require_ok(result))
        job = require_ok(client.batch(
            [{"get_title": {}}], asynchronous=True, stop_on_error=True))
        completed = client.wait_job(job["jobId"])
        job_data = require_ok(completed)
        if job_data.get("status") != "done":
            raise RuntimeError("异步作业未成功完成:" + str(job_data))
        print(job_data)
    finally:
        closed = client.close()
        if not closed.ok:
            print("任务关闭失败:", closed.msg, file=sys.stderr)

if __name__ == "__main__":
    try:
        main()
    except TransportError as error:
        print("传输失败,请核对服务和任务状态:", error, file=sys.stderr)
        raise SystemExit(1)

Client.command 的业务失败不抛异常,调用方检查 response.ok、msg、data;连接失败、超时和无法解析的响应抛 TransportError。Client.wait_job 返回 Response 对象,应通过 completed.data 读取作业状态和逐步结果,不能把它当成字典,也不能把“轮询结束”当作任务成功。

5. 上传和页面脚本

python client/dsb.py upload data/sample.txt
python client/dsb.py --id 1001 js @scripts/page-title.js

upload 将本地文件暂存到服务端;它本身不会选择网页里的文件输入框。读取返回的 path 后,再调用 upload_file,参数见 文件操作。js 通过 execute_js 执行页面脚本,读取与修改页面的脚本应按任务需要区分,不能对修改操作盲目重试。

6. PowerShell 客户端

保存 request.json:

{"id":1001,"method":"get_title","params":{}}
pwsh scripts/trace/browse.ps1 -Health -Port 10049
pwsh scripts/trace/browse.ps1 -PayloadFile request.json -Session tutorial -Json
pwsh scripts/trace/browse.ps1 -Last -Session tutorial -Json

-Last 读取本会话上一次保存的响应,不重新发送浏览器命令。-PayloadJson 适合短 JSON,复杂请求使用 -PayloadFile;-TimeoutSec 设置 HTTP 超时。

Python 客户端默认对终端输出和留档脱敏;PowerShell 客户端默认只对落盘内容脱敏,终端仍显示原文。二者共享操作协议,但输出与脱敏行为不同。

7. 日志、退出码与调用链

Python 默认在 logs/agent/会话名 下写入编号 req.json、res.json 和 steps.log。编号接续已有记录;--no-record 可关闭留档。脱敏是按规则处理,截图和未知敏感字段仍需另行检查。

CLI 退出码含义
0调用成功
1传输或响应协议错误
2业务失败
3参数用法错误
CLI 参数或 Python 方法调用
  → Client.command 组装 id / method / params
  → Client.request 发送 HTTP、解码、解析 JSON
  → Response 提供 ok / msg / data
  → Client._record 保存请求和响应
  → Printer 或调用程序展示结果

下面给出当前客户端实现。它们是现有模块中的方法,依赖同文件的辅助函数;直接使用仓库中的完整客户端,不需要自行拼接这些片段。

源码:HTTP 传输与 JSON 解码

HTTP 错误响应仍尝试读取 JSON;网络失败与非 JSON 响应转为 TransportError。

源码:client/dsb.py。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

def request(self, path: str, *, method: str = "GET", body: bytes | None = None,
                content_type: str | None = None, query: dict | None = None,
                timeout: float | None = None) -> Response:
        url = self._url(path)
        if query:
            url = url + "?" + urllib.parse.urlencode({k: v for k, v in query.items() if v is not None})
        headers = {"Accept": "application/json", "User-Agent": f"dsb/{__version__}"}
        if content_type:
            headers["Content-Type"] = content_type
        if body is not None and content_type is None:
            headers["Content-Type"] = "application/octet-stream"
        request = urllib.request.Request(url, data=body, headers=headers, method=method)
        started = time.time()
        try:
            with urllib.request.urlopen(request, timeout=timeout or self.timeout) as response:
                raw = response.read()
                status = response.status
                encoding = response.headers.get("Content-Encoding", "")
        except urllib.error.HTTPError as error:  # 服务端自己回了 4xx/5xx
            raw = error.read()
            status = error.code
            encoding = error.headers.get("Content-Encoding", "") if error.headers else ""
        except urllib.error.URLError as error:
            raise TransportError(f"连不上 {url}:{error.reason}(服务起了吗?地址对吗?)") from None
        except TimeoutError:
            raise TransportError(f"请求超时 {url}(超过 {timeout or self.timeout:.0f} 秒)") from None
        elapsed_ms = int((time.time() - started) * 1000)

        text = _decode_body(raw, encoding)
        try:
            envelope = json.loads(text)
        except ValueError:
            raise TransportError(
                f"响应不是合法 JSON({url},HTTP {status},耗时 {elapsed_ms}ms):{text[:300]}") from None
        if not isinstance(envelope, dict):
            raise TransportError(f"响应不是 JSON 对象({url}):{text[:200]}")
        return Response(url, status, envelope, text, elapsed_ms)

源码:统一命令封装

信封级字段与 params 分开设置,成功和传输失败都进入留档流程。

源码:client/dsb.py。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

def command(self, method: str, params: dict | None = None, *, task_id: int | str | None = None,
                timeout: float | None = None, label: str | None = None) -> Response:
        """发一条命令。失败(业务失败)不抛异常,由调用方看 `ok`;传输失败抛 TransportError。"""
        payload = {"id": _as_task_id(task_id if task_id is not None else self.task_id),
                   "method": method, "params": params or {}}
        # responseMode / diagnostics 是**信封级**字段(与 method/params 平级),不是 params 里的东西
        if self.response_mode:
            payload["responseMode"] = self.response_mode
        if self.diagnostics:
            payload["diagnostics"] = True
        try:
            response = self.request(PATH_COMMAND, method="POST", body=_json_bytes(payload),
                                    content_type="application/json", timeout=timeout)
        except TransportError as error:
            self._record(label or method, payload, None, str(error))
            raise
        self.last_response = response
        self._record(label or method, payload, response)
        return response

源码:批量参数封装

注意库调用的 stop_on_error 默认值,以及 async 如何进入服务端参数。

源码:client/dsb.py。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

def batch(self, commands: list, *, stop_on_error: bool = False, stop_on_expect_failure: bool = False,
              asynchronous: bool = False, max_duration_ms: int | None = None,
              timeout: float | None = None, task_id: int | str | None = None) -> Response:
        """跑一批命令。`asynchronous=True` 时立刻返回 jobId(不占用 HTTP 超时)。"""
        params: dict = {"commands": commands, "stopOnError": stop_on_error}
        if stop_on_expect_failure:
            params["stopOnExpectFailure"] = True
        if asynchronous:
            params["async"] = True
        if max_duration_ms:
            params["maxDurationMs"] = max_duration_ms
        return self.command("commands", params, task_id=task_id, timeout=timeout, label="commands")

源码:轮询异步作业

按 jobId 查询,不重复提交原命令;终态内容仍需由业务代码核验。

源码:client/dsb.py。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

def wait_job(self, job_id: str, *, poll: float = 1.0, timeout: float = 600.0,
                 on_tick=None) -> Response:
        """轮询异步任务直到结束(running 之外的状态都算结束)"""
        deadline = time.time() + timeout
        last = None
        while True:
            last = self.job(job_id)
            status = (last.data or {}).get("status") if isinstance(last.data, dict) else None
            if on_tick:
                on_tick(last)
            if status and status != "running":
                return last
            if time.time() >= deadline:
                raise TransportError(f"等任务 {job_id} 超时(超过 {timeout:.0f} 秒,最后状态 {status})")
            time.sleep(poll)

源码:请求和响应留档

先脱敏再保存,日志文件写入失败不改变浏览器动作结果。

源码:client/dsb.py。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

def _record(self, label: str, payload, response: Response | None, error: str | None = None) -> None:
        """把这一次调用落盘:NNN.req.json / NNN.res.json + 一行 steps.log"""
        if not self.record:
            return
        index = self.counter
        self.counter += 1
        try:
            self.record_dir.mkdir(parents=True, exist_ok=True)
            request_text = json.dumps(self._mask(payload), ensure_ascii=False, indent=2)
            (self.record_dir / f"{index:03d}.req.json").write_text(request_text, encoding="utf-8")
            if response is not None:
                response_text = json.dumps(self._mask(response.envelope), ensure_ascii=False, indent=2)
            else:
                response_text = json.dumps({"sendFailed": True, "error": error}, ensure_ascii=False, indent=2)
            (self.record_dir / f"{index:03d}.res.json").write_text(response_text, encoding="utf-8")

            stamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            if response is None:
                line = f"{stamp} #{index:03d} id={self.task_id} {label} SEND-FAILED {error}"
            else:
                verdict = "OK" if response.ok else "FAIL"
                detail = _summarize(response.data)
                line = (f"{stamp} #{index:03d} id={self.task_id} {label} {verdict} "
                        f"{response.elapsed_ms}ms{detail}")
                if not response.ok:
                    line += f" msg={response.msg}"
            with (self.record_dir / "steps.log").open("a", encoding="utf-8") as handle:
                handle.write(line + "\n")
        except OSError as error_io:  # 落盘失败不该让调用失败
            if self.verbose:
                print(f"[warn] 记录失败:{error_io}", file=sys.stderr)

源码:CLI 与响应对象

命令行参数在 build_client 中组装成 Client;cmd_run 负责加载命令参数并将服务业务状态转换为退出码。Response.ok 读取响应信封里的 ok;HTTP 状态另存于 status,需要时由调用方分别检查。

展开 build_client

源码:client/dsb.py。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

def build_client(args) -> Client:
    """按「命令行 > 环境变量 > 内置默认值」的优先级拼出客户端"""
    def opt(name, fallback=None):
        value = getattr(args, name, None)
        return fallback if value is None else value

    base_url = opt("base_url") or os.environ.get("DSB_BASE_URL")
    host = opt("host") or os.environ.get("DSB_HOST") or DEFAULT_HOST
    port = opt("port") or int(os.environ.get("DSB_PORT") or DEFAULT_PORT)
    task_id = opt("id") or os.environ.get("DSB_TASK_ID") or DEFAULT_TASK_ID
    session = None if getattr(args, "no_record", False) else (
        opt("session") or os.environ.get("DSB_SESSION") or DEFAULT_SESSION)
    patterns = tuple(opt("redact_pattern", ()) or ())
    env_patterns = os.environ.get("DSB_REDACT")
    if env_patterns:
        patterns += tuple(part for part in re.split(r"[,;]", env_patterns) if part.strip())
    return Client(base_url=base_url, host=host, port=port, task_id=task_id,
                  timeout=opt("timeout", DEFAULT_TIMEOUT), session=session,
                  redact_enabled=not getattr(args, "no_redact", False), redact_patterns=patterns,
                  verbose=getattr(args, "verbose", False), record_dir=opt("record_dir"),
                  response_mode=getattr(args, "response_mode", None),
                  diagnostics=bool(getattr(args, "diagnostics", False)))
展开 cmd_run

源码:client/dsb.py。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

def cmd_run(client: Client, args, out: Printer) -> int:
    params = load_payload(args.params, args.param)
    response = client.command(args.method, params)
    picked = pick_index(response.envelope, args.index) if args.index is not None else None
    out.response(response, label=args.method, payload=picked)
    return EXIT_OK if response.ok else EXIT_BUSINESS
展开 cmd_batch

源码:client/dsb.py。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

def cmd_batch(client: Client, args, out: Printer) -> int:
    text = read_batch_source(args.file)
    try:
        commands = json.loads(text)
    except ValueError as error:
        raise UsageError(f"批量命令不是合法 JSON:{error}") from None
    if isinstance(commands, dict) and "commands" in commands:
        commands = commands["commands"]  # 允许直接喂 {"commands":[...]} 或整个请求体
    if not isinstance(commands, list):
        raise UsageError("批量命令必须是数组,例如 [{\"get_title\":{}},{\"get_url\":{}}]")

    response = client.batch(commands, stop_on_error=not args.keep_going,
                            stop_on_expect_failure=args.stop_on_expect_failure,
                            asynchronous=args.async_mode, max_duration_ms=args.max_duration_ms)
    if args.async_mode:
        out.response(response, label="commands(async)")
        if not response.ok or not args.wait:
            return EXIT_OK if response.ok else EXIT_BUSINESS
        job_id = (response.data or {}).get("jobId")
        if not job_id:
            out.warn("服务端没有返回 jobId,无法等待")
            return EXIT_BUSINESS
        return wait_and_report(client, out, job_id, poll=args.poll, timeout=args.wait_timeout)
    out.response(response, label="commands",
                 payload=pick_index(response.envelope, args.index) if args.index is not None else None)
    return EXIT_OK if response.ok else EXIT_BUSINESS
展开 Response

源码:client/dsb.py。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

class Response:
    """一次调用的结果:服务端信封 + 本地观测信息"""

    def __init__(self, url: str, status: int, envelope: dict, raw: str, elapsed_ms: int):
        self.url = url
        self.status = status
        self.envelope = envelope
        self.raw = raw
        self.elapsed_ms = elapsed_ms

    @property
    def ok(self) -> bool:
        return bool(self.envelope.get("ok"))

    @property
    def code(self) -> int:
        return int(self.envelope.get("code") or 0)

    @property
    def msg(self) -> str:
        return self.envelope.get("msg") or ""

    @property
    def data(self):
        return self.envelope.get("data")

    def __repr__(self) -> str:  # pragma: no cover - 只为调试方便
        return f"<Response ok={self.ok} code={self.code} msg={self.msg!r} elapsed={self.elapsed_ms}ms>"

源码:PowerShell 完整客户端

按 param 参数声明、JSON 读取、HTTP 请求、Write-Response 和日志写入的顺序阅读。不要将 PowerShell 的终端输出脱敏策略与 Python 客户端混为一谈。

展开 browse.ps1

源码:scripts/trace/browse.ps1。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

<#
.SYNOPSIS
  把一条命令发给本地浏览器服务,并把**发出去的请求**与**收回来的响应**都留档到本地。
  顺带把几件每次都要手写的小事也包进来:换端口(-Port)、只取业务结果(-Json / -Index)、
  探活(-Health)、复看上一次(-Last)。

.DESCRIPTION
  服务端自己在 logs/trace/ 下也留了一份审计(见 CommandTraceLog),这个脚本是客户端这一侧的另一半:
  它记录「我到底发了什么」(发送前就落盘,连没连上服务都能看出来),并给每次调用编一个可读的序号,
  便于按顺序复看整个过程。

  每个会话一个目录 logs/agent/<会话名>/:
    NNN.req.json    第 NNN 次调用发出去的请求体(原文)
    NNN.res.json    第 NNN 次调用收回来的响应体(原文,含整页 data.text)
    steps.log       每次调用一行:时间/序号/任务/方法/成败/耗时/关键字段

  两种「不发命令」的用法:
    -Health  只 GET 一次健康检查地址(/playwright/health),报告 ok/失败后退出,用来判断服务起没起来;
    -Last    不重发,直接把该会话最新一份 NNN.res.json 重新打出来(同样认 -Json / -Index)。
  这两种用法都不会新建会话目录、不会写 .current、不会写新的 NNN 文件。

  结果取值口径(给 -Json / -Index 用,免得每个调用方各写一遍 ConvertFrom-Json):
    单条命令      data.result,没有 result 就退到 data;
    commands 批次 data.results[i].data.result,同样没有 result 就退到那一条的 data。

.PARAMETER PayloadFile
  请求体 JSON 文件的路径(UTF-8)。用文件而不是命令行参数,是为了中文与引号不用层层转义。
  例:{"id":1001,"method":"go_to_url","params":{"url":"https://example.com"}}

.PARAMETER PayloadJson
  直接用一段 JSON 字符串当请求体(简单调用时方便,复杂的还是用 -PayloadFile)。

.PARAMETER Session
  会话目录名;默认沿用 logs/agent/.current 里记着的那一个,没有就按时间戳新建。

.PARAMETER NewSession
  强制新开一个会话目录。

.PARAMETER Compact
  只在屏幕上打印摘要(时间线那一行 + 关键字段),不打印完整响应体。默认打印完整响应 JSON —— 因为
  data.text 这类内容正是调用方要读的。

.PARAMETER Json
  只打印**命令结果**本身(漂亮 JSON),不打印摘要行、不打印文件路径、不打印脱敏提示,方便直接管道给
  别的工具(例如 | ConvertFrom-Json、| jq)。落盘的那份请求/响应照旧写,脱敏照旧生效。
  commands 批次打印「每条命令的结果」组成的数组;单条命令打印 data.result(没有就 data)。

.PARAMETER Index
  只输出 commands 批次里第 N 条命令的结果(N 从 0 起)。配 -Json 时就是「只吐这一条」;
  不配 -Json 时打印摘要行 + 这一条的结果。不填就是整批/整条输出。越界会报错并以 1 退出。

.PARAMETER Last
  不重发命令,直接把该会话最新一份 NNN.res.json(按文件名序号取最新)重新打出来。
  用于「刚才那次到底回了什么」的复查;-Json / -Index 同样生效,输出是当时那份(已脱敏的)原文。
  不给 -Session 时用 .current 记着的会话;会话目录不存在或没有 NNN.res.json 时报错并以 1 退出。

.PARAMETER Health
  只做一次健康检查:GET <base>/../health,打印 ok/失败与响应体后退出,不发命令、不落盘。
  加 -Json 时只打印响应体。服务没起来或响应异常时以 1 退出。

.PARAMETER Port
  服务端口,默认 10049。给了 -Port 又没给 -BaseUrl 时,地址按 http://localhost:<Port>/playwright/command
  拼;两个都给时 -BaseUrl 优先(自定义路径/远端地址还是用 -BaseUrl)。

.PARAMETER BaseUrl
  命令地址,默认 http://localhost:10049/playwright/command(即默认端口 10049)。
  与 -Port 同时给出时以本参数为准。

.PARAMETER NoRedact
  关掉落盘前的脱敏。**默认是脱敏的**:写进 .req.json / .res.json / steps.log 之前,手机号、18 位
  身份证号或统一社会信用代码、邮箱、16~19 位长数字会被替换成 ***,与服务端 CommandTraceLog 用同一套
  规则(服务端默认也脱敏)。屏幕上打印的内容**始终是原文**,方便你当场看值;只有落盘的那份被掩。
  需要「日志里也要原文」时加这个开关(例如排查「这个值到底传没传对」)。

.PARAMETER RedactPattern
  追加的自定义脱敏正则(可给多个),在内置规则之后生效。例:公司名、商标名。

.PARAMETER TimeoutSec
  HTTP 超时(秒),默认 300。超时或响应不是合法 JSON 时,会打印一行带 URL 与耗时的错误并以 1 退出。

.EXAMPLE
  pwsh scripts/trace/browse.ps1 -PayloadFile tmp\step1.json

.EXAMPLE
  pwsh scripts/trace/browse.ps1 -PayloadJson '{"id":1001,"method":"get_url"}' -Compact

.EXAMPLE
  pwsh scripts/trace/browse.ps1 -PayloadFile tmp\step1.json -RedactPattern '某某科技有限公司','某某课堂'

.EXAMPLE
  # 服务起在 10050 上:只给端口就行,不用再手写整条 BaseUrl
  pwsh scripts/trace/browse.ps1 -PayloadFile tmp\payloads\list-unsubmitted.json -Session verify-browse -Port 10050

.EXAMPLE
  # 只要批次里第 0 条命令的业务结果,直接给下游用
  pwsh scripts/trace/browse.ps1 -PayloadFile tmp\payloads\list-unsubmitted.json -Session verify-browse -Port 10050 -Json -Index 0

.EXAMPLE
  # 不重跑,复查上一次到底回了什么
  pwsh scripts/trace/browse.ps1 -Last -Session verify-browse -Json

.EXAMPLE
  # 一条命令判断服务起没起来(ok/失败 + 响应体)
  pwsh scripts/trace/browse.ps1 -Health -Port 10049
#>
[CmdletBinding(DefaultParameterSetName = 'File')]
param(
  [Parameter(ParameterSetName = 'File', Mandatory = $true)]
  [string]$PayloadFile,

  # 参数集名字从 Json 改成 Inline:下面新增的 -Json 开关是另一回事,名字分开免得看混
  [Parameter(ParameterSetName = 'Inline', Mandatory = $true)]
  [string]$PayloadJson,

  # 只探活,不发命令
  [Parameter(ParameterSetName = 'Health', Mandatory = $true)]
  [switch]$Health,

  # 复看:不重发,重新打印该会话最新一份响应
  [Parameter(ParameterSetName = 'Last', Mandatory = $true)]
  [switch]$Last,

  [string]$Session,

  [switch]$NewSession,

  [switch]$Compact,

  [switch]$Json,

  # -1 = 没给 -Index,整批/整条输出
  [int]$Index = -1,

  [switch]$NoRedact,

  [string[]]$RedactPattern,

  [int]$TimeoutSec = 300,

  [int]$Port = 10049,

  [string]$BaseUrl
)

$ErrorActionPreference = 'Stop'
$repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
$agentRoot = Join-Path $repoRoot 'logs\agent'
$currentFile = Join-Path $agentRoot '.current'

# 地址:两者都给时 -BaseUrl 优先;只给 -Port 就按端口拼;都不给时 -Port 的默认值 10049 与原来的默认地址一致
if (-not $BaseUrl) {
  $BaseUrl = "http://localhost:$Port/playwright/command"
}

# 落盘脱敏:与服务端 CommandTraceLog.BUILT_IN_PATTERNS 保持同一套规则,两边日志的口径才一致
# 边界写成「前后不能还是数字/字母」而不是 \b:实测 1[3-9]\d{9} 会把 13 位的毫秒时间戳
# (2026 年的 1790…,开头正好是 17)当成手机号,把 recordedSince 掩成 ***87,反而看不懂。
$script:redactMask = '***'
$script:redactHits = 0
$builtInPatterns = @(
  '(?<!\d)1[3-9]\d{9}(?!\d)',
  '(?<!\d)[1-9]\d{5}(?:19|20)\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])\d{3}[0-9Xx](?![0-9A-Za-z])',
  '(?<![0-9A-Za-z])[0-9A-HJ-NPQRTUWXY]{18}(?![0-9A-Za-z])',
  '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}',
  '(?<!\d)\d{16,19}(?!\d)'
)

<#
  把要落盘的文本里的个人信息替换成掩码。
  只作用于**写文件**的内容:屏幕上打印的仍是原文,免得排查时反而看不到值。
#>
function Protect-Text {
  param([string]$Text)
  if ($NoRedact -or [string]::IsNullOrEmpty($Text)) { return $Text }
  $patterns = @($builtInPatterns)
  if ($RedactPattern) { $patterns += $RedactPattern }
  $result = $Text
  foreach ($pattern in $patterns) {
    if ([string]::IsNullOrWhiteSpace($pattern)) { continue }
    try {
      $hits = [regex]::Matches($result, $pattern).Count
      if ($hits -gt 0) {
        $script:redactHits += $hits
        $result = [regex]::Replace($result, $pattern, $script:redactMask)
      }
    }
    catch {
      # 单条规则写坏不该让整条日志丢掉
    }
  }
  return $result
}

# 摘要行里要露一眼的字段
$highlightKeys = @('url', 'title', 'seq', 'screenshot', 'state_file', 'changed', 'changeStatus', 'status',
  'errorCode', 'engine', 'count', 'succeeded', 'failed', 'pageIndex', 'requestId', 'path')

<#
  从对象里挑几个关键字段拼成一行用的紧凑串。
#>
function Get-Highlight {
  param($Obj, [string[]]$Keys)
  $picked = [ordered]@{}
  foreach ($k in $Keys) {
    $v = $Obj.$k
    if ($null -ne $v -and "$v" -ne '') {
      $s = "$v"
      if ($s.Length -gt 300) { $s = $s.Substring(0, 300) + '…' }
      $picked[$k] = $s
    }
  }
  return $picked
}

<#
  摘要行:时间/序号/任务/方法/成败/耗时/关键字段。本次调用与 -Last 复看共用这一份格式,
  免得两边的输出长得不一样。
#>
function Format-SummaryLine {
  param($ResObj, [string]$Stamp, [string]$TaskId, [string]$Method, [string]$Time, [string]$ElapsedText)
  $line = "$Time #$Stamp id=$TaskId $Method "
  if ($null -ne $ResObj) {
    $okText = if ($ResObj.ok) { 'OK' } else { 'FAIL' }
    $line += "$okText $ElapsedText"
    if ($ResObj.msg) { $line += " msg=$($ResObj.msg -replace '\s+', ' ')" }
    if ($null -ne $ResObj.data) {
      $hl = Get-Highlight -Obj $ResObj.data -Keys $highlightKeys
      foreach ($k in $hl.Keys) { $line += " $k=$($hl[$k])" }
      if ($ResObj.data.PSObject.Properties.Name -contains 'results') {
        foreach ($step in $ResObj.data.results) {
          $mark = if ($step.ok) { 'ok' } else { 'FAIL' }
          $line += " | $($step.index):$($step.command)=$mark"
          if ($step.msg) { $line += "($($step.msg -replace '\s+', ' '))" }
        }
      }
    }
  }
  else {
    $line += "响应不是合法 JSON $ElapsedText"
  }
  return $line
}

<#
  取「业务结果」:有 data.result 就取 result,没有就退到 data,两者都没有就把节点原样给出。
  批次里每条命令也走这个规则,-Json 与 -Index 的取值口径才一致。
#>
function Get-ResultNode {
  param($Node)
  if ($null -eq $Node) { return $null }
  $names = @($Node.PSObject.Properties.Name)
  if ($names -contains 'data') {
    $data = $Node.data
    if ($null -eq $data) { return $null }
    if (@($data.PSObject.Properties.Name) -contains 'result') { return $data.result }
    return $data
  }
  return $Node
}

<#
  算出「要打印什么」:整批(results 数组)/ 单条 / -Index 指定的那一条。
  Valid=$false 表示 -Index 越界,由调用方报错并退出 1。
  用 ArrayList 装批次,单个元素也不会被 PowerShell 拆包(拆了就序列化不出数组)。
#>
function Select-Output {
  param($ResObj)
  $batch = $null
  if ($null -ne $ResObj -and $null -ne $ResObj.data -and
      (@($ResObj.data.PSObject.Properties.Name) -contains 'results')) {
    $batch = New-Object System.Collections.ArrayList
    foreach ($step in @($ResObj.data.results)) { [void]$batch.Add((Get-ResultNode -Node $step)) }
  }
  $count = 1
  if ($null -ne $batch) { $count = $batch.Count }
  $valid = $Index -lt $count
  $picked = $null
  if ($valid -and $Index -ge 0) {
    if ($null -ne $batch) { $picked = $batch[$Index] } else { $picked = Get-ResultNode -Node $ResObj }
  }
  $value = $null
  if ($Index -ge 0) { $value = $picked }
  elseif ($null -ne $batch) { $value = $batch }
  else { $value = Get-ResultNode -Node $ResObj }
  return [pscustomobject]@{ Batch = $batch; Picked = $picked; Value = $value; Count = $count; Valid = $valid }
}

<#
  健康检查地址 = 把命令地址里的 /playwright/command 换成 /playwright/health。
  认不出来就在主机根下拼 /playwright/health,不至于因为自定义路径直接报错。
#>
function Get-HealthUrl {
  param([string]$Url)
  if ($Url -match '^(?<root>.*)/playwright/command/?$') { return $Matches['root'] + '/playwright/health' }
  if ($Url -match '^(?<root>.*)/command/?$') { return $Matches['root'] + '/playwright/health' }
  return $Url.TrimEnd('/') + '/playwright/health'
}

function Resolve-SessionDir {
  param([string]$Name, [switch]$Force)
  if (-not $Name) {
    if (-not $Force -and (Test-Path $currentFile)) {
      $Name = (Get-Content $currentFile -Raw).Trim()
    }
    if (-not $Name) {
      $Name = Get-Date -Format 'yyyyMMdd-HHmmss'
    }
  }
  $dir = Join-Path $agentRoot $Name
  New-Item -ItemType Directory -Force -Path $dir | Out-Null
  # 一律写无 BOM 的 UTF-8,免得下游工具把 BOM 当成内容的一部分
  [System.IO.File]::WriteAllText($currentFile, $Name, [System.Text.UTF8Encoding]::new($false))
  return $dir
}

<#
  只算会话目录路径:不建目录、不写 .current。给 -Last 复看用,免得顺手改了「当前会话」指针。
#>
function Get-SessionDirPath {
  param([string]$Name)
  if (-not $Name -and (Test-Path $currentFile)) {
    $Name = (Get-Content $currentFile -Raw).Trim()
  }
  if (-not $Name) { return $null }
  return (Join-Path $agentRoot $Name)
}

<#
  从文件名里取序号;-Last 按序号取最新,不看 LastWriteTime(复制/同步过的目录时间戳不可靠)。
#>
function Get-ResSeq {
  param([string]$Name)
  if ($Name -match '^(\d+)\.res\.json$') { return [int]$Matches[1] }
  return -1
}

<#
  一行错误:超时/连不上/响应不是 JSON 时统一这么报,带 URL 与耗时,便于一眼定位。
  不抛异常,免得 PowerShell 打一堆堆栈出来。
#>
function Write-Fail {
  param([string]$Message)
  Write-Output ('ERROR ' + (($Message -replace '\s+', ' ').Trim()))
}

<#
  打一份响应:-Json 只吐结果 JSON(可配 -Index 取某一条),否则沿用原来的「标题行 + 摘要行 + 完整响应」。
  -Index 不配 -Json 时打印「摘要行 + 这一条的结果」,因为 -Index 的用意就是只要那一条。
#>
function Write-Response {
  param([string]$Text, $Sel, [string]$Line, [string]$Title)
  if ($Json) {
    Write-Output (ConvertTo-Json -InputObject $Sel.Value -Depth 100)
    return
  }
  if ($Compact -and $Index -lt 0) {
    Write-Output $Line
    return
  }
  Write-Output $Title
  Write-Output $Line
  Write-Output ''
  if ($Index -ge 0) {
    Write-Output (ConvertTo-Json -InputObject $Sel.Picked -Depth 100)
  }
  else {
    Write-Output $Text
  }
}

# ---------------- 探活:不发命令、不落盘 ----------------
if ($Health) {
  $healthUrl = Get-HealthUrl -Url $BaseUrl
  $healthStarted = Get-Date
  $healthBody = $null
  $healthError = $null
  try {
    $healthResp = Invoke-WebRequest -Uri $healthUrl -Method Get -UseBasicParsing -TimeoutSec $TimeoutSec
    $healthBody = $healthResp.Content
  }
  catch {
    $healthError = $_.Exception.Message
  }
  $healthMs = [int]((Get-Date) - $healthStarted).TotalMilliseconds
  if ($null -eq $healthBody) {
    Write-Fail "健康检查失败 url=$healthUrl elapsed=${healthMs}ms error=$healthError"
    exit 1
  }
  $healthObj = $null
  try { $healthObj = $healthBody | ConvertFrom-Json } catch { }
  $healthOk = $true
  if ($null -ne $healthObj -and $healthObj.PSObject.Properties.Name -contains 'ok') {
    $healthOk = [bool]$healthObj.ok
  }
  if ($Json) {
    Write-Output $healthBody
  }
  elseif ($healthOk) {
    Write-Output "ok url=$healthUrl elapsed=${healthMs}ms"
    Write-Output $healthBody
  }
  else {
    Write-Fail "健康检查不 ok url=$healthUrl elapsed=${healthMs}ms body=$(($healthBody -replace '\s+', ' ').Trim())"
  }
  if ($healthOk) { exit 0 }
  exit 1
}

# ---------------- 复看:只读最新一份 NNN.res.json ----------------
if ($Last) {
  $lastDir = Get-SessionDirPath -Name $Session
  if (-not $lastDir -or -not (Test-Path $lastDir)) {
    Write-Fail "会话目录不存在:$lastDir"
    exit 1
  }
  $lastFile = Get-ChildItem -Path $lastDir -Filter '*.res.json' -ErrorAction SilentlyContinue |
    Sort-Object -Property @{ Expression = { Get-ResSeq -Name $_.Name } }, Name -Descending |
    Select-Object -First 1
  if ($null -eq $lastFile) {
    Write-Fail "会话目录里没有 NNN.res.json:$lastDir"
    exit 1
  }
  $lastStamp = (Get-ResSeq -Name $lastFile.Name).ToString('000')
  $lastText = [System.IO.File]::ReadAllText($lastFile.FullName, [System.Text.UTF8Encoding]::new($false))
  $lastObj = $null
  try { $lastObj = $lastText | ConvertFrom-Json } catch { }
  if ($null -eq $lastObj) {
    Write-Fail "保存的响应不是合法 JSON:$($lastFile.FullName)"
    exit 1
  }
  # 序号旁边的 id/method 从对应的 .req.json 里取,摘要行才和当初那次长得一样
  $lastMethod = '?'
  $lastTaskId = '?'
  $lastReq = Join-Path $lastDir "$lastStamp.req.json"
  if (Test-Path $lastReq) {
    try {
      $lastReqObj = (Get-Content $lastReq -Raw -Encoding utf8) | ConvertFrom-Json
      if ($lastReqObj.method) { $lastMethod = $lastReqObj.method }
      if ($null -ne $lastReqObj.id) { $lastTaskId = [string]$lastReqObj.id }
    }
    catch { }
  }
  $lastLine = Format-SummaryLine -ResObj $lastObj -Stamp $lastStamp -TaskId $lastTaskId -Method $lastMethod `
    -Time $lastFile.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss.fff') -ElapsedText '?ms'
  $lastSel = Select-Output -ResObj $lastObj
  if (-not $lastSel.Valid) {
    Write-Fail "-Index $Index 越界:#$lastStamp 只回了 $($lastSel.Count) 条结果"
    exit 1
  }
  $title = "---- #$lastStamp $lastMethod (会话 $(Split-Path -Leaf $lastDir),复看 $(Split-Path -Leaf $lastFile.FullName)) ----"
  if ($Index -ge 0) {
    $title = "---- #$lastStamp $lastMethod idx=$Index (会话 $(Split-Path -Leaf $lastDir),复看 $(Split-Path -Leaf $lastFile.FullName)) ----"
  }
  Write-Response -Text $lastText -Sel $lastSel -Line $lastLine -Title $title
  exit 0
}

# ---------------- 正常发命令 ----------------
if ($PSCmdlet.ParameterSetName -eq 'File') {
  if (-not (Test-Path $PayloadFile)) { throw "请求体文件不存在:$PayloadFile" }
  $payload = Get-Content $PayloadFile -Raw -Encoding utf8
}
else {
  $payload = $PayloadJson
}

$sessionDir = Resolve-SessionDir -Name $Session -Force:$NewSession

# 序号 = 目录里已有的响应文件数 + 1
$seq = @(Get-ChildItem -Path $sessionDir -Filter '*.res.json' -ErrorAction SilentlyContinue).Count + 1
$stamp = $seq.ToString('000')

$reqPath = Join-Path $sessionDir "$stamp.req.json"
$resPath = Join-Path $sessionDir "$stamp.res.json"
$stepsLog = Join-Path $sessionDir 'steps.log'

# 请求先落盘:即使服务没起来/请求发不出去,也能看出「我发了什么」。落盘前脱敏
[System.IO.File]::WriteAllText($reqPath, (Protect-Text $payload), [System.Text.UTF8Encoding]::new($false))

$method = '?'
$taskId = '?'
try {
  $parsed = $payload | ConvertFrom-Json
  if ($parsed.method) { $method = $parsed.method }
  if ($null -ne $parsed.id) { $taskId = [string]$parsed.id }
}
catch { }

$started = Get-Date
$responseText = $null
$failure = $null
try {
  $bytes = [System.Text.Encoding]::UTF8.GetBytes($payload)
  $resp = Invoke-WebRequest -Uri $BaseUrl -Method Post -Body $bytes `
    -ContentType 'application/json; charset=utf-8' -UseBasicParsing -TimeoutSec $TimeoutSec
  $responseText = $resp.Content
}
catch {
  $failure = $_.Exception.Message
}

$elapsedMs = [int]((Get-Date) - $started).TotalMilliseconds
$time = $started.ToString('yyyy-MM-dd HH:mm:ss.fff')

# 注意:解析出来的响应放在 $resObj 里,不能叫 $json —— PowerShell 变量不分大小写,会和 -Json 开关撞车
$resObj = $null
$sendFailed = $false
if ($null -ne $responseText) {
  [System.IO.File]::WriteAllText($resPath, (Protect-Text $responseText), [System.Text.UTF8Encoding]::new($false))
  try { $resObj = $responseText | ConvertFrom-Json } catch { }
  $line = Format-SummaryLine -ResObj $resObj -Stamp $stamp -TaskId $taskId -Method $method -Time $time -ElapsedText "${elapsedMs}ms"
}
else {
  # 没拿到响应:把失败原因写进 res 文件,免得只剩一个空序号
  $sendFailed = $true
  $line = "$time #$stamp id=$taskId $method SEND-FAILED ${elapsedMs}ms error=$failure"
  [System.IO.File]::WriteAllText($resPath, "{`"sendFailed`":true,`"error`":$(ConvertTo-Json $failure)}",
    [System.Text.UTF8Encoding]::new($false))
}

# 落盘的 steps.log 同样脱敏(URL 里也可能带手机号/令牌);屏幕上那行保持原文
if ($script:redactHits -gt 0) {
  $line += " redacted=$($script:redactHits)"
}
[System.IO.File]::AppendAllText($stepsLog, (Protect-Text $line) + [Environment]::NewLine,
  [System.Text.UTF8Encoding]::new($false))

$title = "---- #$stamp $method (会话 $(Split-Path -Leaf $sessionDir)) ----"
if ($Index -ge 0) { $title = "---- #$stamp $method idx=$Index (会话 $(Split-Path -Leaf $sessionDir)) ----" }

$exitCode = 0
if ($sendFailed) {
  # 超时/连不上:一行错误(带 URL 与耗时),不吐堆栈
  Write-Fail "请求发送失败 url=$BaseUrl elapsed=${elapsedMs}ms $method #$stamp error=$failure"
  $exitCode = 1
}
elseif ($null -eq $resObj) {
  # 响应不是合法 JSON:一行错误(带 URL 与耗时),非 -Json 时把原文也打出来便于看个究竟
  Write-Fail "响应不是合法 JSON url=$BaseUrl elapsed=${elapsedMs}ms $method #$stamp"
  if (-not $Json -and -not $Compact) {
    Write-Output $title
    Write-Output $line
    Write-Output ''
    Write-Output $responseText
  }
  $exitCode = 1
}
else {
  $sel = Select-Output -ResObj $resObj
  if (-not $sel.Valid) {
    Write-Fail "-Index $Index 越界:#$stamp $method 只回了 $($sel.Count) 条结果"
    $exitCode = 1
  }
  else {
    Write-Response -Text $responseText -Sel $sel -Line $line -Title $title
    if (-not $Json -and $script:redactHits -gt 0 -and -not $NoRedact) {
      Write-Output "# 落盘日志已脱敏 $($script:redactHits) 处(手机号/证件号/邮箱/长数字);要看原文加 -NoRedact"
    }
  }
}

exit $exitCode

下一步:统一命令协议。

Edit this page
Last Updated: 9/26/26, 2:46 PM
Contributors: Tong Li
Prev
第一个任务:打开页面、读取结果与关闭
Next
统一命令接口与人机协作