@yfwu2020/dsh-selection-explain 0.2.0 → 0.4.1
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/README.md +99 -4
- package/assets/quotes.png +0 -0
- package/assets/voice-inserted.png +0 -0
- package/assets/voice.png +0 -0
- package/lib/client.js +2623 -68
- package/lib/index.js +525 -2
- package/lib/index.js.map +1 -1
- package/lib/prompts.js +5 -0
- package/lib/prompts.js.map +1 -1
- package/lib/types/index.d.ts +56 -0
- package/package.json +6 -3
- package/scripts/build-demo-pages.mjs +58 -4
- package/scripts/build.sh +4 -2
- package/scripts/measure-cost.mjs +251 -0
- package/scripts/smoke-panel.mjs +481 -0
- package/scripts/smoke-voice.mjs +772 -0
- package/scripts/test-client.mjs +1357 -0
- package/scripts/test-prompt.mjs +15 -0
- package/scripts/test-speech.mjs +220 -0
- package/scripts/test-transcript.mjs +74 -1
- package/src/client/index.js +2623 -68
- package/src/index.ts +607 -2
- package/src/prompts.ts +5 -0
package/scripts/test-prompt.mjs
CHANGED
|
@@ -140,6 +140,21 @@ assert(
|
|
|
140
140
|
has(CHAT_SYSTEM_PROMPT, '完整的单文件 HTML', '```html', '样式和脚本都内联'),
|
|
141
141
|
'小窗会当场渲染成可交互页面,只给片段就渲染不出来',
|
|
142
142
|
)
|
|
143
|
+
assert(
|
|
144
|
+
'追问:【引用 N】块要当成提问材料(不许认成上一轮回答)',
|
|
145
|
+
has(CHAT_SYSTEM_PROMPT, '【引用 N】', '当成提问的一部分来答'),
|
|
146
|
+
'小窗的「❝ 引用」把引用拼进用户消息,模型得知道那不是它自己的前情',
|
|
147
|
+
)
|
|
148
|
+
assert(
|
|
149
|
+
'追问:【引用处上下文】要被当成判断依据(引用部分用【】标出)',
|
|
150
|
+
has(CHAT_SYSTEM_PROMPT, '【引用处上下文】', '被引用的部分用【】标出'),
|
|
151
|
+
'主界面/网页的引用自带"当时所在的那一组对话 ± 一组",模型要知道那是上下文而不是正文',
|
|
152
|
+
)
|
|
153
|
+
assert(
|
|
154
|
+
'追问:只写出处的引用要在本轮上下文里找得到它',
|
|
155
|
+
has(CHAT_SYSTEM_PROMPT, '小窗第 2 轮回答', '本来就在本轮上下文里'),
|
|
156
|
+
'小窗自己的对话不重复进引用块,靠出处标签指路',
|
|
157
|
+
)
|
|
143
158
|
|
|
144
159
|
// ───────────────────────── 卫生检查(防低级事故)─────────────────────────
|
|
145
160
|
const ALL = [PROMPT_HEAD, TRANSLATION_PROMPT, CODE_PROMPT, DETAIL_PROMPT, CHAT_SYSTEM_PROMPT].join('\n')
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 语音输入(小窗麦克风)的离线 + 在线测试。
|
|
3
|
+
*
|
|
4
|
+
* 两段:
|
|
5
|
+
* ① 离线:host 的 validateWave 与**客户端编码出来的格式**是同一份契约。
|
|
6
|
+
* 浏览器端只能产出 16kHz 单声道 PCM16 WAV(见 client 的 encodeWave),
|
|
7
|
+
* host 逐字段校验它 —— 这里逐条打表:每个字段错一位都必须被拒。
|
|
8
|
+
* 为什么抠这么细:这个校验是"音频进识别服务"的唯一闸门,漏一位就会让
|
|
9
|
+
* 识别服务按错误的采样率听一段快放(结果全错,而且没人知道为什么)。
|
|
10
|
+
* ② 在线(DSH 在跑时才跑):真路由
|
|
11
|
+
* GET /speech 目录与就绪状态
|
|
12
|
+
* POST /speech/transcribe 一段音频 → 文字
|
|
13
|
+
* 用合成的音频(正弦波/静音)验证"能跑通、坏输入被挡住";
|
|
14
|
+
* 真实的识别效果(真人语音 → 正确文字)由 scripts/smoke-voice.mjs 在真浏览器里跑。
|
|
15
|
+
* SPEECH_WAV=/tmp/x.wav 可以让它顺便验一句真话:那必须是规范 WAV
|
|
16
|
+
* (macOS 上可以用 `say -o /tmp/v.aiff "..."` + `ffmpeg -i /tmp/v.aiff -ac 1 -ar 16000
|
|
17
|
+
* -c:a pcm_s16le -fflags +bitexact /tmp/v.wav` 生成)。
|
|
18
|
+
*
|
|
19
|
+
* 用法:node scripts/test-speech.mjs
|
|
20
|
+
* SEL_ORIGIN=http://127.0.0.1:3080 node scripts/test-speech.mjs
|
|
21
|
+
* SPEECH_WAV=/tmp/v.wav node scripts/test-speech.mjs
|
|
22
|
+
* 退出码:全部 PASS = 0。
|
|
23
|
+
*/
|
|
24
|
+
import { readFileSync } from 'node:fs'
|
|
25
|
+
import { validateWave } from '../lib/index.js'
|
|
26
|
+
|
|
27
|
+
const ORIGIN = process.env.SEL_ORIGIN || 'http://127.0.0.1:3080'
|
|
28
|
+
const SPEECH_API = '/selection-explain/api/speech'
|
|
29
|
+
|
|
30
|
+
let pass = 0
|
|
31
|
+
let fail = 0
|
|
32
|
+
function check(name, ok, extra) {
|
|
33
|
+
if (ok) pass += 1
|
|
34
|
+
else fail += 1
|
|
35
|
+
console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${extra === undefined ? '' : ' — ' + extra}`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 抛错算通过(校验必须拒收)。 */
|
|
39
|
+
function rejects(name, run) {
|
|
40
|
+
try {
|
|
41
|
+
run()
|
|
42
|
+
check(name, false, '没有抛错(本该拒收)')
|
|
43
|
+
} catch (error) {
|
|
44
|
+
check(name, true, error.message)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ───────────────────────── ① 规范 WAV 契约(与客户端 encodeWave 同源) ─────────────────────────
|
|
49
|
+
|
|
50
|
+
/** 造一段规范 WAV(和客户端 encodeWave 逐字节同构)。 */
|
|
51
|
+
function makeWave(seconds, fill = 0.25) {
|
|
52
|
+
const samples = Math.round(seconds * 16000)
|
|
53
|
+
const bytes = Buffer.alloc(44 + samples * 2)
|
|
54
|
+
bytes.write('RIFF', 0, 'ascii')
|
|
55
|
+
bytes.writeUInt32LE(36 + samples * 2, 4)
|
|
56
|
+
bytes.write('WAVE', 8, 'ascii')
|
|
57
|
+
bytes.write('fmt ', 12, 'ascii')
|
|
58
|
+
bytes.writeUInt32LE(16, 16)
|
|
59
|
+
bytes.writeUInt16LE(1, 20)
|
|
60
|
+
bytes.writeUInt16LE(1, 22)
|
|
61
|
+
bytes.writeUInt32LE(16000, 24)
|
|
62
|
+
bytes.writeUInt32LE(32000, 28)
|
|
63
|
+
bytes.writeUInt16LE(2, 32)
|
|
64
|
+
bytes.writeUInt16LE(16, 34)
|
|
65
|
+
bytes.write('data', 36, 'ascii')
|
|
66
|
+
bytes.writeUInt32LE(samples * 2, 40)
|
|
67
|
+
for (let i = 0; i < samples; i += 1) {
|
|
68
|
+
bytes.writeInt16LE(Math.round(Math.sin(i / 20) * fill * 32767), 44 + i * 2)
|
|
69
|
+
}
|
|
70
|
+
return bytes
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const oneSecond = makeWave(1)
|
|
74
|
+
check('规范 WAV:1 秒 → 时长 1.0', validateWave(oneSecond, 60) === 1, String(validateWave(oneSecond, 60)))
|
|
75
|
+
check('规范 WAV:0.25 秒也认', Math.abs(validateWave(makeWave(0.25), 60) - 0.25) < 1e-6)
|
|
76
|
+
check('规范 WAV:正好 60 秒放行(边界含)', validateWave(makeWave(60), 60) === 60)
|
|
77
|
+
|
|
78
|
+
rejects('空录音(只有头,0 采样)被拒', () => validateWave(makeWave(0), 60))
|
|
79
|
+
rejects('超过时长上限被拒', () => validateWave(makeWave(61), 60))
|
|
80
|
+
rejects('太短的字节串被拒(连头都不全)', () => validateWave(Buffer.alloc(20), 60))
|
|
81
|
+
rejects('不是 RIFF 的被拒', () => {
|
|
82
|
+
const bad = makeWave(0.5)
|
|
83
|
+
bad.write('RIFX', 0, 'ascii')
|
|
84
|
+
return validateWave(bad, 60)
|
|
85
|
+
})
|
|
86
|
+
rejects('不是 WAVE 的被拒', () => {
|
|
87
|
+
const bad = makeWave(0.5)
|
|
88
|
+
bad.write('AVI ', 8, 'ascii')
|
|
89
|
+
return validateWave(bad, 60)
|
|
90
|
+
})
|
|
91
|
+
rejects('采样率 8kHz 被拒(会被当成快放听)', () => {
|
|
92
|
+
const bad = makeWave(0.5)
|
|
93
|
+
bad.writeUInt32LE(8000, 24)
|
|
94
|
+
bad.writeUInt32LE(16000, 28)
|
|
95
|
+
return validateWave(bad, 60)
|
|
96
|
+
})
|
|
97
|
+
rejects('采样率对、字节率不对也被拒(自相矛盾的头)', () => {
|
|
98
|
+
const bad = makeWave(0.5)
|
|
99
|
+
bad.writeUInt32LE(16000, 28)
|
|
100
|
+
return validateWave(bad, 60)
|
|
101
|
+
})
|
|
102
|
+
rejects('立体声被拒', () => {
|
|
103
|
+
const bad = makeWave(0.5)
|
|
104
|
+
bad.writeUInt16LE(2, 22)
|
|
105
|
+
return validateWave(bad, 60)
|
|
106
|
+
})
|
|
107
|
+
rejects('8 位深被拒', () => {
|
|
108
|
+
const bad = makeWave(0.5)
|
|
109
|
+
bad.writeUInt16LE(8, 34)
|
|
110
|
+
return validateWave(bad, 60)
|
|
111
|
+
})
|
|
112
|
+
rejects('非 PCM(浮点/压缩)被拒', () => {
|
|
113
|
+
const bad = makeWave(0.5)
|
|
114
|
+
bad.writeUInt16LE(3, 20)
|
|
115
|
+
return validateWave(bad, 60)
|
|
116
|
+
})
|
|
117
|
+
rejects('前面塞了别的 chunk(数据不在 44 偏移)被拒', () => {
|
|
118
|
+
const good = makeWave(0.5)
|
|
119
|
+
const bad = Buffer.concat([good.subarray(0, 36), Buffer.from('FLLR0000', 'ascii'), good.subarray(36)])
|
|
120
|
+
return validateWave(bad, 60)
|
|
121
|
+
})
|
|
122
|
+
rejects('RIFF 长度字段与实际不符被拒', () => {
|
|
123
|
+
const bad = makeWave(0.5)
|
|
124
|
+
bad.writeUInt32LE(bad.length, 4)
|
|
125
|
+
return validateWave(bad, 60)
|
|
126
|
+
})
|
|
127
|
+
rejects('data 长度字段与实际不符被拒', () => {
|
|
128
|
+
const bad = makeWave(0.5)
|
|
129
|
+
bad.writeUInt32LE(bad.length, 40)
|
|
130
|
+
return validateWave(bad, 60)
|
|
131
|
+
})
|
|
132
|
+
rejects('奇数采样字节(半截采样)被拒', () => {
|
|
133
|
+
const bad = makeWave(0.5).subarray(0, 44 + 1000 + 1)
|
|
134
|
+
bad.writeUInt32LE(bad.length - 8, 4)
|
|
135
|
+
bad.writeUInt32LE(bad.length - 44, 40)
|
|
136
|
+
return validateWave(bad, 60)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// ───────────────────────── ② 真路由(DSH 在跑才有意义) ─────────────────────────
|
|
140
|
+
|
|
141
|
+
async function online() {
|
|
142
|
+
let reachable = false
|
|
143
|
+
try {
|
|
144
|
+
const probe = await fetch(`${ORIGIN}${SPEECH_API}`)
|
|
145
|
+
reachable = probe.ok
|
|
146
|
+
} catch (error) {
|
|
147
|
+
reachable = false
|
|
148
|
+
}
|
|
149
|
+
if (!reachable) {
|
|
150
|
+
console.log(`\n=== 在线部分跳过:${ORIGIN} 不可达(DSH 没在跑)===`)
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const catalog = await (await fetch(`${ORIGIN}${SPEECH_API}`)).json()
|
|
155
|
+
check('目录接口:returns ok + limits', catalog.ok === true && !!catalog.limits, JSON.stringify(catalog.limits))
|
|
156
|
+
check(
|
|
157
|
+
'目录接口:报出识别器与各自的就绪状态',
|
|
158
|
+
Array.isArray(catalog.providers) && catalog.providers.every((p) => typeof p.id === 'string' && typeof p.phase === 'string'),
|
|
159
|
+
catalog.providers.map((p) => `${p.id}:${p.phase}`).join(',') || '(空)',
|
|
160
|
+
)
|
|
161
|
+
check('目录接口:available 与 providers 的状态自洽', catalog.available === catalog.providers.some((p) => p.phase === 'ready' || p.phase === 'standby'), `available=${catalog.available}`)
|
|
162
|
+
|
|
163
|
+
const post = (body) =>
|
|
164
|
+
fetch(`${ORIGIN}${SPEECH_API}/transcribe`, {
|
|
165
|
+
method: 'POST',
|
|
166
|
+
headers: { 'content-type': 'application/json' },
|
|
167
|
+
body: JSON.stringify(body),
|
|
168
|
+
}).then((response) => response.json())
|
|
169
|
+
|
|
170
|
+
const garbage = await post({ audioBase64: 'bm90LWF1ZGlv' })
|
|
171
|
+
check('坏 base64(不是音频)被拒且不报 500', garbage.ok === false && garbage.code === 'audio', JSON.stringify(garbage))
|
|
172
|
+
|
|
173
|
+
const notCanonical = await post({ audioBase64: makeWave(0.5).toString('base64').replace(/^/, '') })
|
|
174
|
+
check('规范 WAV 能进到识别(0.5 秒正弦)', typeof notCanonical.ok === 'boolean', JSON.stringify(notCanonical).slice(0, 120))
|
|
175
|
+
check(
|
|
176
|
+
'正弦波不会被"听"成话(空转写或明确失败,不会瞎编)',
|
|
177
|
+
notCanonical.ok === false ? notCanonical.code !== 'audio' : String(notCanonical.text || '').length < 40,
|
|
178
|
+
JSON.stringify(notCanonical).slice(0, 160),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
const wrongRate = makeWave(0.5)
|
|
182
|
+
wrongRate.writeUInt32LE(8000, 24)
|
|
183
|
+
const rejected = await post({ audioBase64: wrongRate.toString('base64') })
|
|
184
|
+
check('采样率不对的音频在 host 就被挡住(不进识别服务)', rejected.ok === false && rejected.code === 'audio', JSON.stringify(rejected))
|
|
185
|
+
|
|
186
|
+
// 超大请求:host 在流读完之前就 reject 并 destroy(和别的路由一致),
|
|
187
|
+
// 客户端这边看到的是连接被掐断而不是 JSON —— 两种都算"被挡住"。
|
|
188
|
+
let oversized = null
|
|
189
|
+
let oversizedError = ''
|
|
190
|
+
try {
|
|
191
|
+
oversized = await post({ audioBase64: Buffer.alloc(6 * 1024 * 1024).toString('base64') })
|
|
192
|
+
} catch (error) {
|
|
193
|
+
oversizedError = String(error && error.cause ? error.cause.message || error.cause : error)
|
|
194
|
+
}
|
|
195
|
+
check(
|
|
196
|
+
'超大音频被拒(不会把内存吃穿)',
|
|
197
|
+
oversizedError !== '' || (oversized && oversized.ok === false),
|
|
198
|
+
oversizedError ? '连接被掐断:' + oversizedError : JSON.stringify(oversized).slice(0, 120),
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
const method = await fetch(`${ORIGIN}${SPEECH_API}/transcribe`, { method: 'GET' })
|
|
202
|
+
check('转写接口只收 POST', method.status === 405, String(method.status))
|
|
203
|
+
|
|
204
|
+
// 真语音(可选):给了 SPEECH_WAV 就顺便验一句人话
|
|
205
|
+
if (process.env.SPEECH_WAV) {
|
|
206
|
+
const wav = readFileSync(process.env.SPEECH_WAV)
|
|
207
|
+
const seconds = validateWave(wav, 60)
|
|
208
|
+
const started = Date.now()
|
|
209
|
+
const result = await post({ audioBase64: wav.toString('base64') })
|
|
210
|
+
check('真语音:识别成功且拿到文字', result.ok === true && String(result.text).length > 0, JSON.stringify(result).slice(0, 200))
|
|
211
|
+
check('真语音:host 报了用时(本地模型也该在几秒内)', result.ok === true && Date.now() - started < 60000, `${Date.now() - started}ms / 音频 ${seconds.toFixed(2)}s`)
|
|
212
|
+
} else {
|
|
213
|
+
console.log('(跳过真语音识别:要验就设 SPEECH_WAV=/tmp/v.wav)')
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
await online()
|
|
218
|
+
|
|
219
|
+
console.log(`\n=== 语音测试结束:${pass} 通过 / ${fail} 失败 ===`)
|
|
220
|
+
process.exit(fail === 0 ? 0 : 1)
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* 用法:node scripts/test-transcript.mjs (先 npm run build)
|
|
11
11
|
*/
|
|
12
|
-
import { transcriptOf } from '../lib/index.js'
|
|
12
|
+
import { quoteContextOf, transcriptOf } from '../lib/index.js'
|
|
13
13
|
|
|
14
14
|
let failed = 0
|
|
15
15
|
const assert = (label, ok, extra) => {
|
|
@@ -74,5 +74,78 @@ assert(
|
|
|
74
74
|
const capped = transcriptOf(events, { maxMessages: 24, maxChars: 200, marker: 'U25' })
|
|
75
75
|
assert('显式上限生效(配置 >0 时)', capped.transcript.length < 2000, `${capped.transcript.length} 字`)
|
|
76
76
|
|
|
77
|
+
// ───────────────────────── 引用上下文(quoteContextOf)─────────────────────────
|
|
78
|
+
// 与解读用的会话背景不同:窗口是**锚点两侧**、按"轮"取整(上下各至少一整组)。
|
|
79
|
+
{
|
|
80
|
+
const lines = (text) => text.split('\n').filter((line) => /^(用户|助手|提示):/.test(line))
|
|
81
|
+
const quoted = quoteContextOf(events, 'U10', {})
|
|
82
|
+
const picked = lines(quoted.transcript)
|
|
83
|
+
assert('引用上下文:命中并报出组数', quoted.matched === true && quoted.rounds === 3, JSON.stringify({ matched: quoted.matched, rounds: quoted.rounds }))
|
|
84
|
+
assert(
|
|
85
|
+
'引用上下文:锚点所在那一组 + 上下各一组(U09/A09 · U10/A10 · U11/A11)',
|
|
86
|
+
picked.length === 6 &&
|
|
87
|
+
picked[0].includes('用户:U09') &&
|
|
88
|
+
picked[1].includes('助手:A09') &&
|
|
89
|
+
picked[2].includes('用户:【U10】') &&
|
|
90
|
+
picked[3].includes('助手:A10') &&
|
|
91
|
+
picked[4].includes('用户:U11') &&
|
|
92
|
+
picked[5].includes('助手:A11'),
|
|
93
|
+
picked.map((line) => line.slice(0, 14)).join(' | '),
|
|
94
|
+
)
|
|
95
|
+
assert('引用上下文:被引用的部分用【】标出', quoted.transcript.includes('用户:【U10】'), quoted.transcript.split('\n')[2])
|
|
96
|
+
|
|
97
|
+
// 引用落在助手回复里(常见:引用 AI 的某句话)→ 同样带上下各一组
|
|
98
|
+
const inAnswer = quoteContextOf(events, 'A10', {})
|
|
99
|
+
const answerLines = lines(inAnswer.transcript)
|
|
100
|
+
assert(
|
|
101
|
+
'引用在助手回复里:一样是上下一整组',
|
|
102
|
+
inAnswer.matched === true && answerLines.length === 6 && inAnswer.transcript.includes('助手:【A10】'),
|
|
103
|
+
answerLines.map((line) => line.slice(0, 14)).join(' | '),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
// 引用跨行/空白不一致(选中文字与消息原文差在换行、缩进)也要标得出来
|
|
107
|
+
const wrapped = [
|
|
108
|
+
userEvent('第一句\n第二句 在这里\n第三句'),
|
|
109
|
+
assistantEvent('A1'),
|
|
110
|
+
userEvent('U2'),
|
|
111
|
+
assistantEvent('A2'),
|
|
112
|
+
]
|
|
113
|
+
const spread = quoteContextOf(wrapped, '第一句 第二句 在这里', {})
|
|
114
|
+
assert('引用跨行时按"空白等价"标记', spread.matched === true && spread.transcript.includes('【第一句\n第二句 在这里】'), spread.transcript.replace(/\n/g, '⏎').slice(0, 60))
|
|
115
|
+
|
|
116
|
+
// 定位不到:不猜、返回空(客户端会退回自己的局部上下文)
|
|
117
|
+
const nowhere = quoteContextOf(events, '这段文字不在会话里', {})
|
|
118
|
+
assert('引用定位不到时返回 matched:false 且不带上下文', nowhere.matched === false && nowhere.transcript === '', JSON.stringify(nowhere))
|
|
119
|
+
|
|
120
|
+
// 长消息:**围绕引用**截断,引用本身不能被裁掉
|
|
121
|
+
const longTurn = [userEvent(`${'前'.repeat(3000)}引用的这句${'后'.repeat(3000)}`), assistantEvent('A1')]
|
|
122
|
+
const clamped = quoteContextOf(longTurn, '引用的这句', { maxCharsPerTurn: 400, maxChars: 6000 })
|
|
123
|
+
assert(
|
|
124
|
+
'长消息围绕引用截断(引用仍在,前后留边)',
|
|
125
|
+
clamped.transcript.includes('【引用的这句】') && clamped.transcript.length < 700 && clamped.transcript.includes('…'),
|
|
126
|
+
`${clamped.transcript.length} 字`,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
// 总上限:超了丢最早的那几组,引用所在那条一定留下
|
|
130
|
+
const many = []
|
|
131
|
+
for (let i = 1; i <= 9; i += 1) {
|
|
132
|
+
many.push(userEvent(`Q${i} ${'x'.repeat(400)}`))
|
|
133
|
+
many.push(assistantEvent(`R${i} ${'y'.repeat(400)}`))
|
|
134
|
+
}
|
|
135
|
+
const budget = quoteContextOf(many, 'Q5', { roundsAround: 3, maxChars: 1200 })
|
|
136
|
+
assert(
|
|
137
|
+
'总上限生效:引用所在那条保留,更早的被丢掉并留提示',
|
|
138
|
+
budget.transcript.includes('【Q5】') && budget.transcript.includes('未包含') && budget.transcript.length < 2200,
|
|
139
|
+
`${budget.transcript.length} 字`,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
// 噪音过滤与解读背景一致
|
|
143
|
+
assert(
|
|
144
|
+
'引用上下文同样剔除工具/系统/harness 注入',
|
|
145
|
+
quoted.transcript.indexOf('工具') < 0 && quoted.transcript.indexOf('系统提示') < 0 && quoted.transcript.indexOf('被 harness 注入') < 0,
|
|
146
|
+
'',
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
77
150
|
console.log(`\n${failed === 0 ? '全部通过' : failed + ' 项失败'}`)
|
|
78
151
|
process.exit(failed === 0 ? 0 : 1)
|