dsh-pisces 0.2.6 → 0.2.8

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/client.js CHANGED
@@ -467,6 +467,23 @@ window.__ModuleLoader__.load({
467
467
  stat('openQuestions', { label: '悬着的问题', value: s.openQuestions == null ? '—' : s.openQuestions }),
468
468
  stat('dreams', { label: '梦', value: s.dreams == null ? '—' : s.dreams }),
469
469
  stat('verified', { label: '核实', value: s.verified == null ? '—' : s.verified, hint: '梦里的假设后来对上了几条' }),
470
+ h(Stat, {
471
+ label: '双流',
472
+ value: s.dual ? `${s.dual.mode || '—'} · 先算 ${s.dual.plans ?? 0} · 缓存 ${s.dual.cacheHits ?? 0}` : '没开',
473
+ hint: s.dual
474
+ ? `退回 ${s.dual.fallbacks ?? 0}${s.dual.seconds ? ` · 最近 ${Number(s.dual.seconds).toFixed(1)}s` : ''}`
475
+ : '设置页可开;改完重启',
476
+ small: true,
477
+ }),
478
+ h(Stat, {
479
+ label: '语料标记',
480
+ value: s.style ? `${s.style.flagged ?? 0} 条` : '—',
481
+ hint:
482
+ s.style && Object.keys(s.style.counts || {}).length
483
+ ? Object.entries(s.style.counts).map(([k, v]) => `${k} ${v}`).join(' · ')
484
+ : '新回复命中才计数;不存原文',
485
+ small: true,
486
+ }),
470
487
  rv ? stat('refusals', { label: '拒绝', value: s.refusals == null ? '—' : s.refusals, hint: '她说过"不"的事' }) : null,
471
488
  rv ? stat('scars', { label: '疤', value: s.scars == null ? '—' : s.scars, hint: '他难过之后留下的' }) : null,
472
489
  h(Stat, { label: '上次整理', value: age(last), hint: last ? when(last) : '夜间流水线还没跑过' }),
@@ -533,8 +550,6 @@ window.__ModuleLoader__.load({
533
550
  h('p', { className: data ? 'err' : 'empty' }, data ? `设置没读到:${data.settingsError}` : '正在读设置…'))
534
551
  }
535
552
 
536
- const bili = cur.bili || {}
537
- const logged = bili.qrLogin === 'logged-in'
538
553
  const set = (path, value) => setDraft((d) => ({ ...d, [path]: value }))
539
554
 
540
555
  async function save() {
@@ -597,27 +612,43 @@ window.__ModuleLoader__.load({
597
612
  h('div', { className: 'row' }, h('button', { type: 'button', disabled: busy, onClick: save }, '保存设置')),
598
613
  note ? h('p', { className: note === '存好了,重启后生效' ? 'hint' : 'err' }, note) : null,
599
614
  ),
600
- h(Card, { title: 'B 站', right: '只读' },
601
- h('p', { className: 'hint' }, `登录状态:${logged ? '已登录 _小苍兰_' : '未登录'}`),
602
- h('div', { className: 'row' }, h('button', { type: 'button', disabled: true }, '扫码登录(下一步接)'))),
615
+ h(BiliCard, null),
603
616
  h(BaiduCard, null),
604
617
  )
605
618
  }
606
619
 
607
- // 百度贴吧登录:粘贴 BDUSS/STOKEN → 后端验证 → 状态就地刷新;不做扫码/QQ/微信。
608
- function BaiduCard() {
620
+ // 登录卡(B 站 / 百度贴吧共用):扫码或粘贴 cookie → 后端验证 → 状态就地刷新。
621
+ // B 站那张多给一条扫码路:二维码由宿主现画(lib/qr.js),页面只拿 SVG 和「登的是谁」。
622
+ function CookieCard({ title, api, placeholder, hint, qr }) {
609
623
  const [status, setStatus] = useState(null)
610
624
  const [text, setText] = useState('')
611
625
  const [busy, setBusy] = useState(false)
612
626
  const [note, setNote] = useState('')
627
+ const [q, setQ] = useState(null)
613
628
 
614
629
  useEffect(() => {
615
630
  let alive = true
616
- getJson('/baidu')
631
+ getJson(api)
617
632
  .then((d) => { if (alive) setStatus(d) })
618
633
  .catch(() => { if (alive) setStatus({ loggedIn: false }) })
619
634
  return () => { alive = false }
620
- }, [])
635
+ }, [api])
636
+
637
+ const key = q && q.key ? String(q.key) : ''
638
+ const state = q && q.state ? String(q.state) : ''
639
+ const polling = Boolean(key) && (state === 'waiting' || state === 'scanned')
640
+
641
+ // 轮询只在这一张码还活着的时候跑;状态变了(confirmed/expired)就停。
642
+ useEffect(() => {
643
+ if (!polling) return undefined
644
+ let alive = true
645
+ const timer = setInterval(() => {
646
+ getJson(`${api}/qr?key=${encodeURIComponent(key)}`)
647
+ .then((r) => { if (alive) setQ((prev) => (prev && prev.key === key ? { ...prev, ...r } : prev)) })
648
+ .catch(() => {})
649
+ }, 1200)
650
+ return () => { alive = false; clearInterval(timer) }
651
+ }, [api, key, polling])
621
652
 
622
653
  const logged = !!(status && status.loggedIn)
623
654
  const name = status && status.name ? String(status.name) : ''
@@ -627,7 +658,7 @@ window.__ModuleLoader__.load({
627
658
  setBusy(true)
628
659
  setNote('')
629
660
  try {
630
- const r = await postJson('/baidu', { cookie: text })
661
+ const r = await postJson(api, { cookie: text })
631
662
  setStatus(r)
632
663
  setText('')
633
664
  setNote(r.name ? `登上了:${r.name}` : '登上了')
@@ -638,22 +669,104 @@ window.__ModuleLoader__.load({
638
669
  }
639
670
  }
640
671
 
672
+ async function startQr() {
673
+ if (busy) return
674
+ setBusy(true)
675
+ setNote('')
676
+ try {
677
+ setQ({ ...(await postJson(`${api}/qr`, {})), state: 'waiting' })
678
+ } catch (e) {
679
+ setNote(String((e && e.message) || e))
680
+ } finally {
681
+ setBusy(false)
682
+ }
683
+ }
684
+
685
+ async function saveQr() {
686
+ if (busy || !key) return
687
+ setBusy(true)
688
+ try {
689
+ const r = await postJson(`${api}/qr/save`, { key })
690
+ setStatus(r)
691
+ setQ(null)
692
+ setNote(r.name ? `登上了:${r.name}` : '登上了')
693
+ } catch (e) {
694
+ setNote(String((e && e.message) || e))
695
+ } finally {
696
+ setBusy(false)
697
+ }
698
+ }
699
+
700
+ function dropQr() {
701
+ const k = key
702
+ setQ(null)
703
+ if (k) postJson(`${api}/qr/drop`, { key: k }).catch(() => {})
704
+ }
705
+
706
+ const qrLine = !q
707
+ ? ''
708
+ : state === 'confirmed'
709
+ ? (q.account && q.account.name
710
+ ? `登的是 ${q.account.name}${q.account.mid ? `(uid ${q.account.mid})` : ''}`
711
+ : '扫码成功,认不出是哪个号')
712
+ : state === 'scanned'
713
+ ? '扫到了,在手机上点一下「确认登录」'
714
+ : state === 'expired'
715
+ ? '二维码过期了,点「换一张」'
716
+ : (q.why || '用手机 B 站扫一下')
717
+
641
718
  return h(
642
719
  Card,
643
- { title: '百度贴吧', right: 'Cookie' },
720
+ { title, right: 'Cookie' },
644
721
  h('p', { className: 'hint' }, `登录状态:${logged ? (name ? `已登录 ${name}` : '已登录') : '未登录'}`),
722
+ qr ? h('div', { className: 'row' }, h('button', { type: 'button', disabled: busy, onClick: startQr }, q ? '换一张' : '用手机扫码登录')) : null,
723
+ q ? h('div', { className: 'qr' },
724
+ q.svg ? h('img', {
725
+ className: 'qrimg',
726
+ src: `data:image/svg+xml;charset=utf-8,${encodeURIComponent(String(q.svg))}`,
727
+ alt: '登录二维码',
728
+ width: 180,
729
+ height: 180,
730
+ }) : null,
731
+ h('p', { className: 'hint' }, qrLine),
732
+ h('div', { className: 'row' },
733
+ state === 'confirmed' ? h('button', { type: 'button', disabled: busy, onClick: saveQr }, '就是这个号,保存') : null,
734
+ h('button', { type: 'button', onClick: dropQr }, '收起'),
735
+ ),
736
+ ) : null,
645
737
  h('textarea', {
646
738
  value: text,
647
739
  rows: 3,
648
- placeholder: '把浏览器的 BDUSS=…; STOKEN=… 贴进来',
740
+ placeholder,
649
741
  onChange: (e) => setText(e.target.value),
650
742
  }),
651
743
  h('div', { className: 'row' }, h('button', { type: 'button', disabled: busy, onClick: save }, '保存并验证')),
652
744
  note ? h('p', { className: note.startsWith('登上了') ? 'hint' : 'err' }, note) : null,
653
- h('p', { className: 'hint' }, '浏览器 F12 → Application → Cookies → tieba.baidu.com → 复制 BDUSS 和 STOKEN'),
745
+ h('p', { className: 'hint' }, hint),
654
746
  )
655
747
  }
656
748
 
749
+ // B 站登录:手机扫码为主,粘贴 SESSDATA/bili_jct 是退路;登录态落 owner.json。
750
+ function BiliCard() {
751
+ return h(CookieCard, {
752
+ title: 'B 站',
753
+ api: '/bili',
754
+ qr: true,
755
+ placeholder: '把浏览器的 SESSDATA=…; bili_jct=… 贴进来',
756
+ hint: '不想扫码:浏览器 F12 → Application → Cookies → bilibili.com → 复制 SESSDATA 和 bili_jct',
757
+ })
758
+ }
759
+
760
+ // 百度贴吧登录:粘贴 BDUSS/STOKEN;只影响贴吧,不碰 B 站。
761
+ function BaiduCard() {
762
+ return h(CookieCard, {
763
+ title: '百度贴吧',
764
+ api: '/baidu',
765
+ placeholder: '把浏览器的 BDUSS=…; STOKEN=… 贴进来',
766
+ hint: '浏览器 F12 → Application → Cookies → tieba.baidu.com → 复制 BDUSS 和 STOKEN',
767
+ })
768
+ }
769
+
657
770
  // -------------------------------------------------------------------------
658
771
  // 页签:心理活动
659
772
  // -------------------------------------------------------------------------
package/index.js CHANGED
@@ -52,7 +52,8 @@ import { nicknameOf, setNickname as writeNickname } from './lib/nickname.js'
52
52
  import { clearFixedOverride, readFixedOverride, writeFixedOverride } from './lib/personafile.js'
53
53
  import { ensureHomeWithRetry, HOME_FILE } from './lib/home.js'
54
54
  import { SCHEMA as SETTINGS_SCHEMA, mergeConfig, publicSettings, readSettings, writeSettings } from './lib/settings.js'
55
- import { hot } from './lib/bili.js'
55
+ import { clearBili, hot, loginBili, readBili } from './lib/bili.js'
56
+ import { createQrLogin } from './lib/biliqr.js'
56
57
  import { clearBaidu, loginBaidu, readBaidu } from './lib/baidu.js'
57
58
  import { speak as speakOnBili } from './lib/speak.js'
58
59
  import { guideFor as teachFor } from './lib/teach.js'
@@ -235,6 +236,9 @@ export function apply(ctx, config = {}) {
235
236
  if (voiceAdapter?.ok) log(`语音回复:已接 ${voiceAdapter.kind}`)
236
237
  else log('语音回复:未接适配器(休眠)')
237
238
 
239
+ // B 站扫码登录:key 与 cookie 只活在内存里,停用时清掉(不落盘、不进日志)。
240
+ const qrLogin = createQrLogin({ log })
241
+
238
242
  let closed = false
239
243
  let disposed = false
240
244
  let store = null
@@ -659,7 +663,7 @@ export function apply(ctx, config = {}) {
659
663
  * 失败只回 {ok:false, reason}、只 log 一行,不阻塞启动(宿主没有 workspaceRegistry 也照跑)。
660
664
  */
661
665
  async function ensureHomeOnce() {
662
- if (cfg.home?.enabled === false) return { ok: false, reason: '家没开(config.home.enabled=false)' }
666
+ if (cfg.home?.enabled === false || cfg.home?.auto === false) return { ok: false, reason: '家没开(config.home.enabled=false 或 home.auto=false)' }
663
667
  let fish = null
664
668
  try {
665
669
  const s = await live()
@@ -1144,6 +1148,42 @@ export function apply(ctx, config = {}) {
1144
1148
  return { ok: true, removed: clearBaidu(root) }
1145
1149
  }
1146
1150
 
1151
+ /** B 站登录状态:只读本地 owner.json(保存时已经验过一次,不每次开面板都联网)。 */
1152
+ async function biliStatus() {
1153
+ const saved = readBili(root)
1154
+ if (!saved) return { ok: true, loggedIn: false, name: null, hasCookie: false }
1155
+ return { ok: true, loggedIn: true, name: saved.name || null, hasCookie: true }
1156
+ }
1157
+
1158
+ /** B 站登录:粘贴 SESSDATA/bili_jct → 验证 → 落盘;失败原因由 lib/bili.js 给,不打印 cookie。 */
1159
+ function biliLogin(text) {
1160
+ return loginBili({ root, text, log })
1161
+ }
1162
+
1163
+ function biliLogout() {
1164
+ return { ok: true, removed: clearBili(root) }
1165
+ }
1166
+
1167
+ /**
1168
+ * B 站扫码登录:生成码 → 轮询 → (人点保存)才落盘。
1169
+ * 四个动作都在 lib/biliqr.js 的状态机里;这里只把 root 和库边界接上。
1170
+ */
1171
+ function biliQrStart() {
1172
+ return qrLogin.start()
1173
+ }
1174
+
1175
+ function biliQrPoll(key) {
1176
+ return qrLogin.poll(key)
1177
+ }
1178
+
1179
+ async function biliQrSave(key) {
1180
+ return qrLogin.save(key, root)
1181
+ }
1182
+
1183
+ function biliQrDrop(key) {
1184
+ return qrLogin.drop(key)
1185
+ }
1186
+
1147
1187
  /**
1148
1188
  * 语音回复状态:只回有没有适配器、是哪种。
1149
1189
  * available 同时受隐藏开关约束;没有适配器时恒为 false(宿主现在就是这样)。
@@ -1177,6 +1217,13 @@ export function apply(ctx, config = {}) {
1177
1217
  baiduStatus,
1178
1218
  baiduLogin,
1179
1219
  baiduLogout,
1220
+ biliStatus,
1221
+ biliLogin,
1222
+ biliLogout,
1223
+ biliQrStart,
1224
+ biliQrPoll,
1225
+ biliQrSave,
1226
+ biliQrDrop,
1180
1227
  // 语音回复:状态 + 条件触发。不进面板;未来桥只调 voiceReply。
1181
1228
  voice: voiceInfo,
1182
1229
  voiceReply,
@@ -1422,8 +1469,18 @@ export function apply(ctx, config = {}) {
1422
1469
  // 读不到 home.json 就跳过这条判据,不影响启动。
1423
1470
  // 两条都不满足 → 不挂。
1424
1471
  if (cfg.attachToAgents !== false) {
1425
- const herHome = join(root, 'home')
1426
1472
  const homeFile = join(root, HOME_FILE)
1473
+ /** 家目录:home.json 的 workspacePath 优先(搬家后 living 就是她家);没有就退回 <root>/home。 */
1474
+ const readHomeWorkspace = () => {
1475
+ try {
1476
+ const raw = readFileSync(homeFile, 'utf8').replace(/^\uFEFF/, '')
1477
+ const j = JSON.parse(raw)
1478
+ return String(j?.workspacePath ?? j?.cwd ?? '')
1479
+ } catch {
1480
+ return ''
1481
+ }
1482
+ }
1483
+ const herHome = readHomeWorkspace() || join(root, 'home')
1427
1484
  /** 每次现读:home.json 可能在插件启动之后才由 ensureHome 写出来。 */
1428
1485
  const readHomeSessionId = () => {
1429
1486
  try {
@@ -1434,15 +1491,34 @@ export function apply(ctx, config = {}) {
1434
1491
  return ''
1435
1492
  }
1436
1493
  }
1494
+ /**
1495
+ * 她屋里开过的会话 id。宿主 llm/stream 的 options 只带 sessionId——真机
1496
+ * buildRequest 里只有 config/messages/toolHistory/tools/sessionId/signal,
1497
+ * 没有 cwd,所以落库判据没法从 options 反推 cwd。只能在 agent/created
1498
+ * 判通过时把 sid 记下来;n77 的门控不放松:只记 shouldAttach 通过的会话。
1499
+ * 只增不删,上限 200 条,超出丢最老的(最近用过的会被重新提到队尾)。
1500
+ */
1501
+ const herSessionIds = new Set()
1502
+ const rememberHerSession = (sid) => {
1503
+ if (!sid) return
1504
+ herSessionIds.delete(sid)
1505
+ herSessionIds.add(sid)
1506
+ while (herSessionIds.size > 200) herSessionIds.delete(herSessionIds.values().next().value)
1507
+ }
1437
1508
  const shouldAttach = (agent) => {
1438
1509
  try {
1439
1510
  const cwd = flatPath(cwdOf(agent))
1440
1511
  const home = flatPath(herHome)
1441
- if (cwd && home && (cwd === home || cwd.startsWith(`${home}/`))) return true
1442
1512
  const sid = sidOf(agent)
1513
+ if (cwd && home && (cwd === home || cwd.startsWith(`${home}/`))) {
1514
+ rememberHerSession(sid)
1515
+ return true
1516
+ }
1443
1517
  if (!sid) return false
1444
1518
  const saved = readHomeSessionId()
1445
- return Boolean(saved && sid === saved)
1519
+ if (!saved || sid !== saved) return false
1520
+ rememberHerSession(sid)
1521
+ return true
1446
1522
  } catch {
1447
1523
  // 判据失败最多是不挂,不能拖垮启动。
1448
1524
  return false
@@ -1492,9 +1568,22 @@ export function apply(ctx, config = {}) {
1492
1568
  }),
1493
1569
  shouldAttach,
1494
1570
  // 聊天落库的门控:llm/stream 是全局 waterfall,别的会话的生成也会经过观察器。
1495
- // 只有她家里那条会话(home.json 的 sessionId)才记 heard/utterance;每次现读,
1496
- // 读不到 home.json → false。shouldAttach 只管挂人设段,不决定记不记事件。
1497
- isHerSession: (sid) => Boolean(sid) && sid === readHomeSessionId(),
1571
+ // 只有她屋里那条会话才记 heard/utterance:home.json 的 sessionId,或 shouldAttach
1572
+ // 通过时记下的会话 id(她家工作区里新开的会话靠这一条——宿主 options 只有
1573
+ // sessionId、没有 cwd;key 的 cwd 分支只是宿主不给 sid 时的老兜底)。
1574
+ // 每次现读 home.json,读不到 → 只认记下的会话。shouldAttach 只管挂人设段。
1575
+ isHerSession: (sid, key) => {
1576
+ if (sid && sid === readHomeSessionId()) return true
1577
+ // agent/created 判通过时记下的会话:只增不删、上限 200(见 herSessionIds)。
1578
+ if (sid && herSessionIds.has(sid)) return true
1579
+ // 宿主没给 sessionId 时 attach 会退回 cwd 当会话 key:家目录(living)下的会话也算她屋里。
1580
+ if (key && String(key).startsWith('cwd:')) {
1581
+ const cwd = flatPath(String(key).slice(4))
1582
+ const home = flatPath(herHome)
1583
+ if (cwd && home && (cwd === home || cwd.startsWith(`${home}/`))) return true
1584
+ }
1585
+ return false
1586
+ },
1498
1587
  config: cfg,
1499
1588
  log,
1500
1589
  })
@@ -1532,6 +1621,8 @@ export function apply(ctx, config = {}) {
1532
1621
  },
1533
1622
  // 诊断用:提示词桥到底挂没挂上(agentsSeen/sectionsAttached/agentsNotHer…)。
1534
1623
  bridge: () => (attachOff?.info ?? null),
1624
+ // 输出侧语料标记:只给计数,不给原文(n12)。
1625
+ style: () => (attachOff?.info?.streamDebug ?? null),
1535
1626
  get nightly() {
1536
1627
  return getNightly()
1537
1628
  },
@@ -1561,6 +1652,15 @@ export function apply(ctx, config = {}) {
1561
1652
  baiduStatus: () => service.baiduStatus(),
1562
1653
  baiduLogin: (text) => service.baiduLogin(text),
1563
1654
  baiduLogout: () => service.baiduLogout(),
1655
+ // B 站登录:GET /bili 只回状态(绝不回 cookie),POST 粘贴验证,DELETE 清掉。
1656
+ biliStatus: () => service.biliStatus(),
1657
+ biliLogin: (text) => service.biliLogin(text),
1658
+ biliLogout: () => service.biliLogout(),
1659
+ // 扫码登录:POST /bili/qr 生成,GET ?key= 轮询,POST /bili/qr/save 保存,drop 取消。
1660
+ biliQrStart: () => service.biliQrStart(),
1661
+ biliQrPoll: (key) => service.biliQrPoll(key),
1662
+ biliQrSave: (key) => service.biliQrSave(key),
1663
+ biliQrDrop: (key) => service.biliQrDrop(key),
1564
1664
  readStore: () => ensureLive(),
1565
1665
  seed: service.seed,
1566
1666
  writeSeed: service.writeSeed,
@@ -1689,7 +1789,7 @@ export function apply(ctx, config = {}) {
1689
1789
  // 11. 家:已经领养/写过起点的话,在宿主侧栏里建一个工作区 + 一条会话(幂等)。
1690
1790
  // 只在事件流里有 demo 或 seed 时才建;config.home.enabled === false 时整块跳过。
1691
1791
  // 失败只 log 一行,不阻塞启动——宿主没有这些服务时插件照样跑。
1692
- if (cfg.home?.enabled !== false) {
1792
+ if (cfg.home?.enabled !== false && cfg.home?.auto !== false) {
1693
1793
  ready.then((s) => {
1694
1794
  if (!s) return
1695
1795
  try {
@@ -1725,6 +1825,12 @@ export function apply(ctx, config = {}) {
1725
1825
  const first = !disposed
1726
1826
  disposed = true
1727
1827
  closed = true
1828
+ // 内存里可能还攥着扫码登录的 cookie:停用就清掉,不靠进程退出兜底。
1829
+ try {
1830
+ qrLogin.clear()
1831
+ } catch {
1832
+ /* 清不掉就算了 */
1833
+ }
1728
1834
  if (first) {
1729
1835
  log(`已停用(apply#${n})`)
1730
1836
  }
package/lib/attach.js CHANGED
@@ -18,6 +18,10 @@ import { steerLines } from './guard.js'
18
18
  import { search as defaultSearchMemory } from './search.js'
19
19
  import { clip } from './util.js'
20
20
  import { MAX_TEXT } from './schema.js'
21
+ import { flagCorpus } from './corpus-rules.js'
22
+ // 宿主噪声的规则抽到 noise.js:写入(heard)和读取(归因/检索)共用同一份。
23
+ import { stripHarnessNoise } from './noise.js'
24
+ export { stripHarnessNoise } from './noise.js'
21
25
 
22
26
  // getStore() 返回 null(ready 还没到)时的重试间隔。定时器 unref,不拦进程退出。
23
27
  const STORE_RETRY_MS = 250
@@ -67,57 +71,13 @@ export const DEV_PROMPT =
67
71
 
68
72
  const errText = (err) => String(err?.message ?? err ?? '未知错误')
69
73
 
70
- // 宿主会往 user 消息里夹带系统噪声:计时行、时区行、运行时上下文行,以及一整块
71
- // 「这个文件属于… / 关于这个工作区的知识库 / available_skills」。它们不是他说的,
72
- // 记进 heard 会污染事件流(真机已见过他把宿主的话记成 heard)。
73
- // 口径:噪声整行删;块从标记行起删到结尾(知识库大纲、技能清单都是尾部附加块);
74
- // available_skills 连同 </available_skills> 一起删;一行全是 ─(U+2500,≥8)也是块头。
75
- //
76
74
  // 真机事故(2026-11-10):宿主把「Elapsed since the preceding model-visible message:
77
75
  // 12m 11s.」注入成最后一条 user 消息,他真正说的那句在它前面。所以光剥还不够——
78
76
  // heard 要往前找最多 HEARD_USER_WINDOW 条 user 消息,取最后一条剥完非空的。
79
- const NOISE_LINE = /^(?:Time sampled while preparing turn\b|Browser time zone\b|Elapsed since the preceding model-visible message:|Elapsed since the preceding step context:|Current runtime context\.|Current working directory:|Current DSH file policy:|Current DeepSeek Harness implementation checkout:|Current DeepSeek Harness Web GUI URL:|Approval prompts are disabled in this session:|关于这个工作区的知识库|<available_skills|<\/available_skills>)/i
80
- const NOISE_BLOCK_MARKERS = [
81
- '这个文件属于',
82
- '关于这个工作的知识库',
83
- '关于这个工作区的知识库',
84
- '知识库(指令见前文',
85
- ]
86
- const RULE_LINE = /^─{8,}$/
87
77
 
88
78
  // heard 往前找的窗口:最多看最近 4 条 user 消息(含最后一条)。
89
79
  const HEARD_USER_WINDOW = 4
90
80
 
91
- /**
92
- * 剥掉宿主夹带在用户消息里的系统噪声;只用于 heard,utterance 不清洗。
93
- * 剥完 trim;可能返回空串(整条都是注入块),调用方据此不落库。
94
- */
95
- export function stripHarnessNoise(text) {
96
- const raw = String(text ?? '')
97
- if (!raw) return ''
98
- const lines = raw.split(/\r?\n/)
99
- const out = []
100
- for (let i = 0; i < lines.length; i++) {
101
- const line = lines[i]
102
- const trimmed = line.trim()
103
- // 分隔线之后到结尾都是宿主附加块(真机形状:一条长横线把对话和注入切开)。
104
- if (RULE_LINE.test(trimmed)) break
105
- // 这几个标记开头的一段是宿主附加的尾部整块:连标记行一起删到结尾。
106
- if (NOISE_BLOCK_MARKERS.some((m) => line.includes(m))) break
107
- if (line.includes('<available_skills')) {
108
- // 技能清单有闭合标签:整块(含闭合行)删掉;没有闭合就当尾部块删到结尾。
109
- let j = i
110
- while (j < lines.length && !lines[j].includes('</available_skills>')) j++
111
- i = j
112
- continue
113
- }
114
- if (line.includes('</available_skills>')) continue
115
- if (NOISE_LINE.test(trimmed)) continue
116
- out.push(line)
117
- }
118
- return out.join('\n').trim()
119
- }
120
-
121
81
  /** 把消息的 content(字符串 / 富文本块数组)摊平成文本。 */
122
82
  function contentText(content) {
123
83
  if (typeof content === 'string') return content
@@ -379,6 +339,9 @@ export function attachToAgents(ctx, {
379
339
  chunksSeen: 0,
380
340
  textParts: 0,
381
341
  utterancesWritten: 0,
342
+ // 输出侧语料标记:只记命中条数与原因计数,不存原文(n12)。
343
+ styleFlagged: 0,
344
+ styleCounts: {},
382
345
  lastChunkShape: '',
383
346
  }
384
347
 
@@ -450,6 +413,8 @@ export function attachToAgents(ctx, {
450
413
  chunksSeen: state.chunksSeen,
451
414
  textParts: state.textParts,
452
415
  utterancesWritten: state.utterancesWritten,
416
+ styleFlagged: state.styleFlagged,
417
+ styleCounts: Object.freeze({ ...state.styleCounts }),
453
418
  lastChunkShape: state.lastChunkShape,
454
419
  }),
455
420
  stream: Object.freeze({
@@ -676,7 +641,7 @@ export function attachToAgents(ctx, {
676
641
  * onWritten 只在真正写成功之后调(异步 append 等它 resolve)——计数器要的是"落库",
677
642
  * 不是"尝试过",否则 source 不合法这类异步失败会被蒙过去。
678
643
  */
679
- function appendChat(type, source, text, onWritten) {
644
+ function appendChat(type, source, text, onWritten, extraMeta = null) {
680
645
  if (!store || !state.storeReady || !text) return false
681
646
  if (typeof store.append !== 'function') return false
682
647
  try {
@@ -684,7 +649,7 @@ export function attachToAgents(ctx, {
684
649
  type,
685
650
  source,
686
651
  text,
687
- meta: { kind: 'chat', via: 'stream' },
652
+ meta: { kind: 'chat', via: 'stream', ...(extraMeta || {}) },
688
653
  })
689
654
  // append 是异步的:失败吞掉,不重试、不阻塞流;只有成功才回调。
690
655
  if (written && typeof written.then === 'function') {
@@ -776,10 +741,24 @@ export function attachToAgents(ctx, {
776
741
  const text = full.slice(0, MAX_TEXT)
777
742
  const entry = chatEntry(sessionKey)
778
743
  if (text !== entry.utterance) {
744
+ // 输出侧语料标记:只算次数,不改回复、不存原文(n12)。
745
+ let style = null
746
+ try {
747
+ const flagged = flagCorpus([text])
748
+ if (flagged.total) style = { flagged: flagged.total, counts: flagged.counts }
749
+ } catch {
750
+ style = null
751
+ }
779
752
  // 先占位(一个流只一条);计数等 append 真正 resolve 之后才 +1。
780
753
  if (appendChat('utterance', 'agent', text, () => {
781
754
  state.utterancesWritten++
782
- })) entry.utterance = text
755
+ if (style) {
756
+ state.styleFlagged += style.flagged
757
+ for (const [k, v] of Object.entries(style.counts)) {
758
+ state.styleCounts[k] = (state.styleCounts[k] || 0) + v
759
+ }
760
+ }
761
+ }, style ? { style } : null)) entry.utterance = text
783
762
  }
784
763
  }
785
764
  } catch { /* 记录失败不影响流 */ }
@@ -812,7 +791,7 @@ export function attachToAgents(ctx, {
812
791
  let her = false
813
792
  if (typeof isHerSession === 'function') {
814
793
  try {
815
- her = Boolean(isHerSession(sid))
794
+ her = Boolean(isHerSession(sid, sessionKeyOf(options)))
816
795
  } catch (err) {
817
796
  log(`聊天落库:会话判据出错(${errText(err)}),这条跳过`)
818
797
  her = false