dsh-fish-soul 0.13.1 → 0.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/lib/browse.js +26 -2
- package/lib/gateway.js +8 -1
- package/lib/index.js +17 -0
- package/lib/routes.js +4 -3
- package/lib/social.js +12 -3
- package/package.json +2 -2
package/README.md
CHANGED
package/lib/browse.js
CHANGED
|
@@ -175,14 +175,32 @@ export class BiliBrowser {
|
|
|
175
175
|
} catch { return []; }
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
/** 最近搜过的词(最新的在前,最多 5 个),给提词当"别重复"的参考。 */
|
|
179
|
+
recentKeywords() {
|
|
180
|
+
try {
|
|
181
|
+
const raw = JSON.parse(getKv(this.db, 'bili.search.recent', '[]'));
|
|
182
|
+
return Array.isArray(raw) ? raw.map((x) => String(x ?? '')).filter((x) => x !== '').slice(0, 5) : [];
|
|
183
|
+
} catch { return []; }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** 记下这次搜的词,重复的挪到最前。 */
|
|
187
|
+
noteKeyword(word) {
|
|
188
|
+
const w = String(word ?? '').trim();
|
|
189
|
+
if (w === '') return;
|
|
190
|
+
const list = [w, ...this.recentKeywords().filter((x) => x !== w)].slice(0, 5);
|
|
191
|
+
try { setKv(this.db, 'bili.search.recent', JSON.stringify(list)); } catch { /* 记不住不影响搜索 */ }
|
|
192
|
+
}
|
|
193
|
+
|
|
178
194
|
/** 没填词表时,让她自己按最近看过的提一个搜索词(一次小模型调用,只输出几个字)。 */
|
|
179
195
|
async draftSearchKeyword() {
|
|
180
196
|
const titles = this.recentTitles(5);
|
|
181
197
|
if (titles.length === 0) return '';
|
|
182
198
|
const p = this.persona?.() ?? { name: '鱼' };
|
|
199
|
+
const recent = this.recentKeywords();
|
|
183
200
|
const prompt = [
|
|
184
201
|
`你是「${p.name}」。你最近刷到这些视频:`,
|
|
185
202
|
...titles.map((t) => `- ${t}`),
|
|
203
|
+
...(recent.length > 0 ? ['', `你最近已经搜过:${recent.join('、')}。这次换个方向,别重复。`] : []),
|
|
186
204
|
'',
|
|
187
205
|
'你现在想在 B站 搜一个新词,找点新鲜的。只输出一个 2-8 字的搜索词,不要解释、不要引号、不要标点。'
|
|
188
206
|
].join('\n');
|
|
@@ -200,8 +218,14 @@ export class BiliBrowser {
|
|
|
200
218
|
if (this.searchedToday() >= Number(c.searchPerDay ?? 2)) return '';
|
|
201
219
|
const words = (Array.isArray(c.searchKeywords) ? c.searchKeywords : [])
|
|
202
220
|
.map((w) => String(w ?? '').trim()).filter((w) => w !== '');
|
|
203
|
-
if (words.length > 0)
|
|
204
|
-
|
|
221
|
+
if (words.length > 0) {
|
|
222
|
+
const picked = words[Math.floor(Math.random() * words.length)];
|
|
223
|
+
this.noteKeyword(picked);
|
|
224
|
+
return picked;
|
|
225
|
+
}
|
|
226
|
+
const drafted = await this.draftSearchKeyword();
|
|
227
|
+
if (drafted !== '') this.noteKeyword(drafted);
|
|
228
|
+
return drafted;
|
|
205
229
|
}
|
|
206
230
|
|
|
207
231
|
promptFor(v, like, share = false, favorite = false) {
|
package/lib/gateway.js
CHANGED
|
@@ -329,7 +329,14 @@ export class ChatGateway {
|
|
|
329
329
|
try {
|
|
330
330
|
const dir = join(workspacePath, '_stickers');
|
|
331
331
|
if (!existsSync(dir)) return [];
|
|
332
|
-
|
|
332
|
+
const all = readdirSync(dir).filter((f) => IMAGE_EXT.has(extname(f).toLowerCase()));
|
|
333
|
+
// 超过 40 张就随机抽 40:固定取前 40 个会让后面加的表情永远进不了提示词
|
|
334
|
+
if (all.length <= 40) return all;
|
|
335
|
+
for (let i = 0; i < 40; i++) {
|
|
336
|
+
const j = i + Math.floor(Math.random() * (all.length - i));
|
|
337
|
+
const t = all[i]; all[i] = all[j]; all[j] = t;
|
|
338
|
+
}
|
|
339
|
+
return all.slice(0, 40);
|
|
333
340
|
} catch {
|
|
334
341
|
return [];
|
|
335
342
|
}
|
package/lib/index.js
CHANGED
|
@@ -535,11 +535,27 @@ async function startup(ctx, config, { log, gate }) {
|
|
|
535
535
|
return out;
|
|
536
536
|
}
|
|
537
537
|
|
|
538
|
+
/**
|
|
539
|
+
* #40 systemPrompt 只该注给鱼的会话:我们的网关会话(QQ / B站 / 抖音)、或 DSH 里认得出人的会话。
|
|
540
|
+
* 宿主自己的技术会话整块不给——以前全局注册把「此刻状态 + 人格」漏进所有对话,装了插件 DSH 就没法当技术助手用。
|
|
541
|
+
* 宿主没给会话 id 时按鱼的会话处理(别把鱼搞哑),给了但认不出 / 是匿名 dsh 会话就不注。
|
|
542
|
+
*/
|
|
543
|
+
function injectsFor(arg) {
|
|
544
|
+
const ids = [arg?.agent?.id, arg?.agent?.session?.id, arg?.scope?.id, arg?.session?.id, arg?.sessionId];
|
|
545
|
+
const found = ids.map((x) => String(x ?? '')).filter((x) => x !== '');
|
|
546
|
+
if (found.length === 0) return true;
|
|
547
|
+
return found.some((id) => {
|
|
548
|
+
const session = sessionFor(id);
|
|
549
|
+
return session !== undefined && (session.channel !== 'dsh' || (session.speakers ?? []).length > 0);
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
538
553
|
/**
|
|
539
554
|
* 同步组装本轮瞬态块:BM25 当场算(本轮消息,零滞后),向量排名若已为同一 query 预取完成则一起融合。
|
|
540
555
|
* 多人合批时按人分别召回,token 预算平分。宿主传入 { agent, scope },按 agent.id 取本会话的上下文。
|
|
541
556
|
*/
|
|
542
557
|
function buildContextBlock(arg) {
|
|
558
|
+
if (injectsFor(arg) === false) return ''; // #40 不是鱼的会话:整块不给
|
|
543
559
|
const session = sessionFor(arg?.agent?.id ?? arg?.scope?.id ?? arg?.sessionId);
|
|
544
560
|
const lines = [stateLine()];
|
|
545
561
|
if (session === undefined) return lines.join('\n');
|
|
@@ -842,6 +858,7 @@ async function startup(ctx, config, { log, gate }) {
|
|
|
842
858
|
personaText: () => persona.staticText,
|
|
843
859
|
buildContextBlock,
|
|
844
860
|
noteInteraction,
|
|
861
|
+
isFishSession: injectsFor, // #40 不是鱼的会话不注人格 / 瞬态块
|
|
845
862
|
log
|
|
846
863
|
});
|
|
847
864
|
|
package/lib/routes.js
CHANGED
|
@@ -731,15 +731,16 @@ export function mountApiGate(ctx, log = () => {}) {
|
|
|
731
731
|
* (message.content 是块数组、message.source 是来源)。
|
|
732
732
|
*/
|
|
733
733
|
export function registerAgentHooks(ctx, deps) {
|
|
734
|
-
const { personaText, buildContextBlock, noteInteraction, log } = deps;
|
|
734
|
+
const { personaText, buildContextBlock, noteInteraction, isFishSession, log } = deps;
|
|
735
735
|
|
|
736
|
-
// 人格卡静态注入(KV Cache
|
|
736
|
+
// 人格卡静态注入(KV Cache 前缀)。#40:宿主把 text 回调在每个会话里都会调,
|
|
737
|
+
// 不是鱼的会话返回空串,别让「鱼」的人格污染用户自己的技术对话。
|
|
737
738
|
try {
|
|
738
739
|
ctx.effect(() => ctx.systemPrompt.section({
|
|
739
740
|
name: 'fish-soul-persona',
|
|
740
741
|
order: 89000, // 宿主自有段排到 ~10200,人格卡放其后、动态段之前
|
|
741
742
|
interpolate: false,
|
|
742
|
-
text: () => (typeof personaText === 'function' ? personaText() : personaText)
|
|
743
|
+
text: (arg) => (isFishSession(arg) === false ? '' : (typeof personaText === 'function' ? personaText() : personaText))
|
|
743
744
|
}), 'dsh-fish-soul: persona section');
|
|
744
745
|
} catch (error) {
|
|
745
746
|
log('error', `[fish-soul] persona section 挂不上:${String(error instanceof Error ? error.message : error)}`);
|
package/lib/social.js
CHANGED
|
@@ -32,6 +32,12 @@ export function cleanSocialText(raw, maxChars) {
|
|
|
32
32
|
return t.replace(/#[^#\s]{1,30}#/g, '').replace(/@\S+/g, '').replace(/https?:\/\/\S+/g, '').replace(/[ \t]{2,}/g, ' ').trim().slice(0, maxChars);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/** 一轮轮询里最多读几个视频(每条 @ 要 2 次接口调用,防止把上限拉满时一口气打 2N 次)。 */
|
|
36
|
+
const READ_VIDEO_PER_ROUND = 3;
|
|
37
|
+
|
|
38
|
+
/** B站 评论的 type:1 = 视频,11 = 动态(只有视频才有简介 / 字幕可读)。 */
|
|
39
|
+
const TYPE_VIDEO = 1;
|
|
40
|
+
|
|
35
41
|
/** B站 / 抖音共用:什么时候发、写什么、记在哪、回评论的记账。子类实现 cfg() / ready / publish() / pollComments()。 */
|
|
36
42
|
export class SocialBase {
|
|
37
43
|
/**
|
|
@@ -268,10 +274,12 @@ export class SocialBase {
|
|
|
268
274
|
}
|
|
269
275
|
|
|
270
276
|
/** 被 @ 时,若开了「@ 她时先读一下视频」,先把简介和字幕捞出来塞进提示词(只读)。 */
|
|
271
|
-
async readVideoIfNeeded(it, c) {
|
|
277
|
+
async readVideoIfNeeded(it, c, budget = { left: READ_VIDEO_PER_ROUND }) {
|
|
272
278
|
if (it.kind !== 'at' || c.comments.atReadVideo !== true) return;
|
|
273
|
-
if (String(it.type) !==
|
|
279
|
+
if (String(it.type) !== String(TYPE_VIDEO) || !it.oid) return;
|
|
274
280
|
if (typeof this.client.viewByAid !== 'function') return;
|
|
281
|
+
if (budget.left <= 0) return; // 这一轮读够了,剩下的等下一轮(防止 30 条 @ × 2 次调用)
|
|
282
|
+
budget.left -= 1;
|
|
275
283
|
try {
|
|
276
284
|
const info = await this.client.viewByAid(it.oid);
|
|
277
285
|
let subtitle = '';
|
|
@@ -334,11 +342,12 @@ export class BiliSocial extends SocialBase {
|
|
|
334
342
|
}
|
|
335
343
|
const fresh = items.filter((it) => it.time >= (Number(cursor.time) || 0) && !this.handled(it.id)).sort((a, b) => a.time - b.time);
|
|
336
344
|
let advanceTo = Number(cursor.time) || 0;
|
|
345
|
+
const readBudget = { left: READ_VIDEO_PER_ROUND }; // 这一轮最多读几个视频(每条 @ 两次接口调用)
|
|
337
346
|
for (const it of fresh) {
|
|
338
347
|
if (it.mid === this.client.uid) { this.record(it, 'skipped'); advanceTo = Math.max(advanceTo, it.time); continue; }
|
|
339
348
|
if (it.time > 0 && Date.now() - it.time * 1000 > STALE_MS) { this.record(it, 'stale'); advanceTo = Math.max(advanceTo, it.time); continue; }
|
|
340
349
|
if (this.repliedLastHour() >= c.comments.perHour) break; // 剩下的下次再看
|
|
341
|
-
await this.readVideoIfNeeded(it, c);
|
|
350
|
+
await this.readVideoIfNeeded(it, c, readBudget);
|
|
342
351
|
const raw = String((await this.callLlm(this.replyPrompt(it, c.master))) ?? '').trim();
|
|
343
352
|
const text = /\[不回\]/.test(raw) ? '' : cleanSocialText(raw, 120);
|
|
344
353
|
if (text === '') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-fish-soul",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.3",
|
|
4
4
|
"description": "鱼 · 人格、按人记忆(可纠正、有印象、能导入旧聊天记录)、日程自走与主动开口,直连 SnowLuma 收发 QQ(群里会自己插话),B站 / 抖音小号私信与自己刷视频;按功能选模型并统计用量花费,能听会说 QQ 语音、收藏表情包、学群里的梗,可选上网查;心情影响说话、去 AI 味、每个群的设定、按聊天选模型、每晚写日记(DSH 插件,单角色 feiyu)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
64
|
"scripts": {
|
|
65
|
-
"test": "node test/smoke.mjs && node test/douyin-browser.mjs && node test/startup-gate.mjs && node test/outbox.mjs && node test/import-coverage.mjs && node test/update-check.mjs && node test/issue-fixes.mjs && node test/import-resume.mjs && node test/maibot-extras.mjs && node test/worldinfo.mjs && node test/memory-bulk.mjs && node test/backup-restore.mjs && node test/douyin-group.mjs && node test/review-fixes.mjs && node test/at-reads-video.mjs && node test/search-source.mjs && node test/client-lint.mjs",
|
|
65
|
+
"test": "node test/smoke.mjs && node test/douyin-browser.mjs && node test/startup-gate.mjs && node test/outbox.mjs && node test/import-coverage.mjs && node test/update-check.mjs && node test/issue-fixes.mjs && node test/import-resume.mjs && node test/maibot-extras.mjs && node test/worldinfo.mjs && node test/memory-bulk.mjs && node test/backup-restore.mjs && node test/douyin-group.mjs && node test/review-fixes.mjs && node test/at-reads-video.mjs && node test/search-source.mjs && node test/followups.mjs && node test/client-lint.mjs",
|
|
66
66
|
"eval": "node test/eval/persona-eval.mjs"
|
|
67
67
|
}
|
|
68
68
|
}
|