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/AGENTS.md +1 -1
- package/CHANGELOG.md +24 -1
- package/README.md +14 -5
- package/THIRD-PARTY-NOTICES.md +12 -3
- package/assets/adult-fish.soul.json +2798 -840
- package/client.js +126 -13
- package/index.js +115 -9
- package/lib/attach.js +27 -48
- package/lib/bili.js +179 -4
- package/lib/biliqr.js +341 -0
- package/lib/console.js +194 -8
- package/lib/corpus-rules.js +50 -0
- package/lib/corpus.js +98 -0
- package/lib/derive.js +68 -4
- package/lib/format.js +59 -0
- package/lib/guard.js +7 -3
- package/lib/noise.js +68 -0
- package/lib/persona.js +21 -7
- package/lib/qr.js +450 -0
- package/lib/reflect.js +4 -1
- package/lib/search.js +94 -10
- package/lib/settings.js +6 -14
- package/lib/store.js +23 -2
- package/lib/tools.js +22 -8
- package/lib/version.js +1 -1
- package/package.json +2 -2
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(
|
|
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
|
-
//
|
|
608
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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' },
|
|
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
|
-
|
|
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
|
-
//
|
|
1496
|
-
//
|
|
1497
|
-
|
|
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
|
-
|
|
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
|