dsh-fish-soul 0.15.55 → 0.16.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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  让一个 QQ 小号背后有"同一个人":固定人格、按人记记忆、有自己的生活节奏、会主动开口;同一个"她"同时出现在 QQ、B站、抖音。
4
4
 
5
- DSH 插件(单角色 `feiyu`)。版本 **0.15.55**。
5
+ DSH 插件(单角色 `feiyu`)。版本 **0.16.0**。
6
6
 
7
7
  QQ 交流群:**1125962189**
8
8
 
@@ -165,7 +165,7 @@ MIT
165
165
 
166
166
  在「记忆检视」的「周记」中,可手动生成或开启自动生成。默认关闭;开启或手动运行前会提示:该功能使用「写日记」配置的模型,记忆内容(包括私密内容)可能发送至远程模型并产生调用费用。每次最多处理 20 条已结束自然周的记忆,按人物、会话和隐私范围分批;模型失败不会更改原记忆。
167
167
 
168
- 周记是带来源的派生记忆,不会删除原文,也不等于释放磁盘空间。可在记忆检视中查看来源;原记忆内容、范围、隐私或状态变更后,关联周记自动失效且不能恢复,已处理原文不会自动重新生成。数据库备份保留周记及其来源关系;普通 JSON 记忆导入不恢复派生周记,请使用数据库备份恢复。
168
+ 周记是带来源的派生记忆,不会删除原文,也不等于释放磁盘空间。在「记忆检视 → 记忆内容」的来源筛选里选择「周记」查看正文和来源;原记忆内容、范围、隐私或状态变更后,关联周记自动失效且不能恢复,已处理原文不会自动重新生成。数据库备份保留周记及其来源关系;普通 JSON 记忆导入不恢复派生周记,请使用数据库备份恢复。
169
169
 
170
170
  ### 存档与容量管理
171
171
 
@@ -175,6 +175,28 @@ window.__ModuleLoader__.load({
175
175
  })))), confirmNode) // #82:confirm() 的对话框要挂进这棵树,否则点「删除这张卡」只是 pending 了、界面什么都不出;
176
176
  }
177
177
 
178
+ // 她自己没有人物关系卡;只展示自己的已删除 / 过时记忆。
179
+ function SelfArchive({ ui, api, notify, onChanged }) {
180
+ const { h, Group, Item, Button, Empty, Muted } = ui;
181
+ const [page, setPage] = react.useState(0);
182
+ const [data, load] = usePoll(api, `/inspector/self-outdated?offset=${page * 30}`, 0);
183
+ if (!data) return null;
184
+ async function restore(m) {
185
+ const r = await api("POST", "/inspector/memory-restore", { id: m.id });
186
+ if (!r?.ok) { notify(r?.error ?? "恢复失败"); return; }
187
+ notify(`已恢复 #${m.id}`); onChanged();
188
+ if (data.items.length === 1 && page > 0) setPage(page - 1); else await load();
189
+ }
190
+ return Group({ title: "她自己的改掉 / 过时的", aside: Muted(`${data.total} 条`) },
191
+ data.items.length === 0 ? Empty("没有旧记忆") : h("div", { className: "fs-list" }, data.items.map((m) => Item({
192
+ key: m.id, lead: ui.Tag(m.status === "outdated" ? "过时" : m.status === "superseded" ? "改掉" : "删了", "outline"),
193
+ text: m.content, title: `${m.content}\n${m.evidence || m.source}`,
194
+ meta: m.kind === "weekly" ? [Muted("失效周记不能恢复")] : [Button({ key: "r", variant: "ghost", onClick: () => restore(m) }, "恢复")]
195
+ }))),
196
+ h("div", { className: "fs-row" }, Button({ variant: "ghost", disabled: page === 0, onClick: () => setPage(page - 1) }, "上一页"),
197
+ Muted(`第 ${page + 1} 页`), Button({ variant: "ghost", disabled: (page + 1) * 30 >= data.total, onClick: () => setPage(page + 1) }, "下一页")));
198
+ }
199
+
178
200
  // ── 导入聊天记录 / 备份 ──
179
201
  function ImportBackup({ ui, api, notify, relTime, onDone }) {
180
202
  const { h, Row, Group, Input, Num, Button, Switch, State, Muted, Tag, useConfirm } = ui;
@@ -389,6 +411,7 @@ window.__ModuleLoader__.load({
389
411
  return h(react.Fragment, null,
390
412
  Group({ title: "周记" },
391
413
  Row({ title: "自动生成", meta: "按周整理,保留原记忆;使用写日记模型" }, h(Switch, { label: "自动生成周记", checked: data.enabled, disabled: busy, onChange: setAuto })),
414
+ Row({ title: "查看周记", meta: "在记忆内容的来源筛选中选择「周记」,可查看正文和来源" }),
392
415
  Row({ title: "待整理" }, Muted(`${data.pending} 条`), Button({ disabled: busy || data.running || !data.pending, onClick: run }, busy || data.running ? "生成中" : "生成一批"))),
393
416
  confirmNode);
394
417
  }
@@ -489,7 +512,7 @@ window.__ModuleLoader__.load({
489
512
  const r = await api("POST", "/inspector/bulk", { ...target, action, value });
490
513
  if (r?.ok) { notify(`${label}:${r.changed} 条`); setSel(new Set()); load(); } else notify(r?.error ?? "操作失败");
491
514
  }
492
- const SOURCES = [["", "所有来源"], ["chat", "聊天里记的"], ["history-import", "导入(聊天记录)"], ["import", "导入(记忆文件)"], ["manual", "主人手动加的"]];
515
+ const SOURCES = [["", "所有来源"], ["chat", "聊天里记的"], ["history-import", "导入(聊天记录)"], ["import", "导入(记忆文件)"], ["manual", "主人手动加的"], ["weekly", "周记"]];
493
516
  const n = sel.size;
494
517
  const bulkBar = !bulk ? null : h("div", { className: "fs-row" },
495
518
  h("div", { className: "fs-ctl", style: { flexWrap: "wrap", gap: "6px" } },
@@ -510,8 +533,8 @@ window.__ModuleLoader__.load({
510
533
  section === "people" ? Group({ title: "人物与关系" },
511
534
  Row({ title: "选择人物" }, ui.Select({ value: owner, "aria-label": "选择人物", onChange: (e) => setOwner(e.target.value) },
512
535
  h("option", { value: "all" }, "请选择"),
513
- ...persons.filter((p) => p.ownerId !== "self").map((p) => h("option", { key: p.ownerId, value: p.ownerId }, `${p.displayName} · ${p.memoryCount} 条`)))),
514
- owner !== "all" && owner !== "self" ? h(PersonCard, { key: owner, ownerId: owner, ui, api, notify, onChanged: load }) : Empty("选一个人,查看关系、称呼和印象")) : null,
536
+ ...persons.map((p) => h("option", { key: p.ownerId, value: p.ownerId }, `${p.ownerId === "self" ? "她自己" : p.displayName} · ${p.memoryCount} 条`)))),
537
+ owner === "self" ? h(SelfArchive, { ui, api, notify, onChanged: load }) : owner !== "all" ? h(PersonCard, { key: owner, ownerId: owner, ui, api, notify, onChanged: load }) : Empty("选一个人,查看关系、称呼和印象")) : null,
515
538
  section === "recall-test" ? Group({ title: "检索测试" },
516
539
  Row({ title: "选择人物" }, ui.Select({ value: owner, "aria-label": "测试哪个人", onChange: (e) => setOwner(e.target.value) },
517
540
  h("option", { value: "all" }, "请选择"),
@@ -748,6 +771,18 @@ window.__ModuleLoader__.load({
748
771
  return h(react.Fragment, null, groups.map((g) => h(SlangGroupCard, { key: `${g.channelId}-${g.learnedAt ?? 0}-${(g.terms ?? []).length}-${(g.expressions ?? []).length}`, g, api, notify, relTime, ui, onChanged: load })));
749
772
  }
750
773
 
774
+ // 未提供召回诊断的事件(如要回、心情、记下、重发)不能被推断为「没组装」。
775
+ function recallStatus(d) {
776
+ if (!d.recallChecked && !d.recallAt && !d.recallMiss) return null;
777
+ const missWhy = d.recallMiss === "notMounted"
778
+ ? "瞬态块压根没挂上(这个会话的人格卡/上下文注册失败,插件启动时可能没挂成)"
779
+ : d.recallMiss === "noSession"
780
+ ? "瞬态块跑了但找不到这个会话的登记(重启后 agent 复用了旧 id,或这一轮没走我们的 beginTurn)"
781
+ : "这一轮没组装记忆(瞬态块没跑)";
782
+ return { badge: !d.recallAt ? "记忆没进这轮" : d.speakerUnknown ? "认不出是谁" : d.memories?.length ? `想起 ${d.memories.length}` : "没召回到",
783
+ tone: !d.recallAt || d.speakerUnknown ? "warning" : null, missWhy: !d.recallAt ? missWhy : "" };
784
+ }
785
+
751
786
  function ActivityPane({ api, apiBase, notify, relTime, ui, section = "activity" }) {
752
787
  const { h, Group, Tag, Empty, Button, Muted } = ui;
753
788
  const [data, load] = usePoll(api, "/activity", 3000);
@@ -769,26 +804,16 @@ window.__ModuleLoader__.load({
769
804
  h("div", { className: "fs-steps" }, decisions.length === 0 ? Empty("还没有动静") : decisions.map((d, i) => {
770
805
  const [label, tone] = KINDS[d.kind] ?? [d.kind, "outline"];
771
806
  const body = d.text ? [d.who && `${d.who}:`, d.text].filter(Boolean).join("") : d.who;
772
- // #78:这一轮记忆到底有没有参与。以前「没跑 / 认不出人 / 跑了没召回到」都是什么都不显示,
773
- // 于是「她怎么想不起来」在界面上查不出来
774
- // miss 由后端算好(notMounted / noSession),别在这里猜是哪一种
775
- const missWhy = d.recallMiss === "notMounted"
776
- ? "瞬态块压根没挂上(这个会话的人格卡/上下文注册失败,插件启动时可能没挂成)"
777
- : d.recallMiss === "noSession"
778
- ? "瞬态块跑了但找不到这个会话的登记(重启后 agent 复用了旧 id,或这一轮没走我们的 beginTurn)"
779
- : "这一轮没组装记忆(瞬态块没跑)";
780
- const recallBadge = !d.recallAt ? "记忆没进这轮" : d.speakerUnknown ? "认不出是谁" : d.memories?.length ? `想起 ${d.memories.length}` : "没召回到";
781
- const recallTone = !d.recallAt || d.speakerUnknown ? "warning" : null;
807
+ const recall = recallStatus(d);
782
808
  const tip = [d.title, body, d.reason, d.memories?.length ? `想起了:\n${d.memories.join("\n")}` : "",
783
- !d.recallAt ? missWhy : ""].filter(Boolean).join("\n");
809
+ recall?.missWhy].filter(Boolean).join("\n");
784
810
  const more = Boolean((body && body.length > 36) || d.reason || d.memories?.length);
785
811
  const head = [
786
812
  h("span", { className: "fs-step-dot", "data-tone": tone, key: "d", "aria-hidden": true }),
787
813
  h("span", { className: "fs-step-kind", key: "k" }, label),
788
814
  h("span", { className: "fs-item-main", key: "m", title: tip }, h("b", { className: "fs-strong" }, d.title), body ? ` ${body}` : ""),
789
815
  h("span", { className: "fs-item-meta", key: "t" },
790
- d.kind === "sent" || d.kind === "silent" || d.kind === "failed" || d.kind === "unconfirmed"
791
- ? (recallTone ? Tag(recallBadge, recallTone) : Muted(recallBadge)) : null,
816
+ recall ? (recall.tone ? Tag(recall.badge, recall.tone) : Muted(recall.badge)) : null,
792
817
  h("span", { className: "num" }, relTime(d.at, now)))];
793
818
  if (!more) return h("div", { className: "fs-step", key: `${d.at}-${i}` }, h("div", { className: "fs-step-head" }, ...head));
794
819
  return h("details", { className: "fs-step", key: `${d.at}-${i}` },
package/lib/db.js CHANGED
@@ -206,6 +206,7 @@ const MIGRATIONS = [
206
206
  "ALTER TABLE memory ADD COLUMN importance_locked INTEGER NOT NULL DEFAULT 0",
207
207
  "ALTER TABLE memory ADD COLUMN importance_auto_recalled INTEGER NOT NULL DEFAULT 0",
208
208
  'CREATE INDEX IF NOT EXISTS idx_memory_about ON memory(character_id, about, status)',
209
+ 'CREATE INDEX IF NOT EXISTS idx_memory_superseded_by ON memory(superseded_by) WHERE superseded_by IS NOT NULL',
209
210
  // v0.8:好感度、上次结算时间、上一次来聊的时间(隔了 1 小时以上才算新的一次)、主人手动定的阶段不自动改
210
211
  'ALTER TABLE person ADD COLUMN affinity REAL',
211
212
  'ALTER TABLE person ADD COLUMN affinity_at INTEGER',
package/lib/gateway.js CHANGED
@@ -275,7 +275,7 @@ export class ChatGateway {
275
275
  }
276
276
 
277
277
  /** 记一条"动态":kind = reply / memorize / interject / sent / silent / failed / paused / recalled / command … */
278
- note(kind, { channelId = '', who = '', text = '', reason = '', title = '', memories, recallAt = 0, speakerUnknown = false, recallMiss = '' } = {}) {
278
+ note(kind, { channelId = '', who = '', text = '', reason = '', title = '', memories, recallAt = null, speakerUnknown = false, recallMiss = '' } = {}) {
279
279
  this.decisions.unshift({
280
280
  at: Date.now(), kind, channelId, title: title || this.titleOf(channelId),
281
281
  who: String(who ?? '').slice(0, 24), text: String(text ?? '').replace(/\s+/g, ' ').slice(0, 80), reason,
@@ -284,6 +284,7 @@ export class ChatGateway {
284
284
  ...(Array.isArray(memories) && memories.length > 0 ? { memories: memories.slice(0, Math.min(20, Math.max(5, Number(this.getConfig?.()?.retrieval?.topK) || 5))) } : {}),
285
285
  // #78:这一轮记忆到底有没有参与。没跑 / 认不出人 / 跑了没召回到,界面要分得开 ——
286
286
  // 以前三种都是"什么都不显示",看不出问题在哪儿
287
+ ...(recallAt !== null ? { recallChecked: true } : {}),
287
288
  ...(Number(recallAt) > 0 ? { recallAt: Number(recallAt), speakerUnknown: speakerUnknown === true } : {}),
288
289
  // 没跑时带上原因(notMounted = 瞬态块压根没挂上;noSession = 挂了但找不到会话登记)
289
290
  ...(Number(recallAt) > 0 || recallMiss === '' ? {} : { recallMiss: String(recallMiss) })
@@ -1444,7 +1445,7 @@ export class ChatGateway {
1444
1445
  : '这一轮不是我们发的,结果不确定';
1445
1446
  // 冷路径:这一轮没正常结束 —— 记下原因。「私信变待核实」时能看出是哪一类:
1446
1447
  // 结束原因不是 completed(provider 异常 / 被打断),还是这轮压根不是我们发的。
1447
- this.diag?.add(`turn-end-failed:${st.channelId ?? ''}`, {
1448
+ if (!completed || !st.dshTyped) this.diag?.add(`turn-end-failed:${st.channelId ?? ''}`, {
1448
1449
  sessionId,
1449
1450
  reasonKind: String(d.reason?.kind ?? ''),
1450
1451
  fromUs: st.fromUs === true,
@@ -8,10 +8,11 @@
8
8
  */
9
9
  export const KINDS = ['fact', 'event', 'feeling', 'episode'];
10
10
  export const SOURCES = {
11
- chat: "COALESCE(source_session, '') NOT IN ('import', 'history-import', 'manual', '')",
11
+ chat: "COALESCE(source_session, '') NOT IN ('import', 'history-import', 'manual', 'weekly', '')",
12
12
  import: "source_session = 'import'",
13
13
  'history-import': "source_session = 'history-import'",
14
- manual: "source_session = 'manual'"
14
+ manual: "source_session = 'manual'",
15
+ weekly: "kind = 'weekly'"
15
16
  };
16
17
  const MAX_IDS = 1000;
17
18
  const MAX_FILTER = 5000;
@@ -67,6 +67,15 @@ export async function handleSoulRoute({ method, path, url, req, res, send, readB
67
67
  };
68
68
  const guard = () => { if (!sameOrigin(req)) { send(403, { ok: false, error: 'forbidden' }); return false; } return true; };
69
69
 
70
+ // 鱼自己的记忆没有 person 行,单独读取旧档,不伪造人物或关系。
71
+ if (path === '/inspector/self-outdated' && method === 'GET') {
72
+ const offset = Math.min(1000000, Math.max(0, Math.floor(Number(url.searchParams.get('offset')) || 0)));
73
+ const where = "character_id = 'feiyu' AND owner_id = 'self' AND status IN ('outdated', 'superseded', 'archived')";
74
+ const total = db.prepare(`SELECT COUNT(*) AS n FROM memory WHERE ${where}`).get().n;
75
+ const items = db.prepare(`SELECT * FROM memory WHERE ${where} ORDER BY updated_at DESC, id DESC LIMIT 30 OFFSET ?`).all(offset).map(memoryView);
76
+ send(200, { ok: true, total, items }); return true;
77
+ }
78
+
70
79
  // ── 人物画像 ──
71
80
  if (path === '/inspector/person' && method === 'GET') {
72
81
  const ownerId = String(url.searchParams.get('ownerId') ?? '');
package/lib/routes.js CHANGED
@@ -403,7 +403,12 @@ export function registerRoutes(ctx, deps) {
403
403
  GROUP BY owner_id
404
404
  `).all().map((r) => [r.owner_id, r.n])
405
405
  );
406
- const memRows = db.prepare(`SELECT * FROM memory WHERE ${listFilter.sql} ORDER BY created_at DESC LIMIT ${listLimit}`).all(...listFilter.params);
406
+ // 同一创建时间的记忆按最早版本的 id 排序:编辑会新建 id,不能因此换位置。
407
+ // UNION 防止旧数据中的异常环造成递归不结束;索引支持按 superseded_by 反查。
408
+ const memRows = db.prepare(`SELECT * FROM memory WHERE ${listFilter.sql} ORDER BY created_at DESC,
409
+ (WITH RECURSIVE ancestors(id) AS (
410
+ SELECT memory.id UNION SELECT old.id FROM memory old JOIN ancestors a ON old.superseded_by = a.id
411
+ ) SELECT MIN(id) FROM ancestors) ASC, id ASC LIMIT ${listLimit}`).all(...listFilter.params);
407
412
  const matchedTotal = db.prepare(`SELECT COUNT(*) AS n FROM memory WHERE ${listFilter.sql}`).get(...listFilter.params).n;
408
413
  const vecGenById = new Map(
409
414
  db.prepare('SELECT memory_id, generation_id FROM vec').all().map((r) => [r.memory_id, r.generation_id])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-fish-soul",
3
- "version": "0.15.55",
3
+ "version": "0.16.0",
4
4
  "description": "鱼 · 人格、按人记忆(可纠正、有印象、能导入旧聊天记录)、日程自走与主动开口,直连 SnowLuma 收发 QQ(群里会自己插话),B站 / 抖音小号私信与自己刷视频;按功能选模型并统计用量花费,能听会说 QQ 语音、收藏表情包、学群里的梗,可选上网查;心情影响说话、去 AI 味、每个群的设定、按聊天选模型、每晚写日记、每周记忆存档与可选周记(DSH 插件,单角色 feiyu ,v0.15.37)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -64,7 +64,7 @@
64
64
  }
65
65
  },
66
66
  "scripts": {
67
- "test": "node test/safe-download.mjs && node test/douyin-exact-id.mjs && node test/douyin-pb-init.mjs && node test/douyin-conversation.mjs && node test/douyin-inbox.mjs && node test/douyin-quarantine.mjs && node test/douyin-send-target.mjs && node test/douyin-send-exclusive.mjs && node test/douyin-gap-dedupe.mjs && node test/douyin-read-fail-retry.mjs && node test/douyin-read-cache.mjs && node test/douyin-dm-media-conv.mjs && node test/douyin-read-fail-retry-poll.mjs && node test/douyin-inbox-contract.mjs && node test/person-attribution.mjs && node test/memory-owner-reassign.mjs && node test/douyin-poll-and-timer-safety.mjs && node test/p2-limits.mjs && node test/resource-safety.mjs && node test/leftover-edge-cases.mjs && node test/douyin-master-uid.mjs && node test/douyin-identity-e2e.mjs && node test/douyin-post-jobs.mjs && node test/retrieval-filtered-vectors.mjs && node test/security-boundaries.mjs && node test/douyin-cdp-cursor.mjs && node test/smoke.mjs && node test/douyin-browser.mjs && node test/douyin-music.mjs && node test/startup-gate.mjs && node test/outbox.mjs && node test/import-coverage.mjs && node test/update-check.mjs && node test/issue-fixes.mjs && node test/import-resume.mjs && node test/maibot-extras.mjs && node test/worldinfo.mjs && node test/memory-bulk.mjs && node test/memory-tiers.mjs && node test/memory-weekly.mjs && node test/memory-storage.mjs && node test/memory-storage-restore.mjs && node test/backup-restore.mjs && node test/douyin-group.mjs && node test/review-fixes.mjs && node test/at-reads-video.mjs && node test/search-source.mjs && node test/followups.mjs && node test/detail-config.mjs && node test/qq-send-any-image.mjs && node test/qq-read-attachment.mjs && node test/qq-batch-window.mjs && node test/comment-video.mjs && node test/frames.mjs && node test/face-name.mjs && node test/comment-reply.mjs && node test/chat-comment.mjs && node test/at-target.mjs && node test/quota-caps.mjs && node test/client-lint.mjs && node test/client-panels.mjs && node test/dsh-desktop.mjs && node test/issue-59-interject.mjs && node test/issue-59-diagnose.mjs && node test/sticker-manage.mjs && node test/bili-emotes.mjs && node test/qq-outbound-sanitize.mjs && node test/person-delete.mjs && node test/qq-group-admin.mjs && node test/douyin-recovery.mjs && node test/session-preset.mjs && node test/qq-connect-error.mjs && node test/douyin-comment-bridge.mjs && node test/bili-comment-emoji.mjs && node test/douyin-state-echo.mjs && node test/issue-75-proactive.mjs && node test/issue-76-capability.mjs && node test/douyin-http-browse.mjs && node test/douyin-verify-backoff.mjs && node test/douyin-digg-bridge.mjs && node test/memory-evidence-transcript.mjs && node test/chat-archive-default.mjs && node test/dream-transcript-wire.mjs && node test/douyin-frames-collect.mjs && node test/master-qq-uin-rebind.mjs && node test/douyin-session-health.mjs && node test/douyin-delivered.mjs && node test/douyin-card-share.mjs && node test/douyin-push-drop.mjs && node test/douyin-stranger-dm.mjs && node test/douyin-uid-binding.mjs && node test/image-webp-fallback.mjs && node test/douyin-emoji-fallback.mjs && node test/relay-other-platform.mjs && node test/voice-volcengine.mjs && node test/voice-minimax-dashscope.mjs && node test/bili-switch-persist.mjs && node test/cross-channel-context.mjs && node test/reply-target.mjs && node test/shared-session.mjs && node test/memory-source-label.mjs && node test/pending-delivery.mjs && node test/recall-window-78.mjs && node test/client-syntax.mjs && node test/diag-log.mjs && node test/check-rings.mjs && node test/sticker-import-gif.mjs && node test/sticker-real-gif.mjs && node test/sticker-upload-binary.mjs && node test/sticker-vision-tags.mjs && node test/sticker-manual-tags.mjs && node test/sticker-auto-name.mjs && node test/self-update.mjs && node test/group-list.mjs && node test/douyin-inbox-noise.mjs && node test/douyin-inbox-diag.mjs && node test/douyin-inbox-stuck.mjs && node test/reply-window.mjs && node test/douyin-sticker-heuristic.mjs",
67
+ "test": "node test/safe-download.mjs && node test/douyin-exact-id.mjs && node test/douyin-pb-init.mjs && node test/douyin-conversation.mjs && node test/douyin-inbox.mjs && node test/douyin-quarantine.mjs && node test/douyin-send-target.mjs && node test/douyin-send-exclusive.mjs && node test/douyin-gap-dedupe.mjs && node test/douyin-read-fail-retry.mjs && node test/douyin-read-cache.mjs && node test/douyin-dm-media-conv.mjs && node test/douyin-read-fail-retry-poll.mjs && node test/douyin-inbox-contract.mjs && node test/person-attribution.mjs && node test/memory-owner-reassign.mjs && node test/douyin-poll-and-timer-safety.mjs && node test/p2-limits.mjs && node test/resource-safety.mjs && node test/leftover-edge-cases.mjs && node test/douyin-master-uid.mjs && node test/douyin-identity-e2e.mjs && node test/douyin-post-jobs.mjs && node test/retrieval-filtered-vectors.mjs && node test/security-boundaries.mjs && node test/douyin-cdp-cursor.mjs && node test/smoke.mjs && node test/douyin-browser.mjs && node test/douyin-music.mjs && node test/startup-gate.mjs && node test/outbox.mjs && node test/import-coverage.mjs && node test/update-check.mjs && node test/issue-fixes.mjs && node test/import-resume.mjs && node test/maibot-extras.mjs && node test/worldinfo.mjs && node test/memory-bulk.mjs && node test/memory-tiers.mjs && node test/memory-weekly.mjs && node test/issue-98-self-archive.mjs && node test/issue-99-memory-order.mjs && node test/issue-78-diagnostic.mjs && node test/memory-storage.mjs && node test/memory-storage-restore.mjs && node test/backup-restore.mjs && node test/douyin-group.mjs && node test/review-fixes.mjs && node test/at-reads-video.mjs && node test/search-source.mjs && node test/followups.mjs && node test/detail-config.mjs && node test/qq-send-any-image.mjs && node test/qq-read-attachment.mjs && node test/qq-batch-window.mjs && node test/comment-video.mjs && node test/frames.mjs && node test/face-name.mjs && node test/comment-reply.mjs && node test/chat-comment.mjs && node test/at-target.mjs && node test/quota-caps.mjs && node test/client-lint.mjs && node test/client-panels.mjs && node test/dsh-desktop.mjs && node test/issue-59-interject.mjs && node test/issue-59-diagnose.mjs && node test/sticker-manage.mjs && node test/bili-emotes.mjs && node test/qq-outbound-sanitize.mjs && node test/person-delete.mjs && node test/qq-group-admin.mjs && node test/douyin-recovery.mjs && node test/session-preset.mjs && node test/qq-connect-error.mjs && node test/douyin-comment-bridge.mjs && node test/bili-comment-emoji.mjs && node test/douyin-state-echo.mjs && node test/issue-75-proactive.mjs && node test/issue-76-capability.mjs && node test/douyin-http-browse.mjs && node test/douyin-verify-backoff.mjs && node test/douyin-digg-bridge.mjs && node test/memory-evidence-transcript.mjs && node test/chat-archive-default.mjs && node test/dream-transcript-wire.mjs && node test/douyin-frames-collect.mjs && node test/master-qq-uin-rebind.mjs && node test/douyin-session-health.mjs && node test/douyin-delivered.mjs && node test/douyin-card-share.mjs && node test/douyin-push-drop.mjs && node test/douyin-stranger-dm.mjs && node test/douyin-uid-binding.mjs && node test/image-webp-fallback.mjs && node test/douyin-emoji-fallback.mjs && node test/relay-other-platform.mjs && node test/voice-volcengine.mjs && node test/voice-minimax-dashscope.mjs && node test/bili-switch-persist.mjs && node test/cross-channel-context.mjs && node test/reply-target.mjs && node test/shared-session.mjs && node test/memory-source-label.mjs && node test/pending-delivery.mjs && node test/recall-window-78.mjs && node test/client-syntax.mjs && node test/diag-log.mjs && node test/check-rings.mjs && node test/sticker-import-gif.mjs && node test/sticker-real-gif.mjs && node test/sticker-upload-binary.mjs && node test/sticker-vision-tags.mjs && node test/sticker-manual-tags.mjs && node test/sticker-auto-name.mjs && node test/self-update.mjs && node test/group-list.mjs && node test/douyin-inbox-noise.mjs && node test/douyin-inbox-diag.mjs && node test/douyin-inbox-stuck.mjs && node test/reply-window.mjs && node test/douyin-sticker-heuristic.mjs",
68
68
  "eval": "node test/eval/persona-eval.mjs"
69
69
  }
70
70
  }