dsh-quality-lens 0.1.0 → 0.1.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 CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
4
4
  [![dsh](https://img.shields.io/badge/dsh-%3E%3D0.2.1--rc.2-8A2BE2)](https://github.com/deepseek-ai/DeepSeek-Harness)
5
+ [![npm](https://img.shields.io/npm/v/dsh-quality-lens)](https://www.npmjs.com/package/dsh-quality-lens)
5
6
  [![tests](https://github.com/lishinho/dsh-quality-lens/actions/workflows/test.yml/badge.svg)](https://github.com/lishinho/dsh-quality-lens/actions/workflows/test.yml)
6
7
 
7
8
  > 给每个 AI 回答做一次体检:动作行显示 **质量分 + 话题关联度**,点开是带证据的质检报告。
@@ -80,7 +81,7 @@
80
81
  2. 来源选 **本地目录**,选择本仓库根目录(包含 `package.json`);
81
82
  3. 重启 / 刷新 DSH。
82
83
 
83
- **GitHub / npm(发布后)**
84
+ **GitHub / npm**
84
85
 
85
86
  ```sh
86
87
  dsh plugin --profile web add github:lishinho/dsh-quality-lens
@@ -151,6 +152,7 @@ node scripts/calibrate.mjs --in calibration/samples.labeled.jsonl
151
152
  - 默认引擎在本地完成,**不调用模型、不联网**;
152
153
  - 报告只存本机 `$DSH_HOME/quality-lens/reports.jsonl`,删目录即清空;
153
154
  - 无遥测;Jev 模式需显式配置 key,且仅发送当前问答;
155
+ - 报告、证据、提示词建议里的凭证样式字符串(`npm_` / `ghp_` / `sk-` 等)会自动打码;旧报告在加载时也会被清洗。
154
156
  - 校准只读你自己的本机会话,不上传。
155
157
 
156
158
  ---
@@ -166,7 +168,7 @@ node scripts/calibrate.mjs --in calibration/samples.labeled.jsonl
166
168
  ## 开发
167
169
 
168
170
  ```sh
169
- node --test test/scoring.test.mjs test/host.test.mjs test/client-bundle.test.mjs test/labels-jev.test.mjs # 31 tests
171
+ node --test test/scoring.test.mjs test/host.test.mjs test/client-bundle.test.mjs test/labels-jev.test.mjs # 32 tests
170
172
  node scripts/demo.mjs # 打印样本评分
171
173
  node prototype/build.mjs # 重建自包含原型
172
174
  ```
package/lib/host.js CHANGED
@@ -10,7 +10,7 @@
10
10
  import fs from 'node:fs'
11
11
  import os from 'node:os'
12
12
  import path from 'node:path'
13
- import { scoreAnswer, scoreWithLabels, labelsToReport, isSubstantiveQuestion, RUBRIC_VERSION } from './scoring.js'
13
+ import { scoreAnswer, scoreWithLabels, labelsToReport, isSubstantiveQuestion, redactSecrets, RUBRIC_VERSION } from './scoring.js'
14
14
  import { classifyWithJev, jevConfigured } from './jev.js'
15
15
 
16
16
  export const name = 'dsh-quality-lens'
@@ -112,7 +112,7 @@ export function apply(ctx, config = {}) {
112
112
  const appendLine = (file, value) => {
113
113
  try {
114
114
  fs.mkdirSync(dir, { recursive: true })
115
- fs.appendFileSync(file, `${JSON.stringify(value)}\n`, 'utf8')
115
+ fs.appendFileSync(file, `${redactSecrets(JSON.stringify(value))}\n`, 'utf8')
116
116
  } catch (error) {
117
117
  ctx.logger?.warn?.(`[quality-lens] could not persist report: ${error?.message ?? error}`)
118
118
  }
@@ -134,9 +134,14 @@ export function apply(ctx, config = {}) {
134
134
  }
135
135
  const cutoff = Date.now() - settings.retentionDays * 24 * 60 * 60 * 1000
136
136
  const lines = text.split('\n').filter(Boolean).slice(-settings.maxReports)
137
+ let sanitized = false
138
+ const safeLines = []
137
139
  for (const line of lines) {
138
140
  try {
139
- const record = JSON.parse(line)
141
+ const safeLine = redactSecrets(line)
142
+ if (safeLine !== line) sanitized = true
143
+ safeLines.push(safeLine)
144
+ const record = JSON.parse(safeLine)
140
145
  if (!record?.messageId || typeof record.score !== 'number') continue
141
146
  if (record.at && Date.parse(record.at) < cutoff) continue
142
147
  reports.set(record.messageId, record)
@@ -145,6 +150,14 @@ export function apply(ctx, config = {}) {
145
150
  }
146
151
  }
147
152
  trim()
153
+ if (sanitized) {
154
+ // Old reports may contain a token the user pasted into a chat; rewrite them.
155
+ try {
156
+ fs.writeFileSync(reportsFile, `${safeLines.join('\n')}\n`, 'utf8')
157
+ } catch {
158
+ // Best effort; the in-memory copy is already redacted.
159
+ }
160
+ }
148
161
  }
149
162
 
150
163
  const registerRoute = (route) => {
package/lib/scoring.js CHANGED
@@ -67,6 +67,29 @@ const NEGATIVE = {
67
67
 
68
68
  const clamp = (n, lo = 0, hi = 100) => Math.max(lo, Math.min(hi, Math.round(n)))
69
69
 
70
+ /** Credential-shaped strings that must never appear in a report, a chip, or a share card. */
71
+ const SECRET_PATTERNS = [
72
+ /\bnpm_[A-Za-z0-9]{20,}\b/g,
73
+ /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g,
74
+ /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g,
75
+ /\bsk-[A-Za-z0-9_-]{16,}\b/g,
76
+ /\bAKIA[0-9A-Z]{16}\b/g,
77
+ /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g,
78
+ /\b(?:Bearer\s+)[A-Za-z0-9._-]{20,}\b/gi,
79
+ ]
80
+
81
+ /**
82
+ * Redact credential-shaped substrings from anything user-facing or persisted.
83
+ * The report must never echo a token a user pasted into the chat.
84
+ * @param {string} text
85
+ * @returns {string}
86
+ */
87
+ export function redactSecrets(text) {
88
+ let out = String(text ?? '')
89
+ for (const pattern of SECRET_PATTERNS) out = out.replace(pattern, '[REDACTED]')
90
+ return out
91
+ }
92
+
70
93
  /** Small synonym expansion so Chinese paraphrases are not scored as off-topic. */
71
94
  const TERM_SYNONYMS = [
72
95
  ['便宜', '价格', '费用', '费率', '更低', '贵', '成本'],
@@ -141,12 +164,18 @@ export function termFrequencies(text) {
141
164
  export function isSubstantiveQuestion(text) {
142
165
  const raw = String(text ?? '').trim()
143
166
  if (!raw) return false
144
- const compact = raw.replace(/[\s。!?!?,,、~~…]+/g, '')
167
+ const redacted = redactSecrets(raw)
168
+ if (redacted !== raw) {
169
+ const residual = redacted.replace(/\[REDACTED\]/g, '').replace(/[\s。!?!?,,、~~::=]+/g, '')
170
+ // "token: npm_..." or a bare token is not a question.
171
+ if (residual.length < 8) return false
172
+ }
173
+ const compact = redacted.replace(/[\s。!?!?,,、~~…]+/g, '')
145
174
  if (compact.length === 0) return false
146
175
  const ack = /^(好的?|可以了?|行|嗯+|哦+|收到|明白了?|了解|谢谢|感谢|辛苦了?|继续|开始吧?|我试试|试一下|好滴|ok|okay|thanks?|thx)$/i
147
176
  if (ack.test(compact)) return false
148
177
  if (/^(好的?|可以了?|嗯+|哦+|行|收到|谢谢|继续)+$/.test(compact)) return false
149
- const hasAsk = /[??]|怎么|如何|什么|为什么|为啥|哪|多少|是否|能否|可不可以|吗|呢|请教|请问|介绍|解释|对比|比较|建议|推荐|写|生成|实现|排查|安装|配置|设计|分析|评估|总结|整理/.test(raw)
178
+ const hasAsk = /[??]|怎么|如何|什么|为什么|为啥|哪|多少|是否|能否|可不可以|吗|呢|请教|请问|介绍|解释|对比|比较|建议|推荐|写|生成|实现|排查|安装|配置|设计|分析|评估|总结|整理/.test(redacted)
150
179
  if (!hasAsk && compact.length < 10) return false
151
180
  return compact.length >= 2
152
181
  }
@@ -275,7 +304,7 @@ function pickEvidence(answer, stats, topic) {
275
304
  const evidence = []
276
305
  const push = (dim, quote, why) => {
277
306
  if (!quote || evidence.some((item) => item.quote === quote)) return
278
- evidence.push({ dim, quote, why })
307
+ evidence.push({ dim, quote: redactSecrets(quote), why: redactSecrets(why) })
279
308
  }
280
309
  const sentences = stats.sentences
281
310
  if (sentences[0]) push('relevance', sentences[0], '开篇直接回应问题')
@@ -605,8 +634,8 @@ function continuationScore(contextAnswer, answer) {
605
634
 
606
635
  export function classifyLabels(input) {
607
636
  const answer = String(input?.answer ?? '').trim()
608
- const question = String(input?.question ?? '').trim() || '(未捕获到用户问题)'
609
- const context = String(input?.context ?? '').trim()
637
+ const question = redactSecrets(String(input?.question ?? '').trim()) || '(未捕获到用户问题)'
638
+ const context = redactSecrets(String(input?.context ?? '').trim())
610
639
  const contextAnswer = String(input?.contextAnswer ?? '').trim()
611
640
  const stats = answerStats(answer)
612
641
  let topic = topicCoverage(question, answer, new Set(tokenize(answer)))
@@ -713,7 +742,8 @@ function dimsFromLabels(labels) {
713
742
  */
714
743
  export function labelsToReport(labels, extra = {}) {
715
744
  const answer = String(extra.answer ?? '')
716
- const question = String(extra.question ?? '')
745
+ const question = redactSecrets(String(extra.question ?? ''))
746
+ const context = redactSecrets(String(extra.context ?? ''))
717
747
  const stats = labels.stats ?? answerStats(answer)
718
748
  const topic = labels.topic
719
749
  const dims = labels.dims ?? dimsFromLabels({ ...labels, answerText: answer })
@@ -732,7 +762,7 @@ export function labelsToReport(labels, extra = {}) {
732
762
  const confidence = labels.source === 'jev' ? labels.confidence ?? 'medium' : stats.chars < 120 || topic.intents.length === 0 ? 'low' : 'medium'
733
763
  const evidence = pickEvidence(answer, stats, topic)
734
764
  const suggestions = buildSuggestions(topic.intents, dims, topic)
735
- const tipQuestion = topic.contextUsed && extra.context ? String(extra.context) : question
765
+ const tipQuestion = topic.contextUsed && context ? context : question
736
766
  const promptTips = buildPromptTips({ question: tipQuestion, dims, topic, labels })
737
767
 
738
768
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-quality-lens",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "为每个 DeepSeek Harness 回答做质量与话题关联体检,并给出可复制的提示词改进建议。只观测不改写,默认本地评分,可选 Jev 精评。",
5
5
  "type": "module",
6
6
  "main": "lib/host.js",