@lqc123qwe/car-runtime 1.3.0 → 1.5.0
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/.github/workflows/ci.yml +12 -0
- package/README.md +19 -3
- package/package.json +4 -1
- package/src/cli.ts +178 -15
- package/src/dx/doctor.ts +52 -3
- package/src/load/config.ts +247 -63
- package/src/load/loader.ts +21 -1
- package/src/loop/stop.ts +12 -1
- package/src/mcp/httpClient.ts +176 -0
- package/src/mcp/stdioClient.ts +75 -0
- package/src/ptc/runCode.ts +10 -0
- package/src/runtime-core/chatStep.ts +19 -5
- package/src/runtime-core/credentials.ts +6 -1
- package/src/runtime-core/llm.ts +291 -61
- package/src/runtime-core/telemetry.ts +42 -6
- package/src/runtime-core/tools.ts +27 -0
- package/src/runtime-core/trace.ts +63 -0
- package/src/runtime-core/types.ts +4 -1
- package/src/session/log.ts +3 -2
- package/test/contract/eventmap.golden.json +1 -1
- package/test/s26-signature-closeout.spec.ts +1 -1
- package/test/s28-session-index-followup.spec.ts +1 -1
- package/test/s31-real-model-wiring.spec.ts +329 -0
- package/test/s32-telemetry-mcp-ptc.spec.ts +218 -0
- package/test/s33-anthropic-adapter.spec.ts +354 -0
- package/test/s34-mcp-remote-transport.spec.ts +250 -0
- package/test/s35-config-ts-zod.spec.ts +145 -0
package/src/load/config.ts
CHANGED
|
@@ -1,25 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 1.1-S3
|
|
2
|
+
* 1.1-S3 签名配置文件通道 → 1.5-S3 完整形态(car.config.ts + zod,D-22)
|
|
3
3
|
*
|
|
4
4
|
* 口径:
|
|
5
|
-
* -
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
5
|
+
* - 载体(D-22):发现序 **car.config.ts > car.config.json**(同目录两者并存 = ts 胜出,
|
|
6
|
+
* json 忽略——禁静默合并);显式 --config 按扩展名分派(.ts 走 TS 载体,其余 JSON 解析);
|
|
7
|
+
* TS 载体 = 动态 import()(Node 原生类型剥离,插件加载同纪律——jiti 继续登记不引入);
|
|
8
|
+
* 形态 = `export default {…}`(非对象/缺 default 显式报错)。
|
|
9
|
+
* - **预热协议**:loadCarConfig 保持同步签名(doctor/resolveSigGate 等既有调用面不动);
|
|
10
|
+
* car.config.ts 须先经 warmCarConfig 异步预热入缓存(CLI 入口统一预热),未预热显式报错
|
|
11
|
+
* (fail-visible,禁静默回退 json——「配置写了但不生效」的漂移面必须显式化)。
|
|
12
|
+
* - 校验层 zod 化(D-11a ① 修订):zod ^3 为首个常规运行时依赖(用户点名登记项);
|
|
13
|
+
* fail-visible 语义与 CAR-E-CONFIG 错误码保持,zod issues 渲染为 `路径 原因`(首个 issue);
|
|
14
|
+
* 键位空间与 1.1/1.4 完全一致(sandbox.* / llm.* / mcp.*;mcp.servers 本迭代加 url/headers)。
|
|
15
|
+
* 未登记键 / 类型错 / 坏 JSON / 坏 TS 一律 CAR-E-CONFIG 显式拒绝(禁静默忽略)。
|
|
16
|
+
* - 优先级(1.1 规划 §3 S3):flag > env > 配置文件 > 缺省。env 已定义即为显式意见。
|
|
15
17
|
* 无配置文件 + 无 env 时行为与 1.0 完全一致(warn / 无信任根 / 不豁免)。
|
|
16
|
-
* - 本模块只读配置与合并优先级;门禁判定在 sigGate/verifier。发现序:显式路径 > cwd
|
|
18
|
+
* - 本模块只读配置与合并优先级;门禁判定在 sigGate/verifier。发现序:显式路径 > cwd。
|
|
19
|
+
* - 代码求值面(D-22 登记):car.config.ts 为本机文件信任边界(与插件同域;配置无签名机制)。
|
|
17
20
|
*/
|
|
18
21
|
import { existsSync, readFileSync } from 'node:fs'
|
|
19
22
|
import { isAbsolute, join } from 'node:path'
|
|
23
|
+
import { pathToFileURL } from 'node:url'
|
|
24
|
+
import { z } from 'zod'
|
|
20
25
|
import type { SignatureGateOptions } from './sigGate.ts'
|
|
21
26
|
|
|
22
27
|
export const CONFIG_FILENAME = 'car.config.json'
|
|
28
|
+
export const CONFIG_TS_FILENAME = 'car.config.ts'
|
|
23
29
|
|
|
24
30
|
export interface SandboxSigConfig {
|
|
25
31
|
unsignedAllow?: boolean
|
|
@@ -27,84 +33,234 @@ export interface SandboxSigConfig {
|
|
|
27
33
|
sigTrustRoot?: string
|
|
28
34
|
}
|
|
29
35
|
|
|
36
|
+
export interface LlmConfig {
|
|
37
|
+
baseUrl?: string
|
|
38
|
+
model?: string
|
|
39
|
+
adapterId?: string
|
|
40
|
+
maxTokens?: number
|
|
41
|
+
allowEnvFallback?: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface McpServerSpec {
|
|
45
|
+
/** stdio 通道启动命令(与 url 互斥——1.5-S2 起二选一显式登记) */
|
|
46
|
+
command?: string
|
|
47
|
+
args?: string[]
|
|
48
|
+
/** 启动环境变量(禁明文凭据——gateway.containsPlaintextCredential 注册时强制检查;仅 command 通道适用) */
|
|
49
|
+
env?: Record<string, string>
|
|
50
|
+
/** 1.5-S2(D-21):远程 Streamable HTTP 通道(与 command 互斥) */
|
|
51
|
+
url?: string
|
|
52
|
+
/** 1.5-S2(D-21):远程请求头;值支持 ${ENV_VAR} 引用(凭据禁令 CR-05;仅 url 通道适用) */
|
|
53
|
+
headers?: Record<string, string>
|
|
54
|
+
}
|
|
55
|
+
|
|
30
56
|
export interface CarConfig {
|
|
31
57
|
sandbox?: SandboxSigConfig
|
|
58
|
+
/** 1.4-S3(D-16):模型接入配置键位空间;凭据禁入(CR-05)——只放端点/模型名/开关 */
|
|
59
|
+
llm?: LlmConfig
|
|
60
|
+
/** 1.4-S7(D-18):MCP server 登记面(--mcp <id> 显式启用) */
|
|
61
|
+
mcp?: { servers: Record<string, McpServerSpec> }
|
|
32
62
|
}
|
|
33
63
|
|
|
34
64
|
export interface ConfigLoadResult {
|
|
35
65
|
/** 实际读取的配置文件路径(cwd 未发现时缺省——未发现是正常态非错误) */
|
|
36
66
|
path?: string
|
|
37
67
|
config: CarConfig
|
|
38
|
-
/** fail-visible:显式路径不存在 / 坏 JSON / 未登记键 / 类型错的原因(非空 = 调用方必须中止) */
|
|
68
|
+
/** fail-visible:显式路径不存在 / 坏 JSON / 坏 TS / 未登记键 / 类型错的原因(非空 = 调用方必须中止) */
|
|
39
69
|
error?: string
|
|
40
70
|
}
|
|
41
71
|
|
|
42
|
-
|
|
43
|
-
|
|
72
|
+
// ==================== zod 校验层(1.5-S3 迁移;错误消息含 s31/s34 回归锚点) ====================
|
|
73
|
+
|
|
74
|
+
// 叶子消息不含路径——渲染层统一拼 `${path} ${message}`(s31 锚点:'llm.maxTokens 必须为正整数')
|
|
75
|
+
const nonEmptyTrimmed = () =>
|
|
76
|
+
z.string({ invalid_type_error: '必须为非空字符串', required_error: '必须为非空字符串' })
|
|
77
|
+
.trim()
|
|
78
|
+
.refine(v => v.length > 0, { message: '必须为非空字符串' })
|
|
79
|
+
|
|
80
|
+
const stringMap = () =>
|
|
81
|
+
z.record(z.string(), z.string({ invalid_type_error: '必须为字符串' }), { invalid_type_error: '必须为字符串映射', required_error: '必须为字符串映射' })
|
|
82
|
+
|
|
83
|
+
const mcpServerSchema = z.object({
|
|
84
|
+
command: nonEmptyTrimmed().optional(),
|
|
85
|
+
url: nonEmptyTrimmed().refine(v => /^https?:\/\//.test(v), { message: '必须为 http(s) URL' }).optional(),
|
|
86
|
+
args: z.array(z.string({ invalid_type_error: '必须为字符串' }), { invalid_type_error: '必须为字符串数组' }).optional(),
|
|
87
|
+
env: stringMap().optional(),
|
|
88
|
+
headers: stringMap().optional(),
|
|
89
|
+
}).strict().superRefine((obj, ctx) => {
|
|
90
|
+
const hasCommand = typeof obj.command === 'string' && !!obj.command
|
|
91
|
+
const hasUrl = typeof obj.url === 'string' && !!obj.url
|
|
92
|
+
if (hasCommand === hasUrl) {
|
|
93
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: [], message: '需要 command(stdio)或 url(远程 Streamable HTTP)恰好其一(互斥)' })
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
if (obj.args !== undefined && !hasCommand) ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['args'], message: '仅 stdio 通道(command)适用' })
|
|
97
|
+
if (obj.env !== undefined && !hasCommand) ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['env'], message: '仅 stdio 通道(command)适用' })
|
|
98
|
+
if (obj.headers !== undefined && !hasUrl) ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['headers'], message: '仅远程通道(url)适用' })
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
const rawSchema = z.object({
|
|
102
|
+
sandbox: z.object({
|
|
103
|
+
unsigned: z.object({
|
|
104
|
+
allow: z.boolean({ invalid_type_error: '必须为布尔', required_error: '必须为布尔' }).optional(),
|
|
105
|
+
}).strict().optional(),
|
|
106
|
+
sig: z.object({
|
|
107
|
+
enforce: z.boolean({ invalid_type_error: '必须为布尔', required_error: '必须为布尔' }).optional(),
|
|
108
|
+
trustRoot: nonEmptyTrimmed().optional(),
|
|
109
|
+
}).strict().optional(),
|
|
110
|
+
}).strict().optional(),
|
|
111
|
+
llm: z.object({
|
|
112
|
+
baseUrl: nonEmptyTrimmed().optional(),
|
|
113
|
+
model: nonEmptyTrimmed().optional(),
|
|
114
|
+
adapterId: nonEmptyTrimmed().optional(),
|
|
115
|
+
maxTokens: z.number({ invalid_type_error: '必须为正整数', required_error: '必须为正整数' })
|
|
116
|
+
.refine(v => Number.isFinite(v) && Number.isInteger(v) && v > 0, { message: '必须为正整数' })
|
|
117
|
+
.optional(),
|
|
118
|
+
allowEnvFallback: z.boolean({ invalid_type_error: '必须为布尔', required_error: '必须为布尔' }).optional(),
|
|
119
|
+
}).strict().optional(),
|
|
120
|
+
mcp: z.object({
|
|
121
|
+
servers: z.record(z.string(), mcpServerSchema, { invalid_type_error: '必须为对象(serverId → 启动规格)' }),
|
|
122
|
+
}).strict().optional(),
|
|
123
|
+
}, { invalid_type_error: '配置顶层必须为 JSON 对象' }).strict()
|
|
124
|
+
|
|
125
|
+
/** 未登记键提示(渲染层拼 path)——与系统设计键位空间同步 */
|
|
126
|
+
const KEY_SPACES = '登记键位:sandbox.* / llm.* / mcp.servers.*'
|
|
127
|
+
|
|
128
|
+
/** zod issue → CAR-E-CONFIG 渲染(首个 issue;unrecognized_keys 按键路径展开) */
|
|
129
|
+
function renderIssue(error: z.ZodError): string {
|
|
130
|
+
const iss = error.issues[0]!
|
|
131
|
+
if (iss.code === 'unrecognized_keys') {
|
|
132
|
+
const keys = (iss as unknown as { keys?: string[] }).keys ?? []
|
|
133
|
+
const keyPath = [...iss.path, keys[0] ?? '?'].join('.')
|
|
134
|
+
return iss.path.length
|
|
135
|
+
? `CAR-E-CONFIG: 未知键 ${keyPath}(${KEY_SPACES})`
|
|
136
|
+
: `CAR-E-CONFIG: 未知顶层键 "${keys[0] ?? '?'}"(${KEY_SPACES})`
|
|
137
|
+
}
|
|
138
|
+
const p = iss.path.join('.')
|
|
139
|
+
return p ? `CAR-E-CONFIG: ${p} ${iss.message}` : `CAR-E-CONFIG: ${iss.message}`
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** zod 解析 → CarConfig(扁平渲染:sandbox 嵌套键拍平——mergeSignatureGate 消费形态不变) */
|
|
143
|
+
function parseCarConfig(raw: unknown): { config: CarConfig; error?: string } {
|
|
144
|
+
const parsed = rawSchema.safeParse(raw)
|
|
145
|
+
if (!parsed.success) return { config: {}, error: renderIssue(parsed.error) }
|
|
146
|
+
const d = parsed.data
|
|
147
|
+
const out: CarConfig = {}
|
|
148
|
+
if (d.sandbox) {
|
|
149
|
+
const sb: SandboxSigConfig = {}
|
|
150
|
+
if (d.sandbox.unsigned?.allow !== undefined) sb.unsignedAllow = d.sandbox.unsigned.allow
|
|
151
|
+
if (d.sandbox.sig?.enforce !== undefined) sb.sigEnforce = d.sandbox.sig.enforce
|
|
152
|
+
if (d.sandbox.sig?.trustRoot !== undefined) sb.sigTrustRoot = d.sandbox.sig.trustRoot
|
|
153
|
+
out.sandbox = sb
|
|
154
|
+
}
|
|
155
|
+
if (d.llm) {
|
|
156
|
+
const llm: LlmConfig = {}
|
|
157
|
+
if (d.llm.baseUrl !== undefined) llm.baseUrl = d.llm.baseUrl
|
|
158
|
+
if (d.llm.model !== undefined) llm.model = d.llm.model
|
|
159
|
+
if (d.llm.adapterId !== undefined) llm.adapterId = d.llm.adapterId
|
|
160
|
+
if (d.llm.maxTokens !== undefined) llm.maxTokens = d.llm.maxTokens
|
|
161
|
+
if (d.llm.allowEnvFallback !== undefined) llm.allowEnvFallback = d.llm.allowEnvFallback
|
|
162
|
+
out.llm = llm
|
|
163
|
+
}
|
|
164
|
+
if (d.mcp) {
|
|
165
|
+
const servers: Record<string, McpServerSpec> = {}
|
|
166
|
+
for (const [sid, sv] of Object.entries(d.mcp.servers)) {
|
|
167
|
+
const spec: McpServerSpec = {}
|
|
168
|
+
if (sv.command !== undefined) spec.command = sv.command
|
|
169
|
+
if (sv.url !== undefined) spec.url = sv.url
|
|
170
|
+
if (sv.args !== undefined) spec.args = sv.args
|
|
171
|
+
if (sv.env !== undefined) spec.env = sv.env
|
|
172
|
+
if (sv.headers !== undefined) spec.headers = sv.headers
|
|
173
|
+
servers[sid] = spec
|
|
174
|
+
}
|
|
175
|
+
out.mcp = { servers }
|
|
176
|
+
}
|
|
177
|
+
return { config: out }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ==================== 载体装载(JSON 同步 / TS 预热缓存) ====================
|
|
181
|
+
|
|
182
|
+
/** car.config.ts 预热缓存(path → 装载结果;warmCarConfig 每次以新 epoch 查询串击穿模块缓存) */
|
|
183
|
+
const tsCache = new Map<string, ConfigLoadResult>()
|
|
184
|
+
let tsEpoch = 0
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 1.5-S3(D-22):car.config.ts 载体预热(动态 import 与插件加载同纪律——Node 原生类型剥离,
|
|
188
|
+
* Node 22 须 --experimental-transform-types / Node ≥23.6 原生支持;报错文案引导)。
|
|
189
|
+
* CLI 入口统一预热;直接调用 loadCarConfig 的库方在存在 car.config.ts 时须先 await 本函数。
|
|
190
|
+
*/
|
|
191
|
+
export async function warmCarConfig(opts: { explicitPath?: string; cwd?: string } = {}): Promise<void> {
|
|
44
192
|
let path: string | undefined
|
|
45
193
|
if (opts.explicitPath) {
|
|
194
|
+
if (!opts.explicitPath.endsWith('.ts')) return // JSON 显式路径走同步通道,无需预热
|
|
46
195
|
path = isAbsolute(opts.explicitPath) ? opts.explicitPath : join(opts.cwd ?? process.cwd(), opts.explicitPath)
|
|
196
|
+
} else {
|
|
197
|
+
const candidate = join(opts.cwd ?? process.cwd(), CONFIG_TS_FILENAME)
|
|
198
|
+
if (!existsSync(candidate)) return // cwd 发现序:无 ts 载体 = 无需预热(json 同步可读)
|
|
199
|
+
path = candidate
|
|
200
|
+
}
|
|
201
|
+
tsEpoch++
|
|
202
|
+
const result = await loadTsConfigFile(path, tsEpoch)
|
|
203
|
+
tsCache.set(path, result)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function loadTsConfigFile(path: string, epoch: number): Promise<ConfigLoadResult> {
|
|
207
|
+
try {
|
|
208
|
+
const mod = (await import(`${pathToFileURL(path).href}?cfg-epoch=${epoch}`)) as { default?: unknown }
|
|
209
|
+
const raw = mod.default
|
|
210
|
+
if (raw === undefined) {
|
|
211
|
+
return { path, config: {}, error: `CAR-E-CONFIG: ${path} 缺 default export(car.config.ts 形态 = export default {…})` }
|
|
212
|
+
}
|
|
213
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
214
|
+
return { path, config: {}, error: `CAR-E-CONFIG: ${path} default export 必须为对象(配置顶层)` }
|
|
215
|
+
}
|
|
216
|
+
const v = parseCarConfig(raw)
|
|
217
|
+
return v.error ? { path, config: {}, error: v.error } : { path, config: v.config }
|
|
218
|
+
} catch (e) {
|
|
219
|
+
const err = e as { code?: string; message?: string }
|
|
220
|
+
const hint = err.code === 'ERR_UNKNOWN_FILE_EXTENSION' || /Unknown file extension|loader/i.test(err.message ?? '')
|
|
221
|
+
? `——当前 Node 未启用 .ts 载体(须 --experimental-transform-types 或 Node ≥23.6 原生类型剥离)`
|
|
222
|
+
: ''
|
|
223
|
+
return { path, config: {}, error: `CAR-E-CONFIG: car.config.ts 载入失败(${err.message})${hint}` }
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** 加载配置:显式路径(必须存在,否则报错)或 cwd 发现(未发现 = 正常态返回空配置) */
|
|
228
|
+
export function loadCarConfig(opts: { explicitPath?: string; cwd?: string } = {}): ConfigLoadResult {
|
|
229
|
+
if (opts.explicitPath) {
|
|
230
|
+
const path = isAbsolute(opts.explicitPath) ? opts.explicitPath : join(opts.cwd ?? process.cwd(), opts.explicitPath)
|
|
231
|
+
if (path.endsWith('.ts')) {
|
|
232
|
+
const hit = tsCache.get(path)
|
|
233
|
+
if (hit) return hit
|
|
234
|
+
return { path, config: {}, error: `CAR-E-CONFIG: car.config.ts 未预热(CLI 入口自动 warmCarConfig;直接调用方须先 await warmCarConfig({ explicitPath }))` }
|
|
235
|
+
}
|
|
47
236
|
if (!existsSync(path)) {
|
|
48
237
|
return { path, config: {}, error: `CAR-E-CONFIG: 配置文件不存在:${path}(--config 显式指定的路径必须存在)` }
|
|
49
238
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
239
|
+
return loadJsonConfigFile(path)
|
|
240
|
+
}
|
|
241
|
+
// cwd 发现序(D-22):car.config.ts > car.config.json(并存 = ts 胜出,json 忽略)
|
|
242
|
+
const tsPath = join(opts.cwd ?? process.cwd(), CONFIG_TS_FILENAME)
|
|
243
|
+
if (existsSync(tsPath)) {
|
|
244
|
+
const hit = tsCache.get(tsPath)
|
|
245
|
+
if (hit) return hit
|
|
246
|
+
return { path: tsPath, config: {}, error: `CAR-E-CONFIG: car.config.ts 未预热(CLI 入口自动 warmCarConfig;直接调用方须先 await warmCarConfig({ cwd }))` }
|
|
53
247
|
}
|
|
248
|
+
const jsonPath = join(opts.cwd ?? process.cwd(), CONFIG_FILENAME)
|
|
249
|
+
if (!existsSync(jsonPath)) return { config: {} }
|
|
250
|
+
return loadJsonConfigFile(jsonPath)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function loadJsonConfigFile(path: string): ConfigLoadResult {
|
|
54
254
|
let raw: unknown
|
|
55
255
|
try {
|
|
56
256
|
raw = JSON.parse(readFileSync(path, 'utf-8'))
|
|
57
257
|
} catch (e) {
|
|
58
258
|
return { path, config: {}, error: `CAR-E-CONFIG: 配置文件解析失败(${(e as Error).message})——坏 JSON 显式拒绝,不静默忽略` }
|
|
59
259
|
}
|
|
60
|
-
const v =
|
|
260
|
+
const v = parseCarConfig(raw)
|
|
61
261
|
return v.error ? { path, config: {}, error: v.error } : { path, config: v.config }
|
|
62
262
|
}
|
|
63
263
|
|
|
64
|
-
/** 手写校验(fail-visible;零依赖红线——不引 zod,D-11a ①) */
|
|
65
|
-
function validate(raw: unknown): { config: CarConfig; error?: string } {
|
|
66
|
-
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
67
|
-
return { config: {}, error: 'CAR-E-CONFIG: 配置顶层必须为 JSON 对象' }
|
|
68
|
-
}
|
|
69
|
-
const out: CarConfig = {}
|
|
70
|
-
for (const [k, v] of Object.entries(raw as Record<string, unknown>)) {
|
|
71
|
-
if (k !== 'sandbox') {
|
|
72
|
-
return { config: {}, error: `CAR-E-CONFIG: 未知顶层键 "${k}"(1.1 登记键位空间仅 sandbox.*)` }
|
|
73
|
-
}
|
|
74
|
-
if (typeof v !== 'object' || v === null || Array.isArray(v)) {
|
|
75
|
-
return { config: {}, error: 'CAR-E-CONFIG: sandbox 必须为对象' }
|
|
76
|
-
}
|
|
77
|
-
const sb: SandboxSigConfig = {}
|
|
78
|
-
for (const [sk, sv] of Object.entries(v as Record<string, unknown>)) {
|
|
79
|
-
if (sk === 'unsigned') {
|
|
80
|
-
if (typeof sv !== 'object' || sv === null || Array.isArray(sv)) return { config: {}, error: 'CAR-E-CONFIG: sandbox.unsigned 必须为对象' }
|
|
81
|
-
for (const [uk, uv] of Object.entries(sv as Record<string, unknown>)) {
|
|
82
|
-
if (uk !== 'allow') return { config: {}, error: `CAR-E-CONFIG: 未知键 sandbox.unsigned.${uk}(登记键位:sandbox.unsigned.allow)` }
|
|
83
|
-
if (typeof uv !== 'boolean') return { config: {}, error: 'CAR-E-CONFIG: sandbox.unsigned.allow 必须为布尔' }
|
|
84
|
-
sb.unsignedAllow = uv
|
|
85
|
-
}
|
|
86
|
-
} else if (sk === 'sig') {
|
|
87
|
-
if (typeof sv !== 'object' || sv === null || Array.isArray(sv)) return { config: {}, error: 'CAR-E-CONFIG: sandbox.sig 必须为对象' }
|
|
88
|
-
for (const [gk, gv] of Object.entries(sv as Record<string, unknown>)) {
|
|
89
|
-
if (gk === 'enforce') {
|
|
90
|
-
if (typeof gv !== 'boolean') return { config: {}, error: 'CAR-E-CONFIG: sandbox.sig.enforce 必须为布尔' }
|
|
91
|
-
sb.sigEnforce = gv
|
|
92
|
-
} else if (gk === 'trustRoot') {
|
|
93
|
-
if (typeof gv !== 'string' || !gv.trim()) return { config: {}, error: 'CAR-E-CONFIG: sandbox.sig.trustRoot 必须为非空字符串(ed25519 spki base64)' }
|
|
94
|
-
sb.sigTrustRoot = gv.trim()
|
|
95
|
-
} else {
|
|
96
|
-
return { config: {}, error: `CAR-E-CONFIG: 未知键 sandbox.sig.${gk}(登记键位:sandbox.sig.enforce / sandbox.sig.trustRoot)` }
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
} else {
|
|
100
|
-
return { config: {}, error: `CAR-E-CONFIG: 未知 sandbox 键 "${sk}"(1.1 登记键位:sandbox.unsigned.* / sandbox.sig.*)` }
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
out.sandbox = sb
|
|
104
|
-
}
|
|
105
|
-
return { config: out }
|
|
106
|
-
}
|
|
107
|
-
|
|
108
264
|
/** 合并产出(mode/unsignedAllow 恒有值——比 SignatureGateOptions 的可选字段更窄,doctor 等消费方免缺省分支) */
|
|
109
265
|
export interface ResolvedSignatureGate {
|
|
110
266
|
mode: 'warn' | 'enforce'
|
|
@@ -112,6 +268,34 @@ export interface ResolvedSignatureGate {
|
|
|
112
268
|
unsignedAllow: boolean
|
|
113
269
|
}
|
|
114
270
|
|
|
271
|
+
/**
|
|
272
|
+
* 1.4-S3(D-16):模型接入配置合并(flag > env > 配置 > 缺省)。
|
|
273
|
+
* env 已定义为显式意见(压过配置),未定义落配置层——与 mergeSignatureGate 同纪律。
|
|
274
|
+
* 凭据不在此层(CR-05):走 CredentialService(keychain → env OPENAI_API_KEY/ANTHROPIC_API_KEY/
|
|
275
|
+
* CAR_LLM_API_KEY,env fallback 需显式开启 = CAR_ALLOW_ENV_CREDENTIALS=1 或 llm.allowEnvFallback)。
|
|
276
|
+
*/
|
|
277
|
+
export function mergeLlmConfig(env: NodeJS.ProcessEnv, cfg: CarConfig, flags: { baseUrl?: string; model?: string; adapterId?: string; maxTokens?: number } = {}): LlmConfig {
|
|
278
|
+
let baseUrl = cfg.llm?.baseUrl
|
|
279
|
+
if (env.CAR_LLM_BASE_URL) baseUrl = env.CAR_LLM_BASE_URL
|
|
280
|
+
if (flags.baseUrl) baseUrl = flags.baseUrl
|
|
281
|
+
let model = cfg.llm?.model
|
|
282
|
+
if (env.CAR_LLM_MODEL) model = env.CAR_LLM_MODEL
|
|
283
|
+
if (flags.model) model = flags.model
|
|
284
|
+
let adapterId = cfg.llm?.adapterId
|
|
285
|
+
if (flags.adapterId) adapterId = flags.adapterId
|
|
286
|
+
let maxTokens = cfg.llm?.maxTokens
|
|
287
|
+
if (flags.maxTokens !== undefined) maxTokens = flags.maxTokens
|
|
288
|
+
let allowEnvFallback = cfg.llm?.allowEnvFallback === true
|
|
289
|
+
if (env.CAR_ALLOW_ENV_CREDENTIALS !== undefined) allowEnvFallback = env.CAR_ALLOW_ENV_CREDENTIALS === '1'
|
|
290
|
+
return {
|
|
291
|
+
...(baseUrl ? { baseUrl } : {}),
|
|
292
|
+
...(model ? { model } : {}),
|
|
293
|
+
...(adapterId ? { adapterId } : {}),
|
|
294
|
+
...(maxTokens !== undefined ? { maxTokens } : {}),
|
|
295
|
+
allowEnvFallback,
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
115
299
|
/**
|
|
116
300
|
* 优先级合并(flag > env > 配置 > 缺省):产出装载签名门选项。
|
|
117
301
|
* flagEnforce = CLI --sig-enforce;env 已定义即为显式意见(压过配置),未定义落配置层。
|
package/src/load/loader.ts
CHANGED
|
@@ -85,24 +85,38 @@ export function parseManifest(raw: unknown, file = '<manifest>'): Manifest {
|
|
|
85
85
|
return m
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
export interface ToolReg {
|
|
88
|
+
export interface ToolReg {
|
|
89
|
+
name: string
|
|
90
|
+
run: (args: any) => Promise<unknown>
|
|
91
|
+
// —— 1.4-S1 声明面加法(进模型 ToolDefinition;缺省收口见 runtime-core/tools.ts T-22)——
|
|
92
|
+
/** 工具描述(进模型 function.description) */
|
|
93
|
+
description?: string
|
|
94
|
+
/** 入参 JSON Schema(进模型 function.parameters) */
|
|
95
|
+
parameters?: unknown
|
|
96
|
+
/** 副作用声明(缺省 write 收口——T-22 同款:未声明按最高约束) */
|
|
97
|
+
declaredSideEffect?: 'readonly' | 'write'
|
|
98
|
+
}
|
|
89
99
|
export interface CommandReg { name: string; run: (args: string[]) => Promise<void> }
|
|
90
100
|
export interface PluginApi {
|
|
91
101
|
registerTool(tool: ToolReg): void
|
|
92
102
|
registerCommand(cmd: CommandReg): void
|
|
93
103
|
getRegisteredTools(): ToolReg[]
|
|
104
|
+
/** 1.4-S2:插件设定 system prompt(session 级;last-wins;bindCore 时冲刷进宿主收集器,落 'system' 事件) */
|
|
105
|
+
setSystemPrompt(text: string): void
|
|
94
106
|
}
|
|
95
107
|
|
|
96
108
|
interface HostBinding {
|
|
97
109
|
registerTool?(tool: ToolReg): void
|
|
98
110
|
registerCommand?(cmd: CommandReg): void
|
|
99
111
|
getRegisteredTools(): ToolReg[]
|
|
112
|
+
setSystemPrompt?(text: string): void
|
|
100
113
|
}
|
|
101
114
|
|
|
102
115
|
class ApiImpl {
|
|
103
116
|
#host: HostBinding | undefined
|
|
104
117
|
#pendingTools: ToolReg[] = []
|
|
105
118
|
#pendingCommands: CommandReg[] = []
|
|
119
|
+
#pendingSystemPrompt: string | null = null
|
|
106
120
|
registerTool(tool: ToolReg): void {
|
|
107
121
|
if (this.#host?.registerTool) { this.#host.registerTool(tool); return }
|
|
108
122
|
this.#pendingTools.push(tool) // 注册类动作始终可用:Stub 期收集,bind 后冲刷
|
|
@@ -111,6 +125,10 @@ class ApiImpl {
|
|
|
111
125
|
if (this.#host?.registerCommand) { this.#host.registerCommand(cmd); return }
|
|
112
126
|
this.#pendingCommands.push(cmd)
|
|
113
127
|
}
|
|
128
|
+
setSystemPrompt(text: string): void {
|
|
129
|
+
if (this.#host?.setSystemPrompt) { this.#host.setSystemPrompt(text); return }
|
|
130
|
+
this.#pendingSystemPrompt = text // last-wins:多次设定取最后一次
|
|
131
|
+
}
|
|
114
132
|
getRegisteredTools(): ToolReg[] {
|
|
115
133
|
// 依赖宿主绑定的能力查询:bindCore 前一调用即抛错(Stub 显式失败)
|
|
116
134
|
if (!this.#host) throw new Error('CAR-STUB: "getRegisteredTools" called before bindCore(启动期显式失败换运行期时序正确)')
|
|
@@ -120,8 +138,10 @@ class ApiImpl {
|
|
|
120
138
|
this.#host = host
|
|
121
139
|
for (const t of this.#pendingTools) host.registerTool?.(t)
|
|
122
140
|
for (const c of this.#pendingCommands) host.registerCommand?.(c)
|
|
141
|
+
if (this.#pendingSystemPrompt !== null) host.setSystemPrompt?.(this.#pendingSystemPrompt)
|
|
123
142
|
this.#pendingTools = []
|
|
124
143
|
this.#pendingCommands = []
|
|
144
|
+
this.#pendingSystemPrompt = null
|
|
125
145
|
}
|
|
126
146
|
}
|
|
127
147
|
|
package/src/loop/stop.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* - 取消:未派发调用补记成对事件(toolCall + 合成错误 toolResult),日志无缺口
|
|
13
13
|
*/
|
|
14
14
|
import type { SessionLog } from '../session/log.ts'
|
|
15
|
+
import { CarM8Error } from '../runtime-core/errors.ts'
|
|
15
16
|
|
|
16
17
|
export type StepOutcome = 'completed' | 'max-tokens' | 'null'
|
|
17
18
|
export type TurnEndReason = 'completed' | 'max-tokens' | 'aborted' | 'error' | 'blocked' | 'interrupted'
|
|
@@ -23,6 +24,9 @@ export interface ToolDef {
|
|
|
23
24
|
concludesTurn?: boolean
|
|
24
25
|
terminate?: boolean
|
|
25
26
|
run(args: Record<string, unknown>): Promise<unknown>
|
|
27
|
+
// —— 1.4-S1 声明面加法(进模型 ToolDefinition;执行面与声明面同一对象——声明面=授权面红线)——
|
|
28
|
+
description?: string
|
|
29
|
+
parameters?: unknown
|
|
26
30
|
}
|
|
27
31
|
/** 模型单步响应(provider 形态;stopReason 为不可变透传——ADR-001) */
|
|
28
32
|
export interface ModelStep {
|
|
@@ -193,7 +197,14 @@ export async function runTurn(opts: {
|
|
|
193
197
|
trail.push({ seq, reason: 'aborted' })
|
|
194
198
|
return { reason: 'aborted', steps, endReasonTrail: trail }
|
|
195
199
|
}
|
|
196
|
-
|
|
200
|
+
// 1.4-S4:CarM8Error 保形——code/userHint/retryable 进 turnEnd(A080001 引导文案「请运行
|
|
201
|
+
// car doctor」可达用户;非 CarM8Error 维持 String(e) 口径不变)
|
|
202
|
+
const m8 = e instanceof CarM8Error
|
|
203
|
+
const detail = m8 ? `${e.slug}: ${e.message}` : String(e)
|
|
204
|
+
const seq = log.append('runtime', 'turnEnd', turnId, null, {
|
|
205
|
+
reason: 'error', detail,
|
|
206
|
+
...(m8 ? { code: e.code, userHint: e.userHint, retryable: e.retryable } : {}),
|
|
207
|
+
}).seq
|
|
197
208
|
trail.push({ seq, reason: 'error' })
|
|
198
209
|
return { reason: 'error', steps, endReasonTrail: trail }
|
|
199
210
|
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 1.5-S2 · MCP 远程传输客户端(D-21):Streamable HTTP(2025-06-18 spec 单轨)
|
|
3
|
+
*
|
|
4
|
+
* 口径:
|
|
5
|
+
* - CAR → 远程 MCP Server 方向(gateway.ts ClientTransport 的第二真实通道——与 stdio 并列);
|
|
6
|
+
* - 懒 initialize 握手:首次 send 前自动 initialize → notifications/initialized;
|
|
7
|
+
* 响应 `Mcp-Session-Id` 头存在则后续请求回传(无会话头的 server 容许);
|
|
8
|
+
* - 响应形态双兼容:application/json 单响应 / text/event-stream 逐事件取本请求 id
|
|
9
|
+
* (SSE 命中即收口,流显式 destroy——悬挂 socket 是 1.4-BUG-2 同源教训);
|
|
10
|
+
* - **出站走 node:http(s) 不用 undici fetch**(1.4-BUG-2 已验证口径:win32 退出 libuv
|
|
11
|
+
* 断言 0xC0000409 与 undici socket 相关,telemetry 出站同此先例);TLS 门复用
|
|
12
|
+
* assertTlsOrLoopback(D-12a:http 仅豁免 loopback——本地假 server 与本地 MCP);
|
|
13
|
+
* - send 内建 30s 超时(与 stdio 同口径;gateway BD-02 外层 race 不变);
|
|
14
|
+
* - headers 凭据规则(CR-05 经配置通道执行):值支持 `${ENV_VAR}` 引用(env 缺席显式报错);
|
|
15
|
+
* 字面值疑似明文凭据(containsPlaintextCredential 同款模式)直接拒绝——env 引用注入
|
|
16
|
+
* 才是登记合规通道(展开后值不复查:引注入即目的,展开值是凭据本身);
|
|
17
|
+
* - legacy HTTP+SSE(2024-11-05)不做(1.5 规划 D-21 登记不追溯)。
|
|
18
|
+
*/
|
|
19
|
+
import { request as httpRequest } from 'node:http'
|
|
20
|
+
import { request as httpsRequest } from 'node:https'
|
|
21
|
+
import type { ClientTransport, JsonRpcRequest, JsonRpcResponse } from './gateway.ts'
|
|
22
|
+
import { containsPlaintextCredential } from './gateway.ts'
|
|
23
|
+
import { assertTlsOrLoopback } from '../runtime-core/llm.ts'
|
|
24
|
+
|
|
25
|
+
export const MCP_PROTOCOL_VERSION = '2025-06-18'
|
|
26
|
+
|
|
27
|
+
export interface HttpMcpServerSpec {
|
|
28
|
+
url: string
|
|
29
|
+
/** 已展开的请求头(明文禁令与 ${ENV} 展开由 resolveMcpHeaders 承担——直接传入者自证合规) */
|
|
30
|
+
headers?: Record<string, string>
|
|
31
|
+
/** 测试注入;缺省 30s(send 内建超时,与 stdio 同口径) */
|
|
32
|
+
timeoutMs?: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** node:http(s) 响应的最小面(真实 res 兼容;测试可注入替身) */
|
|
36
|
+
export interface RawHttpResponse {
|
|
37
|
+
status: number
|
|
38
|
+
headers: Record<string, string | string[] | undefined>
|
|
39
|
+
/** 流式响应体(node:http res 本身即 AsyncIterable<Uint8Array>;destroy 显式丢弃) */
|
|
40
|
+
body: AsyncIterable<Uint8Array> & { destroy(): void }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** node:http(s) POST 单发(1.4-BUG-2 口径:出站不走 undici;TLS 门由 createHttpMcpTransport 把守) */
|
|
44
|
+
export function rawPost(url: string, headers: Record<string, string>, body: string, timeoutMs: number): Promise<RawHttpResponse> {
|
|
45
|
+
const u = new URL(url)
|
|
46
|
+
const send = u.protocol === 'https:' ? httpsRequest : httpRequest
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const req = send(u, { method: 'POST', headers }, res => {
|
|
49
|
+
resolve({
|
|
50
|
+
status: res.statusCode ?? 0,
|
|
51
|
+
headers: res.headers,
|
|
52
|
+
body: res as unknown as RawHttpResponse['body'],
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
req.setTimeout(timeoutMs, () => req.destroy(new Error(`CAR-E-MCP: http transport timeout (${timeoutMs}ms)`)))
|
|
56
|
+
req.on('error', reject)
|
|
57
|
+
req.end(body)
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* ${ENV_VAR} 头引用展开(D-21):值内任意位置的 `${NAME}` 均替换为 env.NAME(如
|
|
63
|
+
* `Bearer ${TOKEN}`);env 缺席显式报错(fail-visible,禁静默空值);字面值疑似明文凭据
|
|
64
|
+
* 直接拒绝(按原始值检查——展开值是凭据本身,引注入即目的)。
|
|
65
|
+
*/
|
|
66
|
+
export function resolveMcpHeaders(raw: Record<string, string>, env: NodeJS.ProcessEnv = process.env): Record<string, string> {
|
|
67
|
+
const out: Record<string, string> = {}
|
|
68
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
69
|
+
const cred = containsPlaintextCredential({ [k]: v })
|
|
70
|
+
if (cred) throw new Error(`CAR-E-MCP: headers.${k}——${cred}(配置明文禁令 CR-05;请改用 \`\${ENV_VAR}\` 引用注入)`)
|
|
71
|
+
out[k] = v.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_all, name: string) => {
|
|
72
|
+
const val = env[name]
|
|
73
|
+
if (val === undefined || val === '') {
|
|
74
|
+
throw new Error(`CAR-E-MCP: headers.${k} 引用的环境变量 ${name} 未设置(fail-visible,禁静默空值)`)
|
|
75
|
+
}
|
|
76
|
+
return val
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
return out
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function createHttpMcpTransport(spec: HttpMcpServerSpec): ClientTransport {
|
|
83
|
+
// D-12a TLS 门(与 LLM 适配器同款):https 直接通过;http 仅豁免 loopback
|
|
84
|
+
assertTlsOrLoopback(spec.url.replace(/\/+$/, ''))
|
|
85
|
+
const timeoutMs = spec.timeoutMs ?? 30_000
|
|
86
|
+
let sessionId: string | null = null
|
|
87
|
+
let initialized = false
|
|
88
|
+
let initPromise: Promise<void> | null = null
|
|
89
|
+
let closed = false
|
|
90
|
+
|
|
91
|
+
const baseHeaders = (): Record<string, string> => ({
|
|
92
|
+
'content-type': 'application/json',
|
|
93
|
+
'accept': 'application/json, text/event-stream',
|
|
94
|
+
...(sessionId ? { 'mcp-session-id': sessionId } : {}),
|
|
95
|
+
...spec.headers,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
/** 流全读(JSON 响应) */
|
|
99
|
+
async function readAll(res: RawHttpResponse): Promise<Buffer> {
|
|
100
|
+
const parts: Buffer[] = []
|
|
101
|
+
for await (const chunk of res.body) parts.push(Buffer.from(chunk))
|
|
102
|
+
return Buffer.concat(parts)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** SSE 响应逐事件取本请求 id(data JSON 自带 id;命中即收口,流显式 destroy) */
|
|
106
|
+
async function readSseForId(res: RawHttpResponse, id: number): Promise<JsonRpcResponse> {
|
|
107
|
+
const decoder = new TextDecoder()
|
|
108
|
+
let buf = ''
|
|
109
|
+
try {
|
|
110
|
+
for await (const chunk of res.body) {
|
|
111
|
+
buf += decoder.decode(chunk, { stream: true })
|
|
112
|
+
let nl: number
|
|
113
|
+
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
114
|
+
const line = buf.slice(0, nl).replace(/\r$/, '')
|
|
115
|
+
buf = buf.slice(nl + 1)
|
|
116
|
+
if (!line.startsWith('data:')) continue
|
|
117
|
+
const data = line.slice(5).trim()
|
|
118
|
+
if (!data) continue
|
|
119
|
+
let evt: any
|
|
120
|
+
try { evt = JSON.parse(data) } catch { continue }
|
|
121
|
+
if (evt?.id === id) return evt as JsonRpcResponse
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} finally {
|
|
125
|
+
try { res.body.destroy() } catch { /* 流已自然结束 */ }
|
|
126
|
+
}
|
|
127
|
+
throw new Error('CAR-E-MCP: SSE 流结束未见本请求响应(阶段:read-response)')
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function postAndRead(body: unknown, matchId: number | null, phase: string): Promise<JsonRpcResponse> {
|
|
131
|
+
const res = await rawPost(spec.url, baseHeaders(), JSON.stringify(body), timeoutMs)
|
|
132
|
+
const sid = res.headers['mcp-session-id']
|
|
133
|
+
if (typeof sid === 'string') sessionId = sid
|
|
134
|
+
if (res.status < 200 || res.status >= 300) {
|
|
135
|
+
try { res.body.destroy() } catch { /* 已结束 */ }
|
|
136
|
+
throw new Error(`CAR-E-MCP: HTTP ${res.status}(阶段:${phase}——server 拒绝或不可达)`)
|
|
137
|
+
}
|
|
138
|
+
const ct = String(res.headers['content-type'] ?? '')
|
|
139
|
+
if (ct.includes('text/event-stream')) return readSseForId(res, matchId ?? Number.NaN)
|
|
140
|
+
const json = JSON.parse((await readAll(res)).toString('utf-8')) as JsonRpcResponse
|
|
141
|
+
return json
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** 懒 initialize 握手(首次 send 前恰好一次):initialize → 会话头捕获 → notifications/initialized */
|
|
145
|
+
async function doInitialize(): Promise<void> {
|
|
146
|
+
const initRes = await postAndRead({
|
|
147
|
+
jsonrpc: '2.0', id: -1, method: 'initialize',
|
|
148
|
+
params: { protocolVersion: MCP_PROTOCOL_VERSION, capabilities: {}, clientInfo: { name: 'car-runtime', version: '0.0.0' } },
|
|
149
|
+
}, -1, 'initialize')
|
|
150
|
+
if (initRes.error) {
|
|
151
|
+
throw new Error(`CAR-E-MCP: initialize 被拒:${initRes.error.message}(阶段:initialize)`)
|
|
152
|
+
}
|
|
153
|
+
// notifications/initialized:无 id 通知——2xx/202 即视为送达(无响应体,显式丢弃)
|
|
154
|
+
const res = await rawPost(spec.url, baseHeaders(), JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }), timeoutMs)
|
|
155
|
+
try { res.body.destroy() } catch { /* 已结束 */ }
|
|
156
|
+
if (res.status < 200 || res.status >= 300) {
|
|
157
|
+
throw new Error(`CAR-E-MCP: notifications/initialized HTTP ${res.status}(阶段:initialized)`)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const transport: ClientTransport = {
|
|
162
|
+
send(req: JsonRpcRequest): Promise<JsonRpcResponse> {
|
|
163
|
+
return (async (): Promise<JsonRpcResponse> => {
|
|
164
|
+
if (closed) throw new Error('CAR-E-MCP: http transport closed(server 已收口)')
|
|
165
|
+
if (!initialized) {
|
|
166
|
+
initPromise ??= doInitialize().then(() => { initialized = true })
|
|
167
|
+
try { await initPromise } catch (e) { initPromise = null; throw e }
|
|
168
|
+
}
|
|
169
|
+
return postAndRead(req, req.id, 'request')
|
|
170
|
+
})()
|
|
171
|
+
},
|
|
172
|
+
alive: () => !closed,
|
|
173
|
+
close: () => { closed = true },
|
|
174
|
+
}
|
|
175
|
+
return transport
|
|
176
|
+
}
|