dsh-deepmoyu 2.4.2 → 2.7.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/CHANGELOG.md +39 -0
- package/README.md +5 -5
- package/client.js +35 -7
- package/docs/RELEASE_AUDIT.md +3 -3
- package/docs/RELEASING.md +7 -0
- package/docs/TEST_MATRIX.md +3 -1
- package/docs/tool-evidence.json +6 -1
- package/index.js +143 -27
- package/lib/background-tools.mjs +2 -0
- package/lib/learn-review.mjs +4 -1
- package/lib/model-relay.mjs +47 -4
- package/lib/persona.mjs +1 -0
- package/lib/prompts.mjs +9 -4
- package/lib/state.mjs +30 -0
- package/lib/sticker-automation.mjs +63 -11
- package/lib/sticker-judge.mjs +183 -0
- package/lib/sticker.mjs +76 -8
- package/package.json +1 -1
- package/prompts/duty.md +7 -2
- package/vendor/bilibili-mcp/mcp_server.py +10 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# 更新记录
|
|
2
2
|
|
|
3
|
+
## 2.7.0
|
|
4
|
+
|
|
5
|
+
本节对应当前源码,不能据此推断 npm 或插件市场已更新。
|
|
6
|
+
|
|
7
|
+
- 修 `2026-11-07` 真机事故:带 gif 配图发评论被 B站 拒(49044 无评论动图权限)后,MCP 发送守卫没把 49044 认作「确定拒绝」,坑位被毒化成 unknown 永久锁死 —— 那条视频改成纯文字重发也被拦,从此欠着一条评论。现在 49044 进 `reject_codes`(释放坑位可重试),回执带「换静态图或纯文字重发」的指引;自动配图(`pickForSend`)不再抽 gif(按真实字节格式判,不认扩展名);显式 `picture` 选到 gif 在宿主本地就拦下并说明原因,不再白耗一次节奏坑位。
|
|
8
|
+
- 刷站政策收紧(使用者 11-07 要求):**完整看完一条片就必须在视频下面发一条新评论**,不再是「想说再说」——`lib/prompts.mjs` 的【在视频下面怎么说话】改成硬要求+「欠着的评论补发出去之前不开新片」,实时简报同步。预筛时只读字幕、没决定看的片不算看完,不在此列。配图冷却默认仍 180 秒(本机配置另调到 90 秒让 80% 概率真的能铺开)。
|
|
9
|
+
- 桌宠菜单底部新增**「使用指南」**折叠块(`client.js`):双击开刷、单击/右键菜单、拖动挪位、鲸鱼摸鱼模式里说「刷B站」=启动目标、扫码登录入口、状态灯含义、隐藏后从右下角小圆点点回来;菜单卡片在内容高过 maxHeight(矮窗口)时纵向可滚。
|
|
10
|
+
- 修学习审核连挂 20 批(11-03 起 `learn-review` 每批都报「后台进程报告错误」,批准/拒绝恒为 0):① 模型中继在上游 finish 失败时补**结构化标记**(`code=`/`status=`,只取元数据字段,消息原文仍不落日志——provider 异常可能回显私信正文);② 审核工具 schema 去掉全套工具里唯一的 `minLength`/`maxLength`(长度由 `decideLearningReview` 截 180 字兜底),不再赌上游对 JSON Schema 关键字的支持范围。
|
|
11
|
+
- 修表情判图上线以来 **0 次成功**(25 次「解析不出」+13 次报错):`glm-5.3` 是推理模型,思考也吃输出预算,320 token 全花在思考上、正文一个字不剩 —— 提到 4096、超时 12s→20s,「解析不出」的回执带上回复开头,方便下次直接看出是哪种病。
|
|
12
|
+
- 新增测试:`sticker-review`(库里只剩 gif 时空手而回)、`sticker-routes`(显式 gif 在本地被拦、不进 MCP)、`model-relay`(上游失败只带 code/status 标记、消息原文不出中继)。
|
|
13
|
+
|
|
14
|
+
## 2.6.0
|
|
15
|
+
|
|
16
|
+
本节对应当前源码,不能据此推断 npm 或插件市场已更新。
|
|
17
|
+
|
|
18
|
+
- 表情收藏上限从 60 提到 **100**(`STICKER_MAX`、`stickerCapacity` 默认值与取值上限、工作台输入框同步)。
|
|
19
|
+
- 评论区自动收集新增**大模型判趣味**(`lib/sticker-judge.mjs`):收图前先让宿主自己的模型看这条评论的正文、发图人、点赞数和图片尺寸,给一个 0–100 的趣味分+一句使用场景+几个标签,分数低于 50 的不收。用的是后台模型那条线路,不额外要密钥、不自动换模型。
|
|
20
|
+
- 模型**看不到图片字节**,只看文字线索——提示词里把这一点写死了,免得模型编造画面细节。所以不会因为线路不支持多模态就失效。
|
|
21
|
+
- 本地词表先兜一层:二维码/扫码/加微信/进群/刷单,色情裸露,血腥自伤,身份证银行卡手机号住址——命中直接丢,不给模型再判一次的机会。**这一层与开关无关,永远生效。**
|
|
22
|
+
- 判图**一律 fail open**:没有线路、线路报错、超时、模型输出不可解析,都照老规矩收,只是这张没有分数。不能因为模型抽风把自动收集整个停掉。
|
|
23
|
+
- 关闭人工审核时,自动收集新增**淘汰机制**:库满之后不再直接放弃,而是比分数——新图分数**严格高于**库里最低分那张才顶掉它(同分不算更好,免得来回换);被顶掉的图片文件真删,`state.json` 其它字段不受影响。待审图片不参与淘汰(删了等于替人拒了);手工上传和人审开着的自动收集也不淘汰(机器不替人做决定)。
|
|
24
|
+
- 挑图不再纯随机:高赞、接近方形的更可能是梗图而不是随手截屏,按权重抽;真正的"够不够有趣"仍由判图决定。
|
|
25
|
+
- 收集窗口与图库列表带上分数与判定来源,工作台看到「收了一张表情(82 分),顶掉了「xxx」」这种实话。
|
|
26
|
+
- 新增开关「大模型判断值不值得收」(`stickerJudge`,默认开),关掉只跳过大模型,本地词表照旧。
|
|
27
|
+
- 新增 `tests/sticker-judge.test.mjs`:锁住词表拦截、模型回复宽容解析、fail open 三态、低分拒绝、淘汰只在"自动收集 + 关审核"时发生、同分不换、被判不合适的图根本不下载、判图炸了照旧收、关掉判图不叫模型但词表仍生效。
|
|
28
|
+
|
|
29
|
+
## 2.5.0
|
|
30
|
+
|
|
31
|
+
本节对应当前源码,不能据此推断 npm 或插件市场已更新。
|
|
32
|
+
|
|
33
|
+
- 修 `2026-11-02` 真机事故:自动发说说一直发不出去,日志里连拦截记录都没有。根因是「当日计数」的跨天归零在三处各写了一份(`bumpState`、私信记账、`bumpMomentBy`),前两处都漏了 `moments`,跨天之后 `state.json` 里 `day` 已经是今天而 `moments` 还留着昨天的 20,`bili_moment` 于是判定 `used >= cap` 并**静默**返回 `blocked`。现在归零名单只有一份(`lib/state.mjs` 的 `DAY_COUNTER_FIELDS`/`dayResetPatch`),并给 `moments` 加了 `momentsDay` 自证日期、无该字段时按 `lastMomentAt` 判断,历史脏状态也能自己恢复。
|
|
34
|
+
- 说说的每一种拦截(自动发说说未开、每日上限为 0、额度已满)现在都会写进 `duty.log`,不再有"什么都不发生"的黑洞。
|
|
35
|
+
- 新增 `bili_comment`:**直接发在视频下面的新评论**,参数里没有 `rpid`/`root`,模型想留言时不必再拐进"回复某人"的语义。同一套质量、判重、节奏、审核、风控闸;同一条视频仍只发一条新评论。
|
|
36
|
+
- `bili_reply` 收敛为纯楼层回复,描述里明确"想给这条视频留言、想让更多人看见 ⇒ 用 `bili_comment`"。工具清单 39 项,`docs/tool-evidence.json`、`TEST_MATRIX.md` 同步。
|
|
37
|
+
- 提示词改为默认在视频下发新评论:`lib/prompts.mjs` 的【在视频下面怎么说话】、`lib/persona.mjs` 的回复守则第 1 条、`prompts/duty.md` 的【想说才说】都点明"楼层回复只有对方看得见,那不是默认开口方式"。
|
|
38
|
+
- 降噪:表情收集区满 60 张的 `collect-skipped` 从"每几分钟一条"改为 6 小时只提示一次(其它跳过原因照旧逐条记录)。- 后台模型桥接:`RELAY_BUSY` 不再由元数据查询(`resolve`)抢占,只有流式请求才占连接,撞车时先等最多 30 秒再判;日志里 10-30 两次、11-01、11-02 的 learn-review 全部失败就是这里。
|
|
39
|
+
- 后台模型线路没注册时,报错会列出宿主**当前真正可用的线路**,而不是只说"没有注册"。
|
|
40
|
+
- 新增 `tests/moment-quota.test.mjs`:锁住跨天重置名单、陈数不拦今天的说说、同一天额度仍然生效、无 `momentsDay` 的历史状态靠 `lastMomentAt` 判定、以及 `bili_comment` 绝不写成楼层回复。
|
|
41
|
+
|
|
3
42
|
## 2.4.2
|
|
4
43
|
|
|
5
44
|
本节对应当前源码,不能据此推断 npm 或插件市场已更新。
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ DeepMoYu 是一个 DSH(DeepSeek Harness)插件:**让一只鲸鱼娘自己
|
|
|
4
4
|
|
|
5
5
|
一句话概括:**把 DeepSeek 这类模型接进 B站当一个真正的up主级用户**——自主浏览、自主互动、有自己的口味和记性,而不是一个只会念摘要的机器人。
|
|
6
6
|
|
|
7
|
-
本项目基于 `dsh-bili-scroll` 0.1.19(MIT)开发,保留原作者和 vendored
|
|
7
|
+
本项目基于 `dsh-bili-scroll` 0.1.19(MIT)开发,保留原作者和 vendored 代码署名。当前源码版本见 [package.json](package.json) 的 `version`(以它为准);源码版本、npm 发布和市场收录分别管理,详见 [更新记录](CHANGELOG.md) 与 [发布说明](docs/RELEASING.md)。
|
|
8
8
|
|
|
9
9
|
**更新**:点鲸鱼 → 设置 → 账号与环境 → 「检查更新」。registry 安装(从 npm 或插件市场装的)可以直接点「升级到最新版」,升级后完全重启 DSH 生效。目录联接/源码检出这类本地安装不会被自动覆盖——按钮会锁定并说明手动升级办法,因为运行数据在插件家目录,源码改动应当由你自己用 git 管理。
|
|
10
10
|
|
|
@@ -92,14 +92,14 @@ DSH 会管理 profile 的依赖和 bundle 列表,不需要手动创建 junctio
|
|
|
92
92
|
|
|
93
93
|
主依赖和自动下载仅在明确操作后执行,没有 npm 安装期脚本。系统没有 Python/uv 时会尝试下载并验证 uv;失败原因显示在安装日志中。
|
|
94
94
|
|
|
95
|
-
**评论、回复、私信和动态发送默认关闭。** 阅读不需要开启发送;想让**她自己发言**,到「互动与投递」逐项打开:`autoComment`(发评论,可 @ 你)、`autoMoment`(发动态,受每日上限约束)、`replyToComments`(回楼层)、`autoReplyDm
|
|
95
|
+
**评论、回复、私信和动态发送默认关闭。** 阅读不需要开启发送;想让**她自己发言**,到「互动与投递」逐项打开:`autoComment`(发评论,可 @ 你)、`autoMoment`(发动态,受每日上限约束)、`replyToComments`(回楼层)、`autoReplyDm`(回私信)。自动收集和自动配图也有各自的开关,不能代替发送授权;配图只发静态图(jpg/png/webp),动图(gif)需要平台单独的「评论动图」权限,没有该权限时自动配图不会选中 gif,显式选 gif 会被拦下并说明。
|
|
96
96
|
|
|
97
97
|
## 使用
|
|
98
98
|
|
|
99
99
|
| 任务 | 入口与行为 |
|
|
100
100
|
| --- | --- |
|
|
101
|
-
| 让她自己刷(当前会话) |
|
|
102
|
-
| 让她自己刷(新会话) |
|
|
101
|
+
| 让她自己刷(当前会话) | 切到鲸鱼摸鱼模式,明确要求开始;选择模式本身不启动循环。之后她会自己找片、读字幕;**完整看完一条就会在视频下面留一条新评论**(节奏闸只管快慢,不管评不评),动态仍是想发才发 |
|
|
102
|
+
| 让她自己刷(新会话) | 菜单或运行页「新开会话刷」,或**直接双击桌宠**;继承来源工作区及实际权限。桌宠单击/右键开菜单,菜单底部「使用指南」是速查说明 |
|
|
103
103
|
| 后台与定时 | 运行页设置独立参数;连续时段可跨午夜,手动停止后暂停至本时段结束;重启 DSH 仍会按时段自动接上 |
|
|
104
104
|
| 停止与恢复 | 运行状态或运行页控制;只影响选中的插件任务 |
|
|
105
105
|
| 让她发言 | 「互动与投递」逐项开授权;获批后仍经过发送前审核、判重、节奏和平台额度 |
|
|
@@ -157,7 +157,7 @@ npm run release:build
|
|
|
157
157
|
npm run release:smoke
|
|
158
158
|
```
|
|
159
159
|
|
|
160
|
-
构建生成 `release/dsh-deepmoyu
|
|
160
|
+
构建生成 `release/dsh-deepmoyu-<版本>.tgz`、源码 ZIP、归档验证报告与 `SHA256SUMS`。构建会验证实际文件内容,不包含运行凭证、日志、缓存或 node_modules;源码包包含测试与发布文档。发布前先完成全部验收,见 [发布指南](docs/RELEASING.md)。
|
|
161
161
|
|
|
162
162
|
插件市场需要单独提交目录条目,npm 发布不会自动收录。可提交文件为 [marketplace/pyyxzi__dsh-deepmoyu.yml](marketplace/pyyxzi__dsh-deepmoyu.yml),截图由仓库根的 `screenshots.json` 声明。外部仓库主题、条目审核、账号权限和 npm 认证需在发布时核对。
|
|
163
163
|
|
package/client.js
CHANGED
|
@@ -289,7 +289,10 @@ window.__ModuleLoader__.load({
|
|
|
289
289
|
100%{opacity:0;transform:translate(var(--dx,10px),-48px) scale(1.3)}}
|
|
290
290
|
/* ⭐ 弹出卡片(菜单/子面板共用):毛玻璃、圆角、阴影、缩放进场;默认在鱼上方,
|
|
291
291
|
* 鱼贴顶时翻到下方(data-flip)、贴右缘时右对齐(data-align)—— 都是 JS 算好挂属性。 */
|
|
292
|
-
[data-bd-pet-card]{position:absolute;left:-4px;bottom:calc(100% + 14px);width:252px;border-radius:14px;
|
|
292
|
+
[data-bd-pet-card]{position:absolute;left:-4px;bottom:calc(100% + 14px);width:252px;border-radius:14px;
|
|
293
|
+
/* 📖 使用指南展开后内容可能高过 maxHeight(矮窗口):纵向兜底可滚(内容放得下时不出滚动条),
|
|
294
|
+
横向照旧裁掉;overscroll 别把滚动漏给页面。 */
|
|
295
|
+
overflow:hidden auto;overscroll-behavior:contain;
|
|
293
296
|
background:rgba(16,28,40,.85);backdrop-filter:blur(16px) saturate(1.4);-webkit-backdrop-filter:blur(16px) saturate(1.4);
|
|
294
297
|
border:1px solid rgba(255,255,255,.14);box-shadow:0 14px 44px rgba(2,12,22,.55),0 2px 8px rgba(2,12,22,.4);
|
|
295
298
|
color:#e8f1f8;transform-origin:38px calc(100% + 14px);
|
|
@@ -316,6 +319,19 @@ window.__ModuleLoader__.load({
|
|
|
316
319
|
[data-bd-pet-item] span{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px;}
|
|
317
320
|
[data-bd-pet-item] span i{font-style:normal;font-size:10.5px;color:#8fabbf;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
318
321
|
[data-bd-pet-sep]{height:1px;margin:3px 8px;border:0;background:rgba(255,255,255,.09);}
|
|
322
|
+
/* 📖 使用指南(2026-11-07 使用者要求):菜单卡片最末尾的折叠块,默认收起 ——
|
|
323
|
+
* 不挤占菜单高度;展开后列表自身限高滚动,卡片 maxHeight 裁不到内容。 */
|
|
324
|
+
[data-bd-menu-guide]{border-top:1px solid rgba(255,255,255,.09);}
|
|
325
|
+
[data-bd-menu-guide] summary{display:flex;align-items:center;gap:9px;padding:9px 12px;font-size:12.5px;cursor:pointer;list-style:none;color:#cfe3f0;}
|
|
326
|
+
[data-bd-menu-guide] summary::-webkit-details-marker{display:none;}
|
|
327
|
+
[data-bd-menu-guide] summary:hover{background:rgba(125,211,252,.13);}
|
|
328
|
+
[data-bd-menu-guide] summary svg{flex:none;font-size:15px;width:20px;height:20px;}
|
|
329
|
+
[data-bd-menu-guide] summary::after{content:'▸';margin-left:auto;color:#7fa0b5;font-size:10px;transition:transform .16s ease;}
|
|
330
|
+
[data-bd-menu-guide][open] summary::after{transform:rotate(90deg);}
|
|
331
|
+
[data-bd-menu-guide] ul{margin:0;padding:2px 12px 10px;max-height:180px;overflow:auto;display:flex;flex-direction:column;gap:5px;
|
|
332
|
+
font-size:11px;line-height:1.55;color:#a8c2d4;}
|
|
333
|
+
[data-bd-menu-guide] li{list-style:none;}
|
|
334
|
+
[data-bd-menu-guide] li b{color:#e8f1f8;font-weight:600;}
|
|
319
335
|
[data-bd-pet-spin]{flex:none;width:12px;height:12px;border-radius:50%;display:inline-block;
|
|
320
336
|
border:2px solid rgba(125,211,252,.3);border-top-color:#7dd3fc;animation:bdPetSpin .7s linear infinite;}
|
|
321
337
|
@keyframes bdPetSpin{to{transform:rotate(360deg)}}
|
|
@@ -2412,7 +2428,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2412
2428
|
const reading = React.useRef(false)
|
|
2413
2429
|
const writing = React.useRef(false)
|
|
2414
2430
|
const [loading, setLoading] = React.useState(false)
|
|
2415
|
-
const defaults = { stickerCollectProbability: 35, stickerCollectCooldown: 60, stickerSendProbability: 30, stickerSendCooldown: 180, stickerCapacity:
|
|
2431
|
+
const defaults = { stickerCollectProbability: 35, stickerCollectCooldown: 60, stickerSendProbability: 30, stickerSendCooldown: 180, stickerCapacity: 100 }
|
|
2416
2432
|
const field = (key) => draft[key] ?? cfg[key] ?? data?.settings?.[key] ?? defaults[key]
|
|
2417
2433
|
const pull = async () => {
|
|
2418
2434
|
if (reading.current || writing.current) return
|
|
@@ -2445,7 +2461,7 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2445
2461
|
const save = async (kind) => {
|
|
2446
2462
|
try {
|
|
2447
2463
|
const p = kind === 'collect' ? {
|
|
2448
|
-
stickerCollectProbability: integer('stickerCollectProbability', 0, 100), stickerCollectCooldown: integer('stickerCollectCooldown', 0, 86400), stickerCapacity: integer('stickerCapacity', 1,
|
|
2464
|
+
stickerCollectProbability: integer('stickerCollectProbability', 0, 100), stickerCollectCooldown: integer('stickerCollectCooldown', 0, 86400), stickerCapacity: integer('stickerCapacity', 1, 100),
|
|
2449
2465
|
} : { stickerSendProbability: integer('stickerSendProbability', 0, 100), stickerSendCooldown: integer('stickerSendCooldown', 0, 86400) }
|
|
2450
2466
|
if (await patch(p, kind === 'collect' ? '自动收集参数' : '自动配图参数')) setDraft((old) => Object.fromEntries(Object.entries(old).filter(([key]) => !(key in p))))
|
|
2451
2467
|
} catch (e) { say(String(e?.message ?? e), 'warn') }
|
|
@@ -2544,14 +2560,15 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
2544
2560
|
h('div', { 'data-bd-pghead': '' }, h('div', {}, h('b', {}, '表情收藏'), h('p', {}, '管理表情收集、上传、审核和评论配图。'))),
|
|
2545
2561
|
h('div', { 'data-bd-pet-grid': '', 'data-bd-sticker-controls': '' },
|
|
2546
2562
|
h('section', { 'data-bd-sec': '' }, h('div', { 'data-bd-h': '' }, h('b', {}, '表情收集')),
|
|
2547
|
-
ToggleRow({ label: '从评论区自动收集', on: Boolean(cfg.stickerAutoCollect), disabled, note: '
|
|
2563
|
+
ToggleRow({ label: '从评论区自动收集', on: Boolean(cfg.stickerAutoCollect), disabled, note: '读取评论时,按设定概率收集附图。相同图片不重复收集;收满后,关掉人工审核时新图分数更高会顶掉最低分的那张。', onChange: (v) => patch({ stickerAutoCollect: v }, '自动收集表情') }),
|
|
2564
|
+
ToggleRow({ label: '大模型判断值不值得收', on: cfg.stickerJudge !== false, disabled, note: '收集前先让模型看这条评论的文字、发图人、点赞和尺寸,判「当表情值不值得收」并打分。广告、二维码、隐私这类本地词表直接拦下。模型没给结论时照旧收,只是没有分数。', onChange: (v) => patch({ stickerJudge: v }, '表情趣味判断') }),
|
|
2548
2565
|
ToggleRow({ label: '新表情人工审核', on: Boolean(cfg.stickerReview), disabled, note: '新收集和上传的图片经批准后方可配图。关闭仅影响后续新增图片,已有待审图片仍需审核。', onChange: (v) => patch({ stickerReview: v }, '表情人工审核') }),
|
|
2549
|
-
input('收集概率', 'stickerCollectProbability', 100, '%'), input('收集冷却(秒)', 'stickerCollectCooldown', 86400, '秒'), input('收集容量', 'stickerCapacity',
|
|
2566
|
+
input('收集概率', 'stickerCollectProbability', 100, '%'), input('收集冷却(秒)', 'stickerCollectCooldown', 86400, '秒'), input('收集容量', 'stickerCapacity', 100, '张', 1),
|
|
2550
2567
|
h('button', { 'data-bd-btn': '', disabled, onClick: () => save('collect') }, '保存收集参数')),
|
|
2551
2568
|
h('section', { 'data-bd-sec': '' }, h('div', { 'data-bd-h': '' }, h('b', {}, '评论配图')),
|
|
2552
2569
|
ToggleRow({ label: '新评论自动配图', on: Boolean(cfg.stickerAutoSend), disabled, note: '评论权限已开启时,从可用图库选择配图。仅适用于支持图片的新评论。', onChange: (v) => patch({ stickerAutoSend: v }, '自动配图') }),
|
|
2553
2570
|
input('配图概率', 'stickerSendProbability', 100, '%'), input('配图冷却(秒)', 'stickerSendCooldown', 86400, '秒'),
|
|
2554
|
-
h('p', { 'data-bd-note': '' }, '0% 表示不自动配图,100%
|
|
2571
|
+
h('p', { 'data-bd-note': '' }, '0% 表示不自动配图,100% 表示每次符合条件时尝试配图。指定表情优先,评论和私信权限独立控制。动图(gif)不参与自动配图,显式选 gif 也会被拦 —— 账号没有评论动图权限(49044)。'),
|
|
2555
2572
|
h('button', { 'data-bd-btn': '', disabled, onClick: () => save('send') }, '保存配图参数'))),
|
|
2556
2573
|
h('section', { 'data-bd-sec': '', 'data-bd-sticker-upload-card': '' },
|
|
2557
2574
|
h('div', { 'data-bd-h': '' }, h('b', {}, '上传本地表情')),
|
|
@@ -4412,7 +4429,18 @@ function PetOtherSettings({ view = 'environment', st, setSt, busy, setBusy, say,
|
|
|
4412
4429
|
item('hide', 'hide', '隐藏桌宠', '', hidePet),
|
|
4413
4430
|
h('footer', { 'data-bd-menu-token': '' },
|
|
4414
4431
|
attention.length ? h('div', { 'data-bd-menu-attention': '' }, '⚠ ' + attention[0]) : null,
|
|
4415
|
-
h('div', {}, h('span', {}, '本轮 token'), h('b', {}, tokenValue)), h('small', {}, tokenNote))
|
|
4432
|
+
h('div', {}, h('span', {}, '本轮 token'), h('b', {}, tokenValue)), h('small', {}, tokenNote)),
|
|
4433
|
+
/* 📖 使用指南(2026-11-07):收在菜单最末尾,展开才占地方。只写代码里真实存在的行为,
|
|
4434
|
+
不写虚的 —— 双击开刷、拖动挪位、会话里说「刷B站」启动目标、灯色含义都核对过实现。 */
|
|
4435
|
+
h('details', { 'data-bd-menu-guide': '' },
|
|
4436
|
+
h('summary', {}, h(PetIcon, { name: 'spark' }), h('span', {}, '使用指南')),
|
|
4437
|
+
h('ul', {},
|
|
4438
|
+
h('li', {}, h('b', {}, '双击我'), ' —— 直接新开会话开刷;按住拖动 —— 挪位置;单击或右键 —— 开这个菜单。'),
|
|
4439
|
+
h('li', {}, '在', h('b', {}, '鲸鱼摸鱼模式'), '的会话里,说一句「去刷B站」就是启动目标:它会自己搜片、读字幕、发评论、写日报。'),
|
|
4440
|
+
h('li', {}, h('b', {}, '后台刷'), ' —— 挂机长跑不用盯着会话;想每天定时自动刷,去 工作台 → 运行方式。'),
|
|
4441
|
+
h('li', {}, 'B站没登录?工作台 → 账号与环境 里', h('b', {}, '扫码登录'), '。'),
|
|
4442
|
+
h('li', {}, '头顶小灯:', h('b', {}, '青绿'), '在刷、', h('b', {}, '琥珀'), '后台跑、', h('b', {}, '淡红'), '没登录或异常、蓝灰空闲;被藏起来后,从右下角小圆点点回来。'),
|
|
4443
|
+
h('li', {}, '评论配图、表情收藏、token 预算、节奏快慢,都在 工作台 里调。'))))
|
|
4416
4444
|
})() : null
|
|
4417
4445
|
|
|
4418
4446
|
return h('div', { style: { position: 'absolute', left: '0', top: '0' } },
|
package/docs/RELEASE_AUDIT.md
CHANGED
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
| 实际视频制作 | Pillow 13 环境完成封面→配音→1920×1080/30fps渲染→媒体校验→持久成片,见下节 |
|
|
26
26
|
| uv 下载 | 当前实现实际从官方来源获取校验值,流式下载/校验/解压/执行版本检查,得到 uv 0.13.6;另有15项隔离回归 |
|
|
27
27
|
| 主依赖审计 | 官方 npm audit:0 已知漏洞;pip-audit 2.10.1 对实际主 Python 环境:0 已知漏洞 |
|
|
28
|
-
| `release:check/build/smoke` | bundle、文档链接、
|
|
28
|
+
| `release:check/build/smoke` | bundle、文档链接、39工具证据、素材授权摘要、截图、凭证模式、归档内容与清洁 tarball 安装通过 |
|
|
29
29
|
|
|
30
|
-
全工具逐项证据见 [功能矩阵](TEST_MATRIX.md) 与 `tool-evidence.json`。
|
|
30
|
+
全工具逐项证据见 [功能矩阵](TEST_MATRIX.md) 与 `tool-evidence.json`。421 项 Node 测试包含正向的全部 39 个工具、模式切出后的 78 次旧句柄拒绝、长 ID 原始 MCP 传输和各深层专项,不用无效参数拒绝来代替成功路径覆盖。
|
|
31
31
|
|
|
32
32
|
## 发现与修复
|
|
33
33
|
|
|
@@ -67,4 +67,4 @@
|
|
|
67
67
|
|
|
68
68
|
安装包清洁安装使用临时家目录与生产依赖,执行包内 `check` 自检。依赖代码、媒体和界面文件调整后需重新构建归档。维护者可以在源码根运行README/CONTRIBUTING列出的全部命令;输出可由 `QA_OUTPUT_DIR`、`RELEASE_TMPDIR` 和 `DSH_QA_TMPDIR` 指定位置。
|
|
69
69
|
|
|
70
|
-
市场所需条目、截图及客观描述已准备。GitHub仓库创建满一天的最早时间为 **2026-10-29 13:18:16(北京时间)**;此前外部年龄门禁不会放行。npm发布、GitHub上传与市场维护者审核尚未执行,不能由本地打包通过推断已发布或已收录,详见 [发布指南](RELEASING.md)。
|
|
70
|
+
市场所需条目、截图及客观描述已准备。GitHub仓库创建满一天的最早时间为 **2026-10-29 13:18:16(北京时间)**;此前外部年龄门禁不会放行。npm发布、GitHub上传与市场维护者审核尚未执行,不能由本地打包通过推断已发布或已收录,详见 [发布指南](RELEASING.md)。
|
package/docs/RELEASING.md
CHANGED
|
@@ -64,6 +64,13 @@ npm whoami --registry=https://registry.npmjs.org
|
|
|
64
64
|
npm publish ./release/dsh-deepmoyu-<版本>.tgz --registry=https://registry.npmjs.org --access public
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
两个容易踩的坑(2026-11-03 实测):
|
|
68
|
+
|
|
69
|
+
- 本包在 npm 上的维护者是 `tzl666`,不是 GitHub 的 `pyyxzi`。`~/.npmrc` 里必须有 `//registry.npmjs.org/:_authToken=`,且 token 属于有发布权限的账号;`npm whoami` 返回 401 就是 token 过期,换好后重试。默认 registry 若指向 npmmirror,`whoami`、`publish` 都必须显式带 `--registry=https://registry.npmjs.org`。
|
|
70
|
+
- `npm publish` 有时在**上传成功之后**仍以 `You cannot publish over the previously published versions` 报错退出。不要据此判定失败,也不要用改版本号的方式绕过:先 `Invoke-RestMethod https://registry.npmjs.org/dsh-deepmoyu` 查 `versions` 与 `time.<版本>`,再下载 registry tarball 比对 SHA-256 与本地归档是否一致,以 registry 的实际内容为准。
|
|
71
|
+
|
|
72
|
+
版本号一经上传不可覆盖。`2.5.0` 只在 GitHub 上有标签,从未发布到 npm,因此 npm 上 `2.4.2` 之后直接是 `2.6.0`;如需补齐序列,必须另起一个未被占用的版本号重新构建。
|
|
73
|
+
|
|
67
74
|
发布完成后查询指定版本的 `dist.integrity`、`dist.shasum` 和仓库信息,下载 registry tarball 并核对内容;本地构建通过不能替代发布后检查。npm 版本不可覆盖,不应为发布失败临时篡改归档内容。
|
|
68
75
|
|
|
69
76
|
## 5. 插件市场
|
package/docs/TEST_MATRIX.md
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
| `bili_comments` | 评论数量、正文、长评论/作者标识 |
|
|
36
36
|
| `bili_sticker` | 图库列表;上传/审核另有专项 |
|
|
37
37
|
| `bili_reply` | 评论包装器、纯文字与 BV 发送 |
|
|
38
|
+
| `bili_comment` | 直接发在视频下的新评论:无 rpid/root、同视频判重、不写楼层账本 |
|
|
38
39
|
| `bili_status` | 登录、权限、长 UID 与运行状态 |
|
|
39
40
|
| `bili_pace` | 指定通道的只读节奏状态 |
|
|
40
41
|
| `bili_say` | 有效旁白记录及来源关联 |
|
|
@@ -62,7 +63,8 @@
|
|
|
62
63
|
| 节奏、永久判重及发送复核 | `pace*.test.mjs`、`dup-cross-channel.test.mjs`、`reply.test.mjs`、`verify*.test.mjs`、`test_guard_cross_channel.py` |
|
|
63
64
|
| 端到端长 ID | `mcp-ids.test.mjs`、`tool-surface.test.mjs`、`check-dm-uid-browser.cjs` |
|
|
64
65
|
| 学习、记忆与隐私 | `memory-chain.test.mjs`、`memory-center*.test.mjs`、`background-output.test.mjs` |
|
|
65
|
-
| 表情与图片 | `sticker*.test.mjs`、`test_reply_pictures.py
|
|
66
|
+
| 表情与图片 | `sticker*.test.mjs`、`test_reply_pictures.py`;包括格式、字节审核、公开地址、容量、取消、并发、大模型判趣味与淘汰 |
|
|
67
|
+
| 表情判趣味与淘汰 | `sticker-judge.test.mjs`:本地词表拦截、模型回复宽容解析、fail open 三态、低分拒绝、只在自动收集且关审核时淘汰、同分不换、被判不合适的图不下载、判图故障不影响收集 |
|
|
66
68
|
| 账本、回顾与陪伴 | `token-ledger.test.mjs`、`product-status*.test.mjs`、`companion*.test.mjs` |
|
|
67
69
|
| 视频编排、终态与媒体 | `pv-*.test.mjs`、`test_pv_*.py`、实际配音/封面/渲染/校验全链 |
|
|
68
70
|
| 可选 ASR 与收藏边车 | `test_optional_sidecars.py`:本地输入保留、独占清理、同视频并发、参数与失败;外部服务为 fake |
|
package/docs/tool-evidence.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.
|
|
2
|
+
"version": "2.7.0",
|
|
3
3
|
"scope": "Isolated positive contracts, not real platform sends",
|
|
4
4
|
"tools": [
|
|
5
5
|
{
|
|
@@ -137,6 +137,11 @@
|
|
|
137
137
|
"category": "send",
|
|
138
138
|
"specialist": "send-review-routes.test.mjs"
|
|
139
139
|
},
|
|
140
|
+
{
|
|
141
|
+
"tool": "bili_comment",
|
|
142
|
+
"category": "send",
|
|
143
|
+
"specialist": "send-review-routes.test.mjs"
|
|
144
|
+
},
|
|
140
145
|
{
|
|
141
146
|
"tool": "bili_status",
|
|
142
147
|
"category": "local",
|
package/index.js
CHANGED
|
@@ -50,7 +50,8 @@ import { withFileLockSync, readJsonFileSync, atomicWriteFileSync } from './lib/r
|
|
|
50
50
|
import { MEMORY_SOURCES, memoryCenterSnapshot, memoryFingerprint } from './lib/memory-center.mjs'
|
|
51
51
|
import { normalizeId, optionalId, readableId, replyTargetId } from './lib/ids.mjs'
|
|
52
52
|
import { verifyRecent, appendVerifyToReport } from './lib/verify.mjs'
|
|
53
|
-
import { WINDOWS, stateDirOf, readJson, writeJson, ensureHomeFiles, ensureCharter, charterPath, inWindow,
|
|
53
|
+
import { WINDOWS, stateDirOf, readJson, writeJson, ensureHomeFiles, ensureCharter, charterPath, inWindow,
|
|
54
|
+
dayKey, dayResetPatch, resetDayCounters } from './lib/state.mjs'
|
|
54
55
|
import { loginStart, loginPoll, saveCredential, renderQrPng } from './lib/login.mjs'
|
|
55
56
|
import { createLoginSession } from './lib/login-session.mjs'
|
|
56
57
|
import { modelConfigDecision, modelForceDecision } from './lib/config-policy.mjs'
|
|
@@ -109,6 +110,7 @@ import {
|
|
|
109
110
|
sniffImageFormat,
|
|
110
111
|
} from './lib/sticker.mjs'
|
|
111
112
|
import { STICKER_DEFAULTS, STICKER_LIMITS, clampStickerSettings, stickerSettings, createStickerAutomation } from './lib/sticker-automation.mjs'
|
|
113
|
+
import { judgeStickerCandidate } from './lib/sticker-judge.mjs'
|
|
112
114
|
// 🧾 每轮任务的 token 账本(2026-10-27):一次任务一条账 —— 会话精确计量、后台如实标"未计量"。
|
|
113
115
|
import {
|
|
114
116
|
tokenTaskStart, tokenTaskUpdate, tokenTaskEnd, tokenLedgerSnapshot, clearTokenLedger, tokenTaskLabel,
|
|
@@ -160,6 +162,8 @@ export const Config = !z ? undefined : z.object({
|
|
|
160
162
|
stickerCapacity: z.natural().default(STICKER_DEFAULTS.stickerCapacity).extra('volatile', true),
|
|
161
163
|
/** 新图先经本人批准;关闭只影响以后收的新图,不放行仍待审核的图片。 */
|
|
162
164
|
stickerReview: z.boolean().default(STICKER_DEFAULTS.stickerReview).extra('volatile', true),
|
|
165
|
+
/** 收进库之前让模型判一次「这张当表情值不值得收」;关掉就只走本地词表,且不产生淘汰用的分数。 */
|
|
166
|
+
stickerJudge: z.boolean().default(STICKER_DEFAULTS.stickerJudge).extra('volatile', true),
|
|
163
167
|
/** ⭐ 私信投递:off=只发评论(现状)· only=只私信(完全不公开评论)· both=评论 + 私信 */
|
|
164
168
|
dmMode: z.string().default('off').extra('volatile', true),
|
|
165
169
|
/** ⭐ 私信发给谁的 UID(数字)。要在 dmMode 不是 off 时填 */
|
|
@@ -497,7 +501,8 @@ export function apply(ctx, config) {
|
|
|
497
501
|
case 'bili_dm_inbox': return '跑去私信收件箱瞅瞅有没有人找我'
|
|
498
502
|
case 'bili_dm_thread': return `翻翻和 ${String(arg.uid || '')} 的聊天记录`
|
|
499
503
|
case 'bili_dm_reply': return `给 ${String(arg.uid || '')} 回一条私信`
|
|
500
|
-
case 'bili_reply': return `在 ${bv}
|
|
504
|
+
case 'bili_reply': return `在 ${bv} 的评论下面接话`
|
|
505
|
+
case 'bili_comment': return `在 ${bv} 下面留一句话`
|
|
501
506
|
case 'bili_reply_opus': return `在动态 ${String(arg.opus_id || '')} 下面接话`
|
|
502
507
|
case 'bili_moment': return '发一条说说~'
|
|
503
508
|
case 'bili_danmaku': return `看 ${bv} 的弹幕`
|
|
@@ -958,7 +963,7 @@ export function apply(ctx, config) {
|
|
|
958
963
|
}
|
|
959
964
|
assertScope()
|
|
960
965
|
signal?.throwIfAborted()
|
|
961
|
-
const sending = ['bili_reply', 'bili_reply_opus', 'bili_send_message', 'bili_send_dynamic'].includes(toolName)
|
|
966
|
+
const sending = ['bili_reply', 'bili_comment', 'bili_reply_opus', 'bili_send_message', 'bili_send_dynamic'].includes(toolName)
|
|
962
967
|
const reviewed = sending ? approvedReviewExecutions.get(toolExecution.getStore()) : null
|
|
963
968
|
if (sending) {
|
|
964
969
|
for (const file of [cfgFile, dutyStateFile]) {
|
|
@@ -1012,12 +1017,42 @@ export function apply(ctx, config) {
|
|
|
1012
1017
|
if (!scope) { scope = new AbortController(); stickerSessionScopes.set(key, scope) }
|
|
1013
1018
|
return exec.signal ? AbortSignal.any([exec.signal, scope.signal]) : scope.signal
|
|
1014
1019
|
}
|
|
1020
|
+
/* 📉 2026-11-02 降噪:`collect-skipped` 里"收集区满了"是**预期内且不会自愈**的状态 —— 册子满了就一直是
|
|
1021
|
+
* 满的,直到本人 forget 一张。原来每几分钟往 duty.log 写一行,11-01 全天刷了几百条,把真正要看的东西
|
|
1022
|
+
* 埋了(使用者 m00002 让"顺手把日志里其他问题修一下")。所以这里只对"容量满"这个原因限流:
|
|
1023
|
+
* 同一个错误文本 6 小时内只写一次,且不记 act(桌宠流水里也不重复冒泡)。
|
|
1024
|
+
* 其它原因(下载失败、格式不支持、URL 不安全…)是真异常,照旧每条都写。 */
|
|
1025
|
+
const stickerSkipLoggedAt = new Map()
|
|
1026
|
+
const logStickerSkip = (value) => {
|
|
1027
|
+
const text = String(value.error || '')
|
|
1028
|
+
const capacity = value.code === 'STICKER_CAPACITY' || /收集区满了/.test(text)
|
|
1029
|
+
if (!capacity) { log(`表情自动化(collect-skipped):${text}`); return }
|
|
1030
|
+
const now = Date.now()
|
|
1031
|
+
for (const [key, at] of stickerSkipLoggedAt) if (now - at > 6 * 3600_000) stickerSkipLoggedAt.delete(key)
|
|
1032
|
+
if (stickerSkipLoggedAt.has(text)) return
|
|
1033
|
+
stickerSkipLoggedAt.set(text, now)
|
|
1034
|
+
log(`表情自动化(collect-skipped):${text}(这条 6 小时内只提示一次;想继续收就 forget 一张)`)
|
|
1035
|
+
}
|
|
1015
1036
|
const stickerAutomation = createStickerAutomation({
|
|
1016
1037
|
stateDir,
|
|
1017
1038
|
getConfig: () => { refreshCfg(); return cfg },
|
|
1039
|
+
/* 🐟 2026-11-02(使用者 m00938 第 2 条):收进库之前先让宿主自己的模型看一眼这条评论,
|
|
1040
|
+
* 判"这张图当表情值不值得收",给的分数同时是**淘汰机制**的依据(见 sticker.mjs)。
|
|
1041
|
+
* 线路复用后台模型那条(backgroundModelId),不额外要密钥、不自动换模型;
|
|
1042
|
+
* 没有线路或线路报错时 judgeStickerCandidate 一律 fail open —— 照老规矩收,只是没有分数。 */
|
|
1043
|
+
judge: async ({ candidate, signal }) => {
|
|
1044
|
+
const route = String(cfg.backgroundModelId || '').trim()
|
|
1045
|
+
if (!route) return { suitable: true, score: null, description: '', tags: [], reason: '没有配置后台模型线路,这次只过本地词表', source: 'skip' }
|
|
1046
|
+
let llm = null
|
|
1047
|
+
try { llm = ctx.get('llm') } catch { llm = null }
|
|
1048
|
+
return judgeStickerCandidate({ llm, route, candidate, signal })
|
|
1049
|
+
},
|
|
1018
1050
|
onEvent: (kind, value) => {
|
|
1019
|
-
if (kind === 'collected')
|
|
1020
|
-
|
|
1051
|
+
if (kind === 'collected') {
|
|
1052
|
+
const tail = value.evicted ? `,顶掉了「${value.evicted.name}」` : ''
|
|
1053
|
+
const score = Number.isFinite(Number(value.score)) ? `(${value.score} 分)` : ''
|
|
1054
|
+
act('note', `收了一张表情「${value.name}」${score}${tail}${value.status === 'pending' ? ',等你审核后再用' : ''}`, { bvid: value.bvid })
|
|
1055
|
+
} else logStickerSkip(value)
|
|
1021
1056
|
},
|
|
1022
1057
|
})
|
|
1023
1058
|
// Only the manual gallery explicitly opts into pending. Public/tool defaults remain sendable-only.
|
|
@@ -1085,7 +1120,7 @@ export function apply(ctx, config) {
|
|
|
1085
1120
|
const browsingTools = new Set(['bili_search', 'bili_subtitle', 'bili_danmaku', 'bili_buzzwords',
|
|
1086
1121
|
'bili_video_info', 'bili_rank', 'bili_hot_videos', 'bili_user_videos', 'bili_comments',
|
|
1087
1122
|
'bili_received_replies', 'bili_my_notices', 'bili_my_dynamics', 'bili_opus_comments',
|
|
1088
|
-
'bili_my_video_comments', 'bili_reply', 'bili_reply_opus', 'bili_dm_inbox',
|
|
1123
|
+
'bili_my_video_comments', 'bili_reply', 'bili_comment', 'bili_reply_opus', 'bili_dm_inbox',
|
|
1089
1124
|
'bili_dm_thread', 'bili_dm_reply', 'bili_moment', 'bili_deliver'])
|
|
1090
1125
|
const tool = (spec) => {
|
|
1091
1126
|
const wrapped = {
|
|
@@ -1095,7 +1130,7 @@ export function apply(ctx, config) {
|
|
|
1095
1130
|
async execute(args, exec) {
|
|
1096
1131
|
const manuallyApproved = exec && approvedReviewExecutions.has(exec)
|
|
1097
1132
|
if (!manuallyApproved && !isFishExecution(exec)) return { ok: false, blocked: true, reason: 'mode_required', hint: 'DeepMoYu 工具仅在鲸鱼摸鱼模式内生效;普通会话不会启用角色工具或记忆。' }
|
|
1098
|
-
if (['bili_reply', 'bili_reply_opus', 'bili_dm_reply', 'bili_moment', 'bili_deliver'].includes(spec.name)) {
|
|
1133
|
+
if (['bili_reply', 'bili_comment', 'bili_reply_opus', 'bili_dm_reply', 'bili_moment', 'bili_deliver'].includes(spec.name)) {
|
|
1099
1134
|
const unauthorized = modelForceDecision(args)
|
|
1100
1135
|
if (unauthorized) return unauthorized
|
|
1101
1136
|
}
|
|
@@ -2284,7 +2319,8 @@ export function apply(ctx, config) {
|
|
|
2284
2319
|
perUser[String(uid)] = { day: today, n: base + 1 }
|
|
2285
2320
|
writeJson(dutyStateFile, {
|
|
2286
2321
|
...st,
|
|
2287
|
-
|
|
2322
|
+
// 跨天归零走统一名单(原先是手写的 `{count,picked,windows}`,漏了 moments)
|
|
2323
|
+
...(crossDay ? dayResetPatch(today) : {}),
|
|
2288
2324
|
day: today,
|
|
2289
2325
|
dmReplied: replied,
|
|
2290
2326
|
dmPerUser: perUser,
|
|
@@ -2444,9 +2480,19 @@ export function apply(ctx, config) {
|
|
|
2444
2480
|
},
|
|
2445
2481
|
})
|
|
2446
2482
|
|
|
2447
|
-
|
|
2483
|
+
/* ⭐ 2026-11-02(使用者 m00002 第 2 条原话:「在视频下面评论,要直接评论啊,不要在别人的评论下评论,
|
|
2484
|
+
* 直接视频下评论就行,更多人才能看到」):
|
|
2485
|
+
* 以前只有 `bili_reply` 一只工具,而且描述里大半篇幅在讲"回复别人的评论时 rpid/root 怎么填",
|
|
2486
|
+
* 判重那边又是"同一条视频只发一条新评论"(比回复严得多)—— 结构上就等于在推模型去楼层回复。
|
|
2487
|
+
* 真机账本也印证了:repliedComments 1941 条里 1148 条是楼中楼、只有 396 条是视频下的新评论。
|
|
2488
|
+
* 现在把"发新评论"拆成独立的一只 `bili_comment`(下面注册),没有 rpid/root 参数,
|
|
2489
|
+
* 默认动作就是"在视频下面说话";`bili_reply` 保留给"确实有人跟你说话、要接他那一句"。
|
|
2490
|
+
* 两者共用同一段实现(`replyTool.execute`),所以开关、质量、判重、节奏、审核、风控闸全都不变。 */
|
|
2491
|
+
const replyTool = {
|
|
2448
2492
|
name: 'bili_reply',
|
|
2449
|
-
description: '
|
|
2493
|
+
description: '在视频下方**回复某一条已有的评论**(楼中楼/楼层回复)。正文简短,按当前会话的语气自然接话。\n'
|
|
2494
|
+
+ '⭐ **想给这条视频留言、想让更多人看见 ⇒ 用 `bili_comment`(发新评论),不要用这只。**'
|
|
2495
|
+
+ '这只只在"有人先跟你说话了、你要接他那一句"时用:楼层回复只有对方和点进楼里的人看得到。\n'
|
|
2450
2496
|
+ '回复别人的评论时:`rpid` 填他那条的 rpid,`root` 填你自己那条主评论的 rpid(`bili_received_replies` 的 '
|
|
2451
2497
|
+ '`how_to_reply` 直接照抄就行)。没有条数上限,但**回过的不再回**(宿主记着账本)。\n'
|
|
2452
2498
|
+ '✍️ **内容要有东西,不是为发而发**:光喊「awsl / 太强了 / 哈哈哈哈」这类话会被打回 —— '
|
|
@@ -2468,11 +2514,14 @@ export function apply(ctx, config) {
|
|
|
2468
2514
|
picture: { type: 'string', description: '(可选,仅发新评论)表情图名字,最多 2 张、用逗号或空格分隔;bili_sticker({action:"list"}) 查;回复楼层不支持图片' },
|
|
2469
2515
|
force: { type: 'boolean', description: '已停用;true 会被拒绝。模型参数不能代表本人授权,请通过工作台手动设置权限并审核。' },
|
|
2470
2516
|
},
|
|
2471
|
-
|
|
2517
|
+
/* `presetRpid` 是 `bili_comment` 用的:传 '0' 就锁定"发新评论"这条分支。
|
|
2518
|
+
⚠️ 必须复用**同一个 args 对象**(不要 `{...args}`)—— 审批路径靠 `approvedReviewArgs` 这个
|
|
2519
|
+
WeakMap 认身份,复制一份就等于把"预览时那张图的字节指纹"丢了。 */
|
|
2520
|
+
async execute(args, exec, presetRpid) {
|
|
2472
2521
|
const bvid = String(args.bvid ?? '').trim()
|
|
2473
|
-
const rpid = optionalId(args.rpid)
|
|
2522
|
+
const rpid = presetRpid === undefined ? optionalId(args.rpid) : String(presetRpid)
|
|
2474
2523
|
const isReply = rpid !== '0'
|
|
2475
|
-
const root = optionalId(args.root)
|
|
2524
|
+
const root = isReply ? optionalId(args.root) : '0'
|
|
2476
2525
|
/* 🔴 2026-10-20:模型拿着「动态(opus)下面的评论」来调这只 —— 那儿根本没有 BV 号。
|
|
2477
2526
|
* 原来不校验,直接往下传,MCP 抛「bvid 提供错误,必须是以 BV 开头的…12 位字符串」,
|
|
2478
2527
|
* 报错暴露在流水里像插件坏了;更要紧的是它会被当成"这条回不了"而放弃,
|
|
@@ -2618,7 +2667,7 @@ export function apply(ctx, config) {
|
|
|
2618
2667
|
const files = [], snapshots = []
|
|
2619
2668
|
for (const nm of names) {
|
|
2620
2669
|
// Read approved bytes under the library lock and hand the sender a private
|
|
2621
|
-
// snapshot. A forget + same-name pending upload
|
|
2670
|
+
// snapshot. A forget + same-name pending upload during the send can no longer swap the file
|
|
2622
2671
|
// while MCP is still doing its own IO.
|
|
2623
2672
|
const shot = snapshotStickerForSend(stateDir, nm)
|
|
2624
2673
|
if (!shot) {
|
|
@@ -2629,6 +2678,18 @@ export function apply(ctx, config) {
|
|
|
2629
2678
|
+ '或 bili_sticker({action:"collect", url:"…", name:"…"}) 先收进来。本条没发(怕你想要的就是那张图)。',
|
|
2630
2679
|
}
|
|
2631
2680
|
}
|
|
2681
|
+
/* 🚫 2026-11-07(49044 实测):动图(gif)要单独的「评论动图」权限,这个账号没有 ——
|
|
2682
|
+
* 带上去整条评论被 B站 拒掉(49044),图和文字一起没了,还白耗一次节奏坑位。
|
|
2683
|
+
* 在本地就拦下并说清楚,比让接口打回来便宜得多;自动配图那一路已在库里排除 gif。 */
|
|
2684
|
+
if (/\.gif$/i.test(String(shot.file ?? ''))) {
|
|
2685
|
+
shot.cleanup()
|
|
2686
|
+
for (const made of snapshots) made.cleanup()
|
|
2687
|
+
return {
|
|
2688
|
+
ok: false, blocked: true, picture_gif: true,
|
|
2689
|
+
hint: `「${shot.name}」是动图(gif)—— 这个账号没有评论动图权限(49044 实测被拒),`
|
|
2690
|
+
+ '带上去整条评论都发不出去。换一张静态图(jpg/png),或本条改纯文字。',
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2632
2693
|
const approved = approvedReviewArgs.get(args)
|
|
2633
2694
|
if (approved) {
|
|
2634
2695
|
const expected = (approved.attachments || []).find(item => item.name === shot.name)
|
|
@@ -2741,6 +2802,39 @@ export function apply(ctx, config) {
|
|
|
2741
2802
|
if (riskNote && out && typeof out === 'object') return { ...out, riskNote }
|
|
2742
2803
|
return out
|
|
2743
2804
|
},
|
|
2805
|
+
}
|
|
2806
|
+
// 回复通道(楼中楼):只在"有人先跟你说话"时用
|
|
2807
|
+
tool(replyTool)
|
|
2808
|
+
|
|
2809
|
+
/* 🗣️ 发新评论(= 直接发在视频下面,所有人点开视频都能看见)——
|
|
2810
|
+
* 使用者 m00002 第 2 条要的就是这个动作,所以把它独立成一只**没有 rpid/root 参数**的工具:
|
|
2811
|
+
* 模型想留言时不必再拐进"回复某人"的语义里,参数面也不给它走错的机会。 */
|
|
2812
|
+
tool({
|
|
2813
|
+
name: 'bili_comment',
|
|
2814
|
+
description: '🗣️ **在视频下方发一条新评论** —— 直接挂在视频下面,点开视频的人都能看见。'
|
|
2815
|
+
+ '想让更多人看到你的话,就是用这只(不要用 bili_reply 去回复别人,那只有对方看得见)。\n'
|
|
2816
|
+
+ '什么时候发:看完这条视频真有话说就发(哪一下、哪一秒、哪个说法打动你,或者你不同意哪儿)。'
|
|
2817
|
+
+ '**没有想说的就不发 —— 配额不是催办;同一条视频只发一条新评论**(发过就换下一条视频)。\n'
|
|
2818
|
+
+ '✍️ **内容要有东西,不是为发而发**:光喊「awsl / 太强了 / 哈哈哈哈」这类话会被打回。'
|
|
2819
|
+
+ '按当前会话的语气自然说话,不要求固定词、表情或自称。\n'
|
|
2820
|
+
+ '🖼️ 想配图就传 `picture`(表情库里的名字,`bili_sticker({action:"list"})` 查,最多 2 张);'
|
|
2821
|
+
+ '小表情码如 [doge] 直接写进正文就行,不算图。\n'
|
|
2822
|
+
+ '⏱️ 发之前先 `bili_pace({kind:"comment"})` 问一句 —— 说等就等,别试探性重发(那是 -352 的最短路径)。\n'
|
|
2823
|
+
+ '🔴 安全闸:桌宠设置「自动化」里的「自动评论视频」**默认是关的**;关着的时候这个工具会拒绝 —— '
|
|
2824
|
+
+ '需要本人在工作台手动打开权限;模型不能用 force 或配置 PATCH 绕过。\n'
|
|
2825
|
+
+ '📌 动态(opus)下面没有 BV 号,那种要用 `bili_reply_opus`。',
|
|
2826
|
+
parameters: {
|
|
2827
|
+
bvid: { type: 'string', description: 'BV号(视频下方发评论)' },
|
|
2828
|
+
text: { type: 'string', description: '评论正文(短、干净、像真人在说话;别写敏感词)' },
|
|
2829
|
+
picture: { type: 'string', description: '(可选)表情图名字,最多 2 张、用逗号或空格分隔;bili_sticker({action:"list"}) 查' },
|
|
2830
|
+
force: { type: 'boolean', description: '已停用;true 会被拒绝。模型参数不能代表本人授权,请通过工作台手动设置权限并审核。' },
|
|
2831
|
+
},
|
|
2832
|
+
async execute(args) {
|
|
2833
|
+
// 复用回复工具的实现:`presetRpid='0'` 强制走"发新评论"分支,
|
|
2834
|
+
// 质量/判重/节奏/审核/风控闸全都是同一套,不另起一条会跑偏的路。
|
|
2835
|
+
// ⚠️ 传**原对象**(不是副本):审批路径用 `approvedReviewArgs` WeakMap 认身份。
|
|
2836
|
+
return replyTool.execute(args, undefined, '0')
|
|
2837
|
+
},
|
|
2744
2838
|
})
|
|
2745
2839
|
|
|
2746
2840
|
tool({
|
|
@@ -2959,6 +3053,7 @@ export function apply(ctx, config) {
|
|
|
2959
3053
|
if (text.length > 300) return { ok: false, error: `正文 ${text.length} 字,B站动态上限约 300 字,先砍短` }
|
|
2960
3054
|
const gateOn = Boolean(cfg.autoMoment)
|
|
2961
3055
|
if (!gateOn && args?.force !== true) {
|
|
3056
|
+
log('🚫 说说被拦:工作台「自动发说说」未开启')
|
|
2962
3057
|
return {
|
|
2963
3058
|
ok: false, blocked: true,
|
|
2964
3059
|
hint: '工作台中的「自动发说说」未开启,请本人手动打开。模型不能通过 force 或配置 PATCH 自授权限。',
|
|
@@ -2969,9 +3064,13 @@ export function apply(ctx, config) {
|
|
|
2969
3064
|
const cap = Number.isFinite(capNum) ? Math.max(0, capNum) : 1
|
|
2970
3065
|
const used = countMomentsToday()
|
|
2971
3066
|
if (cap <= 0 && args?.force !== true) {
|
|
3067
|
+
// ⚠️ 2026-11-02:以前这几种拦截**只回给模型、不写日志** —— 于是 duty.log 里
|
|
3068
|
+
// "动态怎么不发了"完全查不到线索(连一条 blocked 都没有)。拦截也是事件,必须留痕。
|
|
3069
|
+
log(`🚫 说说被拦:工作台的每日上限是 0(不发)`)
|
|
2972
3070
|
return { ok: false, blocked: true, hint: '工作台中的动态每日上限是 0(不发),请本人手动调整;模型不能绕过额度。' }
|
|
2973
3071
|
}
|
|
2974
3072
|
if (used >= cap && args?.force !== true) {
|
|
3073
|
+
log(`🚫 说说被拦:今天已发 ${used}/${cap} 条(额度满),明天再聊`)
|
|
2975
3074
|
return { ok: false, blocked: true, hint: `今天已经发了 ${used}/${cap} 条说说了,明天再聊。` }
|
|
2976
3075
|
}
|
|
2977
3076
|
/* ⏱️ 出手节奏(2026-10-21 m18802 第 1、2 条):发动态是最"显眼"的动作 ——
|
|
@@ -3099,7 +3198,7 @@ export function apply(ctx, config) {
|
|
|
3099
3198
|
* `sendReview` 只对鱼模式的自动发送生效。默认关闭,旧发送语义不变。
|
|
3100
3199
|
* 队列落在 state.json,重启后仍能看到待处理内容;批准时重新走原工具,
|
|
3101
3200
|
* 进程内 WeakMap 只通过预览排队这一层,原开关、质量、判重、节奏和风控闸都不跳过。 */
|
|
3102
|
-
const SEND_REVIEW_TOOLS = new Set(['bili_reply', 'bili_reply_opus', 'bili_dm_reply', 'bili_moment', 'bili_deliver'])
|
|
3201
|
+
const SEND_REVIEW_TOOLS = new Set(['bili_reply', 'bili_comment', 'bili_reply_opus', 'bili_dm_reply', 'bili_moment', 'bili_deliver'])
|
|
3103
3202
|
const SEND_REVIEW_MAX = 80
|
|
3104
3203
|
const approvedReviewArgs = new WeakMap()
|
|
3105
3204
|
const approvedReviewExecutions = new WeakMap()
|
|
@@ -3158,11 +3257,13 @@ export function apply(ctx, config) {
|
|
|
3158
3257
|
try { clean = validateToolArguments(toolInputSchema(backgroundToolRegistry.get(toolName)?.parameters || {}), reviewArgs(args)) }
|
|
3159
3258
|
catch (error) { return { ok: false, blocked: true, error: String(error?.message ?? error) } }
|
|
3160
3259
|
if (JSON.stringify(clean).length > 16000) return { ok: false, blocked: true, error: '预览参数过长,未保存内容' }
|
|
3161
|
-
|
|
3260
|
+
// `bili_comment` 是 `bili_reply` 的"只发新评论"通道,picture 语义完全一致,一起走这段
|
|
3261
|
+
const pictureTool = toolName === 'bili_reply' || toolName === 'bili_comment'
|
|
3262
|
+
if (pictureTool && !Object.hasOwn(clean, 'picture')) clean.picture = ''
|
|
3162
3263
|
const now = Date.now()
|
|
3163
3264
|
const id = reviewIdOf(toolName, clean, now)
|
|
3164
3265
|
const attachments = []
|
|
3165
|
-
if (
|
|
3266
|
+
if (pictureTool && clean.picture) {
|
|
3166
3267
|
const names = String(clean.picture).split(/[,,、]+|\s+/).filter(Boolean)
|
|
3167
3268
|
if (names.length > 2) return { ok: false, blocked: true, error: '发送预览最多选择 2 张表情' }
|
|
3168
3269
|
for (const name of names) {
|
|
@@ -3401,8 +3502,10 @@ export function apply(ctx, config) {
|
|
|
3401
3502
|
*/
|
|
3402
3503
|
const bumpState = (patch) => withStateLock(() => {
|
|
3403
3504
|
const st = readJson(dutyStateFile, {})
|
|
3404
|
-
|
|
3405
|
-
|
|
3505
|
+
// ⚠️ 跨天归零只走 lib/state.mjs 那一份名单(`resetDayCounters`)——
|
|
3506
|
+
// 这里原先手写 `count/picked/windows/tokensToday`,**漏了 moments**,
|
|
3507
|
+
// 导致跨天后 `moments` 还是昨天的数 ⇒ `bili_moment` 静默判定"今天额度满了"、永不再发(2026-11-02 的事故)。
|
|
3508
|
+
if (st.day !== dayKey()) resetDayCounters(st)
|
|
3406
3509
|
writeJson(dutyStateFile, { ...st, ...(typeof patch === 'function' ? patch(st) : patch) })
|
|
3407
3510
|
return st
|
|
3408
3511
|
})
|
|
@@ -5190,30 +5293,43 @@ export function apply(ctx, config) {
|
|
|
5190
5293
|
} catch { return { ok: false, hh: 0, mm: 0, wd: '', part: '', text: '', line: '' } }
|
|
5191
5294
|
}
|
|
5192
5295
|
|
|
5193
|
-
/** 今天发了几条说说(从 state.json 数;宿主记账,不靠模型自觉)
|
|
5296
|
+
/** 今天发了几条说说(从 state.json 数;宿主记账,不靠模型自觉)
|
|
5297
|
+
* ⚠️ 2026-11-02 加固:`moments` 只跟 `st.day` 绑在一起是不够的 —— 只要有**任何**一条跨天写路径
|
|
5298
|
+
* 没重置它(当时 `bumpState`、私信记账就都漏了),今天就会读到昨天的数、然后永久"额度已满"。
|
|
5299
|
+
* 现在再加一道自证的 `momentsDay`:记账时一起写,读的时候对不上就按 0 算、顺手把陈数清掉。
|
|
5300
|
+
*/
|
|
5194
5301
|
const countMomentsToday = (now = new Date()) => {
|
|
5195
5302
|
const st = readJson(dutyStateFile, {})
|
|
5196
|
-
|
|
5303
|
+
const today = dayKey(now)
|
|
5304
|
+
if (st.day !== today) return 0
|
|
5305
|
+
// 老状态没有 momentsDay:那 `moments` 可能是翻篇前留下的陈数 —— 只有在 day 也不对时才可疑,
|
|
5306
|
+
// 这里 day 已经对上了,就信 `momentsDay`;没有 momentsDay 时退回看 lastMomentAt 是不是今天。
|
|
5307
|
+
const stamp = typeof st.momentsDay === 'string' ? st.momentsDay : ''
|
|
5308
|
+
if (stamp) return stamp === today ? Number(st.moments || 0) : 0
|
|
5309
|
+
const at = Date.parse(String(st.lastMomentAt || ''))
|
|
5310
|
+
if (Number.isFinite(at) && dayKey(new Date(at)) !== today) return 0
|
|
5311
|
+
return Number(st.moments || 0)
|
|
5197
5312
|
}
|
|
5198
5313
|
/**
|
|
5199
5314
|
* 记说说账(delta 可正可负)。⚠️ 走 stateLock(`bili_moment` 里是 await 发出去之后才记账,中间能插进别的写)。
|
|
5200
5315
|
* ⭐ 2026-10-26 改成可加可减:`bili_moment` 现在**发送前先在锁内预占一个名额**(并发不会再双发突破上限),
|
|
5201
5316
|
* 发送失败再用 -1 把名额退回去;force(不占额)成功后仍走 +1 补记。
|
|
5202
|
-
* ⚠️ 跨天时必须连 count/picked/windows
|
|
5203
|
-
*
|
|
5204
|
-
* countPickedToday() 把昨天的额度算到今天头上 ⇒ 今天一开局就"额度已满"」。
|
|
5317
|
+
* ⚠️ 跨天时必须连 count/picked/windows/moments 一起重置 —— 现在统一走 lib/state.mjs 的 `dayResetPatch`,
|
|
5318
|
+
* 那边是**唯一**的当日计数名单,别再在这里手写一份(手写漏字段就是 2026-11-02 那次事故)。
|
|
5205
5319
|
*/
|
|
5206
5320
|
const bumpMomentBy = (delta = 1) => withStateLock(() => {
|
|
5207
5321
|
const st = readJson(dutyStateFile, {})
|
|
5208
5322
|
const today = dayKey()
|
|
5209
|
-
|
|
5210
|
-
const
|
|
5323
|
+
// 陈数(没有 momentsDay 或对不上今天)一律当 0 起算,绝不把昨天的额度带进今天
|
|
5324
|
+
const stale = st.momentsDay !== today || st.day !== today
|
|
5325
|
+
const base = stale ? 0 : Number(st.moments || 0)
|
|
5211
5326
|
const next = Math.max(0, base + delta)
|
|
5212
5327
|
writeJson(dutyStateFile, {
|
|
5213
5328
|
...st,
|
|
5214
|
-
...(
|
|
5329
|
+
...(st.day !== today ? dayResetPatch(today) : {}),
|
|
5215
5330
|
day: today,
|
|
5216
5331
|
moments: next,
|
|
5332
|
+
momentsDay: today,
|
|
5217
5333
|
lastMomentAt: new Date().toISOString(),
|
|
5218
5334
|
})
|
|
5219
5335
|
return next
|
package/lib/background-tools.mjs
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
export function backgroundToolNames(cfg = {}) {
|
|
3
3
|
const names = ['bili_whoami', 'bili_search', 'bili_subtitle', 'bili_comments', 'bili_video_info', 'bili_danmaku', 'bili_buzzwords', 'bili_rank', 'bili_hot_videos', 'bili_user_videos', 'bili_seen', 'bili_say', 'bili_pace', 'bili_my_notices', 'bili_my_dynamics', 'bili_opus_comments', 'bili_my_video_comments', 'bili_learn', 'bili_soul', 'bili_taste', 'bili_records', 'bili_sticker']
|
|
4
4
|
if (cfg.autoComment || cfg.replyToComments) names.push('bili_reply', 'bili_reply_opus')
|
|
5
|
+
// 「自动评论视频」开着才给发新评论的通道;回复另算(replyToComments)
|
|
6
|
+
if (cfg.autoComment) names.push('bili_comment')
|
|
5
7
|
if (cfg.replyToComments) names.push('bili_received_replies')
|
|
6
8
|
if (cfg.autoMoment) names.push('bili_moment')
|
|
7
9
|
if (cfg.autoReplyDm) names.push('bili_dm_inbox', 'bili_dm_thread', 'bili_dm_reply')
|