dsh-fish-soul 0.15.2 → 0.15.3
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.js +173 -65
- package/lib/client.models.js +1 -1
- package/lib/client.soul.js +4 -5
- package/lib/panel.css +118 -8
- package/package.json +4 -3
package/README.md
CHANGED
package/lib/client.js
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
* 宿主契约:
|
|
7
7
|
* - window.__ModuleLoader__.load({ id, factory }),exports.apply / inject;inject: ["slots"]
|
|
8
8
|
* - 通过 settings.section 保留设置入口;在宿主支持时通过 main + sidebar.panellist 注册独立页;样式随组件挂载 / 卸载
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
9
|
+
* - 优先使用宿主提供的 @deepseek-ai/dsh-client-ui-primitives;旧宿主缺失时基础控件降级,
|
|
10
|
+
* 个别需要专用语义与布局的控件使用插件局部实现(lib/panel.css,fs- 前缀)
|
|
11
11
|
*
|
|
12
12
|
* 设计信条:Avoid unnecessary secondary labels and descriptions. Let the layout do the explaining.
|
|
13
|
-
* - DeepSeek Harness
|
|
13
|
+
* - DeepSeek Harness 工作台骨架:主页与设置页共用两级顶部导航 | 居中主任务流 | 按需展开的详情面板
|
|
14
14
|
* - 动态 / 整理记录渲染成单行步骤胶囊(<details>),默认收起,点开看原因与想起的记忆
|
|
15
15
|
* - 平铺紧凑行(标题左 / 控件右),1px 低对比分隔;Row 的 desc 只作悬停提示,meta(实时状态)用等宽小字
|
|
16
16
|
* - 状态 = 一颗 StateDot + 一个短句;只在出问题时多说一句
|
|
@@ -24,6 +24,10 @@ window.__ModuleLoader__.load({
|
|
|
24
24
|
var exports = module.exports;
|
|
25
25
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
26
26
|
const react = require("react");
|
|
27
|
+
// 新版宿主把官方组件作为共享模块提供;旧宿主无此模块时保留原控件。
|
|
28
|
+
/** @type {any} */
|
|
29
|
+
let primitives = null;
|
|
30
|
+
try { primitives = require("@deepseek-ai/dsh-client-ui-primitives"); } catch { /* 旧宿主 */ }
|
|
27
31
|
|
|
28
32
|
const inject = ["slots"];
|
|
29
33
|
const PLUGIN_ID = "dsh-fish-soul";
|
|
@@ -85,7 +89,7 @@ window.__ModuleLoader__.load({
|
|
|
85
89
|
return hr < 24 ? `${hr} 小时前` : `${Math.floor(hr / 24)} 天前`;
|
|
86
90
|
}
|
|
87
91
|
|
|
88
|
-
// ──
|
|
92
|
+
// ── 控件:参考 dsh-ui-primitives,自包含以兼容插件加载器 ──
|
|
89
93
|
const ICONS = {
|
|
90
94
|
eye: "M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12Z M12 9.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5Z",
|
|
91
95
|
close: "M7 7l10 10M17 7 7 17",
|
|
@@ -95,27 +99,42 @@ window.__ModuleLoader__.load({
|
|
|
95
99
|
sidebar: "M4 5h16v14H4Z M9 5v14",
|
|
96
100
|
info: "M12 8h.01 M11 12h1v5h1 M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Z"
|
|
97
101
|
};
|
|
98
|
-
const
|
|
99
|
-
|
|
102
|
+
const ICON_COMPONENTS = { close: "IconCloseOutlineRegular", refresh: "IconRefreshOutlineRegular", chevron: "IconChevronDownOutlineRegular", edit: "IconEditOutlineRegular", sidebar: "IconPanelLeftOutlineRegular", info: "IconInfoOutlineRegular" };
|
|
103
|
+
const Icon = (name, size = 16) => primitives?.[ICON_COMPONENTS[name]] ? h(primitives[ICON_COMPONENTS[name]], { size, "aria-hidden": true })
|
|
104
|
+
: 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 }, h("path", { d: ICONS[name] }));
|
|
100
105
|
|
|
101
106
|
function Switch({ checked, onChange, label, disabled }) {
|
|
107
|
+
if (primitives?.Switch) return h(primitives.Switch, { checked: checked === true, onChange, label, disabled, className: "fs-native-switch" });
|
|
102
108
|
return h("button", { type: "button", role: "switch", "aria-checked": checked === true, "aria-label": label, disabled, className: "fs-switch", onClick: () => onChange(!checked) },
|
|
103
109
|
h("span", { className: "fs-switch-thumb" }));
|
|
104
110
|
}
|
|
105
|
-
const Button = ({ variant, ...props }, ...kids) =>
|
|
106
|
-
|
|
111
|
+
const Button = ({ variant, ...props }, ...kids) => primitives?.Button
|
|
112
|
+
? h(primitives.Button, { ...props, variant: variant === "danger" ? "outline" : variant ?? "outline", size: "sm", className: cx("fs-native-btn", variant === "danger" && "fs-native-danger", variant === "primary" && "fs-native-primary", props.className) }, ...kids)
|
|
113
|
+
: h("button", { type: "button", ...props, className: cx("fs-btn", variant) }, ...kids);
|
|
114
|
+
const IconButton = (icon, label, onClick, extra) => primitives?.Button
|
|
115
|
+
? h(primitives.Button, { variant: "ghost", size: "sm", className: "fs-icon-btn fs-native-icon", "aria-label": label, title: label, onClick, ...extra }, Icon(icon, 14))
|
|
116
|
+
: h("button", { type: "button", className: "fs-icon-btn", "aria-label": label, title: label, onClick, ...extra }, Icon(icon, 14));
|
|
107
117
|
|
|
108
118
|
function Seg({ options, value, onChange, label }) {
|
|
109
119
|
const index = Math.max(0, options.findIndex((o) => o.value === value));
|
|
110
120
|
return h("div", { className: "fs-seg", role: "radiogroup", "aria-label": label, style: { "--n": options.length, "--i": index } },
|
|
111
121
|
h("span", { className: "fs-seg-ind", "aria-hidden": true }),
|
|
112
|
-
options.map((o) => h("button", {
|
|
122
|
+
options.map((o) => h(primitives?.Pill ?? "button", {
|
|
113
123
|
type: "button", key: o.value, role: "radio", "aria-checked": o.value === value,
|
|
114
124
|
className: "fs-seg-btn", onClick: () => { if (o.value !== value) onChange(o.value); }
|
|
115
125
|
}, o.label)));
|
|
116
126
|
}
|
|
117
127
|
|
|
118
|
-
function Tabs({ items, value, onChange }) {
|
|
128
|
+
function Tabs({ items, value, onChange, horizontal = false, hasUpdate = false }) {
|
|
129
|
+
const navRef = react.useRef(null);
|
|
130
|
+
react.useEffect(() => {
|
|
131
|
+
const nav = navRef.current;
|
|
132
|
+
const selected = nav?.querySelector('[role="tab"][aria-selected="true"]');
|
|
133
|
+
if (!nav || !selected) return;
|
|
134
|
+
const outer = nav.getBoundingClientRect(), inner = selected.getBoundingClientRect();
|
|
135
|
+
if (inner.left < outer.left) nav.scrollLeft -= outer.left - inner.left;
|
|
136
|
+
else if (inner.right > outer.right) nav.scrollLeft += inner.right - outer.right;
|
|
137
|
+
}, [value]);
|
|
119
138
|
const index = Math.max(0, items.findIndex(([v]) => v === value));
|
|
120
139
|
const onKeyDown = (event, i) => {
|
|
121
140
|
const n = items.length;
|
|
@@ -126,20 +145,60 @@ window.__ModuleLoader__.load({
|
|
|
126
145
|
event.currentTarget.parentElement?.querySelectorAll('[role="tab"]').item(next)?.focus();
|
|
127
146
|
onChange(items[next][0]);
|
|
128
147
|
};
|
|
129
|
-
return h("nav", { className: "fs-nav", role: "tablist", "aria-label": "鱼", "aria-orientation": "vertical", style: { "--n": items.length, "--i": index } },
|
|
148
|
+
return h("nav", { className: "fs-nav", ref: navRef, role: "tablist", "aria-label": "鱼", "aria-orientation": horizontal ? "horizontal" : "vertical", style: { "--n": items.length, "--i": index } },
|
|
130
149
|
items.map(([v, label], i) => h("button", {
|
|
131
150
|
type: "button", key: v, role: "tab", id: `fs-tab-${v}`, "aria-selected": v === value, "aria-controls": "fs-tabpanel",
|
|
132
|
-
tabIndex: v === value ? 0 : -1, className: "fs-tab", title: label, onClick: () => onChange(v), onKeyDown: (e) => onKeyDown(e, i)
|
|
133
|
-
}, h("span", { className: "fs-tab-key", "aria-hidden": true }, TAB_KEYS[v] ?? label.slice(0, 1)), h("span", { className: "fs-tab-label" }, label))));
|
|
151
|
+
tabIndex: v === value ? 0 : -1, className: "fs-tab", title: v === "version" && hasUpdate ? "版本更新(有新版本)" : label, "aria-label": v === "version" && hasUpdate ? "版本更新(有新版本)" : label, onClick: () => onChange(v), onKeyDown: (e) => onKeyDown(e, i)
|
|
152
|
+
}, h("span", { className: "fs-tab-key", "aria-hidden": true }, TAB_KEYS[v] ?? label.slice(0, 1)), h("span", { className: "fs-tab-label" }, label), v === "version" && hasUpdate ? h("span", { className: "fs-update-dot", "aria-hidden": true }) : null)));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 两级导航保留原有页面,按任务分类;引导跳转仍以原页签 id 为真源。
|
|
156
|
+
function GroupedTabs({ items, value, onChange, hasUpdate }) {
|
|
157
|
+
const groups = NAV_GROUPS.map((group) => ({ ...group, items: items.filter(([id]) => group.ids.includes(id)) })).filter((group) => group.items.length > 0);
|
|
158
|
+
const active = groups.find((group) => group.items.some(([id]) => id === value)) ?? groups[0];
|
|
159
|
+
const remembered = react.useRef({});
|
|
160
|
+
const groupRef = react.useRef(null);
|
|
161
|
+
react.useEffect(() => {
|
|
162
|
+
if (active) remembered.current[active.id] = value;
|
|
163
|
+
const nav = groupRef.current;
|
|
164
|
+
const selected = nav?.querySelector('[aria-pressed="true"]');
|
|
165
|
+
if (!nav || !selected) return;
|
|
166
|
+
const outer = nav.getBoundingClientRect(), inner = selected.getBoundingClientRect();
|
|
167
|
+
if (inner.left < outer.left) nav.scrollLeft -= outer.left - inner.left;
|
|
168
|
+
else if (inner.right > outer.right) nav.scrollLeft += inner.right - outer.right;
|
|
169
|
+
}, [value]);
|
|
170
|
+
return h(react.Fragment, null,
|
|
171
|
+
h("div", { className: "fs-group-row" },
|
|
172
|
+
h("span", { className: "fs-brand" }, "鱼"),
|
|
173
|
+
h("nav", { className: "fs-group-nav", ref: groupRef, "aria-label": "功能分类" }, groups.map((group) => {
|
|
174
|
+
const selected = group.id === active?.id;
|
|
175
|
+
const showUpdate = hasUpdate && group.ids.includes("version");
|
|
176
|
+
return h("button", { type: "button", key: group.id, className: "fs-group-tab", "aria-pressed": selected,
|
|
177
|
+
"aria-label": showUpdate ? `${group.label}(有新版本)` : group.label,
|
|
178
|
+
onClick: () => onChange(remembered.current[group.id] ?? group.items[0][0]) },
|
|
179
|
+
group.label, showUpdate ? h("span", { className: "fs-update-dot", "aria-hidden": true }) : null);
|
|
180
|
+
}))),
|
|
181
|
+
h("div", { className: "fs-subnav-row" }, h(Tabs, { items: active?.items ?? [], value, onChange, horizontal: true, hasUpdate })));
|
|
134
182
|
}
|
|
135
183
|
|
|
136
184
|
/** StateDot:done / warning / error / idle 是圆点,ongoing 是转圈。 */
|
|
137
|
-
const Dot = (state) => state === "ongoing"
|
|
185
|
+
const Dot = (state) => primitives?.StateDot ? h(primitives.StateDot, { state }) : state === "ongoing"
|
|
138
186
|
? h("svg", { className: "fs-spin", width: 14, height: 14, viewBox: "0 0 24 24", "aria-hidden": true },
|
|
139
187
|
h("g", null, h("circle", { cx: 12, cy: 12, r: 9.5 }), h("circle", { cx: 12, cy: 12, r: 9.5 })))
|
|
140
188
|
: h("span", { className: "fs-dot", "data-state": state, "aria-hidden": true });
|
|
141
189
|
const State = (state, text, title) => h("span", { className: "fs-state", "data-state": state, title: title ?? text, role: "status" }, Dot(state), h("span", null, text));
|
|
142
|
-
const Tag = (text, tone, extra, ...kids) =>
|
|
190
|
+
const Tag = (text, tone, extra, ...kids) => {
|
|
191
|
+
if (!primitives?.Tag) return h("span", { className: "fs-tag", "data-tone": tone, ...extra }, text, ...kids);
|
|
192
|
+
const tag = h(primitives.Tag, { tone: tone ?? "neutral", className: "fs-native-tag" }, text, ...kids);
|
|
193
|
+
return extra ? h("span", { className: "fs-native-tag-wrap", ...extra }, tag) : tag;
|
|
194
|
+
};
|
|
195
|
+
const Pill = ({ active, children, ...props }) => primitives?.Pill
|
|
196
|
+
? h(primitives.Pill, { ...props, active }, children)
|
|
197
|
+
: h("button", { type: "button", ...props }, children);
|
|
198
|
+
function Check({ checked, onChange, label, hideLabel, title }) {
|
|
199
|
+
return primitives?.Checkbox ? h(primitives.Checkbox, { checked, onChange, label, title, className: cx("fs-native-check", hideLabel && "label-hidden") })
|
|
200
|
+
: h("label", { className: cx("fs-native-check", hideLabel && "label-hidden"), title }, h("input", { type: "checkbox", checked, onChange: (e) => onChange(e.target.checked) }), h("span", null, label));
|
|
201
|
+
}
|
|
143
202
|
const Muted = (text, extra) => h("span", { className: "fs-unit", ...extra }, text);
|
|
144
203
|
|
|
145
204
|
// 行:标题左 / 控件右。desc(解释性文字)不占版面,只作悬停提示;meta(实时状态)以等宽小字跟在标题下
|
|
@@ -152,19 +211,23 @@ window.__ModuleLoader__.load({
|
|
|
152
211
|
meta ? h("span", { className: "fs-row-meta" }, meta) : null),
|
|
153
212
|
h("div", { className: "fs-ctl" }, ...kids));
|
|
154
213
|
}
|
|
155
|
-
/** @param {{ title?: any, aside?: any, tip?: string }} props @param {...any} kids */
|
|
156
|
-
function Group({ title, aside, tip }, ...kids) {
|
|
157
|
-
return h("section", { className: "fs-group" },
|
|
214
|
+
/** @param {{ title?: any, aside?: any, tip?: string, className?: string }} props @param {...any} kids */
|
|
215
|
+
function Group({ title, aside, tip, className }, ...kids) {
|
|
216
|
+
return h("section", { className: cx("fs-group", className) },
|
|
158
217
|
title ? h("h3", { className: "fs-group-head", title: tip }, h("span", null, title), aside ?? null) : null, ...kids);
|
|
159
218
|
}
|
|
160
|
-
const Input = (props) =>
|
|
161
|
-
|
|
219
|
+
const Input = (props) => {
|
|
220
|
+
if (!primitives?.Input) return h("input", { spellCheck: false, autoComplete: "off", ...props, className: cx("fs-input", props.className) });
|
|
221
|
+
const { className, style, ...input } = props;
|
|
222
|
+
return h("span", { className: cx("fs-native-input-wrap", className), style }, h(primitives.Input, { spellCheck: false, autoComplete: "off", ...input, className: "fs-native-input" }));
|
|
223
|
+
};
|
|
224
|
+
const Num = (props) => Input({ type: "number", inputMode: "numeric", ...props, className: cx("num", props.className) });
|
|
162
225
|
const Area = (props) => h("textarea", { rows: 2, spellCheck: false, ...props, className: "fs-input wide" });
|
|
163
226
|
|
|
164
227
|
function Secret({ value, onChange, onBlur, placeholder, label }) {
|
|
165
228
|
const [shown, setShown] = react.useState(false);
|
|
166
229
|
return h("div", { className: "fs-affix" },
|
|
167
|
-
|
|
230
|
+
Input({ type: shown ? "text" : "password", value, placeholder, onChange, onBlur, autoComplete: "off", spellCheck: false, "aria-label": label }),
|
|
168
231
|
IconButton("eye", shown ? "隐藏" : "显示", () => setShown(!shown)));
|
|
169
232
|
}
|
|
170
233
|
const Select = (props, ...options) => h("div", { className: "fs-affix select" },
|
|
@@ -183,7 +246,7 @@ window.__ModuleLoader__.load({
|
|
|
183
246
|
}
|
|
184
247
|
const split = (v, re = /[,,\s]+/) => String(v).split(re).map((x) => x.trim()).filter(Boolean);
|
|
185
248
|
|
|
186
|
-
const ui = { h, cx, Icon, Switch, Button, IconButton, Seg, Dot, State, Tag, Muted, Row, Group, Input, Num, Area, Secret, Select, Empty, Item, useForm, split };
|
|
249
|
+
const ui = { h, cx, Icon, Switch, Button, IconButton, Seg, Dot, State, Tag, Pill, Check, Muted, Row, Group, Input, Num, Area, Secret, Select, Empty, Item, useForm, split };
|
|
187
250
|
|
|
188
251
|
// ── 记忆检索 ──
|
|
189
252
|
function RetrievalPane({ data, refresh, notify }) {
|
|
@@ -328,18 +391,18 @@ window.__ModuleLoader__.load({
|
|
|
328
391
|
onChange: (e) => patch({ [name]: e.target.value === "" ? "" : Number(e.target.value) }), onBlur: () => save({ [name]: form[name] === "" ? fallback : form[name] }) });
|
|
329
392
|
|
|
330
393
|
return h(react.Fragment, null,
|
|
331
|
-
h("section", { className: "fs-
|
|
394
|
+
h("section", { className: "fs-life-overview" },
|
|
332
395
|
h("div", { className: "fs-now" },
|
|
333
|
-
Dot(sleeping ? "idle" : "done"),
|
|
334
|
-
|
|
335
|
-
today.dailyMoodTone ? Tag(today.dailyMoodTone) : null,
|
|
396
|
+
h("div", { className: "fs-now-activity" }, Dot(sleeping ? "idle" : "done"),
|
|
397
|
+
h("span", { className: "fs-now-title" }, slot.activity ?? "未排期")),
|
|
336
398
|
h("span", { className: "fs-now-meta" },
|
|
399
|
+
today.dailyMoodTone ? Tag(today.dailyMoodTone) : null,
|
|
337
400
|
clock.isAccelerated || scale > 1 ? Tag(`${scale}×`, "warning") : null,
|
|
338
401
|
h("span", null, fmtClock(clock.virtualNowMs)))),
|
|
339
402
|
schedule.length > 0 ? h("div", { className: "fs-schedule" },
|
|
340
403
|
schedule.map((s, i) => h("div", { className: "fs-slot", key: i, "aria-current": s.range === slot.range },
|
|
341
404
|
h("span", null, s.range), h("span", { title: s.activity }, s.activity)))) : null),
|
|
342
|
-
Group({},
|
|
405
|
+
Group({ className: "fs-life-settings" },
|
|
343
406
|
Row({ title: "主动开口" }, h(Switch, { label: "主动开口", checked: form.enabled, onChange: (v) => save({ enabled: v }) })),
|
|
344
407
|
form.enabled ? Row({ title: "每天最多" }, Tag(`今天 ${today.proactiveCount ?? 0}`), num("dailyMaxCount", "每天最多", 0, 50, 10), Muted("次")) : null,
|
|
345
408
|
form.enabled ? Row({ title: "时段" }, num("activeStart", "开始", 0, 23, 8), Muted("–"), num("activeEnd", "结束", 1, 24, 24), Muted("点")) : null,
|
|
@@ -347,7 +410,7 @@ window.__ModuleLoader__.load({
|
|
|
347
410
|
form.enabled ? Row({ title: "主人私聊" }, Input({ value: form.masterPrivateChannelId, placeholder: "private:主人 QQ", onChange: (e) => patch({ masterPrivateChannelId: e.target.value }), onBlur: () => save() })) : null,
|
|
348
411
|
form.enabled ? Row({ title: "群聊", stack: true },
|
|
349
412
|
...form.selfGroupChannelIds.map((cid) => Tag(cid, undefined, { key: cid },
|
|
350
|
-
|
|
413
|
+
IconButton("close", `移除 ${cid}`, () => save({ selfGroupChannelIds: form.selfGroupChannelIds.filter((x) => x !== cid) })))),
|
|
351
414
|
Input({ value: newGroup, placeholder: "group:群号", style: { flex: "0 1 160px", width: "160px" }, onChange: (e) => setNewGroup(e.target.value), onKeyDown: (e) => { if (e.key === "Enter") addGroup(); } }),
|
|
352
415
|
Button({ variant: "ghost", disabled: newGroup.trim() === "", onClick: addGroup }, "添加")) : null));
|
|
353
416
|
}
|
|
@@ -622,34 +685,51 @@ window.__ModuleLoader__.load({
|
|
|
622
685
|
}
|
|
623
686
|
|
|
624
687
|
// ── 面板根组件:页签 + 数据装载 + 轮询 ──
|
|
625
|
-
const TAB_KEYS = { retrieval: "索", persona: "人", life: "自", inspector: "忆", activity: "动", behavior: "习", models: "模", qq: "Q", bili: "B", douyin: "抖" };
|
|
688
|
+
const TAB_KEYS = { guide: "引", retrieval: "索", persona: "人", life: "自", inspector: "忆", activity: "动", behavior: "习", models: "模", qq: "Q", bili: "B", douyin: "抖", version: "版" };
|
|
626
689
|
const TABS = [["retrieval", "记忆检索"], ["persona", "人格"], ["life", "自走"], ["inspector", "记忆检视"], ["activity", "动态"], ["behavior", "习惯"], ["models", "模型"], ["qq", "QQ"], ["bili", "B站"], ["douyin", "抖音"]];
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
690
|
+
const NAV_GROUPS = [
|
|
691
|
+
{ id: "start", label: "开始", ids: ["guide"] },
|
|
692
|
+
{ id: "memory", label: "记忆", ids: ["retrieval", "inspector"] },
|
|
693
|
+
{ id: "character", label: "角色", ids: ["persona", "behavior"] },
|
|
694
|
+
{ id: "daily", label: "日常", ids: ["life", "activity"] },
|
|
695
|
+
{ id: "channels", label: "平台", ids: ["qq", "bili", "douyin"] },
|
|
696
|
+
{ id: "system", label: "系统", ids: ["models", "interface", "version"] }
|
|
697
|
+
];
|
|
698
|
+
|
|
699
|
+
// 服务端仍提供原始步骤与状态;短标题只调整展示,未知步骤保留服务端原文。
|
|
700
|
+
const ONBOARDING_LABELS = { persona: "选择人格", model: "配置模型", qq: "连接 QQ", master: "设置主人 QQ", retrieval: "配置向量检索", import: "导入聊天记录", voice: "配置语音" };
|
|
701
|
+
// 独立引导页:关闭自动显示后仍可从「开始」重新打开。
|
|
702
|
+
function Onboarding({ onGo, onDismiss, onHide, notify }) {
|
|
630
703
|
const [data, setData] = react.useState(null);
|
|
631
|
-
const
|
|
704
|
+
const [error, setError] = react.useState(null);
|
|
705
|
+
const load = react.useCallback(async () => { const r = await api("GET", "/onboarding/state"); if (r?.ok) { setData(r); setError(null); } else setError(r?.error ?? "引导状态暂不可用"); }, []);
|
|
632
706
|
react.useEffect(() => { load(); const t = setInterval(load, 10000); return () => clearInterval(t); }, [load]);
|
|
633
|
-
if (!data
|
|
707
|
+
if (!data) return error ? h("div", { className: "fs-notice", role: "alert" }, h("b", null, "引导加载失败"), h("span", null, error), Button({ variant: "secondary", onClick: load }, "重试")) : h("div", { className: "fs-empty" }, Dot("ongoing"));
|
|
634
708
|
async function dismiss() {
|
|
635
709
|
const r = await api("POST", "/onboarding/dismiss", { dismissed: true });
|
|
636
|
-
if (r?.ok) setData({ ...data, dismissed: true }); else notify(r?.error ?? "操作失败");
|
|
710
|
+
if (r?.ok) { setData({ ...data, dismissed: true }); onDismiss(); } else notify(r?.error ?? "操作失败");
|
|
637
711
|
}
|
|
638
|
-
const
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
x.done ?
|
|
712
|
+
const steps = data.steps ?? [];
|
|
713
|
+
const completed = steps.filter((x) => x.done).length;
|
|
714
|
+
const left = steps.filter((x) => !x.done && !x.optional).length;
|
|
715
|
+
return Group({ className: "fs-onboarding", title: `${left === 0 ? "基础就绪" : "开始"} · ${completed}/${steps.length}`, aside: h("span", { className: "fs-group-aside" }, Button({ variant: "ghost", onClick: onHide }, "隐藏开始"), data.dismissed ? null : IconButton("close", "以后不再自动显示", dismiss)) },
|
|
716
|
+
h("div", { className: "fs-list" }, steps.map((x) => {
|
|
717
|
+
const label = ONBOARDING_LABELS[x.id] ?? x.title;
|
|
718
|
+
return h("div", { className: "fs-item", key: x.id },
|
|
719
|
+
h("span", { className: "fs-onboarding-dot", role: "img", "aria-label": x.done ? "已完成" : x.optional ? "可选,未完成" : "未完成" }, Dot(x.done ? "done" : x.optional ? "idle" : "warning")),
|
|
720
|
+
h("span", { className: cx("fs-item-main", x.done && "fs-muted") }, label),
|
|
721
|
+
h("span", { className: "fs-item-meta" },
|
|
722
|
+
x.optional && !x.done ? Tag("可选", "outline") : null,
|
|
723
|
+
x.done ? null : x.tab ? Button({ variant: "ghost", "aria-label": `设置${label}`, onClick: () => onGo(x.tab) }, "去设置") : null));
|
|
724
|
+
})));
|
|
646
725
|
}
|
|
647
726
|
// 版本与更新:显示当前版本,自动 / 手动去 npm(不通再 GitHub)看有没有新版;只提示,不替你装
|
|
648
|
-
function UpdateBar({ notify,
|
|
727
|
+
function UpdateBar({ notify, onUpdateState }) {
|
|
649
728
|
const [data, setData] = react.useState(null);
|
|
650
729
|
const [busy, setBusy] = react.useState(false);
|
|
651
730
|
const load = react.useCallback(async () => { const r = await api("GET", "/update/state"); if (r?.ok) setData(r); }, []);
|
|
652
731
|
react.useEffect(() => { load(); const t = setInterval(load, 60000); return () => clearInterval(t); }, [load]);
|
|
732
|
+
react.useEffect(() => { if (data && onUpdateState) onUpdateState(data.hasUpdate === true); }, [data, onUpdateState]);
|
|
653
733
|
if (!data) return null;
|
|
654
734
|
async function check() {
|
|
655
735
|
setBusy(true);
|
|
@@ -665,9 +745,6 @@ window.__ModuleLoader__.load({
|
|
|
665
745
|
async function copy() {
|
|
666
746
|
try { await navigator.clipboard.writeText(data.command); notify("已复制更新命令"); } catch { notify("复制失败,手动选中复制"); }
|
|
667
747
|
}
|
|
668
|
-
if (compact) return h("button", { type: "button", className: "fs-ver", onClick: onOpen, title: data.hasUpdate ? `v${data.latest}` : undefined, "aria-label": "版本" },
|
|
669
|
-
h("span", { className: "fs-dot", "data-state": data.hasUpdate ? "warning" : data.latest ? "done" : "idle", "aria-hidden": true }),
|
|
670
|
-
h("span", { className: "fs-tab-label num" }, `v${data.current}`));
|
|
671
748
|
const when = data.checkedAt ? new Date(data.checkedAt).toLocaleString("zh-CN", { hour12: false }) : "";
|
|
672
749
|
const status = data.hasUpdate ? Tag(`有新版本 v${data.latest}`, "warning")
|
|
673
750
|
: data.latest ? Tag("已是最新", "success")
|
|
@@ -698,16 +775,33 @@ window.__ModuleLoader__.load({
|
|
|
698
775
|
}
|
|
699
776
|
}
|
|
700
777
|
|
|
701
|
-
function FishSoulSection() {
|
|
778
|
+
function FishSoulSection({ standalone = false } = {}) {
|
|
702
779
|
const css = useCss();
|
|
780
|
+
// 界面偏好只保存在当前宿主浏览器,不修改服务端引导进度。
|
|
781
|
+
const guideVisibilityKey = "fish-soul:guide-visible";
|
|
782
|
+
const readGuideVisible = () => { try { return window.localStorage.getItem(guideVisibilityKey) !== "false"; } catch { return true; } };
|
|
783
|
+
const [guideVisible, setGuideVisible] = react.useState(readGuideVisible);
|
|
703
784
|
const [tab, setTab] = react.useState("retrieval");
|
|
785
|
+
const hasInteracted = react.useRef(false);
|
|
786
|
+
const goTab = (next) => { hasInteracted.current = true; setTab(next); };
|
|
704
787
|
const [retrieval, setRetrieval] = react.useState(null);
|
|
705
788
|
const [life, setLife] = react.useState(null);
|
|
706
789
|
const [channels, setChannels] = react.useState(null);
|
|
707
790
|
const [toast, setToast] = react.useState(null);
|
|
708
|
-
const [
|
|
709
|
-
const [detail, setDetail] = react.useState(null);
|
|
791
|
+
const [updateAvailable, setUpdateAvailable] = react.useState(false);
|
|
710
792
|
const notify = react.useCallback((text) => setToast({ key: Date.now(), text }), []);
|
|
793
|
+
const setGuideVisibility = (visible) => {
|
|
794
|
+
try { window.localStorage.setItem(guideVisibilityKey, String(visible)); }
|
|
795
|
+
catch { notify("无法保存界面设置"); return; }
|
|
796
|
+
setGuideVisible(visible);
|
|
797
|
+
if (!visible) setTab((current) => current === "guide" ? "retrieval" : current);
|
|
798
|
+
window.dispatchEvent(new Event("fish-soul:guide-visibility"));
|
|
799
|
+
};
|
|
800
|
+
react.useEffect(() => {
|
|
801
|
+
const sync = () => { const visible = readGuideVisible(); setGuideVisible(visible); if (!visible) setTab((current) => current === "guide" ? "retrieval" : current); };
|
|
802
|
+
window.addEventListener("fish-soul:guide-visibility", sync); window.addEventListener("storage", sync);
|
|
803
|
+
return () => { window.removeEventListener("fish-soul:guide-visibility", sync); window.removeEventListener("storage", sync); };
|
|
804
|
+
}, []);
|
|
711
805
|
|
|
712
806
|
react.useEffect(() => {
|
|
713
807
|
if (toast === null) return undefined;
|
|
@@ -721,6 +815,14 @@ window.__ModuleLoader__.load({
|
|
|
721
815
|
const loadChannels = react.useCallback(load("/channels/state", setChannels), []);
|
|
722
816
|
|
|
723
817
|
react.useEffect(() => { loadRetrieval(); loadLife(); loadChannels(); }, []);
|
|
818
|
+
// 仅在初次加载且用户尚未操作时自动进入引导,慢请求不能打断用户。
|
|
819
|
+
react.useEffect(() => {
|
|
820
|
+
let active = true;
|
|
821
|
+
api("GET", "/onboarding/state").then((r) => {
|
|
822
|
+
if (active && !hasInteracted.current && readGuideVisible() && r?.ok && r.dismissed === false && Array.isArray(r.steps) && r.steps.some((step) => !step.done)) setTab("guide");
|
|
823
|
+
});
|
|
824
|
+
return () => { active = false; };
|
|
825
|
+
}, []);
|
|
724
826
|
// 检索页 2s 刷新(重建进度);QQ / B站 / 抖音 3s 刷新
|
|
725
827
|
react.useEffect(() => {
|
|
726
828
|
const fn = tab === "retrieval" ? loadRetrieval : tab === "qq" || tab === "bili" || tab === "douyin" ? loadChannels : null;
|
|
@@ -736,8 +838,21 @@ window.__ModuleLoader__.load({
|
|
|
736
838
|
return () => clearInterval(timer);
|
|
737
839
|
}, [tab]);
|
|
738
840
|
|
|
841
|
+
// 导航红点使用同一只读状态接口;版本页自身检查后的结果即时同步。
|
|
842
|
+
react.useEffect(() => {
|
|
843
|
+
if (tab === "version") return undefined;
|
|
844
|
+
let active = true;
|
|
845
|
+
const loadUpdate = async () => { const r = await api("GET", "/update/state"); if (active && r?.ok) setUpdateAvailable(r.hasUpdate === true); };
|
|
846
|
+
loadUpdate();
|
|
847
|
+
const timer = setInterval(loadUpdate, 60000);
|
|
848
|
+
return () => { active = false; clearInterval(timer); };
|
|
849
|
+
}, [tab]);
|
|
850
|
+
|
|
739
851
|
if (css === null) return null;
|
|
740
|
-
const pane = tab === "
|
|
852
|
+
const pane = tab === "guide" ? h(Onboarding, { onGo: goTab, onDismiss: () => goTab("retrieval"), onHide: () => setGuideVisibility(false), notify })
|
|
853
|
+
: tab === "interface" ? Row({ title: "显示开始" }, h(Switch, { label: "显示开始", checked: guideVisible, onChange: setGuideVisibility }))
|
|
854
|
+
: tab === "version" ? h(Boundary, { name: "版本" }, h(UpdateBar, { notify, onUpdateState: setUpdateAvailable }))
|
|
855
|
+
: tab === "retrieval" ? h(RetrievalPane, { data: retrieval, refresh: loadRetrieval, notify })
|
|
741
856
|
: tab === "life" ? h(LifePane, { data: life, refresh: loadLife, notify })
|
|
742
857
|
: tab === "inspector" ? h(ChunkTab, { file: "client.soul.js", pick: (m) => m.InspectorPane, failText: "记忆检视加载失败", notify })
|
|
743
858
|
: tab === "activity" ? h(ChunkTab, { file: "client.soul.js", pick: (m) => m.ActivityPane, failText: "动态加载失败", notify })
|
|
@@ -748,30 +863,23 @@ window.__ModuleLoader__.load({
|
|
|
748
863
|
: tab === "bili" ? h(BiliPane, { data: channels, refresh: loadChannels, notify })
|
|
749
864
|
: h(ChunkTab, { file: "client.douyin.js", pick: (m) => m.DouyinPane, failText: "抖音页加载失败", data: channels, refresh: loadChannels, notify });
|
|
750
865
|
|
|
751
|
-
//
|
|
752
|
-
const
|
|
753
|
-
|
|
866
|
+
// 工作台:主页与设置页都把版本更新作为系统分组下的独立页签。
|
|
867
|
+
const tabs = [...(guideVisible ? [["guide", "使用引导"]] : []), ...TABS, ["interface", "界面"], ["version", "版本更新"]];
|
|
868
|
+
const label = tabs.find(([v]) => v === tab)?.[1] ?? "";
|
|
869
|
+
return h("div", { className: "fs-host" }, h("div", { className: cx("fs", standalone ? "fs-standalone" : "fs-settings"), onPointerDown: () => { hasInteracted.current = true; }, onKeyDown: () => { hasInteracted.current = true; } },
|
|
754
870
|
css === "" ? null : h("style", { "data-plugin-css": `${PLUGIN_ID}/panel.css` }, css),
|
|
755
|
-
h("
|
|
756
|
-
h(
|
|
757
|
-
h("span", { className: "fs-tab-label fs-brand" }, "鱼"),
|
|
758
|
-
IconButton("sidebar", navOpen ? "收起侧栏" : "展开侧栏", () => setNavOpen(!navOpen), { "aria-expanded": navOpen })),
|
|
759
|
-
h(Tabs, { items: TABS, value: tab, onChange: setTab }),
|
|
760
|
-
h("div", { className: "fs-side-foot" }, h(Boundary, { name: "版本" }, h(UpdateBar, { notify, compact: true, onOpen: () => setDetail(detail === "version" ? null : "version") }))))),
|
|
871
|
+
h("div", { className: "fs-side" }, h("div", { className: "fs-side-inner" },
|
|
872
|
+
h(GroupedTabs, { items: tabs, value: tab, onChange: goTab, hasUpdate: updateAvailable }))),
|
|
761
873
|
h("main", { className: "fs-main" },
|
|
762
874
|
h("header", { className: "fs-main-head" }, h("h2", { className: "fs-main-title" }, label)),
|
|
763
|
-
h(Boundary, { name: "引导" }, h(Onboarding, { onGo: setTab, notify })),
|
|
764
875
|
h("div", { className: "fs-pane", role: "tabpanel", id: "fs-tabpanel", "aria-labelledby": `fs-tab-${tab}`, key: tab }, h(Boundary, { name: "这一页" }, pane))),
|
|
765
|
-
detail === "version" ? h("aside", { className: "fs-detail", "aria-label": "版本" }, h("div", { className: "fs-detail-inner" },
|
|
766
|
-
h("div", { className: "fs-detail-head" }, h("span", null, "版本"), IconButton("close", "关闭", () => setDetail(null))),
|
|
767
|
-
h(Boundary, { name: "版本" }, h(UpdateBar, { notify })))) : null,
|
|
768
876
|
toast === null ? null : h("div", { className: "fs-toast", role: "status", key: toast.key }, toast.text)));
|
|
769
877
|
}
|
|
770
878
|
|
|
771
879
|
// 同一组件可在设置弹窗或宿主主面板内渲染;只改变承载容器。
|
|
772
880
|
function FishSoulPage() {
|
|
773
881
|
return h("div", { className: "fs-page-shell" },
|
|
774
|
-
h("div", { className: "fs-page-scroll" }, h(FishSoulSection)));
|
|
882
|
+
h("div", { className: "fs-page-scroll" }, h(FishSoulSection, { standalone: true })));
|
|
775
883
|
}
|
|
776
884
|
|
|
777
885
|
function FishSoulPanelIcon({ size = 16 } = {}) {
|
package/lib/client.models.js
CHANGED
|
@@ -44,7 +44,7 @@ window.__ModuleLoader__.load({
|
|
|
44
44
|
}
|
|
45
45
|
return opts;
|
|
46
46
|
};
|
|
47
|
-
return Group({ title: "按功能选模型", aside: host ? Muted(`DSH 默认:${providerName(host.provider)} · ${host.model}`) : null },
|
|
47
|
+
return Group({ title: "按功能选模型", className: "fs-model-routes", 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
50
|
...(data.features ?? []).map((f) => Row({ title: f.label, key: f.id, meta: f.resolved ? (f.selection ? undefined : `现在用 ${f.resolved.model}`) : "没有可用的模型" },
|
package/lib/client.soul.js
CHANGED
|
@@ -61,7 +61,7 @@ window.__ModuleLoader__.load({
|
|
|
61
61
|
h("span", { className: "fs-item-sub" }, origin));
|
|
62
62
|
}
|
|
63
63
|
return h("div", { className: "fs-item" },
|
|
64
|
-
selectable ? h(
|
|
64
|
+
selectable ? h(ui.Check, { checked: selected === true, label: `选中 #${m.id}`, hideLabel: true, onChange: (v) => onSelect(m.id, v) }) : null,
|
|
65
65
|
Tag(KIND[m.kind] ?? m.kind),
|
|
66
66
|
h("span", { className: "fs-item-main", title: `${m.content}\n${origin}` }, m.content),
|
|
67
67
|
h("span", { className: "fs-item-meta" },
|
|
@@ -142,7 +142,7 @@ window.__ModuleLoader__.load({
|
|
|
142
142
|
h("div", { className: "fs-list" }, data.outdated.map((m) => Item({
|
|
143
143
|
key: m.id, lead: Tag(m.status === "outdated" ? "过时" : m.status === "superseded" ? "改掉" : "删了", "outline"),
|
|
144
144
|
text: m.content, title: `${m.content}\n${m.evidence || m.source}`,
|
|
145
|
-
meta: [
|
|
145
|
+
meta: [Button({ key: "r", variant: "ghost", onClick: () => restore(m.id) }, "恢复")]
|
|
146
146
|
})))));
|
|
147
147
|
}
|
|
148
148
|
|
|
@@ -267,8 +267,7 @@ window.__ModuleLoader__.load({
|
|
|
267
267
|
Row({ title: `${FORMAT[pv.format] ?? pv.format} · ${pv.chatCount} 个聊天 · ${pv.messageCount} 条消息${pv.memoryCount ? ` · 现成记忆 ${pv.memoryCount} 条` : ""}` },
|
|
268
268
|
...(pv.warnings ?? []).map((w, i) => Tag(w, "warning", { key: i }))),
|
|
269
269
|
pv.speakers.length > 0 ? h("div", { className: "fs-list" }, pv.speakers.slice(0, 40).map((s) => h("div", { className: "fs-item", key: s.key },
|
|
270
|
-
h(
|
|
271
|
-
h("input", { type: "checkbox", checked: opts.self[s.key] === true, onChange: (e) => setOpts({ ...opts, self: { ...opts.self, [s.key]: e.target.checked } }) }), "是她"),
|
|
270
|
+
h(ui.Check, { checked: opts.self[s.key] === true, label: "是她", title: "勾上 = 这是她自己说的话", onChange: (v) => setOpts({ ...opts, self: { ...opts.self, [s.key]: v } }) }),
|
|
272
271
|
h("span", { className: "fs-item-main", title: s.chats.join("、") }, s.name),
|
|
273
272
|
h("span", { className: "fs-item-meta" },
|
|
274
273
|
s.existing ? Tag(`已认识 · ${stageLabel(s.existing.relationStage)}`, "success") : null,
|
|
@@ -360,7 +359,7 @@ window.__ModuleLoader__.load({
|
|
|
360
359
|
const persons = data.persons ?? [];
|
|
361
360
|
const memories = data.memories ?? [];
|
|
362
361
|
const pendings = data.pendings ?? [];
|
|
363
|
-
const pill = (ownerId, label, title) =>
|
|
362
|
+
const pill = (ownerId, label, title) => ui.Pill({ key: ownerId, active: owner === ownerId, className: "fs-pill", "aria-pressed": owner === ownerId, title, onClick: () => setOwner(ownerId), children: label });
|
|
364
363
|
const onSelect = (id, on) => setSel((prev) => { const next = new Set(prev); if (on) next.add(id); else next.delete(id); return next; });
|
|
365
364
|
const filter = { ownerId: owner, q: qApplied, source };
|
|
366
365
|
const narrowed = owner !== "all" || qApplied !== "" || source !== "";
|
package/lib/panel.css
CHANGED
|
@@ -47,13 +47,10 @@ html[data-ds-theme-source] body:not([data-ds-dark-theme]) :is(.fs,.fs-page-shell
|
|
|
47
47
|
.fs-tab[aria-selected=true]{border-color:var(--fs-line);background:var(--bg-control);color:var(--fg-primary);font-weight:500}
|
|
48
48
|
.fs-tab[aria-selected=true] .fs-tab-key{border-color:var(--fs-line-strong);color:var(--fg-primary)}
|
|
49
49
|
@media (hover:hover){.fs-tab:hover{background:var(--bg-hover);color:var(--fg-primary)}}
|
|
50
|
-
.fs-side-foot{margin-top:auto;padding-top:8px;border-top:1px solid var(--fs-line)}
|
|
51
|
-
.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(--fg-tertiary);cursor:pointer}
|
|
52
|
-
@media (hover:hover){.fs-ver:hover{background:var(--bg-hover);color:var(--fg-secondary)}}
|
|
53
50
|
.fs.nav-collapsed .fs-side{padding:8px 6px}
|
|
54
51
|
.fs.nav-collapsed .fs-side-head{justify-content:center;padding:0}
|
|
55
52
|
.fs.nav-collapsed .fs-tab-label{display:none}
|
|
56
|
-
.fs.nav-collapsed .fs-tab
|
|
53
|
+
.fs.nav-collapsed .fs-tab{justify-content:center;padding:0}
|
|
57
54
|
|
|
58
55
|
/* 主任务流 */
|
|
59
56
|
.fs-main{display:flex;flex-direction:column;gap:20px;min-width:0;width:100%;max-width:760px;justify-self:center;padding:14px 24px 24px}
|
|
@@ -88,6 +85,13 @@ html[data-ds-theme-source] body:not([data-ds-dark-theme]) :is(.fs,.fs-page-shell
|
|
|
88
85
|
.fs-row.stack .fs-ctl{justify-content:flex-start}
|
|
89
86
|
.fs-unit{font-family:var(--fs-mono);font-variant-numeric:tabular-nums;font-size:12px;line-height:18px;color:var(--fg-secondary);white-space:normal;overflow-wrap:anywhere}
|
|
90
87
|
|
|
88
|
+
/* 首次引导:沿用宿主的细边框与低对比背景,窄屏标题允许换行。 */
|
|
89
|
+
.fs-onboarding{padding:10px 12px;border:1px solid var(--fs-line);border-radius:8px;background:var(--bg-control)}
|
|
90
|
+
.fs-onboarding .fs-group-head{min-height:28px;padding-bottom:5px}
|
|
91
|
+
.fs-onboarding .fs-item{min-height:38px;gap:8px}
|
|
92
|
+
.fs-onboarding .fs-item-main{white-space:normal;overflow-wrap:anywhere}
|
|
93
|
+
.fs-onboarding .fs-item-meta{gap:4px}
|
|
94
|
+
|
|
91
95
|
/* 输入框:settings-form 的 input */
|
|
92
96
|
.fs-input{flex:0 1 240px;width:240px;min-width:0;height:28px;padding:0 10px;border:1px solid var(--fs-line-strong);border-radius:6px;background:var(--bg-control);font-size:13px;line-height:20px;color:var(--fg-primary)}
|
|
93
97
|
.fs-input::placeholder{color:var(--fg-tertiary);opacity:.7}
|
|
@@ -111,8 +115,16 @@ select.fs-input{appearance:none;-webkit-appearance:none;padding-right:32px;curso
|
|
|
111
115
|
.fs-switch[aria-checked=false] .fs-switch-thumb{background:var(--fg-secondary)}
|
|
112
116
|
.fs-switch[aria-checked=true] .fs-switch-thumb{transform:translateX(16px)}
|
|
113
117
|
|
|
118
|
+
/* 官方控件只补充插件布局与危险态,视觉主体来自宿主。 */
|
|
119
|
+
.fs-native-btn{flex:none}
|
|
120
|
+
.fs-native-btn.fs-native-danger{color:var(--status-error)}
|
|
121
|
+
/* 白字主按钮使用稍深的蓝,确保浅色宿主中仍有足够对比度。 */
|
|
122
|
+
.fs .fs-native-btn.fs-native-primary:not(:disabled){background:#4663f3;color:#fff}
|
|
123
|
+
@media (hover:hover){.fs .fs-native-btn.fs-native-primary:hover:not(:disabled){background:#3b57e7}}
|
|
124
|
+
.fs-native-switch[aria-checked=true]{background:var(--accent)}
|
|
125
|
+
|
|
114
126
|
/* Button(sm):outline / primary / ghost / danger */
|
|
115
|
-
.fs-btn{display:inline-flex;flex:none;align-items:center;justify-content:center;gap:4px;height:
|
|
127
|
+
.fs-btn{display:inline-flex;flex:none;align-items:center;justify-content:center;gap:4px;height:28px;padding:0 10px;border:0;border-radius:6px;background:transparent;box-shadow:inset 0 0 0 1px var(--fs-line-strong);color:var(--fg-primary);font-size:12px;line-height:18px;white-space:nowrap;cursor:pointer}
|
|
116
128
|
.fs-btn.primary{box-shadow:none;background:var(--accent);color:var(--fg-on-accent)}
|
|
117
129
|
.fs-btn.ghost{box-shadow:none;color:var(--fg-secondary)}
|
|
118
130
|
.fs-btn.danger{color:var(--status-error)}
|
|
@@ -278,7 +290,7 @@ summary.fs-step-head>svg{flex:none;color:var(--fg-tertiary);transform:rotate(-90
|
|
|
278
290
|
|
|
279
291
|
/* 窄容器:侧栏变顶部横条,详情面板落到下方 */
|
|
280
292
|
@container (max-width:720px){
|
|
281
|
-
.fs,.fs.has-detail,.fs.nav-collapsed{grid-template-columns:minmax(0,1fr)}
|
|
293
|
+
.fs,.fs.has-detail,.fs.nav-collapsed{grid-template-columns:minmax(0,1fr);grid-template-rows:auto minmax(0,1fr)}
|
|
282
294
|
.fs-side{position:sticky;top:0;z-index:4;padding:6px;border-right:0;border-bottom:1px solid var(--fs-line);border-radius:12px 12px 0 0}
|
|
283
295
|
.fs-side-inner{position:static;flex-direction:row;align-items:center;gap:6px;min-height:0}
|
|
284
296
|
.fs-main-head{position:static;height:28px;margin-top:0;padding-top:0;padding-bottom:10px}
|
|
@@ -289,8 +301,6 @@ summary.fs-step-head>svg{flex:none;color:var(--fg-tertiary);transform:rotate(-90
|
|
|
289
301
|
.fs-tab-key{display:none}
|
|
290
302
|
.fs.nav-collapsed .fs-tab-label{display:inline}
|
|
291
303
|
.fs.nav-collapsed .fs-tab{padding:0 8px}
|
|
292
|
-
.fs-side-foot{flex:none;margin:0;padding:0;border:0}
|
|
293
|
-
.fs-ver .fs-tab-label{display:none}
|
|
294
304
|
.fs-main{padding:12px 14px 18px}
|
|
295
305
|
.fs-group-head{height:auto;min-height:24px;flex-wrap:wrap;gap:2px 8px}
|
|
296
306
|
.fs-group-head>:first-child{flex:none}
|
|
@@ -302,3 +312,103 @@ summary.fs-step-head>svg{flex:none;color:var(--fg-tertiary);transform:rotate(-90
|
|
|
302
312
|
.fs-dock .fs-input{flex:1 1 auto;width:auto}
|
|
303
313
|
}
|
|
304
314
|
@media (prefers-reduced-motion:reduce){summary.fs-step-head>svg{transition:none}}
|
|
315
|
+
|
|
316
|
+
/* 页面层次:把长表单分成清晰、安静的工作区。 */
|
|
317
|
+
.fs{--fs-side:200px}
|
|
318
|
+
.fs-main{max-width:800px;padding-right:32px;padding-left:32px;gap:24px}
|
|
319
|
+
.fs-pane{gap:24px}
|
|
320
|
+
.fs-settings .fs-main-head{margin-top:0;padding-top:0;height:34px}
|
|
321
|
+
.fs-life-overview,.fs-life-settings,.fs-model-routes{min-width:0;padding:16px;border:1px solid var(--fs-line);border-radius:10px;background:var(--bg-surface)}
|
|
322
|
+
.fs-life-overview{background:var(--bg-control)}
|
|
323
|
+
.fs-now{display:flex;align-items:flex-start;gap:12px;padding:0 0 16px}
|
|
324
|
+
.fs-now-activity{display:flex;align-items:flex-start;gap:8px;min-width:0;flex:1 1 260px}
|
|
325
|
+
.fs-now-activity .fs-dot{margin-top:5px}
|
|
326
|
+
.fs-now-title{min-width:0;font-size:14px;line-height:21px;font-weight:500;overflow-wrap:anywhere}
|
|
327
|
+
.fs-now-meta{flex:none;margin-left:auto;display:flex;align-items:center;gap:8px}
|
|
328
|
+
.fs-schedule{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:6px;border:0;border-radius:0;overflow:visible}
|
|
329
|
+
.fs-slot{gap:4px;min-height:66px;padding:9px 10px;border:1px solid var(--fs-line);border-radius:6px;background:var(--bg-surface)}
|
|
330
|
+
.fs-slot:first-child{border-left:1px solid var(--fs-line)}
|
|
331
|
+
.fs-slot>span{white-space:normal;overflow-wrap:anywhere}
|
|
332
|
+
.fs-slot>span:last-child{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden}
|
|
333
|
+
.fs-slot[aria-current=true]{background:var(--bg-sidebar);border-color:var(--fs-line-strong);box-shadow:none}
|
|
334
|
+
.fs-model-routes .fs-group-head{height:auto;min-height:32px;gap:8px 16px;flex-wrap:wrap}
|
|
335
|
+
.fs-model-routes .fs-group-head>:nth-child(2){min-width:0;max-width:100%;font-weight:400;text-align:right;overflow-wrap:anywhere}
|
|
336
|
+
.fs-model-routes .fs-row{min-height:42px}
|
|
337
|
+
|
|
338
|
+
@container (max-width:720px){
|
|
339
|
+
.fs-main{padding:12px 14px 20px;gap:20px}
|
|
340
|
+
.fs-life-overview,.fs-life-settings,.fs-model-routes{padding:12px}
|
|
341
|
+
.fs-now-meta{margin-left:0}
|
|
342
|
+
}
|
|
343
|
+
/* 小高度窗口可滚动侧栏导航。 */
|
|
344
|
+
.fs-side-inner{min-height:min(464px,calc(100vh - 48px));max-height:calc(100vh - 48px);overflow-y:auto;scrollbar-width:thin}
|
|
345
|
+
@container (max-width:720px){.fs-side-inner{min-height:0;max-height:none;overflow:visible}}
|
|
346
|
+
|
|
347
|
+
/* 官方原语适配:只补齐工作台布局,不接管组件事件。 */
|
|
348
|
+
.fs-native-input-wrap{display:inline-flex;flex:0 1 240px;width:240px;min-width:0}
|
|
349
|
+
.fs-native-input-wrap>.fs-native-input{width:100%;min-width:0;height:28px;padding:0 10px;border:1px solid var(--fs-line-strong);border-radius:6px;background:var(--bg-control)}
|
|
350
|
+
.fs-native-input-wrap>.fs-native-input:focus-within{border-color:var(--accent)}
|
|
351
|
+
.fs-native-input-wrap>.fs-native-input>input{width:100%;min-width:0;font-size:13px;line-height:20px;color:var(--fg-primary)}
|
|
352
|
+
.fs-native-input-wrap>.fs-native-input>input::placeholder{color:var(--fg-tertiary);opacity:.7}
|
|
353
|
+
.fs-native-input-wrap.num{flex:none;width:64px}
|
|
354
|
+
.fs-native-input-wrap.num>.fs-native-input{padding:0 8px}
|
|
355
|
+
.fs-native-input-wrap.num input,.fs-native-input-wrap input[inputmode=numeric]{font-family:var(--fs-mono);font-variant-numeric:tabular-nums}
|
|
356
|
+
.fs-native-input-wrap.num input{text-align:center}
|
|
357
|
+
.fs-native-input-wrap.wide,.fs-row.stack .fs-native-input-wrap:not(.num){flex:1 1 auto;width:100%}
|
|
358
|
+
.fs-affix .fs-native-input-wrap{flex:1 1 auto;width:100%}
|
|
359
|
+
.fs-affix .fs-native-input-wrap>.fs-native-input{padding-right:36px}
|
|
360
|
+
.fs-dock .fs-native-input-wrap{flex:1 1 auto;width:auto}
|
|
361
|
+
.fs-dock .fs-native-input-wrap>.fs-native-input{border-color:transparent;background:transparent}
|
|
362
|
+
.fs-native-icon{width:24px;height:24px;padding:0}
|
|
363
|
+
.fs-native-tag-wrap{display:inline-flex;min-width:0}
|
|
364
|
+
.fs-native-tag{gap:4px;font-family:var(--fs-mono);font-variant-numeric:tabular-nums}
|
|
365
|
+
.fs-native-tag>button{display:inline-flex;padding:0;border:0;background:none;color:inherit;cursor:pointer}
|
|
366
|
+
.fs-native-check.label-hidden>span{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
|
|
367
|
+
@container (max-width:720px){.fs-native-input-wrap:not(.num){flex:1 1 100%;width:100%}.fs-dock .fs-native-input-wrap{flex:1 1 auto;width:auto}}
|
|
368
|
+
|
|
369
|
+
.fs-onboarding-dot{display:inline-flex;flex:none}
|
|
370
|
+
.fs-native-tag .fs-icon-btn,.fs-tag .fs-icon-btn{width:16px;height:16px;min-width:16px;padding:0}
|
|
371
|
+
|
|
372
|
+
/* 主页面:单层横向导航,与宿主侧栏分工;设置页布局保持不变。 */
|
|
373
|
+
.fs.fs-standalone,.fs.fs-standalone.has-detail{grid-template-columns:minmax(0,1fr);grid-template-rows:auto minmax(0,1fr)}
|
|
374
|
+
.fs-standalone .fs-side{position:sticky;top:0;z-index:4;align-self:start;min-width:0;padding:7px 12px;border-right:0;border-bottom:1px solid var(--fs-line);border-radius:12px 12px 0 0;background:var(--bg-surface)}
|
|
375
|
+
.fs-standalone .fs-side-inner{position:static;display:flex;flex-direction:row;align-items:center;gap:14px;min-height:0;max-height:none;overflow:visible}
|
|
376
|
+
.fs-standalone .fs-side-head{height:32px;margin:0;padding:0 4px;flex:none}
|
|
377
|
+
.fs-standalone .fs-nav{flex:1 1 auto;min-width:0;display:flex;flex-direction:row;flex-wrap:nowrap;gap:3px;overflow-x:auto;overflow-y:hidden;scrollbar-width:none}
|
|
378
|
+
.fs-standalone .fs-nav::-webkit-scrollbar{display:none}
|
|
379
|
+
.fs-standalone .fs-tab{flex:none;height:32px;padding:0 10px;gap:6px}
|
|
380
|
+
.fs-standalone .fs-tab-key{display:none}
|
|
381
|
+
.fs-standalone .fs-main-head{position:static;height:34px;margin-top:0;padding-top:0;padding-bottom:10px}
|
|
382
|
+
.fs-update-dot{flex:none;width:6px;height:6px;border-radius:50%;background:var(--status-warn)}
|
|
383
|
+
@container (max-width:720px){
|
|
384
|
+
.fs-standalone .fs-side{padding:6px}
|
|
385
|
+
.fs-standalone .fs-side-inner{gap:6px}
|
|
386
|
+
.fs-standalone .fs-side-head{padding:0 2px}
|
|
387
|
+
.fs-standalone .fs-tab{padding:0 8px}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/* 两级导航:分类和对应页面一起吸顶,内容独立滚动。 */
|
|
391
|
+
.fs.fs-standalone,.fs.fs-settings,.fs.fs-standalone.has-detail,.fs.fs-settings.has-detail{grid-template-columns:minmax(0,1fr);grid-template-rows:auto minmax(0,1fr)}
|
|
392
|
+
.fs.fs-standalone .fs-side,.fs.fs-settings .fs-side{position:sticky;top:0;z-index:4;align-self:start;min-width:0;padding:0 16px;border-right:0;border-bottom:1px solid var(--fs-line);border-radius:12px 12px 0 0;background:var(--bg-surface)}
|
|
393
|
+
.fs.fs-standalone .fs-side-inner,.fs.fs-settings .fs-side-inner{position:static;display:block;min-height:0;max-height:none;overflow:visible}
|
|
394
|
+
.fs-group-row{display:flex;align-items:center;gap:18px;min-width:0;height:48px;border-bottom:1px solid var(--fs-line)}
|
|
395
|
+
.fs-group-row .fs-brand{flex:none;padding:0 2px;font-size:14px}
|
|
396
|
+
.fs-group-nav{display:flex;align-self:stretch;align-items:stretch;gap:8px;flex:1 1 auto;min-width:0;overflow-x:auto;overflow-y:hidden;scrollbar-width:none}
|
|
397
|
+
.fs-group-nav::-webkit-scrollbar{display:none}
|
|
398
|
+
.fs-group-tab{display:inline-flex;position:relative;flex:none;align-items:center;gap:6px;height:100%;padding:0 8px;border:0;background:transparent;color:var(--fg-secondary);font:inherit;white-space:nowrap;cursor:pointer}
|
|
399
|
+
.fs-group-tab[aria-pressed=true]{color:var(--fg-primary);font-weight:600}
|
|
400
|
+
.fs-group-tab[aria-pressed=true]::after{content:"";position:absolute;right:8px;bottom:0;left:8px;height:2px;border-radius:2px;background:var(--fg-primary)}
|
|
401
|
+
@media (hover:hover){.fs-group-tab:hover{color:var(--fg-primary)}}
|
|
402
|
+
.fs-subnav-row{display:flex;align-items:center;min-width:0;min-height:44px}
|
|
403
|
+
.fs.fs-standalone .fs-nav,.fs.fs-settings .fs-nav{flex:1 1 auto;min-width:0;display:flex;flex-direction:row;flex-wrap:nowrap;gap:6px;overflow-x:auto;overflow-y:hidden;scrollbar-width:none}
|
|
404
|
+
.fs.fs-standalone .fs-nav::-webkit-scrollbar,.fs.fs-settings .fs-nav::-webkit-scrollbar{display:none}
|
|
405
|
+
.fs.fs-standalone .fs-tab,.fs.fs-settings .fs-tab{flex:none;height:28px;padding:0 10px;gap:6px;border-radius:999px}
|
|
406
|
+
.fs.fs-standalone .fs-tab-key,.fs.fs-settings .fs-tab-key{display:none}
|
|
407
|
+
.fs.fs-standalone .fs-main-head,.fs.fs-settings .fs-main-head{position:static;height:34px;margin-top:0;padding-top:0;padding-bottom:10px}
|
|
408
|
+
@container (max-width:720px){
|
|
409
|
+
.fs.fs-standalone .fs-side,.fs.fs-settings .fs-side{padding:0 12px}
|
|
410
|
+
.fs-group-row{height:44px;gap:8px}
|
|
411
|
+
.fs-group-nav{gap:2px}
|
|
412
|
+
.fs-group-tab{padding:0 8px}
|
|
413
|
+
.fs-subnav-row{min-height:42px}
|
|
414
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-fish-soul",
|
|
3
|
-
"version": "0.15.
|
|
4
|
-
"description": "鱼 · 人格、按人记忆(可纠正、有印象、能导入旧聊天记录)、日程自走与主动开口,直连 SnowLuma 收发 QQ(群里会自己插话),B站 / 抖音小号私信与自己刷视频;按功能选模型并统计用量花费,能听会说 QQ 语音、收藏表情包、学群里的梗,可选上网查;心情影响说话、去 AI 味、每个群的设定、按聊天选模型、每晚写日记(DSH 插件,单角色 feiyu ,v0.15.
|
|
3
|
+
"version": "0.15.3",
|
|
4
|
+
"description": "鱼 · 人格、按人记忆(可纠正、有印象、能导入旧聊天记录)、日程自走与主动开口,直连 SnowLuma 收发 QQ(群里会自己插话),B站 / 抖音小号私信与自己刷视频;按功能选模型并统计用量花费,能听会说 QQ 语音、收藏表情包、学群里的梗,可选上网查;心情影响说话、去 AI 味、每个群的设定、按聊天选模型、每晚写日记(DSH 插件,单角色 feiyu ,v0.15.3) ",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
7
7
|
"files": [
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
"slots"
|
|
58
58
|
],
|
|
59
59
|
"external": [
|
|
60
|
-
"react"
|
|
60
|
+
"react",
|
|
61
|
+
"@deepseek-ai/dsh-client-ui-primitives"
|
|
61
62
|
]
|
|
62
63
|
}
|
|
63
64
|
},
|