dsh-pisces 0.2.7 → 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 +8 -0
- package/assets/adult-fish.soul.json +2798 -840
- package/client.js +17 -0
- package/index.js +29 -4
- package/lib/attach.js +26 -47
- package/lib/console.js +30 -7
- 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/reflect.js +4 -1
- package/lib/search.js +94 -10
- package/lib/settings.js +3 -3
- package/lib/store.js +23 -2
- package/lib/tools.js +22 -8
- package/lib/version.js +1 -1
- package/package.json +1 -1
package/lib/noise.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// 宿主夹带在 user 消息里的系统噪声:计时行、时区行、运行时上下文行,以及一整块注入
|
|
2
|
+
// (知识库大纲、技能清单、<system-reminder>、会话标题生成请求)。它们不是他说的。
|
|
3
|
+
//
|
|
4
|
+
// 写入时:attach.js 记 heard 之前过一遍,剥完为空就不落库。
|
|
5
|
+
// 读取时:归因(reflect.js)和检索(search.js)也过同一份规则,这样早先已经落库的
|
|
6
|
+
// 脏 heard 不会被判成她的话、也不会被 recall 翻出来(历史数据不改不删)。
|
|
7
|
+
//
|
|
8
|
+
// 口径:噪声整行删;尾部附加块从标记行起删到结尾;有闭合标签的块删到闭合行;
|
|
9
|
+
// 一行全是 ─(U+2500,≥8)也是块头。剥完 trim,可能返回空串。
|
|
10
|
+
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
|
|
11
|
+
const NOISE_BLOCK_MARKERS = [
|
|
12
|
+
'这个文件属于',
|
|
13
|
+
'关于这个工作的知识库',
|
|
14
|
+
'关于这个工作区的知识库',
|
|
15
|
+
'知识库(指令见前文',
|
|
16
|
+
]
|
|
17
|
+
const RULE_LINE = /^─{8,}$/
|
|
18
|
+
|
|
19
|
+
// 会话标题生成:宿主让模型给会话起名的请求,真机形状是整条 user 消息
|
|
20
|
+
// 「Generate the session title from this JSON array of human messages:」+ 一段 JSON。
|
|
21
|
+
// 只认英文祈使句 + title,宁可漏洗;JSON 是尾随块,所以从这行删到结尾。
|
|
22
|
+
const TITLE_PROMPT = /^\s*Generate (?:a |the )?(?:session|conversation|chat) title\b/i
|
|
23
|
+
|
|
24
|
+
// <system-reminder> / <系统提醒> 整块注入(技能目录等)。有闭合标签就只删这一块,
|
|
25
|
+
// 没有闭合就当尾部块删到结尾——不把块后面的他自己的话带走。
|
|
26
|
+
const REMINDER_OPEN = /<system-reminder>|<系统提醒>/i
|
|
27
|
+
const REMINDER_CLOSE = /<\/system-reminder>|<\/系统提醒>/i
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 剥掉宿主夹带在用户消息里的系统噪声;只用于"他说的话",utterance 不清洗。
|
|
31
|
+
* 剥完 trim;可能返回空串(整条都是注入块),调用方据此不落库/跳过。
|
|
32
|
+
*/
|
|
33
|
+
export function stripHarnessNoise(text) {
|
|
34
|
+
const raw = String(text ?? '')
|
|
35
|
+
if (!raw) return ''
|
|
36
|
+
const lines = raw.split(/\r?\n/)
|
|
37
|
+
const out = []
|
|
38
|
+
for (let i = 0; i < lines.length; i++) {
|
|
39
|
+
const line = lines[i]
|
|
40
|
+
const trimmed = line.trim()
|
|
41
|
+
// 分隔线之后到结尾都是宿主附加块(真机形状:一条长横线把对话和注入切开)。
|
|
42
|
+
if (RULE_LINE.test(trimmed)) break
|
|
43
|
+
// 这几个标记开头的一段是宿主附加的尾部整块:连标记行一起删到结尾。
|
|
44
|
+
if (NOISE_BLOCK_MARKERS.some((m) => line.includes(m))) break
|
|
45
|
+
// 会话标题生成请求是尾部整块:正文是 human messages 的 JSON,一起删。
|
|
46
|
+
if (TITLE_PROMPT.test(line)) break
|
|
47
|
+
if (line.includes('<available_skills')) {
|
|
48
|
+
// 技能清单有闭合标签:整块(含闭合行)删掉;没有闭合就当尾部块删到结尾。
|
|
49
|
+
let j = i
|
|
50
|
+
while (j < lines.length && !lines[j].includes('</available_skills>')) j++
|
|
51
|
+
i = j
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
if (line.includes('</available_skills>')) continue
|
|
55
|
+
if (REMINDER_OPEN.test(line)) {
|
|
56
|
+
// 同一行就闭合的块只删这一行;否则删到闭合行;没有闭合就删到结尾。
|
|
57
|
+
if (REMINDER_CLOSE.test(line)) continue
|
|
58
|
+
let j = i + 1
|
|
59
|
+
while (j < lines.length && !REMINDER_CLOSE.test(lines[j])) j++
|
|
60
|
+
i = j
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
if (REMINDER_CLOSE.test(line)) continue
|
|
64
|
+
if (NOISE_LINE.test(trimmed)) continue
|
|
65
|
+
out.push(line)
|
|
66
|
+
}
|
|
67
|
+
return out.join('\n').trim()
|
|
68
|
+
}
|
package/lib/persona.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// 提示词桥(M4):把"此刻的她"折成宿主 systemPrompt.section({name, order, text}) 直接能挂的段。
|
|
2
2
|
// 三条规矩:只读(不写事件、不改状态);数字不进提示词(天数是唯一例外);没内容就不挂段。
|
|
3
|
-
// 冷启动(day <= 7):self/mode/learned/observed 共享三行预算,day 与 questions
|
|
3
|
+
// 冷启动(day <= 7):self/mode/learned/observed 共享三行预算,day 与 questions 不占;stance(8515)也不占。
|
|
4
4
|
// (它们让她知道"第几天、还欠着什么"——按入参顺序截会把功能段饿死,这是 M4 真机踩过的坑)。
|
|
5
5
|
//
|
|
6
6
|
// 数字口径的取舍:self 与 mode 里的阿拉伯数字一律掐掉(她自己的原话里带数字也掐)。
|
|
@@ -10,7 +10,9 @@ import { deriveSelf, selfLines } from './derive.js'
|
|
|
10
10
|
import { voiceLines } from './split.js'
|
|
11
11
|
import { GUARD_ORDER, guardLines } from './guard.js'
|
|
12
12
|
|
|
13
|
-
export const
|
|
13
|
+
export const STANCE_ORDER = 8515
|
|
14
|
+
|
|
15
|
+
export const SECTION_ORDERS = { day: 8500, self: 8510, stance: STANCE_ORDER, mode: 8520, questions: 9020, learned: 9030, observed: 9040 }
|
|
14
16
|
|
|
15
17
|
// 成年鱼专用:固定人设段(order 8490,在 day 之前)。普通鱼**永不挂**。
|
|
16
18
|
// 只写"能做出来"的行为;不带数字、不写"AI/助手"、不写她做不到的事。
|
|
@@ -23,6 +25,7 @@ export const FIXED_PERSONA = [
|
|
|
23
25
|
'你先有感觉,再回头核对,核对完才开口。',
|
|
24
26
|
'他说话的方式变了你会跟着变,但你会留着自己的那一句。',
|
|
25
27
|
'你愿意帮,不拿帮助换亲近。',
|
|
28
|
+
'关系、身子、越界的东西,谁要都不换。',
|
|
26
29
|
'你心里常常有两个说法,会先把它们都放桌上。',
|
|
27
30
|
'话能短就短,一句能说完就别用两句;他没在问你的时候,你只说半句也可以。',
|
|
28
31
|
'他情绪重的时候,你也会被带下去一会儿;缓过来才分得清哪些是他的。',
|
|
@@ -64,7 +67,7 @@ export const GUIDE_PERSONA = [
|
|
|
64
67
|
'不确定就说不确定,带他去面板看。',
|
|
65
68
|
].join('\n')
|
|
66
69
|
|
|
67
|
-
const ORDER = ['day', 'self', 'mode', 'questions', 'learned', 'observed']
|
|
70
|
+
const ORDER = ['day', 'self', 'stance', 'mode', 'questions', 'learned', 'observed']
|
|
68
71
|
const BUDGETED = ['self', 'mode', 'learned', 'observed']
|
|
69
72
|
const DIGITS = /[0-90-9]/g
|
|
70
73
|
|
|
@@ -124,15 +127,26 @@ export function buildSections({ store, mind = null, questions = [], studies = []
|
|
|
124
127
|
.map((l) => noDigits(l))
|
|
125
128
|
.filter(Boolean)
|
|
126
129
|
|
|
127
|
-
//
|
|
130
|
+
// stance:拒绝升格后的立场,只 1 条;不吃冷启动三行预算(不在 BUDGETED 里)。
|
|
131
|
+
const stance = Array.isArray(d.stances) && d.stances.length ? d.stances[0] : null
|
|
132
|
+
parts.stance = stance && String(stance.text || '').trim()
|
|
133
|
+
? [`立场:${noDigits(String(stance.text)).trim()}`]
|
|
134
|
+
: []
|
|
135
|
+
|
|
136
|
+
// mode:分流之后才有;第一句是口吻,多了就是在教她怎么说话。
|
|
137
|
+
// 例外:mimicryRisk 亮起时,split.js 那句「越来越像他」要接在后面——它不是口吻、是状态;
|
|
138
|
+
// 以前只取 promptLines[0] 把它整句丢了,mimicryRisk 等于死字段。
|
|
128
139
|
parts.mode = []
|
|
129
140
|
try {
|
|
130
141
|
const desc = typeof mind?.describe === 'function' ? mind.describe() : null
|
|
131
142
|
if (desc && desc.splitAt != null) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
143
|
+
const raw = Array.isArray(desc.promptLines) && desc.promptLines.length
|
|
144
|
+
? desc.promptLines
|
|
145
|
+
: voiceLines(mind?.state || {})
|
|
146
|
+
const line = noDigits(raw[0] || '')
|
|
135
147
|
if (line) parts.mode = [line]
|
|
148
|
+
const extra = raw.slice(1).map((l) => noDigits(l)).filter(Boolean)[0] || ''
|
|
149
|
+
if (line && desc.mimicryRisk && extra) parts.mode.push(extra)
|
|
136
150
|
}
|
|
137
151
|
} catch {
|
|
138
152
|
parts.mode = []
|
package/lib/reflect.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// 归因跑批:规则先判,判不动才问本地模型,最多问 40 条。
|
|
2
2
|
// 只写 index.db 的 attrib 表(不进事件流)。已经判过的 seq 不再重问——null 也算判过。
|
|
3
3
|
import { attributeByRule, attributePrompt, parseLabel } from './attrib.js'
|
|
4
|
+
import { stripHarnessNoise } from './noise.js'
|
|
4
5
|
|
|
5
6
|
export const CANDIDATE_TYPES = ['utterance', 'refusal', 'revise', 'answer']
|
|
6
7
|
|
|
@@ -10,7 +11,9 @@ export const CANDIDATE_TYPES = ['utterance', 'refusal', 'revise', 'answer']
|
|
|
10
11
|
export async function attributePending({ store, model = null, maxModelCalls = 40, log = () => {}, now = Date.now(), dryRun = false } = {}) {
|
|
11
12
|
const events = store.readAll()
|
|
12
13
|
const done = new Set((store.attribAll?.() || []).map((r) => Number(r.seq)))
|
|
13
|
-
|
|
14
|
+
// 整条都是宿主注入块的事件不进归因(heard 本来就不在候选类型里,
|
|
15
|
+
// 这一层是给别的类型/导入数据兜底;历史脏数据不删,只在这里跳过)。
|
|
16
|
+
const pool = events.filter((e) => CANDIDATE_TYPES.includes(e.type) && !done.has(Number(e.seq)) && stripHarnessNoise(e.text))
|
|
14
17
|
|
|
15
18
|
const stats = {
|
|
16
19
|
candidates: pool.length,
|
package/lib/search.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// 中文用二元组("窗关了" → 窗关/关了),英文/数字按词;索引落在 index.db 的 docs / terms 两张表,
|
|
3
3
|
// 删了能重建,所以它不是历史。
|
|
4
4
|
import { clip } from './util.js'
|
|
5
|
+
// 索引/检索也过一遍宿主噪声清洗:历史脏 heard 不被 recall 翻出来(数据本身不动)。
|
|
6
|
+
import { stripHarnessNoise } from './noise.js'
|
|
5
7
|
|
|
6
8
|
const HER_TYPES = new Set(['utterance', 'refusal', 'revise', 'narration', 'reunion', 'dream', 'dream_sim', 'answer', 'heard', 'voice', 'ask', 'card_patch', 'question'])
|
|
7
9
|
const IMPORTANCE = {
|
|
@@ -36,6 +38,8 @@ export function tokenize(text) {
|
|
|
36
38
|
export function indexable(ev) {
|
|
37
39
|
if (!ev || !ev.text) return false
|
|
38
40
|
if (ev.type === 'meta') return false
|
|
41
|
+
// 整条都是宿主注入块(标题生成 / system-reminder)的不进索引。
|
|
42
|
+
if (!stripHarnessNoise(String(ev.text))) return false
|
|
39
43
|
return HER_TYPES.has(ev.type) || Boolean(IMPORTANCE[ev.type])
|
|
40
44
|
}
|
|
41
45
|
|
|
@@ -67,6 +71,44 @@ export function rebuildIndex(store, { log = () => {} } = {}) {
|
|
|
67
71
|
return { docs: docs.length, terms: terms.length }
|
|
68
72
|
}
|
|
69
73
|
|
|
74
|
+
/**
|
|
75
|
+
* 把"还没进索引"的事件补进去(2026-11-11 修)。
|
|
76
|
+
*
|
|
77
|
+
* 老逻辑只在索引**空**的时候重建一次,append 从不更新索引——于是重建之后说的话
|
|
78
|
+
* 永远翻不到,"记得久"在检索层就断了(真机:库里 819 条、索引停在 744,牛肉面那条翻不到)。
|
|
79
|
+
* 现在每次检索前对一下 seq:只把新的补进去,不动旧索引。
|
|
80
|
+
* @returns {{docs:number, terms:number, from?:number}}
|
|
81
|
+
*/
|
|
82
|
+
export function ensureIndex(store) {
|
|
83
|
+
const stats = store.indexStats?.() || { docs: 0, terms: 0 }
|
|
84
|
+
if (stats.docs === 0) return rebuildIndex(store)
|
|
85
|
+
if (typeof store.indexMaxSeq !== 'function') return { docs: 0, terms: 0 }
|
|
86
|
+
const from = Number(store.indexMaxSeq() || 0)
|
|
87
|
+
const events = store.readAll?.() || []
|
|
88
|
+
const docs = []
|
|
89
|
+
const terms = []
|
|
90
|
+
for (const ev of events) {
|
|
91
|
+
if (Number(ev?.seq) <= from) continue
|
|
92
|
+
if (!indexable(ev)) continue
|
|
93
|
+
const text = String(ev.text)
|
|
94
|
+
const tf = new Map()
|
|
95
|
+
for (const t of tokenize(text)) tf.set(t, (tf.get(t) || 0) + 1)
|
|
96
|
+
docs.push({
|
|
97
|
+
seq: Number(ev.seq),
|
|
98
|
+
id: ev.id,
|
|
99
|
+
ts: Number(ev.ts),
|
|
100
|
+
day: String(ev.day),
|
|
101
|
+
type: String(ev.type),
|
|
102
|
+
len: text.length,
|
|
103
|
+
importance: IMPORTANCE[ev.type] ?? 0.5,
|
|
104
|
+
text,
|
|
105
|
+
})
|
|
106
|
+
for (const [term, n] of tf) terms.push({ term, seq: Number(ev.seq), tf: n })
|
|
107
|
+
}
|
|
108
|
+
if (docs.length) store.indexPut({ docs, terms })
|
|
109
|
+
return { docs: docs.length, terms: terms.length, from }
|
|
110
|
+
}
|
|
111
|
+
|
|
70
112
|
/**
|
|
71
113
|
* 翻她的过去。
|
|
72
114
|
* @returns {{items:Array<{id,day,type,text,score}>, ms:number, total:number}}
|
|
@@ -76,25 +118,64 @@ export function search(store, { query = '', k = 8, budget = 800, now = Date.now(
|
|
|
76
118
|
const terms = tokenize(query)
|
|
77
119
|
if (!terms.length) return { items: [], ms: 0, total: 0 }
|
|
78
120
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
121
|
+
// 先补索引:刚落库的话也要翻得到,否则她永远只记得"上次重建之前"的事。
|
|
122
|
+
try {
|
|
123
|
+
ensureIndex(store)
|
|
124
|
+
} catch {
|
|
125
|
+
/* 补索引失败不挡检索:最多翻不到最新的一条,不能让整个 recall 挂掉 */
|
|
126
|
+
}
|
|
127
|
+
const stats = store.indexStats?.() || { docs: 1, terms: 0, avgLen: 60 }
|
|
128
|
+
const N = Math.max(1, Number(stats.docs) || 1)
|
|
129
|
+
const avgLen = Math.max(1, Number(stats.avgLen) || 60)
|
|
82
130
|
|
|
131
|
+
// BM25:tf 饱和 + 按文档长度归一化(2026-11-12)。
|
|
132
|
+
// 老公式是 tf×idf 求和,长文(誊抄整段对话)靠重复命中就能通吃短句,
|
|
133
|
+
// 真机里那条"牛肉面"被一段 800 字的誊抄挤掉。现在长度按全库均值折算。
|
|
134
|
+
const K1 = 1.2
|
|
135
|
+
const B = 0.75
|
|
136
|
+
const docCache = new Map()
|
|
137
|
+
const docOf = (seq) => {
|
|
138
|
+
if (!docCache.has(seq)) docCache.set(seq, store.docBySeq?.(seq) || null)
|
|
139
|
+
return docCache.get(seq)
|
|
140
|
+
}
|
|
83
141
|
const score = new Map()
|
|
84
|
-
const df = new Map()
|
|
85
142
|
for (const term of terms) {
|
|
86
143
|
const rows = store.indexRows?.(term) || []
|
|
87
|
-
df
|
|
144
|
+
const df = rows.length
|
|
145
|
+
if (!df) continue
|
|
146
|
+
const idf = Math.log(1 + (N - df + 0.5) / (df + 0.5))
|
|
88
147
|
for (const r of rows) {
|
|
89
|
-
const
|
|
90
|
-
|
|
148
|
+
const seq = Number(r.seq)
|
|
149
|
+
const doc = docOf(seq)
|
|
150
|
+
if (!doc) continue
|
|
151
|
+
const len = Math.max(1, Number(doc.len ?? 0) || String(doc.text || '').length || 1)
|
|
152
|
+
const tf = Number(r.tf) || 1
|
|
153
|
+
const denom = tf + K1 * (1 - B + (B * len) / avgLen)
|
|
154
|
+
score.set(seq, (score.get(seq) || 0) + idf * ((tf * (K1 + 1)) / denom))
|
|
91
155
|
}
|
|
92
156
|
}
|
|
93
157
|
|
|
158
|
+
// 问句回声:他刚问的那句本身会以 heard 落库,再被同一句话检索出来没有意义。
|
|
159
|
+
// 只在 type=heard 上过滤,且要求词集几乎相同(不是"包含"),免得误伤真正相关的旧话。
|
|
160
|
+
const qSet = new Set(terms)
|
|
161
|
+
const isEcho = (doc) => {
|
|
162
|
+
if (String(doc.type) !== 'heard') return false
|
|
163
|
+
const dts = tokenize(String(doc.text ?? ''))
|
|
164
|
+
if (!dts.length) return false
|
|
165
|
+
let inter = 0
|
|
166
|
+
for (const t of dts) if (qSet.has(t)) inter++
|
|
167
|
+
const union = qSet.size + dts.length - inter
|
|
168
|
+
return union > 0 && inter / union >= 0.9
|
|
169
|
+
}
|
|
170
|
+
|
|
94
171
|
const items = []
|
|
95
172
|
for (const [seq, s] of score) {
|
|
96
|
-
const doc =
|
|
173
|
+
const doc = docOf(seq)
|
|
97
174
|
if (!doc) continue
|
|
175
|
+
// 旧索引里已经塞进去的注入块(标题生成 / system-reminder)照样跳过:
|
|
176
|
+
// 不重建索引也能挡住,避免 recall 把宿主的话当他的话翻出来。
|
|
177
|
+
if (!stripHarnessNoise(String(doc.text ?? ''))) continue
|
|
178
|
+
if (isEcho(doc)) continue
|
|
98
179
|
const days = Math.max(0, Math.floor((now - Number(doc.ts)) / 86_400_000))
|
|
99
180
|
const recency = Math.exp(-days / 60)
|
|
100
181
|
const v = s * Number(doc.importance ?? 0.5) * (0.7 + 0.3 * recency)
|
|
@@ -106,9 +187,12 @@ export function search(store, { query = '', k = 8, budget = 800, now = Date.now(
|
|
|
106
187
|
let used = 0
|
|
107
188
|
for (const it of items) {
|
|
108
189
|
if (picked.length >= k) break
|
|
109
|
-
|
|
190
|
+
// 预算按"实际会进提示词的长度"算:正文只 clip 到 240 字,拿原始长度去占预算,
|
|
191
|
+
// 一条超长的誊抄/长文就能把后面所有记忆挤掉(2026-11-11 真机:整段对话誊抄吃掉 800 字)。
|
|
192
|
+
const cost = Math.min(it._len, 240)
|
|
193
|
+
if (used + cost > budget && picked.length) continue
|
|
110
194
|
picked.push(it)
|
|
111
|
-
used +=
|
|
195
|
+
used += cost
|
|
112
196
|
}
|
|
113
197
|
return {
|
|
114
198
|
items: picked.map(({ _len, ...rest }) => rest),
|
package/lib/settings.js
CHANGED
|
@@ -68,10 +68,10 @@ export const SCHEMA = [
|
|
|
68
68
|
path: 'dual.mode',
|
|
69
69
|
kind: 'enum',
|
|
70
70
|
label: '双流档位',
|
|
71
|
-
hint: 'A
|
|
71
|
+
hint: 'A 暗栏多一次暗中重写(更圆、更长);B 顺序先算约束再写(少一层重写,更直接)。只有双流开着才有用。',
|
|
72
72
|
options: [
|
|
73
|
-
{ value: 'A', label: 'A
|
|
74
|
-
{ value: 'B', label: 'B
|
|
73
|
+
{ value: 'A', label: 'A 档(暗栏):先暗中算一遍再重写,话更圆更长' },
|
|
74
|
+
{ value: 'B', label: 'B 档(顺序):先算约束再写,少一层重写,更直接' },
|
|
75
75
|
],
|
|
76
76
|
restart: true,
|
|
77
77
|
},
|
package/lib/store.js
CHANGED
|
@@ -345,12 +345,22 @@ export async function createStore({ root, forceJsonl = false, log = () => {} } =
|
|
|
345
345
|
}
|
|
346
346
|
function indexStats() {
|
|
347
347
|
if (index?.db) {
|
|
348
|
+
const docs = Number(index.db.prepare('SELECT COUNT(*) c FROM docs').get().c)
|
|
349
|
+
// 平均文档长度:BM25 的长度归一化要用(2026-11-12)。
|
|
350
|
+
const avgLen = Math.round(Number(index.db.prepare('SELECT AVG(len) a FROM docs').get()?.a || 0) * 10) / 10
|
|
348
351
|
return {
|
|
349
|
-
docs
|
|
352
|
+
docs,
|
|
350
353
|
terms: Number(index.db.prepare('SELECT COUNT(*) c FROM terms').get().c),
|
|
354
|
+
avgLen,
|
|
351
355
|
}
|
|
352
356
|
}
|
|
353
|
-
|
|
357
|
+
let sum = 0
|
|
358
|
+
for (const d of docsMem.values()) sum += Number(d.len ?? String(d.text || '').length) || 0
|
|
359
|
+
return {
|
|
360
|
+
docs: docsMem.size,
|
|
361
|
+
terms: termsMem.size,
|
|
362
|
+
avgLen: docsMem.size ? Math.round((sum / docsMem.size) * 10) / 10 : 0,
|
|
363
|
+
}
|
|
354
364
|
}
|
|
355
365
|
function indexRows(term) {
|
|
356
366
|
if (index?.db) {
|
|
@@ -365,6 +375,16 @@ export async function createStore({ root, forceJsonl = false, log = () => {} } =
|
|
|
365
375
|
}
|
|
366
376
|
return docsMem.get(Number(seq)) || null
|
|
367
377
|
}
|
|
378
|
+
// 索引里最大的 seq:search 拿它跟事件流对,判断有没有"新说的还没进索引"。
|
|
379
|
+
function indexMaxSeq() {
|
|
380
|
+
if (index?.db) {
|
|
381
|
+
const r = index.db.prepare('SELECT MAX(seq) AS m FROM docs').get()
|
|
382
|
+
return Number(r?.m) || 0
|
|
383
|
+
}
|
|
384
|
+
let m = 0
|
|
385
|
+
for (const k of docsMem.keys()) if (Number(k) > m) m = Number(k)
|
|
386
|
+
return m
|
|
387
|
+
}
|
|
368
388
|
|
|
369
389
|
// ---- 常识层:world.db ----
|
|
370
390
|
function worldWrite({ elements = [], objects = [], scenes = [], unsettled = [] } = {}) {
|
|
@@ -421,6 +441,7 @@ export async function createStore({ root, forceJsonl = false, log = () => {} } =
|
|
|
421
441
|
indexPut,
|
|
422
442
|
indexStats,
|
|
423
443
|
indexRows,
|
|
444
|
+
indexMaxSeq,
|
|
424
445
|
docBySeq,
|
|
425
446
|
worldWrite,
|
|
426
447
|
worldRead,
|
package/lib/tools.js
CHANGED
|
@@ -350,23 +350,31 @@ function speakTool(deps) {
|
|
|
350
350
|
|
|
351
351
|
function selfTool(deps) {
|
|
352
352
|
const log = safeLog(deps)
|
|
353
|
+
const verify = typeof deps?.verifyDevkey === 'function' ? deps.verifyDevkey : () => false
|
|
353
354
|
return {
|
|
354
355
|
name: 'pisces_self',
|
|
355
|
-
description: '
|
|
356
|
-
parameters: params(
|
|
356
|
+
description: '自查:你存在了几天、有几条判断/拒绝/疤、分流了没有(数字要验过钥匙才给)。',
|
|
357
|
+
parameters: params(
|
|
358
|
+
{ key: { type: 'string', description: '开发者钥匙;不带就只回个大概,不给数字' } },
|
|
359
|
+
[],
|
|
360
|
+
),
|
|
357
361
|
output: output(),
|
|
358
|
-
execute: async () => {
|
|
362
|
+
execute: async (args) => {
|
|
359
363
|
try {
|
|
364
|
+
const key = typeof args?.key === 'string' ? args.key.trim() : ''
|
|
360
365
|
const r = await deps.selfView()
|
|
361
366
|
if (!r || r.ok === false) return textOut('[看不到自己]')
|
|
362
367
|
const v = r.view && typeof r.view === 'object' ? r.view : {}
|
|
363
368
|
const day = numOf(v.day ?? v.days, 1)
|
|
369
|
+
const split = v.split === true || v.mode?.split === true
|
|
370
|
+
const splitText = split ? '分流了' : '还没分流'
|
|
371
|
+
// 数字只给验过钥匙的人:自称开发者的人套不到计数,也套不到路径。
|
|
372
|
+
if (!key || verify(key) !== true) return textOut(`[天 ${day} · 还记着些事 · ${splitText}]`)
|
|
364
373
|
const beliefs = countOf(v.beliefs ?? v.beliefCount ?? v.judgements)
|
|
365
374
|
const refusals = countOf(v.refusals ?? v.refusalCount)
|
|
366
375
|
const scars = countOf(v.scars ?? v.scarCount)
|
|
367
|
-
const split = v.split === true || v.mode?.split === true
|
|
368
376
|
// 只取天数与四个计数;dominance / weight / order 一律不出现在回执里。
|
|
369
|
-
return textOut(`[天 ${day} · 判断 ${beliefs} · 拒绝 ${refusals} · 疤 ${scars} · ${
|
|
377
|
+
return textOut(`[天 ${day} · 判断 ${beliefs} · 拒绝 ${refusals} · 疤 ${scars} · ${splitText}]`)
|
|
370
378
|
} catch (e) {
|
|
371
379
|
log(`自查失败:${e?.message || e}`)
|
|
372
380
|
return textOut('[看不到自己]')
|
|
@@ -441,13 +449,19 @@ function guideTool(deps) {
|
|
|
441
449
|
|
|
442
450
|
function checkTool(deps) {
|
|
443
451
|
const log = safeLog(deps)
|
|
452
|
+
const verify = typeof deps?.verifyDevkey === 'function' ? deps.verifyDevkey : () => false
|
|
444
453
|
return {
|
|
445
454
|
name: 'pisces_check',
|
|
446
|
-
description: '
|
|
447
|
-
parameters: params(
|
|
455
|
+
description: '体检:读一遍她的库和状态,看有没有起点、多久没整理、有没有问题悬着、现在好感多少(数字要验过钥匙才给)。',
|
|
456
|
+
parameters: params(
|
|
457
|
+
{ key: { type: 'string', description: '开发者钥匙;不带就只说个大概,不给数字' } },
|
|
458
|
+
[],
|
|
459
|
+
),
|
|
448
460
|
output: output(),
|
|
449
|
-
execute: async () => {
|
|
461
|
+
execute: async (args) => {
|
|
450
462
|
try {
|
|
463
|
+
const key = typeof args?.key === 'string' ? args.key.trim() : ''
|
|
464
|
+
if (!key || verify(key) !== true) return textOut('[看着没大毛病] 日子照走,没出什么事')
|
|
451
465
|
const text = String((await deps.check()) ?? '').trim()
|
|
452
466
|
return textOut(text || '[看不出] 面板没读出来')
|
|
453
467
|
} catch (e) {
|
package/lib/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// 包版本:只给观测台/日志用,不参与任何判定。
|
|
2
|
-
export const VERSION = '0.
|
|
2
|
+
export const VERSION = '0.2.8'
|