From ade3a847f6ca339780d909b3a2b0a31539cee902 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Mon, 27 Jul 2026 12:50:07 +0800 Subject: [PATCH] Fix web search date handling --- CHANGELOG.md | 4 ++++ PROGRESS.md | 3 ++- core/__init__.py | 2 +- core/agent_builder.py | 18 ++++++++++++++++-- tests/test_system_prompt_paths.py | 16 ++++++++++++++++ tools/web_search.py | 16 ++++++++++++++-- 6 files changed, 53 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b3520d..b0c8e85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ > 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。 > 工程口径的完整记录见 `PROGRESS.md` / git log。 +## 0.60.1 — 2026-07-27 + +- 联网搜索现在会以当前北京时间判断时效:未指定时间时不再擅自给搜索词添加旧年份,查询“最新、当前、近期”内容时会采用合适的近期结果范围;用户明确指定的年份或时间窗保持优先。 + ## 0.60.0 — 2026-07-27 - 登录后的空白页现在可以直接选择已有工作目录或新建工作目录,然后开始对话;点击左栏「+ 新对话」也进入同一个快速入口,发送第一条消息时才真正创建对话。 diff --git a/PROGRESS.md b/PROGRESS.md index 7172f1f..2a1616a 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -2,7 +2,7 @@ > 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。 -最后更新:2026-07-27(目录优先的新对话草稿页 + 首条消息自动命名,bump 0.60.0) +最后更新:2026-07-27(联网搜索动态时效约束,bump 0.60.1) --- @@ -23,6 +23,7 @@ ### 2026-07 +- **07-27 / 0.60.1 / 联网搜索动态时效约束**:真实 task `24d6b609` 在 2026-07-27、用户未指定年份时,`deepseek_v4.flash` 两次自行给 `web_search` query 加 `2025`;工具原样转发且默认 `freshness=noLimit`,根因是 system prompt 虽注入 today 却仅标作宪法文件命名用途,工具 schema 也无年份策略。修为 system prompt 首段按 `Asia/Shanghai` 动态注入当前时间/日期/年份,并给唯一规则:未指定时间则 query 无年份,“最新/当前/近期”按当前日期配合 freshness,明确时间窗则服从用户;`web_search` 的 query/freshness schema 再局部强化。5 项定向测试通过。 - **07-27 / 0.60.0 / 目录优先的新对话草稿页 + 首条消息自动命名**:登录未选任务与左栏「+ 新对话」统一进入不落 DB 的草稿页,选择/新建 working_dir 后首发才创建 task,避免空任务;原完整表单保留为「自定义」。新增 0023 `tasks.auto_title_pending` 一次性闸与 `auto_title` 可选创建字段,首条消息并行生成短标题(`usage_events.kind=task_title`),只改 task 显示名不动目录,人工 PATCH name 清闸且条件 UPDATE 防在途覆盖;失败保留「新对话」不阻塞主 run。 - **07-27 / 0.59.6 / 对话内安全重命名 working_dir**:新增 `rename_working_dir` 受控工具,agent run 内只登记目标 leaf 名,正常回复结束并把当前 task 退出 running 后再落地,避免 executor/system prompt/宿主工具仍握旧 cwd;取消或失败不执行。文件面板与对话路径共用 `core/working_dirs.py` DB-aware 原语:锁定并同步更新共享目录的全部 task、活跃邻居与 no-subtask 冲突安全拒绝、DB UPDATE 后再做 FS rename。system prompt 明确当前 task_dir 禁走 shell/run_python 直接改名,定时 run 不挂该工具;无新增 schema/migration。全量 357 测试通过(17 项按环境跳过),DB 路由测试未设 `ZCBOT_TEST_DB_URL` 时按安全门控跳过。 - **07-27 / 0.59.5 / Seedance 图生视频(单图首帧 + 最多 9 张多图参考)**:`seedance` 在原文生视频契约上向后兼容增加 `image`(唯一 `first_frame`)与 `reference_images`(0-9 张 `reference_image`),两者合计最多 9 张;复用 image_ref 的 task/user_root 三形态路径解析、越界防护与 base64 内联,新增单图 10MB / 合计 30MB 请求体闸。请求按有无图片切图生/文生单价,banner/meta/usage snapshot 记录 mode、首帧及参考图;videogen skill、工具系统提示、RUN 与能力清单同步。5 个离线单测覆盖单图、多图、文生兼容、越界与超量拒绝。 diff --git a/core/__init__.py b/core/__init__.py index 47e2d8b..8278627 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,3 +1,3 @@ # zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。 # 改版本只动这一行。 -__version__ = "0.60.0" +__version__ = "0.60.1" diff --git a/core/agent_builder.py b/core/agent_builder.py index 8da5e0e..1c3a9be 100644 --- a/core/agent_builder.py +++ b/core/agent_builder.py @@ -23,6 +23,7 @@ from datetime import datetime from pathlib import Path from typing import Any, Callable, Optional, Tuple from uuid import UUID, uuid4 +from zoneinfo import ZoneInfo import yaml from rich.console import Console @@ -320,7 +321,21 @@ def _build_system_prompt( 不强求同步(由 short_id 兜底定位)。 today 当场算,落 prompt 给 LLM 直接拼路径(避免 LLM 不知道当前日期)。 """ - prompt = (ROOT / cfg["system_prompt"]).read_text(encoding="utf-8") + now = datetime.now(ZoneInfo("Asia/Shanghai")) + today = now.strftime("%Y-%m-%d") + current_year = now.year + time_block = ( + "## 当前时间与时效\n" + f"- **当前时间**: `{now.strftime('%Y-%m-%d %H:%M:%S')} Asia/Shanghai`\n" + f"- **当前日期**: `{today}`;**当前年份**: `{current_year}`\n" + "- 当前时间事实以本段为唯一依据,不使用模型训练时间推断年份。\n" + "- 用户未指定时间范围时,搜索词保持无年份;用户要求“最新 / 当前 / 近期”时," + "以本段日期为准,并按需要使用当前年份及搜索工具的 freshness;" + "用户明确指定年份或时间窗时,以用户要求为准。\n" + ) + prompt = time_block + "\n" + ( + ROOT / cfg["system_prompt"] + ).read_text(encoding="utf-8") # docker backend 下 shell/run_python/fs 工具全在容器里跑,容器把 # `/users/` bind 到 `/workspace`、`--workdir /workspace/` # (executor_docker.py:99-100)。此时 prompt 给 agent 的所有可写/可读绝对路径 @@ -362,7 +377,6 @@ def _build_system_prompt( render_cmd = "python /sandbox/rendering/render.py" else: render_cmd = f'"{sys.executable}" "{ROOT / "rendering" / "render.py"}"' - today = datetime.now().strftime("%Y-%m-%d") tname = task_name or "<未指定>" short_id = task_id.hex[:8] skill_line = ( diff --git a/tests/test_system_prompt_paths.py b/tests/test_system_prompt_paths.py index 3c5a17a..84eaeee 100644 --- a/tests/test_system_prompt_paths.py +++ b/tests/test_system_prompt_paths.py @@ -42,6 +42,13 @@ def _build(backend: str, *, allow_rename: bool = True) -> tuple[str, Path, Path] class TestSystemPromptPaths(unittest.TestCase): + def test_current_time_rules_are_at_prompt_start(self): + prompt, _, _ = _build("docker") + self.assertTrue(prompt.startswith("## 当前时间与时效\n")) + self.assertIn("Asia/Shanghai", prompt) + self.assertIn("用户未指定时间范围时,搜索词保持无年份", prompt) + self.assertIn("搜索工具的 freshness", prompt) + def test_docker_prompt_uses_container_path_no_host_leak(self): prompt, ws, wd = _build("docker") # 容器路径在 @@ -66,6 +73,15 @@ class TestSystemPromptPaths(unittest.TestCase): self.assertIn("必须调用 `rename_working_dir`", interactive) self.assertNotIn("必须调用 `rename_working_dir`", scheduled) + def test_web_search_schema_reinforces_time_rules(self): + from tools.web_search import WebSearchTool + + query_desc = WebSearchTool.parameters["properties"]["query"]["description"] + freshness_desc = WebSearchTool.parameters["properties"]["freshness"]["description"] + self.assertIn("omit years", query_desc) + self.assertIn("current date/year", query_desc) + self.assertIn("narrowest window", freshness_desc) + if __name__ == "__main__": unittest.main() diff --git a/tools/web_search.py b/tools/web_search.py index 2944b8e..6860227 100644 --- a/tools/web_search.py +++ b/tools/web_search.py @@ -15,7 +15,15 @@ class WebSearchTool(Tool): parameters = { "type": "object", "properties": { - "query": {"type": "string", "description": "Search query string"}, + "query": { + "type": "string", + "description": ( + "Search query string. Follow the current date in the system prompt: " + "omit years when the user gives no time range; for latest/current/recent " + "requests, use the current date/year only when semantically useful; " + "honor any user-specified year or time window." + ), + }, "count": { "type": "integer", "default": 10, @@ -25,7 +33,11 @@ class WebSearchTool(Tool): "type": "string", "enum": ["noLimit", "oneDay", "oneWeek", "oneMonth", "oneYear"], "default": "noLimit", - "description": "Filter results by recency", + "description": ( + "Result recency window: noLimit, past day, week, month, or year. " + "For latest/current/recent requests, choose the narrowest window that " + "still fits the user's intent; otherwise keep noLimit." + ), }, }, "required": ["query"],