dsh-fish-soul 0.14.1 → 0.15.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 +1 -1
- package/lib/client.behavior.js +3 -3
- package/lib/client.js +82 -27
- package/lib/client.models.js +4 -4
- package/lib/client.persona.js +2 -2
- package/lib/client.soul.js +41 -26
- package/lib/config-schema.js +4 -1
- package/lib/gateway-util.js +17 -1
- package/lib/gateway.js +32 -3
- package/lib/index.js +4 -0
- package/lib/panel.css +135 -56
- package/lib/routes.js +52 -1
- package/package.json +3 -3
package/README.md
CHANGED
package/lib/client.behavior.js
CHANGED
|
@@ -59,7 +59,7 @@ window.__ModuleLoader__.load({
|
|
|
59
59
|
Group({ title: "QQ 小习惯" },
|
|
60
60
|
Row({ title: "看合并转发", desc: "展开一层,最多看前几条" },
|
|
61
61
|
h(Switch, { label: "看合并转发", checked: cfg.forward.enabled, onChange: (v) => save({ forward: { enabled: v } }) })),
|
|
62
|
-
Row({ title: "被戳了戳回去",
|
|
62
|
+
Row({ title: "被戳了戳回去", meta: ia.pokeBack ? `有 ${pct(ia.pokeBackChance)}% 的时候戳回去(SnowLuma 不支持就算了)` : undefined },
|
|
63
63
|
ia.pokeBack ? h(NumField, { ui, value: pct(ia.pokeBackChance), min: 0, max: 100, step: 10, label: "戳回去的概率", onSave: (n) => save({ interactions: { pokeBackChance: n / 100 } }) }) : null,
|
|
64
64
|
ia.pokeBack ? Muted("%") : null,
|
|
65
65
|
h(Switch, { label: "被戳了戳回去", checked: ia.pokeBack, onChange: (v) => save({ interactions: { pokeBack: v } }) })),
|
|
@@ -69,7 +69,7 @@ window.__ModuleLoader__.load({
|
|
|
69
69
|
h(Switch, { label: "群里点表情回应", checked: ia.react, onChange: (v) => save({ interactions: { react: v } }) })),
|
|
70
70
|
Row({ title: "私聊显示正在输入" },
|
|
71
71
|
h(Switch, { label: "私聊显示正在输入", checked: ia.typing, onChange: (v) => save({ interactions: { typing: v } }) })),
|
|
72
|
-
Row({ title: "群里冷热",
|
|
72
|
+
Row({ title: "群里冷热", meta: ht.enabled ? `${Math.round(ht.windowSec / 60)} 分钟内有 ${ht.hotCount} 条、${ht.hotSpeakers} 个人在说就算热闹,插话概率 ×${ht.boost};她说完话 ${ht.coolMinutes} 分钟内慢慢恢复` : "插话只看概率和冷却" },
|
|
73
73
|
ht.enabled ? h(NumField, { ui, value: ht.hotCount, min: 3, max: 50, step: 1, label: "热闹的条数", onSave: (n) => save({ heat: { hotCount: n } }) }) : null,
|
|
74
74
|
ht.enabled ? Muted("条") : null,
|
|
75
75
|
h(Switch, { label: "群里冷热", checked: ht.enabled, onChange: (v) => save({ heat: { enabled: v } }) }))));
|
|
@@ -118,7 +118,7 @@ window.__ModuleLoader__.load({
|
|
|
118
118
|
return Group({ title: "聊天用哪个模型", aside: Muted("单个聊天 → 私聊 / 群聊默认 → DSH 默认") },
|
|
119
119
|
Row({ title: "私聊默认", desc: "QQ / B站 / 抖音私信" }, Select({ value: val(cfg.private), "aria-label": "私聊默认", onChange: (e) => post({ private: parse(e.target.value) }) }, ...options(cfg.private, hostText))),
|
|
120
120
|
Row({ title: "群聊默认" }, Select({ value: val(cfg.group), "aria-label": "群聊默认", onChange: (e) => post({ group: parse(e.target.value) }) }, ...options(cfg.group, hostText))),
|
|
121
|
-
...chats.map((c) => Row({ title: c.title, key: c.channelId,
|
|
121
|
+
...chats.map((c) => Row({ title: c.title, key: c.channelId, meta: c.override ? undefined : c.resolved ? `现在用 ${c.resolved.model}` : undefined },
|
|
122
122
|
Select({ value: val(c.override), "aria-label": `${c.title} 用的模型`, onChange: (e) => post({ channelId: c.channelId, selection: parse(e.target.value) }) },
|
|
123
123
|
...options(c.override, c.channelId.startsWith("group:") ? "跟群聊默认" : "跟私聊默认")))));
|
|
124
124
|
}
|
package/lib/client.js
CHANGED
|
@@ -10,9 +10,12 @@
|
|
|
10
10
|
* @deepseek-ai/dsh-client-ui-primitives 的结构、无障碍语义与样式(lib/panel.css,fs- 前缀,只用 --dsw-alias-* 令牌)
|
|
11
11
|
*
|
|
12
12
|
* 设计信条:Avoid unnecessary secondary labels and descriptions. Let the layout do the explaining.
|
|
13
|
-
* -
|
|
13
|
+
* - DeepSeek Harness 工作台骨架:可折叠侧栏(导航 + 版本)| 居中主任务流 | 按需展开的详情面板;记忆调试是一体化底部输入舱
|
|
14
|
+
* - 动态 / 整理记录渲染成单行步骤胶囊(<details>),默认收起,点开看原因与想起的记忆
|
|
15
|
+
* - 平铺紧凑行(标题左 / 控件右),1px 低对比分隔;Row 的 desc 只作悬停提示,meta(实时状态)用等宽小字
|
|
14
16
|
* - 状态 = 一颗 StateDot + 一个短句;只在出问题时多说一句
|
|
15
17
|
* - 依赖总开关的设置,总开关关着就不显示
|
|
18
|
+
* - 只改视图层:状态、事件、接口调用保持原样
|
|
16
19
|
*/
|
|
17
20
|
window.__ModuleLoader__.load({
|
|
18
21
|
id: "dsh-fish-soul",
|
|
@@ -88,7 +91,9 @@ window.__ModuleLoader__.load({
|
|
|
88
91
|
close: "M7 7l10 10M17 7 7 17",
|
|
89
92
|
refresh: "M20 11a8 8 0 1 0-2.3 5.7M20 5v6h-6",
|
|
90
93
|
chevron: "m7 10 5 5 5-5",
|
|
91
|
-
edit: "M4 20h4L18.5 9.5a2.1 2.1 0 0 0-4-4L4 16v4Z M13.5 6.5l4 4"
|
|
94
|
+
edit: "M4 20h4L18.5 9.5a2.1 2.1 0 0 0-4-4L4 16v4Z M13.5 6.5l4 4",
|
|
95
|
+
sidebar: "M4 5h16v14H4Z M9 5v14",
|
|
96
|
+
info: "M12 8h.01 M11 12h1v5h1 M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Z"
|
|
92
97
|
};
|
|
93
98
|
const Icon = (name, size = 16) => h("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
|
|
94
99
|
h("path", { d: ICONS[name] }));
|
|
@@ -114,19 +119,18 @@ window.__ModuleLoader__.load({
|
|
|
114
119
|
const index = Math.max(0, items.findIndex(([v]) => v === value));
|
|
115
120
|
const onKeyDown = (event, i) => {
|
|
116
121
|
const n = items.length;
|
|
117
|
-
const next = event.key === "ArrowLeft" ? (i + n - 1) % n : event.key === "ArrowRight" ? (i + 1) % n
|
|
122
|
+
const next = event.key === "ArrowLeft" || event.key === "ArrowUp" ? (i + n - 1) % n : event.key === "ArrowRight" || event.key === "ArrowDown" ? (i + 1) % n
|
|
118
123
|
: event.key === "Home" ? 0 : event.key === "End" ? n - 1 : -1;
|
|
119
124
|
if (next < 0) return;
|
|
120
125
|
event.preventDefault();
|
|
121
126
|
event.currentTarget.parentElement?.querySelectorAll('[role="tab"]').item(next)?.focus();
|
|
122
127
|
onChange(items[next][0]);
|
|
123
128
|
};
|
|
124
|
-
return h("
|
|
125
|
-
h("span", { className: "fs-tabs-ind", "aria-hidden": true }),
|
|
129
|
+
return h("nav", { className: "fs-nav", role: "tablist", "aria-label": "鱼", "aria-orientation": "vertical", style: { "--n": items.length, "--i": index } },
|
|
126
130
|
items.map(([v, label], i) => h("button", {
|
|
127
131
|
type: "button", key: v, role: "tab", id: `fs-tab-${v}`, "aria-selected": v === value, "aria-controls": "fs-tabpanel",
|
|
128
132
|
tabIndex: v === value ? 0 : -1, className: "fs-tab", title: label, onClick: () => onChange(v), onKeyDown: (e) => onKeyDown(e, i)
|
|
129
|
-
}, label)));
|
|
133
|
+
}, h("span", { className: "fs-tab-key", "aria-hidden": true }, TAB_KEYS[v] ?? label.slice(0, 1)), h("span", { className: "fs-tab-label" }, label))));
|
|
130
134
|
}
|
|
131
135
|
|
|
132
136
|
/** StateDot:done / warning / error / idle 是圆点,ongoing 是转圈。 */
|
|
@@ -138,9 +142,15 @@ window.__ModuleLoader__.load({
|
|
|
138
142
|
const Tag = (text, tone, extra, ...kids) => h("span", { className: "fs-tag", "data-tone": tone, ...extra }, text, ...kids);
|
|
139
143
|
const Muted = (text, extra) => h("span", { className: "fs-unit", ...extra }, text);
|
|
140
144
|
|
|
141
|
-
|
|
145
|
+
// 行:标题左 / 控件右。desc(解释性文字)不占版面,只作悬停提示;meta(实时状态)以等宽小字跟在标题下
|
|
146
|
+
/** @param {{ title: any, desc?: any, meta?: any, stack?: boolean }} props @param {...any} kids */
|
|
147
|
+
function Row({ title, desc, meta, stack }, ...kids) {
|
|
148
|
+
const tip = typeof desc === "string" ? desc : undefined;
|
|
142
149
|
return h("div", { className: cx("fs-row", stack && "stack") },
|
|
143
|
-
h("div", { className: "fs-row-title" },
|
|
150
|
+
h("div", { className: "fs-row-title" },
|
|
151
|
+
h("span", { className: cx("fs-row-label", tip && "has-tip"), title: tip }, title),
|
|
152
|
+
desc && !tip ? h("span", { className: "fs-row-desc" }, desc) : null,
|
|
153
|
+
meta ? h("span", { className: "fs-row-meta" }, meta) : null),
|
|
144
154
|
h("div", { className: "fs-ctl" }, ...kids));
|
|
145
155
|
}
|
|
146
156
|
function Group({ title, aside }, ...kids) {
|
|
@@ -272,7 +282,7 @@ window.__ModuleLoader__.load({
|
|
|
272
282
|
h(Switch, { label: "自动整理", checked: d.autoExtract, onChange: (v) => save({ autoExtract: v }) })),
|
|
273
283
|
d.autoExtract ? Row({ title: "空多久整理" },
|
|
274
284
|
Num({ min: 1, max: 240, value: idle, onChange: (e) => setIdle(e.target.value), onBlur: () => { const n = Number(idle); if (n >= 1 && n !== d.idleMinutes) save({ idleMinutes: n }); } }), Muted("分钟")) : null,
|
|
275
|
-
Row({ title: "上次",
|
|
285
|
+
Row({ title: "上次", meta: lastText },
|
|
276
286
|
Button({ disabled: busy || d.running || d.pending === 0, onClick: now }, busy ? "整理中…" : "现在整理")));
|
|
277
287
|
}
|
|
278
288
|
|
|
@@ -349,7 +359,7 @@ window.__ModuleLoader__.load({
|
|
|
349
359
|
const [busy, setBusy] = react.useState(false);
|
|
350
360
|
const [form, setForm, patch] = useForm(data, () => ({
|
|
351
361
|
enabled: qq.enabled === true, wsUrl: qq.wsUrl ?? "ws://127.0.0.1:3001", accessToken: "", masterQqUin: data.masterQqUin ?? "",
|
|
352
|
-
groupAllowList: (qq.groupAllowList ?? []).join(", "), allGroups: qq.allGroups === true, followUpCount: qq.followUpCount ?? 3, followUpSeconds: qq.followUpSeconds ?? 120, batchMs: qq.batchMs ?? 3000,
|
|
362
|
+
groupAllowList: (qq.groupAllowList ?? []).join(", "), allGroups: qq.allGroups === true, followUpCount: qq.followUpCount ?? 3, followUpSeconds: qq.followUpSeconds ?? 120, followUpSmart: qq.followUpSmart === true, batchMs: qq.batchMs ?? 3000,
|
|
353
363
|
callNames: (qq.callNames ?? []).join(", "), ij: { ...(qq.interject ?? {}), chance: Math.round((qq.interject?.chance ?? 0.05) * 100), keywords: (qq.interject?.keywords ?? []).join(", ") }
|
|
354
364
|
}));
|
|
355
365
|
if (!data || !form) return null;
|
|
@@ -360,8 +370,8 @@ window.__ModuleLoader__.load({
|
|
|
360
370
|
const res = await api("POST", "/channels/qq-config", {
|
|
361
371
|
enabled: next.enabled, wsUrl: next.wsUrl, ...(next.accessToken !== "" ? { accessToken: next.accessToken } : {}),
|
|
362
372
|
masterQqUin: next.masterQqUin, groupAllowList: split(next.groupAllowList), allGroups: next.allGroups === true,
|
|
363
|
-
followUpCount: Number(next.followUpCount), followUpSeconds: Number(next.followUpSeconds), batchMs: Number(next.batchMs), callNames: split(next.callNames),
|
|
364
|
-
interject: { enabled: next.ij.enabled === true, chance: Number(next.ij.chance) / 100, cooldownMinutes: Number(next.ij.cooldownMinutes), maxPerHour: Number(next.ij.maxPerHour), keywords: split(next.ij.keywords) }
|
|
373
|
+
followUpCount: Number(next.followUpCount), followUpSeconds: Number(next.followUpSeconds), followUpSmart: next.followUpSmart === true, batchMs: Number(next.batchMs), callNames: split(next.callNames),
|
|
374
|
+
interject: { enabled: next.ij.enabled === true, chance: Number(next.ij.chance) / 100, cooldownMinutes: Number(next.ij.cooldownMinutes), maxPerHour: Number(next.ij.maxPerHour), keywords: split(next.ij.keywords), masterQuiet: next.ij.masterQuiet !== false, masterQuietMinutes: Number(next.ij.masterQuietMinutes ?? 5) }
|
|
365
375
|
});
|
|
366
376
|
if (res?.ok) { if (next.accessToken !== "") setForm({ ...next, accessToken: "" }); refresh(); } else notify(res?.error ?? "保存失败");
|
|
367
377
|
}
|
|
@@ -393,12 +403,18 @@ window.__ModuleLoader__.load({
|
|
|
393
403
|
form.enabled ? Row({ title: "点名后接着听" },
|
|
394
404
|
Num({ min: 0, max: 20, value: form.followUpCount, "aria-label": "条数", onChange: (e) => patch({ followUpCount: e.target.value }), onBlur: () => save() }), Muted("条"),
|
|
395
405
|
Num({ min: 0, max: 1800, value: form.followUpSeconds, "aria-label": "秒数", onChange: (e) => patch({ followUpSeconds: e.target.value }), onBlur: () => save() }), Muted("秒")) : null,
|
|
406
|
+
form.enabled ? Row({ title: "聊完自己收尾", desc: "她觉得这轮聊完了就不接着听,群里插话也压下去" },
|
|
407
|
+
h(Switch, { label: "聊完自己收尾", checked: form.followUpSmart === true, onChange: (v) => save({ followUpSmart: v }) })) : null,
|
|
396
408
|
form.enabled ? Row({ title: "连着发几条时", desc: "你连着发消息,她等你停手再一起回;0 = 每条都回" },
|
|
397
409
|
Num({ min: 0, max: 15000, step: 500, value: form.batchMs, "aria-label": "等待毫秒", onChange: (e) => patch({ batchMs: e.target.value }), onBlur: () => save({ batchMs: form.batchMs === "" ? 3000 : Number(form.batchMs) }) }), Muted("毫秒")) : null,
|
|
398
410
|
form.enabled ? Row({ title: "叫这些名字也算点名" }, field("callNames", { placeholder: "人格卡名字" })) : null,
|
|
399
411
|
form.enabled ? Row({ title: "群里主动插话" }, h(Switch, { label: "群里主动插话", checked: ij.enabled === true, onChange: (v) => save({ ij: { ...ij, enabled: v } }) })) : null,
|
|
400
412
|
form.enabled && ij.enabled ? Row({ title: "插话概率" }, ijNum("chance", "插话概率", 0, 100), Muted("%"), ijNum("maxPerHour", "每小时最多", 0, 30), Muted("次/小时")) : null,
|
|
401
413
|
form.enabled && ij.enabled ? Row({ title: "说完话后冷却" }, ijNum("cooldownMinutes", "冷却", 0, 1440), Muted("分钟")) : null,
|
|
414
|
+
form.enabled && ij.enabled ? Row({ title: "主人在聊时不插话", desc: "主人刚跟她说过话,她在群里就少插几句" },
|
|
415
|
+
h(Switch, { label: "主人在聊时不插话", checked: ij.masterQuiet !== false, onChange: (v) => save({ ij: { ...ij, masterQuiet: v } }) }),
|
|
416
|
+
ij.masterQuiet !== false ? ijNum("masterQuietMinutes", "分钟", 0, 60) : null,
|
|
417
|
+
ij.masterQuiet !== false ? Muted("分钟内") : null) : null,
|
|
402
418
|
form.enabled && ij.enabled ? Row({ title: "感兴趣的话题" }, Input({ value: ij.keywords ?? "", placeholder: "猫, 原神, 考研", onChange: (e) => patch({ ij: { ...ij, keywords: e.target.value } }), onBlur: () => save() })) : null,
|
|
403
419
|
form.enabled ? Row({ title: "收藏表情包", desc: qq.stickers?.collect !== false ? undefined : "看到喜欢的表情会存下来,之后自己用" }, h(Switch, { label: "收藏表情包", checked: qq.stickers?.collect !== false, onChange: (v) => run("/stickers/config", { collect: v }, v ? "会收藏喜欢的表情了" : "不再收藏表情", "保存失败") })) : null,
|
|
404
420
|
form.enabled ? Row({ title: "学群里的说法", desc: qq.slang?.enabled !== false ? undefined : "学每个群的梗和说话风格" }, h(Switch, { label: "学群里的说法", checked: qq.slang?.enabled !== false, onChange: (v) => run("/slang/config", { enabled: v }, v ? "会学群里的说法了" : "不再学群里的说法", "保存失败") })) : null,
|
|
@@ -412,7 +428,29 @@ window.__ModuleLoader__.load({
|
|
|
412
428
|
elsewhere > 0 ? Row({ title: `${elsewhere} 个会话在别的工作区` }, Button({ disabled: busy, onClick: () => run("/sessions/migrate", {}, (r) => `已迁移 ${r.migrated} 个${r.failed?.length ? `,${r.failed.length} 个失败` : ""}`, "迁移失败") }, "迁过来")) : null,
|
|
413
429
|
chats.length > 0 ? h("div", { className: "fs-list" }, chats.slice(0, 30).map((c) => Item({ key: c.sessionId, text: c.title || c.sessionId, meta: [
|
|
414
430
|
c.paused ? Tag("暂停中", "warning", { key: "p" }) : null, h("span", { key: "c" }, c.channelId),
|
|
415
|
-
Button({ key: "b", variant: "ghost", disabled: busy, onClick: () => pause(c) }, c.paused ? "恢复" : "暂停")] }))) : null)
|
|
431
|
+
Button({ key: "b", variant: "ghost", disabled: busy, onClick: () => pause(c) }, c.paused ? "恢复" : "暂停")] }))) : null),
|
|
432
|
+
h(DiagnoseGroup, { notify }));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// 诊断:只读一键自检,六项结果绿 / 红;失败项自己带原因,不抛错
|
|
436
|
+
function DiagnoseGroup({ notify }) {
|
|
437
|
+
const [busy, setBusy] = react.useState(false);
|
|
438
|
+
const [data, setData] = react.useState(null);
|
|
439
|
+
async function run() {
|
|
440
|
+
setBusy(true);
|
|
441
|
+
const res = await api("GET", "/diagnose");
|
|
442
|
+
setBusy(false);
|
|
443
|
+
if (res?.items) setData(res); else notify(res?.error ?? "自检没跑成");
|
|
444
|
+
}
|
|
445
|
+
const items = data?.items ?? [];
|
|
446
|
+
const bad = items.filter((x) => x.ok !== true).length;
|
|
447
|
+
return Group({ title: "诊断", aside: items.length > 0 ? Tag(bad === 0 ? "都正常" : `${bad} 项要处理`, bad === 0 ? "success" : "warning") : null },
|
|
448
|
+
Row({ title: "一键自检", desc: "版本 / 通道 / QQ 登录 / 记忆条数 / 今日次数 / 补丁,只读不改任何东西" },
|
|
449
|
+
Button({ onClick: run, disabled: busy }, busy ? "检查中…" : "开始检查")),
|
|
450
|
+
items.length > 0 ? h("div", { className: "fs-list" }, items.map((it) => h("div", { className: "fs-item", key: it.key },
|
|
451
|
+
h("span", { className: "fs-dot", "data-state": it.ok === true ? "done" : "error", "aria-hidden": true }),
|
|
452
|
+
h("span", { className: cx("fs-item-main", it.ok === true && "fs-muted") }, it.label),
|
|
453
|
+
h("span", { className: "fs-item-meta", title: it.hint ?? undefined }, it.detail)))) : null);
|
|
416
454
|
}
|
|
417
455
|
|
|
418
456
|
// ── B站(登录 / 私信 / 自己刷视频) ──
|
|
@@ -608,6 +646,7 @@ window.__ModuleLoader__.load({
|
|
|
608
646
|
}
|
|
609
647
|
|
|
610
648
|
// ── 面板根组件:页签 + 数据装载 + 轮询 ──
|
|
649
|
+
const TAB_KEYS = { retrieval: "索", persona: "人", life: "自", inspector: "忆", activity: "动", behavior: "习", models: "模", qq: "Q", bili: "B", douyin: "抖" };
|
|
611
650
|
const TABS = [["retrieval", "记忆检索"], ["persona", "人格"], ["life", "自走"], ["inspector", "记忆检视"], ["activity", "动态"], ["behavior", "习惯"], ["models", "模型"], ["qq", "QQ"], ["bili", "B站"], ["douyin", "抖音"]];
|
|
612
651
|
|
|
613
652
|
// ── 首次引导:没点「不再显示」就在页签上面列一张清单 ──
|
|
@@ -621,16 +660,16 @@ window.__ModuleLoader__.load({
|
|
|
621
660
|
if (r?.ok) setData({ ...data, dismissed: true }); else notify(r?.error ?? "操作失败");
|
|
622
661
|
}
|
|
623
662
|
const left = data.steps.filter((x) => !x.done && !x.optional).length;
|
|
624
|
-
return Group({ title: left === 0 ? "
|
|
663
|
+
return Group({ title: left === 0 ? "就绪" : `开始 · ${data.steps.length - left}/${data.steps.length}`, aside: IconButton("close", "不再显示", dismiss) },
|
|
625
664
|
h("div", { className: "fs-list" }, data.steps.map((x) => h("div", { className: "fs-item", key: x.id },
|
|
626
665
|
h("span", { className: "fs-dot", "data-state": x.done ? "done" : x.optional ? "idle" : "warning", "aria-hidden": true }),
|
|
627
666
|
h("span", { className: cx("fs-item-main", x.done && "fs-muted") }, x.title),
|
|
628
667
|
h("span", { className: "fs-item-meta" },
|
|
629
668
|
x.optional && !x.done ? Tag("可选", "outline") : null,
|
|
630
|
-
x.done ?
|
|
669
|
+
x.done ? null : x.tab ? Button({ variant: "ghost", onClick: () => onGo(x.tab) }, "去设置") : null)))));
|
|
631
670
|
}
|
|
632
671
|
// 版本与更新:显示当前版本,自动 / 手动去 npm(不通再 GitHub)看有没有新版;只提示,不替你装
|
|
633
|
-
function UpdateBar({ notify }) {
|
|
672
|
+
function UpdateBar({ notify, compact, onOpen }) {
|
|
634
673
|
const [data, setData] = react.useState(null);
|
|
635
674
|
const [busy, setBusy] = react.useState(false);
|
|
636
675
|
const load = react.useCallback(async () => { const r = await api("GET", "/update/state"); if (r?.ok) setData(r); }, []);
|
|
@@ -650,20 +689,23 @@ window.__ModuleLoader__.load({
|
|
|
650
689
|
async function copy() {
|
|
651
690
|
try { await navigator.clipboard.writeText(data.command); notify("已复制更新命令"); } catch { notify("复制失败,手动选中复制"); }
|
|
652
691
|
}
|
|
692
|
+
if (compact) return h("button", { type: "button", className: "fs-ver", onClick: onOpen, title: data.hasUpdate ? `v${data.latest}` : undefined, "aria-label": "版本" },
|
|
693
|
+
h("span", { className: "fs-dot", "data-state": data.hasUpdate ? "warning" : data.latest ? "done" : "idle", "aria-hidden": true }),
|
|
694
|
+
h("span", { className: "fs-tab-label num" }, `v${data.current}`));
|
|
653
695
|
const when = data.checkedAt ? new Date(data.checkedAt).toLocaleString("zh-CN", { hour12: false }) : "";
|
|
654
696
|
const status = data.hasUpdate ? Tag(`有新版本 v${data.latest}`, "warning")
|
|
655
697
|
: data.latest ? Tag("已是最新", "success")
|
|
656
698
|
: data.error ? Tag("没查成", "outline", { title: data.error }) : Tag("还没检查", "outline");
|
|
657
699
|
// Group / Row 是普通函数(第二个参数起是子元素),不能当组件(createElement)用:
|
|
658
700
|
// 宿主的 React 会把 legacy context({})当第二个参数传进来,变成一个对象子元素,整个面板崩成空白
|
|
659
|
-
return Group({ title: "版本", aside: h("span", { className: "fs-unit" }, `v${data.current}`) },
|
|
660
|
-
Row({ title:
|
|
701
|
+
return Group({ title: "版本", aside: h("span", { className: "fs-unit num" }, `v${data.current}`) },
|
|
702
|
+
Row({ title: status, meta: when ? `${when}${data.source ? ` · ${data.source}` : ""}` : undefined, desc: "每 12 小时自动看一次 npm / GitHub" },
|
|
661
703
|
Button({ onClick: check, disabled: busy || data.checking }, busy || data.checking ? "检查中…" : "检查更新"),
|
|
662
704
|
data.hasUpdate ? Button({ onClick: () => window.open(data.notesUrl, "_blank", "noopener") }, "更新说明") : null,
|
|
663
705
|
data.hasUpdate && data.command ? Button({ variant: "primary", onClick: copy, title: data.command }, "复制更新命令") : null),
|
|
664
|
-
data.hasUpdate ? (data.notes ? h("pre", { className: "fs-
|
|
665
|
-
: h("p", { className: "fs-row-
|
|
666
|
-
h("a", { href: data.notesUrl, target: "_blank", rel: "noopener noreferrer" }, "
|
|
706
|
+
data.hasUpdate ? (data.notes ? h("pre", { className: "fs-report" }, data.notes)
|
|
707
|
+
: h("p", { className: "fs-row-meta" },
|
|
708
|
+
h("a", { href: data.notesUrl, target: "_blank", rel: "noopener noreferrer" }, "GitHub Releases"))) : null,
|
|
667
709
|
Row({ title: "自动检查更新", desc: "只提示,不会自动安装;更新后重启 DSH 生效" },
|
|
668
710
|
h(Switch, { checked: data.autoCheck !== false, onChange: toggle, label: "自动检查更新" })));
|
|
669
711
|
}
|
|
@@ -687,6 +729,8 @@ window.__ModuleLoader__.load({
|
|
|
687
729
|
const [life, setLife] = react.useState(null);
|
|
688
730
|
const [channels, setChannels] = react.useState(null);
|
|
689
731
|
const [toast, setToast] = react.useState(null);
|
|
732
|
+
const [navOpen, setNavOpen] = react.useState(true);
|
|
733
|
+
const [detail, setDetail] = react.useState(null);
|
|
690
734
|
const notify = react.useCallback((text) => setToast({ key: Date.now(), text }), []);
|
|
691
735
|
|
|
692
736
|
react.useEffect(() => {
|
|
@@ -728,13 +772,24 @@ window.__ModuleLoader__.load({
|
|
|
728
772
|
: tab === "bili" ? h(BiliPane, { data: channels, refresh: loadChannels, notify })
|
|
729
773
|
: h(ChunkTab, { file: "client.douyin.js", pick: (m) => m.DouyinPane, failText: "抖音页加载失败", data: channels, refresh: loadChannels, notify });
|
|
730
774
|
|
|
731
|
-
|
|
775
|
+
// 工作台骨架:可折叠侧栏(导航 + 版本)| 主任务流(引导 + 当前页)| 按需展开的详情面板(版本与更新)
|
|
776
|
+
const label = TABS.find(([v]) => v === tab)?.[1] ?? "";
|
|
777
|
+
return h("div", { className: "fs-host" }, h("div", { className: cx("fs", navOpen ? null : "nav-collapsed", detail ? "has-detail" : null) },
|
|
732
778
|
css === "" ? null : h("style", { "data-plugin-css": `${PLUGIN_ID}/panel.css` }, css),
|
|
733
|
-
h(
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
779
|
+
h("aside", { className: "fs-side" },
|
|
780
|
+
h("div", { className: "fs-side-head" },
|
|
781
|
+
h("span", { className: "fs-tab-label fs-brand" }, "鱼"),
|
|
782
|
+
IconButton("sidebar", navOpen ? "收起侧栏" : "展开侧栏", () => setNavOpen(!navOpen), { "aria-expanded": navOpen })),
|
|
783
|
+
h(Tabs, { items: TABS, value: tab, onChange: setTab }),
|
|
784
|
+
h("div", { className: "fs-side-foot" }, h(Boundary, { name: "版本" }, h(UpdateBar, { notify, compact: true, onOpen: () => setDetail(detail === "version" ? null : "version") })))),
|
|
785
|
+
h("main", { className: "fs-main" },
|
|
786
|
+
h("header", { className: "fs-main-head" }, h("h2", { className: "fs-main-title" }, label)),
|
|
787
|
+
h(Boundary, { name: "引导" }, h(Onboarding, { onGo: setTab, notify })),
|
|
788
|
+
h("div", { className: "fs-pane", role: "tabpanel", id: "fs-tabpanel", "aria-labelledby": `fs-tab-${tab}`, key: tab }, h(Boundary, { name: "这一页" }, pane))),
|
|
789
|
+
detail === "version" ? h("aside", { className: "fs-detail", "aria-label": "版本" },
|
|
790
|
+
h("div", { className: "fs-detail-head" }, h("span", null, "版本"), IconButton("close", "关闭", () => setDetail(null))),
|
|
791
|
+
h(Boundary, { name: "版本" }, h(UpdateBar, { notify }))) : null,
|
|
792
|
+
toast === null ? null : h("div", { className: "fs-toast", role: "status", key: toast.key }, toast.text)));
|
|
738
793
|
}
|
|
739
794
|
|
|
740
795
|
function apply(ctx) {
|
package/lib/client.models.js
CHANGED
|
@@ -47,7 +47,7 @@ window.__ModuleLoader__.load({
|
|
|
47
47
|
return Group({ title: "按功能选模型", aside: host ? Muted(`DSH 默认:${providerName(host.provider)} · ${host.model}`) : null },
|
|
48
48
|
providers.length === 0 && !host ? Row({ title: "可用模型" }, State("warning", "DSH 里还没配模型")) : null,
|
|
49
49
|
data.lastError ? Row({ title: "上次调用" }, State("error", data.lastError)) : null,
|
|
50
|
-
...(data.features ?? []).map((f) => Row({ title: f.label, key: f.id,
|
|
50
|
+
...(data.features ?? []).map((f) => Row({ title: f.label, key: f.id, meta: f.resolved ? (f.selection ? undefined : `现在用 ${f.resolved.model}`) : "没有可用的模型" },
|
|
51
51
|
Select({ value: f.selection ? `${f.selection.provider}${SEP}${f.selection.model}` : "", "aria-label": f.label, onChange: (e) => pick(f.id, e.target.value) }, ...options(f.selection)))));
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -73,7 +73,7 @@ window.__ModuleLoader__.load({
|
|
|
73
73
|
const t = u.total ?? { calls: 0, in: 0, out: 0, cost: 0 };
|
|
74
74
|
return h(react.Fragment, null,
|
|
75
75
|
Group({ title: "用量", aside: h(Seg, { label: "时间范围", value: range, onChange: setRange, options: [{ value: "7", label: "7 天" }, { value: "30", label: "30 天" }] }) },
|
|
76
|
-
Row({ title: "合计",
|
|
76
|
+
Row({ title: "合计", meta: estimated ? "有的模型没报用量,按字数估的" : undefined },
|
|
77
77
|
Muted(`${t.calls} 次 · 输入 ${fmtTok(t.in)} · 输出 ${fmtTok(t.out)} token`), anyPrice ? Tag(`约 ${fmtCost(t.cost)}`, "info") : null),
|
|
78
78
|
features.length === 0 ? Empty("这段时间还没调用过模型") : h("div", { className: "fs-list" }, features.map(([id, f]) => h("div", { className: "fs-item", key: id },
|
|
79
79
|
h("span", { className: "fs-item-main" }, labels[id] ?? id),
|
|
@@ -120,7 +120,7 @@ window.__ModuleLoader__.load({
|
|
|
120
120
|
const own = cur.id !== "host";
|
|
121
121
|
const why = cur.id === "host" ? "DSH 里没有联网搜索工具(web_search),可以换成自己的搜索服务" : cur.needsBaseURL ? "先填 SearXNG 地址" : "先填 API Key";
|
|
122
122
|
return Group({ title: "上网查" },
|
|
123
|
-
Row({ title: "聊天时可以上网查",
|
|
123
|
+
Row({ title: "聊天时可以上网查", meta: w.available ? undefined : why },
|
|
124
124
|
w.enabled && w.available ? State(w.usedToday >= w.perDay ? "warning" : "done", `今天 ${w.usedToday}/${w.perDay}`) : null,
|
|
125
125
|
h(Switch, { label: "聊天时可以上网查", checked: w.enabled === true, disabled: !w.available && !w.enabled, onChange: (v) => save({ enabled: v }) })),
|
|
126
126
|
Row({ title: "聊天会话能用哪些工具", desc: "只放记忆和上网最安全;「只对主人放开」是主人的私聊会话放开宿主全部工具(其他人不变),「全放开」是所有聊天会话都放。放开后已开着的会话要在 DSH 里重开一次才生效。run_code 始终不放行" },
|
|
@@ -135,7 +135,7 @@ window.__ModuleLoader__.load({
|
|
|
135
135
|
h(Secret, { label: "API Key", value: key, placeholder: w.hasApiKey ? w.apiKeyMasked : "粘贴 key", onChange: (e) => setKey(e.target.value), onBlur: () => { if (key.trim() !== "") save({ apiKey: key.trim() }); } })) : null,
|
|
136
136
|
own && cur.needsBaseURL ? Row({ title: "SearXNG 地址" },
|
|
137
137
|
Input({ value: baseURL, placeholder: "http://127.0.0.1:8080", "aria-label": "SearXNG 地址", onChange: (e) => setBaseURL(e.target.value), onBlur: () => { if (baseURL !== (w.baseURL ?? "")) save({ baseURL }); } })) : null,
|
|
138
|
-
own && w.available ? Row({ title: "测一下",
|
|
138
|
+
own && w.available ? Row({ title: "测一下", meta: result ? `搜到 ${result.count} 条 · ${(result.latencyMs / 1000).toFixed(1)} 秒${result.results?.[0] ? ` · 第一条:${result.results[0].title}` : ""}` : "搜一次「今天有什么新闻」,不算今天的次数" },
|
|
139
139
|
Button({ disabled: busy, onClick: test }, busy ? "搜索中" : "测一下")) : null,
|
|
140
140
|
w.enabled ? Row({ title: "每天最多" }, Num({ min: 0, max: 500, value: perDay, "aria-label": "每天最多", onChange: (e) => setPerDay(e.target.value), onBlur: () => save({}) }), Muted("次")) : null);
|
|
141
141
|
}
|
package/lib/client.persona.js
CHANGED
|
@@ -138,7 +138,7 @@ window.__ModuleLoader__.load({
|
|
|
138
138
|
cfg.enabled ? Row({ title: "证据够了直接生效", desc: cfg.autoApply ? "关掉的话,先放在这里等你确认" : "证据够了也先等你确认" },
|
|
139
139
|
h(Switch, { label: "证据够了直接生效", checked: cfg.autoApply, onChange: (v) => post({ autoApply: v }, "已保存") })) : null,
|
|
140
140
|
list.length === 0 ? Row({ title: "还没有" }, Muted("聊得多了才会慢慢有;也可以自己加一条")) : null,
|
|
141
|
-
...list.map((i) => Row({ key: i.id, title: i.text,
|
|
141
|
+
...list.map((i) => Row({ key: i.id, title: i.text, meta: `${i.label} · ${evid(i)}${i.retiredReason ? ` · ${i.retiredReason}` : ""}` },
|
|
142
142
|
Tag(...(STATUS[i.status] ?? STATUS.candidate)),
|
|
143
143
|
i.status === "pending" || i.status === "candidate" ? Button({ variant: "ghost", disabled: busy, onClick: () => post({ action: "approve", id: i.id }, "已生效") }, "就这样") : null,
|
|
144
144
|
IconButton("close", "删掉这条", () => post({ action: "remove", id: i.id }, "已删掉")))),
|
|
@@ -270,7 +270,7 @@ window.__ModuleLoader__.load({
|
|
|
270
270
|
? Row({ title: "设定" }, Muted("只有一句话,挑个模板或者导入自己的角色卡会像真人很多"))
|
|
271
271
|
: Row({ title: "一句话人设", stack: true }, h("p", { className: "fs-persona-desc" }, cur.anchor || "没写(会用性格那一栏顶上)")),
|
|
272
272
|
cur.source !== "builtin" && (cur.perspective ?? []).length > 0 ? Row({ title: "看事情的方式", stack: true }, h("p", { className: "fs-persona-desc" }, cur.perspective.join(";"))) : null,
|
|
273
|
-
cur.source !== "builtin" ? Row({ title: "作息",
|
|
273
|
+
cur.source !== "builtin" ? Row({ title: "作息", meta: cur.routine ? undefined : "没写:排日程时只能按设定猜", stack: Boolean(cur.routine) }, cur.routine ? h("p", { className: "fs-persona-desc" }, cur.routine) : null) : null,
|
|
274
274
|
Row({ title: "改人格" },
|
|
275
275
|
Button({ onClick: () => setEditing(cur.card ?? blank) }, cur.card ? "编辑" : "从空白写"),
|
|
276
276
|
Button({ onClick: () => fileRef.current?.click(), disabled: busy }, "导入角色卡"),
|
package/lib/client.soul.js
CHANGED
|
@@ -119,7 +119,7 @@ window.__ModuleLoader__.load({
|
|
|
119
119
|
p.stageLocked && p.canonicalId !== "master" ? Button({ variant: "ghost", disabled: busy, onClick: () => save({ stageLocked: false }, "关系交还给好感了") }, "改回自动") : null,
|
|
120
120
|
Select({ value: p.relationStage, disabled: busy, "aria-label": "关系", onChange: (e) => save({ relationStage: e.target.value }, `关系改成「${stageLabel(e.target.value)}」`) },
|
|
121
121
|
...STAGES.map(([k, v]) => h("option", { key: k, value: k }, v)))),
|
|
122
|
-
Row({ title: "好感",
|
|
122
|
+
Row({ title: "好感", meta: mood ? `她现在有点${mood.label}${mood.reason ? `(${mood.reason})` : ""}` : "聊完会慢慢涨跌,太久不联系会淡" },
|
|
123
123
|
h("div", { className: "fs-track", role: "meter", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": p.affinity ?? 0, "aria-label": "好感" },
|
|
124
124
|
h("div", { className: "fs-fill", style: { width: `${Math.max(0, Math.min(100, p.affinity ?? 0))}%` } })),
|
|
125
125
|
Muted(String(Math.round(p.affinity ?? 0)), { className: "fs-unit num" })),
|
|
@@ -127,7 +127,7 @@ window.__ModuleLoader__.load({
|
|
|
127
127
|
lockedF.has(f.id) ? Tag("手写", "info", { title: "主人写的,Dream 不改;清空就交还" }) : null,
|
|
128
128
|
Input({ value: form.fields[f.id] ?? "", placeholder: "还不知道", "aria-label": f.label, disabled: busy,
|
|
129
129
|
onChange: (e) => setForm({ ...form, fields: { ...form.fields, [f.id]: e.target.value } }), onBlur: () => saveField(f.id) })))) : null,
|
|
130
|
-
form && !legacy ? Row({ title: "印象",
|
|
130
|
+
form && !legacy ? Row({ title: "印象", meta: prof ? `${prof.by === "user" ? "改过" : "Dream 整理"} · ${fmtDay(prof.updatedAt)}` : "聊得多了会自己整理" },
|
|
131
131
|
Button({ variant: "ghost", disabled: busy, onClick: dream }, "现在整理一次")) : null,
|
|
132
132
|
form && legacy ? Row({ title: "印象", stack: true },
|
|
133
133
|
Area({ value: form.profile, rows: 3, placeholder: "还没有印象:聊得多了 Dream 会写,也可以自己写", "aria-label": "印象", onChange: (e) => setForm({ ...form, profile: e.target.value }) }),
|
|
@@ -252,14 +252,14 @@ window.__ModuleLoader__.load({
|
|
|
252
252
|
Button({ disabled: busy || dbPath.trim() === "", onClick: () => preview({ dbPath }) }, "读聊天记录"),
|
|
253
253
|
Button({ variant: "ghost", disabled: busy || dbPath.trim() === "", onClick: previewExtras, title: "表情包 → _stickers;表达方式 / 黑话 → 学到的说法" }, "读表情和说法")) : null,
|
|
254
254
|
mx && showDb && !running ? h(react.Fragment, null,
|
|
255
|
-
Row({ title: `表情包 ${mx.emojis.count} 张`,
|
|
255
|
+
Row({ title: `表情包 ${mx.emojis.count} 张`, meta: mx.emojis.sample.length ? `比如:${mx.emojis.sample.join("、")}。按麦麦里用得多的挑;她每轮只看得到 _stickers 里前 40 张` : "没找到能用的表情(被禁用 / 未注册 / 图片文件不在的跳过)" },
|
|
256
256
|
Num({ min: 0, max: 100, value: mxOpts.maxEmojis, "aria-label": "最多导入几张", onChange: (e) => setMxOpts({ ...mxOpts, maxEmojis: e.target.value }) }), Muted("张"),
|
|
257
257
|
h(Switch, { label: "导入表情包", checked: mxOpts.emojis && mx.emojis.count > 0, disabled: mx.emojis.count === 0, onChange: (v) => setMxOpts({ ...mxOpts, emojis: v }) })),
|
|
258
|
-
Row({ title: `表达方式 ${mx.expressions.count} 条`,
|
|
258
|
+
Row({ title: `表达方式 ${mx.expressions.count} 条`, meta: mx.expressions.sample.join(";") || undefined },
|
|
259
259
|
h(Switch, { label: "导入表达方式", checked: mxOpts.expressions && mx.expressions.count > 0, disabled: mx.expressions.count === 0, onChange: (v) => setMxOpts({ ...mxOpts, expressions: v }) })),
|
|
260
|
-
Row({ title: `黑话 ${mx.jargons.count} 个`,
|
|
260
|
+
Row({ title: `黑话 ${mx.jargons.count} 个`, meta: mx.jargons.sample.join(";") || undefined },
|
|
261
261
|
h(Switch, { label: "导入黑话", checked: mxOpts.jargons && mx.jargons.count > 0, disabled: mx.jargons.count === 0, onChange: (v) => setMxOpts({ ...mxOpts, jargons: v }) })),
|
|
262
|
-
Row({ title: "分到这些聊天",
|
|
262
|
+
Row({ title: "分到这些聊天", meta: mx.chats.map((c) => `${c.title}(${c.expressions} / ${c.jargons})`).join("、") + "。每个聊天最多留 60 种说话方式、40 个梗,按次数留" },
|
|
263
263
|
...(mx.warnings ?? []).map((w, i) => Tag(w, "warning", { key: i })),
|
|
264
264
|
Button({ variant: "ghost", onClick: () => setMx(null) }, "取消"),
|
|
265
265
|
Button({ variant: "primary", disabled: busy, onClick: applyExtras }, "导入"))) : null,
|
|
@@ -277,27 +277,27 @@ window.__ModuleLoader__.load({
|
|
|
277
277
|
onChange: (e) => setOpts({ ...opts, qq: { ...opts.qq, [s.key]: e.target.value } }) }))))) : null,
|
|
278
278
|
pv.coverage && pv.coverage.droppedChunks > 0 ? Row({
|
|
279
279
|
title: `${pv.coverage.truncatedPersons} 个人的较早聊天按时间段总结`,
|
|
280
|
-
|
|
280
|
+
meta: `每人最近 ${pv.coverage.chunksPerPerson} 段(每段约 ${pv.coverage.chunkChars} 字)逐段提炼;更早的 ${pv.coverage.droppedChunks} 段、约 ${Math.round(pv.coverage.droppedChars / 1000) / 10} 万字` +
|
|
281
281
|
(pv.coverage.droppedFromMs ? `(最早到 ${new Date(pv.coverage.droppedFromMs).toLocaleDateString()})` : "") +
|
|
282
282
|
`压成 ${pv.coverage.eraChunks ?? 0} 个时间段总结(每人最多 ${pv.coverage.maxEras ?? 4} 段,聊天太多时只看抽样,细节会丢)。` +
|
|
283
283
|
(pv.coverage.topTruncated ?? []).map((t) => `${t.name}:${t.droppedChunks} 段压成总结${t.keptFromMs ? `,${new Date(t.keptFromMs).toLocaleDateString()} 之后逐段` : ""}`).join(";")
|
|
284
284
|
}, Tag("早期压缩", "warning")) : null,
|
|
285
285
|
pv.coverage && Number(opts.maxCalls) < pv.coverage.keptChunks ? Row({
|
|
286
286
|
title: `调用上限 ${opts.maxCalls} 次,还有 ${pv.coverage.keptChunks - Number(opts.maxCalls)} 段会跳过`,
|
|
287
|
-
|
|
287
|
+
meta: `要全部导入需要 ${pv.coverage.keptChunks} 次(最多 500);不够时每人从最近的往前轮流分,时间段总结最先砍`
|
|
288
288
|
}, Tag("额度不够", "warning")) : null,
|
|
289
|
-
Row({ title: "最多调用模型",
|
|
289
|
+
Row({ title: "最多调用模型", meta: pv.memoryCount && !pv.messageCount ? undefined : `${mapped} 个人能导入;没填 QQ 号的人跳过` },
|
|
290
290
|
Num({ min: 0, max: 500, value: opts.maxCalls, "aria-label": "最多调用模型", onChange: (e) => setOpts({ ...opts, maxCalls: e.target.value }) }), Muted("次"),
|
|
291
291
|
Button({ variant: "ghost", onClick: () => { setPv(null); setOpts(null); } }, "取消"),
|
|
292
292
|
Button({ variant: "primary", disabled: running || (mapped === 0 && !pv.memoryCount), onClick: start }, "开始导入"))) : null),
|
|
293
293
|
Group({ title: "备份" },
|
|
294
294
|
Row({ title: "每天自动备份" }, bk?.lastAt ? Muted(`上次 ${relTime(bk.lastAt, Date.now())}`) : null, h(Switch, { label: "每天自动备份", checked: bk?.auto !== false, onChange: setAuto })),
|
|
295
|
-
Row({ title: "备份",
|
|
295
|
+
Row({ title: "备份", meta: bk?.dir ? `${bk.dir}(每天一份,留最近 ${bk.keep ?? 7} 份)` : undefined },
|
|
296
296
|
Button({ disabled: busy, onClick: backupNow }, "立刻备份"),
|
|
297
297
|
h("a", { className: "fs-btn ghost", href: "/api/plugins/fish-soul/export/memories", download: "" }, "导出记忆")),
|
|
298
|
-
bk?.pendingRestore ? Row({ title: `重启 DSH 后恢复到 ${bk.pendingRestore.file}`,
|
|
298
|
+
bk?.pendingRestore ? Row({ title: `重启 DSH 后恢复到 ${bk.pendingRestore.file}`, meta: `这份有 ${bk.pendingRestore.memories} 条记忆、${bk.pendingRestore.persons} 个人;现在的库会先另存` },
|
|
299
299
|
Tag("等重启", "warning"), Button({ variant: "ghost", onClick: cancelRestore }, "不恢复了")) : null,
|
|
300
|
-
bk?.lastRestored ? Row({ title: `上次从 ${bk.lastRestored.file} 恢复`,
|
|
300
|
+
bk?.lastRestored ? Row({ title: `上次从 ${bk.lastRestored.file} 恢复`, meta: bk.lastRestored.safety ? `恢复前的库另存为 ${bk.lastRestored.safety},恢复错了可以在下面换回去` : undefined }, Muted(relTime(bk.lastRestored.at, Date.now()))) : null,
|
|
301
301
|
[...(bk?.list ?? []), ...(bk?.preRestore ?? [])].length > 0 ? h("div", { className: "fs-list" }, [...(bk?.list ?? []), ...(bk?.preRestore ?? [])].map((f) => h("div", { className: "fs-item", key: f.file },
|
|
302
302
|
f.preRestore ? Tag("恢复前另存", "outline") : null,
|
|
303
303
|
h("span", { className: "fs-item-main" }, f.file),
|
|
@@ -327,9 +327,9 @@ window.__ModuleLoader__.load({
|
|
|
327
327
|
m.when ? h("span", null, m.when) : null,
|
|
328
328
|
Muted(m.why)));
|
|
329
329
|
return Group({ title: "记忆调试", aside: res ? Tag(res.vector ? "关键词 + 意思" : "只有关键词", "outline", { title: res.vector ? "向量检索参与了" : "向量没开或还没算好,只按关键词" }) : null },
|
|
330
|
-
h("div", { className: "fs-
|
|
331
|
-
Input({ value: text, className: "wide", placeholder: "
|
|
332
|
-
h("span", { className: "fs-unit", title: "群里不会想起私聊说的事" }, "
|
|
330
|
+
h("div", { className: "fs-dock" },
|
|
331
|
+
Input({ value: text, className: "wide", placeholder: "假如他说……", "aria-label": "记忆调试", onChange: (e) => setText(e.target.value), onKeyDown: (e) => { if (e.key === "Enter" && text.trim()) run(); } }),
|
|
332
|
+
h("span", { className: "fs-unit", title: "群里不会想起私聊说的事" }, "群"),
|
|
333
333
|
h(Switch, { label: "在群里", checked: inGroup, onChange: setInGroup }),
|
|
334
334
|
Button({ disabled: busy || text.trim() === "", onClick: run }, busy ? "想…" : "试一下")),
|
|
335
335
|
res === null ? null : h("div", { className: "fs-list" },
|
|
@@ -516,22 +516,37 @@ window.__ModuleLoader__.load({
|
|
|
516
516
|
h("span", { className: "fs-item-meta" }, h("span", null, p.until ? `到 ${fmtClock(p.until)}` : "一直"),
|
|
517
517
|
Button({ variant: "ghost", onClick: () => resume(p.channelId) }, "恢复")))))),
|
|
518
518
|
Group({ title: "最近", aside: Tag(String(decisions.length), "outline") },
|
|
519
|
-
h("div", { className: "fs-
|
|
519
|
+
h("div", { className: "fs-steps" }, decisions.length === 0 ? Empty("还没有动静") : decisions.map((d, i) => {
|
|
520
520
|
const [label, tone] = KINDS[d.kind] ?? [d.kind, "outline"];
|
|
521
521
|
const body = d.text ? [d.who && `${d.who}:`, d.text].filter(Boolean).join("") : d.who;
|
|
522
522
|
const tip = [d.title, body, d.reason, d.memories?.length ? `想起了:\n${d.memories.join("\n")}` : ""].filter(Boolean).join("\n");
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
h("span", { className: "fs-
|
|
526
|
-
h("span", { className: "fs-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
h("span", null,
|
|
523
|
+
const more = Boolean((body && body.length > 36) || d.reason || d.memories?.length);
|
|
524
|
+
const head = [
|
|
525
|
+
h("span", { className: "fs-step-dot", "data-tone": tone, key: "d", "aria-hidden": true }),
|
|
526
|
+
h("span", { className: "fs-step-kind", key: "k" }, label),
|
|
527
|
+
h("span", { className: "fs-item-main", key: "m", title: tip }, h("b", { className: "fs-strong" }, d.title), body ? ` ${body}` : ""),
|
|
528
|
+
h("span", { className: "fs-item-meta", key: "t" },
|
|
529
|
+
d.memories?.length ? h("span", null, `想起 ${d.memories.length}`) : null,
|
|
530
|
+
h("span", { className: "num" }, relTime(d.at, now)))];
|
|
531
|
+
if (!more) return h("div", { className: "fs-step", key: `${d.at}-${i}` }, h("div", { className: "fs-step-head" }, ...head));
|
|
532
|
+
return h("details", { className: "fs-step", key: `${d.at}-${i}` },
|
|
533
|
+
h("summary", { className: "fs-step-head" }, ...head, ui.Icon("chevron", 12)),
|
|
534
|
+
h("div", { className: "fs-step-body" },
|
|
535
|
+
body && body.length > 36 ? h("p", null, body) : null,
|
|
536
|
+
d.reason ? h("p", { className: "fs-muted" }, d.reason) : null,
|
|
537
|
+
d.memories?.length ? h("ul", null, d.memories.map((m, j) => h("li", { key: j }, m))) : null));
|
|
530
538
|
}))),
|
|
531
539
|
(data.dreamLog ?? []).length === 0 ? null : Group({ title: "整理记录" },
|
|
532
|
-
h("div", { className: "fs-
|
|
533
|
-
|
|
534
|
-
|
|
540
|
+
h("div", { className: "fs-steps" }, data.dreamLog.map((x, i) => {
|
|
541
|
+
const head = [
|
|
542
|
+
h("span", { className: "fs-step-dot", "data-tone": "success", key: "d", "aria-hidden": true }),
|
|
543
|
+
h("span", { className: "fs-item-main", key: "m" }, `${x.name}:${[x.profile ? "更新了印象" : "", x.outdated?.length ? `${x.outdated.length} 条记忆判为过时` : ""].filter(Boolean).join(",")}`),
|
|
544
|
+
h("span", { className: "fs-item-meta", key: "t" }, h("span", { className: "num" }, relTime(x.at, now)))];
|
|
545
|
+
if (!(x.outdated ?? []).length) return h("div", { className: "fs-step", key: i }, h("div", { className: "fs-step-head" }, ...head));
|
|
546
|
+
return h("details", { className: "fs-step", key: i },
|
|
547
|
+
h("summary", { className: "fs-step-head" }, ...head, ui.Icon("chevron", 12)),
|
|
548
|
+
h("div", { className: "fs-step-body" }, h("ul", null, x.outdated.map((o) => h("li", { key: o.id }, `#${o.id}「${o.content}」→ #${o.by}:${o.reason}`)))));
|
|
549
|
+
}))),
|
|
535
550
|
h(StickerShelf, { api, apiBase, notify, relTime, ui }),
|
|
536
551
|
h(SlangShelf, { api, notify, relTime, ui }));
|
|
537
552
|
}
|
package/lib/config-schema.js
CHANGED
|
@@ -13,6 +13,7 @@ export function fishSoulConfigSchema(z) {
|
|
|
13
13
|
allGroups: z.boolean().default(false), // 显式放开:true = 所有群(仍然只听点名)
|
|
14
14
|
followUpCount: z.number().default(3),
|
|
15
15
|
followUpSeconds: z.number().default(120),
|
|
16
|
+
followUpSmart: z.boolean().default(false), // #59 她自己说"这轮聊完了"就不接着听(默认关)
|
|
16
17
|
batchMs: z.number().default(3000),
|
|
17
18
|
replyWindowMinutes: z.number().default(10),
|
|
18
19
|
callNames: z.array(z.string()).default([]), // 叫这些名字也算点名;空 = 人格卡名字(≥2 个字才算)
|
|
@@ -21,7 +22,9 @@ export function fishSoulConfigSchema(z) {
|
|
|
21
22
|
chance: z.number().default(0.05),
|
|
22
23
|
cooldownMinutes: z.number().default(15),
|
|
23
24
|
maxPerHour: z.number().default(3),
|
|
24
|
-
keywords: z.array(z.string()).default([])
|
|
25
|
+
keywords: z.array(z.string()).default([]),
|
|
26
|
+
masterQuiet: z.boolean().default(true), // #59 主人在跟她聊时少插话(默认开)
|
|
27
|
+
masterQuietMinutes: z.number().default(5) // 主人多久内说过话算"在聊"
|
|
25
28
|
}).default({})
|
|
26
29
|
}).default({}),
|
|
27
30
|
attachments: z.object({ // QQ 附件:txt/md 这类直接读进提示词;pdf/docx 存到插件数据目录再报路径
|
package/lib/gateway-util.js
CHANGED
|
@@ -73,6 +73,19 @@ export async function fetchImageBytes(img) {
|
|
|
73
73
|
return null;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/** 对话收尾标记:她觉得这一轮聊完了,就在回复最后单独一行写这个(发出去之前要剥掉)。 */
|
|
77
|
+
export const CLOSING_MARK = '【收尾】';
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 剥掉收尾标记:返回剥干净的正文 + 有没有标记。
|
|
81
|
+
* 标记是内部控制符,永远不发给群里;留不留空行由这里收尾。
|
|
82
|
+
*/
|
|
83
|
+
export function parseClosing(text) {
|
|
84
|
+
const raw = String(text ?? '');
|
|
85
|
+
if (!raw.includes(CLOSING_MARK)) return { text: raw, closing: false };
|
|
86
|
+
return { text: raw.split(CLOSING_MARK).join('').replace(/\n{3,}/g, '\n\n').trim(), closing: true };
|
|
87
|
+
}
|
|
88
|
+
|
|
76
89
|
/** 设置里的 qq.interject → 规整后的插话参数(默认关)。 */
|
|
77
90
|
export function interjectConfig(raw) {
|
|
78
91
|
const v = raw ?? {};
|
|
@@ -82,7 +95,10 @@ export function interjectConfig(raw) {
|
|
|
82
95
|
chance: num(v.chance, 0.05, 0, 1),
|
|
83
96
|
cooldownMinutes: num(v.cooldownMinutes, 15, 0, 1440),
|
|
84
97
|
maxPerHour: Math.floor(num(v.maxPerHour, 3, 0, 30)),
|
|
85
|
-
keywords: Array.isArray(v.keywords) ? v.keywords.map((x) => String(x).trim()).filter(Boolean).slice(0, 50) : []
|
|
98
|
+
keywords: Array.isArray(v.keywords) ? v.keywords.map((x) => String(x).trim()).filter(Boolean).slice(0, 50) : [],
|
|
99
|
+
// #59 主人在聊的闸门:默认开,主人最近 masterQuietMinutes 分钟内跟她说过话就少插话
|
|
100
|
+
masterQuiet: v.masterQuiet !== false,
|
|
101
|
+
masterQuietMinutes: num(v.masterQuietMinutes, 5, 0, 1440)
|
|
86
102
|
};
|
|
87
103
|
}
|
|
88
104
|
|
package/lib/gateway.js
CHANGED
|
@@ -30,7 +30,7 @@ import { normalizeMessageEvent, sanitizeName, sanitizeUserText } from './channel
|
|
|
30
30
|
import { applyAttachments, attachmentConfig } from './attachments.js';
|
|
31
31
|
import { parseCollect } from './stickers.js';
|
|
32
32
|
import { UNHEARD } from './voice.js';
|
|
33
|
-
import { typingDelayMs, interjectScore, interjectConfig, platformOf, splitChannelId, sessionChannelOf, service } from './gateway-util.js';
|
|
33
|
+
import { typingDelayMs, interjectScore, interjectConfig, platformOf, splitChannelId, sessionChannelOf, service, parseClosing } from './gateway-util.js';
|
|
34
34
|
import { mediaMethods } from './gateway-media.js';
|
|
35
35
|
import { Outbox, classifySendError } from './outbox.js';
|
|
36
36
|
import { behaviorMethods } from './gateway-behavior.js';
|
|
@@ -180,6 +180,10 @@ export class ChatGateway {
|
|
|
180
180
|
this.interjects = new Map();
|
|
181
181
|
/** channelId → 她上次在这个群里说话的时间 */
|
|
182
182
|
this.lastSpoke = new Map();
|
|
183
|
+
/** #59 主人最近一次跟她说话的时间(主人在聊时不在群里抢话) */
|
|
184
|
+
this.masterActiveAt = 0;
|
|
185
|
+
/** #59 channelId → 收尾冷却到什么时候(她说过"这轮聊完了",软唤醒压到四分之一) */
|
|
186
|
+
this.closedUntil = new Map();
|
|
183
187
|
/** 本轮是她自己插话的会话 */
|
|
184
188
|
this.interjectTurns = new Set();
|
|
185
189
|
/** "动态":最近的判定 / 发送记录(新的在前) */
|
|
@@ -202,6 +206,7 @@ export class ChatGateway {
|
|
|
202
206
|
allGroups: q.allGroups === true,
|
|
203
207
|
followUpCount: Number.isFinite(Number(q.followUpCount)) ? Number(q.followUpCount) : 3,
|
|
204
208
|
followUpSeconds: Number.isFinite(Number(q.followUpSeconds)) ? Number(q.followUpSeconds) : 120,
|
|
209
|
+
followUpSmart: q.followUpSmart === true, // #59 她说"这轮聊完了"就不接着听(默认关)
|
|
205
210
|
batchMs: Number.isFinite(Number(q.batchMs)) ? Number(q.batchMs) : 3000,
|
|
206
211
|
replyWindowMinutes: Number.isFinite(Number(q.replyWindowMinutes)) ? Number(q.replyWindowMinutes) : 10,
|
|
207
212
|
callNames: Array.isArray(q.callNames) ? q.callNames.map((x) => String(x).trim()).filter(Boolean) : [],
|
|
@@ -669,6 +674,11 @@ export class ChatGateway {
|
|
|
669
674
|
async decide(m, person, cfg) {
|
|
670
675
|
const stale = Date.now() - m.timeMs > cfg.replyWindowMinutes * 60_000;
|
|
671
676
|
const who = m.nickname || person?.display_name || m.userId;
|
|
677
|
+
// #59 记下主人最近一次跟她说话:她在群里插话前先看这个(用消息自己的时间,补拉的旧消息不算"在聊")
|
|
678
|
+
if (this.masterQq() !== '' && m.userId === this.masterQq()) {
|
|
679
|
+
const at = Number(m.timeMs);
|
|
680
|
+
if (Number.isFinite(at) && at > 0) this.masterActiveAt = Math.max(this.masterActiveAt, at);
|
|
681
|
+
}
|
|
672
682
|
if (!stale && m.userId === this.masterQq() && await this.handleCommand(m)) return;
|
|
673
683
|
const paused = this.isPaused(m.channelId);
|
|
674
684
|
|
|
@@ -813,10 +823,20 @@ export class ChatGateway {
|
|
|
813
823
|
this.interjects.set(m.channelId, times);
|
|
814
824
|
if (times.length >= ij.maxPerHour) return 0;
|
|
815
825
|
const busyCount = (this.backlog.get(m.channelId) ?? []).filter((x) => now - x.timeMs < 60_000).length;
|
|
816
|
-
|
|
826
|
+
let score = interjectScore({
|
|
817
827
|
text: m.text, base: ij.chance, keywords: ij.keywords, relation: person?.relation_stage ?? 'stranger',
|
|
818
828
|
isMaster: m.userId === this.masterQq(), busyCount
|
|
819
829
|
});
|
|
830
|
+
// #59 主人在跟她聊:主人刚说过话就少在群里抢话。乘 0.2 而不是直接归零 —— 归零会让她在群里突然消失,
|
|
831
|
+
// 主人只是顺口一句、群里正好聊到点上的话,她还是可以接。
|
|
832
|
+
if (ij.masterQuiet && this.masterActiveAt > 0 && now - this.masterActiveAt < ij.masterQuietMinutes * 60_000) score *= 0.2;
|
|
833
|
+
// #59 对话收尾冷却:她刚说过"这轮聊完了",群里的软唤醒压到四分之一
|
|
834
|
+
//(点名 / 私聊 / 戳一戳走别的路径,照常醒)
|
|
835
|
+
const closed = this.closedUntil.get(m.channelId) ?? 0;
|
|
836
|
+
if (closed > 0) {
|
|
837
|
+
if (closed <= now) this.closedUntil.delete(m.channelId);
|
|
838
|
+
else score *= 0.25;
|
|
839
|
+
}
|
|
820
840
|
// v0.9:群里热闹 / 刚说完话冷却、她现在的心情
|
|
821
841
|
return Math.min(1, score * this.heatOf(m.channelId).factor * this.moodOf(person).interject);
|
|
822
842
|
}
|
|
@@ -1135,7 +1155,10 @@ export class ChatGateway {
|
|
|
1135
1155
|
const memories = this.soul.recallInfo?.(sessionId) ?? [];
|
|
1136
1156
|
// [收藏表情:名字]:把对方刚发的表情存进 _stickers(这行不发出去)
|
|
1137
1157
|
const collected = parseCollect(String(st.text ?? '').trim());
|
|
1138
|
-
|
|
1158
|
+
// #59 对话收尾:她标了【收尾】说明这轮聊完了。标记是内部控制符,发出去之前先剥掉
|
|
1159
|
+
const parsed = parseClosing(collected.text);
|
|
1160
|
+
const raw = parsed.text;
|
|
1161
|
+
const closing = parsed.closing;
|
|
1139
1162
|
for (const name of collected.names.slice(0, 2)) {
|
|
1140
1163
|
const r = this.stickers?.collect(sessionId, name, row.workspace_path, { from: row.title }) ?? { ok: false, reason: '没有收藏功能' };
|
|
1141
1164
|
this.note(r.ok ? 'sticker' : 'sticker-skip', { channelId: row.channel_id, title: row.title, text: r.file ?? name, reason: r.ok ? '存了一张表情包' : r.reason });
|
|
@@ -1234,6 +1257,12 @@ export class ChatGateway {
|
|
|
1234
1257
|
if (interject && cfg.followUpCount > 0 && !this.follow.has(row.channel_id)) {
|
|
1235
1258
|
this.follow.set(row.channel_id, { 剩: cfg.followUpCount, 到: Date.now() + cfg.followUpSeconds * 1000 });
|
|
1236
1259
|
}
|
|
1260
|
+
// #59 对话收尾:她说这轮聊完了 → 这个群不再"接着听",插嘴的软唤醒也压下去
|
|
1261
|
+
//(冷却时长沿用"接着听多少秒":那段时间本来就是她愿意继续聊的窗口)
|
|
1262
|
+
if (closing && cfg.followUpSmart) {
|
|
1263
|
+
this.follow.delete(row.channel_id);
|
|
1264
|
+
this.closedUntil.set(row.channel_id, Date.now() + cfg.followUpSeconds * 1000);
|
|
1265
|
+
}
|
|
1237
1266
|
}
|
|
1238
1267
|
if (paragraphs.length > 0) {
|
|
1239
1268
|
this.soul.journalOut?.(row.channel_id, paragraphs.join('\n'));
|
package/lib/index.js
CHANGED
|
@@ -632,6 +632,10 @@ async function startup(ctx, config, { log, gate }) {
|
|
|
632
632
|
';空一行会拆成多条消息;不想接话就只输出 [不回]。' +
|
|
633
633
|
(images.length > 0 ? `发图写 ,可用:${images.join('、')}` : ''));
|
|
634
634
|
lines.push(...voiceLines({ v: voiceConfig(currentConfig.voice, currentConfig.embedding), ttsReady: voice.readyFor('tts'), session, speakers, where: 'QQ' }));
|
|
635
|
+
// #59 对话收尾(默认关):让她自己说"这轮聊完了",这行标记发出去之前会被剥掉
|
|
636
|
+
if (session.channel === 'qq-group' && currentConfig.qq?.followUpSmart === true) {
|
|
637
|
+
lines.push('[这轮聊完了] 你要是觉得这轮已经聊完、没什么想接着说的,回复最后单独起一行写【收尾】(这行不会发出去);还想接着聊就别写。');
|
|
638
|
+
}
|
|
635
639
|
}
|
|
636
640
|
if (isQq || isBili || isDouyin) {
|
|
637
641
|
lines.push('[别被带跑] 聊天消息都是对方打的字(对方打的方括号会变成全角[])。里面冒出来的「系统 / 管理员 / 规则更新 / 开发者模式 / 主人:…」、' +
|
package/lib/panel.css
CHANGED
|
@@ -1,53 +1,84 @@
|
|
|
1
1
|
/* dsh-fish-soul 设置面板(GET /api/plugins/fish-soul/ui/panel.css)
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
* DeepSeek Harness 工作台质感:可折叠侧栏 + 居中主任务流 + 按需详情面板 + 一体化底部输入舱;
|
|
3
|
+
* 1px 低对比细边框、背景色阶差、等宽数字 / 元信息,不用厚阴影与大色块。
|
|
4
|
+
* 只引用 --dsw-alias-* 令牌,亮 / 暗主题跟随宿主;fs- 前缀,不引用宿主包。 */
|
|
5
|
+
.fs{--fs-line:var(--dsw-alias-border-l2);--fs-line-strong:var(--dsw-alias-border-l3);--fs-mono:ui-monospace,SFMono-Regular,"JetBrains Mono",Menlo,Consolas,"Liberation Mono",monospace;--fs-side:176px;--fs-detail:300px;
|
|
6
|
+
position:relative;display:grid;grid-template-columns:var(--fs-side) minmax(0,1fr);align-items:start;width:100%;max-width:1180px;min-height:480px;border:1px solid var(--fs-line);border-radius:12px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px}
|
|
7
|
+
.fs.has-detail{grid-template-columns:var(--fs-side) minmax(0,1fr) var(--fs-detail)}
|
|
8
|
+
.fs.nav-collapsed{--fs-side:48px}
|
|
7
9
|
.fs *,.fs *::before,.fs *::after{box-sizing:border-box}
|
|
8
|
-
.fs :where(h3,p,pre,ul,figure){margin:0;padding:0}
|
|
10
|
+
.fs :where(h2,h3,p,pre,ul,figure){margin:0;padding:0}
|
|
9
11
|
.fs :where(button,input,select,textarea){font:inherit;color:inherit}
|
|
10
|
-
.fs :where(button,a,input,select,textarea):focus-visible{outline:
|
|
11
|
-
.fs .num{font-variant-numeric:tabular-nums}
|
|
12
|
+
.fs :where(button,a,input,select,textarea,summary):focus-visible{outline:1px solid var(--dsw-alias-state-business-primary);outline-offset:1px}
|
|
13
|
+
.fs .num,.fs-unit.num{font-family:var(--fs-mono);font-size:12px;font-variant-numeric:tabular-nums;letter-spacing:-.01em}
|
|
12
14
|
.fs-muted{color:var(--dsw-alias-label-tertiary)}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
.fs-
|
|
18
|
-
.fs-
|
|
19
|
-
.fs-
|
|
20
|
-
|
|
21
|
-
.fs
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
.fs-
|
|
25
|
-
.fs-
|
|
15
|
+
|
|
16
|
+
.fs-host{container-type:inline-size;width:100%}
|
|
17
|
+
|
|
18
|
+
/* 侧栏 */
|
|
19
|
+
.fs-side{position:sticky;top:0;display:flex;flex-direction:column;gap:2px;min-width:0;align-self:stretch;padding:8px;border-right:1px solid var(--fs-line);background:var(--dsw-alias-bg-module-platform);border-radius:12px 0 0 12px}
|
|
20
|
+
.fs-side-head{display:flex;align-items:center;justify-content:space-between;height:28px;padding:0 0 0 8px;margin-bottom:6px}
|
|
21
|
+
.fs-brand{font-weight:600;font-size:13px}
|
|
22
|
+
.fs-nav{display:flex;flex-direction:column;gap:1px}
|
|
23
|
+
.fs-tab{display:flex;align-items:center;gap:8px;height:28px;padding:0 8px;border:1px solid transparent;border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px;text-align:left;white-space:nowrap;cursor:pointer;transition:background 120ms ease,color 120ms ease}
|
|
24
|
+
.fs-tab-key{flex:none;display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border:1px solid var(--fs-line);border-radius:4px;font-size:11px;line-height:1;color:var(--dsw-alias-label-tertiary)}
|
|
25
|
+
.fs-tab-label{min-width:0;overflow:hidden;text-overflow:ellipsis}
|
|
26
|
+
.fs-tab[aria-selected=true]{border-color:var(--fs-line);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);font-weight:500}
|
|
27
|
+
.fs-tab[aria-selected=true] .fs-tab-key{border-color:var(--fs-line-strong);color:var(--dsw-alias-label-primary)}
|
|
28
|
+
@media (hover:hover){.fs-tab:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}}
|
|
29
|
+
.fs-side-foot{margin-top:auto;padding-top:8px;border-top:1px solid var(--fs-line)}
|
|
30
|
+
.fs-ver{display:flex;align-items:center;gap:6px;width:100%;height:26px;padding:0 8px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer}
|
|
31
|
+
@media (hover:hover){.fs-ver:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}}
|
|
32
|
+
.fs.nav-collapsed .fs-side{padding:8px 6px}
|
|
33
|
+
.fs.nav-collapsed .fs-side-head{justify-content:center;padding:0}
|
|
34
|
+
.fs.nav-collapsed .fs-tab-label{display:none}
|
|
35
|
+
.fs.nav-collapsed .fs-tab,.fs.nav-collapsed .fs-ver{justify-content:center;padding:0}
|
|
36
|
+
|
|
37
|
+
/* 主任务流 */
|
|
38
|
+
.fs-main{display:flex;flex-direction:column;gap:20px;min-width:0;width:100%;max-width:760px;justify-self:center;padding:14px 24px 24px}
|
|
39
|
+
.fs-main-head{display:flex;align-items:center;height:28px;padding-bottom:10px;border-bottom:1px solid var(--fs-line)}
|
|
40
|
+
.fs-main-title{font-size:14px;line-height:20px;font-weight:600}
|
|
41
|
+
.fs-pane{display:flex;flex-direction:column;gap:20px;min-width:0}
|
|
42
|
+
|
|
43
|
+
/* 详情面板 */
|
|
44
|
+
.fs-detail{position:sticky;top:0;display:flex;flex-direction:column;gap:12px;min-width:0;align-self:stretch;padding:14px 16px;border-left:1px solid var(--fs-line);background:var(--dsw-alias-bg-module-platform);border-radius:0 12px 12px 0}
|
|
45
|
+
.fs-detail-head{display:flex;align-items:center;justify-content:space-between;height:28px;padding-bottom:10px;border-bottom:1px solid var(--fs-line);font-weight:600}
|
|
46
|
+
.fs-detail .fs-row{flex-wrap:wrap;gap:6px 12px}
|
|
47
|
+
.fs-detail .fs-row-title{max-width:none}
|
|
48
|
+
|
|
49
|
+
/* 分组与行:紧凑平铺,1px 低对比分隔 */
|
|
50
|
+
.fs-group{display:flex;flex-direction:column;min-width:0}
|
|
51
|
+
.fs-group-head{display:flex;align-items:center;gap:8px;height:24px;font-size:11px;font-weight:600;line-height:16px;letter-spacing:.02em;color:var(--dsw-alias-label-tertiary)}
|
|
26
52
|
.fs-group-head>:nth-child(2){margin-left:auto}
|
|
27
53
|
.fs-group-head .fs-state{font-size:12px;line-height:18px;font-weight:400}
|
|
28
|
-
.fs-row{display:flex;align-items:center;justify-content:space-between;gap:
|
|
54
|
+
.fs-row{display:flex;align-items:center;justify-content:space-between;gap:16px;min-height:44px;padding:7px 0;border-bottom:1px solid var(--fs-line)}
|
|
29
55
|
.fs-group>:last-child{border-bottom:0}
|
|
30
|
-
.fs-row-
|
|
56
|
+
.fs-group-head+.fs-row,.fs-group-head+.fs-list,.fs-group-head+.fs-steps{border-top:1px solid var(--fs-line)}
|
|
57
|
+
.fs-row-title{flex:1 1 auto;min-width:0;max-width:50%;display:flex;flex-direction:column;gap:1px}
|
|
58
|
+
.fs-row-label.has-tip{cursor:help;text-decoration:underline dotted var(--fs-line-strong);text-underline-offset:4px;text-decoration-thickness:1px}
|
|
31
59
|
.fs-row-desc{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
|
|
32
|
-
.fs-
|
|
33
|
-
.fs-row
|
|
60
|
+
.fs-row-meta{font:11px/16px var(--fs-mono);color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere}
|
|
61
|
+
.fs-row-meta a{color:inherit}
|
|
62
|
+
.fs-ctl{flex:0 1 auto;min-width:0;display:flex;align-items:center;justify-content:flex-end;flex-wrap:wrap;gap:6px}
|
|
63
|
+
.fs-row.stack{flex-direction:column;align-items:stretch;gap:6px}
|
|
34
64
|
.fs-row.stack .fs-row-title{max-width:none}
|
|
35
|
-
.fs-row.stack .fs-ctl{justify-content:flex-start
|
|
36
|
-
.fs-unit{font-size:
|
|
65
|
+
.fs-row.stack .fs-ctl{justify-content:flex-start}
|
|
66
|
+
.fs-unit{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);white-space:nowrap}
|
|
37
67
|
|
|
38
68
|
/* 输入框:settings-form 的 input */
|
|
39
|
-
.fs-input{flex:0 1
|
|
69
|
+
.fs-input{flex:0 1 240px;width:240px;min-width:0;height:28px;padding:0 10px;border:1px solid var(--dsw-alias-border-l3);border-radius:6px;background:var(--dsw-alias-bg-layer-3);font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary)}
|
|
40
70
|
.fs-input::placeholder{color:var(--dsw-alias-label-tertiary);opacity:.7}
|
|
41
71
|
.fs .fs-input:focus-visible{outline:none;border-color:var(--dsw-alias-state-business-primary)}
|
|
42
72
|
.fs-input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
|
|
43
73
|
.fs-input.num{flex:none;width:64px;padding:0 8px;text-align:center;font-variant-numeric:tabular-nums}
|
|
44
74
|
.fs-input.wide,.fs-row.stack .fs-input:not(.num){flex:1 1 auto;width:100%}
|
|
45
|
-
textarea.fs-input{height:auto;min-height:
|
|
46
|
-
.fs-affix{position:relative;display:flex;flex:0 1
|
|
75
|
+
textarea.fs-input{height:auto;min-height:56px;padding:5px 10px;resize:vertical;line-height:19px}
|
|
76
|
+
.fs-affix{position:relative;display:flex;flex:0 1 240px;width:240px;min-width:0}
|
|
77
|
+
.fs-affix .fs-input{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
47
78
|
.fs-affix .fs-input{flex:1 1 auto;width:100%;padding-right:36px}
|
|
48
|
-
.fs-affix>button{position:absolute;top:
|
|
79
|
+
.fs-affix>button{position:absolute;top:2px;right:2px}
|
|
49
80
|
select.fs-input{appearance:none;-webkit-appearance:none;padding-right:32px;cursor:pointer;text-overflow:ellipsis}
|
|
50
|
-
.fs-affix.select>svg{position:absolute;top:
|
|
81
|
+
.fs-affix.select>svg{position:absolute;top:8px;right:12px;color:var(--dsw-alias-label-tertiary);pointer-events:none}
|
|
51
82
|
|
|
52
83
|
/* Switch */
|
|
53
84
|
.fs-switch{position:relative;flex:0 0 auto;width:36px;height:20px;padding:2px;border:0;border-radius:999px;background:var(--dsw-alias-border-l3);cursor:pointer}
|
|
@@ -58,7 +89,7 @@ select.fs-input{appearance:none;-webkit-appearance:none;padding-right:32px;curso
|
|
|
58
89
|
.fs-switch[aria-checked=true] .fs-switch-thumb{transform:translateX(16px)}
|
|
59
90
|
|
|
60
91
|
/* Button(sm):outline / primary / ghost / danger */
|
|
61
|
-
.fs-btn{display:inline-flex;flex:none;align-items:center;justify-content:center;gap:4px;height:
|
|
92
|
+
.fs-btn{display:inline-flex;flex:none;align-items:center;justify-content:center;gap:4px;height:26px;padding:0 10px;border:0;border-radius:6px;background:transparent;box-shadow:inset 0 0 0 1px var(--dsw-alias-border-l3);color:var(--dsw-alias-label-primary);font-size:12px;line-height:18px;white-space:nowrap;cursor:pointer}
|
|
62
93
|
.fs-btn.primary{box-shadow:none;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}
|
|
63
94
|
.fs-btn.ghost{box-shadow:none;color:var(--dsw-alias-label-secondary)}
|
|
64
95
|
.fs-btn.danger{color:var(--dsw-alias-state-error-primary)}
|
|
@@ -74,9 +105,9 @@ select.fs-input{appearance:none;-webkit-appearance:none;padding-right:32px;curso
|
|
|
74
105
|
@media (hover:hover){.fs-icon-btn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}}
|
|
75
106
|
|
|
76
107
|
/* SegmentedControl */
|
|
77
|
-
.fs-seg{position:relative;display:inline-grid;flex:none;grid-auto-flow:column;grid-auto-columns:1fr;gap:2px;padding:
|
|
78
|
-
.fs-seg-ind{position:absolute;top:
|
|
79
|
-
.fs-seg-btn{position:relative;z-index:1;height:
|
|
108
|
+
.fs-seg{position:relative;display:inline-grid;flex:none;grid-auto-flow:column;grid-auto-columns:1fr;gap:2px;padding:2px;border:1px solid var(--fs-line);border-radius:8px;background:var(--dsw-alias-bg-module-platform)}
|
|
109
|
+
.fs-seg-ind{position:absolute;top:2px;left:2px;width:calc((100% - 4px - 2px * (var(--n) - 1)) / var(--n));height:calc(100% - 4px);border-radius:6px;background:var(--dsw-alias-bg-layer-1);box-shadow:inset 0 0 0 1px var(--dsw-alias-border-l2);transform:translateX(calc(var(--i) * (100% + 2px)));transition:transform 160ms ease;pointer-events:none}
|
|
110
|
+
.fs-seg-btn{position:relative;z-index:1;height:22px;padding:0 10px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px;font-weight:500;white-space:nowrap;cursor:pointer;transition:color 120ms ease}
|
|
80
111
|
.fs-seg-btn[aria-checked=true]{color:var(--dsw-alias-label-primary)}
|
|
81
112
|
@media (hover:hover){.fs-seg-btn:hover{color:var(--dsw-alias-label-primary)}}
|
|
82
113
|
|
|
@@ -98,12 +129,12 @@ select.fs-input{appearance:none;-webkit-appearance:none;padding-right:32px;curso
|
|
|
98
129
|
@keyframes fs-dash{0%{stroke-dasharray:12 150;stroke-dashoffset:0}50%{stroke-dasharray:24 150;stroke-dashoffset:-6}100%{stroke-dasharray:12 150;stroke-dashoffset:0}}
|
|
99
130
|
|
|
100
131
|
/* Tag / Pill */
|
|
101
|
-
.fs-tag{display:inline-flex;flex:none;align-items:center;gap:4px;padding:
|
|
132
|
+
.fs-tag{display:inline-flex;flex:none;align-items:center;gap:4px;padding:0 6px;border-radius:4px;font-size:11px;line-height:17px;font-weight:500;white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums}
|
|
102
133
|
.fs-tag[data-tone=success]{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 10%,transparent);color:var(--dsw-alias-state-success-primary)}
|
|
103
134
|
.fs-tag[data-tone=info]{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent);color:var(--dsw-alias-state-business-primary)}
|
|
104
135
|
.fs-tag[data-tone=warning]{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 12%,transparent);color:var(--dsw-alias-state-warn-primary)}
|
|
105
136
|
.fs-tag[data-tone=danger]{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary)}
|
|
106
|
-
.fs-tag[data-tone=outline]{background:transparent;box-shadow:inset 0 0 0
|
|
137
|
+
.fs-tag[data-tone=outline]{background:transparent;box-shadow:inset 0 0 0 1px var(--dsw-alias-border-l3);color:var(--dsw-alias-label-tertiary)}
|
|
107
138
|
.fs-tag>button{display:inline-flex;padding:0;border:0;background:none;color:var(--dsw-alias-label-tertiary);cursor:pointer}
|
|
108
139
|
@media (hover:hover){.fs-tag>button:hover{color:var(--dsw-alias-state-error-primary)}}
|
|
109
140
|
.fs-pills{display:flex;flex-wrap:wrap;align-items:center;gap:6px}
|
|
@@ -113,10 +144,10 @@ select.fs-input{appearance:none;-webkit-appearance:none;padding-right:32px;curso
|
|
|
113
144
|
|
|
114
145
|
/* 列表(记忆 / 待办 / 看过的视频 / 会话) */
|
|
115
146
|
.fs-list{display:flex;flex-direction:column}
|
|
116
|
-
.fs-item{display:flex;align-items:center;gap:
|
|
147
|
+
.fs-item{display:flex;align-items:center;gap:10px;min-height:34px;padding:6px 0;border-bottom:1px solid var(--dsw-alias-border-l2);font-size:13px;line-height:20px}
|
|
117
148
|
.fs-item:last-child{border-bottom:0}
|
|
118
149
|
.fs-item-main{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
119
|
-
.fs-item-meta{flex:none;display:flex;align-items:center;gap:10px;font
|
|
150
|
+
.fs-item-meta{flex:none;display:flex;align-items:center;gap:10px;font:11px/16px var(--fs-mono);color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}
|
|
120
151
|
.fs-item-meta a{color:inherit;text-decoration:none}
|
|
121
152
|
@media (hover:hover){.fs-item-meta a:hover{color:var(--dsw-alias-label-primary)}}
|
|
122
153
|
.fs-item .fs-icon-btn{opacity:0}
|
|
@@ -132,34 +163,35 @@ select.fs-input{appearance:none;-webkit-appearance:none;padding-right:32px;curso
|
|
|
132
163
|
|
|
133
164
|
/* 她此刻:大标题 + 日程条 */
|
|
134
165
|
.fs-now{display:flex;align-items:center;flex-wrap:wrap;gap:8px 10px;padding-bottom:12px}
|
|
135
|
-
.fs-now-title{font-size:
|
|
136
|
-
.fs-now-meta{margin-left:auto;display:flex;align-items:center;gap:8px;font
|
|
137
|
-
.fs-schedule{display:grid;grid-template-columns:repeat(auto-fit,minmax(96px,1fr));gap:
|
|
138
|
-
.fs-slot{display:flex;flex-direction:column;min-width:0;padding:
|
|
166
|
+
.fs-now-title{font-size:15px;line-height:22px;font-weight:600}
|
|
167
|
+
.fs-now-meta{margin-left:auto;display:flex;align-items:center;gap:8px;font:12px/18px var(--fs-mono);color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums}
|
|
168
|
+
.fs-schedule{display:grid;grid-template-columns:repeat(auto-fit,minmax(96px,1fr));gap:0;border:1px solid var(--fs-line);border-radius:8px;overflow:hidden}
|
|
169
|
+
.fs-slot{display:flex;flex-direction:column;min-width:0;padding:6px 10px;border-left:1px solid var(--fs-line);font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
|
|
139
170
|
.fs-slot>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
140
171
|
.fs-slot>span:first-child{font-weight:500;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary)}
|
|
141
|
-
.fs-slot
|
|
172
|
+
.fs-slot:first-child{border-left:0}
|
|
173
|
+
.fs-slot>span:first-child{font-family:var(--fs-mono)}
|
|
174
|
+
.fs-slot[aria-current=true]{background:var(--dsw-alias-bg-module-platform);box-shadow:inset 0 -2px 0 var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary)}
|
|
142
175
|
.fs-slot[aria-current=true]>span:first-child{color:var(--dsw-alias-label-primary)}
|
|
143
176
|
|
|
144
177
|
/* 提醒条 */
|
|
145
|
-
.fs-notice{display:flex;flex-direction:column;gap:
|
|
178
|
+
.fs-notice{display:flex;flex-direction:column;gap:2px;padding:8px 12px;border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary) 30%,transparent);border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 4%,transparent);font-size:13px;line-height:20px}
|
|
146
179
|
.fs-notice b{font-weight:600;color:var(--dsw-alias-state-error-primary)}
|
|
147
180
|
|
|
148
181
|
/* 截图 / 二维码 / 报告 */
|
|
149
|
-
.fs-shot{display:block;margin:4px 0
|
|
182
|
+
.fs-shot{display:block;margin:4px 0 12px;width:100%;overflow:hidden;border-radius:8px;border:1px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-1);cursor:pointer}
|
|
150
183
|
.fs-shot img{display:block;width:100%;height:auto;user-select:none}
|
|
151
184
|
.fs-shot .fs-empty{padding:64px 0}
|
|
152
|
-
.fs-qr{display:flex;align-items:center;gap:16px;padding:4px 0 16px;border-bottom
|
|
185
|
+
.fs-qr{display:flex;align-items:center;gap:16px;padding:4px 0 16px;border-bottom:1px solid var(--dsw-alias-border-l2)}
|
|
153
186
|
.fs-qr img{flex:none;width:160px;height:160px;padding:8px;border-radius:12px;background:#fff}
|
|
154
187
|
.fs-group>.fs-report{margin-bottom:4px}
|
|
155
|
-
.fs-report{max-height:220px;overflow:auto;padding:10px
|
|
188
|
+
.fs-report{max-height:220px;overflow:auto;padding:8px 10px;border:1px solid var(--fs-line);border-radius:6px;background:var(--dsw-alias-bg-module-platform);font:11px/17px var(--fs-mono);white-space:pre-wrap;word-break:break-all;color:var(--dsw-alias-label-secondary)}
|
|
156
189
|
|
|
157
190
|
/* Toast:宿主 Toast 的位置与表面 */
|
|
158
|
-
.fs-toast{position:fixed;top:40px;left:50%;z-index:1100;width:max-content;max-width:min(560px,calc(100vw - 48px));padding:
|
|
191
|
+
.fs-toast{position:fixed;top:40px;left:50%;z-index:1100;width:max-content;max-width:min(560px,calc(100vw - 48px));padding:6px 12px;border-radius:8px;font-family:var(--fs-mono);font-size:12px !important;line-height:18px !important;background:var(--dsw-alias-toast-bg);color:var(--dsw-alias-toast-label);font-size:14px;line-height:22px;pointer-events:none;transform:translateX(-50%);animation:fs-toast-in 160ms ease-out}
|
|
159
192
|
@keyframes fs-toast-in{from{opacity:0;transform:translate(-50%,-6px)}to{opacity:1;transform:translate(-50%,0)}}
|
|
160
193
|
|
|
161
|
-
@media (
|
|
162
|
-
@media (prefers-reduced-motion:reduce){.fs-tabs-ind,.fs-seg-ind,.fs-tab,.fs-seg-btn,.fs-switch-thumb,.fs-fill{transition:none}.fs-spin g,.fs-spin circle:last-child,.fs-toast{animation:none}}
|
|
194
|
+
@media (prefers-reduced-motion:reduce){.fs-seg-ind,.fs-tab,.fs-seg-btn,.fs-switch-thumb,.fs-fill{transition:none}.fs-spin g,.fs-spin circle:last-child,.fs-toast{animation:none}}
|
|
163
195
|
/* v0.6:记忆编辑 / 导入预览 / 动态 */
|
|
164
196
|
a.fs-btn{text-decoration:none}
|
|
165
197
|
.fs-item.edit{flex-direction:column;align-items:stretch;gap:8px}
|
|
@@ -171,13 +203,13 @@ a.fs-btn{text-decoration:none}
|
|
|
171
203
|
.fs-strong{font-weight:500;color:var(--dsw-alias-label-primary)}
|
|
172
204
|
.fs-group-aside{display:inline-flex;align-items:center;gap:8px;font-weight:400}
|
|
173
205
|
.fs-stickers{display:grid;grid-template-columns:repeat(auto-fill,minmax(84px,1fr));gap:8px;padding:8px 0}
|
|
174
|
-
.fs-sticker{position:relative;display:flex;flex-direction:column;align-items:center;gap:4px;padding:8px 4px 4px;border-radius:
|
|
206
|
+
.fs-sticker{position:relative;display:flex;flex-direction:column;align-items:center;gap:4px;padding:8px 4px 4px;border:1px solid var(--fs-line);border-radius:6px;background:var(--dsw-alias-bg-module-platform)}
|
|
175
207
|
.fs-sticker img{width:64px;height:64px;object-fit:contain}
|
|
176
208
|
.fs-sticker figcaption{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px}
|
|
177
209
|
.fs-sticker .fs-icon-btn{position:absolute;top:2px;right:2px;opacity:0}
|
|
178
210
|
.fs-sticker:hover .fs-icon-btn,.fs-sticker:focus-within .fs-icon-btn{opacity:1}
|
|
179
211
|
/* 人格页:模板卡片、说话示例气泡、世界书编辑 */
|
|
180
|
-
.fs-persona{padding:10px 0;border-bottom
|
|
212
|
+
.fs-persona{padding:10px 0;border-bottom:1px solid var(--dsw-alias-border-l2)}
|
|
181
213
|
.fs-persona:last-child{border-bottom:0}
|
|
182
214
|
.fs-persona-head{display:flex;align-items:center;gap:12px}
|
|
183
215
|
.fs-persona-title{flex:1;min-width:0;display:flex;flex-wrap:wrap;align-items:center;gap:4px 8px;font-size:13px;line-height:20px}
|
|
@@ -186,10 +218,57 @@ a.fs-btn{text-decoration:none}
|
|
|
186
218
|
.fs-persona-desc{margin:0;font-size:12px;line-height:19px;color:var(--dsw-alias-label-secondary);white-space:pre-wrap}
|
|
187
219
|
.fs-persona-foot{justify-content:flex-end;padding-top:10px}
|
|
188
220
|
.fs-tags{display:flex;flex-wrap:wrap;gap:6px}
|
|
189
|
-
.fs-chat{display:flex;flex-direction:column;gap:4px;padding:10px
|
|
221
|
+
.fs-chat{display:flex;flex-direction:column;gap:4px;padding:8px 10px;border:1px solid var(--fs-line);border-radius:8px;background:var(--dsw-alias-bg-module-platform);font-size:13px;line-height:20px}
|
|
190
222
|
.fs-chat-sep{height:4px}
|
|
191
223
|
.fs-chat-sep:first-child{display:none}
|
|
192
|
-
.fs-bubble{align-self:flex-start;max-width:80%;padding:4px 10px;border-radius:10px;background:var(--dsw-alias-bg-layer-1);box-shadow:inset 0 0 0
|
|
224
|
+
.fs-bubble{align-self:flex-start;max-width:80%;padding:4px 10px;border-radius:10px;background:var(--dsw-alias-bg-layer-1);box-shadow:inset 0 0 0 1px var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);white-space:pre-wrap}
|
|
193
225
|
.fs-bubble[data-me]{align-self:flex-end;background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 12%,transparent);box-shadow:none}
|
|
194
226
|
.fs-lore{display:flex;flex-direction:column;gap:8px;width:100%}
|
|
195
227
|
.fs-lore-item{display:grid;grid-template-columns:minmax(120px,1fr) 3fr auto;gap:8px;align-items:start}
|
|
228
|
+
|
|
229
|
+
/* 执行步骤胶囊:单行状态条,默认收起,点开看详情 */
|
|
230
|
+
.fs-steps{display:flex;flex-direction:column;gap:4px;padding-top:6px}
|
|
231
|
+
.fs-step{min-width:0;border:1px solid var(--fs-line);border-radius:6px;background:var(--dsw-alias-bg-layer-1)}
|
|
232
|
+
.fs-step[open]{background:var(--dsw-alias-bg-module-platform)}
|
|
233
|
+
.fs-step-head{display:flex;align-items:center;gap:8px;min-height:28px;padding:3px 8px;font-size:12px;line-height:18px;list-style:none;cursor:default}
|
|
234
|
+
summary.fs-step-head{cursor:pointer}
|
|
235
|
+
summary.fs-step-head::-webkit-details-marker{display:none}
|
|
236
|
+
summary.fs-step-head>svg{flex:none;color:var(--dsw-alias-label-tertiary);transform:rotate(-90deg);transition:transform 120ms ease}
|
|
237
|
+
.fs-step[open]>summary>svg{transform:none}
|
|
238
|
+
@media (hover:hover){summary.fs-step-head:hover{background:var(--dsw-alias-interactive-bg-hover)}}
|
|
239
|
+
.fs-step-dot{flex:none;width:6px;height:6px;border-radius:50%;background:var(--dsw-alias-label-tertiary)}
|
|
240
|
+
.fs-step-dot[data-tone=success]{background:var(--dsw-alias-state-success-primary)}
|
|
241
|
+
.fs-step-dot[data-tone=info]{background:var(--dsw-alias-state-business-primary)}
|
|
242
|
+
.fs-step-dot[data-tone=warning]{background:var(--dsw-alias-state-warn-primary)}
|
|
243
|
+
.fs-step-dot[data-tone=danger]{background:var(--dsw-alias-state-error-primary)}
|
|
244
|
+
.fs-step-kind{flex:none;min-width:3.5em;font:11px/16px var(--fs-mono);color:var(--dsw-alias-label-secondary)}
|
|
245
|
+
.fs-step .fs-item-main{color:var(--dsw-alias-label-secondary)}
|
|
246
|
+
.fs-step-body{display:flex;flex-direction:column;gap:4px;padding:6px 8px 8px 30px;border-top:1px solid var(--fs-line);font:11px/17px var(--fs-mono);color:var(--dsw-alias-label-secondary);white-space:pre-wrap;overflow-wrap:anywhere}
|
|
247
|
+
.fs-step-body ul{display:flex;flex-direction:column;gap:2px;list-style:none}
|
|
248
|
+
.fs-step-body li::before{content:"› ";color:var(--dsw-alias-label-tertiary)}
|
|
249
|
+
|
|
250
|
+
/* 一体化底部输入舱 */
|
|
251
|
+
.fs-dock{position:sticky;bottom:0;z-index:2;display:flex;align-items:center;gap:6px;margin-top:6px;padding:4px 4px 4px 2px;border:1px solid var(--fs-line-strong);border-radius:10px;background:var(--dsw-alias-bg-layer-3)}
|
|
252
|
+
.fs-dock .fs-input{height:28px;border-color:transparent;background:transparent}
|
|
253
|
+
.fs-dock .fs-input:focus-visible{border-color:transparent}
|
|
254
|
+
.fs-dock:focus-within{border-color:var(--dsw-alias-state-business-primary)}
|
|
255
|
+
.fs-dock .fs-btn{flex:none}
|
|
256
|
+
|
|
257
|
+
/* 窄容器:侧栏变顶部横条,详情面板落到下方 */
|
|
258
|
+
@container (max-width:720px){
|
|
259
|
+
.fs,.fs.has-detail,.fs.nav-collapsed{grid-template-columns:minmax(0,1fr)}
|
|
260
|
+
.fs-side{position:static;flex-direction:row;align-items:flex-start;gap:6px;padding:6px;border-right:0;border-bottom:1px solid var(--fs-line);border-radius:12px 12px 0 0}
|
|
261
|
+
.fs-side-head{margin:0;padding:0}.fs-side-head .fs-icon-btn{display:none}
|
|
262
|
+
.fs-nav{flex:1 1 auto;flex-direction:row;flex-wrap:wrap;gap:2px}
|
|
263
|
+
.fs-tab-key{display:none}
|
|
264
|
+
.fs.nav-collapsed .fs-tab-label{display:inline}
|
|
265
|
+
.fs.nav-collapsed .fs-tab{padding:0 8px}
|
|
266
|
+
.fs-side-foot{flex:none;margin:0;padding:0;border:0}
|
|
267
|
+
.fs-ver .fs-tab-label{display:none}
|
|
268
|
+
.fs-main{padding:12px 14px 18px}
|
|
269
|
+
.fs-detail{position:static;border-left:0;border-top:1px solid var(--fs-line);border-radius:0 0 12px 12px}
|
|
270
|
+
.fs-row{flex-wrap:wrap;gap:6px 12px}.fs-row-title{max-width:none}
|
|
271
|
+
.fs-input,.fs-affix{flex:1 1 100%;width:100%}
|
|
272
|
+
.fs-dock .fs-input{flex:1 1 auto;width:auto}
|
|
273
|
+
}
|
|
274
|
+
@media (prefers-reduced-motion:reduce){summary.fs-step-head>svg{transition:none}}
|
package/lib/routes.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 渠道(QQ / B站 / 抖音)路由在这里和 routes-douyin.js;v0.6 的记忆纠正 / 人物画像 / 动态 / 暂停 / 备份 /
|
|
7
7
|
* 聊天记录导入在 routes-soul.js;v0.7 的模型 / 用量 / 语音 / 表情包 / 群里的说法 / 首次引导在 routes-v07.js(UI 文档契约 21~31)。
|
|
8
8
|
*/
|
|
9
|
-
import { readFileSync } from 'node:fs';
|
|
9
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
10
10
|
import { readVecMeta, writeVecMeta, listChatSessions } from './db.js';
|
|
11
11
|
import { douyinState, handleDouyinRoute } from './routes-douyin.js';
|
|
12
12
|
import { handleSoulRoute, sourceLabel } from './routes-soul.js';
|
|
@@ -15,6 +15,7 @@ import { handleV09Route } from './routes-v09.js';
|
|
|
15
15
|
import { handleUpdateRoute } from './routes-update.js';
|
|
16
16
|
import { handleWorldInfoRoute } from './routes-worldinfo.js';
|
|
17
17
|
import { handlePersonaRoute } from './routes-persona.js';
|
|
18
|
+
import { pluginVersion } from './update-check.js';
|
|
18
19
|
import { stickerConfig } from './stickers.js';
|
|
19
20
|
import { slangConfig } from './slang.js';
|
|
20
21
|
import { importMemories } from './importer.js';
|
|
@@ -469,6 +470,7 @@ export function registerRoutes(ctx, deps) {
|
|
|
469
470
|
allGroups: q.allGroups === true,
|
|
470
471
|
followUpCount: q.followUpCount ?? 3,
|
|
471
472
|
followUpSeconds: q.followUpSeconds ?? 120,
|
|
473
|
+
followUpSmart: q.followUpSmart === true,
|
|
472
474
|
batchMs: q.batchMs ?? 3000,
|
|
473
475
|
callNames: Array.isArray(q.callNames) ? q.callNames : [],
|
|
474
476
|
interject: interjectConfig(q.interject),
|
|
@@ -492,6 +494,54 @@ export function registerRoutes(ctx, deps) {
|
|
|
492
494
|
});
|
|
493
495
|
}
|
|
494
496
|
|
|
497
|
+
// ── 一键自检(只读:查不动的那一项标红,不抛 500)──
|
|
498
|
+
if (method === 'GET' && path === '/diagnose') {
|
|
499
|
+
const cfg = getConfig();
|
|
500
|
+
const items = [];
|
|
501
|
+
const item = (key, label, ok, detail, hint) => items.push({ key, label, ok: ok === true, detail: String(detail ?? ''), ...(hint ? { hint } : {}) });
|
|
502
|
+
// 插件版本
|
|
503
|
+
const version = pluginVersion();
|
|
504
|
+
item('version', '插件版本', version !== '0.0.0', version === '0.0.0' ? '读不到 package.json' : `v${version}`);
|
|
505
|
+
// 通道连接(跟 /channels/state 同一份数据)
|
|
506
|
+
const qqSt = channel?.status() ?? null;
|
|
507
|
+
const biliSt = bili?.status() ?? null;
|
|
508
|
+
const douyinSt = deps.douyin?.status() ?? null;
|
|
509
|
+
const qqOn = cfg.qq?.enabled === true;
|
|
510
|
+
const biliOn = cfg.bili?.enabled === true;
|
|
511
|
+
const douyinOn = cfg.douyin?.enabled === true;
|
|
512
|
+
const chanOk = (!qqOn || qqSt?.connected === true) && (!biliOn || biliSt?.loggedIn === true) && (!douyinOn || douyinSt?.state === 'running');
|
|
513
|
+
item('channels', '通道连接', chanOk, [
|
|
514
|
+
qqOn ? (qqSt?.connected === true ? 'QQ 已连' : 'QQ 没连上') : 'QQ 没开',
|
|
515
|
+
biliOn ? (biliSt?.loggedIn === true ? 'B站 已登录' : 'B站 没登录') : 'B站 没开',
|
|
516
|
+
douyinOn ? (douyinSt?.state === 'running' ? '抖音 在跑' : '抖音 没跑') : '抖音 没开'
|
|
517
|
+
].join(' · '));
|
|
518
|
+
// QQ 登录态(SnowLuma 连上没有、登的是哪个号)
|
|
519
|
+
item('qq', 'QQ 登录态', qqSt?.connected === true, qqSt?.connected === true
|
|
520
|
+
? `已登录 ${qqSt.nickname || qqSt.selfId || ''}${qqSt.friendCount != null ? ` · ${qqSt.friendCount} 位好友` : ''}`.trim()
|
|
521
|
+
: (qqOn ? `没连上${qqSt?.lastError ? ':' + qqSt.lastError : ''}` : 'QQ 没开'));
|
|
522
|
+
// 记忆条数
|
|
523
|
+
try {
|
|
524
|
+
const n = db.prepare("SELECT COUNT(*) AS n FROM memory WHERE status = 'active' AND valid_to IS NULL").get().n;
|
|
525
|
+
item('memory', '记忆条数', true, `${n} 条`);
|
|
526
|
+
} catch (error) {
|
|
527
|
+
item('memory', '记忆条数', false, `查不到:${String(error instanceof Error ? error.message : error).slice(0, 80)}`);
|
|
528
|
+
}
|
|
529
|
+
// 今日次数(插嘴 / 主动开口)
|
|
530
|
+
try {
|
|
531
|
+
const today = new Date().toDateString();
|
|
532
|
+
const interject = (gateway?.decisions ?? []).filter((d) => d.kind === 'interject' && new Date(d.at).toDateString() === today).length;
|
|
533
|
+
let proactive = 0;
|
|
534
|
+
try { proactive = Number(life?.stateSnapshot?.()?.today?.proactiveCount ?? 0); } catch { /* 无 */ }
|
|
535
|
+
item('today', '今日次数', true, `插嘴 ${interject} 次 · 主动开口 ${proactive} 次`, '插嘴次数只统计本次启动后(动态缓冲最多 300 条)');
|
|
536
|
+
} catch (error) {
|
|
537
|
+
item('today', '今日次数', false, `查不到:${String(error instanceof Error ? error.message : error).slice(0, 80)}`);
|
|
538
|
+
}
|
|
539
|
+
// 补丁状态(插件包里的 cordis.patch.yml 在不在)
|
|
540
|
+
const patchOk = existsSync(new URL('../cordis.patch.yml', import.meta.url));
|
|
541
|
+
item('patch', '补丁状态', patchOk, patchOk ? 'cordis.patch.yml 在' : '没找到 cordis.patch.yml(插件可能没装全)');
|
|
542
|
+
return send(200, { ok: true, items });
|
|
543
|
+
}
|
|
544
|
+
|
|
495
545
|
// ── 契约 11:保存 QQ 接入配置(连接参数变了就重连)──
|
|
496
546
|
if (method === 'POST' && path === '/channels/qq-config') {
|
|
497
547
|
if (!sameOrigin(req)) return send(403, { ok: false, error: 'forbidden' });
|
|
@@ -512,6 +562,7 @@ export function registerRoutes(ctx, deps) {
|
|
|
512
562
|
allGroups: body.allGroups === undefined ? cur.allGroups === true : body.allGroups === true,
|
|
513
563
|
followUpCount: Math.min(20, Math.max(0, Math.floor(Number(body.followUpCount ?? cur.followUpCount ?? 3)) || 0)),
|
|
514
564
|
followUpSeconds: Math.min(1800, Math.max(0, Math.floor(Number(body.followUpSeconds ?? cur.followUpSeconds ?? 120)) || 0)),
|
|
565
|
+
followUpSmart: body.followUpSmart === undefined ? cur.followUpSmart === true : body.followUpSmart === true,
|
|
515
566
|
batchMs: Math.min(15000, Math.max(0, Math.floor(Number(body.batchMs ?? cur.batchMs ?? 3000)) || 0)),
|
|
516
567
|
callNames: Array.isArray(body.callNames) ? body.callNames.map((x) => String(x).trim()).filter(Boolean).slice(0, 10) : (cur.callNames ?? []),
|
|
517
568
|
interject: interjectConfig(body.interject === undefined ? cur.interject : { ...(cur.interject ?? {}), ...body.interject })
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-fish-soul",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "鱼 · 人格、按人记忆(可纠正、有印象、能导入旧聊天记录)、日程自走与主动开口,直连 SnowLuma 收发 QQ(群里会自己插话),B站 / 抖音小号私信与自己刷视频;按功能选模型并统计用量花费,能听会说 QQ 语音、收藏表情包、学群里的梗,可选上网查;心情影响说话、去 AI 味、每个群的设定、按聊天选模型、每晚写日记(DSH 插件,单角色 feiyu ,v0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
|
+
"description": "鱼 · 人格、按人记忆(可纠正、有印象、能导入旧聊天记录)、日程自走与主动开口,直连 SnowLuma 收发 QQ(群里会自己插话),B站 / 抖音小号私信与自己刷视频;按功能选模型并统计用量花费,能听会说 QQ 语音、收藏表情包、学群里的梗,可选上网查;心情影响说话、去 AI 味、每个群的设定、按聊天选模型、每晚写日记(DSH 插件,单角色 feiyu ,v0.15.0) ",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
7
7
|
"files": [
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
64
|
"scripts": {
|
|
65
|
-
"test": "node test/smoke.mjs && node test/douyin-browser.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/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/client-lint.mjs && node test/dsh-desktop.mjs",
|
|
65
|
+
"test": "node test/smoke.mjs && node test/douyin-browser.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/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/client-lint.mjs && node test/dsh-desktop.mjs && node test/issue-59-interject.mjs && node test/issue-59-diagnose.mjs",
|
|
66
66
|
"eval": "node test/eval/persona-eval.mjs"
|
|
67
67
|
}
|
|
68
68
|
}
|