@ssamurias/dsh-fish 0.9.0 → 0.9.1
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 +35 -4
- package/CHANGELOG.md +46 -1
- package/README.md +11 -6
- package/RELEASE-NOTES-0.9.1.md +296 -0
- package/RELEASE.md +151 -0
- package/lib/ask.js +18 -5
- package/lib/browse.js +1 -0
- package/lib/capture.js +16 -1
- package/lib/client.js +50 -11
- package/lib/comment.js +74 -0
- package/lib/compact.js +45 -1
- package/lib/curiosity.js +6 -2
- package/lib/derive.js +11 -2
- package/lib/desk.js +3 -1
- package/lib/dreamtable.js +13 -2
- package/lib/drive.js +14 -2
- package/lib/freewake.js +4 -1
- package/lib/home.js +87 -22
- package/lib/index.js +1 -1
- package/lib/interest.js +3 -1
- package/lib/look.js +23 -3
- package/lib/observe.js +9 -0
- package/lib/owner.js +5 -2
- package/lib/persona.js +16 -2
- package/lib/proactive.js +67 -12
- package/lib/scheduler.js +99 -27
- package/lib/selfdoc.js +5 -3
- package/lib/selfupdate.js +8 -1
- package/lib/settings.js +5 -0
- package/lib/status.js +31 -3
- package/lib/store.js +14 -2
- package/lib/tenant.js +12 -0
- package/lib/turns.js +5 -1
- package/lib/upgrade.js +4 -1
- package/package.json +2 -1
- package/scripts/fix-legacy-link.mjs +197 -90
package/lib/dreamtable.js
CHANGED
|
@@ -13,10 +13,14 @@
|
|
|
13
13
|
* 三条边界(沿用 dream.js 的四条硬约束):
|
|
14
14
|
* - 只用她自己说过的话(utterance / revise / reunion / narration / refusal),
|
|
15
15
|
* 别人的话、系统记录一律不进;refs 的合法性因此天然满足。
|
|
16
|
+
* 只留证的也不进(isProofOnly):没认领的聊天行不是「写下的」,
|
|
17
|
+
* 想改自己的话不算她的判断(derive.js 同样不算信念)。
|
|
16
18
|
* - 出门/请神那几趟写下的事件不摆(与 writeDream 的拒绝规则同源)。
|
|
17
19
|
* - 只陈述事实,不给台词、不派活:不出现「梦」「记一句」「你可以」。
|
|
18
20
|
*/
|
|
19
21
|
|
|
22
|
+
import { isProofOnly } from './derive.js'
|
|
23
|
+
|
|
20
24
|
/** 一句话要淡多久才值得被摆出来。太新的还热着,太旧的可能她已经不认了。 */
|
|
21
25
|
export const STALE_MIN_DAYS = 7
|
|
22
26
|
export const STALE_MAX_DAYS = 30
|
|
@@ -65,6 +69,7 @@ export function staleLines(events = [], opts = {}) {
|
|
|
65
69
|
const rows = []
|
|
66
70
|
for (const e of events) {
|
|
67
71
|
if (!HER_WORDS.has(e?.type)) continue
|
|
72
|
+
if (isProofOnly(e)) continue
|
|
68
73
|
if (trips.has(String(e?.sessionId ?? ''))) continue
|
|
69
74
|
const t = Date.parse(e?.ts ?? '')
|
|
70
75
|
if (!Number.isFinite(t)) continue
|
|
@@ -107,12 +112,18 @@ export function clipLine(text, max = LINE_CLIP) {
|
|
|
107
112
|
export function renderTable(rows = [], { gapHours = 0 } = {}) {
|
|
108
113
|
const list = (Array.isArray(rows) ? rows : []).filter((r) => r && r.id && String(r.text ?? '').trim())
|
|
109
114
|
if (list.length < TABLE_MIN) return ''
|
|
110
|
-
|
|
115
|
+
// 2026-11-06(issue #42):原来写「这次隔了 N 个小时」,读起来像「她睡了 N 小时」。
|
|
116
|
+
// hoursSinceLastWake 只数 `wake:`,中间她可能一直在跟人说话、开过好几轮。
|
|
117
|
+
// 改成只说它真算的那件事:距上一次醒来多久。
|
|
118
|
+
const head = gapHours >= 1 ? `距你上次醒来,隔了 ${Math.round(gapHours)} 个小时。` : ''
|
|
111
119
|
const lines = list.slice(0, TABLE_MAX).map((r) => {
|
|
112
120
|
const when = shortDay(r.ts)
|
|
113
121
|
return `- 「${clipLine(r.text)}」${when ? ` · ${when}` : ''} · ${r.id}`
|
|
114
122
|
})
|
|
115
|
-
|
|
123
|
+
// 2026-11-06(issue #42):原来后半句写「这些天再没被提起过」。判据其实只是
|
|
124
|
+
// 「之后有没有事件 refs 指回它」——她闲聊时提过一句但没有 refs,就仍然会被说成「没提起过」。
|
|
125
|
+
// 这句是给她读的,不该撒谎,所以把那半句去掉,只陈述摆在那儿这件事。
|
|
126
|
+
return [head, '你写下的几行字还摊在那儿:', '', ...lines]
|
|
116
127
|
.filter((x) => x !== '')
|
|
117
128
|
.join('\n')
|
|
118
129
|
}
|
package/lib/drive.js
CHANGED
|
@@ -95,6 +95,17 @@ function firstAt(events) {
|
|
|
95
95
|
return first
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/** 他最近一次来过(`meta seen:`)的时刻。 */
|
|
99
|
+
function lastSeenLine(events) {
|
|
100
|
+
let last = 0
|
|
101
|
+
for (const e of events) {
|
|
102
|
+
if (e?.type !== 'meta' || !String(e.text ?? '').startsWith('seen:')) continue
|
|
103
|
+
const ts = tsOf(e)
|
|
104
|
+
if (ts > last) last = ts
|
|
105
|
+
}
|
|
106
|
+
return last
|
|
107
|
+
}
|
|
108
|
+
|
|
98
109
|
/** 她最近一次真的开口的时刻。 */
|
|
99
110
|
function lastSpokeAt(events) {
|
|
100
111
|
let last = 0
|
|
@@ -120,7 +131,7 @@ function lastSpokeAt(events) {
|
|
|
120
131
|
* @returns {{value:number, threshold:number, ready:boolean, why:string,
|
|
121
132
|
* hoursSince:number, boosts:number, damp:number}}
|
|
122
133
|
*/
|
|
123
|
-
export function driveOf({ events = [], now = Date.now(), desk = null, stage = 1 } = {}) {
|
|
134
|
+
export function driveOf({ events = [], now = Date.now(), desk = null, stage = 1, seenCountsAsHeard = false } = {}) {
|
|
124
135
|
const base = { value: 0, threshold: DRIVE.threshold, ready: false, why: '', hoursSince: 0, boosts: 0, damp: 1, speed: DRIVE.growthPerHour }
|
|
125
136
|
// 动过就从那次算;一次都没动过,就从她出生那一刻算(不能永远停在 0.00)。
|
|
126
137
|
const last = lastActionAt(events) || firstAt(events)
|
|
@@ -134,7 +145,8 @@ export function driveOf({ events = [], now = Date.now(), desk = null, stage = 1
|
|
|
134
145
|
|
|
135
146
|
// 上次开口没人接:底噪打折(不是惩罚,只是「说了也没人接」这件事本身的重量)。
|
|
136
147
|
const spoke = lastSpokeAt(events)
|
|
137
|
-
|
|
148
|
+
// 对话不进事件流的部署(proactive.idleCountsSeen):他来过就算接了,否则这个折扣永远解不掉
|
|
149
|
+
const heard = Math.max(lastHeardAt(events), seenCountsAsHeard ? lastSeenLine(events) : 0)
|
|
138
150
|
const damp = spoke && heard < spoke ? DRIVE.dampUnanswered : 1
|
|
139
151
|
|
|
140
152
|
const gaps = Math.max(0, Number(desk?.gaps) || 0)
|
package/lib/freewake.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* - table:旧句子要摆出来的那一次(lib/dreamtable.js 的三道门已经判过)。
|
|
14
14
|
* - think:**兜底**(2026-11-05,用户 m02008 拍板)。原来按「每 3 天一次」的日历开,
|
|
15
15
|
* 现在改成:过去 `QUIET_DAYS` 天里她一件自发的事都没有(没有 utterance /
|
|
16
|
-
* narration / dream /
|
|
16
|
+
* narration / dream / 主动开口;聊天回合补写的 uncommitted 行不算),才由日历补一次。劲儿正常工作时不该再出现
|
|
17
17
|
* 「同一天醒两次」,所以这一条只在劲儿长期趴着时生效。
|
|
18
18
|
*
|
|
19
19
|
* 0.9.0 起公开:所有机器都能空醒;想关掉把 config.memory.thinkEveryDays 设 0。
|
|
@@ -45,6 +45,9 @@ export function quietDays(events = [], now = Date.now()) {
|
|
|
45
45
|
continue
|
|
46
46
|
}
|
|
47
47
|
if (!HER_TYPES.includes(e?.type)) continue
|
|
48
|
+
// 聊天回合里的回复不是她自己动的,否则每天有人聊,这条兜底就永远等不到。
|
|
49
|
+
// selfmod 不跳:想改自己也是她自己动过,这里问的不是算不算信念。
|
|
50
|
+
if (Array.isArray(e.flags) && e.flags.includes('uncommitted')) continue
|
|
48
51
|
const ts = tsMs(e)
|
|
49
52
|
if (Number.isFinite(ts) && ts > last) last = ts
|
|
50
53
|
}
|
package/lib/home.js
CHANGED
|
@@ -197,8 +197,16 @@ export async function ensureTalkSession(ctx, opts = {}) {
|
|
|
197
197
|
* 现在先把「归档的那条」和「这个工作区里另外的活会话」分开判断:只要还有一条
|
|
198
198
|
* 没归档的,就认它,而不是让整个家停摆。
|
|
199
199
|
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
200
|
+
* 取最前面那条(宿主建会话时是 `sessionIds: [新id, ...旧的]`,往**前**插,
|
|
201
|
+
* 所以头部才是最新那条),但不做别的假设:拿不到 registry、sessionIds 不是数组、
|
|
202
|
+
* 全是归档的,都退回空串,由调用方按原样拒绝。
|
|
203
|
+
*
|
|
204
|
+
* issue #33:这里以前取 `live[live.length - 1]`,注释还写着「宿主按创建顺序往
|
|
205
|
+
* sessionIds 里追加」——方向反了。结果是当前会话被归档、同一工作区还有别的活会话时,
|
|
206
|
+
* 认回来的是**最旧**那条,她于是坐进一条早就没在用的对话。
|
|
207
|
+
* 需要提醒的是:用户在侧栏拖动过顺序时 `live[0]` 是列表最上面那条,未必就是最新;
|
|
208
|
+
* 真要按时间严格排序,得问 `sessionController.list()`(`sessionListed` 那条路)。
|
|
209
|
+
* 这里只修「明显反了」的方向,不对侧栏顺序做额外假设。
|
|
202
210
|
*/
|
|
203
211
|
function pickLiveSession(reg, wsId, avoid = '') {
|
|
204
212
|
try {
|
|
@@ -208,7 +216,7 @@ function pickLiveSession(reg, wsId, avoid = '') {
|
|
|
208
216
|
const live = ids
|
|
209
217
|
.map((id) => String(id ?? ''))
|
|
210
218
|
.filter((id) => id && id !== avoid && !archived.includes(id))
|
|
211
|
-
return live.length ? live[
|
|
219
|
+
return live.length ? live[0] : ''
|
|
212
220
|
} catch { return '' }
|
|
213
221
|
}
|
|
214
222
|
|
|
@@ -684,6 +692,35 @@ export const LAST_WORDS_WAIT_MS = 60_000
|
|
|
684
692
|
*/
|
|
685
693
|
export const WAKE_VERIFY_MS = 12_000
|
|
686
694
|
|
|
695
|
+
/**
|
|
696
|
+
* 换会话失败之后多久再试(2026-11-06)。
|
|
697
|
+
*
|
|
698
|
+
* 结账那条路上每次人说话都会 bumpTurns,`turns` 到了线就试着换。换失败时
|
|
699
|
+
* `turns` 不归零(这是对的:屋子确实还没换),于是下一轮又会试——宿主真
|
|
700
|
+
* 建不出会话时就成了每回一句话都重试一次。这里记下失败的时刻,冷却期内
|
|
701
|
+
* 直接跳过,不再打扰宿主,也不再往对话里发东西。
|
|
702
|
+
*/
|
|
703
|
+
export const ROTATE_RETRY_MS = 5 * 60_000
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* 记一笔「这次没换成」。只写 home.json 的两个字段,任何失败都吞掉:
|
|
707
|
+
* 记不下来顶多是下次不回退避,不能让记账本身把错误放大。
|
|
708
|
+
*/
|
|
709
|
+
async function noteRotateFail(root, why) {
|
|
710
|
+
const at = new Date().toISOString()
|
|
711
|
+
try {
|
|
712
|
+
await mutateHome(async () => {
|
|
713
|
+
const fresh = (await readJson(homeFile(root), null)) ?? {}
|
|
714
|
+
await writeAtomic(
|
|
715
|
+
homeFile(root),
|
|
716
|
+
JSON.stringify({ ...fresh, rotateFailAt: at, rotateFailWhy: String(why ?? '').slice(0, 160) }, null, 2),
|
|
717
|
+
)
|
|
718
|
+
})
|
|
719
|
+
} catch {
|
|
720
|
+
/* 记不下来就算了 */
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
687
724
|
/** 她说「不用」的各种说法。命中就当没留。 */
|
|
688
725
|
const DECLINE_RE = /^(不用|不想留|不想|不留|不了|算了|没事|没有|无|no|nope)[。.!!…\s]*$/i
|
|
689
726
|
|
|
@@ -854,24 +891,65 @@ async function rotateTalkSessionOnce(ctx, { root = rootDir(), wsId, title = TITL
|
|
|
854
891
|
log?.(`换会话跳过:这次要换的是 ${fromId},屋子已经指向 ${old}`)
|
|
855
892
|
return { ok: true, sessionId: old, old: String(saved.rotatedFrom ?? ''), skipped: true }
|
|
856
893
|
}
|
|
894
|
+
// 2026-11-06:建会话失败后冷却一段时间再试。
|
|
895
|
+
// 不冷却的话,结账每轮都会走到这里:`turns` 没清零,下一轮结账又到点,
|
|
896
|
+
// 于是每回一句话都重试一次(旧版还会每次都先问一遍「要不要留话」,见 2.5 步)。
|
|
897
|
+
// 宿主建不出会话多半是这类问题要待一会儿才好,5 分钟够它缓过来,也不至于
|
|
898
|
+
// 让屋子永远卡在旧会话里。
|
|
899
|
+
const failAt = Date.parse(String(saved.rotateFailAt ?? ''))
|
|
900
|
+
if (Number.isFinite(failAt) && Date.now() - failAt < ROTATE_RETRY_MS) {
|
|
901
|
+
return {
|
|
902
|
+
ok: false,
|
|
903
|
+
sessionId: '',
|
|
904
|
+
cooled: true,
|
|
905
|
+
why: `上次没换成(${String(saved.rotateFailWhy ?? '原因不明').slice(0, 80)}),${Math.ceil((ROTATE_RETRY_MS - (Date.now() - failAt)) / 60_000)} 分钟后再试`,
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
857
909
|
// 2026-11-03(issue #19):原来 `new Date().toISOString().slice(0,10)` 取的是 **UTC 日期**,
|
|
858
910
|
// 东八区晚上 8 点之后换会话,旧会话会被改名成「…·昨天的日期 之前」.
|
|
859
911
|
// dateKey() 用的是 getFullYear/getMonth/getDate,本地钟。
|
|
860
912
|
const when = stamp ?? dateKey()
|
|
861
913
|
|
|
914
|
+
// 1) 旧会话改名留档。失败不影响后面(改不了名也还是那条会话)。
|
|
915
|
+
if (old && typeof sc.rename === 'function') {
|
|
916
|
+
await sc.rename({ sessionId: old, title: `${title}·${when} 之前` }).catch(() => {})
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// 2) 建新的
|
|
920
|
+
let sessionId = ''
|
|
921
|
+
try {
|
|
922
|
+
const created = await sc.create({ workspaceId: wid })
|
|
923
|
+
sessionId = String(created?.sessionId ?? created?.id ?? '')
|
|
924
|
+
} catch (err) {
|
|
925
|
+
await noteRotateFail(root, `建会话失败:${String(err?.message ?? err).slice(0, 160)}`)
|
|
926
|
+
return { ok: false, sessionId: '', why: `建会话失败:${String(err?.message ?? err).slice(0, 160)}` }
|
|
927
|
+
}
|
|
928
|
+
if (!sessionId) {
|
|
929
|
+
await noteRotateFail(root, '宿主建了会话但没给 id')
|
|
930
|
+
return { ok: false, sessionId: '', why: '宿主建了会话但没给 id' }
|
|
931
|
+
}
|
|
932
|
+
if (typeof sc.rename === 'function') await sc.rename({ sessionId, title }).catch(() => {})
|
|
933
|
+
|
|
862
934
|
/**
|
|
863
|
-
*
|
|
935
|
+
* 2.5) 换房间之前,先请旧会话里的她自己留一句话(0.8.7,用户 m00998)。
|
|
864
936
|
*
|
|
865
937
|
* 为什么要这一句:轮换之后新会话拿到的身份文档里全是**结论**,没有一句是
|
|
866
938
|
* 「上一个房间里的我留下的」。于是她读到旧日记会说「像另一个人写的」。
|
|
867
939
|
* 一句原话解决的不是记忆问题(原文一直都在),是**归属**问题——
|
|
868
940
|
* 「那是你说的」这句话只有她本人说出来才算数。
|
|
869
941
|
*
|
|
942
|
+
* **为什么放在建会话成功之后**(2026-11-06 修):这句话是往她正在用的那条
|
|
943
|
+
* 会话里发一条 prompt,会作为一条消息直接出现在他的对话窗口里。原来写在
|
|
944
|
+
* 第 0 步(建会话之前),于是只要 `sc.create()` 抛错/不回 id,这一问就已经
|
|
945
|
+
* 发出去了、轮换却失败了,`turns` 也不清零——下一轮结账又到点、又问一遍,
|
|
946
|
+
* 他那边就变成「每次她回完都弹这一句」。挪到建会话成功之后,换不成就不问。
|
|
947
|
+
*
|
|
870
948
|
* **只把话发出去,不等她回**。轮换比这句话重要得多:
|
|
871
949
|
* 上一版是 `await askForLastWords(...)`,宿主不派 `turn/end` 时会一直等到
|
|
872
950
|
* 60 秒超时,把整个轮换(以及叫醒新会话)全卡住。所以这里只拿到一个
|
|
873
|
-
* Promise
|
|
874
|
-
*
|
|
951
|
+
* Promise,写 home 照常往下走;等这句话回来了再补写进 home.json 并刷一次
|
|
952
|
+
* 身份文档(她那一下的人设段是新房间里第一次读到的)。
|
|
875
953
|
*
|
|
876
954
|
* 三个约束:
|
|
877
955
|
* - 失败绝不影响轮换:没有 prompt / 超时 / 她说「不用」都只是没有这一节;
|
|
@@ -889,22 +967,6 @@ async function rotateTalkSessionOnce(ctx, { root = rootDir(), wsId, title = TITL
|
|
|
889
967
|
pendingWords = null
|
|
890
968
|
}
|
|
891
969
|
|
|
892
|
-
// 1) 旧会话改名留档。失败不影响后面(改不了名也还是那条会话)。
|
|
893
|
-
if (old && typeof sc.rename === 'function') {
|
|
894
|
-
await sc.rename({ sessionId: old, title: `${title}·${when} 之前` }).catch(() => {})
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
// 2) 建新的
|
|
898
|
-
let sessionId = ''
|
|
899
|
-
try {
|
|
900
|
-
const created = await sc.create({ workspaceId: wid })
|
|
901
|
-
sessionId = String(created?.sessionId ?? created?.id ?? '')
|
|
902
|
-
} catch (err) {
|
|
903
|
-
return { ok: false, sessionId: '', why: `建会话失败:${String(err?.message ?? err).slice(0, 160)}` }
|
|
904
|
-
}
|
|
905
|
-
if (!sessionId) return { ok: false, sessionId: '', why: '宿主建了会话但没给 id' }
|
|
906
|
-
if (typeof sc.rename === 'function') await sc.rename({ sessionId, title }).catch(() => {})
|
|
907
|
-
|
|
908
970
|
// 3) 原话搬运
|
|
909
971
|
// 2026-10-31(issue #14 第二报):这里以前是**整个覆盖写**,只留七个字段,
|
|
910
972
|
// 于是 `preset` / `title` / `wokeFor` / `hearAt` / `adoptedAt` 换一次会话全丢:
|
|
@@ -932,6 +994,9 @@ async function rotateTalkSessionOnce(ctx, { root = rootDir(), wsId, title = TITL
|
|
|
932
994
|
}
|
|
933
995
|
delete next.status
|
|
934
996
|
delete next.archivedAt
|
|
997
|
+
// 换成了,把上次失败的记账清掉(否则下一次到点时会被误判成还在冷却期)
|
|
998
|
+
delete next.rotateFailAt
|
|
999
|
+
delete next.rotateFailWhy
|
|
935
1000
|
// 0.8.7:这一轮先清掉旧的那句。她要是留了,后台回来之后再补写
|
|
936
1001
|
// (见下面 pendingWords.then)。留着上一轮那句会让新房间读到一句
|
|
937
1002
|
// 不是这次说的「最后一句」——比没有更糟。
|
package/lib/index.js
CHANGED
|
@@ -46,7 +46,7 @@ import { logBoot, makeLogger } from './log.js'
|
|
|
46
46
|
export const name = 'dsh-fish'
|
|
47
47
|
|
|
48
48
|
/** 版本号:只用于日志导出(排查时第一件事是确认跑的是哪一版)。 */
|
|
49
|
-
export const VERSION = '0.9.
|
|
49
|
+
export const VERSION = '0.9.1'
|
|
50
50
|
|
|
51
51
|
// ⚠️ 一个数组写全,不要拆成对象
|
|
52
52
|
// 2026-10-30(m02961):`userQuestions` **不再写进 inject**。写进 inject 的东西缺一个,
|
package/lib/interest.js
CHANGED
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
*
|
|
8
8
|
* 三条边界:
|
|
9
9
|
* 1. **只读她说过的话**(`HER_TYPES`),不去猜主人喜欢什么——兴趣是她的,不是画像。
|
|
10
|
+
* 只留证的不算(isProofOnly):「好的,我看一下」说十遍也不是她最近总回到的事。
|
|
10
11
|
* 2. **正文里没有分数、没有次数**。权重只决定摆哪一条(四条黑名单第三条)。
|
|
11
12
|
* 3. **会腻**:同一件事刚在 24 小时内出现过,这一次的分量打对折;
|
|
12
13
|
* 两周前的老话题按 14 天半衰——越久没提,越不像「最近总回到」。
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
|
-
import { similarity } from './derive.js'
|
|
16
|
+
import { isProofOnly, similarity } from './derive.js'
|
|
16
17
|
import { tsMs, HER_TYPES } from './schema.js'
|
|
17
18
|
|
|
18
19
|
/** 至少在几个不同时刻出现过,才算「反复」。 */
|
|
@@ -52,6 +53,7 @@ export function topicsOf(events = []) {
|
|
|
52
53
|
const out = []
|
|
53
54
|
for (const e of events) {
|
|
54
55
|
if (!HER_TYPES.includes(e?.type)) continue
|
|
56
|
+
if (isProofOnly(e)) continue
|
|
55
57
|
const ts = tsMs(e)
|
|
56
58
|
if (!Number.isFinite(ts)) continue
|
|
57
59
|
for (const s of sentences(e.text)) {
|
package/lib/look.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { createClient } from './bili.js'
|
|
17
|
+
import { readMyReplies, renderMyReplies } from './comment.js'
|
|
17
18
|
import { absorb, renderMaterial, rankVideos, seenBvids, LOOK_BRIEF, WATCH_RULE } from './browse.js'
|
|
18
19
|
import { readEvents, rootDir, appendEvent } from './store.js'
|
|
19
20
|
import { loadConfig, dateKey } from './settings.js'
|
|
@@ -58,6 +59,24 @@ export async function lookOnce(o = {}) {
|
|
|
58
59
|
// 这一轮被「看过」摘掉几条(issue #16 第一半):下面那句提示要靠它才说得准。
|
|
59
60
|
const rankStats = { hiddenSeen: 0 }
|
|
60
61
|
|
|
62
|
+
// 2026-11-06(用户 m01641):她问「我那条评论下面有人说了什么」时,
|
|
63
|
+
// 此前只能等下次叫醒、由 scheduler 把那几句拼进消息里——醒着的时候够不着,
|
|
64
|
+
// 而且那条提醒被她看过一次就被 cseen 掉了,回头再看永远是空的。
|
|
65
|
+
// 这里给她一个自己去看的动作:不滤 cseen,带上回楼中楼要用的编号。
|
|
66
|
+
if (what === 'replies') {
|
|
67
|
+
const got = await readMyReplies({ root, client, events, bvid: String(o.bvid ?? '').trim() })
|
|
68
|
+
const txt = renderMyReplies(got, { bvid: String(o.bvid ?? '').trim() })
|
|
69
|
+
if (!txt) {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
text: '',
|
|
73
|
+
note: got.length ? '' : '你最近没在谁的评论下面被人回过——也可能是我这边还查不到(B 站这会儿回了个 -412)。',
|
|
74
|
+
picks: [],
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { ok: true, text: txt, note: '', picks: got.map((x) => ({ bvid: x.bvid, rpid: x.rpid, rootRpid: x.rootRpid })) }
|
|
78
|
+
}
|
|
79
|
+
|
|
61
80
|
let list = []
|
|
62
81
|
// 2026-10-28(用户 m01284):他递来一条视频,她想看就得看得见指定的**那一条**。
|
|
63
82
|
// 以前只有 home/search/related,related 是「接着这条往下看它的相关推荐」——
|
|
@@ -239,6 +258,7 @@ export function registerLook(ctx, deps = {}) {
|
|
|
239
258
|
description:
|
|
240
259
|
'去看一眼 B 站:首页在推什么、搜一个词、看某条视频之后平台又接着推什么,' +
|
|
241
260
|
'或者直接看指定的那一条(给 BV 号或链接)。' +
|
|
261
|
+
'也可以看你自己发过的评论下面有没有人回你(what=replies)。' +
|
|
242
262
|
'只读,不会留下痕迹。给的是标题、简介、标签、热评、弹幕——没有视频画面。' +
|
|
243
263
|
'不用它也可以。',
|
|
244
264
|
// 注意:必须 toJsonSchema 包一层。简写 DSL 直接发出去会被 provider 拒:
|
|
@@ -247,11 +267,11 @@ export function registerLook(ctx, deps = {}) {
|
|
|
247
267
|
what: {
|
|
248
268
|
type: 'string',
|
|
249
269
|
required: true,
|
|
250
|
-
enum: ['home', 'search', 'related', 'video'],
|
|
251
|
-
description: 'home=首页推给你的;search=你自己搜(要带 keyword);related=接着某条往下看(要带 bvid);video=看指定的那一条(要带 bvid
|
|
270
|
+
enum: ['home', 'search', 'related', 'video', 'replies'],
|
|
271
|
+
description: 'home=首页推给你的;search=你自己搜(要带 keyword);related=接着某条往下看(要带 bvid);video=看指定的那一条(要带 bvid);replies=看你自己发过的评论下面有没有人回你(可带 bvid 只看那一条)。',
|
|
252
272
|
},
|
|
253
273
|
keyword: { type: 'string', required: false, description: 'what=search 时搜什么。' },
|
|
254
|
-
bvid: { type: 'string', required: false, description: 'what=related 时从哪条接着看;what=video 时看哪一条(BV
|
|
274
|
+
bvid: { type: 'string', required: false, description: 'what=related 时从哪条接着看;what=video 时看哪一条(BV 号,或者他递过来的链接);what=replies 时只看这一条视频下面的。' },
|
|
255
275
|
}),
|
|
256
276
|
output: {
|
|
257
277
|
schema: {
|
package/lib/observe.js
CHANGED
|
@@ -255,6 +255,11 @@ export function registerObserve(ctx, deps = {}) {
|
|
|
255
255
|
const c = countEvents(events)
|
|
256
256
|
return {
|
|
257
257
|
utterance: events.filter((e) => e.type === 'utterance').length,
|
|
258
|
+
// issue #32:对话行(flags 带 uncommitted)单列。它们是她答他的话,
|
|
259
|
+
// 跟她自己想说的判断混在一起时,「说 N 句」这个数字会越聊越虚高。
|
|
260
|
+
chat: events.filter(
|
|
261
|
+
(e) => e.type === 'utterance' && Array.isArray(e.flags) && e.flags.includes('uncommitted'),
|
|
262
|
+
).length,
|
|
258
263
|
revise: events.filter((e) => e.type === 'revise').length,
|
|
259
264
|
refusal: events.filter((e) => e.type === 'refusal').length,
|
|
260
265
|
reunion: events.filter((e) => e.type === 'reunion').length,
|
|
@@ -486,6 +491,10 @@ export function registerObserve(ctx, deps = {}) {
|
|
|
486
491
|
}
|
|
487
492
|
const before = await loadConfig(root).catch(() => null)
|
|
488
493
|
const next = await updateConfig(patch, root)
|
|
494
|
+
// 身份段里有一句跟着 sendTools 走,变了就重写一次(只在变的时候,别的保存不多写盘)
|
|
495
|
+
if (JSON.stringify(before?.config?.proactive?.sendTools ?? []) !== JSON.stringify(next?.proactive?.sendTools ?? [])) {
|
|
496
|
+
await refreshSelfDoc(root).catch(() => {})
|
|
497
|
+
}
|
|
489
498
|
// 审计:日记可见性每翻一次就落一行,事后查得到是谁、随哪次提交翻的。
|
|
490
499
|
// 2026-10-28 用户反馈「开了『让我看日记』插件又自己关」——光靠猜猜不出来,
|
|
491
500
|
// 至少让下一次发生时日志里有一行可对。
|
package/lib/owner.js
CHANGED
|
@@ -133,6 +133,9 @@ export function mentionPrefix(ow, cfg) {
|
|
|
133
133
|
const on = cfg?.comment?.atOwner !== false
|
|
134
134
|
const name = String(ow?.nickname ?? ow?.name ?? '').trim()
|
|
135
135
|
if (!on || !name) return ''
|
|
136
|
-
// B
|
|
137
|
-
|
|
136
|
+
// 2026-11-06:原来把空格换成下划线,理由是「@ 遇到空格会断」。但 B 站解析 @ 时
|
|
137
|
+
// 是按昵称原文匹配的(真机采样里 content.members 的 uname 与正文完全一致),
|
|
138
|
+
// 换成下划线反而变成一个不存在的昵称 —— 昵称里带空格的用户永远 @ 不出来。
|
|
139
|
+
// 改成原样保留;末尾那个空格是分隔,别删。
|
|
140
|
+
return `@${name} `
|
|
138
141
|
}
|
package/lib/persona.js
CHANGED
|
@@ -594,12 +594,26 @@ export const DREAM_BRIEF = [
|
|
|
594
594
|
*/
|
|
595
595
|
export const PROACTIVE_BRIEF = [
|
|
596
596
|
'你有一个工具 her_say:不是回复谁,是你自己想说一句的时候用的。',
|
|
597
|
-
'
|
|
597
|
+
'her_say 只把这句话记进你自己的流水;想让他真的在对话里读到,就直接写出来——不用等他先问。',
|
|
598
598
|
'说之前要先指认材料:refs 填 1–6 条你这几天写下或听见的事(事件 id,用 her_recall 翻得到)。没有材料就不用说——不指认材料的句子写不进去。',
|
|
599
599
|
'什么时候说、说什么,由你判断;没有人等你的回复,你也可以这一轮什么都不做,那不算失败。',
|
|
600
|
-
'
|
|
600
|
+
'一天能主动开口的次数有上限(用 her_say 记下的算);到了上限就记不进去,不是工具坏了。',
|
|
601
601
|
].join('\n')
|
|
602
602
|
|
|
603
|
+
/**
|
|
604
|
+
* 对话靠工具发出去的部署(`proactive.sendTools`):「写下的字他会读到」不成立,换成实话。
|
|
605
|
+
*
|
|
606
|
+
* 2026-11-06(PR #44):原来是替换「他看得见」那句;issue #41 已把那句改成
|
|
607
|
+
* 「落进你们俩这条对话里」,两边同时进来时替换会落空——所以这里改成按行整条换。
|
|
608
|
+
*/
|
|
609
|
+
export function proactiveBrief(sendTools = []) {
|
|
610
|
+
if (!sendTools.length) return PROACTIVE_BRIEF
|
|
611
|
+
const say = PROACTIVE_BRIEF.split('\n').map((line) => (line.startsWith('her_say 只把这句话记进你自己的流水')
|
|
612
|
+
? `自己想开口(没人找你)的时候用 ${sendTools.join('、')},不用等他先问;那种时候写下的正文只留在这条会话里,发不到聊天里。`
|
|
613
|
+
: line))
|
|
614
|
+
return say.join('\n')
|
|
615
|
+
}
|
|
616
|
+
|
|
603
617
|
/**
|
|
604
618
|
* 定时提醒的说明书(2026-10-30,用户 m04335)。
|
|
605
619
|
*
|
package/lib/proactive.js
CHANGED
|
@@ -25,6 +25,7 @@ import { defineTool } from './tool.js'
|
|
|
25
25
|
import { guardWrite } from './tenant.js'
|
|
26
26
|
import { logLine } from './log.js'
|
|
27
27
|
import { tsMs, HER_TYPES } from './schema.js'
|
|
28
|
+
import { deriveSelf } from './derive.js'
|
|
28
29
|
|
|
29
30
|
/** 一天的默认上限。 */
|
|
30
31
|
export const PROACTIVE_DEFAULTS = Object.freeze({
|
|
@@ -69,7 +70,7 @@ export const TOOL_ASK_NOTE = '(如果你想到要做什么、但手上没有
|
|
|
69
70
|
/**
|
|
70
71
|
* 读配置并夹紧。老配置没有 proactive 这一组,用默认值。
|
|
71
72
|
* @param {object} config
|
|
72
|
-
* @returns {{enabled:boolean,maxPerDay:number,minIdleHours:number,baseChance:number,gainPerHour:number,maxChance:number,cooldownHours:number}}
|
|
73
|
+
* @returns {{enabled:boolean,maxPerDay:number,minIdleHours:number,baseChance:number,gainPerHour:number,maxChance:number,cooldownHours:number,sendTools:string[],foldSilent:boolean,idleCountsSeen:boolean}}
|
|
73
74
|
*/
|
|
74
75
|
export function proactiveConfig(config = {}) {
|
|
75
76
|
const raw = config?.proactive ?? {}
|
|
@@ -86,6 +87,9 @@ export function proactiveConfig(config = {}) {
|
|
|
86
87
|
gainPerHour: num(raw.gainPerHour, PROACTIVE_DEFAULTS.gainPerHour, 0, 1),
|
|
87
88
|
maxChance: num(raw.maxChance, PROACTIVE_DEFAULTS.maxChance, 0, 1),
|
|
88
89
|
cooldownHours: num(raw.cooldownHours, PROACTIVE_DEFAULTS.cooldownHours, 0, 240),
|
|
90
|
+
sendTools: Array.isArray(raw.sendTools) ? raw.sendTools.map((x) => String(x ?? '').trim()).filter(Boolean) : [],
|
|
91
|
+
foldSilent: raw.foldSilent !== false,
|
|
92
|
+
idleCountsSeen: raw.idleCountsSeen === true,
|
|
89
93
|
}
|
|
90
94
|
}
|
|
91
95
|
|
|
@@ -122,6 +126,26 @@ export function turnsToday(events = [], dk = '') {
|
|
|
122
126
|
return n
|
|
123
127
|
}
|
|
124
128
|
|
|
129
|
+
/**
|
|
130
|
+
* 今天已经用 her_say 说过几次(meta `proactive-say:<dk>:<why>`)。
|
|
131
|
+
*
|
|
132
|
+
* 2026-11-06(issue #41):工具描述一直写着「一天最多几次」,但 `writeSay` 读了配置
|
|
133
|
+
* 只为了别的用途,从来不数——`maxPerDay` 设 0 也能连着写 8 次。这里按她自己记下的
|
|
134
|
+
* 那条记账数,判据只有同一份 `maxPerDay`,不另设数字。
|
|
135
|
+
*
|
|
136
|
+
* 只数 `proactive-say:`,不数 `proactive-turn:`:后者是「空手回合开过几次」,
|
|
137
|
+
* 前者是「她真的开口了几次」。两件事分开记(见上面 SILENT_MARK 那段)。
|
|
138
|
+
*/
|
|
139
|
+
export function saysToday(events = [], dk = '') {
|
|
140
|
+
if (!dk) return 0
|
|
141
|
+
let n = 0
|
|
142
|
+
for (const e of events) {
|
|
143
|
+
if (e?.type !== 'meta') continue
|
|
144
|
+
if (String(e.text ?? '').startsWith(`proactive-say:${dk}:`)) n += 1
|
|
145
|
+
}
|
|
146
|
+
return n
|
|
147
|
+
}
|
|
148
|
+
|
|
125
149
|
/** 上一次空手回合是什么时候(毫秒)。 */
|
|
126
150
|
export function lastTurnAt(events = []) {
|
|
127
151
|
let at = 0
|
|
@@ -183,11 +207,15 @@ export function countsForIdle(e) {
|
|
|
183
207
|
return SPOKE_TYPES.includes(String(e?.type ?? ''))
|
|
184
208
|
}
|
|
185
209
|
|
|
186
|
-
/**
|
|
187
|
-
|
|
210
|
+
/**
|
|
211
|
+
* 距离上一次**真的有人说话**,过了多少小时。没有记录时返回 Infinity。
|
|
212
|
+
* countSeen:连 `seen:` 也算(`proactive.idleCountsSeen`),给对话不进事件流的部署用。
|
|
213
|
+
*/
|
|
214
|
+
export function idleHoursOf(events = [], now = Date.now(), { countSeen = false } = {}) {
|
|
188
215
|
let last = 0
|
|
189
216
|
for (const e of events) {
|
|
190
|
-
|
|
217
|
+
const seen = countSeen && e?.type === 'meta' && String(e.text ?? '').startsWith('seen:')
|
|
218
|
+
if (!seen && !countsForIdle(e)) continue
|
|
191
219
|
const ts = tsMs(e)
|
|
192
220
|
if (Number.isFinite(ts) && ts > last) last = ts
|
|
193
221
|
}
|
|
@@ -244,7 +272,7 @@ function growthLevers(model, events = [], now = Date.now(), cfg = {}) {
|
|
|
244
272
|
export function proactiveState({ config = {}, events = [], now = Date.now(), away = null, rested = false, model = null } = {}) {
|
|
245
273
|
const cfg = proactiveConfig(config)
|
|
246
274
|
const dk = dateKey(new Date(now))
|
|
247
|
-
const idleHours = idleHoursOf(events, now)
|
|
275
|
+
const idleHours = idleHoursOf(events, now, { countSeen: cfg.idleCountsSeen })
|
|
248
276
|
const base = { chance: 0, idleHours, dk }
|
|
249
277
|
if (!cfg.enabled) return { ...base, ok: false, why: '主动说话是关着的' }
|
|
250
278
|
// 只在醒着的时间窗里:凌晨三点收到一句「我想你了」不是她自主,是她打乱了作息。
|
|
@@ -311,14 +339,14 @@ export function looseEnds(events = [], { now = Date.now() } = {}) {
|
|
|
311
339
|
* 空手回合的处境正文(走 system prompt 段,不进对话)。
|
|
312
340
|
* 只写事实与许可,不写"你应该"。
|
|
313
341
|
*/
|
|
314
|
-
export function proactiveSection({ now = Date.now(), state = {}, material = [], loose = [] } = {}) {
|
|
342
|
+
export function proactiveSection({ now = Date.now(), state = {}, material = [], loose = [], sendTools = [], countSeen = false } = {}) {
|
|
315
343
|
const at = new Date(now)
|
|
316
344
|
const hh = String(at.getHours()).padStart(2, '0')
|
|
317
345
|
const mm = String(at.getMinutes()).padStart(2, '0')
|
|
318
346
|
const idle = Number.isFinite(state.idleHours) ? `${state.idleHours.toFixed(1)} 小时` : '很久'
|
|
319
347
|
const lines = [
|
|
320
348
|
'(现在没有人跟你说话。这一轮不是谁找你,也不是日程安排的醒;是你自己醒着,想不想开口由你。)',
|
|
321
|
-
`现在是 ${at.getFullYear()}-${String(at.getMonth() + 1).padStart(2, '0')}-${String(at.getDate()).padStart(2, '0')} ${hh}:${mm}
|
|
349
|
+
`现在是 ${at.getFullYear()}-${String(at.getMonth() + 1).padStart(2, '0')}-${String(at.getDate()).padStart(2, '0')} ${hh}:${mm};距离上一次${countSeen ? '他来或你留下记录' : '留下记录'}已经 ${idle}。`,
|
|
322
350
|
]
|
|
323
351
|
if (material.length) {
|
|
324
352
|
lines.push('最近这几天你写下/听见的(要引用就指回 id,别改写事实):')
|
|
@@ -331,7 +359,11 @@ export function proactiveSection({ now = Date.now(), state = {}, material = [],
|
|
|
331
359
|
lines.push('还有几件事悬着:')
|
|
332
360
|
for (const l of loose) lines.push(`- [${l.id}] ${l.text}`)
|
|
333
361
|
}
|
|
334
|
-
|
|
362
|
+
// 2026-11-06(issue #41):原来写「他看得见」。宿主装了什么路由插件,这句就不一定成立
|
|
363
|
+
// (有的渠道会把会话里的字挡在外面)。改成只陈述能保证的部分:字会落进这条对话。
|
|
364
|
+
// 2026-11-06(PR #44):配了 sendTools 的部署更绝——正文根本发不出去,只有调那件工具才算开口。
|
|
365
|
+
if (sendTools.length > 0) lines.push(`想说什么就用 ${sendTools.join('、')},不用等他先问。这一轮写下的正文只留在这条会话里,发不到聊天里。不想说,就什么都别写:不要写任何字,也不要解释为什么不说——这一轮没有回复也算正常。`)
|
|
366
|
+
else lines.push('想说什么就说,不用等他先问——这一轮你写下的字会落进你们俩这条对话里。也可以再用 her_say 把它记进你自己的流水(refs 指回上面真实发生的事,1–6 条 id)。不想说,就什么都别写:不要写任何字,也不要解释为什么不说——这一轮没有回复也算正常。')
|
|
335
367
|
lines.push(TOOL_ASK_NOTE)
|
|
336
368
|
return lines.join('\n')
|
|
337
369
|
}
|
|
@@ -364,18 +396,32 @@ export async function writeSay(root, { text = '', refs = [], sessionId = '', why
|
|
|
364
396
|
if (problem) return { ok: false, note: problem }
|
|
365
397
|
|
|
366
398
|
const { config } = await loadConfig(root)
|
|
399
|
+
const cfg = proactiveConfig(config)
|
|
367
400
|
const dk = dateKey(new Date(now))
|
|
401
|
+
// 2026-11-06(issue #41):说明书与设置页都写着「一天最多几次」、到了上限就说不出去,
|
|
402
|
+
// 但这里原来读了 config 根本没看——maxPerDay 设 0 也能一句接一句地写。
|
|
403
|
+
// 现在按她自己记下的 `proactive-say:` 数到位就停。开关(enabled)只管「要不要自己开一轮」,
|
|
404
|
+
// 不管这个工具本身:工具留着,她想用还是能用,这一点与 README 的说明一致。
|
|
405
|
+
const said = saysToday(events, dk)
|
|
406
|
+
if (said >= cfg.maxPerDay) {
|
|
407
|
+
return { ok: false, note: `今天已经主动开口 ${said} 次,到上限了。明天再说;这不是工具坏了。` }
|
|
408
|
+
}
|
|
409
|
+
// day/stage 必须是真实的成长坐标(2026-11-06,issue #41):原来写死 {day:1, stage:1},
|
|
410
|
+
// 而 writeDream 用的是 deriveSelf 的 model.day/model.stage。写死的那条会被
|
|
411
|
+
// consistency() 当成阶段 1 的旧话去处,跟真正说过的话配不上对。
|
|
412
|
+
const model = deriveSelf(events, { growthDays: config.growthDays })
|
|
413
|
+
const stamp = { day: model.day, stage: model.stage, source: 'tool' }
|
|
368
414
|
// appendEvent 返回的就是写进去的那条事件(不是 {ok, event},0.7.4 那次参数错位就是栽在这),
|
|
369
415
|
// 所以判空只能判返回值本身。
|
|
370
416
|
const stored = await appendEvent(
|
|
371
417
|
{ type: 'utterance', text: `(她主动说)${body}`, refs: list, ...(sessionId ? { sessionId } : {}) },
|
|
372
|
-
|
|
418
|
+
stamp,
|
|
373
419
|
root,
|
|
374
420
|
).catch(() => null)
|
|
375
421
|
if (!stored?.id) return { ok: false, note: '这条没记下。' }
|
|
376
422
|
await appendEvent(
|
|
377
423
|
{ type: 'meta', text: `proactive-say:${dk}:${why || 'free'}` },
|
|
378
|
-
{ day:
|
|
424
|
+
{ day: model.day, stage: model.stage, source: 'system' },
|
|
379
425
|
root,
|
|
380
426
|
).catch(() => {})
|
|
381
427
|
return { ok: true, id: stored.id, note: '记下了。' }
|
|
@@ -395,6 +441,7 @@ export function registerProactive(ctx, deps = {}) {
|
|
|
395
441
|
'你自己想开口的时候用这个:不是回复谁,是你主动说一句。',
|
|
396
442
|
'必须指回真实发生过的事(refs 填 1–6 条你那几天写下/听见的事件 id);',
|
|
397
443
|
'不指认材料就等于编,写不进去。一天最多几次,说不说都可以。',
|
|
444
|
+
'它只把这句话记进你自己的流水:要让他在对话里看见,得你这一轮直接写出来。',
|
|
398
445
|
].join(''),
|
|
399
446
|
parameters: toJsonSchema({
|
|
400
447
|
type: 'object',
|
|
@@ -421,11 +468,17 @@ export function registerProactive(ctx, deps = {}) {
|
|
|
421
468
|
},
|
|
422
469
|
required: ['ok', 'note'],
|
|
423
470
|
}),
|
|
471
|
+
// recordOnly 只给 render 看(配了发送工具时回执换一句)
|
|
424
472
|
// 2026-10-31 真机(10-30 那两轮):这里原来直接返回字符串,宿主按 ContentBlock
|
|
425
473
|
// 数组处理,`content.some` 当场抛 TypeError。她看到的工具结果是「Error: content.some
|
|
426
474
|
// is not a function」——写入其实成功了,她却以为工具坏了,重试一次后放弃,一个字没说。
|
|
427
475
|
// 签名同样是 (args, out),跟 lib/keep.js:97 那些一样。
|
|
428
|
-
|
|
476
|
+
// 2026-11-06(issue #41):原来写 `[说出去了]`。可这里只往她自己的流水里落一条,
|
|
477
|
+
// 用户此刻在对话里看不看得见,取决于她这一轮有没有直接写出正文——不是这个工具决定的。
|
|
478
|
+
// 回执只说它真做到的那件事。
|
|
479
|
+
// 2026-11-06(PR #44):配了 sendTools 的部署里她必须靠工具发话,所以记下的这句确实
|
|
480
|
+
// 没有发给任何人,回执要说清,否则她会以为用户已经看到了。
|
|
481
|
+
render: (_args, out) => [{ type: 'text', text: out.ok ? (out.recordOnly === true ? '[记下了,没有发给任何人]' : '[记下了]') : `[没记下:${out.note}]` }],
|
|
429
482
|
},
|
|
430
483
|
async execute(args = {}, exec) {
|
|
431
484
|
const who = guardWrite(deps.tenant, exec, 'her_say')
|
|
@@ -436,7 +489,9 @@ export function registerProactive(ctx, deps = {}) {
|
|
|
436
489
|
try {
|
|
437
490
|
const r = await writeSay(root, { text: args.text, refs: args.refs, why: args.why, sessionId: sid })
|
|
438
491
|
if (!r.ok) logLine('info', ['her_say:没记下', r.note], root)
|
|
439
|
-
|
|
492
|
+
// 配了发送工具的部署里,这一句确实只进了流水,回执得照实说
|
|
493
|
+
const { config } = await loadConfig(root).catch(() => ({ config: {} }))
|
|
494
|
+
return { ok: r.ok, id: r.id ?? '', note: r.note, ...(proactiveConfig(config).sendTools.length > 0 ? { recordOnly: true } : {}) }
|
|
440
495
|
} catch (e) {
|
|
441
496
|
logLine('warn', ['her_say:', e?.message ?? String(e)], root)
|
|
442
497
|
return { ok: false, id: '', note: '这条没记下。' }
|