dsh-pisces 0.1.3 → 0.2.0
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/README.md +8 -4
- package/client.js +9 -18
- package/lib/attach.js +82 -1
- package/lib/console.js +2 -0
- package/lib/nightly.js +6 -2
- package/lib/split.js +56 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,9 +35,13 @@
|
|
|
35
35
|
|
|
36
36
|
这些是**思路借鉴**,不是代码依赖;每个项目的代码各自独立,本仓库没有复制它们的实现。
|
|
37
37
|
|
|
38
|
-
- **dsh-fish
|
|
39
|
-
- **
|
|
40
|
-
-
|
|
41
|
-
-
|
|
38
|
+
- **dsh-fish** —— 事件流 + 纯函数派生 + 面板形态,本项目的骨架从它长出来。npm:<https://www.npmjs.com/package/@ssamurias/dsh-fish>
|
|
39
|
+
- **dsh-fish-memory** —— 记忆鲜活度与半衰期、中文检索。
|
|
40
|
+
- **Character Card V2** —— `export.js` 的 CCv2 导出格式。
|
|
41
|
+
- **dsh-companion** —— 好感分档、关系产品化的样本。
|
|
42
|
+
- **Generative Agents** —— memory stream + 定期 reflection 的启发。
|
|
43
|
+
- **DeepSeek Harness** —— 宿主、插件体系、slots / systemPrompt / tools。
|
|
44
|
+
- **双鱼座性格的公开资料**(经 Exa 检索整理)—— 她的性格起点。
|
|
45
|
+
- **所有养过鱼的人** —— 社区里"把 AI 当室友养"的尝试。
|
|
42
46
|
|
|
43
47
|
如果你觉得某个想法该被写在这里,发反馈告诉我们(反馈页有加群方式)。
|
package/client.js
CHANGED
|
@@ -894,24 +894,15 @@ window.__ModuleLoader__.load({
|
|
|
894
894
|
// -------------------------------------------------------------------------
|
|
895
895
|
|
|
896
896
|
const THANKS = [
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
},
|
|
902
|
-
{
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
},
|
|
906
|
-
{
|
|
907
|
-
name: '双鱼座性格的公开资料',
|
|
908
|
-
what: '(经 Exa 检索整理)派希丝"温和、先退一步、心里两条线"的起点。',
|
|
909
|
-
href: 'https://exa.ai',
|
|
910
|
-
},
|
|
911
|
-
{
|
|
912
|
-
name: '所有养过鱼的人',
|
|
913
|
-
what: '这条鱼最初的样子来自社区里那些"把 AI 当室友养"的尝试。',
|
|
914
|
-
},
|
|
897
|
+
// 2026-11-10 定稿:八条;用户自己的插件/前作不鸣谢,dsh-fish 排第一。
|
|
898
|
+
{ name: 'dsh-fish', what: '事件流 + 纯函数派生 + 面板形态,本项目的骨架从它长出来。', href: 'https://www.npmjs.com/package/@ssamurias/dsh-fish' },
|
|
899
|
+
{ name: 'dsh-fish-memory', what: '记忆鲜活度与半衰期、中文检索。' },
|
|
900
|
+
{ name: 'Character Card V2', what: '`export.js` 的 CCv2 导出格式。' },
|
|
901
|
+
{ name: 'dsh-companion', what: '好感分档、关系产品化的样本。' },
|
|
902
|
+
{ name: 'Generative Agents', what: 'memory stream + 定期 reflection 的启发。' },
|
|
903
|
+
{ name: 'DeepSeek Harness', what: '宿主、插件体系、slots / systemPrompt / tools。' },
|
|
904
|
+
{ name: '双鱼座性格的公开资料', what: '(经 Exa 检索整理)她的性格起点。' },
|
|
905
|
+
{ name: '所有养过鱼的人', what: '社区里"把 AI 当室友养"的尝试。' },
|
|
915
906
|
]
|
|
916
907
|
|
|
917
908
|
function ThanksTab() {
|
package/lib/attach.js
CHANGED
|
@@ -15,11 +15,51 @@
|
|
|
15
15
|
// 记进她的库。写库是副作用:失败只吞掉,绝不拖慢/改变流。
|
|
16
16
|
import { buildSections as defaultBuildSections } from './persona.js'
|
|
17
17
|
import { steerLines } from './guard.js'
|
|
18
|
+
import { search as defaultSearchMemory } from './search.js'
|
|
19
|
+
import { clip } from './util.js'
|
|
18
20
|
import { MAX_TEXT } from './schema.js'
|
|
19
21
|
|
|
20
22
|
// getStore() 返回 null(ready 还没到)时的重试间隔。定时器 unref,不拦进程退出。
|
|
21
23
|
const STORE_RETRY_MS = 250
|
|
22
24
|
|
|
25
|
+
// 「此刻想起」:每轮按他刚说的话检索她的记忆,把最相关的几条只塞进这一次请求的
|
|
26
|
+
// system(不写事件、不落库、不进对话)。检索失败/没结果就当没这回事,绝不报错。
|
|
27
|
+
// 上限是保守值:单条 120 字、整段 800 字(约 800 tokens),超了截断。
|
|
28
|
+
export const RECALL_HEADER = '[此刻想起]'
|
|
29
|
+
export const RECALL_K = 5
|
|
30
|
+
export const RECALL_ITEM_CHARS = 120
|
|
31
|
+
export const RECALL_MAX_CHARS = 800
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 把检索结果折成这一次的注入块;没有可用条目返回空串。
|
|
35
|
+
* 格式固定:第一行标题,之后每条一行 `- 原文`。单条 ≤120 字、整段 ≤800 字,
|
|
36
|
+
* 超了截断(不会抛错);不写"检索到/相关度"这类词,也不带 id/天数。
|
|
37
|
+
*/
|
|
38
|
+
export function recallBlockFrom(items, {
|
|
39
|
+
k = RECALL_K,
|
|
40
|
+
max = RECALL_MAX_CHARS,
|
|
41
|
+
itemChars = RECALL_ITEM_CHARS,
|
|
42
|
+
} = {}) {
|
|
43
|
+
const list = Array.isArray(items) ? items : []
|
|
44
|
+
const lines = []
|
|
45
|
+
let used = RECALL_HEADER.length
|
|
46
|
+
let picked = 0
|
|
47
|
+
for (const it of list) {
|
|
48
|
+
if (picked >= k || used >= max) break
|
|
49
|
+
const text = clip(it?.text, itemChars)
|
|
50
|
+
if (!text) continue
|
|
51
|
+
// 留一个换行的位置;剩得太少就不塞了(半行没有意义)。
|
|
52
|
+
const room = max - used - 1
|
|
53
|
+
if (room < 4) break
|
|
54
|
+
const line = `- ${text}`
|
|
55
|
+
const body = line.length > room ? line.slice(0, room) : line
|
|
56
|
+
lines.push(body)
|
|
57
|
+
used += 1 + body.length
|
|
58
|
+
picked++
|
|
59
|
+
}
|
|
60
|
+
return lines.length ? [RECALL_HEADER, ...lines].join('\n') : ''
|
|
61
|
+
}
|
|
62
|
+
|
|
23
63
|
// 开发者钥匙命中时追加到这一次请求 system 的那段话。
|
|
24
64
|
// 只在这里出现;钥匙本身一个字都不进来,也不写事件、不写日志。
|
|
25
65
|
export const DEV_PROMPT =
|
|
@@ -292,6 +332,11 @@ function resolveSystemPrompt(agent) {
|
|
|
292
332
|
* waterfall,宿主每个会话的生成都会经过同一个监听器;只有它返回真才 recordHeard /
|
|
293
333
|
* wrapUtterance。返回假时这条流原样 next():不改 options、不记事件、不包流。
|
|
294
334
|
* 不传 = 默认不记(安全优先),并 log 一行「聊天落库:没有会话判据,跳过」。
|
|
335
|
+
* searchMemory 可选:`(store, {query,k,budget}) => {items:[{text}]}`,默认用 lib/search.js
|
|
336
|
+
* 的 search。只在 isHerSession 为真、库就绪、且清洗后的他有话时说:取 top 5、单条 ≤120 字、
|
|
337
|
+
* 整段 ≤800 字,折成 `[此刻想起]` 追加到这一次请求的 system 末尾(排在 guard/dev 之后)。
|
|
338
|
+
* 只进 system,不写事件、不落库、不进对话;检索失败/没结果 = 不注入、不报错、不影响流。
|
|
339
|
+
* 注入次数进 state.recallInjected(只给 /bridge 看次数,不留正文)。
|
|
295
340
|
* @returns {((() => void) & { info?: object, attachInfo?: object })}
|
|
296
341
|
* dispose():摘掉 agent/created 与 llm/stream 监听,幂等。
|
|
297
342
|
* dispose.info / dispose.attachInfo:只读快照(enabled/storeReady/agentsSeen/sectionsAttached/stream…)。
|
|
@@ -303,6 +348,7 @@ export function attachToAgents(ctx, {
|
|
|
303
348
|
isDevkey = null,
|
|
304
349
|
shouldAttach = null,
|
|
305
350
|
isHerSession = null,
|
|
351
|
+
searchMemory = defaultSearchMemory,
|
|
306
352
|
config = {},
|
|
307
353
|
log = () => {},
|
|
308
354
|
now = () => Date.now(),
|
|
@@ -321,6 +367,8 @@ export function attachToAgents(ctx, {
|
|
|
321
367
|
// 只给 /bridge 看的次数:命中几次,不给任何正文/原因。
|
|
322
368
|
guardHits: 0,
|
|
323
369
|
devHits: 0,
|
|
370
|
+
// 「此刻想起」注入过几次(只给 /bridge 看次数,不留正文)。
|
|
371
|
+
recallInjected: 0,
|
|
324
372
|
// 聊天落库的可观测计数(只给次数/形状,不给正文):
|
|
325
373
|
// streamsWrapped 包过的流数(只数她的流);streamsSkipped 判据判掉、原样放行的流数;
|
|
326
374
|
// chunksSeen 看到的 chunk 总数;textParts 拼出文本的 chunk 数;
|
|
@@ -388,6 +436,7 @@ export function attachToAgents(ctx, {
|
|
|
388
436
|
agentsNotHer: state.agentsNotHer,
|
|
389
437
|
guardHits: state.guardHits,
|
|
390
438
|
devHits: state.devHits,
|
|
439
|
+
recallInjected: state.recallInjected,
|
|
391
440
|
// 平铺给 dispose.info,另外给一份 streamDebug 供 /bridge 白名单整块取。
|
|
392
441
|
streamsWrapped: state.streamsWrapped,
|
|
393
442
|
streamsSkipped: state.streamsSkipped,
|
|
@@ -662,6 +711,32 @@ export function attachToAgents(ctx, {
|
|
|
662
711
|
if (appendChat('heard', 'owner', body)) entry.heard = body
|
|
663
712
|
}
|
|
664
713
|
|
|
714
|
+
/**
|
|
715
|
+
* 按他刚说的话检索她的记忆,折成这一次的 `[此刻想起]` 块。
|
|
716
|
+
* 只读、只动这一次请求的 options:库没就绪 / 没 query / 检索抛错 / 没结果 → 原样返回。
|
|
717
|
+
* 不写事件、不落库、不进对话;注入次数只记 state.recallInjected。
|
|
718
|
+
*/
|
|
719
|
+
function injectRecall(baseOptions, query) {
|
|
720
|
+
if (!query || !store || !state.storeReady) return baseOptions
|
|
721
|
+
if (typeof searchMemory !== 'function') return baseOptions
|
|
722
|
+
let found = null
|
|
723
|
+
try {
|
|
724
|
+
found = searchMemory(store, { query, k: RECALL_K, budget: RECALL_MAX_CHARS })
|
|
725
|
+
} catch {
|
|
726
|
+
// 检索失败:不注入、不报错、不影响流。
|
|
727
|
+
return baseOptions
|
|
728
|
+
}
|
|
729
|
+
let block = ''
|
|
730
|
+
try {
|
|
731
|
+
block = recallBlockFrom(found?.items)
|
|
732
|
+
} catch {
|
|
733
|
+
return baseOptions
|
|
734
|
+
}
|
|
735
|
+
if (!block) return baseOptions
|
|
736
|
+
state.recallInjected++
|
|
737
|
+
return withSystemLine(baseOptions, block)
|
|
738
|
+
}
|
|
739
|
+
|
|
665
740
|
/**
|
|
666
741
|
* 包住宿主拿到的流,把文本增量攒成一句,流结束(finally)时写一条 utterance。
|
|
667
742
|
* - 只有 AsyncIterable 才包;其它返回值原样交回,不改变 next() 的语义;
|
|
@@ -769,10 +844,12 @@ export function attachToAgents(ctx, {
|
|
|
769
844
|
const userText = lastUserText(options)
|
|
770
845
|
// 聊天落库:只有 her 才记。会话 key 优先取 options;宿主没给就退回第一条
|
|
771
846
|
// 挂上的 agent 的 sessionId/cwd。
|
|
847
|
+
let heardBody = ''
|
|
772
848
|
if (her) {
|
|
773
849
|
sessionKey = sessionKeyOf(options) || agentSessionKey
|
|
774
850
|
// 往前找最多 4 条 user 消息:最后一条常常是宿主注入块,他的话在它前面。
|
|
775
|
-
|
|
851
|
+
heardBody = recentUserText(options)
|
|
852
|
+
recordHeard(heardBody, sessionKey)
|
|
776
853
|
}
|
|
777
854
|
if (typeof scanGuard === 'function') {
|
|
778
855
|
let verdict = null
|
|
@@ -806,6 +883,10 @@ export function attachToAgents(ctx, {
|
|
|
806
883
|
nextOptions = withSystemLine(nextOptions, DEV_PROMPT)
|
|
807
884
|
}
|
|
808
885
|
}
|
|
886
|
+
|
|
887
|
+
// 此刻想起:排在 guard、dev 之后(先规矩,再身份,最后是记忆)。
|
|
888
|
+
// 检索用的是上面清洗过的他那句话;失败/没结果都不动 system。
|
|
889
|
+
if (her) nextOptions = injectRecall(nextOptions, heardBody)
|
|
809
890
|
} catch (err) {
|
|
810
891
|
log(`提示词桥:llm/stream 观察失败(${errText(err)})`)
|
|
811
892
|
}
|
package/lib/console.js
CHANGED
|
@@ -1097,6 +1097,8 @@ function bridgeRoute(deps) {
|
|
|
1097
1097
|
agentsNotHer: pick('agentsNotHer'),
|
|
1098
1098
|
guardHits: pick('guardHits'),
|
|
1099
1099
|
devHits: pick('devHits'),
|
|
1100
|
+
// 「此刻想起」注入过几次;只给次数,不给检索内容。
|
|
1101
|
+
recallInjected: pick('recallInjected'),
|
|
1100
1102
|
stream: pick('stream'),
|
|
1101
1103
|
// 聊天落库诊断:只给次数/形状,不给正文。
|
|
1102
1104
|
// { streamsWrapped, chunksSeen, textParts, utterancesWritten, lastChunkShape }
|
package/lib/nightly.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// - 任何一步抛错都只记进 `steps.x.error`,后面的照跑;跑完(非 dryRun)照常记"今天整理过"。
|
|
8
8
|
// - 状态记在旧库同一处:index.db 的 modes 表,`nightly.last`(毫秒)和 `nightly.<YYYY-MM-DD>`('1')。
|
|
9
9
|
import { attributePending } from './reflect.js'
|
|
10
|
-
import { createMind } from './split.js'
|
|
10
|
+
import { createMind, userDomOf } from './split.js'
|
|
11
11
|
import { syncQuestions } from './questions.js'
|
|
12
12
|
import { dream } from './dream.js'
|
|
13
13
|
import { verifyDreams } from './verify.js'
|
|
@@ -147,9 +147,13 @@ export function createNightly({ store, log = () => {}, config = {}, model = null
|
|
|
147
147
|
steps.mind = { ok: true, dryRun: true, folded: rows.length, ...m.describe() }
|
|
148
148
|
} else {
|
|
149
149
|
const m = mind && typeof mind.fold === 'function' ? mind : createMind({ store, log, config: cfg })
|
|
150
|
+
// 归因表里可能已经有几十条历史:按 seq 取"到这一条为止"的最近 20 条,算出 userDom 再传进去。
|
|
151
|
+
// 不指望缓存里的 recentLabels(老缓存没有),否则升级后要等 20 条新归因才判得出主导度。
|
|
152
|
+
const history = [...(store.attribAll?.() || [])].sort((a, b) => Number(a.seq) - Number(b.seq))
|
|
150
153
|
let folded = 0
|
|
151
154
|
for (const row of rows) {
|
|
152
|
-
|
|
155
|
+
const upto = history.filter((r) => Number(r.seq) <= Number(row.seq))
|
|
156
|
+
m.fold(row, { now: at, userDom: userDomOf(upto) })
|
|
153
157
|
folded += 1
|
|
154
158
|
}
|
|
155
159
|
steps.mind = { ok: true, folded, ...m.describe() }
|
package/lib/split.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// 分流(M2):开局一条线;够了才分;分完才谈谁主导;主导会变,变要带证据。
|
|
2
2
|
// 状态形状沿用旧实现的 modes.state:
|
|
3
3
|
// {version,w:{d,i},n,counts:{d,i},dominance,split:{at,ev,n,share,tension[]},order,shifts[],
|
|
4
|
-
// userDom:
|
|
4
|
+
// userDom:number|null,recentLabels[],lastDivergenceN,mimicryRisk,lastIds[],updatedAt}
|
|
5
|
+
// userDom 是"最近 N 条归因里 d 占已判归因的比例",没得判时是 null(不是 0)。
|
|
5
6
|
// 这是可重建的小状态,不是历史:丢了可以从事件重放。
|
|
6
7
|
import { clip } from './util.js'
|
|
7
8
|
|
|
@@ -15,7 +16,8 @@ export const ZERO_STATE = () => ({
|
|
|
15
16
|
split: null,
|
|
16
17
|
order: 'E=R',
|
|
17
18
|
shifts: [],
|
|
18
|
-
userDom:
|
|
19
|
+
userDom: null,
|
|
20
|
+
recentLabels: [],
|
|
19
21
|
lastDivergenceN: null,
|
|
20
22
|
mimicryRisk: false,
|
|
21
23
|
lastIds: [],
|
|
@@ -33,6 +35,8 @@ export const DEFAULT_MIND = {
|
|
|
33
35
|
shiftCooldownDays: 30,
|
|
34
36
|
shiftRefs: 2,
|
|
35
37
|
mimicryN: 20,
|
|
38
|
+
// 用户主导度离 0.5 小于这个值,才算"看不出他偏哪边"——模仿风险的前提。
|
|
39
|
+
mimicryBand: 0.15,
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v))
|
|
@@ -50,6 +54,24 @@ export function orderOf(band) {
|
|
|
50
54
|
return 'E=R'
|
|
51
55
|
}
|
|
52
56
|
|
|
57
|
+
/**
|
|
58
|
+
* 用户主导度:最近 n 条归因里 d 占已判归因(d + i)的比例。
|
|
59
|
+
* - 窗口按"最近 n 条归因"取,null(纯描述)占窗口位置但不进分母;
|
|
60
|
+
* - 窗口里一条 d/i 都没有 → null(还没得判,不是 0)。
|
|
61
|
+
* @param {Array<{label?:string}|string>} rows 归因行(或裸 label)
|
|
62
|
+
* @param {{n?:number}} [opts]
|
|
63
|
+
* @returns {number|null} 0–1
|
|
64
|
+
*/
|
|
65
|
+
export function userDomOf(rows = [], { n = DEFAULT_MIND.mimicryN } = {}) {
|
|
66
|
+
const limit = Math.max(1, Number(n) || DEFAULT_MIND.mimicryN)
|
|
67
|
+
const window = (Array.isArray(rows) ? rows : [])
|
|
68
|
+
.map((r) => (typeof r === 'string' ? r : r?.label))
|
|
69
|
+
.slice(-limit)
|
|
70
|
+
const judged = window.filter((l) => l === 'd' || l === 'i')
|
|
71
|
+
if (!judged.length) return null
|
|
72
|
+
return round(judged.filter((l) => l === 'd').length / judged.length, 3)
|
|
73
|
+
}
|
|
74
|
+
|
|
53
75
|
/** 口吻:只给说法,不给数字。 */
|
|
54
76
|
export function voiceLines(state) {
|
|
55
77
|
const lines = []
|
|
@@ -75,7 +97,19 @@ export function createMind({ store, log = () => {}, config = {}, startDominance
|
|
|
75
97
|
const raw = store?.modeGet?.(KEY)
|
|
76
98
|
if (raw) {
|
|
77
99
|
const j = JSON.parse(raw)
|
|
78
|
-
if (j && Number(j.version) === 1)
|
|
100
|
+
if (j && Number(j.version) === 1) {
|
|
101
|
+
state = {
|
|
102
|
+
...ZERO_STATE(),
|
|
103
|
+
...j,
|
|
104
|
+
w: { ...j.w },
|
|
105
|
+
counts: { ...j.counts },
|
|
106
|
+
// 老缓存里 userDom 是没喂过数据的 {d,i}:不认,归为 null,等下一次 fold 重算。
|
|
107
|
+
userDom: typeof j.userDom === 'number' && Number.isFinite(j.userDom) ? round(clamp(j.userDom, 0, 1), 3) : null,
|
|
108
|
+
recentLabels: Array.isArray(j.recentLabels)
|
|
109
|
+
? j.recentLabels.map(String).filter((l) => l === 'd' || l === 'i' || l === 'null').slice(-cfg.mimicryN)
|
|
110
|
+
: [],
|
|
111
|
+
}
|
|
112
|
+
}
|
|
79
113
|
}
|
|
80
114
|
} catch (e) {
|
|
81
115
|
log?.('读分流缓存失败,从头来:', e?.message || e)
|
|
@@ -91,9 +125,10 @@ export function createMind({ store, log = () => {}, config = {}, startDominance
|
|
|
91
125
|
|
|
92
126
|
/**
|
|
93
127
|
* 折一条归因。row: {seq, day, label:'d'|'i'|'null', conf, why, by, model, at, id?, text?}
|
|
128
|
+
* opts.userDom:调用方已经算好的"最近 N 条归因的 d 占比"(number|null);不给就用内部窗口。
|
|
94
129
|
* 返回 {changed, split?:{...}, bandChange?:{from,to}}。
|
|
95
130
|
*/
|
|
96
|
-
function fold(row = {}, { now = Date.now(), refs = [] } = {}) {
|
|
131
|
+
function fold(row = {}, { now = Date.now(), refs = [], userDom: givenUserDom } = {}) {
|
|
97
132
|
const label = row.label === 'd' || row.label === 'i' ? row.label : null
|
|
98
133
|
const before = { band: bandOf(state.dominance), order: state.order }
|
|
99
134
|
state.n += 1
|
|
@@ -112,6 +147,17 @@ export function createMind({ store, log = () => {}, config = {}, startDominance
|
|
|
112
147
|
state.lastIds = state.lastIds.slice(-4)
|
|
113
148
|
}
|
|
114
149
|
|
|
150
|
+
// 用户主导度:最近 N 条归因里 d 的占比。null 归因占窗口位置,但不进分母。
|
|
151
|
+
// 老缓存没有 recentLabels,就从这一条开始重新攒——宁可先判"看不出来",也不要拿全局比例冒充。
|
|
152
|
+
if (!Array.isArray(state.recentLabels)) state.recentLabels = []
|
|
153
|
+
state.recentLabels.push(label || 'null')
|
|
154
|
+
state.recentLabels = state.recentLabels.slice(-cfg.mimicryN)
|
|
155
|
+
state.userDom = givenUserDom === undefined
|
|
156
|
+
? userDomOf(state.recentLabels, { n: cfg.mimicryN })
|
|
157
|
+
: (givenUserDom == null || !Number.isFinite(Number(givenUserDom))
|
|
158
|
+
? null
|
|
159
|
+
: round(clamp(Number(givenUserDom), 0, 1), 3))
|
|
160
|
+
|
|
115
161
|
// 分流:四条都满足才写,写一次就不回头。
|
|
116
162
|
let splitNow = null
|
|
117
163
|
if (!state.split && state.n >= cfg.splitN && state.counts.d >= cfg.sideMin && state.counts.i >= cfg.sideMin) {
|
|
@@ -141,9 +187,12 @@ export function createMind({ store, log = () => {}, config = {}, startDominance
|
|
|
141
187
|
}
|
|
142
188
|
state.order = orderOf(band)
|
|
143
189
|
|
|
144
|
-
//
|
|
190
|
+
// 模仿风险:只有"连续很多条归因都没有分歧证据"、且"看不出他偏哪边"(userDom 在 0.5 附近)才亮。
|
|
191
|
+
// 没有 userDom(还没得判)一律不亮:不拿 null 当 0,否则 i 多的鱼会在第 20 条后恒为 true。
|
|
145
192
|
if (String(row.why || '').includes('divergence')) state.lastDivergenceN = state.n
|
|
146
|
-
|
|
193
|
+
const noDivergence = state.n - (state.lastDivergenceN ?? 0) >= cfg.mimicryN
|
|
194
|
+
const nearHalf = state.userDom != null && Math.abs(state.userDom - 0.5) < cfg.mimicryBand
|
|
195
|
+
state.mimicryRisk = noDivergence && nearHalf
|
|
147
196
|
|
|
148
197
|
save(now)
|
|
149
198
|
return { changed: true, split: splitNow, bandChange }
|
|
@@ -170,6 +219,7 @@ export function createMind({ store, log = () => {}, config = {}, startDominance
|
|
|
170
219
|
splitShare: state.split?.share ?? null,
|
|
171
220
|
tension: state.split?.tension ?? [],
|
|
172
221
|
shifts: state.shifts.length,
|
|
222
|
+
userDom: state.userDom,
|
|
173
223
|
mimicryRisk: state.mimicryRisk,
|
|
174
224
|
promptLines: voiceLines(state),
|
|
175
225
|
updatedAt: state.updatedAt,
|