dsh-biocore 0.1.4 → 0.1.5

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/lib/index.js CHANGED
@@ -167,6 +167,7 @@ export function readFullConfig(dataDir) {
167
167
  embedding: storedEmbedding(source),
168
168
  rerank: normalizeRerank(source.rerank),
169
169
  expression: normalizeExpression(source.expression),
170
+ sanitize_reply: source.sanitize_reply !== false,
170
171
  }
171
172
  }
172
173
 
@@ -176,6 +177,7 @@ function writeFullConfig(file, config) {
176
177
  embedding: config.embedding,
177
178
  rerank: config.rerank,
178
179
  expression: config.expression,
180
+ sanitize_reply: config.sanitize_reply !== false,
179
181
  }, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 })
180
182
  try { chmodSync(file, 0o600) } catch { /* Windows may ignore POSIX modes */ }
181
183
  }
@@ -191,6 +193,7 @@ export async function handleSettings(req, res, dataDir) {
191
193
  embedding: maskedEmbedding(config.embedding),
192
194
  rerank: maskedRerank(config.rerank),
193
195
  expression: config.expression,
196
+ sanitize_reply: config.sanitize_reply !== false,
194
197
  restart_required: false,
195
198
  })
196
199
  }
@@ -213,7 +216,8 @@ export async function handleSettings(req, res, dataDir) {
213
216
  const hasEmbedding = body.embedding && typeof body.embedding === 'object'
214
217
  const hasRerank = body.rerank && typeof body.rerank === 'object'
215
218
  const hasExpression = body.expression && typeof body.expression === 'object'
216
- if (!hasEmbedding && !hasRerank && !hasExpression) {
219
+ const hasSanitize = typeof body.sanitize_reply === 'boolean'
220
+ if (!hasEmbedding && !hasRerank && !hasExpression && !hasSanitize) {
217
221
  return sendJson(res, 400, { ok: false, reason: 'no_settings' })
218
222
  }
219
223
  if (hasEmbedding) {
@@ -236,12 +240,14 @@ export async function handleSettings(req, res, dataDir) {
236
240
  config.rerank = rerank
237
241
  }
238
242
  if (hasExpression) config.expression = normalizeExpression(body.expression)
243
+ if (hasSanitize) config.sanitize_reply = body.sanitize_reply === true
239
244
  writeFullConfig(paths.file, config)
240
245
  return sendJson(res, 200, {
241
246
  ok: true,
242
247
  embedding: maskedEmbedding(config.embedding),
243
248
  rerank: maskedRerank(config.rerank),
244
249
  expression: config.expression,
250
+ sanitize_reply: config.sanitize_reply !== false,
245
251
  restart_required: true,
246
252
  hint: '重启 BioCore 生效',
247
253
  })
@@ -26,6 +26,11 @@ import { applyGateText, assistantTextOf } from './safety-gate.js'
26
26
  import { defaultStoreDir } from './trace.js'
27
27
  import { runtimeAction, runTurn } from '../py-bridge.js'
28
28
  import { EXPRESSION_DEFAULTS, readExpressionSettings } from './expression-config.js'
29
+ import {
30
+ readSanitizeReplySetting,
31
+ sanitizeReply,
32
+ sanitizeReplyEnabled,
33
+ } from './sanitize-reply.js'
29
34
 
30
35
  export const TURN_CONTEXT_NAME = 'biocore-expression-turn'
31
36
  export const TURN_CONTEXT_ORDER = -190
@@ -118,6 +123,7 @@ export function turnEndPayload(options) {
118
123
  store: opts.store || defaultStoreDir(),
119
124
  capture: false,
120
125
  memoryEnabled: false,
126
+ sanitize_reply: opts.sanitizeReply !== false,
121
127
  diagnostics,
122
128
  }, diagnostics, {
123
129
  modules_trace: diagnostics.modules,
@@ -135,6 +141,9 @@ export function makeCoreSessionListener(options) {
135
141
  const getExpression = typeof opts.getExpression === 'function'
136
142
  ? opts.getExpression
137
143
  : () => readExpressionSettings(stateDir)
144
+ const getSanitizeReply = typeof opts.getSanitizeReply === 'function'
145
+ ? opts.getSanitizeReply
146
+ : () => readSanitizeReplySetting(stateDir)
138
147
  const store = opts.store || defaultStoreDir()
139
148
  const log = typeof opts.log === 'function' ? opts.log : (line) => {
140
149
  try { console.error(line) } catch { /* noop */ }
@@ -186,9 +195,11 @@ export function makeCoreSessionListener(options) {
186
195
  }).catch(() => null)
187
196
  return
188
197
  }
189
- // assistant/message: gate + trace via one turn_end call.
198
+ // assistant/message: sanitize -> gate + trace via one turn_end call.
190
199
  const original = assistantTextOf(event)
191
200
  if (!original) return
201
+ const sanitizeEnabled = sanitizeReplyEnabled(getSanitizeReply())
202
+ const cleaned = sanitizeEnabled ? sanitizeReply(original) : original
192
203
  const userText = lastUser.get(sessionId) || ''
193
204
  void (async () => {
194
205
  try {
@@ -198,11 +209,12 @@ export function makeCoreSessionListener(options) {
198
209
  const state = read && !read.error && !read.missing ? read.state : {}
199
210
  const payload = turnEndPayload({
200
211
  sessionId,
201
- assistantText: original,
212
+ assistantText: cleaned,
202
213
  userText,
203
214
  switches,
204
215
  store,
205
216
  state,
217
+ sanitizeReply: sanitizeEnabled,
206
218
  })
207
219
  const result = await runAction('turn_end', payload)
208
220
  if (!result || result.ok !== true) return
@@ -216,6 +228,9 @@ export function makeCoreSessionListener(options) {
216
228
  + ' blocked=' + (gate.blocked === true ? '1' : '0')
217
229
  + ' rule=' + stringId(gate.rule || '')
218
230
  + ' chars=' + original.length + '->' + gate.text.length)
231
+ } else if (cleaned !== original) {
232
+ // No gate rewrite: still write the sanitized text back.
233
+ applyGateText(event, cleaned)
219
234
  }
220
235
  if (result.trace_file) {
221
236
  logLine(log, CORE_TRACE_PREFIX + ' session=' + sessionId
@@ -0,0 +1,60 @@
1
+ /** Strip BioCore output markers from assistant replies (task-223).
2
+ *
3
+ * Removes:
4
+ * - <bc>...</bc> and <biocore>...</biocore> blocks (also unclosed to EOL/EOS)
5
+ * - bare compact JSON objects that only carry e/i[/n] keys
6
+ * - extra whitespace; empty result stays empty (never falls back to input)
7
+ */
8
+ import { readFileSync } from 'node:fs'
9
+ import { join } from 'node:path'
10
+
11
+ export const DEFAULT_SANITIZE_REPLY = true
12
+
13
+ const TAG_RE = /<(bc|biocore)\b[^>]*>/gi
14
+ const BARE_JSON_RE = /\{\s*"e"\s*:\s*"[^"\\]*(?:\\.[^"\\]*)*"\s*,\s*"i"\s*:\s*-?\d+(?:\.\d+)?(?:\s*,\s*"n"\s*:\s*-?\d+(?:\.\d+)?)?\s*\}/g
15
+
16
+ function stripClosedTags(text) {
17
+ return String(text)
18
+ .replace(/<bc\b[^>]*>[\s\S]*?<\/bc\s*>/gi, '')
19
+ .replace(/<biocore\b[^>]*>[\s\S]*?<\/biocore\s*>/gi, '')
20
+ }
21
+
22
+ function stripUnclosedTags(text) {
23
+ // Until line end or a Chinese/ASCII sentence terminator; the terminator stays.
24
+ return String(text)
25
+ .replace(/<bc\b[^>]*>[\s\S]*?(?:[\n。!?!?]|$)/gi, '')
26
+ .replace(/<biocore\b[^>]*>[\s\S]*?(?:[\n。!?!?]|$)/gi, '')
27
+ }
28
+
29
+ function normalizeWhitespace(text) {
30
+ return String(text)
31
+ .replace(/[ \t]{2,}/g, ' ')
32
+ .replace(/[ \t]*\n[ \t]*/g, '\n')
33
+ .replace(/\n{3,}/g, '\n\n')
34
+ .trim()
35
+ }
36
+
37
+ /** Main entry: returns the cleaned reply (may be ''); never reverts to input. */
38
+ export function sanitizeReply(text, options) {
39
+ const opts = options || {}
40
+ if (opts.enabled === false) return String(text == null ? '' : text)
41
+ const source = String(text == null ? '' : text)
42
+ let out = stripClosedTags(source)
43
+ out = stripUnclosedTags(out)
44
+ out = out.replace(BARE_JSON_RE, '')
45
+ return normalizeWhitespace(out)
46
+ }
47
+
48
+ /** Config.json top-level flag: only an explicit false disables cleaning. */
49
+ export function readSanitizeReplySetting(dataDir) {
50
+ try {
51
+ const doc = JSON.parse(readFileSync(join(String(dataDir || '').trim(), 'config.json'), 'utf8'))
52
+ if (doc && typeof doc === 'object' && doc.sanitize_reply === false) return false
53
+ } catch { /* missing/bad config -> default on */ }
54
+ return DEFAULT_SANITIZE_REPLY
55
+ }
56
+
57
+ /** Payload helper: sanitize_reply defaults to true unless explicitly false. */
58
+ export function sanitizeReplyEnabled(value) {
59
+ return value !== false
60
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-biocore",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "description": "BioCore DSH UI: per-session state tab, standalone instrument panel, /api/biocore/state, session/event one_turn updates, /bio sync.",
6
6
  "main": "lib/index.js",
package/ui/panel.html CHANGED
@@ -247,6 +247,7 @@
247
247
  <dl class="advanced-info">
248
248
  <div><dt>数据目录</dt><dd id="dataDirText">宿主托管</dd></div>
249
249
  <div><dt>插件版本</dt><dd id="pluginVersion">0.1.0</dd></div>
250
+ <div><dt>出口清洗</dt><dd id="sanitizeReplyText">开</dd></div>
250
251
  </dl>
251
252
  <div class="advanced-actions">
252
253
  <button id="resetLayout" class="advanced-button" type="button">恢复默认布局</button>
package/ui/panel.js CHANGED
@@ -1050,6 +1050,7 @@
1050
1050
  fillModel(embedding)
1051
1051
  fillRerank(body.rerank)
1052
1052
  fillExpression(body.expression)
1053
+ setText('sanitizeReplyText', body.sanitize_reply === false ? '关' : '开')
1053
1054
  setModelStatus('已载入')
1054
1055
  return
1055
1056
  }