# Kaiyan Kanshijie · dsh-see-world

**"Open Eyes on the World" — make your agent search the web first when it should, and answer with sources. Local tasks stay zero-disturbance.**

**English** ｜ [简体中文](README.md)

<p align="center">
  <a href="https://www.npmjs.com/package/dsh-see-world"><img alt="npm version" src="https://img.shields.io/npm/v/dsh-see-world?style=flat-square&label=npm"/></a>
  <a href="https://www.npmjs.com/package/dsh-see-world"><img alt="npm downloads" src="https://img.shields.io/npm/dm/dsh-see-world?style=flat-square&label=downloads"/></a>
  <a href="https://github.com/windygo123/dsh-see-world/blob/main/LICENSE"><img alt="license" src="https://img.shields.io/github/license/windygo123/dsh-see-world?style=flat-square"/></a>
  <a href="https://github.com/windygo123/dsh-see-world"><img alt="GitHub stars" src="https://img.shields.io/github/stars/windygo123/dsh-see-world?style=flat-square"/></a>
</p>

> A **decision-layer** plugin for DeepSeek Harness: it decides **when** to search (not how), and enforces **search-first answers**. Local tasks stay zero-disturbance.

- **Decision layer**: it judges whether the current message needs fresh information; search *execution* is delegated to capability-layer plugins (Tavily / SearXNG provider / built-in `web_search`, etc.). This plugin decides the timing and enforces "search first, answer with sources, say 'uncertain' when nothing is found".

---

## Features

| | |
|---|---|
| 🎯 **Search-first answers** | When a message needs fresh info, a directive is injected before the reply: search first, cite sources, and say "uncertain" if nothing is found — no fabrication |
| ⚡ **Zero disturbance** | Local tasks (code / files / math / chat) pass through instantly — no search, no flicker, no noise |
| 🧭 **Fresh-info first** | Asking about latest news, prices, versions, policies? Defaults to verifying online before relying on stale knowledge |
| 🔍 **Visible judgment** | Breathing status bar while judging, "🔍 searched N sources" reply marker, and an explicit "judge timeout" notice after 8s |
| 🛡️ **Fault-proof** | No search plugin installed, judge errors, model timeout — everything degrades silently; the conversation never breaks |
| 🖥️ **Visual config** | Settings card (Settings → Plugins → "开眼看世界"), applies live on save; per-field ❓ help tooltips + a "Test connection" button for the judge model |
| 📈 **Observable decisions** | Local JSONL decision log (reason / latency / cost / whether searched), stored locally only, summaries by default |
| 🔒 **Privacy-friendly** | Judge reuses the session model by default; optionally use a local Ollama model — judging never leaves your machine |
| 📝 **Testable** | 26-case appendix B regression locks (≤1 miss, ≤1 false positive); misclassified cases can be re-imported into the test suite |

---

## Installation

```bash
dsh plugin --profile web add dsh-see-world
```

> After install/update, **restart the DSH GUI** so the browser half (settings card, judge status bar) loads (see [FAQ #5](#known-limits--faq)).

For local development (repo path — run `npm run build` first):

```bash
dsh plugin --profile web add D:\dsh-open-eyes
```

---

## Zero Configuration

**Works out of the box.** The default gear is *lenient* (search when unsure), and the judge **reuses the session model** — no extra model to pick, no extra quota (with a local Ollama judge, judging never leaves your machine; see [CONFIG.md](CONFIG.md)).

Pure local tasks (code, files, commands, math, chat) do not trigger search by default; messages that need fresh info are answered search-first, with a "🔍 searched N sources" marker appended.

---

## Configuration Reference

Full plain-language explanations are in **[CONFIG.md](CONFIG.md)**; edit everything in the settings card (Settings → Plugin config → 开眼看世界, namespace `open-eyes`). Changes apply live on the next turn; invalid values are rejected on save (`log_dir` applies after restart).

| Key | Default | Meaning |
|---|---|---|
| `trigger_gear` | `lenient` | Trigger gear: `lenient` (search when unsure, default) / `balanced` / `conservative` (less disturbance) |
| `judge_model` | `''` | Judge model. Empty = reuse session model; `provider/model` (e.g. `deepseek/deepseek-chat`) sets a route; `ollama/name` (e.g. `ollama/qwen3.5:4b`) talks to local Ollama directly — judging stays on your machine, consuming no session-model quota |
| `must_search` | `[]` | Whitelist: matching messages always search |
| `never_search` | `[]` | Blacklist: matching messages never search (wins over whitelist) |
| `budget` | `0` | Max searches per session; `0` = unlimited. Over budget → mark "unverified/uncertain" instead of searching |
| `context_turns` | `3` | Recent turns carried into judging (3 ≈ last 2–3 turns) |
| `mark_reply` | `true` | Append "🔍 已搜索 N 个来源" to replies of triggered turns that actually searched |
| `log_dir` | `''` | Decision log directory; empty = `~/.dsh/dsh-open-eyes/decisions/` |
| `log_input_verbatim` | `false` | Log the full input text; default stores only a summary (privacy) |

### Message-level force marks (highest priority, above every config)

- Two-char prefixes (guaranteed): start with 「**不搜**」 = never search this message; start with 「**先搜**」 = must search (e.g. `先搜 帮我看看这个报错` — "search first: look at this error");
- Natural language (primary): "这个不用搜 / 别搜了" (don't search this) or "先查一下 / 上网搜搜再答" (look it up first) also work;
- Prefix wins when both appear.

---

## Compatibility with Capability-Layer Plugins

**This plugin ships no search engine**; it composes with DSH search capability plugins: it decides **when**, they perform **how**.

- Search execution goes through the DSH standard seam: ① the `ctx.web` search seam (`@deepseek-ai/dsh-web`'s `WebRuntime.search`; `dsh-tool-web`'s `web_search` uses it too); ② a standard search tool registered via `ctx.tools` (e.g. `web_search`);
- Works with any provider plugin (Tavily / SearXNG / built-in web search) with **no extra config** — detected automatically;
- **No search plugin installed** (or all removed): the plugin degrades silently — judging still runs (logged only), nothing is injected, nothing errors, the conversation is unaffected.

---

## Privacy

- **Decision logs are local files only** (JSON Lines, default `~/.dsh/dsh-open-eyes/decisions/decisions.jsonl`, configurable via `log_dir`), **never uploaded**; deletable anytime; public format (fields in [CONFIG.md](CONFIG.md));
- **Judge input is stored as a summary by default** (truncated, with context-turn count); verbatim text requires `log_input_verbatim: true`;
- **Local Ollama judging**: with `judge_model: ollama/name`, judging hits the local Ollama native `/api/chat` — **it never leaves your machine**, and consumes no session-model quota; if Ollama is down, the plugin degrades gracefully (this turn is skipped — no block, no error).

---

## Known Limits / FAQ

### 1. Text-only judging — image content is not considered

The judge reads **text blocks** only — image content does not affect the search decision. A **pure-image message defaults to no search** (and skips judging entirely): instant, zero-disturbance. To search based on image content, prefix the message with `先搜` + intent, e.g. `先搜 这张图里的产品报价` — the force mark always works.

### 2. Judge model: session model by default, local/cheap models supported

Empty `judge_model` reuses the session model (zero-config default). To cut cost/latency, set `provider/model` (e.g. `deepseek/deepseek-chat`) or a local model `ollama/name` (e.g. `ollama/qwen3.5:4b`, judging stays on your machine). **Click "Test connection" after changing it**, then save.

### 3. 8-second judge timeout — skips the turn, shows the notice, never blocks

The semantic judge has a hard 8s timeout. On timeout it **skips judging this turn** (degraded pass-through) and shows "判定超时" (judge timed out) in the status bar — the conversation continues normally. Fast paths (clear local tasks / obvious time-sensitive queries) complete in milliseconds; if your model times out often, switch to a faster judge model (see #2).

### 4. No built-in search — install a search capability plugin

This plugin only decides "when to search"; it never executes a search. To actually fetch content, install a DSH **search capability plugin** (anything exposing the `ctx.web` search seam or a `web_search` standard tool — Tavily, SearXNG provider, built-in web search, etc.). Without one, the plugin degrades silently: everyday conversation is unaffected, but nothing is really fetched online.

### 5. Settings card missing? Restart the GUI after install/update

The "开眼看世界" config card is part of the browser half and loads when the GUI starts. **If the card does not appear in Settings after installing/updating the plugin, restart the DSH GUI once** (also restart before looking for it if the plugin is mid-run / hot-reloading).

### 6. `EADDRINUSE 127.0.0.1:3080`: an old DSH process is still running

Port 3080 in use means a previous DSH process is still alive (an old window may not have fully closed). **Close the old DSH window/process first** (make sure no dsh process remains in Task Manager), then start a new instance.

---

## Decision Observability (R4)

Triggered or not, reason, confidence, judge latency & token cost, whether a search actually happened, source count… each turn appends one line to the **local JSONL decision log** (`~/.dsh/dsh-open-eyes/decisions/decisions.jsonl`); deletable anytime, never uploaded. Turns that triggered and searched get a "🔍 已搜索 N 个来源" reply marker. Programmatic access:

```js
import { loadRecords, aggregateStats } from 'dsh-see-world'
const stats = aggregateStats(loadRecords()) // default log path; or loadRecords('/your/path')
```

---

## Development

```bash
npm install
npm run build            # tsc → lib/
npm test                 # full unit tests (node --test; R1–R5 regression)
npm run test:regression  # appendix B judge regression (offline deterministic fixture + report)
npm run cases            # decision log → appendix B candidate cases (scripts/export-cases.mjs)
npm pack                 # preview the published tarball
```

> Test-suite expansion, the `npm run cases` workflow, and the two hard-won code pitfalls (`session.append` / `inject` declaration) are documented in **[CONTRIBUTING.md](CONTRIBUTING.md)**.

---

## Repository Layout

| Path | Description |
|---|---|
| `src/index.ts` | Plugin entry (`name` / `Config` / `apply`; R2 turn flow + R4 decision records; SSE / diagnostics / test-connection routes) |
| `src/config.ts` | Config schema + defaults + settings namespace registration (live) |
| `src/judge.ts` | Information-gap risk judge (force marks / domain rules / fast paths / semantic judging / gear / degrade) |
| `src/model.ts` | Judge model abstraction + session-model reuse + Ollama direct connect + connectivity test |
| `src/search.ts` | The only capability-layer seam: `ctx.web` search seam / standard search tool detection & execution (no built-in search) |
| `src/r2.ts` | Search-first injection + turn accounting (triggered/violation/budget) |
| `src/decision-log.ts` | R4 decision log (JSONL read/write / session stats / summaries) |
| `src/judge-bus.ts` `judge-signal.ts` `judge-sources.ts` | Judge-visual signal: in-process bus + SSE route (**never written to session logs**) |
| `src/settings-card.ts` `settings-form.ts` `client/OpenEyesCard.tsx` | Settings card (❓ help tooltips / test-connection button / form engine) |
| `cordis.patch.yml` | DSH bundle patch: mounts on install |
| `tests/` `test/` | Unit + turn-level integration tests + appendix B regression (runner + samples) |
| `LICENSE` | MIT |

---

## Documentation

- PRD (v0.2, incl. §6 open-source & release requirements): `fresh-info-first-需求文档.md`
- Configuration guide (plain-language per-field + test-connection usage): **[CONFIG.md](CONFIG.md)**
- Contributing guide (test-suite expansion / pitfalls): **[CONTRIBUTING.md](CONTRIBUTING.md)**
- Changelog: **[CHANGELOG.md](CHANGELOG.md)**
- 中文版说明：[README.md](README.md)

## License

[MIT](LICENSE)