@ssamurias/dsh-fish 0.9.0 → 0.9.2
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 +60 -4
- package/CHANGELOG.md +85 -1
- package/README.md +13 -6
- package/RELEASE-NOTES-0.9.1.md +296 -0
- package/RELEASE-NOTES-0.9.2.md +46 -0
- package/RELEASE.md +152 -0
- package/lib/absence.js +41 -27
- package/lib/ask.js +91 -30
- package/lib/askinchat.js +83 -1
- package/lib/browse.js +1 -0
- package/lib/capture.js +35 -2
- package/lib/cardanswer.js +84 -0
- package/lib/client.js +116 -19
- package/lib/comment.js +111 -2
- package/lib/compact.js +45 -1
- package/lib/curiosity.js +6 -2
- package/lib/deliver.js +46 -0
- package/lib/derive.js +25 -13
- package/lib/desk.js +3 -1
- package/lib/dreamtable.js +13 -2
- package/lib/drive.js +14 -2
- package/lib/events.js +25 -1
- package/lib/freewake.js +4 -1
- package/lib/generator.js +2 -1
- package/lib/grant.js +18 -2
- package/lib/home.js +142 -29
- package/lib/identity.js +111 -14
- package/lib/index.js +50 -10
- package/lib/interest.js +3 -1
- package/lib/keep.js +21 -9
- package/lib/keepsake.js +20 -13
- package/lib/look.js +23 -3
- package/lib/note.js +255 -20
- package/lib/observe.js +14 -4
- package/lib/owner.js +5 -2
- package/lib/persona.js +26 -9
- package/lib/proactive.js +67 -12
- package/lib/say.js +73 -13
- package/lib/scheduler.js +186 -56
- package/lib/schema.js +18 -0
- package/lib/selfdoc.js +5 -3
- package/lib/selfupdate.js +8 -1
- package/lib/settings.js +5 -0
- package/lib/status.js +56 -6
- package/lib/store.js +14 -2
- package/lib/tenant.js +12 -0
- package/lib/turns.js +47 -2
- package/lib/upgrade.js +4 -1
- package/package.json +2 -1
- package/scripts/fix-legacy-link.mjs +197 -90
package/lib/askinchat.js
CHANGED
|
@@ -20,13 +20,79 @@
|
|
|
20
20
|
* 否则 CALLER_NOT_LIVE / DELEGATED_CALLER
|
|
21
21
|
* - 没人在线时 noAnswerer 抛 NO_PROVIDER
|
|
22
22
|
* - 卡片挂在 `ctx.sessions.scopeOf(agent)` 那条会话上
|
|
23
|
+
*
|
|
24
|
+
* 人不在这条会话前的部署(都从 IM 上说话):卡片他看不见,她这一轮还要白等 5 分钟,
|
|
25
|
+
* 一天后记成「一直没人答」。所以弹卡之前先问一声有没有插件能把话送到人在的地方
|
|
26
|
+
* (`dsh-fish/card`,AGENTS.md §7.3)。接走了就不弹卡、不等,回答走 `dsh-fish/answer`
|
|
27
|
+
* 回来(lib/cardanswer.js),人隔几个小时才答、中间重启过都能结算。
|
|
23
28
|
*/
|
|
24
29
|
|
|
30
|
+
import { looksLikeRewriting, looksLikeSecret } from './guard.js'
|
|
25
31
|
import { liveAgentOf } from './identity.js'
|
|
32
|
+
import { logLine } from './log.js'
|
|
26
33
|
|
|
27
34
|
/** 一次提问默认等多久:5 分钟。超时不当失败,当「这次没问到」。 */
|
|
28
35
|
export const ASK_TIMEOUT_MS = 5 * 60_000
|
|
29
36
|
|
|
37
|
+
/**
|
|
38
|
+
* 等别的插件接卡最多等多久。它发不出去,这张卡还来得及弹在会话上。
|
|
39
|
+
* 等得比一次发送的超时久(IM 插件常见 30 秒):先放弃的话桌面弹一张、那边又送到一张,两边都能答。
|
|
40
|
+
*/
|
|
41
|
+
export const RELAY_WAIT_MS = 35_000
|
|
42
|
+
|
|
43
|
+
/** 卡上有没有看着像凭据的字:她的问题、理由、选项都算(§3:秘密外发一律拦下)。 */
|
|
44
|
+
function cardHasSecret(card) {
|
|
45
|
+
const texts = [card.detail]
|
|
46
|
+
for (const q of card.questions) texts.push(q?.header, q?.question, ...(Array.isArray(q?.options) ? q.options.map((o) => o?.label) : []))
|
|
47
|
+
return texts.some((text) => looksLikeSecret(text).hit)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 回执里要念给她听的去处。这是别的插件写的字(§3:新输入口都过 guard.js)。
|
|
52
|
+
* 空的算没接走;看着像改写指令的,卡还是接走了,只是不把这几个字念给她。
|
|
53
|
+
*/
|
|
54
|
+
function viaOf(taken) {
|
|
55
|
+
const raw = typeof taken?.via === 'string' ? taken.via : ''
|
|
56
|
+
const via = raw.replace(/\s+/g, ' ').trim().slice(0, 40)
|
|
57
|
+
if (!via) return ''
|
|
58
|
+
// 按行看:改写指令的判断认行首,压成一行之后「QQ\nsystem: …」就认不出来了
|
|
59
|
+
const risky = raw.split(/[\r\n\u2028\u2029]+/).some((line) => looksLikeRewriting(line).hit) || looksLikeRewriting(via).hit || looksLikeSecret(raw).hit
|
|
60
|
+
return risky ? '别的地方' : via
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 问问有没有插件接这张卡。只有回了 `{ via: '说给她听的去处' }` 才算接走;
|
|
65
|
+
* 没人接、抛错、拒绝、超时、这一轮被取消都按没接,**绝不抛**。Promise 的拒绝由 race 接住:
|
|
66
|
+
* 宿主把未处理的拒绝当致命错误,超时之后才拒绝的也一样要接住。
|
|
67
|
+
* `bail` 停在第一个非空返回值上,所以 async 监听只有排第一的那个算数。
|
|
68
|
+
* 看着带凭据的卡不问:它只留在本机的卡片上,不发给任何插件。截断之前的原文也要看,
|
|
69
|
+
* 不然长 token 的前半段正好够不上判断的长度。这条判断偏严,长文件名也可能中,所以留一行日志,只记 id。
|
|
70
|
+
*/
|
|
71
|
+
async function relayCard(ctx, card, waitMs, signal, { rawTexts = [], root = undefined } = {}) {
|
|
72
|
+
if (cardHasSecret(card) || rawTexts.some((text) => looksLikeSecret(text).hit)) {
|
|
73
|
+
if (root) logLine('warn', ['卡上有看着像凭据的字,没转给别的插件,只留在本机:', card.id], root)
|
|
74
|
+
return ''
|
|
75
|
+
}
|
|
76
|
+
let timer
|
|
77
|
+
let onAbort
|
|
78
|
+
try {
|
|
79
|
+
const r = ctx?.bail?.('dsh-fish/card', card)
|
|
80
|
+
const taken = typeof r?.then === 'function'
|
|
81
|
+
? await Promise.race([r, new Promise((resolve) => {
|
|
82
|
+
timer = setTimeout(() => resolve(null), waitMs)
|
|
83
|
+
onAbort = () => resolve(null)
|
|
84
|
+
signal?.addEventListener?.('abort', onAbort, { once: true })
|
|
85
|
+
})])
|
|
86
|
+
: r
|
|
87
|
+
return viaOf(taken)
|
|
88
|
+
} catch {
|
|
89
|
+
return ''
|
|
90
|
+
} finally {
|
|
91
|
+
clearTimeout(timer)
|
|
92
|
+
if (onAbort) signal?.removeEventListener?.('abort', onAbort)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
30
96
|
/**
|
|
31
97
|
* 把她想问的话丢进会话里,等那个人选一个。
|
|
32
98
|
*
|
|
@@ -36,7 +102,12 @@ export const ASK_TIMEOUT_MS = 5 * 60_000
|
|
|
36
102
|
* @param {Array} opts.questions 宿主格式的问题数组
|
|
37
103
|
* @param {number} [opts.timeoutMs]
|
|
38
104
|
* @param {object} [opts.signal] 外部取消信号(例如这一轮已经结束了)
|
|
39
|
-
* @
|
|
105
|
+
* @param {string} [opts.cardId] 这张卡的 id(`ask:…` 等,见 lib/cardanswer.js)。不给就不会被别的插件接走
|
|
106
|
+
* @param {string} [opts.detail] 只给接卡的插件看的补充说明(卡片本身放不下的那句「为什么」)
|
|
107
|
+
* @param {Array} [opts.rawTexts] 卡上的字截断之前的原文,只用来判断带不带凭据
|
|
108
|
+
* @param {string} [opts.root] 这个家,用来留日志
|
|
109
|
+
* @returns {Promise<{ok: boolean, answers?: Array, code?: string, via?: string}>}
|
|
110
|
+
* 被接走时 `{ok:false, code:'RELAYED', via}`:调用方照「问了,还没答」处理
|
|
40
111
|
*/
|
|
41
112
|
export async function askInChat(ctx, opts = {}) {
|
|
42
113
|
const {
|
|
@@ -45,9 +116,20 @@ export async function askInChat(ctx, opts = {}) {
|
|
|
45
116
|
timeoutMs = ASK_TIMEOUT_MS,
|
|
46
117
|
signal = undefined,
|
|
47
118
|
waitAgentMs = 10_000,
|
|
119
|
+
cardId = '',
|
|
120
|
+
detail = '',
|
|
121
|
+
relayWaitMs = RELAY_WAIT_MS,
|
|
122
|
+
rawTexts = [],
|
|
123
|
+
root = undefined,
|
|
48
124
|
} = opts
|
|
49
125
|
if (!Array.isArray(questions) || questions.length === 0) return { ok: false, code: 'NO_QUESTIONS' }
|
|
50
126
|
|
|
127
|
+
if (cardId && !signal?.aborted) {
|
|
128
|
+
const via = await relayCard(ctx, { id: cardId, sessionId, questions, detail }, relayWaitMs, signal, { rawTexts, root })
|
|
129
|
+
if (via) return { ok: false, code: 'RELAYED', via }
|
|
130
|
+
if (signal?.aborted) return { ok: false, code: 'AbortError' }
|
|
131
|
+
}
|
|
132
|
+
|
|
51
133
|
// 宿主只接受 live root agent。她那轮刚被 wake 顶起来时,agent 可能还没就位,
|
|
52
134
|
// 所以这里给一小段等待窗口(默认 10 秒)而不是一次查不到就放弃。
|
|
53
135
|
const agent = await waitAgent(ctx, sessionId, { totalMs: waitAgentMs, signal })
|
package/lib/browse.js
CHANGED
|
@@ -540,6 +540,7 @@ export const LOOK_BRIEF = [
|
|
|
540
540
|
' what=search 搜一个词(要带 keyword)',
|
|
541
541
|
' what=related 看某个视频之后,平台又接着推什么(要带 bvid)',
|
|
542
542
|
' what=video 看指定的那一条(要带 bvid:BV 号,或者他递过来的链接)',
|
|
543
|
+
' what=replies 看你自己发过的评论下面有没有人回你(可带 bvid 只看那一条)',
|
|
543
544
|
'它只读,不会留下任何痕迹,也不会让别人知道你在看。',
|
|
544
545
|
'它能给你标题、简介、标签、热评、弹幕——给不了视频画面。',
|
|
545
546
|
'和 growth_commit 一样,你不用它也可以。',
|
package/lib/capture.js
CHANGED
|
@@ -23,6 +23,7 @@ import { inWake } from './wakewin.js'
|
|
|
23
23
|
import { refreshSelfDoc } from './selfdoc.js'
|
|
24
24
|
import { guardWrite } from './tenant.js'
|
|
25
25
|
import { activeOf, activePossessionOf, readOutings, readPossessions } from './outing.js'
|
|
26
|
+
import { readChannels } from './channels.js'
|
|
26
27
|
import { tripKey } from './keepsake.js'
|
|
27
28
|
|
|
28
29
|
/**
|
|
@@ -185,13 +186,31 @@ export function registerCapture(ctx, deps = {}) {
|
|
|
185
186
|
return { ok: true, id: dk, note: '写下了。' }
|
|
186
187
|
}
|
|
187
188
|
|
|
189
|
+
// 写盘的 schema 会把越界的数字悄悄改成最近的(confidence 夹到 0–1)。
|
|
190
|
+
// 她传 5 会以为记下了 5,读回来却是 1——和 issue #43 的 wakesPerDay 是同一类问题。
|
|
191
|
+
// 入口先拒,别让「说好的」和「落地」不一样;这时还没写事件,可以改完重试。
|
|
192
|
+
if (args?.confidence != null) {
|
|
193
|
+
const c = Number(args.confidence)
|
|
194
|
+
if (!Number.isFinite(c) || c < 0 || c > 1) {
|
|
195
|
+
return {
|
|
196
|
+
ok: false,
|
|
197
|
+
id: '',
|
|
198
|
+
note: `confidence 要写 0–1 之间的小数(你写的是 ${String(args.confidence)})。写盘时越界会被悄悄夹到边上,所以这里先不收。`,
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// verdict 同理:schema 认不出来的会落成 null,她以为写了 confirmed。
|
|
203
|
+
if (args?.verdict != null && !['confirmed', 'refuted', 'refined'].includes(args.verdict)) {
|
|
204
|
+
return { ok: false, id: '', note: 'verdict 只能是 confirmed / refuted / refined。' }
|
|
205
|
+
}
|
|
206
|
+
|
|
188
207
|
const ev = await appendEvent(
|
|
189
208
|
{
|
|
190
209
|
type: args?.type,
|
|
191
210
|
text: args?.text,
|
|
192
211
|
refs: args?.refs,
|
|
193
212
|
verdict: args?.verdict,
|
|
194
|
-
confidence: args?.confidence,
|
|
213
|
+
confidence: args?.confidence == null ? undefined : Number(args.confidence),
|
|
195
214
|
// 「谁写的」:这一条是她这条会话说的,不是别的屋子里那一位。
|
|
196
215
|
sessionId: who.sessionId,
|
|
197
216
|
},
|
|
@@ -206,7 +225,21 @@ export function registerCapture(ctx, deps = {}) {
|
|
|
206
225
|
// 坑(p1.test.mjs 抓到的):这里的 source 必须是 'system'。
|
|
207
226
|
// 写成 'tool' 时 schema 会以 NOT_HER_TYPE 拒掉 meta,而外面套着 .catch(()=>{}),
|
|
208
227
|
// 于是 seen 永远写不进去 → 冷落检测永远返回 no-contact-yet → 整条链路静默失效。
|
|
209
|
-
|
|
228
|
+
//
|
|
229
|
+
// issue #34:这条判据以前只看「在不在叫醒窗口里」,不看这句话是在哪说的。
|
|
230
|
+
// 群聊那轮她调一次 growth_commit,就给群里多记一条 `seen:` —— 群友说一百句
|
|
231
|
+
// 也算「他来过」,冷落检测于是永远不触发(他一周没露面,面板还显示今天聊过)。
|
|
232
|
+
// AGENTS.md §7.2 第 2 条本来就要求「外客不动生命体征」,这里补上:
|
|
233
|
+
// 这条会话如果是群房,就当他在场这件事没发生过。
|
|
234
|
+
//
|
|
235
|
+
// 为什么只跳群房(2026-11-05 用户确认):另外两处「窗口外的自己醒」——
|
|
236
|
+
// 换房前问她上一间还有没有话(home.js:753)、定时任务与 goal——
|
|
237
|
+
// 一个通常是在应他上一轮的话,一个分不出是不是在回他,所以照记。
|
|
238
|
+
// 要再收紧得先给每种叫醒打标记,不在 #34 这一刀里做。
|
|
239
|
+
const outsideWake = !inWake()
|
|
240
|
+
const inGroupRoom = outsideWake && (await readChannels(root).catch(() => []))
|
|
241
|
+
.some((r) => who.sessionId && r.sessionId === who.sessionId && r.scene === 'group')
|
|
242
|
+
if (outsideWake && !inGroupRoom) {
|
|
210
243
|
await appendEvent(
|
|
211
244
|
{ type: 'meta', text: `seen:${new Date().toISOString()}` },
|
|
212
245
|
{ day: model.day, stage: model.stage, source: 'system' },
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cardanswer.js —— 卡片被别的插件接走之后,人的回答从这里回来
|
|
3
|
+
*
|
|
4
|
+
* 人不在她那条会话前的部署(都从 IM 上说话),插件可以用 `dsh-fish/card` 把卡接走
|
|
5
|
+
* (lib/askinchat.js,AGENTS.md §7.3)。人可能隔几个小时才答,中间宿主可能还重启过,
|
|
6
|
+
* 所以回答不靠闭包:插件记着卡的 id,答了就发
|
|
7
|
+
*
|
|
8
|
+
* const r = await ctx.bail('dsh-fish/answer', { id: card.id, selected: ['可以'] })
|
|
9
|
+
*
|
|
10
|
+
* id 是 `种类:引用`:`ask:<事件 id>`、`keepsake:<条目 id>`、`grant:<申请 id>`。
|
|
11
|
+
* selected 和宿主 userQuestions 的回答同形,填选项原文。没选、对不上选项都不结算,
|
|
12
|
+
* 这张卡还开着:人回的可能只是一句话,猜成同意或以后再说都可能是错的,而且只结算一次,错了改不回来。
|
|
13
|
+
* 结算走的都是观测台和卡片用的同一条路,「只结算一次」也由它们管。
|
|
14
|
+
* `ok: false` 不代表这张卡结了,写盘失败也是它,接卡的插件留着 id,人再答一次就再试一次。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { rootDir, readEvents } from './store.js'
|
|
18
|
+
import { ASK_TTL_MS, answersByAsk, asksOf, settleAsk, settleStaleAsks } from './ask.js'
|
|
19
|
+
import { tsMs } from './schema.js'
|
|
20
|
+
import { picked } from './askinchat.js'
|
|
21
|
+
import { decideKeepsake } from './keepsake.js'
|
|
22
|
+
import { YES as KEEP_YES, NO as KEEP_NO } from './keep.js'
|
|
23
|
+
import { recordGrant, GRANT_YES, GRANT_NO } from './grant.js'
|
|
24
|
+
import { pendingOffer } from './unlock.js'
|
|
25
|
+
|
|
26
|
+
const ASK_VERDICTS = ['approve', 'reject', 'later']
|
|
27
|
+
|
|
28
|
+
async function answerAsk(root, askId, label) {
|
|
29
|
+
// 过期结算是定时器做的,她出门或者宿主刚起来时可能没跑。过没过期要在这里就算数。
|
|
30
|
+
await settleStaleAsks(root)
|
|
31
|
+
const { events } = await readEvents(root)
|
|
32
|
+
const ev = asksOf(events).find((e) => e?.id === askId)
|
|
33
|
+
if (!ev) return { ok: false, why: '事件流里没有这一条请求' }
|
|
34
|
+
// 上面那次过期结算可能没写进去,过了一天的回答一样不算数。
|
|
35
|
+
if (!answersByAsk(events).has(askId) && Date.now() - tsMs(ev) >= ASK_TTL_MS) return { ok: false, why: '放了一天没人答,算以后再说了' }
|
|
36
|
+
// 只认选项原文。别的插件把人回的「1」直接传进来,不能按下标算成第二个。
|
|
37
|
+
const options = Array.isArray(ev?.ask?.options) ? ev.ask.options.map((x) => String(x ?? '').trim()) : []
|
|
38
|
+
const verdict = ASK_VERDICTS[options.indexOf(label)]
|
|
39
|
+
if (!verdict) return { ok: false, why: '这张卡没有这个选项' }
|
|
40
|
+
const r = await settleAsk(root, { askId, verdict, reason: label, via: 'relay' })
|
|
41
|
+
return r.ok ? { ok: true } : { ok: false, why: r.why }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function answerKeepsake(root, id, label) {
|
|
45
|
+
if (label !== KEEP_YES && label !== KEEP_NO) return { ok: false, why: '这张卡没有这个选项' }
|
|
46
|
+
const d = decideKeepsake(root, { id, yes: label === KEEP_YES })
|
|
47
|
+
return d.ok ? { ok: true } : { ok: false, why: d.note }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function answerGrant(root, offerId, label) {
|
|
51
|
+
if (label !== GRANT_YES && label !== GRANT_NO) return { ok: false, why: '这张卡没有这个选项' }
|
|
52
|
+
// 读不到账要照实说,不能说成申请不在了。
|
|
53
|
+
const { events } = await readEvents(root)
|
|
54
|
+
const offer = pendingOffer(events)
|
|
55
|
+
if (offer?.id !== offerId) return { ok: false, why: '这张申请已经不在等了' }
|
|
56
|
+
const w = await recordGrant(root, { offerId, rule: offer.grant?.rule || '', verdict: label === GRANT_YES ? 'approve' : 'reject' })
|
|
57
|
+
return w.ok ? { ok: true } : { ok: false, why: w.error }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 结算一张被接走的卡。**绝不抛**,别的插件不用替我们兜底。
|
|
62
|
+
* @param {string} root
|
|
63
|
+
* @param {{id: string, selected?: Array}} answer
|
|
64
|
+
* @returns {Promise<{ok: boolean, why?: string}>}
|
|
65
|
+
*/
|
|
66
|
+
export async function answerCard(root = rootDir(), answer = {}) {
|
|
67
|
+
try {
|
|
68
|
+
const id = String(answer?.id ?? '')
|
|
69
|
+
const at = id.indexOf(':')
|
|
70
|
+
const ref = at > 0 ? id.slice(at + 1) : ''
|
|
71
|
+
if (!ref) return { ok: false, why: '没说是哪一张卡' }
|
|
72
|
+
const label = picked([{ id, selected: answer?.selected }], id)
|
|
73
|
+
if (!label) return { ok: false, why: '没选' }
|
|
74
|
+
switch (id.slice(0, at)) {
|
|
75
|
+
case 'ask': return await answerAsk(root, ref, label)
|
|
76
|
+
case 'keepsake': return answerKeepsake(root, ref, label)
|
|
77
|
+
case 'grant': return await answerGrant(root, ref, label)
|
|
78
|
+
default: return { ok: false, why: '不认识这种卡' }
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
// 原因可能转告给人,也可能送进她的对话:只说错误码,不带本机路径。
|
|
82
|
+
return { ok: false, why: String(err?.code ?? err?.message ?? err).slice(0, 160) }
|
|
83
|
+
}
|
|
84
|
+
}
|
package/lib/client.js
CHANGED
|
@@ -383,6 +383,10 @@ window.__ModuleLoader__.load({
|
|
|
383
383
|
const [proactiveOn, setProactiveOn] = useState(false)
|
|
384
384
|
const [proactiveMax, setProactiveMax] = useState(2)
|
|
385
385
|
const [proactiveIdle, setProactiveIdle] = useState(6)
|
|
386
|
+
const [proactiveTools, setProactiveTools] = useState('')
|
|
387
|
+
const proactiveToolsTimer = useRef(null)
|
|
388
|
+
const [proactiveCompact, setProactiveCompact] = useState(true)
|
|
389
|
+
const [proactiveSeen, setProactiveSeen] = useState(false)
|
|
386
390
|
// 2026-11-05(用户 m02634):梦 / 主动性 / 兴趣 三个开关,默认全开。
|
|
387
391
|
const [driveOn, setDriveOn] = useState(true)
|
|
388
392
|
const [interestOn, setInterestOn] = useState(true)
|
|
@@ -448,6 +452,10 @@ window.__ModuleLoader__.load({
|
|
|
448
452
|
const [owCookie, setOwCookie] = useState('')
|
|
449
453
|
const [atOwner, setAtOwner] = useState(true)
|
|
450
454
|
const [owNote, setOwNote] = useState('')
|
|
455
|
+
// 2026-11-06(用户反馈):领养人两格原来只有「B 站登录 → 粘贴 cookie」页签底部那一颗保存键,
|
|
456
|
+
// 停在「领养人验证」这一组里根本找不到保存路径,停在扫码页签里那颗键更是连渲染都不渲染。
|
|
457
|
+
// 这一组现在自带一颗,提示也写在这一组里,不再借用下面 cookie 那行的 owNote。
|
|
458
|
+
const [owIdNote, setOwIdNote] = useState('')
|
|
451
459
|
// 扫码登录(2026-11-01):浮层里的这张码。
|
|
452
460
|
// qr 只在浮层开着的时候有值;关掉就 drop,宿主那边攥着的 cookie 一起忘掉。
|
|
453
461
|
const [owTab, setOwTab] = useState('qr')
|
|
@@ -596,6 +604,9 @@ window.__ModuleLoader__.load({
|
|
|
596
604
|
setProactiveOn(pr.enabled === true)
|
|
597
605
|
setProactiveMax(Number(pr.maxPerDay ?? 2))
|
|
598
606
|
setProactiveIdle(Number(pr.minIdleHours ?? 6))
|
|
607
|
+
setProactiveTools(Array.isArray(pr.sendTools) ? pr.sendTools.join(', ') : '')
|
|
608
|
+
setProactiveCompact(pr.foldSilent !== false)
|
|
609
|
+
setProactiveSeen(pr.idleCountsSeen === true)
|
|
599
610
|
}
|
|
600
611
|
setDriveOn(s.config?.drive?.enabled !== false)
|
|
601
612
|
setInterestOn(s.config?.interest?.enabled !== false)
|
|
@@ -988,8 +999,14 @@ window.__ModuleLoader__.load({
|
|
|
988
999
|
const saveOwnerCard = async () => {
|
|
989
1000
|
setBusy('owner'); setOwNote('')
|
|
990
1001
|
try {
|
|
991
|
-
|
|
1002
|
+
// 2026-11-06(用户反馈):这三格原来是「一律原样提交」,于是只粘 cookie、两格
|
|
1003
|
+
// 没填(或先清空了)的人,点一次保存就把 mid / 昵称覆写成空串 —— 昵称一空,
|
|
1004
|
+
// mentionPrefix 就返回空串,评论里再也 @ 不出来。现在只提交填了的格子。
|
|
1005
|
+
const body = {}
|
|
1006
|
+
if (owMid.trim()) body.mid = owMid.trim()
|
|
1007
|
+
if (owName.trim()) body.nickname = owName.trim()
|
|
992
1008
|
if (owCookie.trim()) body.cookie = owCookie.trim()
|
|
1009
|
+
if (!Object.keys(body).length) { setOwNote('三格都是空的,没东西可存。'); setBusy(''); return }
|
|
993
1010
|
const r = await jpost('/owner', body)
|
|
994
1011
|
setOwNote(r.note || (r.ok ? '存下了。' : '没存上。'))
|
|
995
1012
|
setOwCookie('')
|
|
@@ -997,6 +1014,26 @@ window.__ModuleLoader__.load({
|
|
|
997
1014
|
} catch (e) { setOwNote(e.message) } finally { setBusy('') }
|
|
998
1015
|
}
|
|
999
1016
|
|
|
1017
|
+
/**
|
|
1018
|
+
* 只保存「领养人」那两格(2026-11-06,用户反馈)。
|
|
1019
|
+
* 以前这两格唯一能存的地方挂在下面「B 站登录 → 粘贴 cookie」页签底部:
|
|
1020
|
+
* 停在领养人这一组里找不到,停在扫码页签里那颗键压根不渲染,填完等于白填。
|
|
1021
|
+
* 现在这组自带一颗,提交范围也只有这两格——不动下面那份 cookie,也不把空串写回去。
|
|
1022
|
+
* 存完刷一次 /owner:旧版本写脏时那句提示会跟着消失,两格也换回服务端真正存下的值。
|
|
1023
|
+
*/
|
|
1024
|
+
const saveOwnerIdentity = async () => {
|
|
1025
|
+
setBusy('owid'); setOwIdNote('')
|
|
1026
|
+
try {
|
|
1027
|
+
const body = {}
|
|
1028
|
+
if (owMid.trim()) body.mid = owMid.trim()
|
|
1029
|
+
if (owName.trim()) body.nickname = owName.trim()
|
|
1030
|
+
if (!Object.keys(body).length) { setOwIdNote('两格都空着,没有可存的。'); setBusy(''); return }
|
|
1031
|
+
const r = await jpost('/owner', body)
|
|
1032
|
+
setOwIdNote(r.note || (r.ok ? '存下了。' : '没存上。'))
|
|
1033
|
+
if (r.ok) { flashSaved(); await load() }
|
|
1034
|
+
} catch (e) { setOwIdNote(e.message) } finally { setBusy('') }
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1000
1037
|
const testOwner = async () => {
|
|
1001
1038
|
setBusy('owtest'); setOwNote('')
|
|
1002
1039
|
try {
|
|
@@ -1329,6 +1366,17 @@ window.__ModuleLoader__.load({
|
|
|
1329
1366
|
onChange: (e) => o.onChange(e.target.value) }))
|
|
1330
1367
|
}
|
|
1331
1368
|
function TimeRange(o) {
|
|
1369
|
+
// 2026-11-06(0.9.2 反馈):原生时间框跟随系统 12 小时制,00:00 显示成
|
|
1370
|
+
// 「上午 12:00」——中文用户会读成中午十二点,于是以为窗口是 12:00–23:00,
|
|
1371
|
+
// 再看到凌晨五点醒来就当成 bug。输入框保留(有选择器),下面补一行
|
|
1372
|
+
// 24 小时制把值说死;跨零点时标明「次日」。
|
|
1373
|
+
const hhmm = (v) => {
|
|
1374
|
+
const m = /^(\d{1,2}):(\d{2})$/.exec(String(v ?? ''))
|
|
1375
|
+
return m ? `${m[1].padStart(2, '0')}:${m[2]}` : ''
|
|
1376
|
+
}
|
|
1377
|
+
const from = hhmm(o.from)
|
|
1378
|
+
const to = hhmm(o.to)
|
|
1379
|
+
const nextDay = from && to && to < from ? '次日 ' : ''
|
|
1332
1380
|
return h('div', { className: 'fld' },
|
|
1333
1381
|
h('div', { className: 'fl' },
|
|
1334
1382
|
h('span', { className: 'flabel' }, o.label),
|
|
@@ -1336,7 +1384,11 @@ window.__ModuleLoader__.load({
|
|
|
1336
1384
|
h('div', { className: 'row', style: { gap: '6px' } },
|
|
1337
1385
|
h('input', { type: 'time', value: o.from, onChange: (e) => o.onFrom(e.target.value) }),
|
|
1338
1386
|
h('span', { className: 'dim' }, '到'),
|
|
1339
|
-
h('input', { type: 'time', value: o.to, onChange: (e) => o.onTo(e.target.value) }))
|
|
1387
|
+
h('input', { type: 'time', value: o.to, onChange: (e) => o.onTo(e.target.value) })),
|
|
1388
|
+
from && to
|
|
1389
|
+
? h('div', { className: 'dim', style: { fontSize: '12px', marginTop: '2px' } },
|
|
1390
|
+
`24 小时制:${from} → ${nextDay}${to}`)
|
|
1391
|
+
: null)
|
|
1340
1392
|
}
|
|
1341
1393
|
|
|
1342
1394
|
// 工具面:把限制的理由写给用户看,给一个总开关,另留一处额外放行口。
|
|
@@ -2051,7 +2103,9 @@ window.__ModuleLoader__.load({
|
|
|
2051
2103
|
: rowTip('立场', '还没开始', '要等四个阶段(信任 / 自主 / 主动 / 整合)都走完,立场才开始产生。'),
|
|
2052
2104
|
rowTip('最近的称呼', s.name ? s.name : '还没想好', '她自己决定怎么称呼你。这个不是设置项,改不了。'),
|
|
2053
2105
|
rowTip('冷落', s.absenceDays > 0 ? `${s.absenceDays} 天` : '没有', '你多久没说话了。超过门槛她会记下「你不在」。'),
|
|
2054
|
-
|
|
2106
|
+
// issue #32:`counts.utterance` 含对话行(flags 带 uncommitted),把它们减掉之后
|
|
2107
|
+
// 才是「她自己写下、不是答他的话」的句数;那些行单列成「聊天」。
|
|
2108
|
+
rowTip('互动计数', `说 ${Math.max(0, (s.counts.utterance ?? 0) - (s.counts.chat ?? 0))} · 改口 ${s.counts.revise} · 拒绝 ${s.counts.refusal} · 重聚 ${s.counts.reunion}${s.counts.chat ? ` · 聊天 ${s.counts.chat}` : ''}`, '说=她写过几句自己的判断(含留话);改口=按照规则推翻历史认知并指明改的是哪条;拒绝=她明确登记拒绝过什么;重聚=久别之后她自己写下的感受。聊天=对话里她回你的话,单列,不算进「说」。'),
|
|
2055
2109
|
s.quality ? h('div', { style: { marginTop: '4px', marginBottom: '2px' } },
|
|
2056
2110
|
h('div', {
|
|
2057
2111
|
style: { display: 'flex', gap: '8px', alignItems: 'baseline', fontSize: '13px', marginBottom: '5px' },
|
|
@@ -2144,14 +2198,14 @@ window.__ModuleLoader__.load({
|
|
|
2144
2198
|
hint: '超过这么多天没说话,她会记下「你不在」。' }))),
|
|
2145
2199
|
// 主动说话(0.8.1 本机实验):只有 .dev 下才出现。频率是上限,不是任务——
|
|
2146
2200
|
// 开关开着也不代表她每天都会说,说不说由她自己判断(见 lib/proactive.js)。
|
|
2147
|
-
// 主动搭话(0.8.1 起,0.9.0
|
|
2201
|
+
// 主动搭话(0.8.1 起,0.9.0 公开):只留跟「开口」直接有关的事。
|
|
2148
2202
|
// 劲儿与兴趣是另一套东西,各自一张卡——混在一张卡里,用户分不清哪一项在管什么。
|
|
2149
2203
|
group('主动搭话',
|
|
2150
2204
|
'她可以自己找你说话,不用你先开口。这里调的是上限:开着也可能好几天不说,那是她的判断,不是坏了。',
|
|
2151
2205
|
h('div', { className: 'grid2' },
|
|
2152
2206
|
Toggle({ label: '允许她主动开口', on: proactiveOn,
|
|
2153
2207
|
onChange: (v) => { setProactiveOn(v); patchProactive({ enabled: v }) },
|
|
2154
|
-
hint: '
|
|
2208
|
+
hint: '关掉:她不会自己开一轮来找你。her_say 这个工具还在,她想说时仍可以用,只是不再由系统替她开这一轮。' }),
|
|
2155
2209
|
Sel({ label: '一天最多几次', value: proactiveMax,
|
|
2156
2210
|
onChange: (v) => { setProactiveMax(v); patchProactive({ maxPerDay: Number(v) }) },
|
|
2157
2211
|
options: [0, 1, 2, 3, 4, 5, 6],
|
|
@@ -2160,7 +2214,22 @@ window.__ModuleLoader__.load({
|
|
|
2160
2214
|
Num({ label: '你多久没说话,她才可能开口', value: proactiveIdle,
|
|
2161
2215
|
onChange: (v) => { setProactiveIdle(v); patchProactive({ minIdleHours: Number(v) }) },
|
|
2162
2216
|
min: 1, max: 24, unit: '小时',
|
|
2163
|
-
hint: '越久没说话,她越可能开口(有上限)。刚聊完不会马上又来找你。' })
|
|
2217
|
+
hint: '越久没说话,她越可能开口(有上限)。刚聊完不会马上又来找你。' }),
|
|
2218
|
+
Text({ label: '她靠哪些工具把话发出去', value: proactiveTools, wide: true,
|
|
2219
|
+
placeholder: '工具名,逗号分隔;不填就只认正文',
|
|
2220
|
+
// 打字时不逐键保存:停手 700ms 再存,和这一页别的自动保存一样
|
|
2221
|
+
onChange: (v) => {
|
|
2222
|
+
setProactiveTools(v)
|
|
2223
|
+
clearTimeout(proactiveToolsTimer.current)
|
|
2224
|
+
proactiveToolsTimer.current = setTimeout(() => patchProactive({ sendTools: v.split(/[,,\s]+/).filter(Boolean) }), 700)
|
|
2225
|
+
},
|
|
2226
|
+
hint: '她写的字不会被插件自动发出去、要调工具才发得出去时才填,并且要放进下面的插件能力白名单。填了之后只有工具调成功才算她开了口。' }),
|
|
2227
|
+
Toggle({ label: '没开口的那一轮折起来', on: proactiveCompact,
|
|
2228
|
+
onChange: (v) => { setProactiveCompact(v); patchProactive({ foldSilent: v }) },
|
|
2229
|
+
hint: '省上下文。上下文很大、靠缓存省钱时可以关:改历史会让缓存失效。' }),
|
|
2230
|
+
Toggle({ label: '你来过也算打断安静', on: proactiveSeen,
|
|
2231
|
+
onChange: (v) => { setProactiveSeen(v); patchProactive({ idleCountsSeen: v }) },
|
|
2232
|
+
hint: '你只从聊天软件找她、她又不写正文时才开,不然你刚聊完她也觉得安静了很久。代价是你在这台机器上和别的 agent 说话也算。' }))),
|
|
2164
2233
|
// 主观能动性:不是「定时叫她说话」,而是她心里慢慢攒的一股劲。
|
|
2165
2234
|
group('主观能动性',
|
|
2166
2235
|
'她心里会慢慢攒一股想动的劲:越久没自己动过、桌上越有值得看的东西,攒得越快。攒够了才会自己醒一次——不按钟点,也不是每天必须。',
|
|
@@ -2170,7 +2239,7 @@ window.__ModuleLoader__.load({
|
|
|
2170
2239
|
hint: '关掉:她不会自己开一轮,只在你叫她、或者日程到点时才醒。已经发生的事不受影响。' }))),
|
|
2171
2240
|
// 兴趣与惦记:她醒来时桌上摆的那几样东西。
|
|
2172
2241
|
group('兴趣',
|
|
2173
|
-
'
|
|
2242
|
+
'她醒来的时候,桌上会摆几样她自己的东西:她反复问起过的事、翻到过的旧句子、你回没回她留的那句话、最近总说到的那件事。这些只给她看,不会变成待办。关掉之后连旧句子也不再摆,她只会按时被叫醒。',
|
|
2174
2243
|
h('div', { className: 'grid2' },
|
|
2175
2244
|
Toggle({ label: '醒来时把这些摆出来', on: interestOn,
|
|
2176
2245
|
onChange: (v) => { setInterestOn(v); patchGroup('interest', { enabled: v }) },
|
|
@@ -2240,6 +2309,8 @@ window.__ModuleLoader__.load({
|
|
|
2240
2309
|
else setAskMsg(String(r?.why || '没答上。'))
|
|
2241
2310
|
} catch (e) {
|
|
2242
2311
|
setAskMsg(`没答上:${e.message}`)
|
|
2312
|
+
// 多半是别的地方先答了:把列表刷新一下,别还留着这张卡
|
|
2313
|
+
jget('/ask').then(setAsks).catch(() => {})
|
|
2243
2314
|
}
|
|
2244
2315
|
}
|
|
2245
2316
|
|
|
@@ -2517,15 +2588,21 @@ window.__ModuleLoader__.load({
|
|
|
2517
2588
|
// 旧版本(≤0.5.9)会把登录账号写进这两格,只有点「保存」才会重写;这里点出来,别让人以为是刷新丢的。
|
|
2518
2589
|
owMid && ow?.account?.mid && String(owMid) === String(ow.account.mid)
|
|
2519
2590
|
? h('p', { className: 'warn' },
|
|
2520
|
-
'这两格现在跟下面登录的账号是同一个号,像是旧版本写进去的。改成你要 @ 的 uid
|
|
2591
|
+
'这两格现在跟下面登录的账号是同一个号,像是旧版本写进去的。改成你要 @ 的 uid 和昵称,再点下面「保存这两格」。')
|
|
2521
2592
|
: null,
|
|
2522
2593
|
h('div', { className: 'grid2' },
|
|
2523
2594
|
Text({ label: '要 @ 的 B 站 uid', value: owMid, onChange: setOwMid, placeholder: '数字,比如 12345678', wide: true,
|
|
2524
2595
|
hint: '就是 B 站个人主页地址里那串数字。' }),
|
|
2525
2596
|
Text({ label: '要 @ 的 B 站昵称', value: owName, onChange: setOwName, placeholder: '评论里 @ 的就是这个名字', wide: true,
|
|
2526
|
-
hint: '
|
|
2597
|
+
hint: '要跟 B 站上显示的一字不差(带空格也照填)。没填就 @ 不出来——空的 @ 只是一串没人认领的字符。' }),
|
|
2527
2598
|
Toggle({ label: '发评论时在最前面 @ 你', on: atOwner, onChange: setAtOwner,
|
|
2528
|
-
hint: '关掉就不
|
|
2599
|
+
hint: '关掉就不 @,评论会显得更像自言自语。这个开关改完就生效,不用按保存。' })),
|
|
2600
|
+
// 这组自己的保存键(2026-11-06,用户反馈):填完就地能存,不用再去下面 cookie 那栏找。
|
|
2601
|
+
h('div', { className: 'row', style: { marginTop: '8px', gap: '8px' } },
|
|
2602
|
+
h('button', { onClick: saveOwnerIdentity, disabled: busy === 'owid' },
|
|
2603
|
+
busy === 'owid' ? '保存中…' : '保存这两格'),
|
|
2604
|
+
h('span', { className: 'hint' }, '只存上面填了的格子,不动下面那份 cookie;留空的不改原值。'),
|
|
2605
|
+
owIdNote ? h('span', { className: 'dim' }, owIdNote) : null)),
|
|
2529
2606
|
group('B 站登录',
|
|
2530
2607
|
'她刷视频、发评论用的账号,建议放一个小号。没存也能看视频,只是不能说话。',
|
|
2531
2608
|
// 两个入口:扫码是给人用的,粘贴是退路(扫码不顺利、或者想换个号)。
|
|
@@ -2536,7 +2613,7 @@ window.__ModuleLoader__.load({
|
|
|
2536
2613
|
|
|
2537
2614
|
h('div', { className: 'dim', style: { marginBottom: '8px' } },
|
|
2538
2615
|
ow?.hasCookie
|
|
2539
|
-
? `已存${ow?.account?.name ? `,登录账号:${ow.account.name}${ow.account.mid ? `(uid ${ow.account.mid})` : ''}` : '
|
|
2616
|
+
? `已存${ow?.account?.name ? `,登录账号:${ow.account.name}${ow.account.mid ? `(uid ${ow.account.mid})` : ''}` : '(点下面的「测试 cookie 能不能说话」认一下是哪个账号)'}${ow.hasCsrf ? '' : ',但少了 bili_jct(不能发评论)'}。换一份就重新登。`
|
|
2540
2617
|
: '还没存。'),
|
|
2541
2618
|
|
|
2542
2619
|
owTab === 'qr'
|
|
@@ -2551,7 +2628,7 @@ window.__ModuleLoader__.load({
|
|
|
2551
2628
|
qrNote ? h('span', { className: 'qrerr', style: { margin: 0 } }, qrNote) : null))
|
|
2552
2629
|
: h('div', null,
|
|
2553
2630
|
h('p', { className: 'hint', style: { margin: '0 0 8px' } },
|
|
2554
|
-
'cookie
|
|
2631
|
+
'cookie 存好之后为隐私起见不再回显,输入框永远是空的——空着不代表没存上,上面那行「已存…」才是凭据。'),
|
|
2555
2632
|
h('textarea', {
|
|
2556
2633
|
rows: 3,
|
|
2557
2634
|
value: owCookie,
|
|
@@ -2562,13 +2639,18 @@ window.__ModuleLoader__.load({
|
|
|
2562
2639
|
String(ow?.hint ?? '').split('\\n').map((line, i) => h('div', { key: i }, line))),
|
|
2563
2640
|
h('div', { className: 'row', style: { marginTop: '8px' } },
|
|
2564
2641
|
h('button', { onClick: saveOwnerCard, disabled: busy === 'owner' },
|
|
2565
|
-
busy === 'owner' ? '保存中…' : '保存'),
|
|
2566
|
-
h('
|
|
2567
|
-
busy === 'owtest' ? '测试中…' : '测试 cookie 能不能说话'),
|
|
2568
|
-
h('span', { className: 'hint' }, '只验下面那份 cookie,不保存、不动上面两格;两格要改就点「保存」。'))),
|
|
2642
|
+
busy === 'owner' ? '保存中…' : '保存 cookie'),
|
|
2643
|
+
h('span', { className: 'hint' }, '「保存 cookie」会顺带存一次上面填了的格子;只想改那两格就点上面那组里的「保存这两格」。'))),
|
|
2569
2644
|
|
|
2570
2645
|
h('div', { className: 'row', style: { marginTop: '8px' } },
|
|
2571
2646
|
owNote ? h('span', { className: 'dim' }, owNote) : null,
|
|
2647
|
+
// 测试键原来只长在「粘贴 cookie」页签里(issue #52):扫码登进来的人
|
|
2648
|
+
// 想看「这份 cookie 还能不能说话」得先翻页签,上面那行「点测试认一下是哪个账号」
|
|
2649
|
+
// 更是指向一颗不在当前页签上的按钮。挪到两组共用的这一行,两个页签都点得到。
|
|
2650
|
+
ow?.hasCookie
|
|
2651
|
+
? h('button', { onClick: testOwner, disabled: busy === 'owtest' },
|
|
2652
|
+
busy === 'owtest' ? '测试中…' : '测试 cookie 能不能说话')
|
|
2653
|
+
: null,
|
|
2572
2654
|
ow?.hasCookie
|
|
2573
2655
|
? h('button', {
|
|
2574
2656
|
disabled: busy === 'owclear',
|
|
@@ -2577,6 +2659,9 @@ window.__ModuleLoader__.load({
|
|
|
2577
2659
|
clearLogin()
|
|
2578
2660
|
},
|
|
2579
2661
|
}, busy === 'owclear' ? '清除中…' : '清除登录')
|
|
2662
|
+
: null,
|
|
2663
|
+
ow?.hasCookie
|
|
2664
|
+
? h('span', { className: 'hint' }, '测试只验这份 cookie,不保存、也不动上面那两格。')
|
|
2580
2665
|
: null)),
|
|
2581
2666
|
|
|
2582
2667
|
// ---- 以前看了什么(视频观看历史,分页) ----
|
|
@@ -2658,7 +2743,7 @@ window.__ModuleLoader__.load({
|
|
|
2658
2743
|
'一个侧栏会话聊到几百轮,上下文必然撑爆——不是变慢,是每轮都为几十万字付费,最后直接失败。',
|
|
2659
2744
|
h('div', { className: 'grid2' },
|
|
2660
2745
|
Num({ label: '聊满多少轮换新会话', value: rotateAfter, onChange: setRotateAfter, min: 0, max: 500, unit: '轮',
|
|
2661
|
-
hint: '请在不断的循环中,找到能引发本质变化的唯一解。0 =
|
|
2746
|
+
hint: '请在不断的循环中,找到能引发本质变化的唯一解。0 = 永不换——那条会话会一直聊到宿主上下文撑不住为止,之后这一轮会直接失败(不会自动换)。换的时候会把当前已形成的核心认知和初始信息,作为提示词带到新会话。' }),
|
|
2662
2747
|
Num({ label: '单次上下文限制(身份文档多少字)', value: docBudget, onChange: setDocBudget, min: 600, max: 4000, step: 100, unit: '字',
|
|
2663
2748
|
hint: '超了按照规则遗忘部分认知/经历;「初始人格」和四条禁令不会遗忘。根据经历不同,部分认知可能会变成永久记忆不再遗忘。' }))),
|
|
2664
2749
|
|
|
@@ -2705,7 +2790,9 @@ window.__ModuleLoader__.load({
|
|
|
2705
2790
|
mem.sections?.voiceLast
|
|
2706
2791
|
? h('div', { className: 'dim' }, ` 最近读到:「${mem.sections.voiceLast.text}」${mem.sections.voiceLast.echoed ? '(她认下了)' : ''}`)
|
|
2707
2792
|
: null,
|
|
2708
|
-
|
|
2793
|
+
// issue #32:口径同面板标题那行——含她读到的别人说法与开局起点,不只她写的。
|
|
2794
|
+
h('div', { title: '她自己写的判断、留话、日记,加上她读到并记下的别人说法和开局那份起点;不含系统后台记账。' },
|
|
2795
|
+
`· 记下的事:${mem.events ?? 0} 条 · 日记 ${mem.diaries ?? 0} 篇`),
|
|
2709
2796
|
h('div', null, `· 这条会话:${mem.session?.rotateAfter > 0
|
|
2710
2797
|
? `${mem.session?.turns ?? 0} / ${mem.session.rotateAfter} 轮,满了自动换新会话`
|
|
2711
2798
|
: '不自动换'}`),
|
|
@@ -3311,6 +3398,7 @@ window.__ModuleLoader__.load({
|
|
|
3311
3398
|
async function doDecideKeepsake(id, yes) {
|
|
3312
3399
|
setOutingBusy(true)
|
|
3313
3400
|
setOutingMsg(yes ? '正在带回来……' : '正在回绝……')
|
|
3401
|
+
const refreshOuting = () => jget('/outing').then(setOuting).catch(() => {})
|
|
3314
3402
|
try {
|
|
3315
3403
|
const r = await jpost('/outing', { action: 'keepsake', id, yes })
|
|
3316
3404
|
setOuting(await jget('/outing').catch(() => outing))
|
|
@@ -3321,6 +3409,8 @@ window.__ModuleLoader__.load({
|
|
|
3321
3409
|
)
|
|
3322
3410
|
} catch (e) {
|
|
3323
3411
|
setOutingMsg(`没处理成:${e.message}`)
|
|
3412
|
+
// 多半是别的地方先答了:刷新一下,别还留着这一行
|
|
3413
|
+
refreshOuting()
|
|
3324
3414
|
} finally {
|
|
3325
3415
|
setOutingBusy(false)
|
|
3326
3416
|
}
|
|
@@ -4082,7 +4172,14 @@ window.__ModuleLoader__.load({
|
|
|
4082
4172
|
|
|
4083
4173
|
return h('div', { className: 'dfish', ref: (el) => { topEl = el } },
|
|
4084
4174
|
h('h2', null, '大肥鱼'),
|
|
4085
|
-
h('p', {
|
|
4175
|
+
h('p', {
|
|
4176
|
+
className: 'sub',
|
|
4177
|
+
// issue #32:这个数字(counts.total = countEvents().real)从来不只是「她写的」——
|
|
4178
|
+
// 她读到的别人说法、开局那份起点、日记、答你的话都在里面。以前文案写「她记下
|
|
4179
|
+
// N 条」,跟面板上「记下的事」一起被当成了「她写了 N 条」,于是永远对不上。
|
|
4180
|
+
// 口径不改(改 real 的定义会动事件流语义与一堆派生数字),把话说清楚。
|
|
4181
|
+
title: '她自己写的判断、留话、日记,加上她读到并记下的别人说法和开局那份起点;不含系统后台记账(另计)。',
|
|
4182
|
+
},
|
|
4086
4183
|
s ? `第 ${s.day} / ${s.growthDays} 天 · ${s.stageName} · 她记下 ${s.counts.total} 条${s.counts.meta ? `,另有 ${s.counts.meta} 条系统记录` : ''}` : '读取中…'),
|
|
4087
4184
|
err ? h('pre', { className: 'err' }, err) : null,
|
|
4088
4185
|
note ? h('pre', { className: 'ok' }, note) : null,
|