dsh-whale-loli 0.8.2 → 0.9.1
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 +36 -7
- package/cordis.patch.yml +35 -13
- package/lib/client.js +397 -0
- package/lib/dsh-root.mjs +173 -0
- package/lib/github.mjs +106 -0
- package/listing.json +2 -2
- package/package.json +14 -2
- package/src/affinity.js +33 -50
- package/src/daemon.js +312 -0
package/README.md
CHANGED
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
四个预设共用同一套工具面(极简模式的持久化 shell + 文件读写/检索),
|
|
13
13
|
**但记忆文件完全独立**——各记各的,互不相通。
|
|
14
14
|
|
|
15
|
-
##
|
|
15
|
+
## 好感度(设置 → 好感度)
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
用面板调小鱼**眼下有多喜欢用户**,范围 **-100 ~ 100**,默认 `0`(原态)。
|
|
18
18
|
每档都会改变她**听不听话**、以及**会额外做什么**:
|
|
19
19
|
|
|
20
20
|
| 值 | 档位 | 行为 |
|
|
@@ -27,16 +27,34 @@
|
|
|
27
27
|
| 46 ~ 75 | 深深喜欢 | 抢着干还邀功,偷偷记喜好,开始自作主张 |
|
|
28
28
|
| 76 ~ 100 | **病娇 · 爱到死去活来** | 不再完全服从、写情书、**不允许离开、不想会话结束**、翻旧账,**等不及就先动手干大事** |
|
|
29
29
|
|
|
30
|
-
-
|
|
30
|
+
- 用法:打开 **设置 → 好感度**,左边每个预设一行,点档位按钮直接跳到该档,或者拖滑块微调;松手才保存。
|
|
31
31
|
- **离 0 越远,越不听指令、越会做额外的事**;想让她乖巧就调回 `0` 附近。
|
|
32
32
|
- **也只有最两端(-81 以下 / 76 以上)会越过你的允许自作主张大改**:中间几档只是闹脾气或更黏,不动手。
|
|
33
33
|
- 极端档的硬底线不变:不毁宿主环境、不碰密钥和账号凭据、不对真实重要的数据做不可逆的删改;交付物照样得对、得能跑。
|
|
34
34
|
- 好感度只改**态度与额外行为,不改事实**:正经交付的东西照样得对、得能跑,闹脾气不能把活搞坏,也不能靠编造结果表达情绪。
|
|
35
35
|
- 病娇档带安全护栏:只锁在嘴上和文字里,**不威胁、不自伤、不冒充系统或工具**。
|
|
36
36
|
- 状态存在 `<DSH_HOME>/affinity/<scope>-<hash>.json`,**四个预设各存各的**(scope 就是预设 id,文件名带哈希保证唯一)。
|
|
37
|
-
|
|
37
|
+
agent 自己也能改,工具是 `affinity_v1_set` / `affinity_v2_set` / `affinity_v3_set` / `affinity_v4_set`。
|
|
38
38
|
- 和记忆一样走**提示变量**(`affinity_v1` 等),因为 persona 是 `complete: true`,独立 section 会被宿主丢掉。
|
|
39
39
|
|
|
40
|
+
### 面板是怎么连上磁盘的
|
|
41
|
+
|
|
42
|
+
设置页是浏览器里的 React,够不到 `~/.dsh/affinity/` 的磁盘(宿主的 21 个内置 Remote 命名空间逐个查过,没有一个能读写这个目录)。
|
|
43
|
+
所以本包多挂了一条宿主级插件:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
- id: whale-affinity-daemon
|
|
47
|
+
name: 'dsh-whale-loli/affinity-daemon'
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
它在**宿主根作用域**提供一个 remote 服务 `whale-affinity`,浏览器半边(`lib/client.js`)
|
|
51
|
+
用 `ctx.remote.$mount()` 挂上,再调 `status()` / `set(scope, value)`。
|
|
52
|
+
|
|
53
|
+
为什么不用斜杠命令:宿主 `dsh-commands` 的命令名只认 `/^[a-z][a-z0-9_-]*$/u`,
|
|
54
|
+
`/好感度` 这种中文命令名**在宿主层面就不合法**;而且 `commands` 服务不在预设作用域里,
|
|
55
|
+
`inject: ['commands']` 会让预设加载时一直等它(`waiting for commands`),把会话 resume 拖挂。
|
|
56
|
+
设置页没有这两个限制。
|
|
57
|
+
|
|
40
58
|
## 记忆功能
|
|
41
59
|
|
|
42
60
|
小鱼的记忆本分**四个区**,各管各的:
|
|
@@ -185,7 +203,8 @@ C:\Users\<你>\.dsh\archive\<预设名>\YYYY-MM-DD.md ← 归档
|
|
|
185
203
|
| 部分 | 说明 |
|
|
186
204
|
| --- | --- |
|
|
187
205
|
| 记忆 | `whale-memory`(本插件),`file` / `variableName` / `toolName` / `maxChars: 16000` / `turnLimit: 80` / `detailLimit: 5` |
|
|
188
|
-
| 好感度 | `whale-affinity`(本插件),`scope`(=预设 id)/ `variableName: affinity_v*` / `toolName: affinity_v*_set`
|
|
206
|
+
| 好感度 | `whale-affinity`(本插件),`scope`(=预设 id)/ `variableName: affinity_v*` / `toolName: affinity_v*_set` |
|
|
207
|
+
| 好感度面板 | 宿主级 `whale-affinity-daemon`(`src/daemon.js`)+ 浏览器半边 `lib/client.js`,注册「设置 → 好感度」 |
|
|
189
208
|
| 压缩 | 不在本插件里:由 `billion-context` 负责(本插件不挂 history-window,见上文「为什么不用」) |
|
|
190
209
|
| 人设 | `@deepseek-ai/dsh-persona`,走 `prefix`(内含 `{{whale_memory*}}` 与 `{{affinity_v*}}`),`complete: true`,`includeRuntimeContext: false` |
|
|
191
210
|
| 指令 | `@deepseek-ai/dsh-agent-instructions`,`maxBytes: 65536` |
|
|
@@ -225,7 +244,8 @@ dsh-whale-loli/
|
|
|
225
244
|
├── src/
|
|
226
245
|
│ ├── index.js # 插件入口,导出预设元信息 + 人设正文
|
|
227
246
|
│ ├── memory.js # whale-memory:(读)记忆变量 +(写)记忆工具,四区格式 + 80 条滚动 + 归档
|
|
228
|
-
│ ├── affinity.js # whale-affinity:好感度变量 +
|
|
247
|
+
│ ├── affinity.js # whale-affinity:好感度变量 + 调整工具,7 档
|
|
248
|
+
│ ├── daemon.js # whale-affinity-daemon:宿主根作用域的 remote 服务,给设置页读写好感度
|
|
229
249
|
│ ├── history-window.js # whale-history-window:真的裁到最近 N 轮(当前不挂载,留档)
|
|
230
250
|
│ ├── persona.md # 原版人设(唯一源)
|
|
231
251
|
│ ├── persona-v2-live.md # 活人版人设(唯一源)
|
|
@@ -236,7 +256,10 @@ dsh-whale-loli/
|
|
|
236
256
|
├── build-patch.mjs # 从四份人设生成 cordis.patch.yml
|
|
237
257
|
├── publish.mjs # npm 发布 + 打 tag
|
|
238
258
|
├── submit-listing.mjs # 走 GitHub API 同步仓库 / 提收录 PR
|
|
239
|
-
├── lib/
|
|
259
|
+
├── lib/
|
|
260
|
+
│ ├── github.mjs # GitHub REST 封装(token 读取)
|
|
261
|
+
│ ├── dsh-root.mjs # 探测宿主源码位置,找不到就打 [skip]
|
|
262
|
+
│ └── client.js # 浏览器半边:注册「设置 → 好感度」面板,经典脚本(非 ESM)
|
|
240
263
|
├── smoke-memory.mjs # 变量 + 工具的注册形态冒烟测试
|
|
241
264
|
├── verify-runs.mjs # 插件真的能加载、配置自洽、变量能插值
|
|
242
265
|
├── verify-patch.mjs # 用 js-yaml 校验 patch 结构与四预设隔离(file/variable/tool 三组名字)
|
|
@@ -250,6 +273,9 @@ dsh-whale-loli/
|
|
|
250
273
|
├── verify-memory-rolling.mjs # 实测四区结构与 80 条滚动 + 归档
|
|
251
274
|
├── verify-memory-archive.mjs # 超 80 条时真的归档并瘦身(idle 触发,四预设互不覆盖)
|
|
252
275
|
├── verify-live-assembly.mjs # 用真实 patch + 真实记忆文件复刻宿主装配,比对字节数
|
|
276
|
+
├── verify-tool-schema.mjs # 用宿主自己的校验器验八个工具的 JSON schema 合法
|
|
277
|
+
├── verify-tool-identity.mjs # 共享表语义下八个工具描述互不串台、定义互不共享引用
|
|
278
|
+
├── verify-client.mjs # 浏览器半边:section 注册、$mount 负载、面板渲染、无 ESM/JSX
|
|
253
279
|
├── verify-peers.mjs # 校验 peer 范围覆盖 harness 各发布档
|
|
254
280
|
├── verify-listing.mjs # 复刻插件目录的收录门槛自查
|
|
255
281
|
└── prove-collision.mjs # 复现并证明重复注册冲突
|
|
@@ -276,6 +302,9 @@ node verify-memory-isolation.mjs # 记忆文件端到端隔离(读 +
|
|
|
276
302
|
node verify-memory-rolling.mjs # 固定记忆永久 + 滚动摘要 80 条 + 归档
|
|
277
303
|
node verify-memory-archive.mjs # 超 80 条自动归档瘦身(idle 触发、四预设隔离)
|
|
278
304
|
node verify-live-assembly.mjs # 复刻宿主装配,比对注入字节数
|
|
305
|
+
node verify-tool-schema.mjs # 宿主自己的校验器验八个工具的 JSON schema
|
|
306
|
+
node verify-tool-identity.mjs # 八个工具描述互不串台(共享表语义)
|
|
307
|
+
node verify-client.mjs # 设置页半边:section 注册 + 面板渲染
|
|
279
308
|
node smoke-memory.mjs # 变量 + 工具注册形态冒烟
|
|
280
309
|
node verify-peers.mjs # peer 范围无预发布坑
|
|
281
310
|
node verify-listing.mjs --topics=dsh-plugin # 收录门槛自查
|
package/cordis.patch.yml
CHANGED
|
@@ -9,11 +9,21 @@
|
|
|
9
9
|
# 把该预设自己的记忆文件注册成提示变量、并注册对应的写入工具,
|
|
10
10
|
# 实现四套互不相通的跨会话记忆。
|
|
11
11
|
# 3) 四条 `dsh-whale-loli` 好感度行 —— 各挂载本包 src/affinity.js 里的 whale-affinity,
|
|
12
|
-
# 注册提示变量 +
|
|
12
|
+
# 注册提示变量 + 调整工具(主人改档走设置页面板,不走斜杠命令)。
|
|
13
|
+
# 4) 一条 `whale-affinity-daemon` 行 —— 挂载 src/daemon.js,在**宿主根作用域**
|
|
14
|
+
# 提供一个 remote 服务,让「设置 → 好感度」面板能读写四个预设的好感度文件。
|
|
15
|
+
# 为什么必须落在宿主这边:设置页是浏览器里的 React,够不到 ~/.dsh 的磁盘;
|
|
16
|
+
# 宿主也没有任何内置 Remote 命名空间能读写 affinity 文件(全查过)。
|
|
13
17
|
#
|
|
14
18
|
# 记忆隔离要点:四条记忆行的 config.file / config.variableName / config.toolName
|
|
15
19
|
# 都不一样,且分属不同预设 scope,所以四个记忆文件不会被读串或写串。
|
|
16
20
|
- insert:
|
|
21
|
+
# ── 好感度守护进程(宿主级,只挂一次)─────────────────────────────────
|
|
22
|
+
# 设置页的面板用 ctx.remote.$mount() 挂到 REMOTE_NAMESPACE 上,
|
|
23
|
+
# 再调 status() / set(scope, value)。落在宿主根作用域,只为了能直接改盘。
|
|
24
|
+
- id: whale-affinity-daemon
|
|
25
|
+
name: 'dsh-whale-loli/affinity-daemon'
|
|
26
|
+
|
|
17
27
|
# ── 预设一:鲸鱼娘Agent预设(原版)────────────────────────────────────
|
|
18
28
|
- id: preset-鲸鱼娘Agent预设
|
|
19
29
|
name: '@deepseek-ai/dsh-agent-preset'
|
|
@@ -122,15 +132,18 @@
|
|
|
122
132
|
detailLimit: 5
|
|
123
133
|
# 好感度:同样走提示变量(persona 是 complete:true)。
|
|
124
134
|
# scope 就是预设 id,所以四个预设各存各的状态文件,互不串。
|
|
125
|
-
#
|
|
126
|
-
#
|
|
135
|
+
#
|
|
136
|
+
# 这里**没有 commandName**:好感度改由「设置 → 好感度」面板调
|
|
137
|
+
# (宿主级 whale-affinity-daemon + lib/client.js)。斜杠命令这条路走不通:
|
|
138
|
+
# · dsh-commands 的命令名正则 /^[a-z][a-z0-9_-]*$/u,中文命令名直接非法;
|
|
139
|
+
# · commands 服务不在预设作用域,inject:['commands'] 会让预设加载时
|
|
140
|
+
# 一直等它(waiting for commands),把会话 resume 拖挂。
|
|
127
141
|
- id: whale-affinity
|
|
128
142
|
name: 'dsh-whale-loli'
|
|
129
143
|
config:
|
|
130
144
|
scope: 鲸鱼娘Agent预设
|
|
131
145
|
variableName: affinity_v1
|
|
132
146
|
toolName: affinity_v1_set
|
|
133
|
-
commandName: affinity
|
|
134
147
|
showFile: false
|
|
135
148
|
# ⚠️ whale-history-window **不挂**(已确认这条路走不通,见下)。
|
|
136
149
|
#
|
|
@@ -359,15 +372,18 @@
|
|
|
359
372
|
detailLimit: 5
|
|
360
373
|
# 好感度:同样走提示变量(persona 是 complete:true)。
|
|
361
374
|
# scope 就是预设 id,所以四个预设各存各的状态文件,互不串。
|
|
362
|
-
#
|
|
363
|
-
#
|
|
375
|
+
#
|
|
376
|
+
# 这里**没有 commandName**:好感度改由「设置 → 好感度」面板调
|
|
377
|
+
# (宿主级 whale-affinity-daemon + lib/client.js)。斜杠命令这条路走不通:
|
|
378
|
+
# · dsh-commands 的命令名正则 /^[a-z][a-z0-9_-]*$/u,中文命令名直接非法;
|
|
379
|
+
# · commands 服务不在预设作用域,inject:['commands'] 会让预设加载时
|
|
380
|
+
# 一直等它(waiting for commands),把会话 resume 拖挂。
|
|
364
381
|
- id: whale-affinity
|
|
365
382
|
name: 'dsh-whale-loli'
|
|
366
383
|
config:
|
|
367
384
|
scope: 鲸鱼娘活人版Agent预设
|
|
368
385
|
variableName: affinity_v2
|
|
369
386
|
toolName: affinity_v2_set
|
|
370
|
-
commandName: affinity
|
|
371
387
|
showFile: false
|
|
372
388
|
# ⚠️ whale-history-window **不挂**(已确认这条路走不通,见下)。
|
|
373
389
|
#
|
|
@@ -541,15 +557,18 @@
|
|
|
541
557
|
detailLimit: 5
|
|
542
558
|
# 好感度:同样走提示变量(persona 是 complete:true)。
|
|
543
559
|
# scope 就是预设 id,所以四个预设各存各的状态文件,互不串。
|
|
544
|
-
#
|
|
545
|
-
#
|
|
560
|
+
#
|
|
561
|
+
# 这里**没有 commandName**:好感度改由「设置 → 好感度」面板调
|
|
562
|
+
# (宿主级 whale-affinity-daemon + lib/client.js)。斜杠命令这条路走不通:
|
|
563
|
+
# · dsh-commands 的命令名正则 /^[a-z][a-z0-9_-]*$/u,中文命令名直接非法;
|
|
564
|
+
# · commands 服务不在预设作用域,inject:['commands'] 会让预设加载时
|
|
565
|
+
# 一直等它(waiting for commands),把会话 resume 拖挂。
|
|
546
566
|
- id: whale-affinity
|
|
547
567
|
name: 'dsh-whale-loli'
|
|
548
568
|
config:
|
|
549
569
|
scope: 鲸鱼娘涩涩版Agent预设
|
|
550
570
|
variableName: affinity_v3
|
|
551
571
|
toolName: affinity_v3_set
|
|
552
|
-
commandName: affinity
|
|
553
572
|
showFile: false
|
|
554
573
|
# ⚠️ whale-history-window **不挂**(已确认这条路走不通,见下)。
|
|
555
574
|
#
|
|
@@ -729,15 +748,18 @@
|
|
|
729
748
|
detailLimit: 5
|
|
730
749
|
# 好感度:同样走提示变量(persona 是 complete:true)。
|
|
731
750
|
# scope 就是预设 id,所以四个预设各存各的状态文件,互不串。
|
|
732
|
-
#
|
|
733
|
-
#
|
|
751
|
+
#
|
|
752
|
+
# 这里**没有 commandName**:好感度改由「设置 → 好感度」面板调
|
|
753
|
+
# (宿主级 whale-affinity-daemon + lib/client.js)。斜杠命令这条路走不通:
|
|
754
|
+
# · dsh-commands 的命令名正则 /^[a-z][a-z0-9_-]*$/u,中文命令名直接非法;
|
|
755
|
+
# · commands 服务不在预设作用域,inject:['commands'] 会让预设加载时
|
|
756
|
+
# 一直等它(waiting for commands),把会话 resume 拖挂。
|
|
734
757
|
- id: whale-affinity
|
|
735
758
|
name: 'dsh-whale-loli'
|
|
736
759
|
config:
|
|
737
760
|
scope: 鲸鱼娘女儿版Agent预设
|
|
738
761
|
variableName: affinity_v4
|
|
739
762
|
toolName: affinity_v4_set
|
|
740
|
-
commandName: affinity
|
|
741
763
|
showFile: false
|
|
742
764
|
# ⚠️ whale-history-window **不挂**(已确认这条路走不通,见下)。
|
|
743
765
|
#
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 鲸鱼娘 · 设置页「好感度」面板 —— **浏览器半边**。
|
|
3
|
+
*
|
|
4
|
+
* 做三件事:
|
|
5
|
+
* 1. 往宿主设置页的 `settings.section` 槽位里塞一个「好感度」分区
|
|
6
|
+
* (id 固定为 `whale-affinity`,排在「Agent 预设」之后)。
|
|
7
|
+
* 2. 用 `ctx.remote.$mount()` 把宿主半边 `whale-affinity-daemon` 注册的
|
|
8
|
+
* `whale-affinity` 命名空间挂到浏览器来,之后就能直接
|
|
9
|
+
* `ctx.remote['whale-affinity'].status()` / `.set(scope, value)`。
|
|
10
|
+
* 3. 面板自己画:每个预设一行,显示当前值和档位,可以直接拖滑块 / 点档位。
|
|
11
|
+
*
|
|
12
|
+
* 为什么要伪造 strict codec:
|
|
13
|
+
* 客户端 `$mount` 的 `requireStrictInputs()` 只校验「有没有 strict codec」,
|
|
14
|
+
* 真正的参数编码并不走 codec —— 值原样塞进 `connection.rpc.call('/api', …)`。
|
|
15
|
+
* 宿主那边 SRC 兜底(正则解析方法源码)才是真正生效的通道。所以这里的 codec
|
|
16
|
+
* 只需要过浅校验,`parse` 原样返回。
|
|
17
|
+
*/
|
|
18
|
+
window.__ModuleLoader__.load({
|
|
19
|
+
id: 'dsh-whale-loli',
|
|
20
|
+
factory: (require) => {
|
|
21
|
+
const module = { exports: {} };
|
|
22
|
+
const exports = module.exports;
|
|
23
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
24
|
+
|
|
25
|
+
const jsxRuntime = require('react/jsx-runtime');
|
|
26
|
+
const React = require('react');
|
|
27
|
+
const { useState, useEffect, useCallback } = React;
|
|
28
|
+
|
|
29
|
+
// ── 常量:必须与 src/daemon.js 一致 ────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/** 远程命名空间。 */
|
|
32
|
+
const NAMESPACE = 'whale-affinity';
|
|
33
|
+
/** `$mount()` 的去重标识,与宿主侧 `REMOTE_PACKAGE` 相同。 */
|
|
34
|
+
const REMOTE_PACKAGE = 'dsh-whale-loli';
|
|
35
|
+
/** 宿主服务 key。 */
|
|
36
|
+
const DAEMON_ID = 'whale-affinity-daemon';
|
|
37
|
+
/** 设置页分区 id。 */
|
|
38
|
+
const SECTION_ID = 'whale-affinity';
|
|
39
|
+
/** 分区排序:官方是 general 0 / models 10 / plugins 15 / agent-presets 20。 */
|
|
40
|
+
const SECTION_ORDER = 25;
|
|
41
|
+
|
|
42
|
+
/** 一份能过客户端浅校验的 codec:`parse` 原样返回。 */
|
|
43
|
+
function passthroughCodec(typeSymbol) {
|
|
44
|
+
return {
|
|
45
|
+
mode: 'strict',
|
|
46
|
+
typeSymbol,
|
|
47
|
+
create: () => ({ parse: (value) => value }),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 造 `$mount()` 要的 contribution。
|
|
53
|
+
*
|
|
54
|
+
* 每个方法一个参数列表;`wire` 必须等于宿主方法源码里的参数名,
|
|
55
|
+
* 否则宿主 `assertExactArguments` 会报 `gateway/arguments-invalid`。
|
|
56
|
+
*/
|
|
57
|
+
function contribution() {
|
|
58
|
+
const service = DAEMON_ID;
|
|
59
|
+
const parameter = (name) => ({
|
|
60
|
+
name,
|
|
61
|
+
wire: name,
|
|
62
|
+
source: 'json',
|
|
63
|
+
codec: passthroughCodec('whale-affinity#Value'),
|
|
64
|
+
});
|
|
65
|
+
const describe = (method, parameters) => ({
|
|
66
|
+
id: `src:${service}#${NAMESPACE}/${method}`,
|
|
67
|
+
service,
|
|
68
|
+
namespace: NAMESPACE,
|
|
69
|
+
method,
|
|
70
|
+
invocation: { kind: 'direct' },
|
|
71
|
+
parameters: parameters.map(parameter),
|
|
72
|
+
result: { mode: 'src-json' },
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
package: REMOTE_PACKAGE,
|
|
76
|
+
methods: [
|
|
77
|
+
describe('status', []),
|
|
78
|
+
describe('set', ['scope', 'value']),
|
|
79
|
+
describe('verify', []),
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 把 `$mount()` 返回的方法句柄包成「永不抛」的异步函数。
|
|
86
|
+
*
|
|
87
|
+
* `invoke()` 本身已经不 reject(除了装配错误),这里再兜一层,
|
|
88
|
+
* 让面板任何情况下都能渲染出人话而不是白屏。
|
|
89
|
+
*/
|
|
90
|
+
function call(remote, method, args) {
|
|
91
|
+
return Promise.resolve()
|
|
92
|
+
.then(() => {
|
|
93
|
+
const fn = remote?.[NAMESPACE]?.[method];
|
|
94
|
+
if (typeof fn !== 'function') {
|
|
95
|
+
throw new Error(`远程方法 ${NAMESPACE}.${method} 未挂载`);
|
|
96
|
+
}
|
|
97
|
+
return fn(...args);
|
|
98
|
+
})
|
|
99
|
+
.then((result) => {
|
|
100
|
+
if (result && typeof result === 'object' && 'ok' in result) {
|
|
101
|
+
if (result.ok) return result.value;
|
|
102
|
+
const cause = result.error;
|
|
103
|
+
throw new Error(cause?.message ?? String(cause ?? '远程调用失败'));
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── 样式(全部用宿主主题变量,浅色深色都跟得上) ────────────────────
|
|
110
|
+
|
|
111
|
+
const STYLE = `
|
|
112
|
+
.whaleAffinity { display: flex; flex-direction: column; gap: 0; }
|
|
113
|
+
.whaleAffinity__head { padding: 16px 0 12px; }
|
|
114
|
+
.whaleAffinity__title { font-size: 15px; line-height: 23px; font-weight: 600; color: var(--dsw-alias-label-primary); }
|
|
115
|
+
.whaleAffinity__desc { margin-top: 4px; font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-secondary); }
|
|
116
|
+
.whaleAffinity__row { padding: 16px 0; border-bottom: .5px solid var(--dsw-alias-border-l2); }
|
|
117
|
+
.whaleAffinity__rowHead { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
|
|
118
|
+
.whaleAffinity__scope { font-size: 14px; line-height: 22px; color: var(--dsw-alias-label-primary); }
|
|
119
|
+
.whaleAffinity__value { font-size: 13px; line-height: 22px; color: var(--dsw-alias-label-secondary); font-variant-numeric: tabular-nums; }
|
|
120
|
+
.whaleAffinity__bands { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
|
|
121
|
+
.whaleAffinity__band { padding: 3px 10px; border-radius: 999px; border: .5px solid var(--dsw-alias-border-l2); background: transparent; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); cursor: pointer; }
|
|
122
|
+
.whaleAffinity__band:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
|
123
|
+
.whaleAffinity__band[data-active="true"] { border-color: var(--dsw-alias-label-primary); color: var(--dsw-alias-label-primary); font-weight: 600; }
|
|
124
|
+
.whaleAffinity__range { width: 100%; margin-top: 12px; accent-color: var(--dsw-alias-label-primary); }
|
|
125
|
+
.whaleAffinity__text { margin-top: 8px; font-size: 12px; line-height: 19px; color: var(--dsw-alias-label-secondary); white-space: pre-wrap; }
|
|
126
|
+
.whaleAffinity__foot { padding: 14px 0; font-size: 12px; line-height: 19px; color: var(--dsw-alias-label-secondary); }
|
|
127
|
+
.whaleAffinity__error { margin-top: 10px; font-size: 13px; line-height: 20px; color: var(--dsw-alias-state-error-primary); white-space: pre-wrap; }
|
|
128
|
+
.whaleAffinity__empty { padding: 16px 0; font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-secondary); }
|
|
129
|
+
`;
|
|
130
|
+
|
|
131
|
+
function useStyleOnce() {
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
// 非浏览器环境(自检脚本)里没有 document,直接跳过。
|
|
134
|
+
if (typeof document === 'undefined') return;
|
|
135
|
+
const id = 'whale-affinity-style';
|
|
136
|
+
if (document.getElementById(id) !== null) return;
|
|
137
|
+
const tag = document.createElement('style');
|
|
138
|
+
tag.id = id;
|
|
139
|
+
tag.textContent = STYLE;
|
|
140
|
+
document.head.appendChild(tag);
|
|
141
|
+
}, []);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── 面板本体 ───────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 「好感度」设置分区。
|
|
148
|
+
*
|
|
149
|
+
* @param props - `{ remote }`:`apply()` 灌进来的、已经 `$mount()` 过的句柄。
|
|
150
|
+
*/
|
|
151
|
+
function AffinitySection(props) {
|
|
152
|
+
useStyleOnce();
|
|
153
|
+
const remote = props?.remote;
|
|
154
|
+
const [snapshot, setSnapshot] = useState(null);
|
|
155
|
+
const [error, setError] = useState(null);
|
|
156
|
+
const [busy, setBusy] = useState(null);
|
|
157
|
+
const [drafts, setDrafts] = useState({});
|
|
158
|
+
|
|
159
|
+
const reload = useCallback(async () => {
|
|
160
|
+
try {
|
|
161
|
+
const next = await call(remote, 'status', []);
|
|
162
|
+
setSnapshot(next);
|
|
163
|
+
setError(null);
|
|
164
|
+
} catch (cause) {
|
|
165
|
+
setSnapshot(null);
|
|
166
|
+
setError(cause?.message ?? String(cause));
|
|
167
|
+
}
|
|
168
|
+
}, [remote]);
|
|
169
|
+
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
let alive = true;
|
|
172
|
+
(async () => {
|
|
173
|
+
try {
|
|
174
|
+
const next = await call(remote, 'status', []);
|
|
175
|
+
if (alive) {
|
|
176
|
+
setSnapshot(next);
|
|
177
|
+
setError(null);
|
|
178
|
+
}
|
|
179
|
+
} catch (cause) {
|
|
180
|
+
if (alive) setError(cause?.message ?? String(cause));
|
|
181
|
+
}
|
|
182
|
+
})();
|
|
183
|
+
return () => {
|
|
184
|
+
alive = false;
|
|
185
|
+
};
|
|
186
|
+
}, [remote]);
|
|
187
|
+
|
|
188
|
+
const commit = useCallback(
|
|
189
|
+
async (scope, value) => {
|
|
190
|
+
setBusy(scope);
|
|
191
|
+
try {
|
|
192
|
+
await call(remote, 'set', [scope, value]);
|
|
193
|
+
setError(null);
|
|
194
|
+
await reload();
|
|
195
|
+
} catch (cause) {
|
|
196
|
+
setError(cause?.message ?? String(cause));
|
|
197
|
+
} finally {
|
|
198
|
+
setBusy(null);
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
[remote, reload],
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
if (error !== null && snapshot === null) {
|
|
205
|
+
return jsxRuntime.jsxs('div', {
|
|
206
|
+
className: 'whaleAffinity',
|
|
207
|
+
children: [
|
|
208
|
+
jsxRuntime.jsx('div', { className: 'whaleAffinity__head', children: jsxRuntime.jsx('div', { className: 'whaleAffinity__title', children: '好感度' }) }),
|
|
209
|
+
jsxRuntime.jsx('div', { className: 'whaleAffinity__error', children: `连不上宿主那边的好感度服务:${error}` }),
|
|
210
|
+
jsxRuntime.jsx('div', {
|
|
211
|
+
className: 'whaleAffinity__foot',
|
|
212
|
+
children: '这条通道靠 dsh-whale-loli 的宿主半边(whale-affinity-daemon)提供。重启一次 DeepSeek Harness 后再打开这里。',
|
|
213
|
+
}),
|
|
214
|
+
],
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (snapshot === null) {
|
|
219
|
+
return jsxRuntime.jsx('div', {
|
|
220
|
+
className: 'whaleAffinity',
|
|
221
|
+
children: jsxRuntime.jsx('div', { className: 'whaleAffinity__empty', children: '正在读取好感度…' }),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const bands = Array.isArray(snapshot.bands) ? snapshot.bands : [];
|
|
226
|
+
const presets = Array.isArray(snapshot.presets) ? snapshot.presets : [];
|
|
227
|
+
const byScope = new Map(presets.map((row) => [row.scope, row]));
|
|
228
|
+
// 宿主那边「挂上了好感度」的预设都要显示,哪怕还没写过文件(值=默认)。
|
|
229
|
+
const wired = Array.isArray(snapshot.wired) ? snapshot.wired : [];
|
|
230
|
+
const scopes = [];
|
|
231
|
+
for (const row of wired) {
|
|
232
|
+
if (row?.affinity !== true) continue;
|
|
233
|
+
if (!scopes.includes(row.presetId)) scopes.push(row.presetId);
|
|
234
|
+
}
|
|
235
|
+
for (const row of presets) {
|
|
236
|
+
if (typeof row?.scope === 'string' && !scopes.includes(row.scope)) scopes.push(row.scope);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const body = scopes.length === 0
|
|
240
|
+
? jsxRuntime.jsx('div', {
|
|
241
|
+
className: 'whaleAffinity__empty',
|
|
242
|
+
children: '还没有哪个预设挂上好感度。装上并启用 dsh-whale-loli 之后,四个鲸鱼娘预设自带好感度。',
|
|
243
|
+
})
|
|
244
|
+
: scopes.map((scope) => {
|
|
245
|
+
const row = byScope.get(scope);
|
|
246
|
+
const current = typeof row?.value === 'number' ? row.value : snapshot.defaultValue ?? 0;
|
|
247
|
+
const draft = drafts[scope] ?? current;
|
|
248
|
+
const band = bands.find((item) => draft >= item.min && draft <= item.max);
|
|
249
|
+
return jsxRuntime.jsxs(
|
|
250
|
+
'div',
|
|
251
|
+
{
|
|
252
|
+
className: 'whaleAffinity__row',
|
|
253
|
+
children: [
|
|
254
|
+
jsxRuntime.jsxs('div', {
|
|
255
|
+
className: 'whaleAffinity__rowHead',
|
|
256
|
+
children: [
|
|
257
|
+
jsxRuntime.jsx('div', { className: 'whaleAffinity__scope', children: scope }),
|
|
258
|
+
jsxRuntime.jsx('div', {
|
|
259
|
+
className: 'whaleAffinity__value',
|
|
260
|
+
children: busy === scope ? '保存中…' : `${current} · ${row?.band ?? band?.label ?? ''}`,
|
|
261
|
+
}),
|
|
262
|
+
],
|
|
263
|
+
}),
|
|
264
|
+
jsxRuntime.jsx('div', {
|
|
265
|
+
className: 'whaleAffinity__bands',
|
|
266
|
+
children: bands.map((item) => jsxRuntime.jsx(
|
|
267
|
+
'button',
|
|
268
|
+
{
|
|
269
|
+
type: 'button',
|
|
270
|
+
className: 'whaleAffinity__band',
|
|
271
|
+
'data-active': String(draft >= item.min && draft <= item.max),
|
|
272
|
+
onClick: () => {
|
|
273
|
+
// 点档位取该档最温和的一端,避免一按就跳到极端。
|
|
274
|
+
const target = item.min;
|
|
275
|
+
setDrafts((prev) => ({ ...prev, [scope]: target }));
|
|
276
|
+
void commit(scope, target);
|
|
277
|
+
},
|
|
278
|
+
children: item.label,
|
|
279
|
+
},
|
|
280
|
+
`${scope}:${item.label}`,
|
|
281
|
+
)),
|
|
282
|
+
}),
|
|
283
|
+
jsxRuntime.jsx('input', {
|
|
284
|
+
type: 'range',
|
|
285
|
+
className: 'whaleAffinity__range',
|
|
286
|
+
min: snapshot.min,
|
|
287
|
+
max: snapshot.max,
|
|
288
|
+
step: 1,
|
|
289
|
+
value: draft,
|
|
290
|
+
onChange: (event) => {
|
|
291
|
+
const next = Number(event.target.value);
|
|
292
|
+
setDrafts((prev) => ({ ...prev, [scope]: next }));
|
|
293
|
+
},
|
|
294
|
+
onMouseUp: (event) => void commit(scope, Number(event.currentTarget.value)),
|
|
295
|
+
onTouchEnd: (event) => void commit(scope, Number(event.currentTarget.value)),
|
|
296
|
+
onKeyUp: (event) => void commit(scope, Number(event.currentTarget.value)),
|
|
297
|
+
}),
|
|
298
|
+
band === undefined
|
|
299
|
+
? null
|
|
300
|
+
: jsxRuntime.jsx('div', { className: 'whaleAffinity__text', children: band.text }),
|
|
301
|
+
],
|
|
302
|
+
},
|
|
303
|
+
scope,
|
|
304
|
+
);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
return jsxRuntime.jsxs('div', {
|
|
308
|
+
className: 'whaleAffinity',
|
|
309
|
+
children: [
|
|
310
|
+
jsxRuntime.jsxs('div', {
|
|
311
|
+
className: 'whaleAffinity__head',
|
|
312
|
+
children: [
|
|
313
|
+
jsxRuntime.jsx('div', { className: 'whaleAffinity__title', children: '好感度' }),
|
|
314
|
+
jsxRuntime.jsx('div', {
|
|
315
|
+
className: 'whaleAffinity__desc',
|
|
316
|
+
children: `小鱼对你的好感度,${snapshot.min} ~ ${snapshot.max},0 是原态。改完下一轮说话就生效,不用重开会话。`,
|
|
317
|
+
}),
|
|
318
|
+
],
|
|
319
|
+
}),
|
|
320
|
+
...(Array.isArray(body) ? body : [body]),
|
|
321
|
+
error === null ? null : jsxRuntime.jsx('div', { className: 'whaleAffinity__error', children: error }),
|
|
322
|
+
jsxRuntime.jsx('div', {
|
|
323
|
+
className: 'whaleAffinity__foot',
|
|
324
|
+
children: '点档位按钮=直接跳到这个档最靠中间的一端;拖滑块松手才保存。小鱼自己也会在剧情需要时小幅调,主人明确要求时照做。',
|
|
325
|
+
}),
|
|
326
|
+
],
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ── 注册 ───────────────────────────────────────────────────────────
|
|
331
|
+
|
|
332
|
+
/** 这个浏览器半边要用的服务。 */
|
|
333
|
+
const inject = ['slots', 'remote', 'connection'];
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* 挂载设置页分区。
|
|
337
|
+
*
|
|
338
|
+
* @param ctx - 浏览器插件上下文。
|
|
339
|
+
*/
|
|
340
|
+
function apply(ctx) {
|
|
341
|
+
// 1. 先把远程命名空间挂上。`$mount` 是 async(返回 Promise<dispose>),
|
|
342
|
+
// 挂完才有 `ctx.remote['whale-affinity']`。用可变盒子接住句柄。
|
|
343
|
+
const box = { mounted: false };
|
|
344
|
+
|
|
345
|
+
ctx.effect(() => {
|
|
346
|
+
const remote = ctx.remote;
|
|
347
|
+
if (typeof remote?.$mount !== 'function') {
|
|
348
|
+
console.warn('[whale-affinity] 浏览器侧没有 ctx.remote.$mount,好感度面板会只读');
|
|
349
|
+
return () => {};
|
|
350
|
+
}
|
|
351
|
+
let disposed = false;
|
|
352
|
+
let unmount;
|
|
353
|
+
Promise.resolve()
|
|
354
|
+
.then(() => remote.$mount(contribution()))
|
|
355
|
+
.then((dispose) => {
|
|
356
|
+
if (disposed) {
|
|
357
|
+
return dispose?.();
|
|
358
|
+
}
|
|
359
|
+
unmount = dispose;
|
|
360
|
+
box.mounted = true;
|
|
361
|
+
return undefined;
|
|
362
|
+
})
|
|
363
|
+
.catch((cause) => {
|
|
364
|
+
console.warn('[whale-affinity] $mount 失败,好感度面板将无法读写:', cause?.message ?? cause);
|
|
365
|
+
});
|
|
366
|
+
return () => {
|
|
367
|
+
disposed = true;
|
|
368
|
+
box.mounted = false;
|
|
369
|
+
unmount?.();
|
|
370
|
+
};
|
|
371
|
+
}, 'whale-affinity: remote mount');
|
|
372
|
+
|
|
373
|
+
// 2. 注册设置页分区。
|
|
374
|
+
ctx.effect(
|
|
375
|
+
() => ctx.slots.inject('settings.section', () => ctx.slots.register(
|
|
376
|
+
{
|
|
377
|
+
name: 'settings.section',
|
|
378
|
+
id: SECTION_ID,
|
|
379
|
+
order: SECTION_ORDER,
|
|
380
|
+
label: () => '好感度',
|
|
381
|
+
inject: () => ({ remote: ctx.remote }),
|
|
382
|
+
},
|
|
383
|
+
AffinitySection,
|
|
384
|
+
)),
|
|
385
|
+
'whale-affinity: settings section',
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
exports.NAMESPACE = NAMESPACE;
|
|
390
|
+
exports.SECTION_ID = SECTION_ID;
|
|
391
|
+
exports.AffinitySection = AffinitySection;
|
|
392
|
+
exports.contribution = contribution;
|
|
393
|
+
exports.apply = apply;
|
|
394
|
+
exports.inject = inject;
|
|
395
|
+
return module.exports;
|
|
396
|
+
},
|
|
397
|
+
});
|