luckytri 1.3.0 → 1.3.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/docs/en/CHANGELOG.md +16 -0
- package/docs/zh/CHANGELOG.md +16 -0
- package/package.json +1 -1
- package/public/guide.en.html +3 -3
- package/public/guide.html +3 -3
- package/server/core/conversation-grounding.js +216 -1
- package/server/core/conversation-needs.js +10 -3
- package/server/core/dialogue-context.js +171 -26
- package/server/core/dialogue-contract.js +2 -2
- package/server/core/message-scheduler.js +13 -10
- package/server/core/reply-delivery.js +35 -15
- package/server/core/response-validator.js +1 -1
- package/server/core/turn-processor.js +2 -0
- package/server/core/turn.js +20 -1
- package/server/core/understanding.js +60 -4
- package/server/core/vision-manager.js +76 -7
- package/server/i18n/decisions.js +10 -5
- package/server/i18n/errors.js +2 -0
- package/server/mind/guard.js +88 -68
- package/server/mind/relationship-context.js +110 -71
- package/server/mind/time/sharing.js +582 -482
- package/server/mind/time/works.js +51 -48
- package/server/runtime/update-manager.js +34 -2
- package/server/stickers/offer.js +44 -15
|
@@ -406,57 +406,60 @@ export class Works {
|
|
|
406
406
|
const terms = interestTerms([words]);
|
|
407
407
|
// Search the work catalogue, not just the two newest activities. Select
|
|
408
408
|
// the version that existed at the requested time, including in replays.
|
|
409
|
-
return
|
|
410
|
-
.
|
|
411
|
-
|
|
409
|
+
return (
|
|
410
|
+
this.db
|
|
411
|
+
.prepare(
|
|
412
|
+
`SELECT w.*,COALESCE(p.kind,(SELECT kind FROM mind_creations WHERE id=w.legacy_creation)) kind,p.title project_title,p.bible,v.version,v.title,v.created version_created,v.summary,v.sources,v.provenance,length(v.content) characters
|
|
412
413
|
FROM mind_time_works w LEFT JOIN mind_time_projects p ON p.id=w.project_id
|
|
413
414
|
JOIN mind_time_versions v ON v.work_id=w.id AND v.version=(SELECT max(h.version) FROM mind_time_versions h WHERE h.work_id=w.id AND h.created<=?)
|
|
414
415
|
WHERE w.created<=? ORDER BY v.created DESC`,
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
(
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
416
|
+
)
|
|
417
|
+
.all(now, now)
|
|
418
|
+
.filter(
|
|
419
|
+
(w) =>
|
|
420
|
+
w.created <= now &&
|
|
421
|
+
this.time.visible(
|
|
422
|
+
{ ...w, sources: parse(w.sources, []) },
|
|
423
|
+
session,
|
|
424
|
+
now,
|
|
425
|
+
),
|
|
426
|
+
)
|
|
427
|
+
.map((w) => ({
|
|
428
|
+
...w,
|
|
429
|
+
score: [...interestTerms([w.title, w.project_title, w.summary])]
|
|
430
|
+
.filter((term) => terms.has(term))
|
|
431
|
+
.reduce(
|
|
432
|
+
(total, term) =>
|
|
433
|
+
total + (/^[a-z][a-z0-9]{2,}$/.test(term) ? 8 : 1),
|
|
434
|
+
0,
|
|
435
|
+
),
|
|
436
|
+
}))
|
|
437
|
+
.sort(
|
|
438
|
+
(a, b) => b.score - a.score || b.version_created - a.version_created,
|
|
439
|
+
)
|
|
440
|
+
// No topic hit → empty. Do not fall back to "latest two" and pollute
|
|
441
|
+
// ordinary chatter with manuscript fragments.
|
|
442
|
+
.filter((w) => w.score >= 2)
|
|
443
|
+
.filter((w) => this.materialSupported(w))
|
|
444
|
+
.slice(0, 3)
|
|
445
|
+
.map((w) => ({
|
|
446
|
+
id: w.id,
|
|
447
|
+
title: w.title,
|
|
448
|
+
state: this.get(w.id, w.version, { before: now }).state,
|
|
449
|
+
kind: w.kind,
|
|
450
|
+
version: w.version,
|
|
451
|
+
ordinal: w.ordinal,
|
|
452
|
+
summary: text(w.summary, 180),
|
|
453
|
+
fragment: text(this.get(w.id, w.version).content, 300),
|
|
454
|
+
...(w.kind === "game"
|
|
455
|
+
? {
|
|
456
|
+
experienceMode: "reference",
|
|
457
|
+
provenance: this.get(w.id, w.version).provenance,
|
|
458
|
+
}
|
|
459
|
+
: {}),
|
|
460
|
+
characters: w.characters,
|
|
461
|
+
}))
|
|
462
|
+
);
|
|
460
463
|
}
|
|
461
464
|
|
|
462
465
|
activityEvidence({ session, now = this.time.now(), cue = [] } = {}) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
2
3
|
import { mkdirSync, readFileSync } from "node:fs";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import {
|
|
@@ -33,14 +34,45 @@ export const isUpdateFinished = (stage) =>
|
|
|
33
34
|
stage === "done" || stage === "error";
|
|
34
35
|
const beforeStop = (stage) =>
|
|
35
36
|
["queued", "downloading", "checking"].includes(stage);
|
|
37
|
+
function isZombieProcess(pid) {
|
|
38
|
+
if (process.platform === "linux") {
|
|
39
|
+
try {
|
|
40
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
41
|
+
const commandEnd = stat.lastIndexOf(")");
|
|
42
|
+
return commandEnd >= 0 && stat[commandEnd + 2] === "Z";
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// A process can disappear after kill(pid, 0) succeeds but before its
|
|
45
|
+
// proc entry is read. Treat that race as an exited process.
|
|
46
|
+
return error.code === "ENOENT";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (process.platform === "darwin") {
|
|
50
|
+
try {
|
|
51
|
+
const state = execFileSync("ps", ["-o", "stat=", "-p", String(pid)], {
|
|
52
|
+
encoding: "utf8",
|
|
53
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
54
|
+
timeout: 1000,
|
|
55
|
+
windowsHide: true,
|
|
56
|
+
}).trimStart();
|
|
57
|
+
return !state || state.startsWith("Z");
|
|
58
|
+
} catch (error) {
|
|
59
|
+
// `ps` reports no rows with status 1 when the process exited in the gap
|
|
60
|
+
// after kill(pid, 0). Keep other inspection failures conservative.
|
|
61
|
+
return error.status === 1 || error.code === "ENOENT" ? false : true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
36
66
|
export const processAlive = (pid) => {
|
|
37
67
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
38
68
|
try {
|
|
39
69
|
process.kill(pid, 0);
|
|
40
|
-
return true;
|
|
41
70
|
} catch (e) {
|
|
42
|
-
|
|
71
|
+
if (e.code === "ESRCH") return false;
|
|
43
72
|
}
|
|
73
|
+
// On POSIX, kill(pid, 0) also succeeds for a zombie. A zombie cannot run or
|
|
74
|
+
// answer requests, so keep the foreground supervisor from waiting forever.
|
|
75
|
+
return !isZombieProcess(pid);
|
|
44
76
|
};
|
|
45
77
|
export function readUpdateJob(home, id) {
|
|
46
78
|
updatePath(home, id);
|
package/server/stickers/offer.js
CHANGED
|
@@ -103,24 +103,53 @@ export function expressionInput(snapshot) {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
// "@小红" typed as text is not an @ on QQ. A name she wrote that way for
|
|
106
|
-
// someone offered becomes a real
|
|
107
|
-
// message does not show the name twice.
|
|
106
|
+
// someone offered becomes a real @ on THAT bubble only, and the typed copy
|
|
107
|
+
// is removed so the message does not show the name twice. Two bubbles for
|
|
108
|
+
// two people must not dump both @s onto the first bubble.
|
|
108
109
|
export function typedAt(bubbles, at, snapshot) {
|
|
109
110
|
const offered = snapshot.canAt || [];
|
|
110
|
-
const
|
|
111
|
-
const out =
|
|
112
|
-
|
|
111
|
+
const fromTurn = new Map((at || []).map((p) => [String(p.userId), p]));
|
|
112
|
+
const out = [];
|
|
113
|
+
const atByBubble = [];
|
|
114
|
+
const typedIds = new Set();
|
|
115
|
+
for (const line of bubbles || []) {
|
|
116
|
+
let text = String(line || "");
|
|
117
|
+
const onThis = new Map();
|
|
113
118
|
for (const p of offered) {
|
|
119
|
+
if (!p.name) continue;
|
|
114
120
|
const mark = `@${p.name}`;
|
|
115
|
-
if (!
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
121
|
+
if (!text.includes(mark)) continue;
|
|
122
|
+
const id = String(p.userId);
|
|
123
|
+
typedIds.add(id);
|
|
124
|
+
onThis.set(id, fromTurn.get(id) || p);
|
|
125
|
+
text = text
|
|
126
|
+
.split(mark)
|
|
127
|
+
.join("")
|
|
128
|
+
.replace(/^[\s,,]+/, "");
|
|
122
129
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
130
|
+
text = text.trim();
|
|
131
|
+
if (!text) continue;
|
|
132
|
+
out.push(text);
|
|
133
|
+
atByBubble.push([...onThis.values()].slice(0, 2));
|
|
134
|
+
}
|
|
135
|
+
const placed = new Set(atByBubble.flat().map((p) => String(p.userId)));
|
|
136
|
+
const unplaced = [...fromTurn.values()].filter(
|
|
137
|
+
(p) => !placed.has(String(p.userId)) && !typedIds.has(String(p.userId)),
|
|
138
|
+
);
|
|
139
|
+
if (unplaced.length) {
|
|
140
|
+
if (out.length === 1) {
|
|
141
|
+
atByBubble[0] = [...(atByBubble[0] || []), ...unplaced].slice(0, 2);
|
|
142
|
+
} else if (out.length > 1 && atByBubble.every((list) => !list.length)) {
|
|
143
|
+
// turn.at with no typed @ marks: pair person i with bubble i
|
|
144
|
+
unplaced.forEach((p, i) => {
|
|
145
|
+
if (i < atByBubble.length) atByBubble[i] = [p];
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
bubbles: out,
|
|
151
|
+
atByBubble,
|
|
152
|
+
// First bubble only — never the union of every addressee.
|
|
153
|
+
at: atByBubble[0] || [],
|
|
154
|
+
};
|
|
126
155
|
}
|