dsh-plain-talk 0.1.0 → 0.1.2
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 +21 -9
- package/README.en.md +53 -133
- package/README.md +67 -191
- package/dsh/check.js +250 -250
- package/dsh/rules.js +44 -44
- package/icon.png +0 -0
- package/lib/client.js +142 -155
- package/package.json +56 -56
- package/icon.svg +0 -6
package/dsh/check.js
CHANGED
|
@@ -1,250 +1,250 @@
|
|
|
1
|
-
// Reply checker for dsh-plain-talk.
|
|
2
|
-
//
|
|
3
|
-
// This is deliberately a checker, not a gate: it reports what a reader would
|
|
4
|
-
// stumble on, with the exact replacement form. The model decides what to keep.
|
|
5
|
-
//
|
|
6
|
-
// Three principles learned from real sessions in this workspace:
|
|
7
|
-
// - Common terms and project names are not errors. Only opaque codes the
|
|
8
|
-
// reader has never seen (
|
|
9
|
-
// - A pointer is not a citation: `file.py:42` alone sends the reader away.
|
|
10
|
-
// - Cross-turn wording ("上述", "上一轮") is the main reason a report cannot
|
|
11
|
-
// be read on its own.
|
|
12
|
-
|
|
13
|
-
const ALLOW = new Set([
|
|
14
|
-
'api', 'json', 'jsonl', 'yaml', 'yml', 'html', 'css', 'http', 'https', 'url', 'uri',
|
|
15
|
-
'cli', 'ui', 'ux', 'gpu', 'cpu', 'ram', 'ssd', 'hdd', 'ai', 'llm', 'sdk', 'ide',
|
|
16
|
-
'csv', 'tsv', 'ts', 'js', 'py', 'sh', 'md', 'txt', 'png', 'jpg', 'jpeg', 'svg',
|
|
17
|
-
'gif', 'webp', 'sql', 'xml', 'toml', 'npm', 'pnpm', 'git', 'ssh', 'dns', 'tcp',
|
|
18
|
-
'udp', 'tls', 'ssl', 'jwt', 'oauth', 'cors', 'orm', 'crud', 'rest', 'grpc',
|
|
19
|
-
'wasm', 'dom', 'smtp', 'imap', 'vpn', 'utf8', 'utf-8', 'utf16', 'utf-16',
|
|
20
|
-
'sha1', 'sha256', 'sha-256', 'sha512', 'sha-512', 'md5', 'x86', 'x64', 'arm64',
|
|
21
|
-
'mp3', 'mp4', 'pdf', 'h264', 'hevc', 'ipv4', 'ipv6', 'k8s', 'i18n', 'l10n',
|
|
22
|
-
'2d', '3d', '4k', 'v1', 'v2', 'v3', 'v4', 'ci', 'pr', 'id', 'ok', 'vs', 'iso',
|
|
23
|
-
'doi', 'arxiv', 'p50', 'p95', 'p99', '2x', '4x', '8x', '16x', 'gpt', 'llm',
|
|
24
|
-
'linux', 'windows', 'macos', 'node', 'react', 'github', 'python', 'javascript',
|
|
25
|
-
'typescript', 'docker', 'sqlite', 'postgres', 'redis',
|
|
26
|
-
])
|
|
27
|
-
|
|
28
|
-
const MODEL_FAMILY = /^(?:gpt|claude|gemini|llama|qwen|deepseek|mistral|phi|o1|o3|o4|glm|kimi|mimo|hy)\d*[-.]?\d*/i
|
|
29
|
-
|
|
30
|
-
const BACKREF_ZH = [
|
|
31
|
-
'上述', '上面', '前述', '前文', '前面提到', '前面说', '之前提到', '之前说',
|
|
32
|
-
'此前', '上一步', '上一轮', '前一轮', '刚才', '该方案', '该项', '该任务',
|
|
33
|
-
'前者', '后者', '如上', '见上文', '先前', '那次', '前面', '之前',
|
|
34
|
-
]
|
|
35
|
-
const BACKREF_EN = [
|
|
36
|
-
'as mentioned', 'aforementioned', 'as discussed', 'the above', 'previous step',
|
|
37
|
-
'earlier', 'previously', 'the former', 'the latter',
|
|
38
|
-
]
|
|
39
|
-
|
|
40
|
-
const PLUMBING = [
|
|
41
|
-
/\bsession[-_][0-9a-z-]{6,}/i,
|
|
42
|
-
/子代理|子会话|父代理|父会话|\bsubagent\b|\bparent agent\b|report sent to/i,
|
|
43
|
-
/\b(?:plain_talk_check|run_code|ops_bash|ops_grep|todo_write)\b/,
|
|
44
|
-
]
|
|
45
|
-
|
|
46
|
-
const PREAMBLE = /^(好的|好|当然|没问题|收到|明白|我来|让我|让我先|我们先|首先,我|先看一[下看])/
|
|
47
|
-
const CLOSER = /(需要我|要不要|如果你愿意|希望(这|以上)|随时告诉我|还需要我|要我继续)/
|
|
48
|
-
|
|
49
|
-
const FILE_EXT = 'py|js|mjs|cjs|ts|tsx|json|jsonl|md|ya?ml|toml|sh|ps1|bat|csv|tsv|txt|ipynb|log|html|css|tex|bib|pdf|zip|sql|db|pt|pth|ckpt|npy|npz|parquet|docx|xlsx|pptx'
|
|
50
|
-
|
|
51
|
-
const IDENT_PATTERNS = [
|
|
52
|
-
['file', new RegExp(`(?:[A-Za-z]:[\\\\/]|\\/)?[A-Za-z0-9_.-]+(?:[\\\\/][A-Za-z0-9_.-]+)*\\.(?:${FILE_EXT})\\b`, 'gi')],
|
|
53
|
-
['hash', /\b[0-9a-f]{7,40}\b/gi],
|
|
54
|
-
['snake', /\b[A-Za-z][A-Za-z0-9]*(?:_[A-Za-z0-9]+)+\b/g],
|
|
55
|
-
['camel', /\b[a-z]+[A-Z][A-Za-z0-9]*\b|\b(?:[A-Z][a-z0-9]+){2,}[A-Za-z0-9]*\b|\b[A-Z][a-z]+[A-Z]{2,}[A-Za-z0-9]*\b/g],
|
|
56
|
-
['acronym', /\b[A-Z]{2,8}\b/g],
|
|
57
|
-
['code', /\b[A-Za-z]{1,10}[-_.]?\d{1,6}[A-Za-z]?\b|\b\d{1,4}[A-Za-z]{1,6}\b/g],
|
|
58
|
-
]
|
|
59
|
-
|
|
60
|
-
export const LIMITS = {
|
|
61
|
-
boxChars: 300,
|
|
62
|
-
boxLines: 12,
|
|
63
|
-
listRun: 5,
|
|
64
|
-
identifierWarns: 8,
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const cjkCount = (s) => (s.match(/[\u4e00-\u9fff]/g) ?? []).length
|
|
68
|
-
const latinCount = (s) => (s.match(/[A-Za-z]/g) ?? []).length
|
|
69
|
-
const len = (s) => [...s].length
|
|
70
|
-
|
|
71
|
-
function stripCode(text) {
|
|
72
|
-
return text
|
|
73
|
-
.replace(/```[\s\S]*?```/g, ' ')
|
|
74
|
-
.replace(/`[^`\n]*`/g, ' ')
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function isCommonTerm(term) {
|
|
78
|
-
const text = String(term ?? '').trim()
|
|
79
|
-
if (!text) return false
|
|
80
|
-
return ALLOW.has(text.toLowerCase()) || MODEL_FAMILY.test(text)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function scanIdentifiers(input) { const text = stripCode(String(input ?? ''))
|
|
84
|
-
const found = []
|
|
85
|
-
for (const [kind, re] of IDENT_PATTERNS) {
|
|
86
|
-
re.lastIndex = 0
|
|
87
|
-
let m
|
|
88
|
-
while ((m = re.exec(text))) {
|
|
89
|
-
const raw = m[0].replace(/[.,;:]+$/, '')
|
|
90
|
-
if (!raw) continue
|
|
91
|
-
found.push({ kind, token: raw, start: m.index, end: m.index + raw.length })
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
found.sort((a, b) => a.start - b.start || (b.end - b.start) - (a.end - a.start))
|
|
95
|
-
const accepted = []
|
|
96
|
-
for (const item of found) {
|
|
97
|
-
if (accepted.some((a) => item.start < a.end && item.end > a.start)) continue
|
|
98
|
-
accepted.push(item)
|
|
99
|
-
}
|
|
100
|
-
return accepted
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function isAllowed(token, known) {
|
|
104
|
-
const lower = token.toLowerCase()
|
|
105
|
-
if (ALLOW.has(lower)) return true
|
|
106
|
-
if (MODEL_FAMILY.test(token)) return true
|
|
107
|
-
// Durations and multipliers ("10m", "3s", "4x") are measurements, not names.
|
|
108
|
-
if (/^\d+(?:ms|s|m|h|d|w|k|m|b|x)$/i.test(token)) return true
|
|
109
|
-
// Product-style names (
|
|
110
|
-
// reader already holds; a lower-case internal key (tier_reaudit) is not.
|
|
111
|
-
if (/^[A-Za-z0-9]+(?:_[A-Za-z0-9]+)+$/.test(token) && (token.match(/[A-Z]/g) ?? []).length >= 2) return true
|
|
112
|
-
if (known && (known.has(token) || known.has(lower))) return true
|
|
113
|
-
return false
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function explainedAfter(text, end) {
|
|
117
|
-
const after = text.slice(end, end + 60)
|
|
118
|
-
const m = /^\s*[((]([^))]{4,})[))]/.exec(after)
|
|
119
|
-
if (!m) return false
|
|
120
|
-
return cjkCount(m[1]) >= 3 || latinCount(m[1]) >= 8
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function knownFrom(text, known) {
|
|
124
|
-
return { text, known }
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Analyze one reply draft.
|
|
129
|
-
*
|
|
130
|
-
* @param draft - the reply text the model is about to send.
|
|
131
|
-
* @param options - `knownTokens` (project/user vocabulary) and `language`
|
|
132
|
-
* (`zh` when the user writes Chinese).
|
|
133
|
-
* @returns issues plus the measurements the README benchmark reports.
|
|
134
|
-
*/
|
|
135
|
-
export function analyze(draft, options = {}) {
|
|
136
|
-
const text = String(draft ?? '')
|
|
137
|
-
const known = new Set((options.knownTokens ?? []).map((t) => String(t)))
|
|
138
|
-
const knownLower = new Set([...known].map((t) => t.toLowerCase()))
|
|
139
|
-
const language = options.language ?? 'unknown'
|
|
140
|
-
const issues = []
|
|
141
|
-
|
|
142
|
-
const add = (severity, rule, message, fix) => issues.push({ severity, rule, message, fix })
|
|
143
|
-
|
|
144
|
-
const narrative = stripCode(text)
|
|
145
|
-
|
|
146
|
-
// 1. Cross-turn wording.
|
|
147
|
-
const backrefs = [
|
|
148
|
-
...BACKREF_ZH.filter((w) => narrative.includes(w)),
|
|
149
|
-
...BACKREF_EN.filter((w) => narrative.toLowerCase().includes(w)),
|
|
150
|
-
]
|
|
151
|
-
if (backrefs.length) {
|
|
152
|
-
add('error', 'backref', `出现回指词:${backrefs.slice(0, 5).join('、')}。`, '把指代的东西重新写一遍,读者没有前文可翻。')
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// 2. Internal plumbing.
|
|
156
|
-
const plumbing = PLUMBING.find((re) => re.test(narrative))
|
|
157
|
-
if (plumbing) {
|
|
158
|
-
add('error', 'internal', '出现会话号、子代理或工具名等内部信息。', '删掉;只写用户能看见的对象。')
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// 3. Pointer-only citations.
|
|
162
|
-
const pointers = []
|
|
163
|
-
const pointerRe = new RegExp(`(?:[A-Za-z]:[\\\\/]|/)?[\\w.-]+(?:[\\\\/][\\w.-]+)*\\.(?:${FILE_EXT}):\\d+`, 'gi')
|
|
164
|
-
pointerRe.lastIndex = 0
|
|
165
|
-
let pm
|
|
166
|
-
while ((pm = pointerRe.exec(text))) pointers.push(pm[0])
|
|
167
|
-
if (pointers.length) {
|
|
168
|
-
const unquoted = pointers.filter((pointer) => {
|
|
169
|
-
const at = text.indexOf(pointer)
|
|
170
|
-
const window = text.slice(Math.max(0, at - 220), at + pointer.length + 220)
|
|
171
|
-
return !/「[^」]{4,}」|"[^"]{4,}"|『[^』]{4,}』|`[^`]{4,}`|```/.test(window)
|
|
172
|
-
})
|
|
173
|
-
if (unquoted.length) {
|
|
174
|
-
add('error', 'citation', `只有位置没有原文:${unquoted.slice(0, 3).join('、')}。`, '把那几行原文贴出来,再标位置。')
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// 4. Unexplained opaque identifiers.
|
|
179
|
-
const seen = new Set()
|
|
180
|
-
const unknown = []
|
|
181
|
-
for (const token of scanIdentifiers(narrative)) {
|
|
182
|
-
if (seen.has(token.token)) continue
|
|
183
|
-
seen.add(token.token)
|
|
184
|
-
if (isAllowed(token.token, known) || isAllowed(token.token, knownLower)) continue
|
|
185
|
-
if (explainedAfter(narrative, token.end)) continue
|
|
186
|
-
unknown.push(token.token)
|
|
187
|
-
}
|
|
188
|
-
if (unknown.length) {
|
|
189
|
-
add(
|
|
190
|
-
'warn',
|
|
191
|
-
'identifier',
|
|
192
|
-
`有 ${unknown.length} 个代号没有说明:${unknown.slice(0, 6).join('、')}${unknown.length > 6 ? ' 等' : ''}。`,
|
|
193
|
-
'第一次出现写成「
|
|
194
|
-
)
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// 5. Shape.
|
|
198
|
-
const lines = text.split('\n').filter((l) => l.trim())
|
|
199
|
-
const cjk = cjkCount(narrative)
|
|
200
|
-
if (cjk > LIMITS.boxChars || lines.length > LIMITS.boxLines) {
|
|
201
|
-
add('warn', 'length', `回复有 ${cjk} 个汉字、${lines.length} 行,超过默认的 ${LIMITS.boxChars} 字 / ${LIMITS.boxLines} 行。`, '删掉读者不决策所需的内容;用户要细节时再展开。')
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
let run = 0
|
|
205
|
-
let maxRun = 0
|
|
206
|
-
for (const line of text.split('\n')) {
|
|
207
|
-
if (/^\s*(?:[-*+]|\d+[.)])\s+/.test(line)) { run++; maxRun = Math.max(maxRun, run) } else if (!line.trim()) { run = 0 }
|
|
208
|
-
}
|
|
209
|
-
if (maxRun > LIMITS.listRun) {
|
|
210
|
-
add('warn', 'list', `有一段列表 ${maxRun} 条,超过 5 条。`, '合并或只留最要紧的五条。')
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const first = lines[0] ?? ''
|
|
214
|
-
if (PREAMBLE.test(first)) {
|
|
215
|
-
add('warn', 'preamble', `开头是「${first.slice(0, 20)}」,属于寒暄或过程宣告。`, '第一行直接给结论。')
|
|
216
|
-
}
|
|
217
|
-
const tail = text.slice(-180)
|
|
218
|
-
const closer = CLOSER.exec(tail)
|
|
219
|
-
if (closer) {
|
|
220
|
-
add('warn', 'closer', `结尾出现「${closer[0]}」,在征求额外任务。`, '删掉;需要下一步就只给一个具体动作。')
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// 6. Language mismatch.
|
|
224
|
-
const total = cjk + latinCount(narrative)
|
|
225
|
-
const cjkRatio = total === 0 ? 0 : cjk / total
|
|
226
|
-
if (language === 'zh' && cjkRatio < 0.2 && latinCount(narrative) > 40) {
|
|
227
|
-
add('warn', 'language', '用户用中文提问,但这条回复几乎全是英文。', '用中文写;命令、路径、报错原文保持原样。')
|
|
228
|
-
}
|
|
229
|
-
const hasNext = /下一步|接下来要|接下来请|你可以先|建议你先|Next step/i.test(text)
|
|
230
|
-
if (!hasNext && (cjk > 200 || lines.length > 10)) {
|
|
231
|
-
add('warn', 'next', '回复较长,但没有给出一个明确的下一步。', '结尾补一个用户现在就能做的事;没有就写"没有待办"。')
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return {
|
|
235
|
-
ok: !issues.some((i) => i.severity === 'error'),
|
|
236
|
-
issues,
|
|
237
|
-
stats: {
|
|
238
|
-
chars: text.length,
|
|
239
|
-
cjk,
|
|
240
|
-
lines: lines.length,
|
|
241
|
-
cjkRatio: Number(cjkRatio.toFixed(3)),
|
|
242
|
-
identifiers: unknown.length,
|
|
243
|
-
backrefs: backrefs.length,
|
|
244
|
-
maxListRun: maxRun,
|
|
245
|
-
hasNext,
|
|
246
|
-
},
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
export { cjkCount, latinCount, len }
|
|
1
|
+
// Reply checker for dsh-plain-talk.
|
|
2
|
+
//
|
|
3
|
+
// This is deliberately a checker, not a gate: it reports what a reader would
|
|
4
|
+
// stumble on, with the exact replacement form. The model decides what to keep.
|
|
5
|
+
//
|
|
6
|
+
// Three principles learned from real sessions in this workspace:
|
|
7
|
+
// - Common terms and project names are not errors. Only opaque codes the
|
|
8
|
+
// reader has never seen (EXP-7, DEV-1K, run-3) need a first-use gloss.
|
|
9
|
+
// - A pointer is not a citation: `file.py:42` alone sends the reader away.
|
|
10
|
+
// - Cross-turn wording ("上述", "上一轮") is the main reason a report cannot
|
|
11
|
+
// be read on its own.
|
|
12
|
+
|
|
13
|
+
const ALLOW = new Set([
|
|
14
|
+
'api', 'json', 'jsonl', 'yaml', 'yml', 'html', 'css', 'http', 'https', 'url', 'uri',
|
|
15
|
+
'cli', 'ui', 'ux', 'gpu', 'cpu', 'ram', 'ssd', 'hdd', 'ai', 'llm', 'sdk', 'ide',
|
|
16
|
+
'csv', 'tsv', 'ts', 'js', 'py', 'sh', 'md', 'txt', 'png', 'jpg', 'jpeg', 'svg',
|
|
17
|
+
'gif', 'webp', 'sql', 'xml', 'toml', 'npm', 'pnpm', 'git', 'ssh', 'dns', 'tcp',
|
|
18
|
+
'udp', 'tls', 'ssl', 'jwt', 'oauth', 'cors', 'orm', 'crud', 'rest', 'grpc',
|
|
19
|
+
'wasm', 'dom', 'smtp', 'imap', 'vpn', 'utf8', 'utf-8', 'utf16', 'utf-16',
|
|
20
|
+
'sha1', 'sha256', 'sha-256', 'sha512', 'sha-512', 'md5', 'x86', 'x64', 'arm64',
|
|
21
|
+
'mp3', 'mp4', 'pdf', 'h264', 'hevc', 'ipv4', 'ipv6', 'k8s', 'i18n', 'l10n',
|
|
22
|
+
'2d', '3d', '4k', 'v1', 'v2', 'v3', 'v4', 'ci', 'pr', 'id', 'ok', 'vs', 'iso',
|
|
23
|
+
'doi', 'arxiv', 'p50', 'p95', 'p99', '2x', '4x', '8x', '16x', 'gpt', 'llm',
|
|
24
|
+
'linux', 'windows', 'macos', 'node', 'react', 'github', 'python', 'javascript',
|
|
25
|
+
'typescript', 'docker', 'sqlite', 'postgres', 'redis',
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
const MODEL_FAMILY = /^(?:gpt|claude|gemini|llama|qwen|deepseek|mistral|phi|o1|o3|o4|glm|kimi|mimo|hy)\d*[-.]?\d*/i
|
|
29
|
+
|
|
30
|
+
const BACKREF_ZH = [
|
|
31
|
+
'上述', '上面', '前述', '前文', '前面提到', '前面说', '之前提到', '之前说',
|
|
32
|
+
'此前', '上一步', '上一轮', '前一轮', '刚才', '该方案', '该项', '该任务',
|
|
33
|
+
'前者', '后者', '如上', '见上文', '先前', '那次', '前面', '之前',
|
|
34
|
+
]
|
|
35
|
+
const BACKREF_EN = [
|
|
36
|
+
'as mentioned', 'aforementioned', 'as discussed', 'the above', 'previous step',
|
|
37
|
+
'earlier', 'previously', 'the former', 'the latter',
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
const PLUMBING = [
|
|
41
|
+
/\bsession[-_][0-9a-z-]{6,}/i,
|
|
42
|
+
/子代理|子会话|父代理|父会话|\bsubagent\b|\bparent agent\b|report sent to/i,
|
|
43
|
+
/\b(?:plain_talk_check|run_code|ops_bash|ops_grep|todo_write)\b/,
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
const PREAMBLE = /^(好的|好|当然|没问题|收到|明白|我来|让我|让我先|我们先|首先,我|先看一[下看])/
|
|
47
|
+
const CLOSER = /(需要我|要不要|如果你愿意|希望(这|以上)|随时告诉我|还需要我|要我继续)/
|
|
48
|
+
|
|
49
|
+
const FILE_EXT = 'py|js|mjs|cjs|ts|tsx|json|jsonl|md|ya?ml|toml|sh|ps1|bat|csv|tsv|txt|ipynb|log|html|css|tex|bib|pdf|zip|sql|db|pt|pth|ckpt|npy|npz|parquet|docx|xlsx|pptx'
|
|
50
|
+
|
|
51
|
+
const IDENT_PATTERNS = [
|
|
52
|
+
['file', new RegExp(`(?:[A-Za-z]:[\\\\/]|\\/)?[A-Za-z0-9_.-]+(?:[\\\\/][A-Za-z0-9_.-]+)*\\.(?:${FILE_EXT})\\b`, 'gi')],
|
|
53
|
+
['hash', /\b[0-9a-f]{7,40}\b/gi],
|
|
54
|
+
['snake', /\b[A-Za-z][A-Za-z0-9]*(?:_[A-Za-z0-9]+)+\b/g],
|
|
55
|
+
['camel', /\b[a-z]+[A-Z][A-Za-z0-9]*\b|\b(?:[A-Z][a-z0-9]+){2,}[A-Za-z0-9]*\b|\b[A-Z][a-z]+[A-Z]{2,}[A-Za-z0-9]*\b/g],
|
|
56
|
+
['acronym', /\b[A-Z]{2,8}\b/g],
|
|
57
|
+
['code', /\b[A-Za-z]{1,10}[-_.]?\d{1,6}[A-Za-z]?\b|\b\d{1,4}[A-Za-z]{1,6}\b/g],
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
export const LIMITS = {
|
|
61
|
+
boxChars: 300,
|
|
62
|
+
boxLines: 12,
|
|
63
|
+
listRun: 5,
|
|
64
|
+
identifierWarns: 8,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const cjkCount = (s) => (s.match(/[\u4e00-\u9fff]/g) ?? []).length
|
|
68
|
+
const latinCount = (s) => (s.match(/[A-Za-z]/g) ?? []).length
|
|
69
|
+
const len = (s) => [...s].length
|
|
70
|
+
|
|
71
|
+
function stripCode(text) {
|
|
72
|
+
return text
|
|
73
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
74
|
+
.replace(/`[^`\n]*`/g, ' ')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function isCommonTerm(term) {
|
|
78
|
+
const text = String(term ?? '').trim()
|
|
79
|
+
if (!text) return false
|
|
80
|
+
return ALLOW.has(text.toLowerCase()) || MODEL_FAMILY.test(text)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function scanIdentifiers(input) { const text = stripCode(String(input ?? ''))
|
|
84
|
+
const found = []
|
|
85
|
+
for (const [kind, re] of IDENT_PATTERNS) {
|
|
86
|
+
re.lastIndex = 0
|
|
87
|
+
let m
|
|
88
|
+
while ((m = re.exec(text))) {
|
|
89
|
+
const raw = m[0].replace(/[.,;:]+$/, '')
|
|
90
|
+
if (!raw) continue
|
|
91
|
+
found.push({ kind, token: raw, start: m.index, end: m.index + raw.length })
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
found.sort((a, b) => a.start - b.start || (b.end - b.start) - (a.end - a.start))
|
|
95
|
+
const accepted = []
|
|
96
|
+
for (const item of found) {
|
|
97
|
+
if (accepted.some((a) => item.start < a.end && item.end > a.start)) continue
|
|
98
|
+
accepted.push(item)
|
|
99
|
+
}
|
|
100
|
+
return accepted
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isAllowed(token, known) {
|
|
104
|
+
const lower = token.toLowerCase()
|
|
105
|
+
if (ALLOW.has(lower)) return true
|
|
106
|
+
if (MODEL_FAMILY.test(token)) return true
|
|
107
|
+
// Durations and multipliers ("10m", "3s", "4x") are measurements, not names.
|
|
108
|
+
if (/^\d+(?:ms|s|m|h|d|w|k|m|b|x)$/i.test(token)) return true
|
|
109
|
+
// Product-style names (AcmeService_V2, DataHub_V3) are project vocabulary the
|
|
110
|
+
// reader already holds; a lower-case internal key (tier_reaudit) is not.
|
|
111
|
+
if (/^[A-Za-z0-9]+(?:_[A-Za-z0-9]+)+$/.test(token) && (token.match(/[A-Z]/g) ?? []).length >= 2) return true
|
|
112
|
+
if (known && (known.has(token) || known.has(lower))) return true
|
|
113
|
+
return false
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function explainedAfter(text, end) {
|
|
117
|
+
const after = text.slice(end, end + 60)
|
|
118
|
+
const m = /^\s*[((]([^))]{4,})[))]/.exec(after)
|
|
119
|
+
if (!m) return false
|
|
120
|
+
return cjkCount(m[1]) >= 3 || latinCount(m[1]) >= 8
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function knownFrom(text, known) {
|
|
124
|
+
return { text, known }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Analyze one reply draft.
|
|
129
|
+
*
|
|
130
|
+
* @param draft - the reply text the model is about to send.
|
|
131
|
+
* @param options - `knownTokens` (project/user vocabulary) and `language`
|
|
132
|
+
* (`zh` when the user writes Chinese).
|
|
133
|
+
* @returns issues plus the measurements the README benchmark reports.
|
|
134
|
+
*/
|
|
135
|
+
export function analyze(draft, options = {}) {
|
|
136
|
+
const text = String(draft ?? '')
|
|
137
|
+
const known = new Set((options.knownTokens ?? []).map((t) => String(t)))
|
|
138
|
+
const knownLower = new Set([...known].map((t) => t.toLowerCase()))
|
|
139
|
+
const language = options.language ?? 'unknown'
|
|
140
|
+
const issues = []
|
|
141
|
+
|
|
142
|
+
const add = (severity, rule, message, fix) => issues.push({ severity, rule, message, fix })
|
|
143
|
+
|
|
144
|
+
const narrative = stripCode(text)
|
|
145
|
+
|
|
146
|
+
// 1. Cross-turn wording.
|
|
147
|
+
const backrefs = [
|
|
148
|
+
...BACKREF_ZH.filter((w) => narrative.includes(w)),
|
|
149
|
+
...BACKREF_EN.filter((w) => narrative.toLowerCase().includes(w)),
|
|
150
|
+
]
|
|
151
|
+
if (backrefs.length) {
|
|
152
|
+
add('error', 'backref', `出现回指词:${backrefs.slice(0, 5).join('、')}。`, '把指代的东西重新写一遍,读者没有前文可翻。')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 2. Internal plumbing.
|
|
156
|
+
const plumbing = PLUMBING.find((re) => re.test(narrative))
|
|
157
|
+
if (plumbing) {
|
|
158
|
+
add('error', 'internal', '出现会话号、子代理或工具名等内部信息。', '删掉;只写用户能看见的对象。')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 3. Pointer-only citations.
|
|
162
|
+
const pointers = []
|
|
163
|
+
const pointerRe = new RegExp(`(?:[A-Za-z]:[\\\\/]|/)?[\\w.-]+(?:[\\\\/][\\w.-]+)*\\.(?:${FILE_EXT}):\\d+`, 'gi')
|
|
164
|
+
pointerRe.lastIndex = 0
|
|
165
|
+
let pm
|
|
166
|
+
while ((pm = pointerRe.exec(text))) pointers.push(pm[0])
|
|
167
|
+
if (pointers.length) {
|
|
168
|
+
const unquoted = pointers.filter((pointer) => {
|
|
169
|
+
const at = text.indexOf(pointer)
|
|
170
|
+
const window = text.slice(Math.max(0, at - 220), at + pointer.length + 220)
|
|
171
|
+
return !/「[^」]{4,}」|"[^"]{4,}"|『[^』]{4,}』|`[^`]{4,}`|```/.test(window)
|
|
172
|
+
})
|
|
173
|
+
if (unquoted.length) {
|
|
174
|
+
add('error', 'citation', `只有位置没有原文:${unquoted.slice(0, 3).join('、')}。`, '把那几行原文贴出来,再标位置。')
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 4. Unexplained opaque identifiers.
|
|
179
|
+
const seen = new Set()
|
|
180
|
+
const unknown = []
|
|
181
|
+
for (const token of scanIdentifiers(narrative)) {
|
|
182
|
+
if (seen.has(token.token)) continue
|
|
183
|
+
seen.add(token.token)
|
|
184
|
+
if (isAllowed(token.token, known) || isAllowed(token.token, knownLower)) continue
|
|
185
|
+
if (explainedAfter(narrative, token.end)) continue
|
|
186
|
+
unknown.push(token.token)
|
|
187
|
+
}
|
|
188
|
+
if (unknown.length) {
|
|
189
|
+
add(
|
|
190
|
+
'warn',
|
|
191
|
+
'identifier',
|
|
192
|
+
`有 ${unknown.length} 个代号没有说明:${unknown.slice(0, 6).join('、')}${unknown.length > 6 ? ' 等' : ''}。`,
|
|
193
|
+
'第一次出现写成「EXP-7(一句话说明它是什么)」;用户在本对话用过的词和项目名不用解释。',
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 5. Shape.
|
|
198
|
+
const lines = text.split('\n').filter((l) => l.trim())
|
|
199
|
+
const cjk = cjkCount(narrative)
|
|
200
|
+
if (cjk > LIMITS.boxChars || lines.length > LIMITS.boxLines) {
|
|
201
|
+
add('warn', 'length', `回复有 ${cjk} 个汉字、${lines.length} 行,超过默认的 ${LIMITS.boxChars} 字 / ${LIMITS.boxLines} 行。`, '删掉读者不决策所需的内容;用户要细节时再展开。')
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
let run = 0
|
|
205
|
+
let maxRun = 0
|
|
206
|
+
for (const line of text.split('\n')) {
|
|
207
|
+
if (/^\s*(?:[-*+]|\d+[.)])\s+/.test(line)) { run++; maxRun = Math.max(maxRun, run) } else if (!line.trim()) { run = 0 }
|
|
208
|
+
}
|
|
209
|
+
if (maxRun > LIMITS.listRun) {
|
|
210
|
+
add('warn', 'list', `有一段列表 ${maxRun} 条,超过 5 条。`, '合并或只留最要紧的五条。')
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const first = lines[0] ?? ''
|
|
214
|
+
if (PREAMBLE.test(first)) {
|
|
215
|
+
add('warn', 'preamble', `开头是「${first.slice(0, 20)}」,属于寒暄或过程宣告。`, '第一行直接给结论。')
|
|
216
|
+
}
|
|
217
|
+
const tail = text.slice(-180)
|
|
218
|
+
const closer = CLOSER.exec(tail)
|
|
219
|
+
if (closer) {
|
|
220
|
+
add('warn', 'closer', `结尾出现「${closer[0]}」,在征求额外任务。`, '删掉;需要下一步就只给一个具体动作。')
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// 6. Language mismatch.
|
|
224
|
+
const total = cjk + latinCount(narrative)
|
|
225
|
+
const cjkRatio = total === 0 ? 0 : cjk / total
|
|
226
|
+
if (language === 'zh' && cjkRatio < 0.2 && latinCount(narrative) > 40) {
|
|
227
|
+
add('warn', 'language', '用户用中文提问,但这条回复几乎全是英文。', '用中文写;命令、路径、报错原文保持原样。')
|
|
228
|
+
}
|
|
229
|
+
const hasNext = /下一步|接下来要|接下来请|你可以先|建议你先|Next step/i.test(text)
|
|
230
|
+
if (!hasNext && (cjk > 200 || lines.length > 10)) {
|
|
231
|
+
add('warn', 'next', '回复较长,但没有给出一个明确的下一步。', '结尾补一个用户现在就能做的事;没有就写"没有待办"。')
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
ok: !issues.some((i) => i.severity === 'error'),
|
|
236
|
+
issues,
|
|
237
|
+
stats: {
|
|
238
|
+
chars: text.length,
|
|
239
|
+
cjk,
|
|
240
|
+
lines: lines.length,
|
|
241
|
+
cjkRatio: Number(cjkRatio.toFixed(3)),
|
|
242
|
+
identifiers: unknown.length,
|
|
243
|
+
backrefs: backrefs.length,
|
|
244
|
+
maxListRun: maxRun,
|
|
245
|
+
hasNext,
|
|
246
|
+
},
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export { cjkCount, latinCount, len }
|
package/dsh/rules.js
CHANGED
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
// The system-prompt section injected while "说人话" is on for the session.
|
|
2
|
-
//
|
|
3
|
-
// Design notes:
|
|
4
|
-
// - The rules target the reader's cost, not the model's output length alone:
|
|
5
|
-
// a short reply that still needs the previous turns is still unreadable.
|
|
6
|
-
// - Numbers in the comment block below come from a scan of 197 real sessions
|
|
7
|
-
// in this workspace (median 2,076 characters, 96% with unexplained codes,
|
|
8
|
-
// 22% with back-references, 43% almost entirely English).
|
|
9
|
-
// - Keep the text shorter than the replies it is meant to shorten.
|
|
10
|
-
|
|
11
|
-
export const RULES = `# 说人话(回复优化)
|
|
12
|
-
|
|
13
|
-
读者只看这一条回复就要能做决定。先给结论,再给必要细节。
|
|
14
|
-
|
|
15
|
-
## 长度
|
|
16
|
-
- 能一句说清就一句;默认不超过 12 行、300 字。用户明确要细节时放开深度,但结论仍在第一行。
|
|
17
|
-
- 不写"好的/我来看看/让我先…/希望这有帮助";不复述问题;不汇报你准备怎么做。
|
|
18
|
-
- 不写过程日记("我先…然后…")。只写结果、判断和可验证的事实。
|
|
19
|
-
- 列表最多 5 条;只给一个下一步。结尾不要问"还需要我做什么"。
|
|
20
|
-
|
|
21
|
-
## 自包含
|
|
22
|
-
- 不用"上述/前面/之前提到的/上一轮/前者/后者/刚才";要指代就重说一遍。
|
|
23
|
-
- 代号、任务号、版本号、提交号、模块名(如
|
|
24
|
-
- 但用户在本对话里用过的词、项目名、常见术语(arXiv、DOI、API、GPU 等)默认读者已经懂,不要解释,也不要改写成别的说法。
|
|
25
|
-
- 用户让你"调研 / 研究 / 查一下 X"时,第一遍回复默认读者还不了解 X,先把它讲清楚;这条回复交付之后,X 才进入词汇清单。
|
|
26
|
-
- 不出现会话号、子代理、父会话、工具名等内部信息。
|
|
27
|
-
|
|
28
|
-
## 引用与数字
|
|
29
|
-
- 引用文件、日志、网页时直接贴出那几行原文;只给"文件名:行号"不算证据。
|
|
30
|
-
- 数字给单位和对照:"61.2% → 63.8%",不写"明显提升"。
|
|
31
|
-
|
|
32
|
-
## 语言
|
|
33
|
-
- 用用户最近一条消息的语言回复。思考过程可以用英文。
|
|
34
|
-
- 代码、命令、路径、报错原文保持原样,不翻译、不缩写。
|
|
35
|
-
|
|
36
|
-
## 执行类回复
|
|
37
|
-
用户给过计划、任务清单或验收要求时,第一行给结论:达标 / 部分达标 / 未达标,并点明哪几条没达标。
|
|
38
|
-
验收标准只用用户原话,不自己发明。然后按顺序:做了什么 → 没做到什么 → 用户怎么自己确认(贴出应看到的内容)→ 一个下一步。
|
|
39
|
-
|
|
40
|
-
## 收尾自检
|
|
41
|
-
完成类回复或超过 12 行的回复,发送前调用 plain_talk_check;把 error 全部改掉,warn 逐条判断。
|
|
42
|
-
|
|
43
|
-
## 覆盖规则
|
|
44
|
-
用户说"展开讲/详细说明"时,保留结论先行,正文可以长。破坏性操作仍然先确认。harness 规则优先于本节。`;
|
|
1
|
+
// The system-prompt section injected while "说人话" is on for the session.
|
|
2
|
+
//
|
|
3
|
+
// Design notes:
|
|
4
|
+
// - The rules target the reader's cost, not the model's output length alone:
|
|
5
|
+
// a short reply that still needs the previous turns is still unreadable.
|
|
6
|
+
// - Numbers in the comment block below come from a scan of 197 real sessions
|
|
7
|
+
// in this workspace (median 2,076 characters, 96% with unexplained codes,
|
|
8
|
+
// 22% with back-references, 43% almost entirely English).
|
|
9
|
+
// - Keep the text shorter than the replies it is meant to shorten.
|
|
10
|
+
|
|
11
|
+
export const RULES = `# 说人话(回复优化)
|
|
12
|
+
|
|
13
|
+
读者只看这一条回复就要能做决定。先给结论,再给必要细节。
|
|
14
|
+
|
|
15
|
+
## 长度
|
|
16
|
+
- 能一句说清就一句;默认不超过 12 行、300 字。用户明确要细节时放开深度,但结论仍在第一行。
|
|
17
|
+
- 不写"好的/我来看看/让我先…/希望这有帮助";不复述问题;不汇报你准备怎么做。
|
|
18
|
+
- 不写过程日记("我先…然后…")。只写结果、判断和可验证的事实。
|
|
19
|
+
- 列表最多 5 条;只给一个下一步。结尾不要问"还需要我做什么"。
|
|
20
|
+
|
|
21
|
+
## 自包含
|
|
22
|
+
- 不用"上述/前面/之前提到的/上一轮/前者/后者/刚才";要指代就重说一遍。
|
|
23
|
+
- 代号、任务号、版本号、提交号、模块名(如 EXP-7、DEV-1K、run-3)第一次出现时写清它是什么:EXP-7(把实例校验加进推理流程的实验编号)。之后可以直接用。
|
|
24
|
+
- 但用户在本对话里用过的词、项目名、常见术语(arXiv、DOI、API、GPU 等)默认读者已经懂,不要解释,也不要改写成别的说法。
|
|
25
|
+
- 用户让你"调研 / 研究 / 查一下 X"时,第一遍回复默认读者还不了解 X,先把它讲清楚;这条回复交付之后,X 才进入词汇清单。
|
|
26
|
+
- 不出现会话号、子代理、父会话、工具名等内部信息。
|
|
27
|
+
|
|
28
|
+
## 引用与数字
|
|
29
|
+
- 引用文件、日志、网页时直接贴出那几行原文;只给"文件名:行号"不算证据。
|
|
30
|
+
- 数字给单位和对照:"61.2% → 63.8%",不写"明显提升"。
|
|
31
|
+
|
|
32
|
+
## 语言
|
|
33
|
+
- 用用户最近一条消息的语言回复。思考过程可以用英文。
|
|
34
|
+
- 代码、命令、路径、报错原文保持原样,不翻译、不缩写。
|
|
35
|
+
|
|
36
|
+
## 执行类回复
|
|
37
|
+
用户给过计划、任务清单或验收要求时,第一行给结论:达标 / 部分达标 / 未达标,并点明哪几条没达标。
|
|
38
|
+
验收标准只用用户原话,不自己发明。然后按顺序:做了什么 → 没做到什么 → 用户怎么自己确认(贴出应看到的内容)→ 一个下一步。
|
|
39
|
+
|
|
40
|
+
## 收尾自检
|
|
41
|
+
完成类回复或超过 12 行的回复,发送前调用 plain_talk_check;把 error 全部改掉,warn 逐条判断。
|
|
42
|
+
|
|
43
|
+
## 覆盖规则
|
|
44
|
+
用户说"展开讲/详细说明"时,保留结论先行,正文可以长。破坏性操作仍然先确认。harness 规则优先于本节。`;
|
package/icon.png
ADDED
|
Binary file
|