@thincoder/core 0.10.7 → 0.10.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/advisor/compaction.mjs +1 -1
- package/advisor/run.mjs +1 -1
- package/agent/chat-call.mjs +40 -27
- package/agent/child-marks.mjs +1 -1
- package/agent/dispatch-gates.mjs +7 -4
- package/agent/dispatch-run.mjs +1 -0
- package/agent/family-tools.mjs +13 -8
- package/agent/helpers.mjs +12 -10
- package/agent/image-heal.mjs +65 -0
- package/agent/light-round.mjs +4 -4
- package/agent/live-beat.mjs +4 -4
- package/agent/post-turn.mjs +34 -0
- package/agent/record-results.mjs +9 -1
- package/agent/run-stages.mjs +45 -10
- package/agent/setup-reminders.mjs +14 -13
- package/agent/setup.mjs +72 -48
- package/agent/spawn-child.mjs +1 -1
- package/agent/subagent-event.mjs +1 -1
- package/agent/suspension.mjs +2 -1
- package/agent/timers.mjs +8 -8
- package/agent/turn-loop.mjs +7 -1
- package/agent-tools/advisor.mjs +1 -1
- package/agent-tools/async-discard.mjs +40 -5
- package/agent-tools/async-settle.mjs +3 -1
- package/agent-tools/audit-block.mjs +5 -3
- package/agent-tools/batch-gate.mjs +198 -0
- package/agent-tools/batch-lifecycle.mjs +216 -36
- package/agent-tools/batch-paths.mjs +20 -20
- package/agent-tools/batch-skeleton.mjs +87 -0
- package/agent-tools/batch.mjs +11 -3
- package/agent-tools/checkpoint.mjs +1 -1
- package/agent-tools/context.mjs +28 -5
- package/agent-tools/design-slots.mjs +20 -7
- package/agent-tools/doc-check.mjs +75 -0
- package/agent-tools/eng.mjs +5 -3
- package/agent-tools/manifest.mjs +18 -18
- package/agent-tools/parent-channel.mjs +3 -3
- package/agent-tools/review-facts.mjs +2 -2
- package/agent-tools/settings.mjs +1 -1
- package/agent-tools/spawn-gates.mjs +108 -47
- package/agent-tools/ssh-session.mjs +486 -0
- package/agent-tools/ssh-transport.mjs +324 -0
- package/agent-tools/ssh-vault.mjs +291 -0
- package/agent-tools/subagent-scheduler.mjs +6 -5
- package/agent-tools/subagent-spawn.mjs +18 -11
- package/agent-tools/subagent.mjs +17 -9
- package/agent-tools/task.mjs +1 -1
- package/agent-tools/timer.mjs +8 -2
- package/agent-tools/zero-write-watch.mjs +2 -2
- package/agent-tools.mjs +3 -2
- package/attachments.mjs +4 -4
- package/browser/cdp.mjs +2 -2
- package/browser/launch.mjs +50 -4
- package/browser/snapshot.mjs +1 -1
- package/config-defaults.mjs +92 -0
- package/config-io.mjs +7 -2
- package/config-normalize.mjs +109 -0
- package/config-watch.mjs +2 -2
- package/config.mjs +88 -190
- package/declaration.mjs +5 -1
- package/doc-check/anchors.mjs +142 -0
- package/doc-check/index.mjs +210 -0
- package/doc-check/version.mjs +84 -0
- package/doc-check/width.mjs +132 -0
- package/file-refs.mjs +6 -6
- package/generate-title.mjs +1 -1
- package/hooks.mjs +1 -1
- package/i18n.mjs +13 -1
- package/ledger-cmd.mjs +314 -62
- package/ledger-db.mjs +122 -31
- package/ledger-migrate.mjs +18 -12
- package/ledger-read.mjs +6 -7
- package/ledger-tools.mjs +138 -77
- package/ledger.mjs +34 -62
- package/log.mjs +17 -3
- package/manifest-discovery.mjs +21 -18
- package/manifest-schema.mjs +17 -14
- package/manifest.mjs +37 -22
- package/mcp/transport-stdio.mjs +1 -1
- package/mcp.mjs +2 -2
- package/memory/core.mjs +1 -1
- package/memory/scan.mjs +1 -1
- package/memory/sweep.mjs +1 -1
- package/model-specs.mjs +7 -7
- package/notify-policy.mjs +6 -6
- package/package.json +2 -1
- package/permission.mjs +21 -1
- package/prompts/advisor-design.md +53 -23
- package/prompts/advisor-round1.md +34 -14
- package/prompts/advisor-round2.md +32 -19
- package/prompts/advisor-round3.md +34 -21
- package/prompts/common.md +148 -79
- package/prompts/consult-base.md +14 -8
- package/prompts/discipline-engineering.md +70 -66
- package/prompts/discipline-normal.md +86 -77
- package/prompts/persona-coder.md +6 -6
- package/prompts/persona-eng-coder.md +19 -18
- package/prompts/persona-eng-designer.md +40 -31
- package/prompts/persona-engineering.md +124 -92
- package/prompts/persona-explore.md +2 -2
- package/prompts/persona-normal.md +20 -17
- package/prompts/persona-plan.md +7 -7
- package/prompts/subagent-base.md +4 -4
- package/provider/anthropic.mjs +4 -2
- package/provider/core.mjs +4 -2
- package/provider/errors.mjs +8 -0
- package/provider/google.mjs +8 -1
- package/provider/normalize.mjs +42 -12
- package/provider/responses-request.mjs +4 -3
- package/provider/responses.mjs +2 -2
- package/provider/wait-status.mjs +1 -1
- package/provider-flows.mjs +1 -1
- package/queued.mjs +2 -2
- package/rules.mjs +4 -4
- package/session-gc.mjs +1 -1
- package/session-lifecycle.mjs +7 -7
- package/session-slot-write.mjs +2 -2
- package/session-slots-manifest.mjs +1 -1
- package/session.mjs +1 -1
- package/shell-candidates.mjs +2 -2
- package/shell-identity.mjs +1 -1
- package/team.mjs +63 -5
- package/think-off.mjs +3 -3
- package/token-ttl.mjs +1 -1
- package/token-window.mjs +1 -1
- package/tool-docs/bash.md +1 -1
- package/tool-docs/batch.md +1 -1
- package/tool-docs/browser.md +1 -1
- package/tool-docs/doc_check.md +54 -0
- package/tool-docs/fetch.md +3 -2
- package/tool-docs/ledger.md +7 -5
- package/tool-docs/manifest.md +14 -2
- package/tool-docs/ssh.md +51 -0
- package/tool-docs/subagent.md +6 -9
- package/tool-docs/timer.md +2 -1
- package/tool-docs/websearch.md +3 -1
- package/tools/browser.mjs +1 -1
- package/tools/execute.mjs +1 -1
- package/tools/git-run.mjs +2 -2
- package/tools/git.mjs +8 -8
- package/tools/index.mjs +3 -0
- package/tools/ops.mjs +1 -1
- package/tools/process-tree.mjs +2 -2
- package/tools/ssh-askpass.mjs +35 -0
- package/tools/ssh.mjs +105 -0
- package/tools/web-deepseek.mjs +172 -0
- package/tools/web.mjs +29 -4
- package/vision-reader.mjs +4 -4
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* web-deepseek.mjs — DeepSeek Search / Fetch backend for the `websearch` & `fetch` tools
|
|
3
|
+
* (design = `docs/core/design/TOOLS.md` §6.23 · ledger #1605).
|
|
4
|
+
*
|
|
5
|
+
* Key source = the first `providers[]` entry whose baseURL host is the official
|
|
6
|
+
* `api.deepseek.com` and whose apiKey resolves non-empty (`${env:VAR}` resolved at use
|
|
7
|
+
* time — an unresolved reference makes that entry ineligible, never throws). Zero new
|
|
8
|
+
* config: a configured DeepSeek provider is enough.
|
|
9
|
+
*
|
|
10
|
+
* The tools never fail because of this backend: every export returns a pure value
|
|
11
|
+
* (`{ status: "ok" | "empty" | "failed" | "unavailable" }`) and never throws.
|
|
12
|
+
* 401/402/403/404 trip a per-agent breaker (WeakSet); 408/429/5xx retry once (500 ms);
|
|
13
|
+
* everything else falls back to the caller's existing chain.
|
|
14
|
+
*/
|
|
15
|
+
import { URL } from "node:url"
|
|
16
|
+
import { truncate, isPrivateHost } from "./shared.mjs"
|
|
17
|
+
import { resolveProviderSecrets } from "../env-ref.mjs"
|
|
18
|
+
import { proxyFetch } from "../proxy.mjs"
|
|
19
|
+
import { logEvent } from "../log.mjs"
|
|
20
|
+
|
|
21
|
+
// D-WB2: endpoints are fixed constants — `baseURL` only decides eligibility (a user's
|
|
22
|
+
// `/v1` suffix or prefix must not build a doubled path, and the key only goes to the
|
|
23
|
+
// official host).
|
|
24
|
+
const SEARCH_URL = "https://api.deepseek.com/v1/tools/search"
|
|
25
|
+
const FETCH_URL = "https://api.deepseek.com/v1/tools/fetch"
|
|
26
|
+
const DEEPSEEK_HOST = "api.deepseek.com"
|
|
27
|
+
const TIMEOUT_MS = 15_000
|
|
28
|
+
const RETRY_DELAY_MS = 500 // D-WB5: fixed single backoff (no Retry-After parsing)
|
|
29
|
+
const COST_PER_CALL = 0.00075 // $0.75 / 1000 successful requests — `count` does not change the price
|
|
30
|
+
const TRIP_STATUS = new Set([401, 402, 403, 404])
|
|
31
|
+
const RETRY_STATUS = (() => {
|
|
32
|
+
const codes = new Set([408, 429])
|
|
33
|
+
for (let code = 500; code <= 599; code++) codes.add(code)
|
|
34
|
+
return codes
|
|
35
|
+
})()
|
|
36
|
+
|
|
37
|
+
let _transport = proxyFetch
|
|
38
|
+
let _breaker = new WeakSet()
|
|
39
|
+
|
|
40
|
+
/** Test seam (CONFIG.md §6.2 ③ — paired): swap the transport; reset also clears the breaker. */
|
|
41
|
+
export function _setDeepseekTransportForTest(fn) { _transport = typeof fn === "function" ? fn : proxyFetch }
|
|
42
|
+
export function _resetDeepseekTransportForTest() { _transport = proxyFetch; _breaker = new WeakSet() }
|
|
43
|
+
|
|
44
|
+
/** `local` skips the DeepSeek backend; anything else (missing ∥ misspelled ∥ non-string) = `auto` (fail-open). */
|
|
45
|
+
export function backendMode(cfg, key) {
|
|
46
|
+
return cfg?.[key]?.backend === "local" ? "local" : "auto"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** First qualifying channel in stored order → `{ apiKey }` | null. Never throws. */
|
|
50
|
+
function deepseekChannel(cfg) {
|
|
51
|
+
const list = Array.isArray(cfg?.providersList) ? cfg.providersList
|
|
52
|
+
: Array.isArray(cfg?.providers) ? cfg.providers : []
|
|
53
|
+
for (const entry of list) {
|
|
54
|
+
if (entry === null || typeof entry !== "object") continue
|
|
55
|
+
let url
|
|
56
|
+
try { url = new URL(String(entry.baseURL ?? "")) } catch { continue }
|
|
57
|
+
if (url.protocol !== "https:") continue
|
|
58
|
+
const host = url.hostname.toLowerCase().replace(/\.$/, "") // trailing-dot host still official
|
|
59
|
+
if (host !== DEEPSEEK_HOST) continue // subdomains never qualify (x.api.deepseek.com)
|
|
60
|
+
if (url.port !== "" && url.port !== "443") continue // non-standard port could impersonate
|
|
61
|
+
let apiKey
|
|
62
|
+
try { apiKey = resolveProviderSecrets(entry).apiKey } catch { continue } // unset env ref ⇒ ineligible
|
|
63
|
+
if (typeof apiKey !== "string" || apiKey.trim() === "") continue
|
|
64
|
+
return { apiKey: apiKey.trim() }
|
|
65
|
+
}
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const tripped = (agent) => agent !== null && typeof agent === "object" && _breaker.has(agent)
|
|
70
|
+
|
|
71
|
+
/** Break the backend for this agent identity (single domain — search ∥ fetch share it, D-WB3). */
|
|
72
|
+
function trip(agent) {
|
|
73
|
+
if (agent !== null && typeof agent === "object") _breaker.add(agent)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** One POST. Returns `{ kind: "ok", data }` | `{ kind: "http", status }` | `{ kind: "net", timeout }` | `{ kind: "json" }`. */
|
|
77
|
+
async function postOnce(url, body, apiKey, proxyUri) {
|
|
78
|
+
const ctrl = new AbortController()
|
|
79
|
+
const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS)
|
|
80
|
+
try {
|
|
81
|
+
const res = await _transport(url, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
// Same header shape as the Tavily leg (`web.mjs` bearer precedent).
|
|
84
|
+
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` },
|
|
85
|
+
body: JSON.stringify(body),
|
|
86
|
+
signal: ctrl.signal,
|
|
87
|
+
}, proxyUri)
|
|
88
|
+
if (!res || res.ok !== true) {
|
|
89
|
+
try { await res?.body?.cancel?.() } catch { /* body already closed */ }
|
|
90
|
+
return { kind: "http", status: Number(res?.status ?? 0) }
|
|
91
|
+
}
|
|
92
|
+
try { return { kind: "ok", data: await res.json() } } catch { return { kind: "json" } }
|
|
93
|
+
} catch (e) {
|
|
94
|
+
return { kind: "net", timeout: e?.name === "AbortError" }
|
|
95
|
+
} finally { clearTimeout(timer) }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** One logical call: retry once (408/429/5xx), trip on 401/402/403/404, otherwise fall back. */
|
|
99
|
+
async function request(url, body, apiKey, proxyUri, tool, agent) {
|
|
100
|
+
const started = Date.now()
|
|
101
|
+
let r = await postOnce(url, body, apiKey, proxyUri)
|
|
102
|
+
if (r.kind === "http" && RETRY_STATUS.has(r.status)) {
|
|
103
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS))
|
|
104
|
+
r = await postOnce(url, body, apiKey, proxyUri)
|
|
105
|
+
}
|
|
106
|
+
if (r.kind === "ok") return { ok: true, data: r.data }
|
|
107
|
+
if (r.kind === "http" && TRIP_STATUS.has(r.status) && agent !== null && typeof agent === "object") {
|
|
108
|
+
trip(agent)
|
|
109
|
+
// Tripping line only (transient fallbacks stay quiet — ⑤).
|
|
110
|
+
logEvent("web:deepseek", { tool, status: r.status, tripped: true, ms: Date.now() - started })
|
|
111
|
+
}
|
|
112
|
+
const reason = r.kind === "http" ? `http ${r.status}`
|
|
113
|
+
: r.kind === "net" ? (r.timeout ? "timeout" : "network")
|
|
114
|
+
: "malformed json"
|
|
115
|
+
return { ok: false, reason }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Response → tool text: `[deepseek]` block per result, same numbered shape as the Tavily ∥ Bing legs. */
|
|
119
|
+
function renderSearch(data, limit) {
|
|
120
|
+
const list = Array.isArray(data?.results) ? data.results : []
|
|
121
|
+
const results = list.slice(0, limit)
|
|
122
|
+
if (results.length === 0) return { n: 0, text: "" }
|
|
123
|
+
const blocks = results.map((r, i) => {
|
|
124
|
+
const meta = []
|
|
125
|
+
if (typeof r?.date === "string" && r.date.trim() !== "") meta.push(r.date)
|
|
126
|
+
if (Number.isFinite(r?.authority) && r.authority >= 0) meta.push(`authority ${r.authority}`) // -1 = unknown ⇒ omitted
|
|
127
|
+
if (Number.isFinite(r?.rerank_score)) meta.push(`rerank ${r.rerank_score}`)
|
|
128
|
+
const summary = typeof r?.summary === "string" && r.summary.trim() !== ""
|
|
129
|
+
? r.summary
|
|
130
|
+
: (typeof r?.snippet === "string" ? r.snippet : "")
|
|
131
|
+
const lines = [`${i + 1}. [deepseek] ${r?.title ?? ""}`, ` ${r?.url ?? ""}`]
|
|
132
|
+
if (meta.length > 0) lines.push(` ${meta.join(" · ")}`)
|
|
133
|
+
if (summary !== "") lines.push(` ${summary}`)
|
|
134
|
+
return lines.join("\n")
|
|
135
|
+
})
|
|
136
|
+
return { n: results.length, text: truncate(blocks.join("\n\n")) }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** search — `count` = the tool-face limit (`web.mjs` already clamps it to 20); `< 1` ⇒ 1. Never throws. */
|
|
140
|
+
export async function deepseekSearch({ cfg, query, limit = 8, proxy = null, agent = null } = {}) {
|
|
141
|
+
const channel = deepseekChannel(cfg)
|
|
142
|
+
if (channel === null) return { status: "unavailable" }
|
|
143
|
+
if (tripped(agent)) return { status: "failed", reason: "breaker open" }
|
|
144
|
+
const count = Math.min(50, Math.max(1, Number.isFinite(limit) ? Math.floor(limit) : 8))
|
|
145
|
+
const started = Date.now()
|
|
146
|
+
const r = await request(SEARCH_URL, { query, count, need_content: false }, channel.apiKey, proxy, "search", agent)
|
|
147
|
+
if (!r.ok) return { status: "failed", reason: r.reason }
|
|
148
|
+
const { n, text } = renderSearch(r.data, count)
|
|
149
|
+
logEvent("web:deepseek", { tool: "search", n, ms: Date.now() - started, cost: n > 0 ? COST_PER_CALL : 0 })
|
|
150
|
+
return n > 0 ? { status: "ok", text, n } : { status: "empty" }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** fetch — title + Markdown; empty `content` ⇒ `empty` (caller falls back to the local fetch). Never throws. */
|
|
154
|
+
export async function deepseekFetch({ cfg, url, proxy = null, agent = null } = {}) {
|
|
155
|
+
const channel = deepseekChannel(cfg)
|
|
156
|
+
if (channel === null) return { status: "unavailable" }
|
|
157
|
+
if (tripped(agent)) return { status: "failed", reason: "breaker open" }
|
|
158
|
+
// `web.mjs` runs the SSRF gate before calling; keep a second one here so a direct
|
|
159
|
+
// caller can never send a private/metadata URL to a third party either.
|
|
160
|
+
let target
|
|
161
|
+
try { target = new URL(url) } catch { return { status: "failed", reason: "invalid url" } }
|
|
162
|
+
if (isPrivateHost(target.hostname)) return { status: "failed", reason: "private url" }
|
|
163
|
+
const started = Date.now()
|
|
164
|
+
const r = await request(FETCH_URL, { url }, channel.apiKey, proxy, "fetch", agent)
|
|
165
|
+
if (!r.ok) return { status: "failed", reason: r.reason }
|
|
166
|
+
const title = typeof r.data?.title === "string" ? r.data.title : ""
|
|
167
|
+
const content = typeof r.data?.content === "string" ? r.data.content : ""
|
|
168
|
+
const n = content.trim() === "" ? 0 : content.length
|
|
169
|
+
logEvent("web:deepseek", { tool: "fetch", n, ms: Date.now() - started, cost: n > 0 ? COST_PER_CALL : 0 })
|
|
170
|
+
if (n === 0) return { status: "empty" }
|
|
171
|
+
return { status: "ok", text: truncate(`[deepseek] ${title}\n\n${content}`), n }
|
|
172
|
+
}
|
package/tools/web.mjs
CHANGED
|
@@ -2,11 +2,17 @@ import { DESC, truncate, stripTags, htmlToText, isPrivateHost } from "./shared.m
|
|
|
2
2
|
import { URL } from "node:url";
|
|
3
3
|
import { proxyFetch } from "../proxy.mjs";
|
|
4
4
|
import { resolveEnvRefs } from "../env-ref.mjs";
|
|
5
|
+
import { backendMode, deepseekSearch, deepseekFetch } from "./web-deepseek.mjs";
|
|
5
6
|
|
|
6
7
|
export const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
|
7
8
|
const FETCH_TIMEOUT = 15_000
|
|
8
9
|
// D-TF3:网络失败错误文本追加 proxy 提示——纯文本提示,不自动路由(2026-08-31 裁定:proxy 显式传不自动应用)
|
|
9
10
|
const PROXY_HINT = "network failure — retry with proxy: 'http://host:port' if the target is blocked"
|
|
11
|
+
// WEB-BACKEND-DEEPSEEK(TOOLS.md §6.23 ④):DeepSeek 未出结果 ⇒ 回落腿承接后结果末尾恰一行(按返回态分叉)。
|
|
12
|
+
// fetch 腿两态零提示(D-WB6);`unavailable` ∥ `local` 零提示(零回归)。
|
|
13
|
+
const deepseekNote = (status, leg) => status === "empty"
|
|
14
|
+
? `(deepseek returned no results — results from ${leg})`
|
|
15
|
+
: `(deepseek backend unavailable — results from ${leg})`
|
|
10
16
|
|
|
11
17
|
// ── Web search (Bing; direct by default, through proxy when configured and web toggle on) ──
|
|
12
18
|
|
|
@@ -107,18 +113,29 @@ export const websearchTool = {
|
|
|
107
113
|
// 2026-08-31 ruling: proxy is a PER-CALL decision (args.proxy), never the config.json
|
|
108
114
|
// fixed configuration. Model picks by target: github/foreign → pass proxy; gitee/domestic → omit.
|
|
109
115
|
const proxyUri = args.proxy ?? null
|
|
116
|
+
// DeepSeek first when a qualifying provider is configured (TOOLS.md §6.23 ②) — `local`,
|
|
117
|
+
// no channel, or a query over 128 chars ⇒ zero calls, and the chain below runs verbatim.
|
|
118
|
+
let dsNote = null
|
|
119
|
+
if (backendMode(ctx?.agent?.config, "websearch") !== "local"
|
|
120
|
+
&& typeof args.query === "string" && args.query.length <= 128) {
|
|
121
|
+
const ds = await deepseekSearch({ cfg: ctx?.agent?.config, query: args.query, limit, proxy: proxyUri, agent: ctx?.agent })
|
|
122
|
+
if (ds.status === "ok") return ds.text
|
|
123
|
+
if (ds.status === "empty" || ds.status === "failed") dsNote = ds.status
|
|
124
|
+
}
|
|
125
|
+
// A DeepSeek outcome only adds a line once a fallback leg actually carries results.
|
|
126
|
+
const withNote = (text, leg) => (dsNote ? `${text}\n\n${deepseekNote(dsNote, leg)}` : text)
|
|
110
127
|
// Structured search first when a Tavily key is configured — stable, dated,
|
|
111
128
|
// no HTML scraping. Falls back to Bing silently.
|
|
112
129
|
const tavily = await fetchTavily(args.query, limit, ctx, proxyUri)
|
|
113
130
|
if (tavily && tavily.results.length > 0) {
|
|
114
|
-
return truncate(tavily.results.slice(0, limit).map((r, i) => `${i + 1}. [tavily] ${r.title}\n ${r.href}\n ${r.snippet}`).join("\n\n"))
|
|
131
|
+
return withNote(truncate(tavily.results.slice(0, limit).map((r, i) => `${i + 1}. [tavily] ${r.title}\n ${r.href}\n ${r.snippet}`).join("\n\n")), "tavily")
|
|
115
132
|
}
|
|
116
133
|
if (args.engine) {
|
|
117
134
|
const engine = ENGINES.find(e => e.name === args.engine)
|
|
118
135
|
if (!engine) return `Unknown engine '${args.engine}'. Available: ${ENGINE_NAMES.join(", ")}`
|
|
119
136
|
const fetched = await fetchEngine(engine, args.query, page, proxyUri)
|
|
120
137
|
if (!fetched || fetched.results.length === 0) return "(no results)"
|
|
121
|
-
return truncate(fetched.results.slice(0, limit).map((r, i) => `${i + 1}. [${engine.label}] ${r.title}\n ${r.href}\n ${r.snippet}`).join("\n\n"))
|
|
138
|
+
return withNote(truncate(fetched.results.slice(0, limit).map((r, i) => `${i + 1}. [${engine.label}] ${r.title}\n ${r.href}\n ${r.snippet}`).join("\n\n")), engine.name)
|
|
122
139
|
}
|
|
123
140
|
const promises = ENGINES.map(e => fetchEngine(e, args.query, 1, proxyUri))
|
|
124
141
|
const fetched = (await Promise.all(promises)).filter(Boolean)
|
|
@@ -135,7 +152,9 @@ export const websearchTool = {
|
|
|
135
152
|
}
|
|
136
153
|
}
|
|
137
154
|
}
|
|
138
|
-
|
|
155
|
+
// Note only when a leg actually carried results — an empty merge keeps today's output.
|
|
156
|
+
const mergedText = truncate(merged.slice(0, limit).map((r, i) => `${i + 1}. [${r._engine}] ${r.title}\n ${r.href}\n ${r.snippet}`).join("\n\n"))
|
|
157
|
+
return merged.length > 0 ? withNote(mergedText, fetched[0].engine) : mergedText
|
|
139
158
|
},
|
|
140
159
|
}
|
|
141
160
|
|
|
@@ -193,9 +212,15 @@ export const fetchTool = {
|
|
|
193
212
|
required: ["url"],
|
|
194
213
|
},
|
|
195
214
|
readonly: true,
|
|
196
|
-
async execute(args,
|
|
215
|
+
async execute(args, ctx) {
|
|
197
216
|
if (!/^https?:\/\//.test(args.url)) throw new Error("url must start with http:// or https://")
|
|
198
217
|
if (isPrivateUrl(args.url)) throw new Error("fetch blocked: internal/private/metadata addresses are not allowed")
|
|
218
|
+
// DeepSeek returns Markdown (W3); `empty` ∥ `failed` fall through to the local fetch,
|
|
219
|
+
// both without a result note (D-WB6 — the content face stays verbatim).
|
|
220
|
+
if (backendMode(ctx?.agent?.config, "fetch") !== "local") {
|
|
221
|
+
const ds = await deepseekFetch({ cfg: ctx?.agent?.config, url: args.url, proxy: args.proxy ?? null, agent: ctx?.agent })
|
|
222
|
+
if (ds.status === "ok") return ds.text
|
|
223
|
+
}
|
|
199
224
|
try {
|
|
200
225
|
// 2026-08-31 ruling: per-call decision — args.proxy when passed, direct otherwise.
|
|
201
226
|
const proxyUri = args.proxy ?? null
|
package/vision-reader.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* vision-reader.mjs — 非多模态降级读图跑者(上提核件 —— parity-b4 W0 · 2026-09-29)。
|
|
3
3
|
*
|
|
4
|
-
* 上提源 = VSC `thincoder-vscode/src/extension/vision-channel.mjs
|
|
5
|
-
* + `image-handler.mjs
|
|
4
|
+
* 上提源 = VSC `thincoder-vscode/src/extension/vision-channel.mjs`(#1127 已删档;`findVisionChannel` 逐字)
|
|
5
|
+
* + `image-handler.mjs`(`runVisionReader` 逐字);VSC 自持跑者 ∕ 渠道副本已随迁移轮(parity-b4)删。
|
|
6
6
|
* 消费面 = VSC `image-handler.mjs`(已改指本档)· 桌面 `src/main/attachments.mjs`
|
|
7
7
|
* (降级适配器缺省 = 本档 —— `visionReader` 缝的核默认实现)。
|
|
8
8
|
*
|
|
@@ -19,13 +19,13 @@
|
|
|
19
19
|
*
|
|
20
20
|
* W8 契约②(2026-09-29 断链):`agent.mjs` ∕ `agent-tools/subagent-async.mjs` 静态链达 `node:sqlite`
|
|
21
21
|
* (`agent.mjs` → `agent/setup.mjs` → `memory.mjs` → `memory/schema.mjs`)⇒ 两端一律**函数体内动态 import**
|
|
22
|
-
* (约定源 = `ledger.mjs
|
|
22
|
+
* (约定源 = `ledger.mjs`;实例 = `tools/index.mjs` 的 `assembleBuiltinTools`);本档静态边只留闭包净件(`config-io.mjs` ∕
|
|
23
23
|
* `tools/index.mjs`)。
|
|
24
24
|
*/
|
|
25
25
|
import { resolveProviders } from "./config-io.mjs"
|
|
26
26
|
import { assembleBuiltinTools } from "./tools/index.mjs"
|
|
27
27
|
|
|
28
|
-
/** 读图墙钟上限(VSC `image-handler.mjs
|
|
28
|
+
/** 读图墙钟上限(VSC `image-handler.mjs` 的 `runVisionReader` 同值——一次性子代理的成功径上限)。 */
|
|
29
29
|
export const VISION_READ_TIMEOUT_MS = 60_000
|
|
30
30
|
|
|
31
31
|
/** 找可跑视觉读图的渠道:`{ provider, model }` ∥ `null`。
|