Fix web search date handling

This commit is contained in:
caoqianming 2026-07-27 12:50:07 +08:00
parent c3a6f7deee
commit ade3a847f6
6 changed files with 53 additions and 6 deletions

View File

@ -5,6 +5,10 @@
> 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。 > 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。
> 工程口径的完整记录见 `PROGRESS.md` / git log。 > 工程口径的完整记录见 `PROGRESS.md` / git log。
## 0.60.1 — 2026-07-27
- 联网搜索现在会以当前北京时间判断时效:未指定时间时不再擅自给搜索词添加旧年份,查询“最新、当前、近期”内容时会采用合适的近期结果范围;用户明确指定的年份或时间窗保持优先。
## 0.60.0 — 2026-07-27 ## 0.60.0 — 2026-07-27
- 登录后的空白页现在可以直接选择已有工作目录或新建工作目录,然后开始对话;点击左栏「+ 新对话」也进入同一个快速入口,发送第一条消息时才真正创建对话。 - 登录后的空白页现在可以直接选择已有工作目录或新建工作目录,然后开始对话;点击左栏「+ 新对话」也进入同一个快速入口,发送第一条消息时才真正创建对话。

View File

@ -2,7 +2,7 @@
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9` > 配合 `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 ### 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.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.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 个离线单测覆盖单图、多图、文生兼容、越界与超量拒绝。 - **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 个离线单测覆盖单图、多图、文生兼容、越界与超量拒绝。

View File

@ -1,3 +1,3 @@
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。 # zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
# 改版本只动这一行。 # 改版本只动这一行。
__version__ = "0.60.0" __version__ = "0.60.1"

View File

@ -23,6 +23,7 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Optional, Tuple from typing import Any, Callable, Optional, Tuple
from uuid import UUID, uuid4 from uuid import UUID, uuid4
from zoneinfo import ZoneInfo
import yaml import yaml
from rich.console import Console from rich.console import Console
@ -320,7 +321,21 @@ def _build_system_prompt(
不强求同步( short_id 兜底定位) 不强求同步( short_id 兜底定位)
today 当场算, prompt LLM 直接拼路径(避免 LLM 不知道当前日期) 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 工具全在容器里跑,容器把 # docker backend 下 shell/run_python/fs 工具全在容器里跑,容器把
# `<workspace>/users/<uid>` bind 到 `/workspace`、`--workdir /workspace/<wd>` # `<workspace>/users/<uid>` bind 到 `/workspace`、`--workdir /workspace/<wd>`
# (executor_docker.py:99-100)。此时 prompt 给 agent 的所有可写/可读绝对路径 # (executor_docker.py:99-100)。此时 prompt 给 agent 的所有可写/可读绝对路径
@ -362,7 +377,6 @@ def _build_system_prompt(
render_cmd = "python /sandbox/rendering/render.py" render_cmd = "python /sandbox/rendering/render.py"
else: else:
render_cmd = f'"{sys.executable}" "{ROOT / "rendering" / "render.py"}"' render_cmd = f'"{sys.executable}" "{ROOT / "rendering" / "render.py"}"'
today = datetime.now().strftime("%Y-%m-%d")
tname = task_name or "<未指定>" tname = task_name or "<未指定>"
short_id = task_id.hex[:8] short_id = task_id.hex[:8]
skill_line = ( skill_line = (

View File

@ -42,6 +42,13 @@ def _build(backend: str, *, allow_rename: bool = True) -> tuple[str, Path, Path]
class TestSystemPromptPaths(unittest.TestCase): 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): def test_docker_prompt_uses_container_path_no_host_leak(self):
prompt, ws, wd = _build("docker") prompt, ws, wd = _build("docker")
# 容器路径在 # 容器路径在
@ -66,6 +73,15 @@ class TestSystemPromptPaths(unittest.TestCase):
self.assertIn("必须调用 `rename_working_dir`", interactive) self.assertIn("必须调用 `rename_working_dir`", interactive)
self.assertNotIn("必须调用 `rename_working_dir`", scheduled) 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__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -15,7 +15,15 @@ class WebSearchTool(Tool):
parameters = { parameters = {
"type": "object", "type": "object",
"properties": { "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": { "count": {
"type": "integer", "type": "integer",
"default": 10, "default": 10,
@ -25,7 +33,11 @@ class WebSearchTool(Tool):
"type": "string", "type": "string",
"enum": ["noLimit", "oneDay", "oneWeek", "oneMonth", "oneYear"], "enum": ["noLimit", "oneDay", "oneWeek", "oneMonth", "oneYear"],
"default": "noLimit", "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"], "required": ["query"],