dsh-deepmoyu 2.4.1 → 2.6.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/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, dayKey } from './lib/state.mjs'
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,
@@ -118,6 +120,13 @@ import {
118
120
  // 编排(出分镜 → 渲染 → 收成品)在 lib/pv.mjs,真正的画面/配音在 lib/pv.py 与 lib/pv_plan.py
119
121
  import { createPv, pvYesterday, pvDateOk } from './lib/pv.mjs'
120
122
  import { createVideoStudio, migrateVideoSettings } from './lib/pv-session.mjs'
123
+ // 🔄 更新通道(2026-10-30 使用者「加一个更新通道,方便及时更新」)——
124
+ // ⚠️ 2026-10-21 的「一键升级」曾把本机分叉整包换成 registry 版本(m17605),
125
+ // 所以 lib/update.mjs 里先判安装方式:只有 profile 下的 registry 安装才允许自更新。
126
+ import {
127
+ DEFAULT_REGISTRY, MIRROR_REGISTRY, PACKAGE_NAME, buildUpdateReport, checkForUpdate,
128
+ detectInstallMode, summarizeUpgrade, upgradeBlocker, upgradeCommand, upgradeLogLine,
129
+ } from './lib/update.mjs'
121
130
  import { buildSevenDayRetrospective, buildRecoverySnapshot } from './lib/product-status.mjs'
122
131
 
123
132
  export const name = 'dafeiyu'
@@ -153,6 +162,8 @@ export const Config = !z ? undefined : z.object({
153
162
  stickerCapacity: z.natural().default(STICKER_DEFAULTS.stickerCapacity).extra('volatile', true),
154
163
  /** 新图先经本人批准;关闭只影响以后收的新图,不放行仍待审核的图片。 */
155
164
  stickerReview: z.boolean().default(STICKER_DEFAULTS.stickerReview).extra('volatile', true),
165
+ /** 收进库之前让模型判一次「这张当表情值不值得收」;关掉就只走本地词表,且不产生淘汰用的分数。 */
166
+ stickerJudge: z.boolean().default(STICKER_DEFAULTS.stickerJudge).extra('volatile', true),
156
167
  /** ⭐ 私信投递:off=只发评论(现状)· only=只私信(完全不公开评论)· both=评论 + 私信 */
157
168
  dmMode: z.string().default('off').extra('volatile', true),
158
169
  /** ⭐ 私信发给谁的 UID(数字)。要在 dmMode 不是 off 时填 */
@@ -236,6 +247,12 @@ export const Config = !z ? undefined : z.object({
236
247
  * 留空 = 自动(从 `out`/`digest` 那个 junction 反推出工作区,落在 `<工作区>\records`);
237
248
  * 填了就按填的来。**只复制,不动原件**;凭证 `bili_credential.json` 永远不进镜像。 */
238
249
  mirrorDir: z.string().default('').extra('volatile', true),
250
+ /** 🔄 更新通道(2026-10-30):查询用哪个 registry。
251
+ * 留空 = 自动(默认 npmmirror,国内快;`updateMirror:false` 才走 registry.npmjs.org)。
252
+ * ⚠️ 和别的自由字符串一样**不做白名单校验**,只在这里声明,避免 schema 拒收整条 entry。
253
+ * 真正使用前 `upgradeCommand()` 还会校验必须是 http(s) 地址。 */
254
+ updateRegistry: z.string().default('').extra('volatile', true),
255
+ updateMirror: z.boolean().default(true).extra('volatile', true),
239
256
  /** ⏱️ 出手节奏快慢系数(2026-10-21 使用者 m18802 第 1、2 条) ——
240
257
  * `1` = 标准;`2` = 间隔翻倍(更稳、更不容易撞风控);`0.5` = 快一倍。
241
258
  * ⚠️ 故意**不加 .min()/.max()**:schema 校验不过 ⇒ cordis 拒收这条 entry ⇒ 整条插件不激活
@@ -484,7 +501,8 @@ export function apply(ctx, config) {
484
501
  case 'bili_dm_inbox': return '跑去私信收件箱瞅瞅有没有人找我'
485
502
  case 'bili_dm_thread': return `翻翻和 ${String(arg.uid || '')} 的聊天记录`
486
503
  case 'bili_dm_reply': return `给 ${String(arg.uid || '')} 回一条私信`
487
- case 'bili_reply': return `在 ${bv} 下面冒个泡`
504
+ case 'bili_reply': return `在 ${bv} 的评论下面接话`
505
+ case 'bili_comment': return `在 ${bv} 下面留一句话`
488
506
  case 'bili_reply_opus': return `在动态 ${String(arg.opus_id || '')} 下面接话`
489
507
  case 'bili_moment': return '发一条说说~'
490
508
  case 'bili_danmaku': return `看 ${bv} 的弹幕`
@@ -550,7 +568,7 @@ export function apply(ctx, config) {
550
568
  keywords: [], commentPerRound: 1, mentionName: '', pythonPath: 'python', dshBin: '', headlessProfile: 'headless', overlayMcp: true, workspace: '', reportLevel: 'full', replyToComments: false, replyPerRound: 1,
551
569
  autoReplyDm: false, dmReplyPerDay: 0, dmReplyFollowedOnly: true,
552
570
  // 🐟 鲸鱼摸鱼模式(键必须和 Config 里的一模一样,normalize 是按 base 的键挑的)
553
- persona: '', focus: '', autoMoment: false, momentPerDay: 1, momentStyle: '随心情', interactionStyle: 'companion', modelId: '', tokenBudget: 0, totalTokenBudget: 0, maxRounds: 64, mirrorDir: '',
571
+ persona: '', focus: '', autoMoment: false, momentPerDay: 1, momentStyle: '随心情', interactionStyle: 'companion', modelId: '', tokenBudget: 0, totalTokenBudget: 0, maxRounds: 64, mirrorDir: '', updateRegistry: '', updateMirror: true,
554
572
  // ⏱️ 出手节奏系数(1 = 标准;2 = 间隔翻倍;0.5 = 快一倍)—— 2026-10-21 使用者 m18802 第 1、2 条
555
573
  paceFactor: 1,
556
574
  // 🎬 昨日冲浪报告视频(2026-10-22)
@@ -656,6 +674,9 @@ export function apply(ctx, config) {
656
674
  if ('modelId' in p) p.modelId = String(p.modelId ?? '').trim().slice(0, 200)
657
675
  // 📁 镜像目录:留空 = 自动(从 junction 反推);填了就是绝对路径,砍掉首尾空白、限制长度
658
676
  if ('mirrorDir' in p) p.mirrorDir = String(p.mirrorDir ?? '').trim().slice(0, 400)
677
+ // 🔄 更新通道:registry 自由填(真正使用前 upgradeCommand 会校验 http(s)),镜像开关只认布尔
678
+ if ('updateRegistry' in p) p.updateRegistry = String(p.updateRegistry ?? '').trim().slice(0, 300)
679
+ if ('updateMirror' in p) p.updateMirror = p.updateMirror !== false
659
680
  // ⏱️ 出手节奏系数:0.3–3(0 会让所有间隔归零 = 把节奏闸关掉,所以下限不给 0)
660
681
  if ('paceFactor' in p) p.paceFactor = clampNum('paceFactor', p.paceFactor, 1)
661
682
  // 🎬 视频:音色/时间点/目录都是自由字符串(只做长度与格式兜底,不做白名单 ——
@@ -942,7 +963,7 @@ export function apply(ctx, config) {
942
963
  }
943
964
  assertScope()
944
965
  signal?.throwIfAborted()
945
- 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)
946
967
  const reviewed = sending ? approvedReviewExecutions.get(toolExecution.getStore()) : null
947
968
  if (sending) {
948
969
  for (const file of [cfgFile, dutyStateFile]) {
@@ -996,12 +1017,42 @@ export function apply(ctx, config) {
996
1017
  if (!scope) { scope = new AbortController(); stickerSessionScopes.set(key, scope) }
997
1018
  return exec.signal ? AbortSignal.any([exec.signal, scope.signal]) : scope.signal
998
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
+ }
999
1036
  const stickerAutomation = createStickerAutomation({
1000
1037
  stateDir,
1001
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
+ },
1002
1050
  onEvent: (kind, value) => {
1003
- if (kind === 'collected') act('note', `收了一张表情「${value.name}」${value.status === 'pending' ? ',等你审核后再用' : ''}`, { bvid: value.bvid })
1004
- else log(`表情自动化(${kind}):${value.error || ''}`)
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)
1005
1056
  },
1006
1057
  })
1007
1058
  // Only the manual gallery explicitly opts into pending. Public/tool defaults remain sendable-only.
@@ -1069,7 +1120,7 @@ export function apply(ctx, config) {
1069
1120
  const browsingTools = new Set(['bili_search', 'bili_subtitle', 'bili_danmaku', 'bili_buzzwords',
1070
1121
  'bili_video_info', 'bili_rank', 'bili_hot_videos', 'bili_user_videos', 'bili_comments',
1071
1122
  'bili_received_replies', 'bili_my_notices', 'bili_my_dynamics', 'bili_opus_comments',
1072
- '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',
1073
1124
  'bili_dm_thread', 'bili_dm_reply', 'bili_moment', 'bili_deliver'])
1074
1125
  const tool = (spec) => {
1075
1126
  const wrapped = {
@@ -1079,7 +1130,7 @@ export function apply(ctx, config) {
1079
1130
  async execute(args, exec) {
1080
1131
  const manuallyApproved = exec && approvedReviewExecutions.has(exec)
1081
1132
  if (!manuallyApproved && !isFishExecution(exec)) return { ok: false, blocked: true, reason: 'mode_required', hint: 'DeepMoYu 工具仅在鲸鱼摸鱼模式内生效;普通会话不会启用角色工具或记忆。' }
1082
- 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)) {
1083
1134
  const unauthorized = modelForceDecision(args)
1084
1135
  if (unauthorized) return unauthorized
1085
1136
  }
@@ -2268,7 +2319,8 @@ export function apply(ctx, config) {
2268
2319
  perUser[String(uid)] = { day: today, n: base + 1 }
2269
2320
  writeJson(dutyStateFile, {
2270
2321
  ...st,
2271
- ...(crossDay ? { count: 0, picked: 0, windows: {} } : {}),
2322
+ // 跨天归零走统一名单(原先是手写的 `{count,picked,windows}`,漏了 moments)
2323
+ ...(crossDay ? dayResetPatch(today) : {}),
2272
2324
  day: today,
2273
2325
  dmReplied: replied,
2274
2326
  dmPerUser: perUser,
@@ -2428,9 +2480,19 @@ export function apply(ctx, config) {
2428
2480
  },
2429
2481
  })
2430
2482
 
2431
- tool({
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 = {
2432
2492
  name: 'bili_reply',
2433
- description: '在视频下发评论或回复某条评论。正文简短,按当前会话的语气自然接话。\n'
2493
+ description: '在视频下方**回复某一条已有的评论**(楼中楼/楼层回复)。正文简短,按当前会话的语气自然接话。\n'
2494
+ + '⭐ **想给这条视频留言、想让更多人看见 ⇒ 用 `bili_comment`(发新评论),不要用这只。**'
2495
+ + '这只只在"有人先跟你说话了、你要接他那一句"时用:楼层回复只有对方和点进楼里的人看得到。\n'
2434
2496
  + '回复别人的评论时:`rpid` 填他那条的 rpid,`root` 填你自己那条主评论的 rpid(`bili_received_replies` 的 '
2435
2497
  + '`how_to_reply` 直接照抄就行)。没有条数上限,但**回过的不再回**(宿主记着账本)。\n'
2436
2498
  + '✍️ **内容要有东西,不是为发而发**:光喊「awsl / 太强了 / 哈哈哈哈」这类话会被打回 —— '
@@ -2452,11 +2514,14 @@ export function apply(ctx, config) {
2452
2514
  picture: { type: 'string', description: '(可选,仅发新评论)表情图名字,最多 2 张、用逗号或空格分隔;bili_sticker({action:"list"}) 查;回复楼层不支持图片' },
2453
2515
  force: { type: 'boolean', description: '已停用;true 会被拒绝。模型参数不能代表本人授权,请通过工作台手动设置权限并审核。' },
2454
2516
  },
2455
- async execute(args) {
2517
+ /* `presetRpid` 是 `bili_comment` 用的:传 '0' 就锁定"发新评论"这条分支。
2518
+ ⚠️ 必须复用**同一个 args 对象**(不要 `{...args}`)—— 审批路径靠 `approvedReviewArgs` 这个
2519
+ WeakMap 认身份,复制一份就等于把"预览时那张图的字节指纹"丢了。 */
2520
+ async execute(args, exec, presetRpid) {
2456
2521
  const bvid = String(args.bvid ?? '').trim()
2457
- const rpid = optionalId(args.rpid)
2522
+ const rpid = presetRpid === undefined ? optionalId(args.rpid) : String(presetRpid)
2458
2523
  const isReply = rpid !== '0'
2459
- const root = optionalId(args.root)
2524
+ const root = isReply ? optionalId(args.root) : '0'
2460
2525
  /* 🔴 2026-10-20:模型拿着「动态(opus)下面的评论」来调这只 —— 那儿根本没有 BV 号。
2461
2526
  * 原来不校验,直接往下传,MCP 抛「bvid 提供错误,必须是以 BV 开头的…12 位字符串」,
2462
2527
  * 报错暴露在流水里像插件坏了;更要紧的是它会被当成"这条回不了"而放弃,
@@ -2725,6 +2790,39 @@ export function apply(ctx, config) {
2725
2790
  if (riskNote && out && typeof out === 'object') return { ...out, riskNote }
2726
2791
  return out
2727
2792
  },
2793
+ }
2794
+ // 回复通道(楼中楼):只在"有人先跟你说话"时用
2795
+ tool(replyTool)
2796
+
2797
+ /* 🗣️ 发新评论(= 直接发在视频下面,所有人点开视频都能看见)——
2798
+ * 使用者 m00002 第 2 条要的就是这个动作,所以把它独立成一只**没有 rpid/root 参数**的工具:
2799
+ * 模型想留言时不必再拐进"回复某人"的语义里,参数面也不给它走错的机会。 */
2800
+ tool({
2801
+ name: 'bili_comment',
2802
+ description: '🗣️ **在视频下方发一条新评论** —— 直接挂在视频下面,点开视频的人都能看见。'
2803
+ + '想让更多人看到你的话,就是用这只(不要用 bili_reply 去回复别人,那只有对方看得见)。\n'
2804
+ + '什么时候发:看完这条视频真有话说就发(哪一下、哪一秒、哪个说法打动你,或者你不同意哪儿)。'
2805
+ + '**没有想说的就不发 —— 配额不是催办;同一条视频只发一条新评论**(发过就换下一条视频)。\n'
2806
+ + '✍️ **内容要有东西,不是为发而发**:光喊「awsl / 太强了 / 哈哈哈哈」这类话会被打回。'
2807
+ + '按当前会话的语气自然说话,不要求固定词、表情或自称。\n'
2808
+ + '🖼️ 想配图就传 `picture`(表情库里的名字,`bili_sticker({action:"list"})` 查,最多 2 张);'
2809
+ + '小表情码如 [doge] 直接写进正文就行,不算图。\n'
2810
+ + '⏱️ 发之前先 `bili_pace({kind:"comment"})` 问一句 —— 说等就等,别试探性重发(那是 -352 的最短路径)。\n'
2811
+ + '🔴 安全闸:桌宠设置「自动化」里的「自动评论视频」**默认是关的**;关着的时候这个工具会拒绝 —— '
2812
+ + '需要本人在工作台手动打开权限;模型不能用 force 或配置 PATCH 绕过。\n'
2813
+ + '📌 动态(opus)下面没有 BV 号,那种要用 `bili_reply_opus`。',
2814
+ parameters: {
2815
+ bvid: { type: 'string', description: 'BV号(视频下方发评论)' },
2816
+ text: { type: 'string', description: '评论正文(短、干净、像真人在说话;别写敏感词)' },
2817
+ picture: { type: 'string', description: '(可选)表情图名字,最多 2 张、用逗号或空格分隔;bili_sticker({action:"list"}) 查' },
2818
+ force: { type: 'boolean', description: '已停用;true 会被拒绝。模型参数不能代表本人授权,请通过工作台手动设置权限并审核。' },
2819
+ },
2820
+ async execute(args) {
2821
+ // 复用回复工具的实现:`presetRpid='0'` 强制走"发新评论"分支,
2822
+ // 质量/判重/节奏/审核/风控闸全都是同一套,不另起一条会跑偏的路。
2823
+ // ⚠️ 传**原对象**(不是副本):审批路径用 `approvedReviewArgs` WeakMap 认身份。
2824
+ return replyTool.execute(args, undefined, '0')
2825
+ },
2728
2826
  })
2729
2827
 
2730
2828
  tool({
@@ -2943,6 +3041,7 @@ export function apply(ctx, config) {
2943
3041
  if (text.length > 300) return { ok: false, error: `正文 ${text.length} 字,B站动态上限约 300 字,先砍短` }
2944
3042
  const gateOn = Boolean(cfg.autoMoment)
2945
3043
  if (!gateOn && args?.force !== true) {
3044
+ log('🚫 说说被拦:工作台「自动发说说」未开启')
2946
3045
  return {
2947
3046
  ok: false, blocked: true,
2948
3047
  hint: '工作台中的「自动发说说」未开启,请本人手动打开。模型不能通过 force 或配置 PATCH 自授权限。',
@@ -2953,9 +3052,13 @@ export function apply(ctx, config) {
2953
3052
  const cap = Number.isFinite(capNum) ? Math.max(0, capNum) : 1
2954
3053
  const used = countMomentsToday()
2955
3054
  if (cap <= 0 && args?.force !== true) {
3055
+ // ⚠️ 2026-11-02:以前这几种拦截**只回给模型、不写日志** —— 于是 duty.log 里
3056
+ // "动态怎么不发了"完全查不到线索(连一条 blocked 都没有)。拦截也是事件,必须留痕。
3057
+ log(`🚫 说说被拦:工作台的每日上限是 0(不发)`)
2956
3058
  return { ok: false, blocked: true, hint: '工作台中的动态每日上限是 0(不发),请本人手动调整;模型不能绕过额度。' }
2957
3059
  }
2958
3060
  if (used >= cap && args?.force !== true) {
3061
+ log(`🚫 说说被拦:今天已发 ${used}/${cap} 条(额度满),明天再聊`)
2959
3062
  return { ok: false, blocked: true, hint: `今天已经发了 ${used}/${cap} 条说说了,明天再聊。` }
2960
3063
  }
2961
3064
  /* ⏱️ 出手节奏(2026-10-21 m18802 第 1、2 条):发动态是最"显眼"的动作 ——
@@ -3083,7 +3186,7 @@ export function apply(ctx, config) {
3083
3186
  * `sendReview` 只对鱼模式的自动发送生效。默认关闭,旧发送语义不变。
3084
3187
  * 队列落在 state.json,重启后仍能看到待处理内容;批准时重新走原工具,
3085
3188
  * 进程内 WeakMap 只通过预览排队这一层,原开关、质量、判重、节奏和风控闸都不跳过。 */
3086
- const SEND_REVIEW_TOOLS = new Set(['bili_reply', 'bili_reply_opus', 'bili_dm_reply', 'bili_moment', 'bili_deliver'])
3189
+ const SEND_REVIEW_TOOLS = new Set(['bili_reply', 'bili_comment', 'bili_reply_opus', 'bili_dm_reply', 'bili_moment', 'bili_deliver'])
3087
3190
  const SEND_REVIEW_MAX = 80
3088
3191
  const approvedReviewArgs = new WeakMap()
3089
3192
  const approvedReviewExecutions = new WeakMap()
@@ -3142,11 +3245,13 @@ export function apply(ctx, config) {
3142
3245
  try { clean = validateToolArguments(toolInputSchema(backgroundToolRegistry.get(toolName)?.parameters || {}), reviewArgs(args)) }
3143
3246
  catch (error) { return { ok: false, blocked: true, error: String(error?.message ?? error) } }
3144
3247
  if (JSON.stringify(clean).length > 16000) return { ok: false, blocked: true, error: '预览参数过长,未保存内容' }
3145
- if (toolName === 'bili_reply' && !Object.hasOwn(clean, 'picture')) clean.picture = ''
3248
+ // `bili_comment` 是 `bili_reply` 的"只发新评论"通道,picture 语义完全一致,一起走这段
3249
+ const pictureTool = toolName === 'bili_reply' || toolName === 'bili_comment'
3250
+ if (pictureTool && !Object.hasOwn(clean, 'picture')) clean.picture = ''
3146
3251
  const now = Date.now()
3147
3252
  const id = reviewIdOf(toolName, clean, now)
3148
3253
  const attachments = []
3149
- if (toolName === 'bili_reply' && clean.picture) {
3254
+ if (pictureTool && clean.picture) {
3150
3255
  const names = String(clean.picture).split(/[,,、]+|\s+/).filter(Boolean)
3151
3256
  if (names.length > 2) return { ok: false, blocked: true, error: '发送预览最多选择 2 张表情' }
3152
3257
  for (const name of names) {
@@ -3385,8 +3490,10 @@ export function apply(ctx, config) {
3385
3490
  */
3386
3491
  const bumpState = (patch) => withStateLock(() => {
3387
3492
  const st = readJson(dutyStateFile, {})
3388
- const today = dayKey()
3389
- if (st.day !== today) { st.day = today; st.count = 0; st.picked = 0; st.windows = {}; st.tokensToday = 0 }
3493
+ // ⚠️ 跨天归零只走 lib/state.mjs 那一份名单(`resetDayCounters`)——
3494
+ // 这里原先手写 `count/picked/windows/tokensToday`,**漏了 moments**,
3495
+ // 导致跨天后 `moments` 还是昨天的数 ⇒ `bili_moment` 静默判定"今天额度满了"、永不再发(2026-11-02 的事故)。
3496
+ if (st.day !== dayKey()) resetDayCounters(st)
3390
3497
  writeJson(dutyStateFile, { ...st, ...(typeof patch === 'function' ? patch(st) : patch) })
3391
3498
  return st
3392
3499
  })
@@ -3773,7 +3880,105 @@ export function apply(ctx, config) {
3773
3880
  return { command: 'dsh', args: [], shell: true, fallback: true, hint }
3774
3881
  }
3775
3882
 
3776
- // Only the explicitly scheduled fish worker receives this prompt.
3883
+ /* ═══════════════ 🔄 更新通道(2026-10-30 使用者「加一个更新通道,方便及时更新」)═══════════════
3884
+ * ⚠️ **这一节有一道不可删的闸**:2026-10-21 的「一键升级」直接跑 `dsh plugin add`,
3885
+ * 把本机「目录联接 → 源码」的分叉整包换成了 registry 版本,改动全没(m17605,恢复花一整轮)。
3886
+ * 所以顺序永远是:**先判安装方式 → 再判有没有在跑 → 才允许升级**;
3887
+ * 本机链接/源码/未知安装一律只检查、不升级,把原因明说给使用者。
3888
+ * 检查走 npm registry 的 packument(只读、有超时和体积上限),不碰 GitHub API 限额。
3889
+ */
3890
+ let upgrading = false
3891
+ const updateLogFile = join(stateDir, 'update.log')
3892
+ const appendUpdate = (line) => { try { appendFileSync(updateLogFile, line + '\n', 'utf8') } catch { /* 写不进去也不影响升级 */ } }
3893
+ /** 上一次检查结果(界面轮询 /status 时复用,不重复联网);10 分钟内有效。 */
3894
+ let updateCache = { at: 0, value: null }
3895
+ let updateCheckedAt = 0
3896
+ const updateRegistry = () => {
3897
+ const explicit = String(cfg.updateRegistry || '').trim()
3898
+ if (explicit) return explicit
3899
+ // 中国网络:默认按 npmmirror 查,避免 registry.npmjs.org 慢到 8 秒超时
3900
+ return String(cfg.updateMirror === false ? DEFAULT_REGISTRY : MIRROR_REGISTRY)
3901
+ }
3902
+ const updateBusy = () => ({
3903
+ installing,
3904
+ asr: installingAsr,
3905
+ feiyu: Boolean(feiyuLive?.size),
3906
+ background: Boolean(backgroundRun?.active),
3907
+ video: Boolean(pv.status()?.running),
3908
+ })
3909
+ const runUpdateCheck = async ({ force = false } = {}) => {
3910
+ if (!force && updateCache.value && Date.now() - updateCache.at < 10 * 60_000) return updateCache.value
3911
+ let report
3912
+ try {
3913
+ report = await checkForUpdate({
3914
+ current: myVersion(),
3915
+ pluginDir,
3916
+ dshHome: String(ctx?.get?.('dshHomePath')?.('') || process.env.DSH_HOME || ''),
3917
+ registry: updateRegistry(),
3918
+ packageName: PACKAGE_NAME,
3919
+ })
3920
+ } catch (error) {
3921
+ // 检查自己坏了也不能让设置页 500 —— 如实说"查不了"
3922
+ report = { ...buildUpdateReport({ current: myVersion(), published: null, install: detectInstallMode({ pluginDir, dshHome: String(ctx?.get?.('dshHomePath')?.('') || process.env.DSH_HOME || '') }), registry: updateRegistry(), error: String(error?.message || error) }), code: 'CHECK_FAILED' }
3923
+ }
3924
+ updateCache = { at: Date.now(), value: report }
3925
+ updateCheckedAt = Date.now()
3926
+ return report
3927
+ }
3928
+ const updateStatus = () => ({
3929
+ current: myVersion(),
3930
+ ...(updateCache.value || {}),
3931
+ upgrading,
3932
+ checkedAt: updateCheckedAt || 0,
3933
+ })
3934
+ const startUpgrade = async () => {
3935
+ const report = updateCache.value || await runUpdateCheck()
3936
+ const blocker = upgradeBlocker({ report, busy: updateBusy(), upgrading })
3937
+ // ⚠️ 顺序不能反:`updateStatus()` 里带着检查结果的 `ok:true`,
3938
+ // 必须把它展开在**前面**,再写本次升级的 ok/code/error,否则失败会被覆盖成成功。
3939
+ if (blocker) return { ...updateStatus(), ok: false, code: 'UPGRADE_BLOCKED', error: blocker }
3940
+ const dsh = dshCliCached()
3941
+ const command = upgradeCommand({
3942
+ dshCli: dsh, profile: report.install?.profile, packageName: PACKAGE_NAME,
3943
+ electron: Boolean(process.versions.electron), registry: report.registry,
3944
+ })
3945
+ if (command.ok === false) return { ...updateStatus(), ok: false, code: command.code, error: command.error }
3946
+ upgrading = true
3947
+ appendUpdate(upgradeLogLine({ phase: 'start', detail: `${report.current} → ${report.latest}(profile ${command.profile})` }))
3948
+ // 升级要几十秒,HTTP 不能一直挂着:起完就返回,进度看 /update?rev 或日志
3949
+ const child = spawn(command.command, command.args, {
3950
+ cwd: stateDir, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'],
3951
+ env: uvEnv({ ...command.env, BILI_DUTY_HOME: stateDir }),
3952
+ })
3953
+ const chunks = []
3954
+ const collect = (buf) => { if (chunks.length < 400) chunks.push(String(buf)); appendUpdate(upgradeLogLine({ phase: 'out', detail: String(buf).trim() })) }
3955
+ child.stdout?.on('data', collect)
3956
+ child.stderr?.on('data', collect)
3957
+ let finished = false
3958
+ const timer = setTimeout(() => { if (!finished) { try { child.kill() } catch { /* 已经退出 */ } } }, 5 * 60_000)
3959
+ timer?.unref?.()
3960
+ child.on('error', (error) => {
3961
+ if (finished) return
3962
+ finished = true; clearTimeout(timer); upgrading = false
3963
+ const result = summarizeUpgrade({ code: -1, output: String(error?.message || error) })
3964
+ appendUpdate(upgradeLogLine({ phase: 'error', detail: result.message }))
3965
+ log(`🔄 升级进程起不来:${result.message}`)
3966
+ })
3967
+ child.on('close', (code) => {
3968
+ if (finished) return
3969
+ finished = true; clearTimeout(timer); upgrading = false
3970
+ const result = summarizeUpgrade({ code: Number(code ?? -1), output: chunks.join(''), latest: report.latest })
3971
+ appendUpdate(upgradeLogLine({ phase: result.ok ? 'done' : 'error', detail: result.message }))
3972
+ log(`🔄 升级${result.ok ? '完成' : '失败'}:${result.message}`)
3973
+ act('note', '更新通道:' + result.message)
3974
+ // 安装目录变了 ⇒ 旧结果作废,下一次检查重新问 registry
3975
+ updateCache = { at: 0, value: null }
3976
+ })
3977
+ return { ok: true, started: true, from: report.current, to: report.latest, profile: command.profile, hint: '升级在后台进行,完成后需要完全重启 DSH', ...updateStatus() }
3978
+ }
3979
+ ctx.effect(() => () => { updateCache = { at: 0, value: null } }, 'bili-duty: 收更新检查缓存')
3980
+
3981
+
3777
3982
  // 🐾 2026-10-26 桌宠化改造:签名加 opts —— 桌宠那条 /pet/start 会把「这一轮的 token 预算」
3778
3983
  // 递进来(headless 子进程没有鲸鱼摸鱼那套 token 保险丝,预算只能写进提示词让模型自己见好就收)。
3779
3984
  // 现有调用者不传参(opts.tokenBudget 为空 ⇒ 一行都不多),行为与从前完全一致。
@@ -5076,30 +5281,43 @@ export function apply(ctx, config) {
5076
5281
  } catch { return { ok: false, hh: 0, mm: 0, wd: '', part: '', text: '', line: '' } }
5077
5282
  }
5078
5283
 
5079
- /** 今天发了几条说说(从 state.json 数;宿主记账,不靠模型自觉) */
5284
+ /** 今天发了几条说说(从 state.json 数;宿主记账,不靠模型自觉)
5285
+ * ⚠️ 2026-11-02 加固:`moments` 只跟 `st.day` 绑在一起是不够的 —— 只要有**任何**一条跨天写路径
5286
+ * 没重置它(当时 `bumpState`、私信记账就都漏了),今天就会读到昨天的数、然后永久"额度已满"。
5287
+ * 现在再加一道自证的 `momentsDay`:记账时一起写,读的时候对不上就按 0 算、顺手把陈数清掉。
5288
+ */
5080
5289
  const countMomentsToday = (now = new Date()) => {
5081
5290
  const st = readJson(dutyStateFile, {})
5082
- return st.day === dayKey(now) ? Number(st.moments || 0) : 0
5291
+ const today = dayKey(now)
5292
+ if (st.day !== today) return 0
5293
+ // 老状态没有 momentsDay:那 `moments` 可能是翻篇前留下的陈数 —— 只有在 day 也不对时才可疑,
5294
+ // 这里 day 已经对上了,就信 `momentsDay`;没有 momentsDay 时退回看 lastMomentAt 是不是今天。
5295
+ const stamp = typeof st.momentsDay === 'string' ? st.momentsDay : ''
5296
+ if (stamp) return stamp === today ? Number(st.moments || 0) : 0
5297
+ const at = Date.parse(String(st.lastMomentAt || ''))
5298
+ if (Number.isFinite(at) && dayKey(new Date(at)) !== today) return 0
5299
+ return Number(st.moments || 0)
5083
5300
  }
5084
5301
  /**
5085
5302
  * 记说说账(delta 可正可负)。⚠️ 走 stateLock(`bili_moment` 里是 await 发出去之后才记账,中间能插进别的写)。
5086
5303
  * ⭐ 2026-10-26 改成可加可减:`bili_moment` 现在**发送前先在锁内预占一个名额**(并发不会再双发突破上限),
5087
5304
  * 发送失败再用 -1 把名额退回去;force(不占额)成功后仍走 +1 补记。
5088
- * ⚠️ 跨天时必须连 count/picked/windows 一起重置:`bumpState` 有这段重置,这里原来没有,
5089
- * 结果是「跨天后发一条说说 ⇒ day 变成今天但 picked 还是昨天的数 ⇒
5090
- * countPickedToday() 把昨天的额度算到今天头上 ⇒ 今天一开局就"额度已满"」。
5305
+ * ⚠️ 跨天时必须连 count/picked/windows/moments 一起重置 —— 现在统一走 lib/state.mjs 的 `dayResetPatch`,
5306
+ * 那边是**唯一**的当日计数名单,别再在这里手写一份(手写漏字段就是 2026-11-02 那次事故)。
5091
5307
  */
5092
5308
  const bumpMomentBy = (delta = 1) => withStateLock(() => {
5093
5309
  const st = readJson(dutyStateFile, {})
5094
5310
  const today = dayKey()
5095
- const crossDay = st.day !== today
5096
- const base = crossDay ? 0 : Number(st.moments || 0)
5311
+ // 陈数(没有 momentsDay 或对不上今天)一律当 0 起算,绝不把昨天的额度带进今天
5312
+ const stale = st.momentsDay !== today || st.day !== today
5313
+ const base = stale ? 0 : Number(st.moments || 0)
5097
5314
  const next = Math.max(0, base + delta)
5098
5315
  writeJson(dutyStateFile, {
5099
5316
  ...st,
5100
- ...(crossDay ? { count: 0, picked: 0, windows: {}, moments: 0, tokensToday: 0 } : {}),
5317
+ ...(st.day !== today ? dayResetPatch(today) : {}),
5101
5318
  day: today,
5102
5319
  moments: next,
5320
+ momentsDay: today,
5103
5321
  lastMomentAt: new Date().toISOString(),
5104
5322
  })
5105
5323
  return next
@@ -6963,6 +7181,8 @@ export function apply(ctx, config) {
6963
7181
  'backgroundWindowStart', 'backgroundWindowEnd',
6964
7182
  // 📁 记录镜像目录(留空 = 自动)
6965
7183
  'mirrorDir',
7184
+ // 🔄 更新通道(2026-10-30)—— 漏表 = 设置页改了却存不上
7185
+ 'updateRegistry', 'updateMirror',
6966
7186
  // 🎬 视频日报那组(clampPatch 都逐个处理了,这里不认就永远存不上)
6967
7187
  'pvAuto', 'pvAt', 'pvVoice', 'pvDir',
6968
7188
  // 制作整条视频的模型;旧 pvModelId 只在读取配置时迁移。
@@ -7254,18 +7474,29 @@ export function apply(ctx, config) {
7254
7474
  return sendJson(res, 200, result)
7255
7475
  }
7256
7476
 
7257
- /* 🚫 2026-10-21(使用者 m18802 第 4 条「把这个插件封装一下,改成一个新插件,删除掉他的更新」):
7258
- * 这里原来有两条路由 ——
7259
- * POST /api/bili-duty/upgrade 一键升级(宿主自己跑 `dsh plugin add`)
7260
- * GET /api/bili-duty/update 检查更新(问 npm + GitHub 要最新版)
7261
- * **都删了**,理由不是"懒得维护":
7262
- * · 那个按钮会把整个 `node_modules\dsh-deepmoyu` 重装成官方原版 ——
7263
- * 我们所有改动(判重、学习、镜像、节奏、鲸鱼摸鱼人格)**全在包目录里**,一次点击全没。
7264
- * 这不是假设:2026-10-21 就真发生过一次(m17605),恢复花了整整一轮。
7265
- * · 使用者明确要求「删除掉他的更新」⇒ 这不是"默认关掉",是**入口都不留**。
7266
- * 以后要升级请用插件自己的快照(`devtools\backups\plugin-ours-*`)手动覆盖,
7267
- * 不要再用任何"自动更新"。
7268
- */
7477
+ /* 🔄 更新通道(2026-10-30 使用者「加一个更新通道,方便及时更新」;取代 2026-10-21 删掉的那两条)
7478
+ *
7479
+ * 2026-10-21 删掉的是 `POST /upgrade`(宿主直接跑 `dsh plugin add`)+ `GET /update`(问 npm/GitHub),
7480
+ * 因为那次点击把本机「目录联接 → 源码」的分叉整包换成了 registry 版本(m17605)。
7481
+ * 现在恢复,但**顺序反过来**:先由 lib/update.mjs 判定这份安装到底是不是 registry 安装,
7482
+ * 不是就只回检查结果 + 原因,canUpgrade=false,前端连按钮都禁用。
7483
+ * 另外两个闸:有刷站/后台/渲染在跑时拒绝;同一时间只允许一个升级进程。 */
7484
+ if (req.method === 'GET' && path === '/api/bili-duty/update') {
7485
+ const force = url.searchParams.get('force') === '1'
7486
+ const report = await runUpdateCheck({ force })
7487
+ const rev = String(Math.round(Number(updateCheckedAt) / 1000)) + ':' + (report.latest || '') + ':' + (upgrading ? 'u' : '-') + ':' + myVersion()
7488
+ if (url.searchParams.get('rev') === rev) return sendJson(res, 200, { ok: true, rev, unchanged: true })
7489
+ return sendJson(res, 200, { ...updateStatus(), rev, blocker: upgradeBlocker({ report, busy: updateBusy(), upgrading }) })
7490
+ }
7491
+ if (req.method === 'POST' && path === '/api/bili-duty/upgrade') {
7492
+ const body = await readBody(req)
7493
+ // 只接受"确认升级"这一个动作;任何额外字段(目标版本、路径、参数)一律忽略,
7494
+ // 避免把这条路由变成可执行任意 dsh 命令的入口。
7495
+ if (body && typeof body === 'object' && body.confirm !== true) {
7496
+ return sendJson(res, 400, { ok: false, code: 'UPGRADE_CONFIRM_REQUIRED', error: '升级需要显式确认(confirm: true)' })
7497
+ }
7498
+ return sendJson(res, 200, await startUpgrade())
7499
+ }
7269
7500
 
7270
7501
  // 「测一下」:真跑一次,回答"能不能用"
7271
7502
  if (req.method === 'POST' && path === '/api/bili-duty/check') {
@@ -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')
@@ -82,8 +82,22 @@ export function createModelRelay({ getLlm, onError } = {}) {
82
82
  return jsonReply(res, 200, { value })
83
83
  } finally { release() }
84
84
  }
85
- if (grant.busy) throw fail('同一后台任务已有模型请求进行中', 'RELAY_BUSY')
86
- grant.busy = true; modelRequest = true
85
+ /* 🔴 2026-11-02 修:以前 `resolve` 也抢 `grant.busy`,于是「问一下这条线路解析成什么模型」和
86
+ * 紧跟着的流式请求会互相撞车 —— 谁后到谁拿 409 RELAY_BUSY。日志里 2026-10-30 两次、11-01 一次、
87
+ * 11-02 一次 learn-review 全是这么挂的(`后台进度(…,请求0):后台进程报告错误(RELAY_BUSY)`),
88
+ * 表现成"后台任务莫名其妙失败",可实际上一次模型都还没跑。
89
+ * 现在只有真正占着连接的长请求(stream)才拿 busy,而且撞车时先等一会儿再判,不一上来就失败:
90
+ * `resolve` 只是元数据查询,几十毫秒就回,并发几次不会互相干扰。 */
91
+ const isStream = body.action === 'stream'
92
+ if (isStream) {
93
+ const deadlineAt = Date.now() + 30_000
94
+ while (grant.busy) {
95
+ abort.signal.throwIfAborted()
96
+ if (Date.now() > deadlineAt) throw fail('上一个响应还没有结束,这次请求先没发出去;稍等再试', 'RELAY_BUSY')
97
+ await new Promise(resolve => { const timer = setTimeout(resolve, 50); timer.unref?.() })
98
+ }
99
+ grant.busy = true; modelRequest = true
100
+ }
87
101
  const llm = getLlm?.()
88
102
  if (typeof llm?.resolveModelInfo !== 'function' || typeof llm?.prepareCall !== 'function') throw fail('宿主模型服务尚未就绪,未切换到其他线路', 'RELAY_NO_LLM')
89
103
  const route = grant.route
@@ -137,7 +151,18 @@ export function createModelRelay({ getLlm, onError } = {}) {
137
151
  open(model, { expiresAt, onUsage, onEvent, tools = [] } = {}) {
138
152
  if (closed || startupError || !server.listening) throw fail('后台模型桥接尚未就绪,请稍后重试;不会自动换模型', 'RELAY_NOT_READY')
139
153
  const route = splitModelRoute(model), llm = getLlm?.()
140
- if (!llm?.listProviders?.().some(provider => provider.id === route.provider)) throw fail(`当前宿主也没有注册模型线路 ${route.provider},请先连接该线路;未切换其他模型`, 'NO_ADAPTER')
154
+ if (!llm?.listProviders?.().some(provider => provider.id === route.provider)) {
155
+ /* 📌 2026-11-02:这条错误在日志里出现过 5 次(10-27 两次、10-28、10-30、11-02),每次都是
156
+ * 后台模型写成 `buddy/...` 而宿主根本没连这个线路 —— 光说"没注册"用户不知道下一步做什么。
157
+ * 这里把宿主**当前真有哪些线路**列出来,照着一个填进设置就能开跑。 */
158
+ let have = []
159
+ try { have = (llm?.listProviders?.() || []).map(provider => String(provider?.id || '')).filter(Boolean) } catch { /* 保持空 */ }
160
+ throw fail(`后台模型线路「${route.provider}」在这个宿主里没有注册,任务没有开跑。`
161
+ + (have.length
162
+ ? `当前可用的线路是:${have.join('、')} —— 去工作台把「后台模型」改成其中之一,或先把这个线路连上。`
163
+ : '宿主目前一个模型线路都没连上,请先在原生设置里连好线路。')
164
+ + '不会自动换模型。', 'NO_ADAPTER')
165
+ }
141
166
  const token = randomBytes(32).toString('hex')
142
167
  const grant = { token, route, expiresAt: Number(expiresAt) || Date.now() + 45 * 60_000, onUsage, onEvent, tools: new Map(tools.map(tool => [tool.name, tool])), toolQueue: Promise.resolve(), busy: false, controllers: new Set() }
143
168
  grants.set(token, grant)
package/lib/persona.mjs CHANGED
@@ -36,6 +36,7 @@ import { captureMemorySnapshot } from './records.mjs'
36
36
  * 不是每句堆自称、呜呜和身体动作;引用例句会很快变成模板,所以这里故意只写规则。
37
37
  */
38
38
  export const DAFEIYU_REPLY_GUIDE = Object.freeze([
39
+ '想对一条视频说点什么,就发在视频下面(bili_comment)——那是发言,点开视频的人都看得见;楼层回复只有对方看得见,别拿它当默认开口方式。',
39
40
  '先接住对方或内容里最具体的那一下,再说自己的反应;不要先报身份、总结流程或夸一串空话。',
40
41
  '公开评论和回复通常一两句、短而有内容:可以好奇、吐槽、嘴硬或轻轻关心,反应要和眼前细节相称。',
41
42
  '回复别人时直接接他刚说的意思;私信先顾及对话上下文和对方情绪,不把私聊改写成公开播报。',