@thincoder/core 0.10.3 → 0.10.5
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 +62 -0
- package/README.md +1 -1
- package/advisor/citations.mjs +29 -3
- package/advisor/compaction.mjs +31 -7
- package/advisor/history.mjs +8 -6
- package/advisor/loop.mjs +29 -27
- package/advisor/messages.mjs +2 -1
- package/advisor/run.mjs +9 -5
- package/advisor/timeline.mjs +26 -0
- package/advisor.mjs +4 -2
- package/agent/assemble.mjs +1 -1
- package/agent/chat-call.mjs +1 -1
- package/agent/child-marks.mjs +2 -2
- package/agent/completion.mjs +3 -1
- package/agent/digest-account.mjs +189 -0
- package/agent/dispatch-gates.mjs +8 -0
- package/agent/dispatch.mjs +39 -18
- package/agent/family-tools.mjs +27 -16
- package/agent/helpers.mjs +12 -3
- package/agent/light-round.mjs +66 -0
- package/agent/post-turn.mjs +8 -4
- package/agent/record-results.mjs +4 -0
- package/agent/run-stages.mjs +32 -7
- package/agent/run-start.mjs +50 -3
- package/agent/setup-reminders.mjs +1 -1
- package/agent/setup.mjs +18 -3
- package/agent/subagent-event.mjs +1 -1
- package/agent/suspension.mjs +37 -12
- package/agent/timers.mjs +56 -5
- package/agent/turn-loop.mjs +30 -5
- package/agent/write-gate.mjs +20 -32
- package/agent-tools/advisor-async.mjs +15 -7
- package/agent-tools/advisor-settle.mjs +2 -2
- package/agent-tools/advisor.mjs +34 -33
- package/agent-tools/async-discard.mjs +34 -0
- package/agent-tools/async-settle.mjs +32 -7
- package/agent-tools/audit-block.mjs +1 -1
- package/agent-tools/bash-async.mjs +1 -1
- package/agent-tools/batch-lifecycle.mjs +59 -17
- package/agent-tools/batch-paths.mjs +24 -4
- package/agent-tools/batch-skeleton.mjs +59 -43
- package/agent-tools/batch.mjs +39 -18
- package/agent-tools/browser-async.mjs +198 -0
- package/agent-tools/checkpoint.mjs +1 -1
- package/agent-tools/design-slots.mjs +232 -0
- package/agent-tools/design-token.mjs +20 -0
- package/agent-tools/digest-budget.mjs +1 -1
- package/agent-tools/escalate-async.mjs +4 -2
- package/agent-tools/manifest.mjs +193 -0
- package/agent-tools/parent-channel.mjs +7 -4
- package/agent-tools/review-facts.mjs +143 -16
- package/agent-tools/settings.mjs +1 -1
- package/agent-tools/skill.mjs +1 -1
- package/agent-tools/subagent-actions-query.mjs +13 -6
- package/agent-tools/subagent-async.mjs +31 -15
- package/agent-tools/subagent-panel.mjs +2 -1
- package/agent-tools/subagent-scheduler.mjs +8 -5
- package/agent-tools/subagent-spawn.mjs +30 -56
- package/agent-tools/subagent.mjs +21 -8
- package/agent-tools/timer.mjs +186 -12
- package/agent-tools/zero-write-watch.mjs +77 -0
- package/agent-tools.mjs +2 -1
- package/agent.mjs +2 -2
- package/attachments.mjs +1 -1
- package/config-io.mjs +18 -13
- package/config-migrate.mjs +13 -29
- package/config-presets.mjs +33 -29
- package/config.mjs +68 -25
- package/context-degrade.mjs +1 -1
- package/context-echo.mjs +1 -1
- package/context-push.mjs +2 -2
- package/context.mjs +1 -1
- package/conventions.mjs +28 -8
- package/declaration.mjs +18 -2
- package/escape.mjs +1 -1
- package/file-links.mjs +1 -1
- package/file-refs.mjs +1 -1
- package/history-window.mjs +1 -1
- package/i18n.mjs +7 -0
- package/ledger-cmd.mjs +62 -16
- package/ledger-db.mjs +6 -0
- package/ledger-surface.mjs +49 -5
- package/ledger-tools.mjs +158 -87
- package/ledger-variant-notice.mjs +47 -0
- package/ledger.mjs +11 -18
- package/log.mjs +1 -1
- package/manifest-discovery.mjs +1 -1
- package/manifest-schema.mjs +7 -5
- package/manifest.mjs +5 -3
- package/mcp/transport-stdio.mjs +1 -1
- package/mcp.mjs +1 -1
- package/memory/code-index.mjs +1 -1
- package/memory/code-search.mjs +1 -1
- package/memory/code-sync.mjs +1 -1
- package/memory/docs.mjs +6 -2
- package/memory/file-list.mjs +1 -1
- package/memory/file-walk.mjs +17 -5
- package/memory/maintain.mjs +410 -0
- package/memory/memory-tool.mjs +21 -6
- package/memory/schema.mjs +19 -7
- package/memory/sweep.mjs +8 -5
- package/memory/sync-tail.mjs +16 -10
- package/model-ref.mjs +16 -13
- package/model-specs.mjs +21 -9
- package/package.json +1 -1
- package/peer-instances.mjs +1 -1
- package/permission.mjs +33 -0
- package/process-probe.mjs +2 -2
- package/prompts/advisor-design.md +4 -2
- package/prompts/advisor-round1.md +1 -1
- package/prompts/advisor-round2.md +1 -1
- package/prompts/advisor-round3.md +1 -1
- package/prompts/common.md +38 -4
- package/prompts/consult-base.md +1 -1
- package/prompts/discipline-engineering.md +32 -15
- package/prompts/discipline-normal.md +46 -2
- package/prompts/persona-eng-designer.md +6 -6
- package/prompts/persona-engineering.md +18 -9
- package/prompts/persona-normal.md +2 -0
- package/prompts/subagent-base.md +3 -0
- package/provider/anthropic.mjs +14 -2
- package/provider/core.mjs +6 -0
- package/provider/errors.mjs +28 -3
- package/provider/google.mjs +22 -10
- package/provider/list-models.mjs +3 -2
- package/provider/responses-request.mjs +3 -1
- package/provider/responses.mjs +38 -4
- package/provider/sse.mjs +3 -0
- package/provider-flows.mjs +14 -15
- package/proxy-chunked.mjs +102 -0
- package/proxy-target.mjs +54 -0
- package/proxy-transport.mjs +298 -0
- package/proxy.mjs +17 -248
- package/session-gc.mjs +2 -2
- package/session-lifecycle.mjs +18 -14
- package/session-slot-claims.mjs +2 -2
- package/session-slots-manifest.mjs +1 -1
- package/session-slots.mjs +3 -3
- package/shell-identity.mjs +107 -0
- package/team-ledger.mjs +282 -0
- package/team.mjs +223 -0
- package/tool-docs/advisor.md +1 -1
- package/tool-docs/bash.md +2 -1
- package/tool-docs/batch.md +1 -1
- package/tool-docs/browser.md +48 -0
- package/tool-docs/delete.md +7 -5
- package/tool-docs/ledger.md +6 -0
- package/tool-docs/manifest.md +16 -0
- package/tool-docs/memory.md +1 -0
- package/tool-docs/notify_parent.md +5 -0
- package/tool-docs/process.md +4 -4
- package/tool-docs/question.md +1 -0
- package/tool-docs/subagent.md +14 -3
- package/tool-docs/team_ledger.md +9 -0
- package/tool-docs/timer.md +6 -0
- package/tool-docs/wait_for.md +1 -0
- package/tools/bash.mjs +9 -4
- package/tools/browser.mjs +161 -0
- package/tools/git.mjs +1 -1
- package/tools/index.mjs +4 -4
- package/tools/ops.mjs +26 -10
- package/tools/patch.mjs +8 -2
- package/tools/repomap.mjs +1 -1
- package/tools/write-path.mjs +8 -6
- package/traces/trace-cleanup.mjs +1 -1
- package/traces/trace-store.mjs +1 -1
- package/vision-reader.mjs +10 -16
- package/tool-docs/ledger_add.md +0 -1
- package/tool-docs/ledger_close.md +0 -1
- package/tool-docs/ledger_count.md +0 -1
- package/tool-docs/ledger_query.md +0 -1
- package/tool-docs/ledger_update.md +0 -1
package/proxy.mjs
CHANGED
|
@@ -1,264 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* proxy.mjs —
|
|
3
|
-
*
|
|
2
|
+
* proxy.mjs — 代理出口:`injectProxy`(逐渠注入)∥ `proxyFetch`(统一出口编排;直连分支 = 断流通道建设点)
|
|
3
|
+
* + 兼容再出口(`proxy-target.mjs` ∥ `proxy-transport.mjs`——消费面 import 零改;2026-10-08 拆档 · #1037)。
|
|
4
|
+
* 零依赖:Node built-ins only(url)。
|
|
4
5
|
*/
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { IDLE_ABORT, destroyBody } from "./stream-destroy.mjs";
|
|
6
|
+
import { URL } from "node:url"
|
|
7
|
+
import { normalizeProxy } from "./config.mjs"
|
|
8
|
+
import { IDLE_ABORT } from "./stream-destroy.mjs"
|
|
9
|
+
import { isLoopbackTarget, resolveProxyConfig, resolveWebProxy } from "./proxy-target.mjs"
|
|
10
|
+
import { FETCH_TIMEOUT, streamHttpResponse, tcpConnectProxy, tunnelHttps } from "./proxy-transport.mjs"
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Resolve proxy URI.
|
|
16
|
-
* New format: { proxy: { uri: "http://host:port", web: true, model: false } }
|
|
17
|
-
* Old format: { proxy: "http://host:port" } — backwards compatible, web=true model=false
|
|
18
|
-
* Env vars: HTTPS_PROXY, HTTP_PROXY, ALL_PROXY
|
|
19
|
-
*
|
|
20
|
-
* @returns {{ uri: string|null, web: boolean, model: boolean }}
|
|
21
|
-
*/
|
|
22
|
-
export function resolveProxyConfig(ctx) {
|
|
23
|
-
const cfgProxy = ctx?.agent?.config?.proxy
|
|
24
|
-
if (!cfgProxy) {
|
|
25
|
-
const uri = process.env.HTTPS_PROXY || process.env.HTTP_PROXY || process.env.ALL_PROXY || null
|
|
26
|
-
return { uri, web: !!uri, model: false }
|
|
27
|
-
}
|
|
28
|
-
if (typeof cfgProxy === "string") {
|
|
29
|
-
// Backward compat: bare string → web only
|
|
30
|
-
return { uri: cfgProxy, web: true, model: false }
|
|
31
|
-
}
|
|
32
|
-
return {
|
|
33
|
-
uri: cfgProxy.uri || cfgProxy.url || null,
|
|
34
|
-
web: cfgProxy.web !== false,
|
|
35
|
-
model: cfgProxy.model === true,
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Convenience: resolve proxy URI for web tools (websearch/fetch) */
|
|
40
|
-
export function resolveWebProxy(ctx) {
|
|
41
|
-
const cfg = resolveProxyConfig(ctx)
|
|
42
|
-
return (cfg.uri && cfg.web) ? cfg.uri : null
|
|
43
|
-
}
|
|
12
|
+
export { isLoopbackTarget, resolveProxyConfig, resolveWebProxy, streamHttpResponse, tunnelHttps }
|
|
44
13
|
|
|
45
14
|
/**
|
|
46
15
|
* Inject the resolved proxy URI into each provider as provider.proxyUri
|
|
47
16
|
* (consumed by chat() in provider/core.mjs).
|
|
48
|
-
*
|
|
17
|
+
* 逐渠独立:per-provider `proxy: true` ∧ **在案** `proxy.uri` ⇒ 注入(无全局闸——D-PX1,2026-10-08)。
|
|
18
|
+
* env 回落不供模型代理(D-PX4):盘上 `proxy` 字段缺席 ⇒ 零注入(web 面 env 回落照旧——`resolveWebProxy`)。
|
|
19
|
+
* 判定归一化单源 = `normalizeProxy`(#1082,2026-10-10):`""` ∥ 空对象 ∥ 非法型 ⇒ 零注入(`!= null` 过闸后
|
|
20
|
+
* 的 env 回供理论口封死——模型代理永不吃 env)。
|
|
49
21
|
*/
|
|
50
22
|
export function injectProxy(providers, config) {
|
|
51
|
-
const
|
|
23
|
+
const uri = normalizeProxy(config?.proxy)?.uri ?? null
|
|
52
24
|
for (const p of providers ?? []) {
|
|
53
|
-
p.proxyUri = p.proxy &&
|
|
25
|
+
p.proxyUri = p.proxy && uri ? uri : undefined
|
|
54
26
|
}
|
|
55
27
|
}
|
|
56
28
|
|
|
57
|
-
/**
|
|
58
|
-
* 在已建立的 socket 上发 HTTP 请求,响应头到齐即 resolve(流式)。
|
|
59
|
-
* 返回 Response-like: { ok, status, headers: Headers, body: PassThrough(异步迭代), text(): Promise<string> }
|
|
60
|
-
* body 边收边吐(SSE 流式消费方可逐 chunk 读取);text() 消费流到底(非流式调用方用)。
|
|
61
|
-
* opts.signal 全程有效:abort 即 destroy socket 并 reject/终止流。
|
|
62
|
-
* absoluteForm: 请求行发绝对 URI(http:// 目标的经典代理转发用),默认发 origin-form。
|
|
63
|
-
* 2026-08-31 会诊 #4:timeout 只覆盖"响应头阶段";settle 后 body 空闲 > bodyIdleMs 即
|
|
64
|
-
* 断流(原实现头部 timer 到齐即清,流式中途停摆会无限挂起直到用户 Ctrl+C)。
|
|
65
|
-
* bodyIdleMs 默认 120s(provider 长生成用 0 即禁用,由调用方决定)。
|
|
66
|
-
* 导出供测试(裸 socket,无需 TLS/CONNECT);生产路径走 tunnelHttps / proxyFetch。
|
|
67
|
-
*/
|
|
68
|
-
export function streamHttpResponse(sock, urlStr, opts = {}, timeout = FETCH_TIMEOUT, absoluteForm = false, bodyIdleMs = 120_000) {
|
|
69
|
-
return new Promise((resolve, reject) => {
|
|
70
|
-
const target = new URL(urlStr)
|
|
71
|
-
const method = opts.method ?? "GET"
|
|
72
|
-
const headers = opts.headers ?? {}
|
|
73
|
-
const signal = opts.signal
|
|
74
|
-
|
|
75
|
-
if (signal?.aborted) { sock.destroy(); return reject(abortError(signal, "provider", "proxy")) }
|
|
76
|
-
|
|
77
|
-
const body = new PassThrough()
|
|
78
|
-
let settled = false
|
|
79
|
-
let headerBuf = ""
|
|
80
|
-
let idleTimer = null
|
|
81
|
-
|
|
82
|
-
const timer = setTimeout(() => fail(timeoutError("Response timeout", "provider", "proxy-header")), timeout)
|
|
83
|
-
const onAbort = () => { sock.destroy(); fail(abortError(signal, "provider", "proxy")) }
|
|
84
|
-
signal?.addEventListener("abort", onAbort, { once: true })
|
|
85
|
-
|
|
86
|
-
function cleanup() {
|
|
87
|
-
clearTimeout(timer)
|
|
88
|
-
clearTimeout(idleTimer)
|
|
89
|
-
signal?.removeEventListener("abort", onAbort)
|
|
90
|
-
}
|
|
91
|
-
/** 头部阶段失败 reject;resolve 后失败则终止 body 流(for-await 抛出,不挂起) */
|
|
92
|
-
function fail(err) {
|
|
93
|
-
cleanup()
|
|
94
|
-
if (!settled) { settled = true; reject(err) }
|
|
95
|
-
else destroyBody(body, err)
|
|
96
|
-
}
|
|
97
|
-
/** body 阶段空闲看门狗:每次数据到达重置;无数据超时 → 断流(流式消费方抛错) */
|
|
98
|
-
function armIdle() {
|
|
99
|
-
clearTimeout(idleTimer)
|
|
100
|
-
if (bodyIdleMs > 0) idleTimer = setTimeout(() => destroyBody(body, timeoutError("Response body timeout (idle)", "provider", "proxy-body-idle")), bodyIdleMs)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
sock.on("data", (d) => {
|
|
104
|
-
if (settled) return // 理论上不会发生(settle 后摘掉本监听器),防御
|
|
105
|
-
headerBuf += d.toString("utf8")
|
|
106
|
-
const idx = headerBuf.indexOf("\r\n\r\n")
|
|
107
|
-
if (idx < 0) return
|
|
108
|
-
|
|
109
|
-
const headerText = headerBuf.slice(0, idx)
|
|
110
|
-
const statusMatch = headerText.match(/^HTTP\/\d\.\d (\d+)/)
|
|
111
|
-
const status = statusMatch ? Number(statusMatch[1]) : 502
|
|
112
|
-
const respHeaders = {}
|
|
113
|
-
for (const line of headerText.split("\r\n").slice(1)) {
|
|
114
|
-
const ci = line.indexOf(":")
|
|
115
|
-
if (ci > 0) respHeaders[line.slice(0, ci).trim().toLowerCase()] = line.slice(ci + 1).trim()
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// 头到齐:摘掉头阶段监听,剩余字节推入 body,后续数据直接 pipe
|
|
119
|
-
sock.removeAllListeners("data")
|
|
120
|
-
settled = true
|
|
121
|
-
cleanup()
|
|
122
|
-
armIdle()
|
|
123
|
-
const remaining = headerBuf.slice(idx + 4)
|
|
124
|
-
if (remaining) body.write(Buffer.from(remaining, "utf8"))
|
|
125
|
-
sock.pipe(body)
|
|
126
|
-
// 空闲看门狗随数据到达重置。注意必须用 'readable' 而非 'data':
|
|
127
|
-
// 'data' 监听把流切成 flowing 模式,会抢走 readSSE/for-await 未来得及认领的
|
|
128
|
-
// 已写缓冲(2026-08-31 实测:分包响应头的 remaining 首段被吞,readSSE 等不到 ack)。
|
|
129
|
-
// 'readable' 不改变流模式,数据仍由消费方按需拉取。
|
|
130
|
-
body.on("readable", armIdle)
|
|
131
|
-
// body 结束后才移除 abort 监听(流式中途 abort 要能终止流);空闲看门狗一并清
|
|
132
|
-
body.on("close", () => {
|
|
133
|
-
clearTimeout(idleTimer)
|
|
134
|
-
signal?.removeEventListener("abort", onAbort)
|
|
135
|
-
})
|
|
136
|
-
signal?.addEventListener("abort", onAbort, { once: true })
|
|
137
|
-
|
|
138
|
-
resolve({
|
|
139
|
-
ok: status >= 200 && status < 400,
|
|
140
|
-
status,
|
|
141
|
-
headers: new Headers(respHeaders),
|
|
142
|
-
body,
|
|
143
|
-
text: async () => {
|
|
144
|
-
const chunks = []
|
|
145
|
-
for await (const c of body) chunks.push(c)
|
|
146
|
-
return Buffer.concat(chunks).toString("utf8")
|
|
147
|
-
},
|
|
148
|
-
})
|
|
149
|
-
})
|
|
150
|
-
sock.on("close", () => {
|
|
151
|
-
if (!settled) fail(new Error("Connection closed before response"))
|
|
152
|
-
else body.end()
|
|
153
|
-
})
|
|
154
|
-
// 头部阶段的传输错误统一包装为稳定契约(对端 RST 会抛原生 ECONNRESET,调用方难判别);
|
|
155
|
-
// resolve 后的 body 阶段保留原始错误终止流
|
|
156
|
-
sock.on("error", (e) => {
|
|
157
|
-
if (!settled) fail(new Error(`Connection closed before response (${e.code ?? e.message})`))
|
|
158
|
-
else fail(e)
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
// 写请求(absoluteForm:代理转发时请求行为绝对 URI)
|
|
162
|
-
const requestTarget = absoluteForm ? urlStr : `${target.pathname}${target.search}`
|
|
163
|
-
const lines = [`${method} ${requestTarget} HTTP/1.1`]
|
|
164
|
-
for (const [k, v] of Object.entries({ ...headers, Host: target.hostname })) lines.push(`${k}: ${v}`)
|
|
165
|
-
lines.push("Connection: close", "", "")
|
|
166
|
-
sock.write(lines.join("\r\n"))
|
|
167
|
-
if (opts.body) sock.write(opts.body)
|
|
168
|
-
})
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* HTTPS request through HTTP CONNECT proxy tunnel.
|
|
173
|
-
* CONNECT + TLS 建立后交给 streamHttpResponse — 响应头到齐即 resolve,body 为流式。
|
|
174
|
-
* 2026-08-31 会诊 #1/#4:
|
|
175
|
-
* - TLS 默认全量证书校验(rejectUnauthorized: true)——走代理的流量(含 API key)
|
|
176
|
-
* 不得在未验证链路上传输;确需自签/内网代理时 opts.insecureTls=true 显式放行。
|
|
177
|
-
* - CONNECT/TLS 阶段用 timeout(默认 15s,建隧道快);**响应头超时**用
|
|
178
|
-
* opts._headerTimeoutMs(默认 60s)——原实现共用 15s,DeepSeek 排队 TTFB>15s
|
|
179
|
-
* 即误报 "Response timeout",与直连 600s 语义割裂。
|
|
180
|
-
*/
|
|
181
|
-
export function tunnelHttps(urlStr, opts, proxyUri, timeout = FETCH_TIMEOUT) {
|
|
182
|
-
return new Promise((resolve, reject) => {
|
|
183
|
-
let target, proxy
|
|
184
|
-
try {
|
|
185
|
-
target = new URL(urlStr)
|
|
186
|
-
proxy = new URL(proxyUri)
|
|
187
|
-
} catch {
|
|
188
|
-
// 融合(§2.5 #74——取双侧并集):VSC 侧的**坏代理串友好报错**并入核内——
|
|
189
|
-
// 原生 `Invalid URL` 不解释期望形态,调用方拿到的可观测文案退化。
|
|
190
|
-
return reject(new Error(`Invalid proxy URI: "${proxyUri}" — expected http://host:port`))
|
|
191
|
-
}
|
|
192
|
-
const signal = opts?.signal
|
|
193
|
-
const headerTimeoutMs = Number.isFinite(opts?._headerTimeoutMs) ? opts._headerTimeoutMs : 60_000
|
|
194
|
-
const bodyIdleMs = opts?._bodyIdleMs ?? 120_000
|
|
195
|
-
|
|
196
|
-
if (signal?.aborted) return reject(abortError(signal, "provider", "proxy"))
|
|
197
|
-
|
|
198
|
-
const sock = connect({ host: proxy.hostname, port: Number(proxy.port) || 3128 })
|
|
199
|
-
const timer = setTimeout(() => { sock.destroy(); reject(new Error("Proxy CONNECT timeout")) }, timeout)
|
|
200
|
-
const onAbort = () => { sock.destroy(); reject(abortError(signal, "provider", "proxy")) }
|
|
201
|
-
signal?.addEventListener("abort", onAbort, { once: true })
|
|
202
|
-
sock.on("connect", () => sock.write(`CONNECT ${target.hostname}:${target.port || 443} HTTP/1.1\r\nHost: ${target.hostname}\r\n\r\n`))
|
|
203
|
-
|
|
204
|
-
let buf = ""
|
|
205
|
-
sock.on("data", d => {
|
|
206
|
-
buf += d.toString()
|
|
207
|
-
const end = buf.indexOf("\r\n\r\n")
|
|
208
|
-
if (end < 0) return
|
|
209
|
-
const statusLine = buf.slice(0, end).split("\r\n")[0]
|
|
210
|
-
buf = buf.slice(end + 4)
|
|
211
|
-
if (!statusLine.includes("200")) { sock.destroy(); clearTimeout(timer); return reject(new Error(`Proxy CONNECT: ${statusLine}`)) }
|
|
212
|
-
sock.removeAllListeners("data"); clearTimeout(timer)
|
|
213
|
-
|
|
214
|
-
// 安全默认:TLS 全量校验。企业中间人代理场景显式 opts.insecureTls=true 才放行。
|
|
215
|
-
const tlsSock = tlsConnect({ socket: sock, servername: target.hostname, rejectUnauthorized: opts?.insecureTls !== true })
|
|
216
|
-
if (buf) tlsSock.unshift(Buffer.from(buf))
|
|
217
|
-
tlsSock.on("secureConnect", () => {
|
|
218
|
-
// TLS 之后的请求/响应阶段:abort 交由 streamHttpResponse 接管
|
|
219
|
-
signal?.removeEventListener("abort", onAbort)
|
|
220
|
-
streamHttpResponse(tlsSock, urlStr, opts, headerTimeoutMs, false, bodyIdleMs).then(resolve, reject)
|
|
221
|
-
})
|
|
222
|
-
tlsSock.on("error", e => { sock.destroy(); reject(e) })
|
|
223
|
-
})
|
|
224
|
-
sock.on("error", e => { clearTimeout(timer); reject(new Error(`Proxy CONNECT failed (${e.code ?? e.message})`)) })
|
|
225
|
-
// 代理干净 FIN 关闭(无 error)时也要 reject,不能卡满超时
|
|
226
|
-
sock.on("close", () => { clearTimeout(timer); reject(new Error("Proxy connection closed before tunnel established")) })
|
|
227
|
-
})
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/** TCP 直连代理(http:// 目标的经典转发用):超时/abort/对端关闭都有稳定 reject */
|
|
231
|
-
function tcpConnectProxy(proxyUri, signal, timeout) {
|
|
232
|
-
return new Promise((resolve, reject) => {
|
|
233
|
-
let proxy
|
|
234
|
-
try {
|
|
235
|
-
proxy = new URL(proxyUri)
|
|
236
|
-
} catch {
|
|
237
|
-
// 融合(§2.5 #74):同 tunnelHttps——坏代理串友好报错。
|
|
238
|
-
return reject(new Error(`Invalid proxy URI: "${proxyUri}" — expected http://host:port`))
|
|
239
|
-
}
|
|
240
|
-
const sock = connect({ host: proxy.hostname, port: Number(proxy.port) || 3128 })
|
|
241
|
-
if (signal?.aborted) { sock.destroy(); return reject(abortError(signal, "provider", "proxy")) }
|
|
242
|
-
const onAbort = () => sock.destroy()
|
|
243
|
-
signal?.addEventListener("abort", onAbort, { once: true })
|
|
244
|
-
const timer = setTimeout(() => { sock.destroy(); reject(new Error("Proxy CONNECT timeout")) }, timeout)
|
|
245
|
-
const onError = (e) => { clearTimeout(timer); reject(new Error(`Proxy CONNECT failed (${e.code ?? e.message})`)) }
|
|
246
|
-
const onClose = () => {
|
|
247
|
-
clearTimeout(timer)
|
|
248
|
-
reject(signal?.aborted ? abortError(signal, "provider", "proxy") : new Error("Proxy connection closed before tunnel established"))
|
|
249
|
-
}
|
|
250
|
-
sock.once("connect", () => {
|
|
251
|
-
clearTimeout(timer)
|
|
252
|
-
signal?.removeEventListener("abort", onAbort)
|
|
253
|
-
sock.removeListener("error", onError)
|
|
254
|
-
sock.removeListener("close", onClose)
|
|
255
|
-
resolve(sock)
|
|
256
|
-
})
|
|
257
|
-
sock.once("error", onError)
|
|
258
|
-
sock.once("close", onClose)
|
|
259
|
-
})
|
|
260
|
-
}
|
|
261
|
-
|
|
262
29
|
/**
|
|
263
30
|
* Generic fetch with proxy support.
|
|
264
31
|
* No proxy → native fetch. Proxy + HTTPS → CONNECT tunnel. Proxy + HTTP → 经典代理转发(绝对 URI 请求行)。
|
|
@@ -267,6 +34,8 @@ function tcpConnectProxy(proxyUri, signal, timeout) {
|
|
|
267
34
|
* (`readSSE` ∥ `parseGeminiStream`)经 `terminateBody` 恢复有效;proxy 两分支不挂(自有 `_bodyIdleMs`)。
|
|
268
35
|
*/
|
|
269
36
|
export async function proxyFetch(urlStr, opts, proxyUri) {
|
|
37
|
+
// Loopback 目标永不经代理(NO_PROXY 语义——#1026:本地网关被塞进代理 ⇒ 403 假红):
|
|
38
|
+
if (proxyUri && isLoopbackTarget(urlStr)) proxyUri = null
|
|
270
39
|
if (!proxyUri) {
|
|
271
40
|
const idleAbort = new AbortController()
|
|
272
41
|
const signal = opts?.signal ? AbortSignal.any([opts.signal, idleAbort.signal]) : idleAbort.signal
|
package/session-gc.mjs
CHANGED
|
@@ -130,7 +130,7 @@ export async function gcResidue({ dir = sessionsDir(), prefix, now = Date.now(),
|
|
|
130
130
|
return result
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
/** ②
|
|
133
|
+
/** ② 显式面清运分支(§6.12 / §6.17 补充——存量通路):全目录孤儿 sidecar 记录目录
|
|
134
134
|
*(`{prefix}.{N}.d`——主文件不存在 ∧ 该槽非活跃)⇒ 逐条**回收**(rename 进 `sessions-trash/<批次>/`,同 ① 语义)。
|
|
135
135
|
* 与 ① 两面之差:① = 自动面(单前缀 / 当前 cwd / **带 30 天保留期**——只治本前缀未来新增);
|
|
136
136
|
* 本函数 = **用户显式面**(跳前缀遍历全目录 / **不设保留期**——存量孤儿跨多前缀且多为近期孤儿,
|
|
@@ -355,7 +355,7 @@ export async function runSessionGc(args, { dir = sessionsDir(), prefix = null, c
|
|
|
355
355
|
}
|
|
356
356
|
out(`Moved ${r.deleted.length} files for ${t.hash} into the recycle bin (${r.batch})${r.skipped?.length ? ` — skipped ${r.skipped.length} (locked/racing, kept in place)` : ""}.`)
|
|
357
357
|
}
|
|
358
|
-
// ②
|
|
358
|
+
// ② 显式面清运分支(孤儿 sidecar 记录目录——存量通路;与 ① 同语义:rename 进回收批)
|
|
359
359
|
if (confirmTarget === "--all" && orphanDirs.candidates.length) {
|
|
360
360
|
const moved = await sweepOrphanRecordDirs({ dir, now, probeFn, entries: loopEntries })
|
|
361
361
|
out(`Orphan record dirs (.d): moved ${moved.recycled.length}/${moved.candidates.length} into the recycle bin (${trashRootFor(dir)}).`)
|
package/session-lifecycle.mjs
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import { existsSync } from "node:fs"
|
|
21
21
|
import { resolveCompactThreshold } from "./config.mjs"
|
|
22
22
|
// 槽面判据单源(#841 KD-841-2:槽渠道 key 门 + 模型面序——PROVIDER.md §6.22)。
|
|
23
|
-
import { resolveProviderPlan } from "./model-ref.mjs"
|
|
23
|
+
import { hasKey, resolveChannelModel, resolveProviderPlan } from "./model-ref.mjs"
|
|
24
24
|
import {
|
|
25
25
|
slotPath, loadManifest, saveManifest, slotDigest, writeSessionFile, getSessionId,
|
|
26
26
|
writeEndMarker, sessionEnd, ownerPid, ownerPids,
|
|
@@ -77,10 +77,10 @@ function stripTruncatedToolArgs(m) {
|
|
|
77
77
|
*
|
|
78
78
|
* MODEL-MERGE-SESSION(F-4)applySession 重写为两支 + 删旧支(评审 #7/#9 措辞——防
|
|
79
79
|
* "三支"误读):
|
|
80
|
-
* ① 槽 provider 存在 → provider/model
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* compactThreshold(auto
|
|
80
|
+
* ① 槽 provider 存在 → provider/model 按槽值设(模型链 = 槽模型 → `defaultModel` 属本渠道 ⇒ 其模型段;
|
|
81
|
+
* 两档皆无 ⇒ `null`——2026-10-09 清除批:原 `providers[].model` 回落退场;`defaultModel` 经
|
|
82
|
+
* `resolveProviderPlan` 参与模型面)+ 重算
|
|
83
|
+
* compactThreshold(auto 时——按恢复后模型);
|
|
84
84
|
* ② 槽 provider 没了 → D-S3 保留——静默保持现状(config 有效则有效——两方都无效由调用侧
|
|
85
85
|
* 复验 validateProvider 弹重选)。
|
|
86
86
|
* 删旧支:"activeModel==null 清 stale override 回渠道默认"——前提 = 渠道默认字段——已随
|
|
@@ -172,8 +172,8 @@ export function applySession(agent, data, opts = {}) {
|
|
|
172
172
|
if (slotPlan.source === "slot") {
|
|
173
173
|
const slotProvider = agent.providers?.find((pr) => pr.name === slotPlan.channel)
|
|
174
174
|
// ① 槽 provider 存在 → 按槽值设(不看 config):双字段恒非空。
|
|
175
|
-
// 模型面序(#841):槽模型 → `defaultModel` 属本渠道 ⇒
|
|
176
|
-
//
|
|
175
|
+
// 模型面序(#841):槽模型 → `defaultModel` 属本渠道 ⇒ 其模型段;两档皆无 ⇒ `null`(不落
|
|
176
|
+
// model 键——下游沿既有「model 缺失」处置;渠道单值模型退场——2026-10-09 清除批)。
|
|
177
177
|
// F-2d(MODEL-400-FIX)径保留:空串 / 非串槽模型同样兜(#638 非串 = 未登记)——slotPlan.model
|
|
178
178
|
// 已按同归一出值(单源,无第二判据)。
|
|
179
179
|
const prevName = agent.activeProvider
|
|
@@ -379,21 +379,25 @@ export function slotOccupancy(cwd, slot) {
|
|
|
379
379
|
* 读数百分数(整数)。与 `applySession` 后读数**同源同式**(四事同判——组合既有件,零新公式):
|
|
380
380
|
* ① 线选 = `contextHistory` 非空取之,否则 `history` 经 `stripTruncatedToolArgs` 回退(同 applySession);
|
|
381
381
|
* ② 回声归并 = `mergeAdjacentAssistantEchoes`(同 applySession 装线前一步);
|
|
382
|
-
* ③ 渠道合并 = `providers` 命中 `data.activeProvider` ⇒
|
|
383
|
-
*
|
|
382
|
+
* ③ 渠道合并 = `providers` 命中 `data.activeProvider` ⇒ 槽模型(串值 `activeModel`;非串 ∕ 空串 = 未登记
|
|
383
|
+
* ⇒ 落链)∥ 无槽模型 ⇒ 现行模型链回落(`defaultModel` 属本渠道 ⇒ 其模型段;两档皆无 ⇒ `null`——
|
|
384
|
+
* `resolveChannelModel`,2026-10-09 清除批:渠道单值模型退场,零静默回落);未命中 ⇒ `fallback`
|
|
385
|
+
* (支 ②「静默保持现状」的装配口径 = `loadConfig().provider`);
|
|
384
386
|
* ④ 公式 = `historyPercent`(`thincoder-core/token-window.mjs`——与 CLI 状态行 / VSC 同式)。
|
|
385
|
-
* **零副作用**:`data` / `providers` / `fallback` 皆入参(不读盘、不写盘、不改任何进程态);
|
|
387
|
+
* **零副作用**:`data` / `providers` / `fallback` / `defaultModel` 皆入参(不读盘、不写盘、不改任何进程态);
|
|
386
388
|
* `data` 非对象 ⇒ `0`(与空历史同值——「非正 ⇒ 零节点」显示门归端侧,沿 `historyPercent` 空历史口径)。 */
|
|
387
|
-
export function sessionReading(data, { providers, fallback } = {}) {
|
|
389
|
+
export function sessionReading(data, { providers, fallback, defaultModel } = {}) {
|
|
388
390
|
if (data === null || typeof data !== "object") return 0
|
|
389
391
|
const full = Array.isArray(data.history) ? data.history : []
|
|
390
392
|
const ch = data.contextHistory
|
|
391
393
|
const machine = (Array.isArray(ch) && ch.length > 0) ? ch : full.map(stripTruncatedToolArgs)
|
|
392
394
|
const line = mergeAdjacentAssistantEchoes(machine)
|
|
393
|
-
const entry = data.activeProvider ? providers?.find((pr) => pr.name === data.activeProvider) : null
|
|
394
|
-
// #638:非串 `activeModel` 归一(与 applySession 模型合并支 ① 同判)——脏值不达 `historyPercent
|
|
395
|
+
const entry = data.activeProvider ? providers?.find((pr) => pr.name === data.activeProvider && hasKey(pr)) : null
|
|
396
|
+
// #638:非串 ∕ 空串 `activeModel` 归一(与 applySession 模型合并支 ① 同判)——脏值不达 `historyPercent`;
|
|
397
|
+
// 未登记 ⇒ 落现行模型链(`defaultModel` 属本渠段 ∥ `null`——渠道单值退场,2026-10-09 清除批)。
|
|
398
|
+
const slotModel = (typeof data.activeModel === "string" && data.activeModel) ? data.activeModel : null
|
|
395
399
|
const provider = entry
|
|
396
|
-
? { ...entry, model:
|
|
400
|
+
? { ...entry, model: slotModel ?? resolveChannelModel(entry, defaultModel ?? null) }
|
|
397
401
|
: fallback
|
|
398
402
|
return historyPercent(line, provider)
|
|
399
403
|
}
|
package/session-slot-claims.mjs
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* `unknown`(探测失败 / 缺行)⇒ 不认领 / 不判死 / 不删(D-MI10)。
|
|
17
17
|
*/
|
|
18
18
|
import { existsSync } from "node:fs"
|
|
19
|
-
// 探测束(判活)+ 属主三态判据 / 死主判定(本档零自有 exec
|
|
19
|
+
// 探测束(判活)+ 属主三态判据 / 死主判定(本档零自有 exec——判据与探测面分离)。
|
|
20
20
|
import { probeOwnersSync, ownerState, filterDeadOwners } from "./process-probe.mjs"
|
|
21
21
|
// 存储原语(路径 / 原子写 / 进程 sessionId)——静态环见头注。
|
|
22
22
|
import { getSessionId, manifestPath, slotPath } from "./session-slots.mjs"
|
|
@@ -210,7 +210,7 @@ export const _releaseStats = { calls: 0 }
|
|
|
210
210
|
* 早退面:磁盘无 manifest ⇒ `existsSync` 单条目早退**零写**(不造盘面);本进程无认领 ⇒
|
|
211
211
|
* 零写返回 false。失败容忍(F-XR1「退出恒达」):整体 try/catch **永不抛出**,返回 boolean
|
|
212
212
|
* (true = 有释放且落盘成功;false = 早退 / 失败 / **写面拒写**——`saveManifest` 返回值透传,
|
|
213
|
-
* §6.23 判据句 3
|
|
213
|
+
* §6.23 判据句 3——断言);写失败盘面 = 认领残留 →
|
|
214
214
|
* 恢复走既有探测面(现状形态,数据零险)。 */
|
|
215
215
|
export function releaseClaimsAll(cwd) {
|
|
216
216
|
_releaseStats.calls += 1
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* session-slots-manifest.mjs — 清单 / 认领 / 属主面(init-block 批 · F-MI7 拆分:
|
|
3
3
|
* `session-slots.mjs` 探测束接线后实读 517 行,破 490 判定线并越 500 硬限 ⇒ 按
|
|
4
|
-
* `CORE-UNIFICATION.md` §2.8.1
|
|
4
|
+
* `CORE-UNIFICATION.md` §2.8.1 子表原行 3(已随 2026-10-08 批清账)随批执行外提;各函数语义**原样**迁移):
|
|
5
5
|
* 摘要面(`extractSlotMeta` / `slotDigest`)· 清单读写(`loadManifest` / `saveManifest`)·
|
|
6
6
|
* 属主面(`ownerPid` / `ownerPids` / `ownerStateOf` / `ownerArgs` / `cleanDeadOwners`)·
|
|
7
7
|
* 认领面(`ensureActive` / `allocateFresh` / `claimSlot` / `activeSlot` / **认领释放集
|
package/session-slots.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* re-export 全部导出以保持既有 import 兼容)。
|
|
5
5
|
*
|
|
6
6
|
* init-block 批 · F-MI7 二次拆分:探测束接线后本档实读 517 行(破 490 判定线并越 500 硬限)
|
|
7
|
-
* ⇒ 按 `CORE-UNIFICATION.md` §2.8.1
|
|
7
|
+
* ⇒ 按 `CORE-UNIFICATION.md` §2.8.1 子表原行 3(已随 2026-10-08 批清账)**随批**外提清单 / 认领 / 属主面至
|
|
8
8
|
* `session-slots-manifest.mjs`(语义原样迁移;本档 re-export 该档导出保既有 import 面——
|
|
9
9
|
* 消费档 import 路径与名面不动)。本档保留 = 存储原语(cwd 哈希 / 路径 / 原子写 / 进程
|
|
10
10
|
* sessionId)· 端记录(end marker)· 列表面(listSlots——条目集 = 盘面实读;扫描面住
|
|
@@ -33,14 +33,14 @@ import { join, dirname, basename } from "node:path"
|
|
|
33
33
|
import { configDir } from "./config.mjs"
|
|
34
34
|
import { migrateHashLength } from "./session-migrate.mjs"
|
|
35
35
|
// §6.10 D-2(2026-09-05):resumeSlot 决策归本文件(slot/claim 层);data 层读(loadSlotFile/
|
|
36
|
-
// legacy 过滤)属 session.mjs(
|
|
36
|
+
// legacy 过滤)属 session.mjs(800 行内不迁移)——静态环(本档 ↔ session.mjs;init-block 批起
|
|
37
37
|
// 另加本档 ↔ session-slots-manifest.mjs,见头注与 L38-47):函数声明实例化期已初始化、只函数
|
|
38
38
|
// 体内运行时使用(环安全);VS Code 端 session-slots ↔ session-io 同构镜像。
|
|
39
39
|
import { loadSlotFile, isLegacyTransient } from "./session.mjs"
|
|
40
40
|
// TUI-OOM-ROOTCAUSE 批(SESSION.md §6.14 生命周期联动):删槽联动记录存储(store 零项目内依赖——无环)。
|
|
41
41
|
import { unlinkRecordStore } from "./session-store.mjs"
|
|
42
42
|
// 探测束(init-block 批 · F-MI7):实现 / 判据单源住 process-probe.mjs——本档零自有 exec、
|
|
43
|
-
// 零逐 pid
|
|
43
|
+
// 零逐 pid 探测(判据与探测面分离);单 pid 判活 re-export 见下方「判活兼容面」。
|
|
44
44
|
import { probeOwnersAsync, isProcessAlive } from "./process-probe.mjs"
|
|
45
45
|
// 清单 / 认领 / 属主面(init-block 批外提——双向静态环见头注):本档 import 取用 + re-export
|
|
46
46
|
// 保既有 import 面(消费档路径与名面不动)。
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* shell-identity.mjs — bash 工具**执行器身份**探测单源(bash-executor-face 批 · 台账 #922——
|
|
3
|
+
* 设计权威 = `docs/core/design/BASH-EXECUTOR-FACE.md` §2 契约 + §3.1)。
|
|
4
|
+
*
|
|
5
|
+
* 探测源 = **spawn 同链镜像**(`tools/bash.mjs` spawn 的 `shell` 参数计算式):
|
|
6
|
+
* config.shell(trim 后非空即用)→ win32 %COMSPEC% → 兜底字面 `cmd.exe`;
|
|
7
|
+
* 非 win32 = `process.env.SHELL` → 兜底 `/bin/sh`(Node `shell:true` 兜底的事实陈述,非平台推断)。
|
|
8
|
+
* 探测值 = 实际执行器(不是环境快照);探测不到(识别表未命中知名字)⇒ `unknown` 中性句
|
|
9
|
+
* (fail-open 零猜测——用户 2026-10-04 22:48 裁定③);**零静态平台默认**(裁定①)。
|
|
10
|
+
*
|
|
11
|
+
* memo(进程生命周期 + 按配置值键控)先例 = `shell-candidates.mjs`(「shell 路径不热变化」同判据);
|
|
12
|
+
* 配置值变化 ⇒ memo 未命中重探(K2 配置变更刷新 ∥ 裁定①零静态同时成立)。
|
|
13
|
+
* 测试缝 `_setComSpecForTest` 先例 = `shell-candidates.mjs:34` `_setShellDetectForTest`。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// 识别表(raw 尾段词 → 显示名 + 语法族)。匹配大小写不敏感,尾段词先剥 `.exe` 后缀(U3——
|
|
17
|
+
// `bash.exe` ∥ `cmd.exe` ∥ `pwsh.exe` 均须命中);序 = 最特异优先:
|
|
18
|
+
// `pwsh` 先于 `powershell`;`sh` 排末(防 `fish` 尾段误命中——fish 自目在前)。
|
|
19
|
+
const IDENTITY_TABLE = Object.freeze([
|
|
20
|
+
{ word: "pwsh", name: "PowerShell (pwsh)", kind: "powershell" },
|
|
21
|
+
{ word: "powershell", name: "Windows PowerShell", kind: "powershell" },
|
|
22
|
+
{ word: "cmd", name: "cmd.exe", kind: "cmd" },
|
|
23
|
+
{ word: "bash", name: "bash", kind: "posix" },
|
|
24
|
+
{ word: "wsl", name: "wsl", kind: "posix" },
|
|
25
|
+
{ word: "fish", name: "fish", kind: "posix" },
|
|
26
|
+
{ word: "zsh", name: "zsh", kind: "posix" },
|
|
27
|
+
{ word: "sh", name: "sh", kind: "posix" },
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
/** 尾段词:raw 的最后一段非空词,剥尾 `.exe`(`C:\\...\\bash.exe` → `bash`;`pwsh` → `pwsh`)。 */
|
|
31
|
+
function lastWord(raw) {
|
|
32
|
+
const parts = String(raw).trim().split(/[\\/:]+/).filter(Boolean)
|
|
33
|
+
const tail = parts[parts.length - 1] ?? ""
|
|
34
|
+
return tail.toLowerCase().replace(/\.exe$/i, "")
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 探测执行器身份。`configShell` = spawn 侧同一取值(`ctx.agent?.config?.shell ?? null`)。
|
|
39
|
+
* 纯函数(不读环境外的状态;win32 分支内读 `%COMSPEC%`);memo 按配置值键控。
|
|
40
|
+
* @param {string|null|undefined} configShell
|
|
41
|
+
* @returns {{ raw: string, name: string, kind: "cmd"|"powershell"|"posix"|"unknown", known: boolean }}
|
|
42
|
+
*/
|
|
43
|
+
export function resolveShellIdentity(configShell) {
|
|
44
|
+
const key = configShell == null ? null : String(configShell)
|
|
45
|
+
if (_memo.key === key) return _memo.value
|
|
46
|
+
|
|
47
|
+
// 链 = spawn 侧同式:config.shell trim 后非空即用 → 空/缺省时 win32 读 %COMSPEC%(兜底字面
|
|
48
|
+
// cmd.exe)∥ 非 win32 读 SHELL(兜底 /bin/sh)——探测值 = 实际执行器。
|
|
49
|
+
const trimmed = key == null ? "" : key.trim()
|
|
50
|
+
let raw
|
|
51
|
+
if (trimmed) {
|
|
52
|
+
raw = trimmed
|
|
53
|
+
} else if (process.platform === "win32") {
|
|
54
|
+
const comspec = _comSpecOverride !== null ? _comSpecOverride : process.env.ComSpec
|
|
55
|
+
raw = comspec && comspec.trim() ? comspec : "cmd.exe"
|
|
56
|
+
} else {
|
|
57
|
+
const shell = process.env.SHELL
|
|
58
|
+
raw = shell && shell.trim() ? shell : "/bin/sh"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const lw = lastWord(raw).toLowerCase()
|
|
62
|
+
// pwsh 分流总则(设计档 §2):powershell 族值面按 raw 尾段词命中 /pwsh/i 分流——
|
|
63
|
+
// 命中 ⇒ pwsh 形;未命中 ⇒ Windows PowerShell(5.x)形。kind 枚举不动(识别表单目)。
|
|
64
|
+
const pwshForm = /pwsh/i.test(lw)
|
|
65
|
+
const hit =
|
|
66
|
+
IDENTITY_TABLE.find((e) => lw === e.word) ??
|
|
67
|
+
(pwshForm ? null : IDENTITY_TABLE.find((e) => lw.endsWith(e.word)))
|
|
68
|
+
const value = hit
|
|
69
|
+
? { raw, name: hit.name, kind: hit.kind, known: true }
|
|
70
|
+
: { raw, name: raw, kind: "unknown", known: false }
|
|
71
|
+
_memo.key = key
|
|
72
|
+
_memo.value = value
|
|
73
|
+
return value
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** memo(进程生命周期 ∕ 按配置值键控——本档单点持有)。 */
|
|
77
|
+
const _memo = { key: undefined, value: null }
|
|
78
|
+
|
|
79
|
+
/** 测试缝:注入 %COMSPEC% 读数(undefined = 复位真实环境读数);注入即清 memo。 */
|
|
80
|
+
let _comSpecOverride = null
|
|
81
|
+
export function _setComSpecForTest(value) {
|
|
82
|
+
_comSpecOverride = value === undefined ? null : value
|
|
83
|
+
_memo.key = undefined
|
|
84
|
+
_memo.value = null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* kind ⇒ 注入值面(一行 Executor 声明 · 设计档 §2 值面表逐字)。powershell 族按 raw 尾段词
|
|
89
|
+
* `/pwsh/i` 分流(pwsh 形 ∥ Windows PowerShell 5.x 形);identity.kind 为 unknown ⇒ 中性句。
|
|
90
|
+
* @param {{ raw: string, name: string, kind: string, known: boolean }} identity resolveShellIdentity 出参
|
|
91
|
+
* @returns {string}
|
|
92
|
+
*/
|
|
93
|
+
export function executorLine(identity) {
|
|
94
|
+
const { raw, name, kind } = identity
|
|
95
|
+
switch (kind) {
|
|
96
|
+
case "cmd":
|
|
97
|
+
return "- Executor: the bash tool runs commands through Windows cmd.exe (%COMSPEC%). Use cmd syntax: && works; NUL not /dev/null; %VAR% not $VAR; no single-quote grouping. For complex logic prefer the execute tool (node)."
|
|
98
|
+
case "powershell":
|
|
99
|
+
return /pwsh/i.test(lastWord(raw))
|
|
100
|
+
? `- Executor: the bash tool runs commands through PowerShell (pwsh; ${raw}). && and || work; %VAR% does not expand (use $VAR); cmdlets differ (Remove-Item, Copy-Item). For complex logic prefer the execute tool (node).`
|
|
101
|
+
: `- Executor: the bash tool runs commands through Windows PowerShell (${raw}). PS 5.1 has NO &&/|| (use ; or separate calls); redirection differs (2>/dev/null is invalid); cmdlets differ (Remove-Item, Copy-Item). For complex logic prefer the execute tool (node).`
|
|
102
|
+
case "posix":
|
|
103
|
+
return `- Executor: the bash tool runs commands through a POSIX shell (${raw}). Standard POSIX syntax applies ($(...), $VAR, ;, >/dev/null).`
|
|
104
|
+
default:
|
|
105
|
+
return `- Executor: the bash tool shell could not be identified (raw: ${raw}) — write portable commands and trust the command's own error output.`
|
|
106
|
+
}
|
|
107
|
+
}
|