@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.
Files changed (149) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/advisor/compaction.mjs +1 -1
  3. package/advisor/run.mjs +1 -1
  4. package/agent/chat-call.mjs +40 -27
  5. package/agent/child-marks.mjs +1 -1
  6. package/agent/dispatch-gates.mjs +7 -4
  7. package/agent/dispatch-run.mjs +1 -0
  8. package/agent/family-tools.mjs +13 -8
  9. package/agent/helpers.mjs +12 -10
  10. package/agent/image-heal.mjs +65 -0
  11. package/agent/light-round.mjs +4 -4
  12. package/agent/live-beat.mjs +4 -4
  13. package/agent/post-turn.mjs +34 -0
  14. package/agent/record-results.mjs +9 -1
  15. package/agent/run-stages.mjs +45 -10
  16. package/agent/setup-reminders.mjs +14 -13
  17. package/agent/setup.mjs +72 -48
  18. package/agent/spawn-child.mjs +1 -1
  19. package/agent/subagent-event.mjs +1 -1
  20. package/agent/suspension.mjs +2 -1
  21. package/agent/timers.mjs +8 -8
  22. package/agent/turn-loop.mjs +7 -1
  23. package/agent-tools/advisor.mjs +1 -1
  24. package/agent-tools/async-discard.mjs +40 -5
  25. package/agent-tools/async-settle.mjs +3 -1
  26. package/agent-tools/audit-block.mjs +5 -3
  27. package/agent-tools/batch-gate.mjs +198 -0
  28. package/agent-tools/batch-lifecycle.mjs +216 -36
  29. package/agent-tools/batch-paths.mjs +20 -20
  30. package/agent-tools/batch-skeleton.mjs +87 -0
  31. package/agent-tools/batch.mjs +11 -3
  32. package/agent-tools/checkpoint.mjs +1 -1
  33. package/agent-tools/context.mjs +28 -5
  34. package/agent-tools/design-slots.mjs +20 -7
  35. package/agent-tools/doc-check.mjs +75 -0
  36. package/agent-tools/eng.mjs +5 -3
  37. package/agent-tools/manifest.mjs +18 -18
  38. package/agent-tools/parent-channel.mjs +3 -3
  39. package/agent-tools/review-facts.mjs +2 -2
  40. package/agent-tools/settings.mjs +1 -1
  41. package/agent-tools/spawn-gates.mjs +108 -47
  42. package/agent-tools/ssh-session.mjs +486 -0
  43. package/agent-tools/ssh-transport.mjs +324 -0
  44. package/agent-tools/ssh-vault.mjs +291 -0
  45. package/agent-tools/subagent-scheduler.mjs +6 -5
  46. package/agent-tools/subagent-spawn.mjs +18 -11
  47. package/agent-tools/subagent.mjs +17 -9
  48. package/agent-tools/task.mjs +1 -1
  49. package/agent-tools/timer.mjs +8 -2
  50. package/agent-tools/zero-write-watch.mjs +2 -2
  51. package/agent-tools.mjs +3 -2
  52. package/attachments.mjs +4 -4
  53. package/browser/cdp.mjs +2 -2
  54. package/browser/launch.mjs +50 -4
  55. package/browser/snapshot.mjs +1 -1
  56. package/config-defaults.mjs +92 -0
  57. package/config-io.mjs +7 -2
  58. package/config-normalize.mjs +109 -0
  59. package/config-watch.mjs +2 -2
  60. package/config.mjs +88 -190
  61. package/declaration.mjs +5 -1
  62. package/doc-check/anchors.mjs +142 -0
  63. package/doc-check/index.mjs +210 -0
  64. package/doc-check/version.mjs +84 -0
  65. package/doc-check/width.mjs +132 -0
  66. package/file-refs.mjs +6 -6
  67. package/generate-title.mjs +1 -1
  68. package/hooks.mjs +1 -1
  69. package/i18n.mjs +13 -1
  70. package/ledger-cmd.mjs +314 -62
  71. package/ledger-db.mjs +122 -31
  72. package/ledger-migrate.mjs +18 -12
  73. package/ledger-read.mjs +6 -7
  74. package/ledger-tools.mjs +138 -77
  75. package/ledger.mjs +34 -62
  76. package/log.mjs +17 -3
  77. package/manifest-discovery.mjs +21 -18
  78. package/manifest-schema.mjs +17 -14
  79. package/manifest.mjs +37 -22
  80. package/mcp/transport-stdio.mjs +1 -1
  81. package/mcp.mjs +2 -2
  82. package/memory/core.mjs +1 -1
  83. package/memory/scan.mjs +1 -1
  84. package/memory/sweep.mjs +1 -1
  85. package/model-specs.mjs +7 -7
  86. package/notify-policy.mjs +6 -6
  87. package/package.json +2 -1
  88. package/permission.mjs +21 -1
  89. package/prompts/advisor-design.md +53 -23
  90. package/prompts/advisor-round1.md +34 -14
  91. package/prompts/advisor-round2.md +32 -19
  92. package/prompts/advisor-round3.md +34 -21
  93. package/prompts/common.md +148 -79
  94. package/prompts/consult-base.md +14 -8
  95. package/prompts/discipline-engineering.md +70 -66
  96. package/prompts/discipline-normal.md +86 -77
  97. package/prompts/persona-coder.md +6 -6
  98. package/prompts/persona-eng-coder.md +19 -18
  99. package/prompts/persona-eng-designer.md +40 -31
  100. package/prompts/persona-engineering.md +124 -92
  101. package/prompts/persona-explore.md +2 -2
  102. package/prompts/persona-normal.md +20 -17
  103. package/prompts/persona-plan.md +7 -7
  104. package/prompts/subagent-base.md +4 -4
  105. package/provider/anthropic.mjs +4 -2
  106. package/provider/core.mjs +4 -2
  107. package/provider/errors.mjs +8 -0
  108. package/provider/google.mjs +8 -1
  109. package/provider/normalize.mjs +42 -12
  110. package/provider/responses-request.mjs +4 -3
  111. package/provider/responses.mjs +2 -2
  112. package/provider/wait-status.mjs +1 -1
  113. package/provider-flows.mjs +1 -1
  114. package/queued.mjs +2 -2
  115. package/rules.mjs +4 -4
  116. package/session-gc.mjs +1 -1
  117. package/session-lifecycle.mjs +7 -7
  118. package/session-slot-write.mjs +2 -2
  119. package/session-slots-manifest.mjs +1 -1
  120. package/session.mjs +1 -1
  121. package/shell-candidates.mjs +2 -2
  122. package/shell-identity.mjs +1 -1
  123. package/team.mjs +63 -5
  124. package/think-off.mjs +3 -3
  125. package/token-ttl.mjs +1 -1
  126. package/token-window.mjs +1 -1
  127. package/tool-docs/bash.md +1 -1
  128. package/tool-docs/batch.md +1 -1
  129. package/tool-docs/browser.md +1 -1
  130. package/tool-docs/doc_check.md +54 -0
  131. package/tool-docs/fetch.md +3 -2
  132. package/tool-docs/ledger.md +7 -5
  133. package/tool-docs/manifest.md +14 -2
  134. package/tool-docs/ssh.md +51 -0
  135. package/tool-docs/subagent.md +6 -9
  136. package/tool-docs/timer.md +2 -1
  137. package/tool-docs/websearch.md +3 -1
  138. package/tools/browser.mjs +1 -1
  139. package/tools/execute.mjs +1 -1
  140. package/tools/git-run.mjs +2 -2
  141. package/tools/git.mjs +8 -8
  142. package/tools/index.mjs +3 -0
  143. package/tools/ops.mjs +1 -1
  144. package/tools/process-tree.mjs +2 -2
  145. package/tools/ssh-askpass.mjs +35 -0
  146. package/tools/ssh.mjs +105 -0
  147. package/tools/web-deepseek.mjs +172 -0
  148. package/tools/web.mjs +29 -4
  149. 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
- return truncate(merged.slice(0, limit).map((r, i) => `${i + 1}. [${r._engine}] ${r.title}\n ${r.href}\n ${r.snippet}`).join("\n\n"))
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, _ctx) {
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:14`(`findVisionChannel` 逐字)
5
- * + `image-handler.mjs:70-95`(`runVisionReader` 逐字);VSC 自持跑者 ∕ 渠道副本已随迁移轮(parity-b4)删。
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:16`;实例 = `tools/index.mjs:63`);本档静态边只留闭包净件(`config-io.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:70` 同值——一次性子代理的成功径上限)。 */
28
+ /** 读图墙钟上限(VSC `image-handler.mjs` 的 `runVisionReader` 同值——一次性子代理的成功径上限)。 */
29
29
  export const VISION_READ_TIMEOUT_MS = 60_000
30
30
 
31
31
  /** 找可跑视觉读图的渠道:`{ provider, model }` ∥ `null`。