# /search —— 深度调研搜索

一句话：**把「一句话」变成「一篇有事实、有出处、分得清时效的概述」**。

它不是一个搜索框的封装——搜索、抓正文、成稿是三段分开做的，模型负责“规划”和“成稿”，中间的执行全在插件这层完成。

## 为什么这样设计

模型最容易犯的两个错：搜到什么就报什么（不核对）、把旧闻当现状（不分时效）。
`/search` 用三个手段压住它们：

1. **规划与执行分离**：模型只决定“搜什么”，URL 和正文由插件自己去拿，模型编不出来
2. **时效先判**：先定时间窗口（`1d/3d/7d/30d/any`），再用它过滤资料，概述和参考来源共用同一批
3. **成稿有硬约束**：禁止转述语气、禁止点评资料本身、禁止编造、禁止 markdown

## 数据通路

全部复用宿主服务，插件自己不直接联网：

| 环节 | 用的服务 | 说明 |
|---|---|---|
| 搜索 | `ctx.web.search({query, maxResults}, signal)` | 与 `web_search` 工具同一后端 |
| 抓正文 | `ctx.web.fetch({url}, signal)` | 取回页面正文，抽要点 |
| 规划 / 成稿 | `ctx.llm.stream({provider, model, messages, maxTokens, reasoningEffort:"off"})` | 与 `/compact` 同一个 llm 服务，用**会话当前模型** |

搜索服务不可用时明确报错，不会假装成功。

## 用法

```
/search <关键词>           标准调研：3 轮查询 + 抓正文 + 事实概述
/search <关键词> deep      超深度：判时效 + 中英混合规划 + 关键词深挖一层
/search <关键词> brief     只给找到的资料，不做概述
/search <关键词> en        英文关键词（外媒优先）
```

开关写在关键词后面，位置随意、可叠加：

| 开关 | 别名 | 作用 |
|---|---|---|
| `deep` | `d` / `-d` | 超深度：模型先判时效窗口、规划 4~6 条中英查询，再从第一轮结果里抽 2~3 个实体关键词补搜一层（**最多 2 层，无递归**） |
| `brief` | `n` / `nosum` / `-n` | 关闭概述，只列找到的资料 |
| `en` | `e` / `-e` | 英文关键词 |

例子：

```
/search A股 大跌                    标准调研
/search A股 大跌 deep               超深度
/search A股 大跌 deep brief         超深度但不要概述
/search REA e                       英文查项目
```

## 输出长什么样

有概述时：

```
🔎 超深度调研：A股 大跌
（5 轮查询 · 时效 1d · 窗口内 4 条 · 深挖 2 个关键词 · 14 条来源 · 9.2s）

📌 概述
<400~700 字的事实档案，按内容分段，必要时带小标题>

──────────────
📎 参考来源（共 14 条，列出 8 条）

1. 标题
   📅 2026-10-29
   🔗 https://…
```

`brief`（或无概述）时：

```
🔎 标准调研：A股 大跌
（3 轮查询 · 9 条来源 · 2.1s）

📎 找到的资料（共 9 条，列出 6 条）

1. 标题
   2026-10-29 · 一句要点
   🔗 https://…
```

头部那行元信息很有用：**`时效 1d · 窗口内 4 条`** 说明它判断的时效窗口和窗口内资料量；出现 `放宽到 3d` 说明窗口内不够，逐级放宽过。

## 超深度（deep）的完整链路

```
① 规划（模型）
   判时效窗口 1d/3d/7d/30d/any + 生成 4~6 条中英混合查询 → JSON
        ↓
② 第一轮搜索（插件）
   并行 ctx.web.search，合并去重
        ↓
③ 关键词深挖（模型 → 插件）
   模型从第一轮标题里挑 2~3 个实体（产品/机构/技术/事件）
   插件用「原词 + 关键词」再搜一层 —— 层数写死 2 层，不递归
        ↓
④ 时效过滤（纯本地）
   按 1d→3d→7d→30d 逐级放宽，找到第一档 ≥3 条的窗口
        ↓
⑤ 抓正文（插件）
   挑 3~4 篇 ctx.web.fetch，抽 1~2 句要点
        ↓
⑥ 成稿（模型）
   把资料消化成事实档案，遵守下面的硬约束
```

## 时效窗口规则

模型在规划阶段先判断这个问题有多“热”：

| 窗口 | 什么时候用 |
|---|---|
| `1d` | 问今天/现在/最新行情、突发新闻 |
| `3d` | 最近几天、本周 |
| `7d` | 最近一周左右 |
| `30d` | 一般调研 |
| `any` | 历史、原理、资料类，不限时间 |

判断依据写死在 prompt 里：查询含「今天/现在/最新/当前/实时/行情/股价/刚刚」或问正在发生的事 → 短窗口。

**过滤与放宽**（`relaxWindow`，纯本地，不重搜）：

- 严格档只认**有发布日期**的来源；无日期 = 无法确认时效，一律排除
- 起始窗口内不足 3 条，就按 `1d→3d→7d→30d` **逐级放宽**，找到第一档够 3 条的
- 阶梯走完仍不够，才用 30 天档兜底（会带上无日期条目）
- 过滤后的同一批资料同时给「概述」和「参考来源」——**不会出现概述用新的、链接列旧的**

## 概述的硬约束（prompt 里写死的）

- 禁止出现「资料 / 文章 / 报道 / 来源 / 原文 / 据…称 / 显示」——只讲事实本身
- 禁止「未提供 / 未见 / 无从查证」——没信息就不写，不交代缺失
- 禁止从名字推断内容（「从名称看」「顾名思义」）
- 禁止 markdown 记号（微信是纯文本）；分段、超 500 字加小标题、单句 ≤40 字、语言平实
- 多份材料讲同一件事 → 合并成一句事实陈述，数字取最具体的
- 时效：现状只用窗口内信息；更早的只能写「此前 / 当时」；时间自然融入，不逐句报日期

模型输出后还会过一层 `plainText()`，把漏网的 `**加粗**`、`# 标题` 剥掉，才发到微信。

## 配置

`/search` 本身没有专属环境变量，全部跟随宿主：

| 变量 | 说明 |
|---|---|
| `DSH_HOME` | 工作区主目录（日志写 `.playground/whale-plugin.log`） |

模型用**会话当前模型**（`agent.options.provider/model`）。想换模型跑调研：先 `/model <模型>` 再 `/search`。

## 排障

| 现象 | 原因 / 处理 |
|---|---|
| 只回「搜索服务还没就绪」 | 宿主 `ctx.web` 没起来（web_search 工具也不可用），稍后重试 |
| 只有来源没有概述 | 模型调用失败或空输出，已降级成“资料要点速览”；看日志 `summary:` |
| 概述里有 `**` 或 `#` | 不应该发生（会剥掉）；若出现，把原文发我 |
| 时效标了 `1d` 但来源是前几天的 | 说明窗口内不足 3 条，已逐级放宽，头部会写「放宽到 Xd」 |
| 概述把旧闻当现状 | 调 `deep` 让它先判时效；仍然串就把输出发我，我收紧 prompt |
| 搜索太慢 | deep 是 2 层搜索 + 3 次模型调用，10~30 秒属正常；微信通道超时上限 5 分钟 |

日志（`$DSH_HOME/.playground/whale-plugin.log`）里能看到：

```
search: 14 条来源，11 条带日期      ← 日期覆盖率
summary: ok len=812 lines=9 head=…  ← lines=1 说明模型没分段
llmCall: …                          ← 模型调用异常
```

## 边界

- 只把**查询词**发给宿主的 web 搜索服务，不读本地文件、不碰私有目录
- 抓正文只抓搜索结果里的公开 URL
- 不登录、不带 cookie、不访问需要鉴权的页面
- 概述基于检索到的公开资料，不构成任何专业建议
