dsh-feini 0.1.4 → 0.1.6
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/bin/cli.mjs +54 -0
- package/package.json +1 -1
- package/src/bridge.mjs +94 -63
- package/src/guess-plan.mjs +79 -0
- package/src/websearch.mjs +9 -0
package/bin/cli.mjs
CHANGED
|
@@ -98,6 +98,57 @@ function tail(file, n = 6) {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
const versionSeenPath = path.join(home, 'version-seen.json');
|
|
102
|
+
|
|
103
|
+
function pkgVersion() {
|
|
104
|
+
return String(readJson(path.join(pkgRoot, 'package.json'), {})?.version || '0.0.0');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** 智谱 key 配了没(识图 + 联网都要它) */
|
|
108
|
+
function visionConfigured() {
|
|
109
|
+
try {
|
|
110
|
+
const p = path.join(home, 'vision-key.txt');
|
|
111
|
+
return fs.existsSync(p) && !!fs.readFileSync(p, 'utf8').trim();
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 版本变了(或者第一次跑)就体检一遍:缺哪个功能、怎么补,说清楚。
|
|
119
|
+
* 同一个版本只提醒一次,免得每次 start 都唠叨。
|
|
120
|
+
*/
|
|
121
|
+
function checkFeatures() {
|
|
122
|
+
const ver = pkgVersion();
|
|
123
|
+
let seen = {};
|
|
124
|
+
try {
|
|
125
|
+
seen = JSON.parse(fs.readFileSync(versionSeenPath, 'utf8'));
|
|
126
|
+
} catch {
|
|
127
|
+
seen = {};
|
|
128
|
+
}
|
|
129
|
+
if (String(seen.version || '') === ver) return;
|
|
130
|
+
const tips = [];
|
|
131
|
+
if (!visionConfigured()) {
|
|
132
|
+
tips.push('识图和联网还没开:dsh-feini key vision <智谱key>(没有这个 key,看不懂图、也查不了现在的事)');
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const persona = path.join(home, 'persona.md');
|
|
136
|
+
if (fs.existsSync(persona) && /自己改这一份就行/.test(fs.readFileSync(persona, 'utf8'))) {
|
|
137
|
+
tips.push('人设还是空模板:打开 persona.md,把名字和性格写上');
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
/* 没有就不提 */
|
|
141
|
+
}
|
|
142
|
+
if (tips.length) {
|
|
143
|
+
console.log(`\n体检(${seen.version ? `${seen.version} → ${ver}` : `首次 ${ver}`}),还差这些:`);
|
|
144
|
+
for (const t of tips) console.log(' · ' + t);
|
|
145
|
+
console.log(' 补完就不用管了,下个版本再体检\n');
|
|
146
|
+
} else if (seen.version) {
|
|
147
|
+
console.log(`升级完成:${seen.version} → ${ver},功能都在\n`);
|
|
148
|
+
}
|
|
149
|
+
writeJson(versionSeenPath, { version: ver, at: Date.now() });
|
|
150
|
+
}
|
|
151
|
+
|
|
101
152
|
const help = `dsh-feini —— 在 QQ 里养一个自己的伙伴
|
|
102
153
|
|
|
103
154
|
用法:
|
|
@@ -245,6 +296,7 @@ async function cmdInit(argv) {
|
|
|
245
296
|
2. dsh-feini start
|
|
246
297
|
3. QQ 上给那个号发一句,看看回不回
|
|
247
298
|
`);
|
|
299
|
+
checkFeatures();
|
|
248
300
|
}
|
|
249
301
|
|
|
250
302
|
/* ---------- start / stop / status ---------- */
|
|
@@ -255,6 +307,7 @@ function cmdStart() {
|
|
|
255
307
|
console.log(`已经在跑了(pid ${running}),不用再起一个`);
|
|
256
308
|
return;
|
|
257
309
|
}
|
|
310
|
+
checkFeatures();
|
|
258
311
|
if (!fs.existsSync(configPath)) {
|
|
259
312
|
console.error('还没有配置,先跑 dsh-feini init');
|
|
260
313
|
process.exit(1);
|
|
@@ -290,6 +343,7 @@ function cmdStop() {
|
|
|
290
343
|
function cmdStatus() {
|
|
291
344
|
const pid = currentPid();
|
|
292
345
|
console.log(pid ? `在跑,pid ${pid}` : '没在跑');
|
|
346
|
+
console.log(`识图/联网:${visionConfigured() ? '已配(智谱 key 在)' : '没配(dsh-feini key vision <key>)'}`);
|
|
293
347
|
console.log('--- 最近日志 ---');
|
|
294
348
|
console.log(tail(logPath, 8));
|
|
295
349
|
}
|
package/package.json
CHANGED
package/src/bridge.mjs
CHANGED
|
@@ -22,7 +22,7 @@ import { readSystemPrompt } from './prompt.mjs';
|
|
|
22
22
|
import { chat, imagePart } from './llm.mjs';
|
|
23
23
|
import { toDisplay, splitParts } from './clean.mjs';
|
|
24
24
|
import { looksLikeCorrection, prevLineInText, correctionHint } from './correct.mjs';
|
|
25
|
-
import { needsSearch, webSearch, searchLine, searchFailLine } from './websearch.mjs';
|
|
25
|
+
import { needsSearch, webSearch, searchLine, searchFailLine, searchNotConfiguredLine } from './websearch.mjs';
|
|
26
26
|
import { recentFacts } from './recent.mjs';
|
|
27
27
|
import { getHistory, getLastTs, lastMessage, appendRound } from './memory.mjs';
|
|
28
28
|
import { startProactive } from './proactive.mjs';
|
|
@@ -37,13 +37,13 @@ import { lastAssistant } from './last-assistant.mjs';
|
|
|
37
37
|
import { takeInterest, addInterest, removeInterest, interestsLine, INTEREST_RULE } from './interests.mjs';
|
|
38
38
|
import { startInterestReview } from './review.mjs';
|
|
39
39
|
import { seaLine } from './sea.mjs';
|
|
40
|
+
import { planHint } from './guess-plan.mjs';
|
|
40
41
|
import { takeGuess, addGuess, resolveGuess, guessesLine, GUESS_RULE } from './guess.mjs';
|
|
41
42
|
import { ANGER_RULE, angerLine, offend, coaxed, looksLikeCoax } from './anger.mjs';
|
|
42
43
|
import { getBalance, BALANCE_RULE } from './balance.mjs';
|
|
43
44
|
import { addUsage, usageSummary, USAGE_RULE } from './usage.mjs';
|
|
44
45
|
import { createDiary } from './diary.mjs';
|
|
45
46
|
import { onUsage } from './llm.mjs';
|
|
46
|
-
import { buildSea } from './build-sea.mjs';
|
|
47
47
|
|
|
48
48
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
49
49
|
const root = process.env.DSH_FEINI_HOME ? path.resolve(process.env.DSH_FEINI_HOME) : path.resolve(here, '..');
|
|
@@ -63,8 +63,6 @@ function loadConfig() {
|
|
|
63
63
|
modelVision: raw.modelVision || {},
|
|
64
64
|
proactive: raw.proactive || { enabled: false },
|
|
65
65
|
stickers: raw.stickers || {},
|
|
66
|
-
// 休眠哨兵/外部通知口子;被占用时只记一行日志,不影响聊天
|
|
67
|
-
notifyPort: Number(raw.notifyPort || 7301),
|
|
68
66
|
};
|
|
69
67
|
if (process.env.BRIDGE_SYSTEM) cfg.systemPromptPath = null; // 测试用:直接给 prompt 文件
|
|
70
68
|
return cfg;
|
|
@@ -156,18 +154,14 @@ const guessFile = path.resolve(root, config.guessPath || 'guesses.json');
|
|
|
156
154
|
const angerFile = path.join(root, 'anger.json');
|
|
157
155
|
const petMemoryDir = config.petMemoryDir || '';
|
|
158
156
|
const interestFile = petMemoryDir ? path.join(petMemoryDir, 'interests.md') : path.join(root, 'interests.md');
|
|
159
|
-
//
|
|
160
|
-
const seaFile =
|
|
161
|
-
? path.
|
|
162
|
-
:
|
|
163
|
-
? path.join(petMemoryDir, '..', 'memory-sea', 'data', 'me.json')
|
|
164
|
-
: path.join(root, 'me.json');
|
|
157
|
+
// 记忆海的数据:pet-memory 的上一级(工作区)里的 memory-sea/data/me.json
|
|
158
|
+
const seaFile = petMemoryDir
|
|
159
|
+
? path.join(petMemoryDir, '..', 'memory-sea', 'data', 'me.json')
|
|
160
|
+
: path.join(root, 'me.json');
|
|
165
161
|
// 笔记本:每轮现读,改完立刻生效(人设里不再抄它)
|
|
166
|
-
const notebookFile =
|
|
167
|
-
? path.
|
|
168
|
-
:
|
|
169
|
-
? path.join(petMemoryDir, '..', 'memory', 'notebook.md')
|
|
170
|
-
: path.join(root, 'notebook.md');
|
|
162
|
+
const notebookFile = petMemoryDir
|
|
163
|
+
? path.join(petMemoryDir, '..', 'memory', 'notebook.md')
|
|
164
|
+
: path.join(root, 'notebook.md');
|
|
171
165
|
|
|
172
166
|
let notebookCache = { mtime: 0, text: '' };
|
|
173
167
|
function notebookText() {
|
|
@@ -184,27 +178,6 @@ function notebookText() {
|
|
|
184
178
|
// 人设里「别硬报时」是给桌面碎碎念写的;QQ 里他问时间必须直接答,所以把这句话改软
|
|
185
179
|
const personaForQQ = baseSystemPrompt.replace(/别硬报时/g, '碎碎念时别反复提时间');
|
|
186
180
|
|
|
187
|
-
/** 记忆海:每两分钟把「对话 + 笔记本 + 日记」重建成索引,启动两秒后先建一次 */
|
|
188
|
-
function rebuildSea() {
|
|
189
|
-
try {
|
|
190
|
-
const r = buildSea({
|
|
191
|
-
memoryPath: config.memoryPath,
|
|
192
|
-
entry: config.bucket.entry,
|
|
193
|
-
pet: config.bucket.pet,
|
|
194
|
-
notebookPath: notebookFile,
|
|
195
|
-
diaryDir: config.diaryDir ? path.resolve(root, config.diaryDir) : path.join(root, 'diary'),
|
|
196
|
-
outFile: seaFile,
|
|
197
|
-
});
|
|
198
|
-
log(`记忆海重建:长期 ${r.longTerm} 条、对话 ${r.messages} 条、日记 ${r.diaries} 篇`);
|
|
199
|
-
} catch (e) {
|
|
200
|
-
log('记忆海重建失败:' + e.message);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
const seaBoot = setTimeout(rebuildSea, 2000);
|
|
204
|
-
seaBoot.unref?.();
|
|
205
|
-
const seaTimer = setInterval(rebuildSea, 2 * 60 * 1000);
|
|
206
|
-
seaTimer.unref?.();
|
|
207
|
-
|
|
208
181
|
// 每次说话都现读人设:sync 改完下一句就生效,不用重启桥
|
|
209
182
|
function freshPersona() {
|
|
210
183
|
if (!config.systemPromptPath) return personaForQQ;
|
|
@@ -514,6 +487,38 @@ async function knownSticker(img) {
|
|
|
514
487
|
}
|
|
515
488
|
}
|
|
516
489
|
|
|
490
|
+
const visionHintFile = path.join(root, 'vision-hint.json');
|
|
491
|
+
|
|
492
|
+
/** 识图的 key 配了没(配了才看得懂图,也才查得了网) */
|
|
493
|
+
function visionKeyReady() {
|
|
494
|
+
try {
|
|
495
|
+
const p = config.modelVision?.keyFile;
|
|
496
|
+
if (!p) return false;
|
|
497
|
+
return fs.existsSync(p) && !!fs.readFileSync(p, 'utf8').trim();
|
|
498
|
+
} catch {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** 记一次「看不懂图」,返回这是第几次;前三次数要提醒,之后不再提 */
|
|
504
|
+
function visionMiss(hit = true) {
|
|
505
|
+
let s = { count: 0 };
|
|
506
|
+
try {
|
|
507
|
+
s = JSON.parse(fs.readFileSync(visionHintFile, 'utf8'));
|
|
508
|
+
} catch {
|
|
509
|
+
/* 没有就是第一次 */
|
|
510
|
+
}
|
|
511
|
+
const count = Number(s.count) || 0;
|
|
512
|
+
if (!hit) return { count };
|
|
513
|
+
const next = count + 1;
|
|
514
|
+
try {
|
|
515
|
+
fs.writeFileSync(visionHintFile, JSON.stringify({ count: next, lastAt: Date.now() }), 'utf8');
|
|
516
|
+
} catch {
|
|
517
|
+
/* 写不进去就每次都当第一次 */
|
|
518
|
+
}
|
|
519
|
+
return { count: next };
|
|
520
|
+
}
|
|
521
|
+
|
|
517
522
|
/** 把一条 QQ 消息变成模型能吃的 user content */
|
|
518
523
|
async function buildUserContent(text, images, quoted = '') {
|
|
519
524
|
const parts = [];
|
|
@@ -558,7 +563,25 @@ async function buildUserContent(text, images, quoted = '') {
|
|
|
558
563
|
log('图片处理失败:', e.message);
|
|
559
564
|
}
|
|
560
565
|
}
|
|
561
|
-
|
|
566
|
+
// 没配识图(或者识图挂了):看不清就说看不清,但最多提醒三次
|
|
567
|
+
const miss = visionMiss();
|
|
568
|
+
if (miss.count <= 3) {
|
|
569
|
+
const tail =
|
|
570
|
+
miss.count === 3
|
|
571
|
+
? '这是第三次了,顺口说一句「这是最后一次提醒你了」,以后别提。'
|
|
572
|
+
: '';
|
|
573
|
+
parts.push({
|
|
574
|
+
type: 'text',
|
|
575
|
+
text:
|
|
576
|
+
`(他发来一张图片/表情包,但我这边没配识图,看不出是什么。` +
|
|
577
|
+
`顺口跟他说一句「我还没配识图,这张看不懂」,别像系统提示,也别猜内容。${tail})`,
|
|
578
|
+
});
|
|
579
|
+
} else {
|
|
580
|
+
parts.push({
|
|
581
|
+
type: 'text',
|
|
582
|
+
text: '(他发来一张图片/表情包,我看不出内容;已经提醒过三次,这次别再提配置的事)',
|
|
583
|
+
});
|
|
584
|
+
}
|
|
562
585
|
label.push('[图片]');
|
|
563
586
|
}
|
|
564
587
|
if (parts.length === 0) parts.push({ type: 'text', text: '(空消息)' });
|
|
@@ -872,14 +895,19 @@ async function finishSearch(bot, userId) {
|
|
|
872
895
|
const lastUser = [...hist].reverse().find((m) => m.role === 'user');
|
|
873
896
|
const q = String(lastUser?.content || '').replace(/[\[\[]?引用[::][^\]]*[\]\]]?/g, '').trim();
|
|
874
897
|
let note = '';
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
898
|
+
if (!visionKeyReady()) {
|
|
899
|
+
note = searchNotConfiguredLine();
|
|
900
|
+
log('补查:没配搜索功能');
|
|
901
|
+
} else {
|
|
902
|
+
try {
|
|
903
|
+
const keyPath = config.modelVision?.keyFile || path.join(root, 'vision-key.txt');
|
|
904
|
+
const hits = await webSearch(q, { keyPath, baseUrl: config.modelVision?.baseUrl });
|
|
905
|
+
note = hits.length ? searchLine(q, hits) : searchFailLine();
|
|
906
|
+
log(`补查「${q.slice(0, 24)}」:${hits.length} 条`);
|
|
907
|
+
} catch (e) {
|
|
908
|
+
note = searchFailLine();
|
|
909
|
+
log('补查失败:', e.message);
|
|
910
|
+
}
|
|
883
911
|
}
|
|
884
912
|
const reply = await chat({
|
|
885
913
|
baseUrl: config.model.baseUrl,
|
|
@@ -1072,19 +1100,25 @@ async function respond(bot, userId, text, images, quoted, replyToId = 0) {
|
|
|
1072
1100
|
// 问到现在的事(天气、价格、赛果、新闻、版本更新):先现查一把,查到的塞进 system
|
|
1073
1101
|
let searchNote = '';
|
|
1074
1102
|
if (needsSearch(text)) {
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1103
|
+
if (!visionKeyReady()) {
|
|
1104
|
+
// 没配搜索:就直说没配,别装成「查了没搜到」
|
|
1105
|
+
searchNote = searchNotConfiguredLine();
|
|
1106
|
+
log('要联网,但没配搜索功能');
|
|
1107
|
+
} else {
|
|
1108
|
+
const keyPath = config.modelVision?.keyFile || path.join(root, 'vision-key.txt');
|
|
1109
|
+
try {
|
|
1110
|
+
const hits = await webSearch(text, { keyPath, baseUrl: config.modelVision?.baseUrl });
|
|
1111
|
+
if (hits.length) {
|
|
1112
|
+
searchNote = searchLine(text, hits);
|
|
1113
|
+
log(`联网查了 ${hits.length} 条:${text.slice(0, 24)}`);
|
|
1114
|
+
} else {
|
|
1115
|
+
searchNote = searchFailLine();
|
|
1116
|
+
log('联网查了,没结果');
|
|
1117
|
+
}
|
|
1118
|
+
} catch (e) {
|
|
1082
1119
|
searchNote = searchFailLine();
|
|
1083
|
-
log('
|
|
1120
|
+
log('联网搜索失败:', e.message);
|
|
1084
1121
|
}
|
|
1085
|
-
} catch (e) {
|
|
1086
|
-
searchNote = searchFailLine();
|
|
1087
|
-
log('联网搜索失败:', e.message);
|
|
1088
1122
|
}
|
|
1089
1123
|
}
|
|
1090
1124
|
|
|
@@ -1094,7 +1128,7 @@ async function respond(bot, userId, text, images, quoted, replyToId = 0) {
|
|
|
1094
1128
|
? []
|
|
1095
1129
|
: getHistory(config.memoryPath, config.bucket.entry, config.bucket.pet, config.historyRounds);
|
|
1096
1130
|
const messages = [
|
|
1097
|
-
{ role: 'system', content: buildSystem(text) + awayNote + correctionLine(text) + searchNote + recentFacts(config.memoryPath, config.bucket.entry, config.bucket.pet) },
|
|
1131
|
+
{ role: 'system', content: buildSystem(text) + awayNote + correctionLine(text) + planHint(seaFile, text) + searchNote + recentFacts(config.memoryPath, config.bucket.entry, config.bucket.pet) },
|
|
1098
1132
|
...history,
|
|
1099
1133
|
{ role: 'user', content },
|
|
1100
1134
|
];
|
|
@@ -1418,7 +1452,6 @@ const notifyPort = Number(config.notifyPort || 7301);
|
|
|
1418
1452
|
const notifyOff = process.env.BRIDGE_NO_NOTIFY === '1'; // 测试时别抢端口
|
|
1419
1453
|
let lastNotify = { text: '', at: 0 }; // 同一个哨兵可能起了两份,去重
|
|
1420
1454
|
try {
|
|
1421
|
-
if (notifyOff) throw new Error('skip'); // 测试环境:连端口都不抢
|
|
1422
1455
|
http
|
|
1423
1456
|
.createServer((req, res) => {
|
|
1424
1457
|
const u = new URL(req.url || '/', 'http://127.0.0.1');
|
|
@@ -1446,12 +1479,10 @@ try {
|
|
|
1446
1479
|
})
|
|
1447
1480
|
.catch((e) => res.writeHead(500).end(String(e?.message || e)));
|
|
1448
1481
|
})
|
|
1449
|
-
.on('error', (e) =>
|
|
1450
|
-
if (!notifyOff) log('通知口子错误:', e.message);
|
|
1451
|
-
})
|
|
1482
|
+
.on('error', (e) => log('通知口子错误:', e.message))
|
|
1452
1483
|
.listen(notifyPort, '127.0.0.1', () => log('通知口子 127.0.0.1:' + notifyPort));
|
|
1453
1484
|
} catch (e) {
|
|
1454
|
-
|
|
1485
|
+
log('通知口子起不来:', e.message);
|
|
1455
1486
|
}
|
|
1456
1487
|
|
|
1457
1488
|
/* ---------- 拉起休眠哨兵 ---------- */
|
|
@@ -1467,7 +1498,7 @@ try {
|
|
|
1467
1498
|
log('休眠哨兵已启动');
|
|
1468
1499
|
}
|
|
1469
1500
|
} catch (e) {
|
|
1470
|
-
|
|
1501
|
+
log('哨兵启动失败:', e.message);
|
|
1471
1502
|
}
|
|
1472
1503
|
bot.connect();
|
|
1473
1504
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「你猜我在干啥」:先翻他昨天说过的行程,翻不到再按习惯猜。
|
|
3
|
+
*
|
|
4
|
+
* 为什么单独一个模块:这段逻辑要能单独测(拿真的 me.json 喂它),
|
|
5
|
+
* 塞在 bridge.mjs 里一导入就把桥跑起来了,没法测。
|
|
6
|
+
*
|
|
7
|
+
* 检索顺序按他定的来:
|
|
8
|
+
* 1. 最近几天他说过的安排(明天/今天/约/说好 + 去/吃/见…),优先他自己说的;
|
|
9
|
+
* 2. 没翻到就让她按习惯猜(作息、最近在玩什么、爱吃什么)。
|
|
10
|
+
*/
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
|
|
13
|
+
const PLAN_RE = /(明天|今天|后天|约好|说好|打算)/;
|
|
14
|
+
const VERB_RE = /(去|吃|见|回|上班|上课|开会|出门)/;
|
|
15
|
+
const NOT_PLAN_RE = /(猜猜|你猜|猜一下|猜我|哼|再说一遍|还记得|记得我|有没有给你说|给你说过)/;
|
|
16
|
+
|
|
17
|
+
/** 像不像「哪天要去干嘛」:短句 + 时间词 + 动作词,别把一整段聊天当成行程 */
|
|
18
|
+
function looksLikePlan(t) {
|
|
19
|
+
if (!t || t.length < 4 || t.length > 70) return false;
|
|
20
|
+
if (!PLAN_RE.test(t) || !VERB_RE.test(t)) return false;
|
|
21
|
+
if (NOT_PLAN_RE.test(t)) return false;
|
|
22
|
+
// 「今天」「明天」后面紧跟着动作,才像安排
|
|
23
|
+
return /(明天|今天|后天|约好|说好|打算)[^。!?;;]{0,18}(去|吃|见|回|上班|上课|开会|出门)/.test(t);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 最近几天里「像安排」的话,他自己说的排前面,然后按新到旧 */
|
|
27
|
+
export function recentPlans(seaFile, { days = 5, limit = 4 } = {}) {
|
|
28
|
+
let data = {};
|
|
29
|
+
try {
|
|
30
|
+
data = JSON.parse(fs.readFileSync(seaFile, 'utf8'));
|
|
31
|
+
} catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
const since = Date.now() - days * 86400000;
|
|
35
|
+
const list = [];
|
|
36
|
+
for (const m of data.messages || []) {
|
|
37
|
+
const at = Number(m.at) || 0;
|
|
38
|
+
const t = String(m.text || '');
|
|
39
|
+
if (at < since || !looksLikePlan(t)) continue;
|
|
40
|
+
list.push({ text: t, at, isOwner: !!m.isOwner });
|
|
41
|
+
}
|
|
42
|
+
for (const lt of data.longTerm || []) {
|
|
43
|
+
const t = String(lt.text || '');
|
|
44
|
+
const at = Number(lt.lastMentionAt || lt.createdAt) || 0;
|
|
45
|
+
if (at < since || !looksLikePlan(t)) continue;
|
|
46
|
+
list.push({ text: t, at, isOwner: true });
|
|
47
|
+
}
|
|
48
|
+
const seen = new Set();
|
|
49
|
+
return list
|
|
50
|
+
.sort((a, b) => (b.isOwner ? 1 : 0) - (a.isOwner ? 1 : 0) || b.at - a.at)
|
|
51
|
+
.filter((x) => {
|
|
52
|
+
const k = x.text.slice(0, 24);
|
|
53
|
+
if (seen.has(k)) return false;
|
|
54
|
+
seen.add(k);
|
|
55
|
+
return true;
|
|
56
|
+
})
|
|
57
|
+
.slice(0, limit);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 拼给 system 的一段;他不是在让猜就返回空 */
|
|
61
|
+
export function planHint(seaFile, text) {
|
|
62
|
+
const t = String(text || '');
|
|
63
|
+
if (!/(猜猜|你猜|猜一下|猜我|猜猜看)/.test(t)) return '';
|
|
64
|
+
const plans = recentPlans(seaFile);
|
|
65
|
+
if (plans.length) {
|
|
66
|
+
const rows = plans
|
|
67
|
+
.map((p) => `${p.at ? new Date(p.at).toLocaleDateString('zh-CN') + ' ' : ''}${p.text.slice(0, 80)}`)
|
|
68
|
+
.join(';');
|
|
69
|
+
return (
|
|
70
|
+
`\n\n【他在让你猜·先看行程】最近他说过的安排:${rows}。` +
|
|
71
|
+
'先按这些安排加上现在的时间点来猜(昨天说今天要去哪、几点做什么),' +
|
|
72
|
+
'对不上再用他的习惯猜(作息、最近在玩什么、爱吃什么)。用一两句说出来,别列清单。'
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return (
|
|
76
|
+
'\n\n【他在让你猜·没查到行程】海里没捞到他最近几天的明确安排,那就按他的习惯猜:' +
|
|
77
|
+
'这个点平时在干嘛、最近在玩什么、爱吃什么。猜一两个就好;他不认就让他给个提示,别硬猜。'
|
|
78
|
+
);
|
|
79
|
+
}
|
package/src/websearch.mjs
CHANGED
|
@@ -109,3 +109,12 @@ export function searchFailLine() {
|
|
|
109
109
|
'直接说「我这边网好像没搜到」,可以让他再问一次;别的就照你会的答。'
|
|
110
110
|
);
|
|
111
111
|
}
|
|
112
|
+
|
|
113
|
+
/** 压根没配搜索:直说没配,别装成「查了没搜到」 */
|
|
114
|
+
export function searchNotConfiguredLine() {
|
|
115
|
+
return (
|
|
116
|
+
'\n\n【没配搜索】他问的是要联网才知道的事,但我这边没配搜索功能,一次都没去查。' +
|
|
117
|
+
'就照实说「我没配搜索功能」——别说「没查到」「没搜到」,那听着像查过了。' +
|
|
118
|
+
'可以顺口提一句:配个智谱 key 就能查。别编答案。'
|
|
119
|
+
);
|
|
120
|
+
}
|