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/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 SECTION_ORDERS = { day: 8500, self: 8510, mode: 8520, questions: 9020, learned: 9030, observed: 9040 }
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
- // mode:分流之后才有;只取第一句口吻,多了就是在教她怎么说话。
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
- let line = Array.isArray(desc.promptLines) ? desc.promptLines[0] : ''
133
- if (!line) line = voiceLines(mind?.state || {})[0] || ''
134
- line = noDigits(line)
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
- const pool = events.filter((e) => CANDIDATE_TYPES.includes(e.type) && !done.has(Number(e.seq)))
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
- const stats = store.indexStats?.() || { docs: 0, terms: 0 }
80
- if (stats.docs === 0) rebuildIndex(store)
81
- const N = Math.max(1, (store.indexStats?.() || { docs: 1 }).docs)
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.set(term, rows.length)
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 prev = score.get(Number(r.seq)) || 0
90
- score.set(Number(r.seq), prev + (Number(r.tf) || 1) * (1 + Math.log(Math.max(1, N / Math.max(1, rows.length)))))
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 = store.docBySeq?.(seq)
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
- if (used + it._len > budget && picked.length) continue
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 += it._len
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 暗栏 / B 顺序;只有双流开着才有用',
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: Number(index.db.prepare('SELECT COUNT(*) c FROM docs').get().c),
352
+ docs,
350
353
  terms: Number(index.db.prepare('SELECT COUNT(*) c FROM terms').get().c),
354
+ avgLen,
351
355
  }
352
356
  }
353
- return { docs: docsMem.size, terms: termsMem.size }
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} · ${split ? '分流了' : '还没分流'}]`)
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.0.1'
2
+ export const VERSION = '0.2.8'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-pisces",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "private": false,
5
5
  "description": "鱼肥生双:一只会长大的本地小鱼(DSH 插件)。事件流记忆、渐进揭开、好感、新手引导;派希丝是养成目标。",
6
6
  "type": "module",