rcs-context 0.0.0-stage → 0.1.39
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/LICENSE.txt +21 -0
- package/README.md +194 -2
- package/condense/chunk.mjs +258 -0
- package/condense/ivr.mjs +104 -0
- package/contextinjector-webui/README.md +43 -0
- package/contextinjector-webui/lib/client.js +3034 -0
- package/contextinjector-webui/lib/index.mjs +1565 -0
- package/contextinjector-webui/lib/trace-read.mjs +180 -0
- package/contextinjector-webui/package.json +21 -0
- package/cordis.patch.yml +25 -0
- package/package.json +66 -4
- package/toolfold/toolfold.mjs +513 -0
- package/web/contextinjector.mjs +3139 -0
- package/web/extreason.mjs +1107 -0
- package/web/ledger-online.mjs +229 -0
- package/web/logcompiler.mjs +369 -0
- package/web/memory-durability.mjs +431 -0
- package/web/memory-janitor.mjs +551 -0
- package/web/memory-ledger-core.mjs +1331 -0
- package/web/memory-ledger.mjs +836 -0
- package/web/memory-observations.mjs +430 -0
- package/web/memory-online-bridge.mjs +688 -0
- package/web/memory-proposal-store.mjs +1054 -0
- package/web/memory-proposals.mjs +2306 -0
- package/web/memory-trace.mjs +533 -0
- package/web/memory-vec.mjs +241 -0
- package/web/memory.mjs +5801 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
// Copyright (c) 2026 ScreamingMaggot. This source code is licensed under the MIT License.
|
|
2
|
+
// toolfold.mjs — 工具结果折叠:结构化短行 [T] 升级 + 值保真链 [V](A档最小集,纯规则)
|
|
3
|
+
// 规格:docs/seminar/Draft of the tool result collapse rules.md (§1.1/1.2/1.3)
|
|
4
|
+
// 行格式(旧档前缀兼容):
|
|
5
|
+
// [T] tool|action|target|status|P<n>|k=v|k="verbatim"|bare...
|
|
6
|
+
// [V] <seq>|tool|action|target|field|<verbatim 原文(单次转义)>
|
|
7
|
+
// status ∈ OK|FAIL|PARTIAL|CANCEL;P3 字段强制同时写 [V];纯规则、无 LLM、正文不回流。
|
|
8
|
+
// 与 DSH 解耦、可离线单测:输入 (action, args, resultText, isError, {fs})。
|
|
9
|
+
import { createHash } from 'node:crypto'
|
|
10
|
+
|
|
11
|
+
const C = { HEAD: 200, TAIL: 200, TEE_BYTES: 2048, GREP_MATCHES: 10, GLOB_MATCHES: 20,
|
|
12
|
+
READ_HEAD_LINES: 24, READ_HEAD_CHARS: 800, READ_TAIL_LINES: 8, READ_TAIL_CHARS: 280, READ_OUTLINE_CHARS: 320 }
|
|
13
|
+
|
|
14
|
+
// read 预览:确定性抽取,不做摘要。目的不是替代原文,而是让模型在折叠后仍知道
|
|
15
|
+
// “这个文件是什么、开头/结尾长什么样、有哪些结构符号”,避免把 8KB 源码压成一行
|
|
16
|
+
// `chars=8365` 后被迫重读。正文仍可经 [T] 的 raw=<seq> 精确展开。
|
|
17
|
+
const CODE_EXT = /\.(?:py|pyw|js|mjs|cjs|ts|tsx|jsx|java|go|rs|c|h|cc|cpp|hpp|cs|rb|php|sh|ps1|tex|md|ya?ml|toml)$/i
|
|
18
|
+
function clipHead(s, n) {
|
|
19
|
+
const t = String(s ?? '')
|
|
20
|
+
if (t.length <= n) return { text: t, cut: false }
|
|
21
|
+
return { text: t.slice(0, n), cut: true }
|
|
22
|
+
}
|
|
23
|
+
// 大纲不是 AST:按语言做行级词法匹配,但补齐了正则方案最容易错的三件事——
|
|
24
|
+
// 多行签名拼接、`const x = (...) =>` 与普通括号表达式的区分、类/impl 上下文前缀。
|
|
25
|
+
const JS_EXT = new Set(['js', 'mjs', 'cjs', 'jsx', 'ts', 'tsx'])
|
|
26
|
+
const CONTROL_NAMES = new Set(['if', 'for', 'while', 'switch', 'catch', 'with', 'return', 'new', 'await', 'yield', 'super', 'this', 'do', 'else', 'try', 'finally', 'foreach', 'using', 'lock', 'fixed'])
|
|
27
|
+
const OUTLINE_RULES = {
|
|
28
|
+
js: [
|
|
29
|
+
{ kind: 'class', ctx: true, re: /^(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+([A-Za-z_$][\w$]*)/ },
|
|
30
|
+
{ kind: 'type', re: /^(?:export\s+)?(?:default\s+)?(?:interface|enum)\s+([A-Za-z_$][\w$]*)/ },
|
|
31
|
+
{ kind: 'type', re: /^(?:export\s+)?type\s+([A-Za-z_$][\w$]*)\s*=/ },
|
|
32
|
+
{ kind: 'function', re: /^(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s*([A-Za-z_$][\w$]*)\s*\(/ },
|
|
33
|
+
// 箭头函数必须出现 `=>`;参数表不嵌套括号,避免把 `const x = (() => ...)` 这类 IIFE 当函数。
|
|
34
|
+
{ kind: 'function', re: /^(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?(?:function\b|\([^()]*\)\s*=>|[A-Za-z_$][\w$]*\s*=>)/ },
|
|
35
|
+
{ kind: 'method', method: true, re: /^(?:(?:static|async|get|set|override|public|private|protected|readonly)\s+)*([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*(?::[^{]+)?$/ },
|
|
36
|
+
],
|
|
37
|
+
py: [
|
|
38
|
+
{ kind: 'class', re: /^class\s+([A-Za-z_]\w*)/ },
|
|
39
|
+
{ kind: 'def', re: /^(?:async\s+)?def\s+([A-Za-z_]\w*)\s*\(/ },
|
|
40
|
+
],
|
|
41
|
+
go: [
|
|
42
|
+
{ kind: 'type', re: /^type\s+([A-Za-z_]\w*)\s+(?:struct|interface)\b/ },
|
|
43
|
+
{ kind: 'function', re: /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)\s*\(/ },
|
|
44
|
+
],
|
|
45
|
+
rs: [
|
|
46
|
+
{ kind: 'class', ctx: true, re: /^(?:pub(?:\([^)]*\))?\s+)?(?:struct|enum|trait)\s+([A-Za-z_]\w*)/ },
|
|
47
|
+
{ kind: 'class', ctx: true, re: /^(?:unsafe\s+)?impl(?:<[^>]*>)?\s+(?:[\w:<>]+\s+for\s+)?([A-Za-z_]\w*)/ },
|
|
48
|
+
{ kind: 'mod', ctx: true, re: /^(?:pub(?:\([^)]*\))?\s+)?mod\s+([A-Za-z_]\w*)/ },
|
|
49
|
+
{ kind: 'function', re: /^(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?:unsafe\s+)?(?:const\s+)?fn\s+([A-Za-z_]\w*)\s*[<(]/ },
|
|
50
|
+
],
|
|
51
|
+
jvm: [
|
|
52
|
+
{ kind: 'class', ctx: true, re: /^(?:(?:public|protected|private|static|final|abstract|sealed|non-sealed|strictfp|partial)\s+)*(?:class|interface|enum|record)\s+([A-Za-z_$][\w$]*)/ },
|
|
53
|
+
{ kind: 'method', method: true, re: /^(?:(?:public|protected|private|static|final|abstract|synchronized|native|default|strictfp|virtual|override|async|sealed|internal|extern|unsafe)\s+)+(?:[\w$<>\[\],.?\s]+\s+)?([A-Za-z_$][\w$]*)\s*\([^;]*\)\s*(?:throws\s+[^{]+)?$/ },
|
|
54
|
+
],
|
|
55
|
+
c: [
|
|
56
|
+
{ kind: 'class', ctx: true, re: /^(?:typedef\s+)?(?:struct|union|enum)\s+([A-Za-z_]\w*)\s*$/ },
|
|
57
|
+
{ kind: 'function', re: /^(?!\s*(?:if|for|while|switch|catch|return|sizeof|else|do|case|goto)\b)(?:[A-Za-z_][\w \t*&:<>,]*?)\b([A-Za-z_]\w*)\s*\([^;]*\)$/ },
|
|
58
|
+
],
|
|
59
|
+
rb: [
|
|
60
|
+
{ kind: 'class', re: /^(?:class|module)\s+([A-Za-z_]\w*)/ },
|
|
61
|
+
{ kind: 'function', re: /^def\s+(?:self\.)?([A-Za-z_]\w*[!?=]?)/ },
|
|
62
|
+
],
|
|
63
|
+
php: [
|
|
64
|
+
{ kind: 'class', ctx: true, re: /^(?:(?:abstract|final)\s+)?(?:class|interface|trait|enum)\s+([A-Za-z_]\w*)/ },
|
|
65
|
+
{ kind: 'method', method: true, re: /^(?:(?:public|private|protected|static|abstract|final)\s+)*function\s+([A-Za-z_]\w*)\s*\([^)]*\)\s*(?::\s*[^{]+)?$/ },
|
|
66
|
+
{ kind: 'function', re: /^function\s+([A-Za-z_]\w*)\s*\(/ },
|
|
67
|
+
],
|
|
68
|
+
sh: [
|
|
69
|
+
{ kind: 'function', re: /^(?:function\s+)?([A-Za-z_]\w*)\s*\(\s*\)\s*$/ },
|
|
70
|
+
{ kind: 'function', re: /^function\s+([A-Za-z_]\w*)\s*$/ },
|
|
71
|
+
],
|
|
72
|
+
ps1: [
|
|
73
|
+
{ kind: 'function', re: /^function\s+([A-Za-z_][\w-]*)/ },
|
|
74
|
+
],
|
|
75
|
+
tex: [
|
|
76
|
+
{ kind: 'section', re: /^\\(?:part|chapter|section|subsection|subsubsection)\*?\{([^}]*)\}/ },
|
|
77
|
+
],
|
|
78
|
+
md: [
|
|
79
|
+
{ kind: 'heading', nameGroup: 2, re: /^(#{1,3})\s+(.+)$/ },
|
|
80
|
+
],
|
|
81
|
+
}
|
|
82
|
+
function rulesFor(ext) {
|
|
83
|
+
if (JS_EXT.has(ext)) return OUTLINE_RULES.js
|
|
84
|
+
if (ext === 'py' || ext === 'pyw') return OUTLINE_RULES.py
|
|
85
|
+
if (ext === 'go') return OUTLINE_RULES.go
|
|
86
|
+
if (ext === 'rs') return OUTLINE_RULES.rs
|
|
87
|
+
if (ext === 'java' || ext === 'cs') return OUTLINE_RULES.jvm
|
|
88
|
+
if (ext === 'c' || ext === 'h' || ext === 'cc' || ext === 'cpp' || ext === 'hpp') return OUTLINE_RULES.c
|
|
89
|
+
if (ext === 'rb') return OUTLINE_RULES.rb
|
|
90
|
+
if (ext === 'php') return OUTLINE_RULES.php
|
|
91
|
+
if (ext === 'sh') return OUTLINE_RULES.sh
|
|
92
|
+
if (ext === 'ps1') return OUTLINE_RULES.ps1
|
|
93
|
+
if (ext === 'tex') return OUTLINE_RULES.tex
|
|
94
|
+
if (ext === 'md') return OUTLINE_RULES.md
|
|
95
|
+
return []
|
|
96
|
+
}
|
|
97
|
+
// 宿主 read 结果带 `N: ` 行号前缀(以及 <content> 包装),结构行必须先剥掉它。
|
|
98
|
+
function outlineLine(raw) {
|
|
99
|
+
return String(raw ?? '').replace(/^\s*\d+:\s?/, '').trim()
|
|
100
|
+
}
|
|
101
|
+
function outlineIndent(raw) {
|
|
102
|
+
const s = String(raw ?? '').replace(/^\s*\d+:\s?/, '')
|
|
103
|
+
return /^[ \t]*/.exec(s)[0].replace(/\t/g, ' ').length
|
|
104
|
+
}
|
|
105
|
+
function tidySignature(s) {
|
|
106
|
+
return String(s ?? '')
|
|
107
|
+
.replace(/\s+/g, ' ')
|
|
108
|
+
.replace(/\(\s+/g, '(')
|
|
109
|
+
.replace(/\s+\)/g, ')')
|
|
110
|
+
.replace(/\s*,\s*/g, ', ')
|
|
111
|
+
.replace(/,\s*\)/g, ')')
|
|
112
|
+
.replace(/\s*[:{]\s*$/, '')
|
|
113
|
+
.trim()
|
|
114
|
+
}
|
|
115
|
+
function needsMoreSignature(s) {
|
|
116
|
+
const t = String(s).replace(/\s*\{\s*$/, '')
|
|
117
|
+
let p = 0, b = 0, c = 0
|
|
118
|
+
for (const ch of t) {
|
|
119
|
+
if (ch === '(') p++
|
|
120
|
+
else if (ch === ')') p--
|
|
121
|
+
else if (ch === '[') b++
|
|
122
|
+
else if (ch === ']') b--
|
|
123
|
+
else if (ch === '{') c++
|
|
124
|
+
else if (ch === '}') c--
|
|
125
|
+
}
|
|
126
|
+
return p > 0 || b > 0 || c > 0 || /\\$/.test(t)
|
|
127
|
+
}
|
|
128
|
+
function signatureAt(lines, i) {
|
|
129
|
+
let text = outlineLine(lines[i])
|
|
130
|
+
for (let j = i + 1; j < Math.min(lines.length, i + 6) && needsMoreSignature(text); j++) {
|
|
131
|
+
const next = outlineLine(lines[j])
|
|
132
|
+
if (!next) break
|
|
133
|
+
text += ' ' + next
|
|
134
|
+
}
|
|
135
|
+
return tidySignature(text)
|
|
136
|
+
}
|
|
137
|
+
// 去掉函数/类的块体,但保留参数里的默认值对象(`opts = {}` 的花括号在括号内,不能切)。
|
|
138
|
+
function cutBody(s) {
|
|
139
|
+
let p = 0, b = 0, c = 0, last = -1
|
|
140
|
+
for (let i = 0; i < s.length; i++) {
|
|
141
|
+
const ch = s[i]
|
|
142
|
+
if (ch === '(') p++
|
|
143
|
+
else if (ch === ')') p--
|
|
144
|
+
else if (ch === '[') b++
|
|
145
|
+
else if (ch === ']') b--
|
|
146
|
+
else if (ch === '{') {
|
|
147
|
+
if (p === 0 && b === 0 && c === 0) last = i
|
|
148
|
+
c++
|
|
149
|
+
} else if (ch === '}') c--
|
|
150
|
+
}
|
|
151
|
+
return last > 0 ? s.slice(0, last).trimEnd() : s
|
|
152
|
+
}
|
|
153
|
+
function braceDelta(line) {
|
|
154
|
+
const s = String(line).replace(/`(?:\\.|[^`\\])*`|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, '""').replace(/\/\/.*$/, '')
|
|
155
|
+
let d = 0
|
|
156
|
+
for (const ch of s) {
|
|
157
|
+
if (ch === '{') d++
|
|
158
|
+
else if (ch === '}') d--
|
|
159
|
+
}
|
|
160
|
+
return d
|
|
161
|
+
}
|
|
162
|
+
function matchRule(line, rules, { inClass, indent }) {
|
|
163
|
+
for (const rule of rules) {
|
|
164
|
+
if (rule.method && (!inClass || indent <= 0)) continue
|
|
165
|
+
const m = rule.re.exec(line)
|
|
166
|
+
if (!m) continue
|
|
167
|
+
const name = m[rule.nameGroup ?? 1] ?? ''
|
|
168
|
+
if (rule.method && CONTROL_NAMES.has(name)) continue
|
|
169
|
+
return { kind: rule.kind, name, rule }
|
|
170
|
+
}
|
|
171
|
+
return null
|
|
172
|
+
}
|
|
173
|
+
export function codeOutline(lines, ext = '', limit = 8) {
|
|
174
|
+
const rules = rulesFor(ext)
|
|
175
|
+
if (!rules.length) return []
|
|
176
|
+
const found = []
|
|
177
|
+
const add = (decl, sig, ctx, isMethod, groupOrder = null, cat = 0) => {
|
|
178
|
+
let clean = tidySignature(sig)
|
|
179
|
+
if (!clean) return
|
|
180
|
+
// 单行函数体不能进大纲:箭头函数截到 `=>`,其余截到配平后的块体 `{`。
|
|
181
|
+
const arrow = /^(?:export\s+)?(?:const|let|var)\b/.test(clean) ? clean.indexOf('=>') : -1
|
|
182
|
+
if (arrow >= 0) clean = clean.slice(0, arrow + 2)
|
|
183
|
+
else if (decl.kind === 'type') clean = clean.replace(/\s*=.*$/, '')
|
|
184
|
+
else if (decl.kind !== 'section' && decl.kind !== 'heading') clean = cutBody(clean)
|
|
185
|
+
const at = decl.name ? clean.indexOf(decl.name) : -1
|
|
186
|
+
const tail = at >= 0 ? clean.slice(at) : clean
|
|
187
|
+
const label = clipHead(isMethod ? (ctx ? ctx + '.' : '') + tail : clean, 100).text
|
|
188
|
+
const order = found.length
|
|
189
|
+
found.push({ label, cat, group: groupOrder ?? order, order })
|
|
190
|
+
}
|
|
191
|
+
if (ext === 'py' || ext === 'pyw') {
|
|
192
|
+
const stack = []
|
|
193
|
+
for (let i = 0; i < lines.length; i++) {
|
|
194
|
+
const line = outlineLine(lines[i])
|
|
195
|
+
// 注释/包装行不是语句,不能让它们参与缩进 dedent,否则类下面的方法会丢上下文。
|
|
196
|
+
if (!line || line.startsWith('#') || line.startsWith('<')) continue
|
|
197
|
+
const indent = outlineIndent(lines[i])
|
|
198
|
+
while (stack.length && indent <= stack[stack.length - 1].indent) stack.pop()
|
|
199
|
+
const decl = matchRule(cutBody(tidySignature(line)), rules, { inClass: stack.some((x) => x.kind === 'class'), indent })
|
|
200
|
+
if (!decl) continue
|
|
201
|
+
const classStack = stack.filter((x) => x.kind === 'class')
|
|
202
|
+
const classes = classStack.map((x) => x.name)
|
|
203
|
+
const isMethod = decl.kind === 'def' && classes.length > 0
|
|
204
|
+
const owner = isMethod ? classStack[classStack.length - 1] : null
|
|
205
|
+
const cat = isMethod ? Math.max(1, owner?.cat ?? 0) : stack.length === 0 ? 0 : 2
|
|
206
|
+
const order = found.length
|
|
207
|
+
add(decl, signatureAt(lines, i), classes.join('.'), isMethod, owner?.order ?? null, cat)
|
|
208
|
+
stack.push({ name: decl.name, indent, kind: decl.kind === 'class' ? 'class' : 'def', order, cat })
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
const stack = []
|
|
212
|
+
let depth = 0
|
|
213
|
+
let pending = null
|
|
214
|
+
for (let i = 0; i < lines.length; i++) {
|
|
215
|
+
const line = outlineLine(lines[i])
|
|
216
|
+
if (!line) continue
|
|
217
|
+
while (stack.length && depth < stack[stack.length - 1].depth) stack.pop()
|
|
218
|
+
const inClass = stack.length > 0 && depth === stack[stack.length - 1].depth
|
|
219
|
+
const decl = matchRule(cutBody(tidySignature(line)), rules, { inClass, indent: outlineIndent(lines[i]) })
|
|
220
|
+
if (decl) {
|
|
221
|
+
const isMethod = !!decl.rule.method || (inClass && decl.kind === 'function')
|
|
222
|
+
const ctx = stack.map((x) => x.name).join('.')
|
|
223
|
+
const order = found.length
|
|
224
|
+
const owner = stack.length ? stack[stack.length - 1] : null
|
|
225
|
+
const cat = isMethod ? Math.max(1, owner?.cat ?? 0) : depth <= 0 ? 0 : 2
|
|
226
|
+
add(decl, signatureAt(lines, i), ctx, isMethod, isMethod && owner ? owner.order : null, cat)
|
|
227
|
+
if (decl.rule.ctx) {
|
|
228
|
+
if (line.includes('{')) stack.push({ name: decl.name || tidySignature(line), order, cat, depth: depth + 1 })
|
|
229
|
+
else pending = { name: decl.name || tidySignature(line), order, cat }
|
|
230
|
+
}
|
|
231
|
+
} else if (pending && line.includes('{')) {
|
|
232
|
+
stack.push({ name: pending.name, order: pending.order, cat: pending.cat, depth: depth + 1 })
|
|
233
|
+
pending = null
|
|
234
|
+
}
|
|
235
|
+
depth += braceDelta(line)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// 顶层 → 成员 → 深层嵌套;同类按文件顺序,方法跟随其所属类,类永远排在自己的方法前。
|
|
239
|
+
found.sort((a, b) => a.cat - b.cat || a.group - b.group || a.order - b.order)
|
|
240
|
+
const out = []
|
|
241
|
+
for (const item of found) {
|
|
242
|
+
if (out.includes(item.label)) continue
|
|
243
|
+
out.push(item.label)
|
|
244
|
+
if (out.length >= limit) break
|
|
245
|
+
}
|
|
246
|
+
return out
|
|
247
|
+
}
|
|
248
|
+
export function readPreview(text, path) {
|
|
249
|
+
const s = String(text ?? '').replace(/\r\n?/g, '\n')
|
|
250
|
+
if (!s) return { head: '', tail: '', outline: '', truncated: false }
|
|
251
|
+
const lines = s.split('\n')
|
|
252
|
+
const headRaw = lines.slice(0, C.READ_HEAD_LINES).join('\n')
|
|
253
|
+
const head = clipHead(headRaw, C.READ_HEAD_CHARS)
|
|
254
|
+
const coveredByHead = headRaw.length
|
|
255
|
+
const wantTail = lines.length > C.READ_HEAD_LINES + C.READ_TAIL_LINES || head.cut
|
|
256
|
+
const tailRaw = wantTail && s.length > coveredByHead ? lines.slice(-C.READ_TAIL_LINES).join('\n') : ''
|
|
257
|
+
const tail = tailRaw.length > C.READ_TAIL_CHARS
|
|
258
|
+
? { text: tailRaw.slice(-C.READ_TAIL_CHARS), cut: true }
|
|
259
|
+
: { text: tailRaw, cut: false }
|
|
260
|
+
let outline = ''
|
|
261
|
+
const ext = (/\.([a-z0-9]+)$/i.exec(String(path ?? ''))?.[1] ?? '').toLowerCase()
|
|
262
|
+
if (CODE_EXT.test(String(path ?? '')) && lines.length > C.READ_HEAD_LINES) {
|
|
263
|
+
const items = codeOutline(lines, ext).filter((x) => !head.text.includes(x))
|
|
264
|
+
const picked = []
|
|
265
|
+
let used = 0
|
|
266
|
+
for (const item of items) {
|
|
267
|
+
const need = (picked.length ? 2 : 0) + item.length
|
|
268
|
+
if (used + need > C.READ_OUTLINE_CHARS) break
|
|
269
|
+
picked.push(item)
|
|
270
|
+
used += need
|
|
271
|
+
}
|
|
272
|
+
if (picked.length >= 1) outline = picked.join('; ')
|
|
273
|
+
}
|
|
274
|
+
return { head: head.text, tail: tail.text, outline, truncated: head.cut || tail.cut || lines.length > C.READ_HEAD_LINES }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
// ---- 转义(只做一次;值内含裸 | \ " 换行时整体引号包裹并转义)----
|
|
279
|
+
function escVal(raw, forceQuote = false) {
|
|
280
|
+
let s = String(raw == null ? '' : raw)
|
|
281
|
+
// ⚠ 必须连 \r 一起转义:本模块的输出是**行式**格式(一行一条 [T]/[V]),而 Windows 工具
|
|
282
|
+
// 正文普遍是 \r\n。只转 \n 会留下裸 CR,写进只增日志后 CR 自身被当换行 ⇒ 一行撑成多行,
|
|
283
|
+
// 面板 parseTranscript 与任何按行解析的下游都会读到垃圾续行(2026-09-29 重建事故日志时暴露)。
|
|
284
|
+
const special = /[|\\"\n\r]/.test(s)
|
|
285
|
+
if (!forceQuote && !special) return s
|
|
286
|
+
return '"' + s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\|/g, '\\|').replace(/\r/g, '\\r').replace(/\n/g, '\\n') + '"'
|
|
287
|
+
}
|
|
288
|
+
// 字段段:k=v(v 只在组装时转义一次)
|
|
289
|
+
const pair = (k, v, quote = false) => ({ k, v, quote })
|
|
290
|
+
|
|
291
|
+
// 【#2 修复(取自 RCS-0.1.0 返回包)】状态词表归一:结构化档用 OK|FAIL|PARTIAL|CANCEL,
|
|
292
|
+
// 注入器非结构化档用 OK|ERR(见 contextinjector `[T] op | path | ${isErr?'ERR':'OK'}`)。
|
|
293
|
+
// 不归一会导致"同一状态两套词"、下游匹配不上(反馈:"[T] 行 ok 解析不匹配")。
|
|
294
|
+
function normalizeStatus(s) {
|
|
295
|
+
const v = String(s ?? '').trim().toUpperCase()
|
|
296
|
+
if (v === 'ERR' || v === 'ERROR') return 'FAIL'
|
|
297
|
+
if (v === 'OK' || v === 'FAIL' || v === 'PARTIAL' || v === 'CANCEL') return v
|
|
298
|
+
return v || 'OK'
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 【#2 修复(取自 RCS-0.1.0 返回包)】兼容两种 [T] 行:
|
|
302
|
+
// ① 结构化(本模块产出):[T] tool|action|target|status|P<n>|k=v|… (≥5 字段)
|
|
303
|
+
// ② 非结构化(contextinjector 产出):[T] op | path | OK/ERR (3 字段)
|
|
304
|
+
// 旧实现要求 ≥5 字段 → 注入器的 [T] 行恒返回 null("ok 解析不匹配"根因)。
|
|
305
|
+
export function parseRow(line) {
|
|
306
|
+
const m = /^\[T\] (.*)$/.exec(String(line).trim())
|
|
307
|
+
if (!m) return null
|
|
308
|
+
const toks = tokenize(m[1])
|
|
309
|
+
if (toks.length >= 5) {
|
|
310
|
+
const [tool, action, target, status, grade] = toks.slice(0, 5)
|
|
311
|
+
const fields = {}
|
|
312
|
+
for (const t of toks.slice(5)) {
|
|
313
|
+
const eq = t.indexOf('=')
|
|
314
|
+
if (eq > 0) fields[t.slice(0, eq)] = t.slice(eq + 1)
|
|
315
|
+
else fields[t] = t
|
|
316
|
+
}
|
|
317
|
+
return { tool, action, target, status: normalizeStatus(status), grade, fields, format: 'structured' }
|
|
318
|
+
}
|
|
319
|
+
if (toks.length >= 3) {
|
|
320
|
+
// 非结构化:[T] op | path | OK/ERR(注入器用 " | " 分隔 → token 需 trim);
|
|
321
|
+
// 新转录还会追加 raw=/call= 指针,解析器容忍并收进 fields。
|
|
322
|
+
const [op, target, status] = toks.slice(0, 3).map((t) => t.trim())
|
|
323
|
+
const fields = {}
|
|
324
|
+
for (const rawTok of toks.slice(3)) {
|
|
325
|
+
const t = rawTok.trim()
|
|
326
|
+
const eq = t.indexOf('=')
|
|
327
|
+
if (eq > 0) fields[t.slice(0, eq)] = t.slice(eq + 1)
|
|
328
|
+
}
|
|
329
|
+
return { tool: op, action: op, target, status: normalizeStatus(status), grade: '', fields, format: 'plain' }
|
|
330
|
+
}
|
|
331
|
+
return null
|
|
332
|
+
}
|
|
333
|
+
function tokenize(str) {
|
|
334
|
+
const out = []; let cur = '', q = false, i = 0
|
|
335
|
+
while (i < str.length) {
|
|
336
|
+
const ch = str[i]
|
|
337
|
+
if (q) {
|
|
338
|
+
if (ch === '\\' && i + 1 < str.length) { cur += str[i + 1]; i += 2; continue }
|
|
339
|
+
if (ch === '"') { q = false; i++; continue }
|
|
340
|
+
cur += ch; i++; continue
|
|
341
|
+
}
|
|
342
|
+
if (ch === '"') { q = true; i++; continue }
|
|
343
|
+
if (ch === '|') { if (cur !== '') out.push(cur); cur = ''; i++; continue }
|
|
344
|
+
cur += ch; i++
|
|
345
|
+
}
|
|
346
|
+
if (cur !== '') out.push(cur)
|
|
347
|
+
return out
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ---- 通用 ---------------------------------------------------------------
|
|
351
|
+
export function statusOf(isError, extra = {}) {
|
|
352
|
+
if (isError) return 'FAIL'
|
|
353
|
+
if (extra.cancelled) return 'CANCEL'
|
|
354
|
+
if (extra.partial) return 'PARTIAL'
|
|
355
|
+
return 'OK'
|
|
356
|
+
}
|
|
357
|
+
function firstLine(text) { return String(text ?? '').split('\n')[0] ?? '' }
|
|
358
|
+
function headTail(text, h = C.HEAD, t = C.TAIL) {
|
|
359
|
+
const s = String(text == null ? '' : text)
|
|
360
|
+
return { head: s.slice(0, h), tail: s.slice(-t), len: s.length }
|
|
361
|
+
}
|
|
362
|
+
function targetOf(action, args = {}) {
|
|
363
|
+
for (const k of ['file_path', 'path', 'directory', 'pattern', 'query', 'url', 'job_id', 'goal_id', 'scope', 'cd', 'name']) {
|
|
364
|
+
if (typeof args[k] === 'string' && args[k]) return args[k]
|
|
365
|
+
}
|
|
366
|
+
if (action === 'ask_user_question' && typeof args.question === 'string') return args.question
|
|
367
|
+
return '-'
|
|
368
|
+
}
|
|
369
|
+
function fsMeta(fs, path) {
|
|
370
|
+
try {
|
|
371
|
+
if (!fs || !path || path === '-') return null
|
|
372
|
+
const buf = fs.readFileSync(path)
|
|
373
|
+
return {
|
|
374
|
+
size: buf.length,
|
|
375
|
+
sha8: createHash('sha256').update(buf).digest('hex').slice(0, 8),
|
|
376
|
+
lines: buf.toString('utf8').split('\n').length,
|
|
377
|
+
}
|
|
378
|
+
} catch { return null }
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ---- action 规则:返回 { status, grade, pairs:[{k,v,quote}], v:[{f,v}] } ----
|
|
382
|
+
const errPair = (pairs, v) => { pairs.push(pair('err', v, true)); return { f: 'err', v } }
|
|
383
|
+
function readRule({ args, text, isError, fs }) {
|
|
384
|
+
const pairs = []; const v = []
|
|
385
|
+
if (isError) { v.push(errPair(pairs, firstLine(text))); return { status: 'FAIL', grade: 'P1', pairs, v } }
|
|
386
|
+
const path = targetOf('read', args)
|
|
387
|
+
const meta = fsMeta(fs, path)
|
|
388
|
+
if (meta) pairs.push(pair('size', meta.size), pair('lines', meta.lines), pair('sha8', meta.sha8))
|
|
389
|
+
else pairs.push(pair('chars', String(text ?? '').length))
|
|
390
|
+
// 有界、确定性的正文预览:head/tail + 代码结构大纲;完整原文仍由 raw 指针承载。
|
|
391
|
+
const pv = readPreview(text, path)
|
|
392
|
+
if (pv.head) pairs.push(pair('head', pv.head, true))
|
|
393
|
+
if (pv.outline) pairs.push(pair('outline', pv.outline, true))
|
|
394
|
+
if (pv.tail) pairs.push(pair('tail', pv.tail, true))
|
|
395
|
+
if (pv.truncated) pairs.push(pair('trunc', 'mid'))
|
|
396
|
+
return { status: 'OK', grade: 'P1', pairs, v }
|
|
397
|
+
}
|
|
398
|
+
function writeRule({ args, text, isError, fs }) {
|
|
399
|
+
const pairs = []; const v = []
|
|
400
|
+
if (isError) { v.push(errPair(pairs, firstLine(text))); return { status: 'FAIL', grade: 'P1', pairs, v } }
|
|
401
|
+
const path = targetOf('write', args)
|
|
402
|
+
const meta = fsMeta(fs, path)
|
|
403
|
+
if (meta) pairs.push(pair('size', meta.size), pair('sha8', meta.sha8))
|
|
404
|
+
else pairs.push(pair('ok', 1))
|
|
405
|
+
// 精确 patch 统计:不摘写正文,只给行数增减;完整参数仍由 call=<seq> 承载。
|
|
406
|
+
const oc = args.old_string ?? args.old_str ?? args.oldText ?? null
|
|
407
|
+
const nc = args.new_string ?? args.new_str ?? args.newText ?? args.content ?? null
|
|
408
|
+
if (typeof oc === 'string' && oc !== '') pairs.push(pair('del', oc.split('\n').length))
|
|
409
|
+
if (typeof nc === 'string' && nc !== '') pairs.push(pair('ins', nc.split('\n').length))
|
|
410
|
+
return { status: 'OK', grade: 'P1', pairs, v }
|
|
411
|
+
}
|
|
412
|
+
function searchRule({ action, args, text, isError }) {
|
|
413
|
+
const pairs = []; const v = []
|
|
414
|
+
if (isError) { v.push(errPair(pairs, firstLine(text))); return { status: 'FAIL', grade: 'P2', pairs, v } }
|
|
415
|
+
const pat = args.pattern || args.query || args.scope || '-'
|
|
416
|
+
const lines = String(text ?? '').split('\n').filter((l) => l.trim())
|
|
417
|
+
const K = action === 'grep' ? C.GREP_MATCHES : C.GLOB_MATCHES
|
|
418
|
+
pairs.push(pair('pattern', pat), pair('count', lines.length))
|
|
419
|
+
if (lines.length) pairs.push(pair('matches', lines.slice(0, K).join('\\n')))
|
|
420
|
+
if (lines.length > K) pairs.push(pair('trunc', 'matches'))
|
|
421
|
+
return { status: 'OK', grade: 'P2', pairs, v }
|
|
422
|
+
}
|
|
423
|
+
function shellRule({ args, text, isError }) {
|
|
424
|
+
const pairs = []; const v = []
|
|
425
|
+
const { head, tail, len } = headTail(text)
|
|
426
|
+
pairs.push(pair('cmd', (args.cmd || args.command || args.arguments || '').slice(0, 80)),
|
|
427
|
+
pair('exit', isError ? '!=0' : '0'), pair('out_n', len))
|
|
428
|
+
if (len > 0) pairs.push(pair('head', head), pair('tail', tail))
|
|
429
|
+
if (len > C.TEE_BYTES) pairs.push(pair('tee', 'REQUIRED'))
|
|
430
|
+
if (isError) v.push(errPair(pairs, firstLine(text)))
|
|
431
|
+
return { status: isError ? 'FAIL' : 'OK', grade: 'P2', pairs, v }
|
|
432
|
+
}
|
|
433
|
+
// 宿主 working_directory:无参调用会把 cwd 设为工作区根,返回当前路径。
|
|
434
|
+
// 显式记录 cd 与 cwd,避免转录只看到"路径变了"却看不出是谁改的。
|
|
435
|
+
function workingDirRule({ args, text, isError }) {
|
|
436
|
+
const pairs = []; const v = []
|
|
437
|
+
const cd = typeof args?.cd === 'string' ? args.cd : (typeof args?.path === 'string' ? args.path : '')
|
|
438
|
+
if (cd) pairs.push(pair('cd', cd))
|
|
439
|
+
const cwd = firstLine(text).slice(0, 240)
|
|
440
|
+
if (isError) v.push(errPair(pairs, cwd))
|
|
441
|
+
else if (cwd) pairs.push(pair('cwd', cwd, true))
|
|
442
|
+
return { status: isError ? 'FAIL' : 'OK', grade: 'P1', pairs, v }
|
|
443
|
+
}
|
|
444
|
+
function askRule({ args, text, isError }) {
|
|
445
|
+
const pairs = []; const v = []
|
|
446
|
+
const val = String(text ?? '')
|
|
447
|
+
v.push({ f: 'val', v: val })
|
|
448
|
+
pairs.push(pair('q', args.question || '-', true))
|
|
449
|
+
if (isError) v.push(errPair(pairs, firstLine(text)))
|
|
450
|
+
return { status: isError ? 'FAIL' : 'OK', grade: 'P3', pairs, v }
|
|
451
|
+
}
|
|
452
|
+
function jobRule({ args, text, isError }) {
|
|
453
|
+
const pairs = []; const v = []
|
|
454
|
+
const id = args.job_id || firstLine(text).slice(0, 40) || '-'
|
|
455
|
+
const { tail, len } = headTail(text)
|
|
456
|
+
v.push({ f: 'job_id', v: id })
|
|
457
|
+
pairs.push(pair('job_id', id, true), pair('state', isError ? 'fail' : 'done'))
|
|
458
|
+
if (len) pairs.push(pair('out_n', len), pair('tail', tail))
|
|
459
|
+
if (isError) v.push(errPair(pairs, firstLine(text)))
|
|
460
|
+
return { status: isError ? 'FAIL' : 'OK', grade: 'P1', pairs, v }
|
|
461
|
+
}
|
|
462
|
+
function stateRule({ text, isError }) {
|
|
463
|
+
const pairs = []; const v = []
|
|
464
|
+
if (isError) v.push(errPair(pairs, firstLine(text)))
|
|
465
|
+
return { status: isError ? 'FAIL' : 'OK', grade: 'P0', pairs, v }
|
|
466
|
+
}
|
|
467
|
+
function imageRule({ isError }) {
|
|
468
|
+
const pairs = []; const v = []
|
|
469
|
+
if (isError) { v.push(errPair(pairs, 'decode-error')); return { status: 'FAIL', grade: 'P1', pairs, v } }
|
|
470
|
+
return { status: 'OK', grade: 'P1', pairs, v }
|
|
471
|
+
}
|
|
472
|
+
const RULES = {
|
|
473
|
+
read: readRule, write: writeRule, edit: writeRule, str_replace: writeRule,
|
|
474
|
+
glob: (x) => searchRule(Object.assign({}, x, { action: 'glob' })),
|
|
475
|
+
grep: (x) => searchRule(Object.assign({}, x, { action: 'grep' })),
|
|
476
|
+
pwsh: shellRule, bash: shellRule,
|
|
477
|
+
working_directory: workingDirRule,
|
|
478
|
+
ask_user_question: askRule, job_output: jobRule,
|
|
479
|
+
create_goal: stateRule, get_goal: stateRule, update_goal: stateRule,
|
|
480
|
+
job_list: stateRule, job_kill: stateRule, todo_write: stateRule, read_image: imageRule,
|
|
481
|
+
}
|
|
482
|
+
function fallbackRule({ text, isError }) {
|
|
483
|
+
const pairs = []; const v = []
|
|
484
|
+
// 未命中专用规则的工具(web_fetch/web_search/skill/present…)不能再只留 status:
|
|
485
|
+
// 保留有界头尾与总字节数,全文靠 [T] 的 raw 指针回取,避免"静默丢正文"。
|
|
486
|
+
const { head, tail, len } = headTail(text)
|
|
487
|
+
pairs.push(pair('out_n', len))
|
|
488
|
+
if (len > 0) pairs.push(pair('head', head))
|
|
489
|
+
if (len > C.HEAD) pairs.push(pair('tail', tail), pair('trunc', 'mid'))
|
|
490
|
+
if (isError) v.push(errPair(pairs, firstLine(text)))
|
|
491
|
+
return { status: isError ? 'FAIL' : 'OK', grade: 'P1', pairs, v }
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
let VSEQ = 0
|
|
495
|
+
// 组装:为一条工具结果生成 [T] 行 + 需要的 [V] 行
|
|
496
|
+
export function foldOne({ action, args = {}, text = '', isError = false, fs = null, tool = 'tool' }) {
|
|
497
|
+
const rule = RULES[action] || fallbackRule
|
|
498
|
+
const r = rule({ action, args, text, isError, fs })
|
|
499
|
+
const target = targetOf(action, args)
|
|
500
|
+
// 先写 [V](拿真实 seq),再在 [T] 里引用首条
|
|
501
|
+
let firstSeq = 0
|
|
502
|
+
const vs = []
|
|
503
|
+
for (const it of r.v || []) {
|
|
504
|
+
const seq = ++VSEQ
|
|
505
|
+
if (!firstSeq) firstSeq = seq
|
|
506
|
+
vs.push(`[V] ${seq}|${tool}|${action}|${target}|${it.f}|${escVal(it.v, true)}`)
|
|
507
|
+
}
|
|
508
|
+
const atom = [tool, action, target, r.status, r.grade].map((s) => escVal(s, false))
|
|
509
|
+
const out = r.pairs.map((p) => p.k + '=' + escVal(p.v, p.quote))
|
|
510
|
+
if (firstSeq) out.push('ref=V' + firstSeq)
|
|
511
|
+
const t = '[T] ' + atom.concat(out).join('|')
|
|
512
|
+
return { t, vs }
|
|
513
|
+
}
|