local-deep-research 1.10.3 深度解读:搜索时间范围默认值变更与多租户安全加固
【免费下载链接】local-deep-research~95% on SimpleQA (e.g. Qwen3.6-27B on a 3090). Supports all local and cloud LLMs (llama.cpp, Ollama, Google, ...). 10+ search engines - arXiv, PubMed, your private documents. Everything Local & Encrypted.项目地址: https://gitcode.com/GitHub_Trending/lo/local-deep-research
local-deep-research 1.10.3 是一次以"行为变更 + 安全加固"为主线的重要发布:它把全局搜索时间范围search.time_period的默认值从"过去一年"改为"全部时间",并对数据库完成 0030 迁移;同时在 WebSocket 会话生命周期、SSRF 防护、出站请求 IP 固定(pinning)、研究库 PDF 存储的多租户隔离等方向做了大量纵深加固。本文以 发布说明 为主体,结合仓库中的迁移脚本、安全环境配置与测试用例逐项展开,帮助你理解每项变更的实际影响、升级时必须执行的运维动作,以及各项防护在源码层面的落点。
读完本文后,你将掌握:升级 1.10.3 前必须知道的行为变更与数据迁移细节;六个新增/变更的运维级环境变量及其适用场景;SSRF 与会话加固在src/local_deep_research/security/与src/local_deep_research/settings/env_definitions/security.py中的实现位置;以及研究库 PDF 从共享目录切换到按用户隔离目录后的兼容性开关组合。
一、破坏性变更:search.time_period默认值改为"全部时间"
1.1 变更内容与动机
从 1.10.3 起,search.time_period的默认值由 "Past year"(y)改为 "All time"(all)。默认搜索不再被静默限制在最近一年之内。动机在迁移脚本的注释中写得很清楚:Tavily 引擎从 1.10.3 起真正尊重该设置(转发为 Tavily API 的time_range参数),而此前serpapi、wikinews等已接线的引擎也一直被同一个默认值驱动——"一个几乎所有人都没有主动选择、却悄悄过滤掉更久远(往往更权威)来源"的默认值,被判定为一个糟糕的默认,因此整体翻转为不做时间过滤(见 0030 迁移脚本 的背景说明)。
1.2 数据迁移 0030 的精确行为
迁移 0030_default_time_period_all.py 执行一条针对settings表的更新:
- 只更新
key = 'search.time_period'且存储值恰为"y"(JSON 序列化后带引号的 3 字符字面量"y",因为该列是 SQLAlchemyJSON类型)的行,将其改写为"all"; - 显式选择过
d(Past 24 hours)/w(week)/m(month)/all的用户不受影响; - 主动选择过 "Past year" 的用户其偏好会被一并翻转——因为主动选择与继承默认值在数据上无法区分;如仍需年度范围的结果,升级后在 Settings 中重新选择Past year即可;
- 该迁移提供
downgrade():把当前值为"all"的行回退为"y"(无法区分迁移行与原本就是all的行,因此保守地一并回退)。
1.3 设置的全局生效路径与优先级
发布说明明确该全局设置覆盖了所有搜索路径,并且优先级高于各引擎自身的默认参数:
- 生效引擎:Tavily、serpapi、Wikinews;
- 生效路径:所有基于
get_search()的流程(包括新闻订阅),以及直接调用create_search_engine()的调用方——包括默认的langgraph-agent策略和 MCP server; - 优先级:当全局设置存在时,
search.engine.web.<engine>.default_params.time_period这类按引擎的覆盖项不生效(仅在程序化快照等"全局设置缺席"的罕见场景下应用); - 例外:Brave 不受该全局设置覆盖,仍由自己的按引擎设置控制。
源码佐证:Tavily 引擎 头部注释说明其把LDR的时间范围代码映射为 Tavily API 的time_range(day/week/month/year),未知取值则省略该参数、不做过滤;默认设置定义见 default_settings.json(search.time_period及各引擎的default_params.time_period键)。
Wikinews 的配套变更:其内置默认时间范围从"隐式的一年窗口"改为 "All time";且遇到无法识别的取值时不再静默回退到一年,而是不施加任何过滤——因此除非显式选择更窄的时间范围,更久远的文章可能出现在结果中。
二、会话与 WebSocket 生命周期加固
2.1 登出立即吊销会话 Cookie
登出操作现在会立即吊销会话 Cookie。HTTP 与 WebSocket 两条认证路径都会校验 Cookie 对应的服务端会话 ID,因此已登出的会话无法再被复用——即使同一用户重新登录也无效;闲置/过期会话同样被拒绝(滑动过期语义)。这是后续多项加固(如后台日志持久化改造)能够成立的前提。
2.2 WebSocket 连接按会话拆除
此前活跃的 WebSocket 连接只在握手时校验一次会话;现在,只要背后的会话终结——登出、修改密码、会话过期、闲置连接被回收——对应连接就会被拆除。效果:
- 一个 Socket 不再能在其会话消亡后继续接收你的事件(包括携带密钥值的
settings_changed事件); - 拆除是按会话粒度(scoped per session)的:在某个标签页登出,不会断开其他标签页或设备上的连接;
- 每次 connect/subscribe/unsubscribe 都会对"仍然有效的会话"重新校验,作为纵深防御;
- 特别地,过期或被抓取的 Cookie 无法再打开一个全新 Socket 加入你的事件房间——即便你还有另一个会话(或一次正在运行的研究任务)保持着数据库连接。
对应的回归测试位于 test_socket_teardown_severs.py 等tests/security/用例中。
2.3 登出与数据库连接的新语义
1.10.3 还改变了"登出时数据库连接何时关闭"的行为,以配合上述会话吊销:
- 后台日志持久化不再能重新打开用户的加密数据库、也不会在登出后恢复其连接状态:排队的日志条目不再携带用户密码;日志队列的 drain 线程只在用户数据库已经打开时写入(登出后积压的条目直接丢弃),且每轮迭代都会清空缓存凭据;
- 登出不再在研究任务仍在运行时关闭用户数据库——以前中途关闭会导致该任务日志被 drain 线程静默丢弃。现在连接会保持到本次运行结束、再额外保留一轮空闲连接清扫(数分钟),
is_user_connected也在同一时点停止把该账号报告为可达; - 上述做法依赖"请求/Socket 认证路径上的服务端会话 ID 吊销"(上游 #5532),因此该变更必须与之配套发布;
- 修改密码不再暂停其他用户的进行中研究:数据库重加密现在按用户门控而非进程级门控,只有你自己的新研究在重加密期间被短暂挂起;队列处理器在重新打开你的加密数据库之前还会立即复查你的会话,使得恰在该瞬间发生的登出无法再把已解密的数据库带回或恢复排队的研究。
三、SSRF 防护体系化加固
1.10.3 是 SSRF 防护集中收口的一个版本,覆盖了静态抓取、JS 渲染抓取、用户可编辑的引擎 URL、出站 HTTP 客户端与通知 Webhook 五条路径。
3.1 静态出站请求:IP 固定(pinning)
通过safe_get/safe_post/SafeSession发出的出站 HTTP 请求(RAG 来源抓取与网页抓取)此前存在 TOCTOU 窗口:SSRF 守卫校验的是一个解析出的 IP,但 HTTP 客户端在真正建连时会独立重新解析域名。现在,连接固定到守卫校验过的那个精确 IP,并在每一次重定向前重新检查;原始主机名仅保留用于 TLS SNI 与证书校验。相关实现与测试见 src/local_deep_research/security/egress/ 与 test_ssrf_dns_pinning.py、test_safe_requests_redirects.py。
3.2 无头浏览器路径:与静态路径同一套 SSRF 规则
使用 JS 渲染的抓取路径(Crawl4AI 与原生 Playwright 两个引擎)此前是防护盲区。现在无头浏览器下载器发出的每一个子请求——初始导航、每一跳重定向、每个子资源——都会对照与静态抓取路径相同的 SSRF 规则做校验或拦截:公开页面把浏览器重定向到云元数据端点或私网/内网主机的行为,会在浏览器建连之前被拦截。同路径上的多个旁路一并关闭:
- robots.txt 改为经受 SSRF 保护的会话抓取,而不是未经检查的客户端;
- 阻止 Service Worker;拒绝 WebSocket 连接;
- 云元数据 IP 在任何情况下保持拦截;
PRIVATE_ONLY出站范围仍可达本地实验主机; - 重定向链中设置的 Cookie 现在按跳(per hop)从浏览器 Cookie 罐重新限定作用域:属于某一域的 Cookie 不会再被转发到后续重定向落到不同域的情况(与正常浏览器行为一致);同域 Cookie 持久化行为不变,服务端在链中途清掉的 Cookie(
Max-Age=0/ 过期Expires)也不会被"复活"。
3.3 用户可编辑的搜索引擎实例 URL 防 SSRF
公开搜索引擎实例 URL(例如 SearXNG 的instance_url)指向私有/回环/链路本地地址时,现在在保存时即被拒绝,并且默认情况下运行时也不再抓取此类实例。是否允许引擎触达私有实例是纯运维决定:刻意不由用户可编辑的policy.egress_scope设置授予。
- 在 localhost/LAN 自托管 SearXNG 的运维可以设置
LDR_SEARCH_ALLOW_PRIVATE_ENGINE_URLS=true恢复放行;Docker 部署若通过环境变量锁定 URL 则不受影响;云元数据端点无论如何保持拦截。 - 更细粒度的替代方案是
LDR_SEARCH_PRIVATE_ENGINE_URL_ALLOWLIST(见 security.py):逗号分隔的精确 URL 源(scheme+host+port,默认端口隐含、主机不区分大小写、忽略路径/查询、不支持通配符与 CIDR),只有当引擎配置的 URL 命中列表时该引擎的请求链(含重定向)才以私有访问权限运行。
升级注意(原文档重点强调):SearXNG 的默认实例http://localhost:8080是回环地址,本变更之后未显式放行即被拒绝。如果你自托管的 SearXNG 升级后停止返回结果,请设置LDR_SEARCH_ALLOW_PRIVATE_ENGINE_URLS=true(或改用环境变量锁定 URL);引擎因该原因自我禁用时会输出一条点名该开关的警告日志。对应测试:test_searxng_private_url_warning.py、test_guarded_engine_url_contract.py、test_egress_settings_save_validators.py。
3.4 通知 Webhook(Apprise):发送时 DNS 重绑定防护
通知服务 URL 此前只在配置时校验一次,而 Apprise 在实际发送时会重新解析域名并跟随重定向——一个重绑定主机可以"给校验器一个公网地址、给发送方一个私有/云元数据地址"。1.10.3 的处置是:
- 发送改为同步、线程内执行,并禁用 HTTP 重定向跟随(直接封死"重定向到内网/外泄",即使用户自带
?redirect=yes也无效); - 每次发送期间,把所有 raw-webhook 主机固定到刚校验过的地址上,任何未固定且解析到私有/回环/链路本地/云元数据 IP 的查找都在 Socket 层、建连之前被拒绝;
- 发送时 DNS 解析带超时边界(缓慢或恶意的解析器无法挂起发送线程);若固定 shim 不是活动解析器、或投递会扇出到线程本地守卫覆盖不到的工作线程,则发送 fail closed;
- 各 scheme 策略与校验器一致(http/https 未放行则拦私有;插件/自托管 scheme 允许 LAN 目标但始终拦截云元数据);原始主机名保留用于 TLS SNI 与证书校验;固定/拦截为线程本地,不影响并发请求;
LDR_NOTIFICATIONS_ALLOW_OUTBOUND总开关(见 security.py,默认关闭)作为纵深防御保留。
后续两轮评审又关闭了具体缺口(发布说明原文逐项列出,值得运维重点阅读):
- AWS 原生 IPv6 实例元数据端点
fd00:ec2::254(一个 ULA,不在 IPv4 元数据列表与 NAT64 内嵌 IPv4 检查的覆盖范围内)现在无条件拦截,包括在私有 IP 放行开启时; - 通知服务 URL 的 authority 中出现多于一个
@的,在提取主机之前即被拒绝(校验器/固定解析器与 Apprise 自带的解析器对这类 authority 会取出不同主机); - 已确认的发送时 SSRF 拦截被识别为"单次尝试立即失败",不再进入重试;有边界的发送时 DNS 查找运行在 daemon 线程上,挂死的恶意解析器不会阻塞进程优雅退出;
- 通知插件/raw-webhook 分区在私有 IP 放行开启时也拦截整个链路本地段(IPv4
169.254.0.0/16、IPv6fe80::/10)——云厂商元数据服务经由链路本地提供(例如 Scaleway 的169.254.42.42),而自托管的 RFC1918/回环/非链路本地 ULA 通知端点不受影响; - 空
//authority 但路径中含主机(例如json:///169.254.169.254/path,校验器解析器看不到主机、而 Apprise 却会拨打路径段)的 URL,在主机提取之前即被拒绝。
四、多租户与本地资源边界加固
4.1 嵌入模型设置限定在模型目录内
Sentence Transformers 嵌入模型设置(local_search_embedding_model)此前允许任何已认证用户填入任意绝对路径,可被用作文件系统存在性/结构探测,并加载任意本地模型。现在:只有能解析到应用模型目录之下的值才被视为本地模型;其余一律按 HuggingFace 模型 ID 处理;试图逃逸模型目录的路径直接拒绝。
4.2 多租户纵深防御的三处收口
- 研究库级联删除辅助函数拒绝符号链接/越出根目录的删除;
- 按用户哈希的咽喉点对空用户名fail closed;
- 按用户的研究并发上限与服务器总上限联动校正——覆盖聊天发起的研究启动(
send_message与retry_attempt),而不只是主研究路由与队列处理器。发布说明特别注明:该上限修正的是"对全局上限的记账",不是按用户公平性——单个用户仍可合法占满全局上限,反垄断调度是后续工作。
4.3 程序化注册对象按用户作用域
通过编程方式注册的搜索 retriever 与自定义 LLM 现在归属注册者本人:内置 provider 与引擎仍全局共享,但一个用户的注册不能再遮蔽另一个用户的 provider/引擎解析、也不会出现在另一个用户的引擎列表中。
同时,运行所属用户被穿入所有过去只依赖设置快照的 LLM 解析缝(seam)。当用户的主要搜索来源是自己的私有(本地专用)retriever 时,"本地专用推理策略"现在会强制作用于:研究运行的模型/provider 覆盖路径、领域分类、以及新闻调度器在运行后的标题/话题生成——关闭了这些调用回落到云端 LLM 处理私有语料运行的通道。
安全网:当 ADAPTIVE 出站策略无法在任何可见命名空间中归类某次运行的主要引擎时,仍解析到宽松(可云端)范围,但现在会输出一条点名主要引擎、并报告是否穿入了用户名的策略审计警告——未来若再漏穿用户名,会在日志中显形而不是静默放行。
SDK/编程调用注意(行为变更):register/register_multiple注册None的 retriever 或 LLM 现在抛出ValueError(不再静默存储);register_multiple是原子的——批次中只要含任一None,整批都不注册。
4.4 研究库 PDF 存储:明文模式收口为运维开关 + 按用户目录隔离
这是发布说明中最长的一段,涉及三个相互关联的变更:
(a) 文件系统明文存储改为纯环境门控。未加密的filesystemPDF 存储模式现在只能通过运维环境变量LDR_RESEARCH_LIBRARY_ALLOW_FILESYSTEM_PDF_STORAGE开启且默认关闭(定义见 security.py)。此前任何用户都可选择该模式,把抓取的第三方 PDF 以明文写入共享库目录。门关闭时:设置 UI 隐藏该选项;任何已存储或环境值中的filesystem在写 PDF 前被强制改写为加密的database模式;一个迁移把既有的filesystem选择重写为database;此前写出的明文文件保持可读。默认的database(加密)与none(仅文本)模式不变。
(b) PDF 改为按用户子目录存储(#5521)。研究库下载的 PDF 现在落在<library>/<username>/下,而不是单一共享目录。此前两个用户的资源 ID(按用户自增)可以在共享目录中撞名,导致一个用户的 PDF 覆盖或暴露另一个用户的文件。兼容与迁移策略:
- 读取可以回退到旧共享位置,因此升级前下载的 PDF 仍能加载(不移动任何文件)——但该回退现在由运维门控、默认关闭(见 (c));
- 共享库模式(
research_library.shared_library)现在要求运维显式开启LDR_RESEARCH_LIBRARY_ALLOW_SHARED_LIBRARY:因为shared_library与storage_path都是用户可编辑的,若继续尊重用户侧的shared_library=true,租户可以拆掉按用户边界、把存储指到另一个用户的目录。默认忽略该用户设置。
升级注意(原文档明确给出):既有的共享库部署必须设置LDR_RESEARCH_LIBRARY_ALLOW_SHARED_LIBRARY=true才能继续共享单一目录;否则新下载进入按用户子目录,而此前下载的共享 PDF 将停止解析——除非运维同时开启旧版读取回退LDR_RESEARCH_LIBRARY_ALLOW_LEGACY_READ_FALLBACK=true。
(c) 旧共享根回退的只读化与门控。旧共享根的回退现在只读:文档删除只在删除者自己的按用户目录内解析,绝不沿回退进入共享根——否则一次普通的自助删除(如相对路径pdfs/5.pdf)可能解析到另一租户的文件并将其 unlink。共享根中的文件保持不被删除。旧共享根的读回退则由LDR_RESEARCH_LIBRARY_ALLOW_LEGACY_READ_FALLBACK门控、默认关闭,封住了一个跨租户读取通道:回退基址源自用户可编辑的research_library.storage_path,攻击者可以把自己的storage_path指到他人目录,利用"按用户自增资源 ID 天然撞号"让自己文档 ID 的读取解析到别人的 PDF。门关闭时,库 PDF 读取严格限制在请求者自己的按用户根内;需要旧文件继续加载的运维可在单用户或互信部署中放行。相关实现与测试见 pdf_storage_manager.py(对空用户名 fail closed 的注释在删除路径上)、test_pdf_storage_gate.py 与 test_per_user_library_root.py。
五、Bug 修复
5.1 OpenAlex 期刊质量数据下载内存问题(#4383)
OpenAlex 快照分区的解码改为逐条记录进行,期刊质量下载不再把整个分区驻留内存——修复小主机上内存耗尽、容器陷入 swap 颠簸的问题。
5.2 Tavily 时间范围真正生效(#4936)
Tavily 引擎现在尊重配置的时间范围:在 Settings → Search 中选择 Past 24 hours / week / month / year 时,该值作为time_range转发给 Tavily API,选择确实收窄结果而不是被静默忽略。默认 "All time"(不过滤)。与第一章一致,该设置现在覆盖所有搜索路径,包括此前绕过它的默认langgraph-agent策略与 MCP server。
5.3 SearXNG 限流改为按实例 URL 进程级共享(#5351)
SearXNG 引擎的请求间隔(delay_between_requests)现在按实例 URL 在进程内共享:研究 agent 工具调用过程中构造的每个按调用实例都能命中同一节流状态。源码中由 search_engine_searxng.py 在初始化时调用respect_rate_limit(self.instance_url, self.delay_between_requests)登记共享限流器。
5.4 列表型 LLM 内容块(.content为块列表)的系统性兼容
对返回.content为块列表的 provider(Anthropic 扩展思考或工具调用场景),1.10.3 修复了多处长时间未处理的AttributeError/TypeError,并统一剥离开始思考推理标签(与同文件内兄弟调用的既有处理对齐):
- 问题生成家族与证据评估器:后续问题生成、子问题分解、原子事实分解、补差问题、BrowseComp 实体抽取与渐进式搜索、证据提取——由于
StandardQuestionGenerator是默认生成器,此前这些故障会在主研究循环的每一轮迭代上触发; - 新闻分析、GitHub 查询优化与话题组织:此前话题文本合成抛
TypeError、GitHub 搜索查询变成块列表的 repr、新闻摘要返回空; - 高级搜索约束解析、fallback 期刊声誉分数解析两处同类修复。
5.5 其他修复
- 连接态测试后泄露的 active-research 状态可被检测(#5466);
- 显式 OpenAI endpoint URL 的 provider 特定校验边界加锁(#5488);
- Wiki 链接自动补全在查询变化时立即使过期请求与结果失效;
- 以不同 chunk 数重新索引文档时,聚合 RAG chunk 计数保持准确;
- 防止旧 Unified Search 响应在查询变化后渲染过期的可点击结果;
- 防止过期的文档/研究笔记面板请求覆盖更新的刷新结果;
- LangGraph 子主题溢出改为有界排队而非丢弃,并在开始前拒绝超大批次;进度元数据以
overflow_strategy="queued"与overflow_queued_count报告排队溢出,省略truncated_from(因为没有话题被截断); - 非字符串的 collection 名称/描述返回 400,而非 500;
- Unified Search 语义结果在舍入响应值之前用完整相似度精度排序。
六、运维升级清单
把分散在发布说明中的运维动作汇总如下,全部环境变量均为环境级(env-only)设置,无法通过用户可写的设置 API 修改(定义集中在 security.py):
| 场景 | 必做动作 | 默认行为 |
|---|---|---|
| 自托管 SearXNG 在 localhost/LAN | 设置LDR_SEARCH_ALLOW_PRIVATE_ENGINE_URLS=true,或改用LDR_SEARCH_PRIVATE_ENGINE_URL_ALLOWLIST精确放行,或 Docker env 锁定instance_url | 回环/LAN 的instance_url保存即被拒、运行时不抓取 |
| 需要时间过滤的默认搜索 | 升级后在 Settings 重新选择Past year | 默认 "All time",无时间过滤 |
| 既有共享库部署 | 设置LDR_RESEARCH_LIBRARY_ALLOW_SHARED_LIBRARY=true;如需旧文件继续可读,同时LDR_RESEARCH_LIBRARY_ALLOW_LEGACY_READ_FALLBACK=true | 新下载进入按用户子目录,旧共享 PDF 停止解析 |
| 需要明文 PDF 存储(谨慎) | LDR_RESEARCH_LIBRARY_ALLOW_FILESYSTEM_PDF_STORAGE=true | 强制加密database模式,UI 隐藏 filesystem 选项 |
| 出站通知 Webhook | 维持LDR_NOTIFICATIONS_ALLOW_OUTBOUND默认关闭,除非接受已披露的残余风险 | 关闭 |
| 单机/可信部署需读旧共享 PDF | LDR_RESEARCH_LIBRARY_ALLOW_LEGACY_READ_FALLBACK=true | 读取严格限制在本人按用户目录内 |
此外有两类面向开发者的接口行为变化需要纳入代码评审:register/register_multiple对None的ValueError与原子批次语义;以及程序化注册对象按用户作用域后,跨用户"借用"注册 provider/引擎的写法不再可行。
七、结语
1.10.3 的三条主线——全局时间范围默认值的纠正、SSRF 与会话生命周期的体系化加固、研究库 PDF 的按用户隔离——都指向同一件事:把"用户可写设置 × 网络出口 × 文件系统边界"三个交叉面上的默认行为统一收敛到保守一侧,并把放宽权限的决策权明确留给运维环境变量。对多用户部署而言,这一版本升级后应逐项核对上表;对单用户自托管者,最主要的可见变化是默认搜索不再限定一年,以及自托管 SearXNG 需要显式放行回环地址。
相关文档可继续参考:搜索引擎配置、SearXNG 部署、研究库与 RAG、通知机制 与仓库根目录的 SECURITY.md。
【免费下载链接】local-deep-research~95% on SimpleQA (e.g. Qwen3.6-27B on a 3090). Supports all local and cloud LLMs (llama.cpp, Ollama, Google, ...). 10+ search engines - arXiv, PubMed, your private documents. Everything Local & Encrypted.项目地址: https://gitcode.com/GitHub_Trending/lo/local-deep-research
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考