luckytri 1.1.2 → 1.2.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/README.en.md +14 -12
- package/README.md +4 -2
- package/docs/en/CHANGELOG.md +24 -0
- package/docs/en/architecture.md +12 -10
- package/docs/en/chat-style.md +13 -13
- package/docs/en/cognition.md +12 -4
- package/docs/en/evolution.md +35 -0
- package/docs/en/her-life.md +5 -2
- package/docs/en/vision.md +50 -0
- package/docs/zh/CHANGELOG.md +24 -0
- package/docs/zh/architecture.md +12 -10
- package/docs/zh/chat-style.md +17 -17
- package/docs/zh/cognition.md +12 -4
- package/docs/zh/evolution.md +35 -0
- package/docs/zh/her-life.md +5 -2
- package/docs/zh/vision.md +50 -0
- package/package.json +4 -1
- package/public/app/assets/{ChatsPage-IfBgNdaD.js → ChatsPage-eDqFIg97.js} +1 -1
- package/public/app/assets/{Empty-DK9nQPCf.js → Empty-CAGOJsfD.js} +1 -1
- package/public/app/assets/{Field-BzKFhuFd.js → Field-CRz-JmN1.js} +1 -1
- package/public/app/assets/{HeartPage-CKxrbfFQ.js → HeartPage-BGWoeMrC.js} +1 -1
- package/public/app/assets/{LifePage-CtxPEi6E.js → LifePage-iao3o3wt.js} +1 -1
- package/public/app/assets/{MemoryPage-B8FLXzLi.js → MemoryPage-Dwi2yi7E.js} +1 -1
- package/public/app/assets/NaturePage-DniImo9U.js +3 -0
- package/public/app/assets/{NaturePage-CHkpb5NK.css → NaturePage-zguDpGWY.css} +1 -1
- package/public/app/assets/{PeoplePage-D3yn0HV0.js → PeoplePage-D-MC3JEt.js} +1 -1
- package/public/app/assets/{PluginsPage-ojv8zawj.js → PluginsPage-D7dJTf96.js} +1 -1
- package/public/app/assets/SystemPage-C5CUHjGh.js +1 -0
- package/public/app/assets/{SystemPage-CGVTvNZw.css → SystemPage-C_Y5M7JU.css} +1 -1
- package/public/app/assets/{TimePage-Dyd-FFbq.js → TimePage-Dwhr3-92.js} +1 -1
- package/public/app/assets/{index-BJfzfgnm.js → index-L0bHax8C.js} +3 -3
- package/public/app/assets/{knowledge-C-bND0Ub.js → knowledge-BTDNafzl.js} +1 -1
- package/public/app/assets/{media-D7G3Nco6.js → media-BBzOAfNG.js} +1 -1
- package/public/app/assets/{mind-ec-G6iNZ.js → mind-CBTkFje2.js} +1 -1
- package/public/app/index.html +3 -3
- package/public/guide.en.html +3 -3
- package/public/guide.html +3 -3
- package/scripts/migrate-instance.js +27 -0
- package/scripts/update-instance.js +21 -2
- package/server/channels/contract.js +5 -1
- package/server/channels/hub.js +2 -2
- package/server/channels/onebot/adapter.js +291 -288
- package/server/channels/onebot/index.js +2 -2
- package/server/channels/qqbot/api.js +194 -199
- package/server/core/context-builder.js +20 -3
- package/server/core/conversation-cues.js +5 -4
- package/server/core/conversation-grounding.js +20 -17
- package/server/core/conversation-needs.js +32 -0
- package/server/core/dialogue-context.js +2 -5
- package/server/core/dialogue-contract.js +23 -16
- package/server/core/message-scheduler.js +85 -78
- package/server/core/model-manager.js +113 -16
- package/server/core/model-presets.js +1 -1
- package/server/core/orchestrator.js +18 -2
- package/server/core/persona-manager.js +14 -5
- package/server/core/reader-check.js +47 -42
- package/server/core/reply-delivery.js +125 -34
- package/server/core/response-validator.js +28 -1
- package/server/core/retired-prompts.js +5 -0
- package/server/core/speech-length.js +43 -0
- package/server/core/turn-processor.js +38 -5
- package/server/core/turn.js +70 -16
- package/server/i18n/errors.js +30 -0
- package/server/i18n/labels.js +5 -0
- package/server/index.js +3 -0
- package/server/knowledge/embedding-profile.js +126 -29
- package/server/knowledge/manager.js +103 -34
- package/server/knowledge/schema.js +197 -158
- package/server/mind/affect.js +267 -265
- package/server/mind/aftermath.js +169 -0
- package/server/mind/anticipations.js +2 -0
- package/server/mind/clock.js +91 -83
- package/server/mind/discretion.js +20 -0
- package/server/mind/episodes.js +2 -1
- package/server/mind/index.js +17 -2
- package/server/mind/initiative.js +29 -14
- package/server/mind/memory.js +47 -5
- package/server/mind/recall/cue.js +67 -0
- package/server/mind/recall/index.js +183 -29
- package/server/mind/recall/roots.js +231 -0
- package/server/mind/recall/schema.js +2 -0
- package/server/mind/recall/store.js +113 -30
- package/server/mind/salience.js +22 -1
- package/server/mind/schema.js +5 -0
- package/server/mind/thoughts.js +276 -274
- package/server/mind/view.js +100 -60
- package/server/plugins/install.js +1 -6
- package/server/plugins/zip.js +18 -4
- package/server/runtime/data-migration.js +193 -0
- package/server/runtime/update-execution.js +43 -4
- package/server/runtime/update-manager.js +4 -1
- package/server/storage/archive.js +1 -1
- package/server/storage/backup-scheduler.js +15 -3
- package/public/app/assets/NaturePage-C7i607ul.js +0 -3
- package/public/app/assets/SystemPage-Cv3rFvyT.js +0 -1
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { speechLength } from "./speech-length.js";
|
|
1
2
|
import { parseSessionKey } from "../channels/session-key.js";
|
|
2
3
|
import { initiativeContext, initiativeSnapshot } from "./initiative-context.js";
|
|
3
4
|
import { withFallback } from "./model-manager.js";
|
|
4
5
|
import { activityRecallCue } from "../mind/time/activity-recall.js";
|
|
6
|
+
import { withRecallContext } from "../mind/recall/cue.js";
|
|
5
7
|
import { replyPrompt, prompts } from "./persona-manager.js";
|
|
6
8
|
import { buildContext, perceive } from "./context-builder.js";
|
|
7
9
|
import {
|
|
@@ -323,12 +325,14 @@ export class TurnProcessor {
|
|
|
323
325
|
/* An unparseable destination has no person to infer. */
|
|
324
326
|
}
|
|
325
327
|
}
|
|
328
|
+
// A short follow-up is recalled together with the line it leans on.
|
|
329
|
+
const recallBatch = withRecallContext(batch, resolved);
|
|
326
330
|
lap("perceive");
|
|
327
331
|
// Replays exclude mutable memory so later corrections cannot leak into
|
|
328
332
|
// the past; her inner state is folded as of the replayed moment.
|
|
329
333
|
const memories =
|
|
330
334
|
policy.memory && !replay && !ownInitiative
|
|
331
|
-
? await this.owner.mind.recall.memories(session,
|
|
335
|
+
? await this.owner.mind.recall.memories(session, recallBatch, now, {
|
|
332
336
|
people,
|
|
333
337
|
touch: live,
|
|
334
338
|
forSpeech: true,
|
|
@@ -371,7 +375,7 @@ export class TurnProcessor {
|
|
|
371
375
|
lap("summaries");
|
|
372
376
|
const meant =
|
|
373
377
|
!replay && !ownInitiative
|
|
374
|
-
? await this.owner.mind.recall.relatedOwn(session,
|
|
378
|
+
? await this.owner.mind.recall.relatedOwn(session, recallBatch, now)
|
|
375
379
|
: null;
|
|
376
380
|
const view = this.owner.mind.view({
|
|
377
381
|
session: preview?.viewSession || session,
|
|
@@ -402,13 +406,18 @@ export class TurnProcessor {
|
|
|
402
406
|
activityCue,
|
|
403
407
|
}),
|
|
404
408
|
);
|
|
409
|
+
let recollected = [];
|
|
405
410
|
if (policy.memory && !replay && !ownInitiative) {
|
|
406
411
|
const recollections = await this.owner.mind.recall.recollect(
|
|
407
412
|
session,
|
|
408
|
-
|
|
413
|
+
recallBatch,
|
|
409
414
|
now,
|
|
410
|
-
{
|
|
415
|
+
{
|
|
416
|
+
people,
|
|
417
|
+
exclude: new Set((view.recalled || []).map((r) => r.id)),
|
|
418
|
+
},
|
|
411
419
|
);
|
|
420
|
+
recollected = recollections;
|
|
412
421
|
if (recollections.length)
|
|
413
422
|
view.inner.recollections = {
|
|
414
423
|
note: RECOLLECTION_NOTE,
|
|
@@ -485,7 +494,7 @@ export class TurnProcessor {
|
|
|
485
494
|
if (!replay)
|
|
486
495
|
snapshot.topics = this.owner.topics.recent(session, watermark, now);
|
|
487
496
|
if (!ownInitiative) {
|
|
488
|
-
await this.addWorkingMemory(session, snapshot,
|
|
497
|
+
await this.addWorkingMemory(session, snapshot, recallBatch, now, {
|
|
489
498
|
replay,
|
|
490
499
|
zone: policy.timeZone,
|
|
491
500
|
});
|
|
@@ -622,6 +631,7 @@ export class TurnProcessor {
|
|
|
622
631
|
return finish("stale", "读房间时又有新话,并入下一批");
|
|
623
632
|
}
|
|
624
633
|
lap("checks");
|
|
634
|
+
const speech = speechLength(nature);
|
|
625
635
|
let turn;
|
|
626
636
|
try {
|
|
627
637
|
turn = normalizeTurn(
|
|
@@ -644,6 +654,7 @@ export class TurnProcessor {
|
|
|
644
654
|
),
|
|
645
655
|
snapshot,
|
|
646
656
|
trace,
|
|
657
|
+
{ speech },
|
|
647
658
|
);
|
|
648
659
|
if (generationImages.length)
|
|
649
660
|
markVisionSeen(
|
|
@@ -716,6 +727,7 @@ export class TurnProcessor {
|
|
|
716
727
|
reason: choice === turn.choice ? turn.reason : "没发出去",
|
|
717
728
|
watermark,
|
|
718
729
|
occasion: occasion?.type || null,
|
|
730
|
+
recalled: whatCameToMind(memories, recollected, view, trace),
|
|
719
731
|
time: now,
|
|
720
732
|
});
|
|
721
733
|
};
|
|
@@ -798,3 +810,24 @@ export class TurnProcessor {
|
|
|
798
810
|
}
|
|
799
811
|
}
|
|
800
812
|
}
|
|
813
|
+
|
|
814
|
+
// The past that was in front of her this turn, for the permanent record next
|
|
815
|
+
// to her choice. A failure here must never cost the turn.
|
|
816
|
+
function whatCameToMind(memories, recollected, view, trace) {
|
|
817
|
+
try {
|
|
818
|
+
return [
|
|
819
|
+
...(memories || []).map((m) => ({
|
|
820
|
+
id: `fact:${m.id}`,
|
|
821
|
+
why: m.whySelected,
|
|
822
|
+
})),
|
|
823
|
+
...(recollected || []).flatMap((r) => [
|
|
824
|
+
{ id: r.ref, why: r.why },
|
|
825
|
+
...(r.linked || []).map((id) => ({ id, why: "linked" })),
|
|
826
|
+
]),
|
|
827
|
+
...(view?.recalled || []),
|
|
828
|
+
];
|
|
829
|
+
} catch (error) {
|
|
830
|
+
trace?.steps?.push(`没能记下这轮想起了什么:${error.message}`);
|
|
831
|
+
return [];
|
|
832
|
+
}
|
|
833
|
+
}
|
package/server/core/turn.js
CHANGED
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
explicitStop,
|
|
12
12
|
} from "./dialogue-context.js";
|
|
13
13
|
import { readerQuestions } from "./reader-check.js";
|
|
14
|
+
import { conversationNeeds } from "./conversation-needs.js";
|
|
15
|
+
import { BOT_GUIDE, CREATIVE_GUIDE } from "./dialogue-contract.js";
|
|
14
16
|
import {
|
|
15
17
|
botExchange,
|
|
16
18
|
CONTRIBUTION_CHECK,
|
|
@@ -41,9 +43,11 @@ const LEGACY = {
|
|
|
41
43
|
SILENT: "silent",
|
|
42
44
|
};
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+
// A ceiling on how many messages one turn may send, never a target. The
|
|
47
|
+
// owner's length setting moves it; someone here asking for one message wins.
|
|
48
|
+
export function maxBubbles(choice, snapshot, speech) {
|
|
45
49
|
if (choice !== "speak") return 1;
|
|
46
|
-
return prefersSingleBubble(snapshot || {}) ? 1 : 3;
|
|
50
|
+
return prefersSingleBubble(snapshot || {}) ? 1 : speech?.maxBubbles || 3;
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
export function joinBubbleParts(parts) {
|
|
@@ -154,6 +158,7 @@ export async function takeTurn(
|
|
|
154
158
|
...(extra.pressure ? { pressure: extra.pressure } : {}),
|
|
155
159
|
...(recallWording(snapshot) ? { guidance: recallWording(snapshot) } : {}),
|
|
156
160
|
...(!initiating ? { exchange: currentExchange(snapshot) } : {}),
|
|
161
|
+
...turnGuides(snapshot),
|
|
157
162
|
};
|
|
158
163
|
let raw = await models.call(
|
|
159
164
|
profile,
|
|
@@ -163,6 +168,7 @@ export async function takeTurn(
|
|
|
163
168
|
trace,
|
|
164
169
|
images,
|
|
165
170
|
);
|
|
171
|
+
raw = withSaidMessages(raw);
|
|
166
172
|
const words = (
|
|
167
173
|
Array.isArray(raw?.bubbles) ? raw.bubbles : [raw?.bubbles]
|
|
168
174
|
).filter((word) => typeof word === "string");
|
|
@@ -235,18 +241,20 @@ export async function takeTurn(
|
|
|
235
241
|
if (review.hasContribution === false && review.shouldRepair === true) {
|
|
236
242
|
trace?.steps?.push("新问题或新段落被上一稿漏接,按当前内容重新想一次");
|
|
237
243
|
try {
|
|
238
|
-
const repaired =
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
244
|
+
const repaired = withSaidMessages(
|
|
245
|
+
await models.call(
|
|
246
|
+
profile,
|
|
247
|
+
"turn",
|
|
248
|
+
`${turnSystem}\n${CONTRIBUTION_REPAIR}`,
|
|
249
|
+
{
|
|
250
|
+
...turnInput,
|
|
251
|
+
contributionRepair: {
|
|
252
|
+
previousReply: raw.bubbles || [],
|
|
253
|
+
},
|
|
246
254
|
},
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
255
|
+
trace,
|
|
256
|
+
images,
|
|
257
|
+
),
|
|
250
258
|
);
|
|
251
259
|
const repairedExchange = botExchange(snapshot, repaired);
|
|
252
260
|
if (
|
|
@@ -343,7 +351,7 @@ function liftMisplaced(raw, trace) {
|
|
|
343
351
|
return out;
|
|
344
352
|
}
|
|
345
353
|
|
|
346
|
-
export function normalizeTurn(raw, snapshot, trace) {
|
|
354
|
+
export function normalizeTurn(raw, snapshot, trace, { speech } = {}) {
|
|
347
355
|
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
348
356
|
throw SyntaxError("回合输出不是 JSON 对象");
|
|
349
357
|
const known = new Set((snapshot.messages || []).map((m) => m.id));
|
|
@@ -393,13 +401,21 @@ export function normalizeTurn(raw, snapshot, trace) {
|
|
|
393
401
|
"这是一句完整回应,按正常发言保留,不为短反应标签改写原意",
|
|
394
402
|
);
|
|
395
403
|
}
|
|
396
|
-
const bubbleLimit = maxBubbles(choice, snapshot);
|
|
404
|
+
const bubbleLimit = maxBubbles(choice, snapshot, speech);
|
|
397
405
|
if (bubbles.length > bubbleLimit) {
|
|
398
406
|
if (choice === "speak" && bubbleLimit === 1) {
|
|
399
407
|
bubbles = [joinBubbleParts(bubbles)];
|
|
400
408
|
trace?.steps?.push("根据对方的偏好,把连续补充合成一条完整消息");
|
|
401
409
|
} else {
|
|
402
|
-
|
|
410
|
+
// Fold the overflow into the last allowed message instead of losing
|
|
411
|
+
// what she meant to say.
|
|
412
|
+
bubbles =
|
|
413
|
+
choice === "speak"
|
|
414
|
+
? [
|
|
415
|
+
...bubbles.slice(0, bubbleLimit - 1),
|
|
416
|
+
joinBubbleParts(bubbles.slice(bubbleLimit - 1)),
|
|
417
|
+
]
|
|
418
|
+
: bubbles.slice(0, bubbleLimit);
|
|
403
419
|
trace?.steps?.push(
|
|
404
420
|
choice === "react"
|
|
405
421
|
? "简短反应只留第一条气泡"
|
|
@@ -430,6 +446,15 @@ export function normalizeTurn(raw, snapshot, trace) {
|
|
|
430
446
|
!crisis &&
|
|
431
447
|
!snapshot.initiative &&
|
|
432
448
|
(grounding.botLoop ||
|
|
449
|
+
// She read the other AI's line as a goodbye herself; answering a
|
|
450
|
+
// goodbye from a bot only starts another round of goodbyes.
|
|
451
|
+
// Who is saying goodbye matters, not how long the bots have talked.
|
|
452
|
+
(raw.understanding?.kind === "closing" &&
|
|
453
|
+
grounding.botMessageIds.length > 0 &&
|
|
454
|
+
batch
|
|
455
|
+
.filter((m) => m.role === "user")
|
|
456
|
+
.every((m) => grounding.botMessageIds.includes(m.id)) &&
|
|
457
|
+
!batch.some((m) => m.relation === "direct")) ||
|
|
433
458
|
(raw.contribution?.kind === "none" &&
|
|
434
459
|
!raw.act &&
|
|
435
460
|
!raw.share &&
|
|
@@ -556,3 +581,32 @@ export function wordingNotes(turn, snapshot) {
|
|
|
556
581
|
.filter(Boolean)
|
|
557
582
|
.join("\n");
|
|
558
583
|
}
|
|
584
|
+
|
|
585
|
+
// Writing or bot-conversation guidance, only when the conversation is about
|
|
586
|
+
// those things. How the room types is deliberately not given: her voice comes
|
|
587
|
+
// from her own state and past words, not from imitating the people here.
|
|
588
|
+
function turnGuides(snapshot) {
|
|
589
|
+
const needs = conversationNeeds(snapshot);
|
|
590
|
+
return {
|
|
591
|
+
...(needs.creative ? { creativeGuide: CREATIVE_GUIDE } : {}),
|
|
592
|
+
...(needs.bots ? { botGuide: BOT_GUIDE } : {}),
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// She writes what she would type as one piece of text; a blank line is where
|
|
597
|
+
// she would press send and start another message. Writing one message is the
|
|
598
|
+
// natural default of typing, so how many she sends is a choice she makes,
|
|
599
|
+
// not a list she is handed to fill. An older reply with "bubbles" still works.
|
|
600
|
+
export function saidMessages(text) {
|
|
601
|
+
return String(text)
|
|
602
|
+
.split(/\n[ \t\u3000]*\n+/)
|
|
603
|
+
.map((part) => part.trim())
|
|
604
|
+
.filter(Boolean);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
export function withSaidMessages(raw) {
|
|
608
|
+
if (!raw || typeof raw !== "object" || typeof raw.say !== "string")
|
|
609
|
+
return raw;
|
|
610
|
+
const { say, ...rest } = raw;
|
|
611
|
+
return { ...rest, bubbles: saidMessages(say) };
|
|
612
|
+
}
|
package/server/i18n/errors.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// key, exactly as the code writes it. `{0}` stands for a value the code puts
|
|
3
3
|
// into the message; patterns are used when a message carries one.
|
|
4
4
|
export const ERRORS = {
|
|
5
|
+
复审超过截止时间: "The reply review passed its deadline",
|
|
5
6
|
"JSON 同一对象含重复字段,不能确定哪份内容有效":
|
|
6
7
|
"A JSON object contains duplicate fields; the intended value is ambiguous",
|
|
7
8
|
"写日记期间关系记录已变化,本次旧稿未保存,下次按新认识整理":
|
|
@@ -33,6 +34,23 @@ export const ERRORS = {
|
|
|
33
34
|
"The update did not complete. The original program and data were retained. Check the runtime log before trying again.",
|
|
34
35
|
"自动恢复未完成,更新前的校验备份已保留,请查看运行日志并手动恢复。":
|
|
35
36
|
"Automatic recovery did not complete. The verified pre-update backup was retained. Check the runtime log and recover manually.",
|
|
37
|
+
"缺少候选版本的数据迁移器,已停止更新":
|
|
38
|
+
"The candidate release has no data migrator. The update was stopped.",
|
|
39
|
+
迁移目标不是现有的绝对路径数据库文件:
|
|
40
|
+
"The migration target must be an existing database at an absolute path",
|
|
41
|
+
迁移目标版本与候选程序不一致:
|
|
42
|
+
"The migration target version does not match the candidate release",
|
|
43
|
+
迁移目标数据库与更新前备份不匹配:
|
|
44
|
+
"The migration database does not match the pre-update backup",
|
|
45
|
+
数据库未迁移到候选版本要求的结构:
|
|
46
|
+
"The database was not migrated to the schema required by the candidate release",
|
|
47
|
+
迁移后数据库身份发生变化: "The database identity changed during migration",
|
|
48
|
+
"迁移后的数据库仍有未关闭的 SQLite 事务":
|
|
49
|
+
"The migrated database still has an open SQLite transaction",
|
|
50
|
+
"旧数据库仍被占用,无法安全切换":
|
|
51
|
+
"The old database is still in use and cannot be switched safely",
|
|
52
|
+
"数据库仍有未关闭的 SQLite 事务文件,请重试更新":
|
|
53
|
+
"SQLite transaction files are still open. Try the update again.",
|
|
36
54
|
修改机会需要明确的记录与原因:
|
|
37
55
|
"An edit opportunity needs an explicit record and reason",
|
|
38
56
|
修改机会记录无效: "Invalid edit opportunity record",
|
|
@@ -283,6 +301,8 @@ export const ERRORS = {
|
|
|
283
301
|
向量数量与输入不一致: "The vector count does not match the input",
|
|
284
302
|
向量内容无效: "The vector content is invalid",
|
|
285
303
|
向量维度不一致: "The vector dimensions are inconsistent",
|
|
304
|
+
"向量服务不支持自动长度,已改用模型自己的长度":
|
|
305
|
+
"The vector service does not support the automatic length; using the model's own length",
|
|
286
306
|
知识库名称无效: "The collection name is invalid",
|
|
287
307
|
知识库不存在: "The collection does not exist",
|
|
288
308
|
"文档内容应为 1–400000 字": "The document must be 1–400000 characters",
|
|
@@ -550,7 +570,17 @@ export const ERRORS = {
|
|
|
550
570
|
// Messages that carry a value. `{0}`, `{1}` are filled with what the code put
|
|
551
571
|
// in; the values are passed through the dictionaries again.
|
|
552
572
|
export const ERROR_PATTERNS = [
|
|
573
|
+
["记忆索引重建失败:{0}", "Memory index rebuild failed: {0}"],
|
|
574
|
+
["迁移后缺少原数据表:{0}", "The migration is missing original table: {0}"],
|
|
575
|
+
[
|
|
576
|
+
"迁移后 {0} 记录数减少({1} → {2}),已停止更新",
|
|
577
|
+
"The migration reduced {0} rows ({1} → {2}); the update was stopped",
|
|
578
|
+
],
|
|
553
579
|
["删除失败({0})", "Removal failed ({0})"],
|
|
580
|
+
[
|
|
581
|
+
"向量维度不一致:接口返回 {0} 维,应为 {1} 维",
|
|
582
|
+
"The vector dimensions are inconsistent: the service returned {0}, expected {1}",
|
|
583
|
+
],
|
|
554
584
|
["清理工作异常结束:{0}", "Cleanup ended unexpectedly: {0}"],
|
|
555
585
|
["{0} Prompt 无效", "{0} prompt is invalid"],
|
|
556
586
|
["模型请求失败 HTTP {0}:{1}", "The model request failed with HTTP {0}: {1}"],
|
package/server/i18n/labels.js
CHANGED
|
@@ -145,6 +145,11 @@ export const LABELS = {
|
|
|
145
145
|
刚才: "just now",
|
|
146
146
|
今天早些时候: "earlier today",
|
|
147
147
|
好几天前: "several days ago",
|
|
148
|
+
一周前: "a week ago",
|
|
149
|
+
两三周前: "two or three weeks ago",
|
|
150
|
+
一个多月前: "over a month ago",
|
|
151
|
+
几个月前: "a few months ago",
|
|
152
|
+
一年多前: "over a year ago",
|
|
148
153
|
很久以前: "a long time ago",
|
|
149
154
|
还未相遇: "Not met yet",
|
|
150
155
|
"还没有真实聊天,时间暂时没有可以延续的落点。":
|
package/server/index.js
CHANGED
|
@@ -15,6 +15,8 @@ import { openSync, closeSync } from "node:fs";
|
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import { UpdateChecker } from "./studio/updates.js";
|
|
17
17
|
import { InstanceUpdater } from "./runtime/update-manager.js";
|
|
18
|
+
import { migrateReleaseData } from "./runtime/data-migration.js";
|
|
19
|
+
import { VERSION } from "./version.js";
|
|
18
20
|
|
|
19
21
|
claimServer();
|
|
20
22
|
process.on("exit", clearStarting);
|
|
@@ -32,6 +34,7 @@ const chatSystem = new ChatSystem(store, channel.send, {
|
|
|
32
34
|
fetchQuoted: (message) => channel.fetchQuoted(message),
|
|
33
35
|
fetchImage: (file, sessionId) => channel.fetchImage(file, sessionId),
|
|
34
36
|
});
|
|
37
|
+
await migrateReleaseData(chatSystem.mind, { version: VERSION });
|
|
35
38
|
const life = new Life(chatSystem, {
|
|
36
39
|
online: () => channel.online(),
|
|
37
40
|
canReach: (session) => channel.canReach(session),
|
|
@@ -16,37 +16,94 @@ export const EMBEDDING_DEFAULTS = {
|
|
|
16
16
|
};
|
|
17
17
|
export const EMBEDDING_PROTOCOLS = new Set(["openai", "multimodal"]);
|
|
18
18
|
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
// Embeddings are any OpenAI-compatible /embeddings endpoint: an address, a
|
|
20
|
+
// model name and a key. Presets only fill in what the user left empty for
|
|
21
|
+
// providers known to need it; none of them is what "embedding" means.
|
|
22
|
+
// `chat: true` keeps the chat address, because that plan's key works only
|
|
23
|
+
// there (an Agent Plan key is refused by the ordinary /api/v3 address).
|
|
24
|
+
export const EMBEDDING_PRESETS = [
|
|
25
|
+
{
|
|
26
|
+
id: "volcengine-agent-plan",
|
|
27
|
+
label: "火山方舟 Agent Plan",
|
|
28
|
+
match: /volces\.com\/api\/plan\//i,
|
|
29
|
+
chat: true,
|
|
30
|
+
model: "doubao-embedding-vision",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "volcengine-ark",
|
|
34
|
+
label: "火山方舟",
|
|
35
|
+
match: /volces\.com|volcengine/i,
|
|
36
|
+
baseUrl: "https://ark.cn-beijing.volces.com/api/v3",
|
|
37
|
+
model: "doubao-embedding-vision",
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const trimUrl = (value) =>
|
|
42
|
+
String(value || "")
|
|
28
43
|
.trim()
|
|
29
44
|
.replace(/\/$/, "");
|
|
45
|
+
export const embeddingPreset = (address) =>
|
|
46
|
+
EMBEDDING_PRESETS.find((preset) => preset.match.test(trimUrl(address))) ||
|
|
47
|
+
null;
|
|
48
|
+
|
|
49
|
+
// Defaults when the user gives an address and a key. Dimension 0 means
|
|
50
|
+
// automatic: see automaticLength below. Without a preset the address is the
|
|
51
|
+
// chat one and the model must be named.
|
|
52
|
+
export function suggestEmbedding(chat = {}, address = "") {
|
|
53
|
+
const given = trimUrl(address);
|
|
54
|
+
const chatBase = trimUrl(chat.baseUrl);
|
|
30
55
|
const base = given || chatBase;
|
|
31
|
-
const
|
|
56
|
+
const preset = embeddingPreset(base);
|
|
32
57
|
return {
|
|
33
58
|
protocol: "openai",
|
|
34
59
|
dimensions: 0,
|
|
35
60
|
batchSize: 10,
|
|
36
61
|
timeoutMs: 90000,
|
|
37
|
-
baseUrl:
|
|
38
|
-
|
|
39
|
-
|
|
62
|
+
baseUrl: given || (preset && !preset.chat ? preset.baseUrl : chatBase),
|
|
63
|
+
model: preset?.model || String(chat.embeddingModel || ""),
|
|
64
|
+
preset: preset?.id || "",
|
|
40
65
|
};
|
|
41
66
|
}
|
|
42
67
|
|
|
68
|
+
// What "automatic" (dimension 0) asks a model for, per model rather than per
|
|
69
|
+
// provider. Every model is asked for nothing and keeps its own length unless
|
|
70
|
+
// an entry here names a shorter `automatic` one. doubao-embedding-vision has
|
|
71
|
+
// no such entry: on the recall benchmark with real vectors (2026-10-12) 1024
|
|
72
|
+
// ranked slightly worse than its own 2048 (one more miss and one more fact
|
|
73
|
+
// attributed to the wrong person in 59 cases), and a shorter default would
|
|
74
|
+
// make every install that already has 2048 vectors embed everything again.
|
|
75
|
+
// `native` says which explicit length is the same space as asking nothing.
|
|
76
|
+
export const MODEL_LENGTHS = [
|
|
77
|
+
{ model: /doubao-embedding-vision/i, native: 2048 },
|
|
78
|
+
];
|
|
79
|
+
const LENGTHS = MODEL_LENGTHS;
|
|
80
|
+
// Providers seen to refuse or ignore the automatic length, by address and
|
|
81
|
+
// model. Saving the profile again clears it.
|
|
82
|
+
const NATIVE = "embedding-native-length";
|
|
83
|
+
|
|
84
|
+
const modelOf = (profile) => profile.embeddingModel || profile.model || "";
|
|
85
|
+
const lengthsOf = (profile) =>
|
|
86
|
+
LENGTHS.find((entry) => entry.model.test(modelOf(profile)));
|
|
87
|
+
const nativeKey = (profile) => `${profile.baseUrl || ""}|${modelOf(profile)}`;
|
|
88
|
+
|
|
89
|
+
// The length actually requested. 0 means "do not name one".
|
|
90
|
+
export function automaticLength(profile, refused = {}) {
|
|
91
|
+
if (profile.dimensions) return profile.dimensions;
|
|
92
|
+
if (refused[nativeKey(profile)]) return 0;
|
|
93
|
+
return lengthsOf(profile)?.automatic || 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Asking for the length a model returns anyway is the same space as not
|
|
97
|
+
// asking, so both share one signature: typing 2048 for doubao finds the
|
|
98
|
+
// vectors made with 0 instead of paying to embed everything twice.
|
|
43
99
|
export function embeddingSignature(profile) {
|
|
100
|
+
const length = profile.dimensions || 0;
|
|
44
101
|
return createHash("sha256")
|
|
45
102
|
.update(
|
|
46
103
|
JSON.stringify([
|
|
47
104
|
profile.baseUrl || "",
|
|
48
|
-
profile
|
|
49
|
-
profile
|
|
105
|
+
modelOf(profile),
|
|
106
|
+
length === lengthsOf(profile)?.native ? 0 : length,
|
|
50
107
|
...(profile.protocol === "multimodal" ? ["multimodal"] : []),
|
|
51
108
|
]),
|
|
52
109
|
)
|
|
@@ -96,25 +153,35 @@ export function saveEmbeddingProfile(repo, input) {
|
|
|
96
153
|
if (!Number.isSafeInteger(next[key]) || next[key] < min || next[key] > max)
|
|
97
154
|
throw Error(`${key} 超出范围`);
|
|
98
155
|
repo.saveConfig(CONFIG, next);
|
|
156
|
+
repo.saveConfig(NATIVE, {});
|
|
99
157
|
return publicEmbeddingProfile(repo);
|
|
100
158
|
}
|
|
101
159
|
|
|
102
160
|
export function resolveEmbeddingProfile(repo, models) {
|
|
103
161
|
const saved = repo.config(CONFIG, null);
|
|
104
|
-
if (saved) {
|
|
105
|
-
const profile = {
|
|
106
|
-
...EMBEDDING_DEFAULTS,
|
|
107
|
-
...saved,
|
|
108
|
-
id: "embedding",
|
|
109
|
-
embedding: saved.enabled,
|
|
110
|
-
embeddingModel: saved.model,
|
|
111
|
-
isolatedEmbeddingKey: true,
|
|
112
|
-
};
|
|
113
|
-
return { profile, signature: embeddingSignature(profile), legacy: false };
|
|
114
|
-
}
|
|
115
162
|
// Existing installations remain compatible until an independent profile is saved.
|
|
116
|
-
const
|
|
117
|
-
|
|
163
|
+
const base = saved
|
|
164
|
+
? {
|
|
165
|
+
...EMBEDDING_DEFAULTS,
|
|
166
|
+
...saved,
|
|
167
|
+
id: "embedding",
|
|
168
|
+
embedding: saved.enabled,
|
|
169
|
+
embeddingModel: saved.model,
|
|
170
|
+
isolatedEmbeddingKey: true,
|
|
171
|
+
}
|
|
172
|
+
: models.profile("default");
|
|
173
|
+
const refused = repo.config(NATIVE, {}) || {};
|
|
174
|
+
const dimensions = automaticLength(base, refused);
|
|
175
|
+
const profile = { ...base, dimensions };
|
|
176
|
+
if (!base.dimensions && dimensions)
|
|
177
|
+
// A provider that refuses the length, or answers another, is remembered;
|
|
178
|
+
// the next request goes back to the model's own length and signature.
|
|
179
|
+
profile.refuseLength = (reason) =>
|
|
180
|
+
repo.saveConfig(NATIVE, {
|
|
181
|
+
...(repo.config(NATIVE, {}) || {}),
|
|
182
|
+
[nativeKey(base)]: { at: Date.now(), reason: String(reason) },
|
|
183
|
+
});
|
|
184
|
+
return { profile, signature: embeddingSignature(profile), legacy: !saved };
|
|
118
185
|
}
|
|
119
186
|
|
|
120
187
|
// A provider may accept fewer inputs per request than configured; its own
|
|
@@ -142,6 +209,8 @@ export async function embedBatches(models, profile, texts, options = {}) {
|
|
|
142
209
|
batchLimits.set(limitKey, size);
|
|
143
210
|
continue;
|
|
144
211
|
}
|
|
212
|
+
if (profile.refuseLength && Number(error?.status) === 400)
|
|
213
|
+
await refusedLength(models, profile, batch, options, error);
|
|
145
214
|
throw error;
|
|
146
215
|
}
|
|
147
216
|
start += batch.length;
|
|
@@ -156,9 +225,37 @@ export async function embedBatches(models, profile, texts, options = {}) {
|
|
|
156
225
|
)
|
|
157
226
|
throw Error("向量内容无效");
|
|
158
227
|
dimension ||= vector.length;
|
|
159
|
-
if (vector.length !== dimension)
|
|
228
|
+
if (vector.length !== dimension) {
|
|
229
|
+
if (profile.refuseLength && vector.length !== profile.dimensions) {
|
|
230
|
+
profile.refuseLength(`answered ${vector.length}`);
|
|
231
|
+
throw lengthChanged();
|
|
232
|
+
}
|
|
233
|
+
throw Error(
|
|
234
|
+
`向量维度不一致:接口返回 ${vector.length} 维,应为 ${dimension} 维`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
160
237
|
vectors.push(vector);
|
|
161
238
|
}
|
|
162
239
|
}
|
|
163
240
|
return vectors;
|
|
164
241
|
}
|
|
242
|
+
|
|
243
|
+
const lengthChanged = () =>
|
|
244
|
+
Object.assign(Error("向量服务不支持自动长度,已改用模型自己的长度"), {
|
|
245
|
+
lengthChanged: true,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// A 400 may be the length or anything else (a bad key arrives as 400 too).
|
|
249
|
+
// One request without the length tells which. Vectors already made under
|
|
250
|
+
// the old signature must not be saved, so the caller still gets an error
|
|
251
|
+
// and asks again under the new one.
|
|
252
|
+
async function refusedLength(models, profile, batch, options, error) {
|
|
253
|
+
try {
|
|
254
|
+
const { refuseLength, beforeBatch, ...plain } = profile;
|
|
255
|
+
await models.embed({ ...plain, dimensions: 0 }, batch.slice(0, 1), options);
|
|
256
|
+
} catch {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
profile.refuseLength(error.detail || error.message);
|
|
260
|
+
throw lengthChanged();
|
|
261
|
+
}
|