thincoder 0.12.61 → 0.12.63
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 +34 -1
- package/README.md +13 -12
- package/bin/thincoder.mjs +63 -28
- package/package.json +6 -5
- package/src/acp/bridge.mjs +35 -15
- package/src/acp/client-caps.mjs +86 -0
- package/src/acp/ext.mjs +86 -0
- package/src/acp/handlers-session.mjs +240 -0
- package/src/acp/handlers-slots.mjs +196 -0
- package/src/acp/login.mjs +48 -0
- package/src/acp/session.mjs +6 -4
- package/src/acp.mjs +67 -371
- package/src/cli/distill-command.mjs +3 -3
- package/src/cli/make-agent.mjs +59 -17
- package/src/cli/memory-command.mjs +3 -3
- package/src/cli/permission.mjs +4 -48
- package/src/cli/setup-wizard.mjs +1 -1
- package/src/completions.mjs +3 -1
- package/src/crash-reports.mjs +32 -10
- package/src/distill.mjs +4 -4
- package/src/heap-watch.mjs +88 -0
- package/src/prompt-injections.mjs +20 -0
- package/src/tui/agent-turn.mjs +40 -9
- package/src/tui/cmd-advisor.mjs +5 -5
- package/src/tui/cmd-clear.mjs +2 -0
- package/src/tui/cmd-config.mjs +8 -8
- package/src/tui/cmd-eng.mjs +25 -9
- package/src/tui/cmd-mcp.mjs +9 -8
- package/src/tui/cmd-model.mjs +1 -1
- package/src/tui/cmd-new.mjs +10 -5
- package/src/tui/cmd-reindex.mjs +1 -1
- package/src/tui/cmd-restore.mjs +2 -2
- package/src/tui/cmd-session.mjs +31 -4
- package/src/tui/cmd-skills.mjs +1 -1
- package/src/tui/cmd-think.mjs +22 -9
- package/src/tui/config-helpers.mjs +1 -1
- package/src/tui/display-budget.mjs +206 -0
- package/src/tui/index.mjs +40 -12
- package/src/tui/interaction.mjs +16 -7
- package/src/tui/key-handler-search.mjs +9 -1
- package/src/tui/key-modes.mjs +9 -4
- package/src/tui/ledger-surface.mjs +85 -0
- package/src/tui/model-catalog.mjs +4 -4
- package/src/tui/model-picker.mjs +8 -7
- package/src/tui/mouse.mjs +11 -6
- package/src/tui/pickers.mjs +15 -2
- package/src/tui/render-conversation.mjs +1 -1
- package/src/tui/render-frame.mjs +17 -6
- package/src/tui/render-loop.mjs +1 -1
- package/src/tui/render-segments.mjs +3 -1
- package/src/tui/slash-commands.mjs +1 -1
- package/src/tui/startup.mjs +49 -17
- package/src/tui/subagent-blocks.mjs +21 -3
- package/src/tui/subagent-children.mjs +86 -14
- package/src/tui/subagent-freeze.mjs +80 -3
- package/src/tui/suspension-drive.mjs +48 -22
- package/src/tui/tool-args.mjs +5 -2
- package/src/tui/tool-display.mjs +16 -2
- package/src/tui/tool-events.mjs +64 -22
- package/src/tui/tui-lifecycle.mjs +9 -2
- package/src/tui/wizard.mjs +3 -3
- package/src/tui/wrapped-spawn.mjs +21 -5
- package/src/abort-provenance.mjs +0 -116
- package/src/advisor/citations.mjs +0 -139
- package/src/advisor/compaction.mjs +0 -174
- package/src/advisor/convergence.mjs +0 -80
- package/src/advisor/history.mjs +0 -77
- package/src/advisor/loop.mjs +0 -293
- package/src/advisor/messages.mjs +0 -299
- package/src/advisor/project-context.mjs +0 -194
- package/src/advisor/repos.mjs +0 -150
- package/src/advisor/run.mjs +0 -293
- package/src/advisor/truncate.mjs +0 -57
- package/src/advisor.mjs +0 -290
- package/src/agent/completion.mjs +0 -146
- package/src/agent/dispatch.mjs +0 -489
- package/src/agent/helpers.mjs +0 -384
- package/src/agent/post-turn.mjs +0 -70
- package/src/agent/record-results.mjs +0 -174
- package/src/agent/relay-prefix.mjs +0 -39
- package/src/agent/run-stages.mjs +0 -242
- package/src/agent/setup-reminders.mjs +0 -69
- package/src/agent/setup.mjs +0 -354
- package/src/agent/spawn-child.mjs +0 -228
- package/src/agent-tools/advisor-async.mjs +0 -346
- package/src/agent-tools/advisor-settle.mjs +0 -231
- package/src/agent-tools/advisor.mjs +0 -260
- package/src/agent-tools/async-settle.mjs +0 -191
- package/src/agent-tools/batch-segment.mjs +0 -195
- package/src/agent-tools/consult.mjs +0 -468
- package/src/agent-tools/design-token.mjs +0 -117
- package/src/agent-tools/digest-budget.mjs +0 -76
- package/src/agent-tools/eng.mjs +0 -67
- package/src/agent-tools/escalate-async.mjs +0 -289
- package/src/agent-tools/goal.mjs +0 -119
- package/src/agent-tools/plan.mjs +0 -81
- package/src/agent-tools/read-history.mjs +0 -294
- package/src/agent-tools/recent-changes.mjs +0 -24
- package/src/agent-tools/review-streak.mjs +0 -93
- package/src/agent-tools/settings.mjs +0 -265
- package/src/agent-tools/skill.mjs +0 -47
- package/src/agent-tools/subagent-actions.mjs +0 -479
- package/src/agent-tools/subagent-async.mjs +0 -434
- package/src/agent-tools/subagent-panel.mjs +0 -160
- package/src/agent-tools/subagent-run.mjs +0 -205
- package/src/agent-tools/subagent-scheduler.mjs +0 -392
- package/src/agent-tools/subagent-spawn.mjs +0 -453
- package/src/agent-tools/subagent.mjs +0 -404
- package/src/agent-tools/task.mjs +0 -87
- package/src/agent-tools/timer.mjs +0 -46
- package/src/agent-tools/verify.mjs +0 -271
- package/src/agent-tools.mjs +0 -17
- package/src/agent.mjs +0 -413
- package/src/auto-think.mjs +0 -115
- package/src/config-migrate.mjs +0 -70
- package/src/config.mjs +0 -496
- package/src/context.mjs +0 -381
- package/src/conventions.mjs +0 -223
- package/src/embedding.mjs +0 -120
- package/src/escape.mjs +0 -152
- package/src/expand-home.mjs +0 -16
- package/src/explore-distill.mjs +0 -155
- package/src/generate-title.mjs +0 -83
- package/src/git/checkpoint.mjs +0 -448
- package/src/git/gitmem.mjs +0 -100
- package/src/hooks.mjs +0 -97
- package/src/log.mjs +0 -195
- package/src/markdown.mjs +0 -106
- package/src/mcp/helpers.mjs +0 -51
- package/src/mcp/transport-http.mjs +0 -248
- package/src/mcp/transport-stdio.mjs +0 -140
- package/src/mcp/transport-ws.mjs +0 -122
- package/src/mcp.mjs +0 -295
- package/src/memory/code-index.mjs +0 -219
- package/src/memory/code-sync.mjs +0 -413
- package/src/memory/core.mjs +0 -300
- package/src/memory/delete.mjs +0 -236
- package/src/memory/docs.mjs +0 -417
- package/src/memory/file-walk.mjs +0 -109
- package/src/memory/schema.mjs +0 -452
- package/src/memory.mjs +0 -21
- package/src/model-ref.mjs +0 -66
- package/src/model-specs.mjs +0 -179
- package/src/peer-domains.mjs +0 -265
- package/src/peer-instances.mjs +0 -231
- package/src/prompt-overlays.mjs +0 -82
- package/src/prompts/advisor-design.md +0 -41
- package/src/prompts/advisor-round1.md +0 -41
- package/src/prompts/advisor-round2.md +0 -46
- package/src/prompts/advisor-round3.md +0 -42
- package/src/prompts/common.md +0 -115
- package/src/prompts/consult-base.md +0 -19
- package/src/prompts/discipline-engineering.md +0 -217
- package/src/prompts/discipline-normal.md +0 -179
- package/src/prompts/persona-coder.md +0 -21
- package/src/prompts/persona-eng-coder.md +0 -37
- package/src/prompts/persona-eng-designer.md +0 -55
- package/src/prompts/persona-engineering.md +0 -54
- package/src/prompts/persona-explore.md +0 -15
- package/src/prompts/persona-normal.md +0 -27
- package/src/prompts/persona-plan.md +0 -26
- package/src/provider/anthropic.mjs +0 -225
- package/src/provider/core.mjs +0 -476
- package/src/provider/errors.mjs +0 -101
- package/src/provider/google.mjs +0 -257
- package/src/provider/index.mjs +0 -7
- package/src/provider/list-models.mjs +0 -93
- package/src/provider/normalize.mjs +0 -81
- package/src/provider/rate.mjs +0 -108
- package/src/provider/responses.mjs +0 -495
- package/src/provider/retry.mjs +0 -88
- package/src/provider/sse.mjs +0 -264
- package/src/proxy.mjs +0 -261
- package/src/rules.mjs +0 -53
- package/src/session-gc.mjs +0 -214
- package/src/session-guard.mjs +0 -47
- package/src/session-migrate.mjs +0 -48
- package/src/session-rename.mjs +0 -38
- package/src/session-slots.mjs +0 -489
- package/src/session.mjs +0 -475
- package/src/skills.mjs +0 -153
- package/src/token-ttl.mjs +0 -274
- package/src/tools/apply_patch.md +0 -15
- package/src/tools/bash.md +0 -37
- package/src/tools/bash.mjs +0 -268
- package/src/tools/checklist-sync.mjs +0 -181
- package/src/tools/checklist.md +0 -13
- package/src/tools/checklist.mjs +0 -299
- package/src/tools/delete.md +0 -13
- package/src/tools/edit-batch.mjs +0 -191
- package/src/tools/edit-diff.mjs +0 -348
- package/src/tools/edit.md +0 -30
- package/src/tools/execute.md +0 -21
- package/src/tools/execute.mjs +0 -228
- package/src/tools/fetch.md +0 -12
- package/src/tools/file.mjs +0 -469
- package/src/tools/file_ops.md +0 -17
- package/src/tools/get_current_time.md +0 -8
- package/src/tools/git-checkpoint.mjs +0 -143
- package/src/tools/git-ext.mjs +0 -173
- package/src/tools/git.md +0 -54
- package/src/tools/git.mjs +0 -356
- package/src/tools/glob-dialect.mjs +0 -130
- package/src/tools/glob.md +0 -11
- package/src/tools/grep.md +0 -19
- package/src/tools/hashline_edit.md +0 -14
- package/src/tools/index.mjs +0 -36
- package/src/tools/insert_after.md +0 -15
- package/src/tools/lint.md +0 -10
- package/src/tools/linter.mjs +0 -128
- package/src/tools/ls.md +0 -12
- package/src/tools/lsp.md +0 -10
- package/src/tools/lsp.mjs +0 -316
- package/src/tools/ops.mjs +0 -299
- package/src/tools/patch.mjs +0 -282
- package/src/tools/process.md +0 -10
- package/src/tools/question.md +0 -16
- package/src/tools/question.mjs +0 -26
- package/src/tools/read.md +0 -20
- package/src/tools/read_image.md +0 -8
- package/src/tools/repomap.mjs +0 -314
- package/src/tools/search.mjs +0 -236
- package/src/tools/shared.mjs +0 -446
- package/src/tools/tree.md +0 -14
- package/src/tools/tree.mjs +0 -66
- package/src/tools/wait_for.md +0 -22
- package/src/tools/web.mjs +0 -224
- package/src/tools/websearch.md +0 -16
- package/src/tools/write.md +0 -11
- package/src/traces/trace-store.mjs +0 -224
package/src/log.mjs
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* log.mjs — 诊断事件日志(docs/design/LOGGING.md 权威规格)。
|
|
3
|
-
* 双端同构:thincoder/src/log.mjs 与 thincoder-vscode/src/log.mjs 同一实现语义
|
|
4
|
-
* (共享 ~/.thincoder/logs/、同格式、同事件面——F-L6)。
|
|
5
|
-
*
|
|
6
|
-
* 常驻骨架日志:回合/LLM/工具/子代理/挂起的关键事件——每行一个 JSON 事件,按天轮转
|
|
7
|
-
* (agent-YYYY-MM-DD.log),保留 1 天——用户零手动维护(F-L3/NF-L4)。问题发生时
|
|
8
|
-
* 直接 tail/grep 定位断点(根治"临时插桩再删"循环)。
|
|
9
|
-
*
|
|
10
|
-
* 纪律(实现侧逐条落实):
|
|
11
|
-
* - fire-and-forget:logEvent 失败静默降级(NF-L1)——主流程零影响。进程内首次写失败
|
|
12
|
-
* 即置死(_dead),当日不再尝试(磁盘满/权限错不逐事件空转)。
|
|
13
|
-
* - 单事件行 <512 字符(NF-L2):head(LLM ≤300 / 工具 ≤200)、err ≤200 由调用方截断,
|
|
14
|
-
* 本模块对任意字符串字段做上限兜底(head 300 / err 200 / 其余 120)+ 超长时丢可选字段。
|
|
15
|
-
* - 敏感字段零落盘(NF-L3/§2.4,2026-09-03 评审 refinement #7 匹配语义定稿):
|
|
16
|
-
* ① 字段名精确匹配黑名单(apiKey/designToken/password/secret/token,大小写不敏感;
|
|
17
|
-
* 另加防御性名称 authorization/proxy/proxyUri——防凭据型 URL/头字段泄漏——§2.5)
|
|
18
|
-
* → 整个字段丢弃;
|
|
19
|
-
* ② 内容只扫密钥形态(sk-xxx / Bearer xxx / key=… 等)→ 截断到形态之前
|
|
20
|
-
* ——宁可丢信息不漏密钥。
|
|
21
|
-
* 工具事件不记 args;URL 不入事件(llm/tool 事件从不携带 URL;err 文本经形态扫描)。
|
|
22
|
-
* - 摘要截断(B 方案,2026-09-03 用户裁定):截断处带 "…" 标记(截后仍 ≤上限)。
|
|
23
|
-
* - 测试隔离:node --test 进程(NODE_TEST_CONTEXT)默认不写盘——防测试事件污染真实
|
|
24
|
-
* 诊断日志(两端测试套件都会跑真实 agent 管线);显式设置 THINCODER_LOG_DIR 强制
|
|
25
|
-
* 写入该目录(log.test.mjs 用它隔离临时目录——refinement #6)。
|
|
26
|
-
* - 轮转清理(refinement #3——长驻进程覆盖):非仅启动时——每进程每日**首次写事件**
|
|
27
|
-
* 时顺带清理 >1 天的 agent-*.log(extension host 可长驻数月,启动清理覆盖不到)。
|
|
28
|
-
* - seq:每进程单调计数器。双端同写一个文件时 seq 会各自重复——定位同文件时序以
|
|
29
|
-
* ts 为准,seq 仅进程内参照(refinement #8)。
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
import { appendFileSync, mkdirSync, readdirSync, rmSync, existsSync, statSync } from "node:fs"
|
|
33
|
-
import { homedir } from "node:os"
|
|
34
|
-
import { join } from "node:path"
|
|
35
|
-
|
|
36
|
-
/** 单行长度硬上限(NF-L2) */
|
|
37
|
-
export const MAX_LINE = 512
|
|
38
|
-
/** 字段名精确黑名单(NF-L3/§2.4——2026-09-03 评审 refinement #7:与 §2.4 词表一致含 token) */
|
|
39
|
-
const BLACKLIST_FIELDS = new Set(["apikey", "designtoken", "password", "secret", "token", "authorization", "proxyuri", "proxy"])
|
|
40
|
-
/** 内容密钥形态扫描(大小写不敏感)——命中即截断到形态前 */
|
|
41
|
-
const SECRET_FORM = /(\bsk-[A-Za-z0-9_-]{6,}|\bBearer\s+[A-Za-z0-9._~+/=-]{6,}|\b(?:api[_-]?key|key|token|secret|password|pwd|passwd)\s*[=:]\s*['"]?[A-Za-z0-9._~+/=-]{6,})/i
|
|
42
|
-
/** 字符串字段长度兜底:head ≤300 / err ≤200 / 其余 ≤120 */
|
|
43
|
-
const FIELD_CAPS = { head: 300, err: 200 }
|
|
44
|
-
|
|
45
|
-
let _seq = 0
|
|
46
|
-
let _dead = false // 进程内写失败即死(NF-L1 静默降级——不逐事件重复空转)
|
|
47
|
-
let _cleanupDate = null // 本进程已执行过清理的日期(每日首次写时清一次)
|
|
48
|
-
|
|
49
|
-
/** 日志目录:THINCODER_LOG_DIR(测试隔离/override)> ~/.thincoder/logs(与 sessions/ 同域) */
|
|
50
|
-
export function logsDir() {
|
|
51
|
-
return process.env.THINCODER_LOG_DIR ?? join(homedir(), ".thincoder", "logs")
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** 今日日志文件路径(agent-YYYY-MM-DD.log) */
|
|
55
|
-
export function todayLogPath(now = new Date()) {
|
|
56
|
-
const ymd = now.toISOString().slice(0, 10)
|
|
57
|
-
return join(logsDir(), `agent-${ymd}.log`)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** 写门(测试隔离):test runner 进程(NODE_TEST_CONTEXT)默认跳过——除显式
|
|
61
|
-
* THINCODER_LOG_DIR override(log.test.mjs 隔离临时目录——refinement #6)。 */
|
|
62
|
-
function writeEnabled() {
|
|
63
|
-
if (_dead) return false
|
|
64
|
-
if (process.env.NODE_TEST_CONTEXT && !process.env.THINCODER_LOG_DIR) return false
|
|
65
|
-
return true
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** 清理 >1 天的 agent-*.log(NF-L4——refinement #3:机会式,非仅启动时)。
|
|
69
|
-
* 保留窗口(2026-09-03 code review #1 修正):以文件名日期的**当日结束**为龄基准——
|
|
70
|
-
* 今天+昨天的文件保留(任何事件至少留存 24h、最多 48h),早于昨天的删除。
|
|
71
|
-
* 无法按名解析日期的 agent-*.log 按 mtime 兜底(>1 天未写的删除)。
|
|
72
|
-
* 导出供测试直呼(T-L7/T-L7b)。静默:任何失败都不影响主流程。 */
|
|
73
|
-
export const LOG_RETENTION_MS = 24 * 60 * 60 * 1000
|
|
74
|
-
export function cleanupOldLogs(now = new Date()) {
|
|
75
|
-
const dir = logsDir()
|
|
76
|
-
if (!existsSync(dir)) return
|
|
77
|
-
let names
|
|
78
|
-
try {
|
|
79
|
-
names = readdirSync(dir)
|
|
80
|
-
} catch {
|
|
81
|
-
return
|
|
82
|
-
}
|
|
83
|
-
for (const name of names) {
|
|
84
|
-
if (!name.startsWith("agent-") || !name.endsWith(".log")) continue
|
|
85
|
-
const m = name.match(/^agent-(\d{4}-\d{2}-\d{2})\.log$/)
|
|
86
|
-
const ts = m ? Date.parse(m[1] + "T00:00:00Z") : NaN
|
|
87
|
-
// 文件名日期 → 龄以该日结束(ts+24h)计:事件在删除时至少已留存 24h
|
|
88
|
-
const ageMs = Number.isFinite(ts) ? now - (ts + LOG_RETENTION_MS) : (tryStatAge(dir, name, now) ?? Infinity)
|
|
89
|
-
if (ageMs > LOG_RETENTION_MS) {
|
|
90
|
-
try { rmSync(join(dir, name), { force: true }) } catch { /* 静默 */ }
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function tryStatAge(dir, name, now) {
|
|
96
|
-
try {
|
|
97
|
-
return now - statSync(join(dir, name)).mtimeMs
|
|
98
|
-
} catch {
|
|
99
|
-
return null
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* 写入一条事件(fire-and-forget)。fields 内所有字符串都经黑名单/截断处理。
|
|
105
|
-
* 事件行结构:{"ts":ISO,"ev":kind,"seq":N,...fields}——单行 JSON <512 字符。
|
|
106
|
-
*/
|
|
107
|
-
export function logEvent(kind, fields = {}) {
|
|
108
|
-
if (!writeEnabled()) return
|
|
109
|
-
const now = new Date()
|
|
110
|
-
// 机会式轮转清理:每进程每日首次写事件时执行一次(refinement #3——长驻进程覆盖)
|
|
111
|
-
const ymd = now.toISOString().slice(0, 10)
|
|
112
|
-
if (_cleanupDate !== ymd) {
|
|
113
|
-
_cleanupDate = ymd
|
|
114
|
-
cleanupOldLogs(now)
|
|
115
|
-
}
|
|
116
|
-
const entry = { ts: now.toISOString(), ev: kind, seq: ++_seq }
|
|
117
|
-
for (const [k, v] of Object.entries(fields ?? {})) {
|
|
118
|
-
if (v === undefined || v === null) continue
|
|
119
|
-
// ① 字段名精确黑名单 → 丢弃(token 族永不落盘)
|
|
120
|
-
if (BLACKLIST_FIELDS.has(String(k).toLowerCase())) continue
|
|
121
|
-
if (typeof v === "string") entry[k] = sanitizeString(k, v)
|
|
122
|
-
else if (typeof v === "number" || typeof v === "boolean") entry[k] = v
|
|
123
|
-
}
|
|
124
|
-
let line = JSON.stringify(entry)
|
|
125
|
-
// ② 防御性收尾(NF-L2):字段上限后仍超长 → 丢可选字段(保留 ts/ev/seq)直至 <512
|
|
126
|
-
while (line.length > MAX_LINE && Object.keys(entry).length > 3) {
|
|
127
|
-
for (const k of Object.keys(entry)) {
|
|
128
|
-
if (k !== "ts" && k !== "ev" && k !== "seq") { delete entry[k]; break }
|
|
129
|
-
}
|
|
130
|
-
line = JSON.stringify(entry)
|
|
131
|
-
}
|
|
132
|
-
try {
|
|
133
|
-
const dir = logsDir()
|
|
134
|
-
mkdirSync(dir, { recursive: true })
|
|
135
|
-
appendFileSync(join(dir, `agent-${ymd}.log`), line + "\n", "utf8")
|
|
136
|
-
} catch {
|
|
137
|
-
_dead = true // NF-L1:写失败静默降级——主流程零影响
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** 共享脱敏(§18.6 D-TR2——trace-store 复用;logEvent 自身保持"丢弃字段"语义,
|
|
142
|
-
* 本函数提供"遮蔽标记"语义——同一字段名黑名单 + 同一 SECRET_FORM 形态扫描,
|
|
143
|
-
* 不发明新遮蔽模式)。字段名命中黑名单 → "[REDACTED](整个字段遮蔽);内容命中
|
|
144
|
-
* 密钥形态 → 截断到形态前 + "[redacted]" 标记(宁可丢信息不漏密钥——§2.4)。 */
|
|
145
|
-
export function redactSecret(fieldKey, value) {
|
|
146
|
-
const s = String(value)
|
|
147
|
-
if (BLACKLIST_FIELDS.has(String(fieldKey).toLowerCase())) return "[REDACTED]"
|
|
148
|
-
const hit = s.match(SECRET_FORM)
|
|
149
|
-
return hit ? s.slice(0, hit.index) + "[redacted]" : s
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/** 内容级净化:密钥形态截断到形态前(②);超长截断 + "…" 标记。 */
|
|
153
|
-
export function sanitizeString(key, value) {
|
|
154
|
-
let s = String(value)
|
|
155
|
-
// 密钥形态 → 截断到形态前(宁可丢信息不漏密钥——§2.4)
|
|
156
|
-
const hit = s.match(SECRET_FORM)
|
|
157
|
-
if (hit) s = s.slice(0, hit.index)
|
|
158
|
-
const cap = FIELD_CAPS[key] ?? 120
|
|
159
|
-
if (s.length > cap) s = s.slice(0, cap - 1) + "…"
|
|
160
|
-
return s
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/** 错误文本提取:message + cause 链首条、压成单行、≤max(默认 200——NF-L2/§2.2) */
|
|
164
|
-
export function errText(err, max = 200) {
|
|
165
|
-
let msg = err?.message ?? String(err ?? "")
|
|
166
|
-
if (!msg && err?.cause) msg = String(err.cause)
|
|
167
|
-
if (err?.cause?.message && !/^LLM API error/.test(msg)) msg += ` (${err.cause.message})`
|
|
168
|
-
msg = msg.replace(/\s*\n\s*/g, " ").trim()
|
|
169
|
-
if (msg.length > max) msg = msg.slice(0, max - 1) + "…"
|
|
170
|
-
return msg
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/** 错误 kind 分类(llm:error 字段):timeout(网关超时/超时信号)/ abort(用户中止)/
|
|
174
|
-
* error(其余——API/网络/未分类)。signal 为请求中止信号(chat 调用点直传)。 */
|
|
175
|
-
export function classifyErr(err, signal) {
|
|
176
|
-
const msg = String(err?.message ?? "").toLowerCase() + " " + String(err?.cause?.message ?? "").toLowerCase()
|
|
177
|
-
if (/timeout|timed ?out|etimedout|time limit exceeded/i.test(msg)) return "timeout"
|
|
178
|
-
if (err?.name === "AbortError" || signal?.aborted) {
|
|
179
|
-
return signal?.reason?.name === "TimeoutError" ? "timeout" : "abort"
|
|
180
|
-
}
|
|
181
|
-
return "error"
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/** 文本头截取:前 max 字符、单行化(JSON 单行约束)、截断带 "…" 标记。
|
|
185
|
-
* paragraph=true(llm:done 响应头):取首段(首个空行前)——B 方案"首段文本"语义。 */
|
|
186
|
-
export function headText(text, max, { paragraph = false } = {}) {
|
|
187
|
-
let s = String(text ?? "")
|
|
188
|
-
if (paragraph) {
|
|
189
|
-
const p = s.split(/\n\s*\n/, 1)[0]
|
|
190
|
-
if (p.length < s.length) s = p
|
|
191
|
-
}
|
|
192
|
-
s = s.replace(/\s*\n+\s*/g, " ").trim()
|
|
193
|
-
if (s.length > max) s = s.slice(0, max - 1) + "…"
|
|
194
|
-
return s
|
|
195
|
-
}
|
package/src/markdown.mjs
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* markdown.mjs — markdown + frontmatter format for memory entries
|
|
3
|
-
* Zero-dependency parsing/serialization. Entry format see ARCHITECTURE-v2.md.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const VALID_TYPES = new Set(["rule", "knowledge", "decision", "pattern"])
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Parse a markdown entry.
|
|
10
|
-
* → { meta: { type, title, tags, author, created, embedding? }, content }
|
|
11
|
-
* Throws if frontmatter is missing or required fields absent.
|
|
12
|
-
*/
|
|
13
|
-
export function parseEntry(text) {
|
|
14
|
-
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/)
|
|
15
|
-
if (!match) throw new Error("entry missing frontmatter (expected --- ... ---)")
|
|
16
|
-
|
|
17
|
-
const meta = parseFrontmatter(match[1])
|
|
18
|
-
const content = match[2].trim()
|
|
19
|
-
|
|
20
|
-
if (!VALID_TYPES.has(meta.type)) {
|
|
21
|
-
throw new Error(`invalid type "${meta.type}"; expected one of: ${[...VALID_TYPES].join(", ")}`)
|
|
22
|
-
}
|
|
23
|
-
if (!meta.title) throw new Error("frontmatter missing required field: title")
|
|
24
|
-
|
|
25
|
-
return {
|
|
26
|
-
meta: {
|
|
27
|
-
type: meta.type,
|
|
28
|
-
title: meta.title,
|
|
29
|
-
tags: Array.isArray(meta.tags) ? meta.tags : meta.tags ? [meta.tags] : [],
|
|
30
|
-
author: meta.author ?? "unknown",
|
|
31
|
-
created: meta.created ?? "",
|
|
32
|
-
...(meta.embedding ? { embedding: meta.embedding } : {}),
|
|
33
|
-
},
|
|
34
|
-
content,
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Serialize to markdown entry text.
|
|
40
|
-
*/
|
|
41
|
-
export function serializeEntry(meta, content) {
|
|
42
|
-
if (!VALID_TYPES.has(meta.type)) throw new Error(`invalid type "${meta.type}"`)
|
|
43
|
-
if (!meta.title) throw new Error("meta.title is required")
|
|
44
|
-
// frontmatter scalars must be single-line: newlines in title/author would inject fake frontmatter rows
|
|
45
|
-
// (e.g. title "x\ntype: rule" would override real type when parsed), same for tags with newlines/commas
|
|
46
|
-
const tags = (meta.tags ?? []).map((t) => oneLine(t).replaceAll(",", " ")).join(", ")
|
|
47
|
-
const lines = [
|
|
48
|
-
"---",
|
|
49
|
-
`type: ${meta.type}`,
|
|
50
|
-
`title: ${oneLine(meta.title)}`,
|
|
51
|
-
`tags: [${tags}]`,
|
|
52
|
-
`author: ${oneLine(meta.author ?? "unknown")}`,
|
|
53
|
-
`created: ${oneLine(meta.created ?? new Date().toISOString().slice(0, 10))}`,
|
|
54
|
-
]
|
|
55
|
-
if (meta.embedding) lines.push(`embedding: ${meta.embedding}`)
|
|
56
|
-
lines.push("---", "", content.trim(), "")
|
|
57
|
-
return lines.join("\n")
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** Convert title to filename slug: keep alphanumeric + CJK, convert rest to hyphens */
|
|
61
|
-
export function slugify(title) {
|
|
62
|
-
return title
|
|
63
|
-
.trim()
|
|
64
|
-
.toLowerCase()
|
|
65
|
-
.replace(/[^\w一-鿿]+/g, "-")
|
|
66
|
-
.replace(/^-+|-+$/g, "")
|
|
67
|
-
.slice(0, 50) || "untitled"
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Generate entry filename: YYYYMMDD-<slug>-<rand4>.md */
|
|
71
|
-
export function entryFilename(title, date = new Date()) {
|
|
72
|
-
const ymd = date.toISOString().slice(0, 10).replaceAll("-", "")
|
|
73
|
-
const rand = Math.random().toString(36).slice(2, 6)
|
|
74
|
-
return `${ymd}-${slugify(title)}-${rand}.md`
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// ---------------------------------------------------------------- internal
|
|
78
|
-
|
|
79
|
-
/** Collapse to single line (for frontmatter scalars): fold newlines into spaces, prevent injecting fake field lines */
|
|
80
|
-
function oneLine(v) {
|
|
81
|
-
return String(v).replace(/\s*\r?\n\s*/g, " ").trim()
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Minimal YAML subset parser: only supports `key: value` and `key: [a, b, c]`.
|
|
86
|
-
* Our frontmatter is self-generated, no need for full YAML.
|
|
87
|
-
*/
|
|
88
|
-
export function parseFrontmatter(text) {
|
|
89
|
-
const meta = {}
|
|
90
|
-
for (const line of text.split(/\r?\n/)) {
|
|
91
|
-
const m = line.match(/^(\w[\w-]*)\s*:\s*(.*)$/)
|
|
92
|
-
if (!m) continue
|
|
93
|
-
const [, key, raw] = m
|
|
94
|
-
const value = raw.trim()
|
|
95
|
-
if (value.startsWith("[") && value.endsWith("]")) {
|
|
96
|
-
meta[key] = value
|
|
97
|
-
.slice(1, -1)
|
|
98
|
-
.split(",")
|
|
99
|
-
.map((s) => s.trim())
|
|
100
|
-
.filter(Boolean)
|
|
101
|
-
} else {
|
|
102
|
-
meta[key] = value
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return meta
|
|
106
|
-
}
|
package/src/mcp/helpers.mjs
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* mcp/helpers.mjs — MCP shared utility functions and constants
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export const INIT_TIMEOUT_MS = 30_000
|
|
6
|
-
export const CALL_TIMEOUT_MS = 120_000
|
|
7
|
-
export const ENDPOINT_WAIT_MS = 5_000
|
|
8
|
-
|
|
9
|
-
let nextRpcId = 0
|
|
10
|
-
/** Generate a unique incrementing RPC ID string */
|
|
11
|
-
export function rpcId() {
|
|
12
|
-
return String(++nextRpcId)
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Race a promise against a timeout, rejecting after ms milliseconds */
|
|
16
|
-
export function withTimeout(promise, ms) {
|
|
17
|
-
let timer
|
|
18
|
-
const timeout = new Promise((_, reject) => {
|
|
19
|
-
timer = setTimeout(() => reject(new Error(`timeout after ${ms}ms`)), ms)
|
|
20
|
-
timer.unref?.()
|
|
21
|
-
})
|
|
22
|
-
return Promise.race([promise.finally(() => clearTimeout(timer)), timeout])
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Quote a shell argument: wrap in double-quotes if it contains whitespace or quotes */
|
|
26
|
-
export function quoteArg(s) {
|
|
27
|
-
return /[\s"]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Convert an Authorization Bearer token into a WebSocket subprotocol.
|
|
31
|
-
* 2026-08-31 MCP 会诊 #10:原实现把 token 塞进 URL query——代理/网关日志会泄露凭证,
|
|
32
|
-
* 且无标准依据。Node 内置 WebSocket(undici)无法自定义请求头,MCP 生态的标准替代
|
|
33
|
-
* 通道是 subprotocol(`bearer.<token>`)。用户 URL 自带的 query token 不动(兼容)。
|
|
34
|
-
* @returns {{ url: string, protocols: string[] }} — protocols 为空数组表示无认证。 */
|
|
35
|
-
export function withAuthToken(wsUrl, authorization) {
|
|
36
|
-
if (!authorization) return { url: wsUrl, protocols: [] }
|
|
37
|
-
const token = authorization.replace(/^Bearer\s+/i, "")
|
|
38
|
-
let u
|
|
39
|
-
try {
|
|
40
|
-
u = new URL(wsUrl)
|
|
41
|
-
} catch {
|
|
42
|
-
throw new Error(`Invalid WebSocket URL: ${String(wsUrl).slice(0, 120)}`)
|
|
43
|
-
}
|
|
44
|
-
// subprotocol 不进入 URL/日志,token 不再注入 query
|
|
45
|
-
return { url: u.href, protocols: [`bearer.${token}`] }
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Sanitize a tool name: replace non-alphanumeric chars with underscores, cap at 64 chars */
|
|
49
|
-
export function sanitizeToolName(name) {
|
|
50
|
-
return name.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64)
|
|
51
|
-
}
|
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* mcp/transport-http.mjs — MCP HTTP + SSE transport (Streamable HTTP)
|
|
3
|
-
*/
|
|
4
|
-
import { rpcId, CALL_TIMEOUT_MS, ENDPOINT_WAIT_MS, INIT_TIMEOUT_MS, withTimeout } from "./helpers.mjs"
|
|
5
|
-
|
|
6
|
-
/** Create an MCP HTTP+SSE transport for Streamable HTTP servers */
|
|
7
|
-
export function httpTransport(baseURL, extraHeaders = {}) {
|
|
8
|
-
const url = baseURL.replace(/\/+$/, "")
|
|
9
|
-
let sessionId = null
|
|
10
|
-
let closed = false
|
|
11
|
-
let eventSource = null
|
|
12
|
-
// 2026-09-01 MCP.md §4 D-1:GET SSE 不可用(405/不支持)降级后的纯 POST 模式标记。
|
|
13
|
-
// POST-only server(如 glm-websearch)无流可断:isAlive 不得因 eventSource == null 判死。
|
|
14
|
-
let postOnly = false
|
|
15
|
-
let abortController = null
|
|
16
|
-
let postUrl = url
|
|
17
|
-
let legacySSE = false
|
|
18
|
-
let deadFired = false
|
|
19
|
-
let deadListeners = new Set()
|
|
20
|
-
|
|
21
|
-
/** 2026-08-31 MCP 会诊 P5:意外死亡通知(SSE 流断/error,非主动 close)。 */
|
|
22
|
-
const fireDead = (msg) => {
|
|
23
|
-
if (deadFired) return
|
|
24
|
-
deadFired = true
|
|
25
|
-
for (const cb of deadListeners) { try { cb(msg) } catch { /* listener error */ } }
|
|
26
|
-
}
|
|
27
|
-
const onDead = (cb) => { deadListeners.add(cb); return () => deadListeners.delete(cb) }
|
|
28
|
-
|
|
29
|
-
const headers = () => {
|
|
30
|
-
const h = { "Content-Type": "application/json", Accept: "text/event-stream, application/json", ...extraHeaders }
|
|
31
|
-
if (sessionId) h["Mcp-Session-Id"] = sessionId
|
|
32
|
-
return h
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const pending = new Map()
|
|
36
|
-
|
|
37
|
-
async function* parseSSE(response) {
|
|
38
|
-
const reader = response.body.getReader()
|
|
39
|
-
const decoder = new TextDecoder()
|
|
40
|
-
let buf = ""
|
|
41
|
-
let current = { data: "", event: "message" }
|
|
42
|
-
try {
|
|
43
|
-
while (true) {
|
|
44
|
-
const { done, value } = await reader.read()
|
|
45
|
-
if (done) break
|
|
46
|
-
buf += decoder.decode(value, { stream: true })
|
|
47
|
-
const lines = buf.split("\n")
|
|
48
|
-
buf = lines.pop() ?? ""
|
|
49
|
-
for (const raw of lines) {
|
|
50
|
-
const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw
|
|
51
|
-
if (line === "") {
|
|
52
|
-
if (current.data) {
|
|
53
|
-
yield { event: current.event, data: current.data.trimEnd() }
|
|
54
|
-
current = { data: "", event: "message" }
|
|
55
|
-
}
|
|
56
|
-
} else if (line.startsWith("data:")) {
|
|
57
|
-
current.data += (current.data ? "\n" : "") + line.slice(5).replace(/^ /, "")
|
|
58
|
-
} else if (line.startsWith("event:")) {
|
|
59
|
-
current.event = line.slice(6).trim()
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
} finally {
|
|
64
|
-
reader.releaseLock()
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async function openSSE() {
|
|
69
|
-
if (closed) return
|
|
70
|
-
abortController?.abort()
|
|
71
|
-
abortController = new AbortController()
|
|
72
|
-
const signal = AbortSignal.any([abortController.signal, AbortSignal.timeout(INIT_TIMEOUT_MS)])
|
|
73
|
-
const resp = await fetch(url, {
|
|
74
|
-
method: "GET",
|
|
75
|
-
headers: { Accept: "text/event-stream", ...extraHeaders },
|
|
76
|
-
signal,
|
|
77
|
-
})
|
|
78
|
-
if (!resp.ok) throw new Error(`SSE connect failed: HTTP ${resp.status}`)
|
|
79
|
-
eventSource = parseSSE(resp)
|
|
80
|
-
let endpointReady
|
|
81
|
-
const gotEndpoint = new Promise((resolve) => { endpointReady = resolve })
|
|
82
|
-
|
|
83
|
-
;(async () => {
|
|
84
|
-
try {
|
|
85
|
-
for await (const { event, data } of eventSource) {
|
|
86
|
-
if (closed) break
|
|
87
|
-
if (event === "endpoint") {
|
|
88
|
-
postUrl = new URL(data.trim(), url).href
|
|
89
|
-
legacySSE = true
|
|
90
|
-
endpointReady()
|
|
91
|
-
continue
|
|
92
|
-
}
|
|
93
|
-
try {
|
|
94
|
-
const msg = JSON.parse(data)
|
|
95
|
-
const resolver = pending.get(msg.id)
|
|
96
|
-
if (resolver) {
|
|
97
|
-
pending.delete(msg.id)
|
|
98
|
-
resolver(msg)
|
|
99
|
-
}
|
|
100
|
-
} catch { /* not JSON, ignore */ }
|
|
101
|
-
}
|
|
102
|
-
// 正常走完 = server 关闭了流(网络/对端退出)→ 非主动关闭视为死亡
|
|
103
|
-
if (!closed) fireDead("SSE stream ended")
|
|
104
|
-
} catch (error) {
|
|
105
|
-
const wasClosed = closed
|
|
106
|
-
if (!wasClosed) {
|
|
107
|
-
for (const [, resolve] of pending) resolve({ id: null, error: { code: -32000, message: `SSE error: ${error.message}` } })
|
|
108
|
-
pending.clear()
|
|
109
|
-
fireDead(`SSE error: ${error.message}`)
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
})()
|
|
113
|
-
|
|
114
|
-
const wait = new Promise((resolve) => {
|
|
115
|
-
const t = setTimeout(resolve, ENDPOINT_WAIT_MS)
|
|
116
|
-
t.unref?.()
|
|
117
|
-
})
|
|
118
|
-
await Promise.race([gotEndpoint, wait])
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async function postRequest(method, params) {
|
|
122
|
-
const id = rpcId()
|
|
123
|
-
const body = JSON.stringify({ jsonrpc: "2.0", id, method, params })
|
|
124
|
-
|
|
125
|
-
if (legacySSE) {
|
|
126
|
-
// 2026-08-31 MCP 会诊 #10:legacy 模式下 POST 若直接带回 JSON-RPC body(违规 server,
|
|
127
|
-
// 规范是响应经 GET SSE 流回)——原实现对 resp.ok 什么都不做 → pending 挂满 120s。
|
|
128
|
-
// 空 body = 规范行为(等待 SSE 流);非空 JSON = 违规 server,直接解析 resolve。
|
|
129
|
-
return new Promise((resolve) => {
|
|
130
|
-
pending.set(id, resolve)
|
|
131
|
-
fetch(postUrl, { method: "POST", headers: headers(), body, signal: AbortSignal.timeout(CALL_TIMEOUT_MS) })
|
|
132
|
-
.then(async (resp) => {
|
|
133
|
-
if (!resp.ok) {
|
|
134
|
-
pending.delete(id)
|
|
135
|
-
resolve({ id, error: { code: -32000, message: `POST failed: HTTP ${resp.status}` } })
|
|
136
|
-
return
|
|
137
|
-
}
|
|
138
|
-
const raw = await resp.text().catch(() => "")
|
|
139
|
-
if (raw.trim()) {
|
|
140
|
-
try {
|
|
141
|
-
const msg = JSON.parse(raw)
|
|
142
|
-
if (msg.id === id) { pending.delete(id); resolve(msg); return }
|
|
143
|
-
} catch { /* ignore — pending 交给 SSE 流 */ }
|
|
144
|
-
}
|
|
145
|
-
// 空体或非本次 id 的 JSON:保持 pending 等 GET SSE 流回包
|
|
146
|
-
})
|
|
147
|
-
.catch((e) => {
|
|
148
|
-
pending.delete(id)
|
|
149
|
-
resolve({ id, error: { code: -32000, message: `POST failed: ${e.message}` } })
|
|
150
|
-
})
|
|
151
|
-
}).finally(() => pending.delete(id))
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// 2026-08-31 MCP 会诊 P4:Streamable HTTP 规范路径(POST→202→GET SSE 回包)。
|
|
155
|
-
// 原实现非 legacy 分支从不注册 pending:202 空 body → resp.json() 抛 SyntaxError,
|
|
156
|
-
// 而 SSE 流里 pending.get(id) 永远 miss——每次调用挂满 120s。
|
|
157
|
-
// 现在两类通道统一:先注册 pending,POST 得到的结果(直接 body / SSE / 202 等待)
|
|
158
|
-
// 都经 pending resolve;SSE 流由 openSSE() 转发。200+JSON 直接 body 解析。
|
|
159
|
-
return new Promise((resolve) => {
|
|
160
|
-
pending.set(id, resolve)
|
|
161
|
-
fetch(postUrl, { method: "POST", headers: headers(), body, signal: AbortSignal.timeout(CALL_TIMEOUT_MS) })
|
|
162
|
-
.then(async (resp) => {
|
|
163
|
-
const ct = resp.headers.get("content-type") ?? ""
|
|
164
|
-
const newSessionId = resp.headers.get("Mcp-Session-Id")
|
|
165
|
-
if (newSessionId) sessionId = newSessionId
|
|
166
|
-
// 202: 已接受,响应将经 GET SSE 流回——pending 保持,由 openSSE 转发 resolve
|
|
167
|
-
if (resp.status === 202) return
|
|
168
|
-
if (!resp.ok) {
|
|
169
|
-
pending.delete(id)
|
|
170
|
-
resolve({ id, error: { code: -32000, message: `HTTP ${resp.status}` } })
|
|
171
|
-
return
|
|
172
|
-
}
|
|
173
|
-
if (ct.includes("text/event-stream")) {
|
|
174
|
-
// 响应体本身就是一条 SSE(一次性流):解析匹配该 id
|
|
175
|
-
try {
|
|
176
|
-
for await (const { data } of parseSSE(resp)) {
|
|
177
|
-
try {
|
|
178
|
-
const msg = JSON.parse(data)
|
|
179
|
-
if (msg.id === id) { pending.delete(id); resolve(msg); return }
|
|
180
|
-
} catch { /* skip */ }
|
|
181
|
-
}
|
|
182
|
-
pending.delete(id)
|
|
183
|
-
resolve({ id, error: { code: -32000, message: "No JSON-RPC response in SSE stream" } })
|
|
184
|
-
} catch (e) {
|
|
185
|
-
pending.delete(id)
|
|
186
|
-
resolve({ id, error: { code: -32000, message: `SSE response failed: ${e.message}` } })
|
|
187
|
-
}
|
|
188
|
-
return
|
|
189
|
-
}
|
|
190
|
-
// 直接 JSON body:拿掉 pending 立即解析
|
|
191
|
-
pending.delete(id)
|
|
192
|
-
try {
|
|
193
|
-
resolve(await resp.json())
|
|
194
|
-
} catch (e) {
|
|
195
|
-
resolve({ id, error: { code: -32000, message: `invalid JSON body: ${e.message}` } })
|
|
196
|
-
}
|
|
197
|
-
})
|
|
198
|
-
.catch((e) => {
|
|
199
|
-
if (pending.delete(id)) {
|
|
200
|
-
resolve({ id, error: { code: -32000, message: `POST failed: ${e.message}` } })
|
|
201
|
-
}
|
|
202
|
-
})
|
|
203
|
-
}).finally(() => pending.delete(id))
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const send = async (method, params) => withTimeout(postRequest(method, params), CALL_TIMEOUT_MS)
|
|
207
|
-
|
|
208
|
-
const notify = (method, params) => {
|
|
209
|
-
fetch(postUrl, {
|
|
210
|
-
method: "POST",
|
|
211
|
-
headers: headers(),
|
|
212
|
-
body: JSON.stringify({ jsonrpc: "2.0", method, params }),
|
|
213
|
-
signal: AbortSignal.timeout(10_000),
|
|
214
|
-
}).catch((e) => {
|
|
215
|
-
// notify is fire-and-forget by design, but log network errors for debugging
|
|
216
|
-
if (e.name !== "AbortError") console.error(`[mcp] notify failed: ${e.message}`)
|
|
217
|
-
})
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const close = () => {
|
|
221
|
-
closed = true
|
|
222
|
-
abortController?.abort()
|
|
223
|
-
if (sessionId) {
|
|
224
|
-
fetch(postUrl, {
|
|
225
|
-
method: "DELETE",
|
|
226
|
-
headers: { "Mcp-Session-Id": sessionId, ...extraHeaders },
|
|
227
|
-
signal: AbortSignal.timeout(5_000),
|
|
228
|
-
}).catch((e) => {
|
|
229
|
-
// close is best-effort cleanup; log but don't throw
|
|
230
|
-
if (e.name !== "AbortError") console.error(`[mcp] close DELETE failed: ${e.message}`)
|
|
231
|
-
})
|
|
232
|
-
sessionId = null
|
|
233
|
-
}
|
|
234
|
-
for (const [, resolve] of pending) resolve({ id: null, error: { code: -32000, message: "Connection closed" } })
|
|
235
|
-
pending.clear()
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/** D-1:openSSE 降级(GET 405/不支持)后由 connect 链调用——标记纯 Streamable POST 模式 */
|
|
239
|
-
const markPostOnly = () => { postOnly = true }
|
|
240
|
-
|
|
241
|
-
return {
|
|
242
|
-
send, notify, close, openSSE, url, headers: extraHeaders,
|
|
243
|
-
// F1:POST-only 降级(postOnly)与 legacy SSE 流(eventSource)都算活连接——
|
|
244
|
-
// 不得因 eventSource == null 误判死(glm-websearch "reconnect failed" 根因)。
|
|
245
|
-
isAlive: () => !closed && (eventSource != null || postOnly),
|
|
246
|
-
onDead, markPostOnly,
|
|
247
|
-
}
|
|
248
|
-
}
|