dsh-fish-soul 0.15.39 → 0.15.41
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 +1 -1
- package/lib/client.diag.js +107 -0
- package/lib/client.douyin.js +1 -1
- package/lib/client.js +3 -2
- package/lib/diag-log.js +130 -0
- package/lib/gateway.js +26 -4
- package/lib/index.js +33 -9
- package/lib/routes-soul.js +14 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-fish-soul — 设置面板「运行日志」页(client.diag.js,按需加载的包内分块)
|
|
3
|
+
*
|
|
4
|
+
* 主面板 client.js 用 require.async("./client.diag.js") 拿到 DiagPane;
|
|
5
|
+
* api / notify / 控件(props.ui)都由主面板传进来,分块自己不 require 主文件。
|
|
6
|
+
*
|
|
7
|
+
* 🔴 必须走 window.__ModuleLoader__.load 注册:裸 ES module 宿主能加载文件,
|
|
8
|
+
* 但不会注册,设置页报 "loaded without registering"(2026-11-12 真机踩过)。
|
|
9
|
+
*
|
|
10
|
+
* 两个视图共用一份数据:
|
|
11
|
+
* - 默认中文时间线:给用户看,也方便原样贴到 issue;
|
|
12
|
+
* - 「看原始」切 JSON:给维护者 grep。
|
|
13
|
+
*
|
|
14
|
+
* 这里只放**装置层面**的事件(启动卡在哪、人格段挂不上、会话恢复失败、上下文长度)。
|
|
15
|
+
* 对话层面的「谁说了什么、想起了几条」在「日常 → 最近动态」那边 —— 一事实一出处。
|
|
16
|
+
*/
|
|
17
|
+
window.__ModuleLoader__.load({
|
|
18
|
+
id: "dsh-fish-soul",
|
|
19
|
+
chunk: "client.diag.js",
|
|
20
|
+
factory: (require) => {
|
|
21
|
+
const react = require("react");
|
|
22
|
+
|
|
23
|
+
/** 跟 ActivityPane 一样的轮询小工具(各分块页各有一份,不跨块 import)。 */
|
|
24
|
+
function usePoll(api, path, ms) {
|
|
25
|
+
const [data, setData] = react.useState(null);
|
|
26
|
+
const load = react.useCallback(async () => {
|
|
27
|
+
const r = await api("GET", path);
|
|
28
|
+
if (r?.ok) setData(r);
|
|
29
|
+
}, [api, path]);
|
|
30
|
+
react.useEffect(() => {
|
|
31
|
+
let alive = true;
|
|
32
|
+
const run = async () => { if (alive) await load(); };
|
|
33
|
+
run();
|
|
34
|
+
const t = setInterval(run, ms);
|
|
35
|
+
return () => { alive = false; clearInterval(t); };
|
|
36
|
+
}, [load, ms]);
|
|
37
|
+
return [data, load];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function DiagPane({ api, notify, ui }) {
|
|
41
|
+
const { h, Group, Tag, Button, Muted } = ui;
|
|
42
|
+
const [raw, setRaw] = react.useState(false);
|
|
43
|
+
const [data, load] = usePoll(api, "/diag-log", 15000);
|
|
44
|
+
if (!data) return null;
|
|
45
|
+
|
|
46
|
+
const entries = data.entries ?? [];
|
|
47
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
48
|
+
const fmtTime = (ms) => {
|
|
49
|
+
const d = new Date(ms);
|
|
50
|
+
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
51
|
+
};
|
|
52
|
+
// 一行中文:时间 + 说人话的标题 + 细节
|
|
53
|
+
const describe = (e) => {
|
|
54
|
+
const d = e.detail ?? {};
|
|
55
|
+
const bits = [];
|
|
56
|
+
if (d.step !== undefined) bits.push(`第 ${d.step} 步${d.name ? `「${d.name}」` : ""}`);
|
|
57
|
+
if (d.sessionId) bits.push(`会话 ${String(d.sessionId).slice(0, 12)}…`);
|
|
58
|
+
if (d.channelId) bits.push(`聊天 ${d.channelId}`);
|
|
59
|
+
if (d.chars !== undefined) bits.push(`${d.chars} 字${d.budget ? ` / 预算 ${d.budget}` : ""}${d.over80 ? "(超 80%)" : ""}`);
|
|
60
|
+
if (d.reason) bits.push(String(d.reason).slice(0, 120));
|
|
61
|
+
if (e.count > 1) bits.push(`重复 ${e.count} 次`);
|
|
62
|
+
return bits.join(",") || "(没有细节)";
|
|
63
|
+
};
|
|
64
|
+
const levelStyle = (lv) => (lv === "error"
|
|
65
|
+
? { color: "var(--dsw-alias-state-error-primary, #d93025)" }
|
|
66
|
+
: lv === "warn" ? { color: "var(--dsw-alias-state-warning-primary, #b26a00)" } : null);
|
|
67
|
+
|
|
68
|
+
return Group({
|
|
69
|
+
title: "运行日志",
|
|
70
|
+
aside: h("span", { style: { display: "flex", gap: 8, alignItems: "center" } },
|
|
71
|
+
Tag(String(entries.length), "outline"),
|
|
72
|
+
Button({ variant: "ghost", onClick: () => load() }, "刷新"),
|
|
73
|
+
Button({ variant: "ghost", onClick: () => setRaw((v) => !v) }, raw ? "看时间线" : "看原始"),
|
|
74
|
+
Button({
|
|
75
|
+
variant: "ghost", disabled: entries.length === 0,
|
|
76
|
+
onClick: async () => {
|
|
77
|
+
const r = await api("POST", "/diag-log/clear", {});
|
|
78
|
+
if (r?.ok) { notify("已清空"); load(); } else notify(r?.error ?? "清空失败");
|
|
79
|
+
}
|
|
80
|
+
}, "清空"))
|
|
81
|
+
},
|
|
82
|
+
Muted("插件自己的运行记录:启动到哪一步、人格卡/瞬态块有没有挂上、会话是新建还是恢复、每轮上下文多长。只留最近 200 条,不落盘。"),
|
|
83
|
+
Muted("这里没有「宿主压缩上下文」的记录 —— 宿主不广播那个事件,插件看不到。上下文变长能在「这一轮上下文长度」里看出来。"),
|
|
84
|
+
entries.length === 0
|
|
85
|
+
? h("div", { className: "fs-empty" }, "还没有记录。重启宿主后回来就能看到启动过程。")
|
|
86
|
+
: raw
|
|
87
|
+
? h("pre", {
|
|
88
|
+
className: "fs-report",
|
|
89
|
+
style: { maxHeight: 420, overflow: "auto", fontSize: 12, whiteSpace: "pre-wrap", wordBreak: "break-all" }
|
|
90
|
+
}, JSON.stringify(entries, null, 2))
|
|
91
|
+
: h("div", { style: { display: "flex", flexDirection: "column", gap: 2 } },
|
|
92
|
+
entries.map((e, i) => h("div", {
|
|
93
|
+
key: `${e.at}-${i}`,
|
|
94
|
+
style: {
|
|
95
|
+
display: "flex", gap: 10, alignItems: "baseline",
|
|
96
|
+
padding: "4px 0", borderBottom: "1px solid var(--dsw-alias-border-l1, #eee)", fontSize: 13
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
h("span", { style: { color: "var(--dsw-alias-text-3, #888)", fontVariantNumeric: "tabular-nums", flex: "0 0 auto" } }, fmtTime(e.at)),
|
|
100
|
+
h("span", { style: { fontWeight: 500, flex: "0 0 auto", ...(levelStyle(e.level) ?? {}) } }, e.label),
|
|
101
|
+
h("span", { style: { color: "var(--dsw-alias-text-2, #555)", wordBreak: "break-word" } }, describe(e)))))
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { DiagPane };
|
|
106
|
+
}
|
|
107
|
+
});
|
package/lib/client.douyin.js
CHANGED
|
@@ -163,7 +163,7 @@ window.__ModuleLoader__.load({
|
|
|
163
163
|
Row({ title: "主人的名字", desc: "仅作显示(提示词里怎么称呼);不授予任何权限" }, Input({ ...bind("masterName"), placeholder: "她私信列表里你的名字" })),
|
|
164
164
|
Row({ title: "回所有能私信她的人", desc: d.replyAll ? "建议在抖音隐私设置里把「谁可以私信我」设为互关好友" : null },
|
|
165
165
|
sw("回所有能私信她的人", d.replyAll === true, (v) => save({ top: { replyAll: v } }))),
|
|
166
|
-
d.replyAll ? null : Row({ title: "白名单", stack: true }, Area({ ...bind("allowList"), placeholder: "对方的抖音数字 uid,每行一个;昵称不参与放行" })),
|
|
166
|
+
d.replyAll ? null : Row({ title: "白名单", stack: true }, Area({ ...bind("allowList"), placeholder: "对方的抖音数字 uid,每行一个;昵称不参与放行" }))),
|
|
167
167
|
Group({ title: "群聊" },
|
|
168
168
|
Row({ title: "群里被叫到时回", desc: "群聊按说话的人分开认,不会当成一个人;没人 @ 她或叫她名字时只看不说" },
|
|
169
169
|
sw("群里被叫到时回", d.groupReply === true, (v) => save({ top: { groupReply: v } }))),
|
package/lib/client.js
CHANGED
|
@@ -806,14 +806,14 @@ window.__ModuleLoader__.load({
|
|
|
806
806
|
}
|
|
807
807
|
|
|
808
808
|
// ── 面板根组件:页签 + 数据装载 + 轮询 ──
|
|
809
|
-
const TAB_KEYS = { memory: "忆", character: "角", daily: "常", channels: "台", models: "模", system: "统", guide: "引", retrieval: "索", persona: "人", life: "自", inspector: "忆", activity: "动", behavior: "习", qq: "Q", bili: "B", douyin: "抖", version: "版" };
|
|
809
|
+
const TAB_KEYS = { memory: "忆", character: "角", daily: "常", channels: "台", models: "模", system: "统", guide: "引", retrieval: "索", persona: "人", life: "自", inspector: "忆", activity: "动", behavior: "习", qq: "Q", bili: "B", douyin: "抖", version: "版", diag: "志" };
|
|
810
810
|
const TABS = [
|
|
811
811
|
["memory", "记忆", [["retrieval", "检索设置"], ["people", "人物与关系"], ["memories", "记忆内容"], ["recall-test", "检索测试"], ["weekly", "周记"], ["archives", "记忆存档"], ["backups", "导入与备份"]]],
|
|
812
812
|
["character", "角色", [["persona", "人格卡"], ["templates", "人格模板"], ["growth", "成长"], ["worldinfo", "世界书"], ["habits", "说话习惯"], ["groups", "群设定"], ["chat-models", "聊天模型"]]],
|
|
813
813
|
["daily", "日常", [["life", "主动开口"], ["activity", "最近动态"], ["diary", "日记"], ["stickers", "表情包"], ["slang", "学到的说法"]]],
|
|
814
814
|
["channels", "平台", [["qq", "QQ"], ["bili", "B站"], ["douyin", "抖音"]]],
|
|
815
815
|
["models", "模型", [["models", "功能模型"], ["usage", "用量与价格"], ["web", "联网搜索"], ["voice", "语音"]]],
|
|
816
|
-
["system", "系统", [["interface", "界面"], ["version", "版本更新"]]]
|
|
816
|
+
["system", "系统", [["interface", "界面"], ["version", "版本更新"], ["diag", "运行日志"]]]
|
|
817
817
|
];
|
|
818
818
|
const NAV_GROUPS = TABS.map(([id, label]) => ({ id, label }));
|
|
819
819
|
const SUBTABS = Object.fromEntries(TABS.map(([id, , subs]) => [id, subs]));
|
|
@@ -1010,6 +1010,7 @@ window.__ModuleLoader__.load({
|
|
|
1010
1010
|
: ["habits", "groups", "chat-models", "diary"].includes(subtab) ? h(ChunkTab, { file: "client.behavior.js", pick: (m) => m.BehaviorPane, failText: "习惯页加载失败", section: subtab === "chat-models" ? "models" : subtab, notify })
|
|
1011
1011
|
: ["models", "usage", "web", "voice"].includes(subtab) ? h(ChunkTab, { file: "client.models.js", pick: (m) => m.ModelsPane, failText: "模型页加载失败", section: subtab, notify })
|
|
1012
1012
|
: ["persona", "templates", "growth", "worldinfo"].includes(subtab) ? h(ChunkTab, { file: "client.persona.js", pick: (m) => m.PersonaPane, failText: "人格页加载失败", section: subtab === "persona" ? "current" : subtab, notify })
|
|
1013
|
+
: subtab === "diag" ? h(ChunkTab, { file: "client.diag.js", pick: (m) => m.DiagPane, failText: "运行日志加载失败", notify })
|
|
1013
1014
|
: subtab === "qq" ? h(ChannelPane, { data: channels, refresh: loadChannels, notify })
|
|
1014
1015
|
: subtab === "bili" ? h(BiliPane, { data: channels, refresh: loadChannels, notify })
|
|
1015
1016
|
: h(ChunkTab, { file: "client.douyin.js", pick: (m) => m.DouyinPane, failText: "抖音页加载失败", data: channels, refresh: loadChannels, notify });
|
package/lib/diag-log.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* diag-log.js —— 排查用的结构化日志(系统页「运行日志」)。
|
|
3
|
+
*
|
|
4
|
+
* 和动态页(gateway.decisions)的区别:
|
|
5
|
+
* - decisions 记的是**对话层面**的事(谁说了什么、想起了几条),给人翻的;
|
|
6
|
+
* - 这里记的是**装置层面**的事(启动卡在哪、人格段挂不上、会话恢复失败降级新建、
|
|
7
|
+
* 上下文长度趋势),是"她为什么突然不对劲"时第一个要看的地方。
|
|
8
|
+
*
|
|
9
|
+
* 两个视图共用一份数据:
|
|
10
|
+
* - 中文时间线:给用户看,也方便贴到 issue 里;
|
|
11
|
+
* - 原始 JSON:给维护者 grep。
|
|
12
|
+
*
|
|
13
|
+
* 设计约束:
|
|
14
|
+
* - 只在内存里留最近 N 条(默认 200),**不落盘** —— 这是排查快照不是审计日志,
|
|
15
|
+
* 落盘会变成第二个事实来源,也会把日志越攒越大。
|
|
16
|
+
* - 同样的 key 在短时间窗内重复发生时只累加 count,不刷屏(启动失败那种每秒一次的
|
|
17
|
+
* 情况不会把缓冲冲干净)。
|
|
18
|
+
*/
|
|
19
|
+
import { getKv, setKv } from './db.js';
|
|
20
|
+
|
|
21
|
+
/** 内存里最多留多少条。 */
|
|
22
|
+
const LIMIT = 200;
|
|
23
|
+
/** 同一个 key 在这个窗口内重复出现就合并计数(毫秒)。 */
|
|
24
|
+
const DEDUPE_MS = 60_000;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 每条日志的中文说法。key 是稳定的机器标识,人话在这里查。
|
|
28
|
+
* 加新 key 时**必须**在这里补一条,否则界面会显示成 key 本身(这是故意的:
|
|
29
|
+
* 宁可露出机器名,也别悄悄显示成空白让人以为没发生)。
|
|
30
|
+
*/
|
|
31
|
+
const LABELS = {
|
|
32
|
+
'startup-step': '启动到某一步',
|
|
33
|
+
'startup-failed': '启动失败',
|
|
34
|
+
'mount-failed': '人格卡 / 瞬态块挂不上',
|
|
35
|
+
'mount-no-ctx': '拿不到 agent.ctx',
|
|
36
|
+
'session-create': '新建了会话',
|
|
37
|
+
'session-resume': '恢复了会话',
|
|
38
|
+
'resume-failed': '恢复会话失败,改为新建',
|
|
39
|
+
'session-retired': '会话退役',
|
|
40
|
+
'context-size': '这一轮上下文长度',
|
|
41
|
+
'image-skip': '图片没送进模型',
|
|
42
|
+
'sticker-skip': '表情没收藏成功',
|
|
43
|
+
'turn-no-recall': '这一轮没组装记忆',
|
|
44
|
+
'config-invalid': '配置文件读不懂'
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export class DiagLog {
|
|
48
|
+
/**
|
|
49
|
+
* @param {{ db: object, log?: Function, now?: () => number }} deps
|
|
50
|
+
*/
|
|
51
|
+
constructor({ db, log, now = () => Date.now() }) {
|
|
52
|
+
this.db = db;
|
|
53
|
+
this.log = log ?? (() => {});
|
|
54
|
+
this.now = now;
|
|
55
|
+
/** @type {Array<{at:number,key:string,level:string,detail:object,count:number}>} */
|
|
56
|
+
this.entries = [];
|
|
57
|
+
this.lastKeyAt = new Map();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 记一条。
|
|
62
|
+
* @param {string} key LABELS 里的机器标识
|
|
63
|
+
* @param {object} [detail] 结构化细节(别塞大对象:这里只留 200 条)
|
|
64
|
+
* @param {'info'|'warn'|'error'} [level]
|
|
65
|
+
*/
|
|
66
|
+
add(key, detail = {}, level = 'info') {
|
|
67
|
+
const at = this.now();
|
|
68
|
+
const k = String(key);
|
|
69
|
+
// 同一个 key 在窗口内重复:合并成一条 + count,别把缓冲冲干净
|
|
70
|
+
const lastAt = this.lastKeyAt.get(k) ?? 0;
|
|
71
|
+
if (at - lastAt < DEDUPE_MS && this.entries.length > 0) {
|
|
72
|
+
const tail = this.entries[this.entries.length - 1];
|
|
73
|
+
if (tail.key === k) {
|
|
74
|
+
tail.count += 1;
|
|
75
|
+
tail.at = at;
|
|
76
|
+
tail.detail = detail;
|
|
77
|
+
this.lastKeyAt.set(k, at);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
this.lastKeyAt.set(k, at);
|
|
82
|
+
this.entries.push({ at, key: k, level, detail, count: 1 });
|
|
83
|
+
if (this.entries.length > LIMIT) this.entries.splice(0, this.entries.length - LIMIT);
|
|
84
|
+
// 同时打一行到宿主日志:设置页没开着的时候也能在终端看到
|
|
85
|
+
if (level !== 'info') this.log(level, `[fish-soul] ${LABELS[k] ?? k}:${JSON.stringify(detail).slice(0, 200)}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 这一轮上下文长度(字符数粗估)。只在明显变化时记,避免每轮刷一条。 */
|
|
89
|
+
noteContextSize(sessionId, chars, budget) {
|
|
90
|
+
const key = `ctx:${String(sessionId)}`;
|
|
91
|
+
const prev = getKv(this.db, key, null);
|
|
92
|
+
const n = Number(chars) || 0;
|
|
93
|
+
// 变化不到 20% 且没跨过 80% 预算线就不记
|
|
94
|
+
const crossed = budget > 0 && n >= budget * 0.8 && !(prev !== null && Number(prev) >= budget * 0.8);
|
|
95
|
+
if (prev !== null && !crossed && Math.abs(n - Number(prev)) < Math.max(200, Number(prev) * 0.2)) return;
|
|
96
|
+
setKv(this.db, key, n);
|
|
97
|
+
this.add('context-size', { sessionId: String(sessionId).slice(0, 24), chars: n, budget: Number(budget) || 0, over80: budget > 0 ? n >= budget * 0.8 : false });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 给界面用:最近的日志,新的在前。 */
|
|
101
|
+
list(limit = LIMIT) {
|
|
102
|
+
return [...this.entries].reverse().slice(0, limit).map((e) => ({
|
|
103
|
+
at: e.at,
|
|
104
|
+
key: e.key,
|
|
105
|
+
label: LABELS[e.key] ?? e.key,
|
|
106
|
+
level: e.level,
|
|
107
|
+
detail: e.detail,
|
|
108
|
+
count: e.count
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** 清空(设置页「清空日志」按钮)。 */
|
|
113
|
+
clear() {
|
|
114
|
+
this.entries.length = 0;
|
|
115
|
+
this.lastKeyAt.clear();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 一行中文(时间线视图用)。放在这里而不是前端:格式化逻辑要能测。 */
|
|
120
|
+
export function describeEntry(entry) {
|
|
121
|
+
const d = entry.detail ?? {};
|
|
122
|
+
const bits = [];
|
|
123
|
+
if (d.step !== undefined) bits.push(`第 ${d.step} 步${d.name ? `「${d.name}」` : ''}`);
|
|
124
|
+
if (d.sessionId) bits.push(`会话 ${String(d.sessionId).slice(0, 12)}…`);
|
|
125
|
+
if (d.channelId) bits.push(`聊天 ${d.channelId}`);
|
|
126
|
+
if (d.chars !== undefined) bits.push(`${d.chars} 字${d.budget ? ` / 预算 ${d.budget}` : ''}${d.over80 ? '(超 80%)' : ''}`);
|
|
127
|
+
if (d.reason) bits.push(String(d.reason).slice(0, 120));
|
|
128
|
+
if (entry.count > 1) bits.push(`重复 ${entry.count} 次`);
|
|
129
|
+
return bits.join(',') || '(没有细节)';
|
|
130
|
+
}
|
package/lib/gateway.js
CHANGED
|
@@ -132,7 +132,7 @@ export class ChatGateway {
|
|
|
132
132
|
* @param {object} deps.soul index.js 提供的人格/记忆回调
|
|
133
133
|
* @param {(agentCtx: object) => void} [deps.agentSections] 给单个 agent 挂人格卡 / 瞬态块(index.js 传入 registerAgentSections)
|
|
134
134
|
*/
|
|
135
|
-
constructor({ ctx, db, clock, getConfig, log, channel, bili = null, browser = null, douyin = null, douyinBrowser = null, canvasConvert = null, soul, voice = null, stickers = null, slang = null, agentSections = null, groupAdmin = null, embedder = null, embeddingCfg = null }) {
|
|
135
|
+
constructor({ ctx, db, clock, getConfig, log, channel, bili = null, browser = null, douyin = null, douyinBrowser = null, canvasConvert = null, soul, voice = null, stickers = null, slang = null, agentSections = null, groupAdmin = null, embedder = null, embeddingCfg = null, diag = null }) {
|
|
136
136
|
this.ctx = ctx;
|
|
137
137
|
this.db = db;
|
|
138
138
|
this.clock = clock;
|
|
@@ -170,6 +170,7 @@ export class ChatGateway {
|
|
|
170
170
|
this.groupAdmin = groupAdmin;
|
|
171
171
|
/** 给单个 agent 挂人格卡 / 瞬态块:`(agentCtx) => void`(在 setup 里调,只对那个 agent 生效) */
|
|
172
172
|
this.agentSections = agentSections;
|
|
173
|
+
this.diag = diag;
|
|
173
174
|
/** sessionId → chat_session 行(只存我们建的会话) */
|
|
174
175
|
this.bound = new Map();
|
|
175
176
|
/** channelId → Promise<Agent>(同一聊天并发来消息时只建一个会话) */
|
|
@@ -278,7 +279,9 @@ export class ChatGateway {
|
|
|
278
279
|
this.decisions.unshift({
|
|
279
280
|
at: Date.now(), kind, channelId, title: title || this.titleOf(channelId),
|
|
280
281
|
who: String(who ?? '').slice(0, 24), text: String(text ?? '').replace(/\s+/g, ' ').slice(0, 80), reason,
|
|
281
|
-
|
|
282
|
+
// #78:以前写死 5 条,用户把「召回条数」调到 10 后动态页永远只显示 5 条,
|
|
283
|
+
// 看起来像「设置没生效」。上限跟 topK 走(再留个 20 的硬顶,别让一条动态撑爆)。
|
|
284
|
+
...(Array.isArray(memories) && memories.length > 0 ? { memories: memories.slice(0, Math.min(20, Math.max(5, Number(this.getConfig?.()?.retrieval?.topK) || 5))) } : {}),
|
|
282
285
|
// #78:这一轮记忆到底有没有参与。没跑 / 认不出人 / 跑了没召回到,界面要分得开 ——
|
|
283
286
|
// 以前三种都是"什么都不显示",看不出问题在哪儿
|
|
284
287
|
...(Number(recallAt) > 0 ? { recallAt: Number(recallAt), speakerUnknown: speakerUnknown === true } : {}),
|
|
@@ -705,9 +708,12 @@ export class ChatGateway {
|
|
|
705
708
|
setup: (agentCtx) => this.setupAgent(agentCtx, key)
|
|
706
709
|
});
|
|
707
710
|
this.log('info', `[fish-soul] 已恢复会话 ${row.title || key}`);
|
|
711
|
+
this.diag?.add('session-resume', { sessionId: row.session_id, channelId: key });
|
|
708
712
|
return handle.agent;
|
|
709
713
|
} catch (error) {
|
|
710
|
-
|
|
714
|
+
const msg = String(error instanceof Error ? error.message : error);
|
|
715
|
+
this.log('warn', `[fish-soul] 恢复会话 ${row.session_id} 失败,改为新建:${msg}`);
|
|
716
|
+
this.diag?.add('resume-failed', { sessionId: row.session_id, channelId: key, reason: msg }, 'warn');
|
|
711
717
|
this.db.prepare("UPDATE chat_session SET status = 'retired' WHERE id = ?").run(row.id);
|
|
712
718
|
this.bound.delete(row.session_id);
|
|
713
719
|
}
|
|
@@ -756,6 +762,7 @@ export class ChatGateway {
|
|
|
756
762
|
setup: (agentCtx) => this.setupAgent(agentCtx, chat.channelId)
|
|
757
763
|
});
|
|
758
764
|
const title = await this.chatTitle(chat);
|
|
765
|
+
this.diag?.add('session-create', { sessionId, channelId: chat.channelId, reason: title });
|
|
759
766
|
try { await ws.attachSession?.(sessionId); } catch (error) {
|
|
760
767
|
this.log('warn', `[fish-soul] 会话挂到工作区失败:${String(error instanceof Error ? error.message : error)}`);
|
|
761
768
|
}
|
|
@@ -1371,7 +1378,9 @@ export class ChatGateway {
|
|
|
1371
1378
|
// 以前每条都重算 fromUs ⇒ 被它们改成 false ⇒ 回复生成了却不发(DSH 里看得见,QQ / 抖音收不到)。
|
|
1372
1379
|
// 现在只有我们自己的来源置 true;只有真人在界面里打字(kind 'user')才置 false;别的宿主上下文不管。
|
|
1373
1380
|
if (OUR_SOURCES.has(kind)) st.fromUs = true;
|
|
1374
|
-
else if (kind === 'user') st.fromUs = false;
|
|
1381
|
+
else if (kind === 'user') { st.fromUs = false; st.dshTyped = true; } // #78:DSH 桌面端打字,不外发但要记动态
|
|
1382
|
+
// 其他宿主上下文(runtime-context / model-selection 等):不动 fromUs ——
|
|
1383
|
+
// 它们不是我们发的消息,但也不是真人打字,重置会把 B站/抖音 的回复憋回去(smoke #13 守着)
|
|
1375
1384
|
// 共享会话里一个会话对应多个通道(QQ + B站):这一轮从哪个通道进来的,来源里也带着 ——
|
|
1376
1385
|
// 开轮时 dispatch 已经登记过,这里兜的是「插件重启后宿主重放消息」那条路(否则会回错平台)
|
|
1377
1386
|
if (OUR_SOURCES.has(kind) && msg?.source?.channelId) st.channelId = String(msg.source.channelId);
|
|
@@ -1409,6 +1418,19 @@ export class ChatGateway {
|
|
|
1409
1418
|
};
|
|
1410
1419
|
if (!completed || !st.fromUs) {
|
|
1411
1420
|
if (st.fromUs) this.note('failed', { channelId: st.channelId ?? this.bound.get(sessionId)?.channel_id ?? '', reason: `这一轮没正常结束(${d.reason?.kind ?? '未知'})` });
|
|
1421
|
+
// #78:DSH 桌面端打字的轮次不外发,但**要记动态** —— 以前在 DSH 里聊,动态页整片空白。
|
|
1422
|
+
if (st.dshTyped === true) {
|
|
1423
|
+
const recall = this.soul.recallInfo?.(sessionId, st) ?? { memories: [], at: 0, unknown: false };
|
|
1424
|
+
this.note('dsh', {
|
|
1425
|
+
channelId: st.channelId ?? this.bound.get(sessionId)?.channel_id ?? '',
|
|
1426
|
+
text: String(st.text ?? '').slice(0, 60),
|
|
1427
|
+
reason: '在 DSH 里聊的,不外发',
|
|
1428
|
+
memories: recall.memories ?? [],
|
|
1429
|
+
recallAt: recall.at ?? 0,
|
|
1430
|
+
speakerUnknown: recall.unknown === true,
|
|
1431
|
+
recallMiss: recall.miss ?? ''
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1412
1434
|
this.interjectTurns.delete(sessionId);
|
|
1413
1435
|
settle(false);
|
|
1414
1436
|
return;
|
package/lib/index.js
CHANGED
|
@@ -52,6 +52,7 @@ import { VirtualClock } from './clock.js';
|
|
|
52
52
|
import { EmbeddingClient, VectorWorker, recall, recallSync, vectorRanks } from './retrieval.js';
|
|
53
53
|
import { LifeEngine } from './life.js';
|
|
54
54
|
import { registerRoutes, registerAgentHooks, registerAgentSections, mountApiGate } from './routes.js';
|
|
55
|
+
import { DiagLog } from './diag-log.js';
|
|
55
56
|
import { UpdateChecker, pluginVersion } from './update-check.js';
|
|
56
57
|
import { GroupAdmin } from './qq-group-admin.js';
|
|
57
58
|
import { tokenizeForBm25 } from './segment.js';
|
|
@@ -71,7 +72,7 @@ const defineTool = hostDeps.defineTool;
|
|
|
71
72
|
export const name = 'dsh-fish-soul';
|
|
72
73
|
|
|
73
74
|
/** 只给测试用:换掉抖音专用浏览器的启动 / 页面驱动(抖音网页在测试里打不开)。正式运行时是空的。 */
|
|
74
|
-
export const testHooks = { douyin: null, memory: null };
|
|
75
|
+
export const testHooks = { douyin: null, memory: null, gateway: null };
|
|
75
76
|
// agents / workspaceRegistry:给每个 QQ 聊天建会话;sessionTitle / attachments / agentPresets /
|
|
76
77
|
// agentDefaultModel 是可选的,运行时用 ctx.get 取,缺了就降级(不写进 inject,免得整个插件不激活)。
|
|
77
78
|
export const inject = ['webServer', 'tools', 'systemPrompt', 'llm', 'agents', 'workspaceRegistry'];
|
|
@@ -188,9 +189,13 @@ export async function apply(ctx, config) {
|
|
|
188
189
|
}
|
|
189
190
|
|
|
190
191
|
async function startup(ctx, config, { log, gate }) {
|
|
192
|
+
// 排查日志:启动每一步都记一条 —— 「插件挂在第几步」是最常被问的问题,
|
|
193
|
+
// 以前只在宿主终端里有,重启就没了(系统页「运行日志」读这个)。
|
|
194
|
+
const diag = new DiagLog({ db: null, log, now: () => Date.now() });
|
|
191
195
|
const step = (name) => {
|
|
192
196
|
gate.step(name);
|
|
193
197
|
log('debug', `[fish-soul] 启动第 ${gate.state.index}/${gate.state.total} 步:${name}`);
|
|
198
|
+
diag.add('startup-step', { step: gate.state.index, name });
|
|
194
199
|
};
|
|
195
200
|
// 配置来源:宿主 Config(schema 默认值)为底,设置页写的文件覆盖其上——
|
|
196
201
|
// 旧实现 {...file, ...host} 浅合并会让 schema 的空默认值把文件里的 Key/BaseURL 清空。
|
|
@@ -223,6 +228,7 @@ async function startup(ctx, config, { log, gate }) {
|
|
|
223
228
|
// 虚拟钟从上次快照续走(加速验收中途重启不倒退)
|
|
224
229
|
const clock = new VirtualClock(undefined, Number(getKv(db, 'clock.virtualMs', 0)) || 0);
|
|
225
230
|
|
|
231
|
+
diag.db = db;
|
|
226
232
|
step(STARTUP_STEPS[1]);
|
|
227
233
|
// ── 检索 ──
|
|
228
234
|
const embedder = new EmbeddingClient(embeddingCfg);
|
|
@@ -831,7 +837,14 @@ async function startup(ctx, config, { log, gate }) {
|
|
|
831
837
|
}
|
|
832
838
|
}
|
|
833
839
|
if (persona.postHistory !== '') lines.push(applyMacros(persona.postHistory, { charName: persona.name, userName }));
|
|
834
|
-
|
|
840
|
+
const block = lines.join(String.fromCharCode(10));
|
|
841
|
+
// 上下文长度趋势:宿主压缩(compaction)插件这边看不到,但「越来越长」是能测的。
|
|
842
|
+
// 只在明显变化或跨过 80% 预算线时记一条,别每轮刷。
|
|
843
|
+
try {
|
|
844
|
+
const sid = arg?.agent?.id ?? arg?.scope?.id ?? arg?.sessionId;
|
|
845
|
+
if (sid !== undefined) diag.noteContextSize(sid, block.length, Number(currentConfig.retrieval?.tokenBudget) || 1200);
|
|
846
|
+
} catch { /* 记日志失败不影响这一轮 */ }
|
|
847
|
+
return block;
|
|
835
848
|
}
|
|
836
849
|
|
|
837
850
|
/** 宿主消息正文(content 是块数组)。 */
|
|
@@ -856,10 +869,15 @@ async function startup(ctx, config, { log, gate }) {
|
|
|
856
869
|
// mountSectionsFor 有幂等保护(sectionsMounted),所有会话都在 inbox 时补挂一次做保险。
|
|
857
870
|
mountSectionsFor(payload?.agent);
|
|
858
871
|
if (source.kind === SOURCE_QQ || source.kind === SOURCE_PROACTIVE || source.kind === SOURCE_BILI || source.kind === SOURCE_DOUYIN) return;
|
|
859
|
-
if (gateway?.isOurSession(agentId)) return;
|
|
860
872
|
if (agentId === undefined || agentId === null) return;
|
|
873
|
+
// #78:**在 DSH 界面里打开我们的会话打字**时,isOurSession 是 true,但消息不走
|
|
874
|
+
// gateway.dispatch(那条路只服务 QQ / B站 / 抖音 的入站),于是没有 beginTurn、
|
|
875
|
+
// 也没有 note —— 动态页整片空白。用户原话:「只能在 QQ 上说话才能正常调起功能」。
|
|
876
|
+
// 现在这条路也登记一轮:身份按 DSH 桌面端算(#57:桌面端就是主人自己的环境)。
|
|
877
|
+
const ours = gateway?.isOurSession(agentId) === true;
|
|
861
878
|
const qqUin = String(source.userId ?? source.qqUin ?? source.user_id ?? '');
|
|
862
|
-
|
|
879
|
+
// 我们的会话在 DSH 里被打开时,说话的人就是主人自己 —— 不去猜 QQ 号(那会建出幽灵 person)
|
|
880
|
+
const person = ours || qqUin === '' ? null : resolveOrCreatePerson(db, {
|
|
863
881
|
qqUin, nickname: String(source.nickname ?? ''), nowMs: clock.now(), masterQqUin: currentConfig.masterQqUin ?? ''
|
|
864
882
|
});
|
|
865
883
|
if (person !== null) life.noteInteraction(ownerOf(person));
|
|
@@ -868,11 +886,14 @@ async function startup(ctx, config, { log, gate }) {
|
|
|
868
886
|
else life.noteInteraction('user:master');
|
|
869
887
|
const groupId = source.groupId ?? source.group_id ?? null;
|
|
870
888
|
// #56:桌面端(person=null)也把消息文本传进去——世界书扫描要用,身份仍是 unknown。
|
|
889
|
+
// 我们的会话在 DSH 里打字:沿用这个会话真正的 channelId(记忆/日志仍按它归),
|
|
890
|
+
// 只是说话人按主人算 —— 这样瞬态块能跑、动态页也能显示这一轮想起了什么。
|
|
891
|
+
const boundChannel = ours ? (gateway?.chatForSession(agentId)?.channel_id ?? '') : '';
|
|
871
892
|
beginTurn(agentId, {
|
|
872
|
-
channel: person === null ? 'dsh' : (groupId != null ? 'qq-group' : 'qq-private'),
|
|
873
|
-
channelId: person === null ? '' : (groupId != null ? `group:${groupId}` : `private:${qqUin}`),
|
|
874
|
-
speakers: [{ person, userId: qqUin, displayName: person?.display_name ?? '对方', text: messageText(message) }],
|
|
875
|
-
workspacePath: ''
|
|
893
|
+
channel: ours ? sessionChannelOf(boundChannel) : (person === null ? 'dsh' : (groupId != null ? 'qq-group' : 'qq-private')),
|
|
894
|
+
channelId: ours ? boundChannel : (person === null ? '' : (groupId != null ? `group:${groupId}` : `private:${qqUin}`)),
|
|
895
|
+
speakers: [{ person, userId: qqUin, displayName: person?.display_name ?? (ours ? '主人' : '对方'), text: messageText(message) }],
|
|
896
|
+
workspacePath: ours ? (gateway?.chatForSession(agentId)?.workspace_path ?? '') : ''
|
|
876
897
|
});
|
|
877
898
|
// #41:别的插件把自己的 QQ 消息转进它建的会话 —— 那个会话不走我们的 create/resume,人格卡没挂上。
|
|
878
899
|
// 认出身份后(有人说话)用宿主给的 `agent.ctx` 补挂,仍然只对这一个 agent 生效,不碰全局。
|
|
@@ -921,6 +942,7 @@ async function startup(ctx, config, { log, gate }) {
|
|
|
921
942
|
slang.start();
|
|
922
943
|
ctx.effect(() => () => slang.stop(), 'dsh-fish-soul: slang');
|
|
923
944
|
|
|
945
|
+
testHooks.gateway = null;
|
|
924
946
|
gateway = new ChatGateway({
|
|
925
947
|
ctx, db, clock, getConfig, log, channel, bili, browser, douyin, douyinBrowser, canvasConvert, voice, stickers, slang,
|
|
926
948
|
// 按感觉挑表情:复用记忆那套嵌入模型(主人拍板)。没配就跟以前一样按文件名发
|
|
@@ -928,6 +950,7 @@ async function startup(ctx, config, { log, gate }) {
|
|
|
928
950
|
groupAdmin, // 群管理(入群申请):主人回"/鱼 同意/拒绝"时用它
|
|
929
951
|
// 每个自家会话建/恢复好后,把人格卡 + 此刻状态挂到它的 agent 上(不注册全局段,别的会话看不见)
|
|
930
952
|
agentSections: (agentCtx) => registerAgentSections(agentCtx, { personaText: () => persona.staticText, buildContextBlock, log }),
|
|
953
|
+
diag,
|
|
931
954
|
soul: {
|
|
932
955
|
beginTurn,
|
|
933
956
|
nowLabel,
|
|
@@ -1069,12 +1092,13 @@ async function startup(ctx, config, { log, gate }) {
|
|
|
1069
1092
|
db, clock, embedder, worker, life, getConfig, saveConfig, log, gateway, channel, bili, biliClient, browser, biliSocial, douyinSocial,
|
|
1070
1093
|
douyin, douyinHost, douyinBrowser, backup, historyImporter, updater, worldBooks,
|
|
1071
1094
|
llmRouter, voice, stickers, slang, persona, reloadPersona, webConfig, hasWebTools, webUsedToday, hostWebAvailable, hostWebState, runWebSearch,
|
|
1072
|
-
journal, digester, diary, weekly, storage, chatArchive,
|
|
1095
|
+
journal, digester, diary, weekly, storage, chatArchive, diag,
|
|
1073
1096
|
debugRecall: (o) => debugRecall({ db, clock, embedder, embeddingCfg: embeddingCfg(), config: retrievalCfg(), ...o })
|
|
1074
1097
|
});
|
|
1075
1098
|
|
|
1076
1099
|
log('info', '[fish-soul] 路由已挂:设置页接口可用');
|
|
1077
1100
|
|
|
1101
|
+
testHooks.gateway = gateway;
|
|
1078
1102
|
step(STARTUP_STEPS[15]);
|
|
1079
1103
|
// 只挂与具体 agent 无关的全局钩子(互动登记)。人格卡 / 瞬态块是 per-agent 的,
|
|
1080
1104
|
// 在 gateway 的 setup(agentCtx) 里挂,这里不再注册任何全局 prompt 段。
|
package/lib/routes-soul.js
CHANGED
|
@@ -58,7 +58,7 @@ export function sourceLabel(src, channel = '') {
|
|
|
58
58
|
* @returns {Promise<boolean>} 处理了就返回 true
|
|
59
59
|
*/
|
|
60
60
|
export async function handleSoulRoute({ method, path, url, req, res, send, readBody, sameOrigin, deps }) {
|
|
61
|
-
const { db, clock, worker, life, gateway, backup, historyImporter, getConfig, saveConfig, journal, digester, weekly, storage, chatArchive } = deps;
|
|
61
|
+
const { db, clock, worker, life, gateway, backup, historyImporter, getConfig, saveConfig, journal, digester, weekly, storage, chatArchive, diag } = deps;
|
|
62
62
|
const post = method === 'POST';
|
|
63
63
|
const body = async () => {
|
|
64
64
|
const raw = await readBody(req);
|
|
@@ -326,6 +326,19 @@ export async function handleSoulRoute({ method, path, url, req, res, send, readB
|
|
|
326
326
|
}
|
|
327
327
|
|
|
328
328
|
// ── 动态 / 暂停 ──
|
|
329
|
+
// ── 运行日志(系统页)──
|
|
330
|
+
// 排查用的装置层面事件:启动卡在哪、人格段挂不上、会话恢复失败、上下文长度。
|
|
331
|
+
// 和 /activity(对话层面的动态)分开:一事实一出处,别混在一起。
|
|
332
|
+
if (path === '/diag-log' && method === 'GET') {
|
|
333
|
+
send(200, { ok: true, entries: (diag?.list?.(200) ?? []) });
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
if (path === '/diag-log/clear' && post) {
|
|
337
|
+
if (!guard()) return true;
|
|
338
|
+
diag?.clear?.();
|
|
339
|
+
send(200, { ok: true });
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
329
342
|
if (path === '/activity' && method === 'GET') {
|
|
330
343
|
const paused = gateway?.pausedMap() ?? {};
|
|
331
344
|
send(200, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-fish-soul",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.41",
|
|
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-gap-dedupe.mjs && node test/douyin-read-fail-retry.mjs && node test/douyin-read-cache.mjs && node test/douyin-read-fail-retry-poll.mjs && node test/douyin-inbox-contract.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/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-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/self-update.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-gap-dedupe.mjs && node test/douyin-read-fail-retry.mjs && node test/douyin-read-cache.mjs && node test/douyin-read-fail-retry-poll.mjs && node test/douyin-inbox-contract.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/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-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/self-update.mjs",
|
|
68
68
|
"eval": "node test/eval/persona-eval.mjs"
|
|
69
69
|
}
|
|
70
70
|
}
|