luckytri 1.2.0 → 1.2.2
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 +12 -14
- package/README.md +1 -3
- package/docs/en/CHANGELOG.md +17 -1
- package/docs/en/architecture.md +2 -2
- package/docs/en/guide.md +2 -0
- package/docs/en/npm-release.md +6 -0
- package/docs/zh/CHANGELOG.md +17 -1
- package/docs/zh/architecture.md +2 -2
- package/docs/zh/guide.md +2 -0
- package/docs/zh/npm-release.md +8 -0
- package/package.json +25 -3
- package/public/app/assets/{ChatsPage-eDqFIg97.js → ChatsPage-GGOaUl3H.js} +1 -1
- package/public/app/assets/{Empty-CAGOJsfD.js → Empty-BSmsWoGM.js} +1 -1
- package/public/app/assets/{Field-CRz-JmN1.js → Field-tUGp0-YL.js} +1 -1
- package/public/app/assets/{HeartPage-BGWoeMrC.js → HeartPage-B5_e1hI9.js} +1 -1
- package/public/app/assets/{LifePage-iao3o3wt.js → LifePage-D5P5kTzy.js} +1 -1
- package/public/app/assets/{MemoryPage-Dwi2yi7E.js → MemoryPage-BrSPiqOS.js} +1 -1
- package/public/app/assets/{NaturePage-DniImo9U.js → NaturePage-C0BdvfFu.js} +1 -1
- package/public/app/assets/{PeoplePage-D-MC3JEt.js → PeoplePage-DBT70lu7.js} +1 -1
- package/public/app/assets/{PluginsPage-D7dJTf96.js → PluginsPage-QBiQg3Jc.js} +1 -1
- package/public/app/assets/{SystemPage-C_Y5M7JU.css → SystemPage-Cf8lKh5C.css} +1 -1
- package/public/app/assets/{SystemPage-C5CUHjGh.js → SystemPage-EFOEvom-.js} +1 -1
- package/public/app/assets/{TimePage-Dwhr3-92.js → TimePage-BuNKjCgN.js} +1 -1
- package/public/app/assets/{index-L0bHax8C.js → index-BaXEYeFH.js} +3 -3
- package/public/app/assets/{knowledge-BTDNafzl.js → knowledge-COyBves6.js} +1 -1
- package/public/app/assets/{media-BBzOAfNG.js → media-8uRD5Ocs.js} +1 -1
- package/public/app/assets/{mind-CBTkFje2.js → mind-CQkvddhx.js} +1 -1
- package/public/app/index.html +3 -3
- package/public/guide.en.html +14 -3
- package/public/guide.html +15 -3
- package/public/tutorial.en.md +2 -0
- package/public/tutorial.md +2 -0
- package/scripts/service-supervisor.js +8 -13
- package/scripts/start-server.js +8 -1
- package/scripts/update-instance.js +93 -27
- package/server/core/context-builder.js +4 -1
- package/server/core/dialogue-contract.js +1 -1
- package/server/core/response-validator.js +18 -2
- package/server/core/turn.js +30 -1
- package/server/i18n/errors.js +36 -0
- package/server/mind/affect.js +4 -2
- package/server/mind/view.js +4 -1
- package/server/runtime/update-errors.js +73 -0
- package/server/runtime/update-execution.js +13 -8
- package/server/runtime/update-manager.js +30 -8
- package/docs/en/evolution.md +0 -35
- package/docs/en/vision.md +0 -50
- package/docs/zh/evolution.md +0 -35
- package/docs/zh/vision.md +0 -50
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
"downloading",
|
|
8
|
-
"checking",
|
|
9
|
-
"stopping",
|
|
10
|
-
"backup",
|
|
11
|
-
"restarting",
|
|
12
|
-
"verifying",
|
|
13
|
-
"restoring",
|
|
14
|
-
]);
|
|
3
|
+
import {
|
|
4
|
+
processAlive,
|
|
5
|
+
isUpdateFinished,
|
|
6
|
+
} from "../server/runtime/update-manager.js";
|
|
15
7
|
|
|
16
8
|
function readJob(home) {
|
|
17
9
|
try {
|
|
@@ -34,7 +26,10 @@ export async function updatedServerAfterExit(
|
|
|
34
26
|
while (true) {
|
|
35
27
|
const job = read();
|
|
36
28
|
if (!job || job.parent !== parentPid) return { matched: false, pid: null };
|
|
37
|
-
|
|
29
|
+
// This supervisor can outlive its installed release. A future worker may
|
|
30
|
+
// introduce stages this older process does not know; only explicit terminal
|
|
31
|
+
// states permit handover while the worker is still alive.
|
|
32
|
+
if (isUpdateFinished(job.status))
|
|
38
33
|
return { matched: true, pid: alive(job.childPid) ? job.childPid : null };
|
|
39
34
|
if (!alive(job.workerPid))
|
|
40
35
|
return { matched: true, pid: alive(job.childPid) ? job.childPid : null };
|
package/scripts/start-server.js
CHANGED
|
@@ -30,7 +30,12 @@ else console.log("未检测到系统代理,外部 API 请求将直连。");
|
|
|
30
30
|
let currentPid;
|
|
31
31
|
for (const signal of ["SIGINT", "SIGTERM"])
|
|
32
32
|
process.on(signal, () => {
|
|
33
|
-
if (currentPid)
|
|
33
|
+
if (currentPid)
|
|
34
|
+
try {
|
|
35
|
+
process.kill(currentPid, signal);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (error.code !== "ESRCH") throw error;
|
|
38
|
+
}
|
|
34
39
|
});
|
|
35
40
|
|
|
36
41
|
async function start(root) {
|
|
@@ -71,6 +76,7 @@ try {
|
|
|
71
76
|
} else {
|
|
72
77
|
await waitForProcessExit(tracked.pid);
|
|
73
78
|
}
|
|
79
|
+
currentPid = undefined;
|
|
74
80
|
|
|
75
81
|
const update = await updatedServerAfterExit(paths.home, tracked.pid);
|
|
76
82
|
if (!update.matched) break;
|
|
@@ -80,6 +86,7 @@ try {
|
|
|
80
86
|
// after rollback). Keep this foreground process alive so systemd does not
|
|
81
87
|
// kill that process or any still-running update work in the same cgroup.
|
|
82
88
|
tracked = { pid: update.pid };
|
|
89
|
+
currentPid = update.pid;
|
|
83
90
|
direct = false;
|
|
84
91
|
continue;
|
|
85
92
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
writeFileSync,
|
|
6
6
|
readFileSync,
|
|
7
7
|
realpathSync,
|
|
8
|
+
writeSync,
|
|
8
9
|
} from "node:fs";
|
|
9
10
|
import { dirname, join, delimiter } from "node:path";
|
|
10
11
|
import { pathToFileURL } from "node:url";
|
|
@@ -23,6 +24,21 @@ import {
|
|
|
23
24
|
writeUpdateJob,
|
|
24
25
|
} from "../server/runtime/update-manager.js";
|
|
25
26
|
import { updateRoot } from "../server/runtime/installation.js";
|
|
27
|
+
import {
|
|
28
|
+
npmFailureCode,
|
|
29
|
+
updateError,
|
|
30
|
+
} from "../server/runtime/update-errors.js";
|
|
31
|
+
|
|
32
|
+
export function requireUpdateNode(version = process.versions.node) {
|
|
33
|
+
const [major, minor] = String(version).split(".").map(Number);
|
|
34
|
+
if (
|
|
35
|
+
!Number.isInteger(major) ||
|
|
36
|
+
!Number.isInteger(minor) ||
|
|
37
|
+
major < 24 ||
|
|
38
|
+
(major === 24 && minor < 5)
|
|
39
|
+
)
|
|
40
|
+
throw updateError("NODE_UNSUPPORTED");
|
|
41
|
+
}
|
|
26
42
|
|
|
27
43
|
export function installEnvironment(env) {
|
|
28
44
|
const kept = {};
|
|
@@ -35,11 +51,11 @@ export function installEnvironment(env) {
|
|
|
35
51
|
kept[key] = value;
|
|
36
52
|
return kept;
|
|
37
53
|
}
|
|
38
|
-
export function findNpmCli(env = process.env) {
|
|
54
|
+
export function findNpmCli(env = process.env, execPath = process.execPath) {
|
|
39
55
|
const candidates = [
|
|
40
56
|
env.npm_execpath,
|
|
41
|
-
join(dirname(
|
|
42
|
-
join(dirname(
|
|
57
|
+
join(dirname(execPath), "node_modules/npm/bin/npm-cli.js"),
|
|
58
|
+
join(dirname(execPath), "../lib/node_modules/npm/bin/npm-cli.js"),
|
|
43
59
|
...String(env.PATH || env.Path || "")
|
|
44
60
|
.split(delimiter)
|
|
45
61
|
.flatMap((dir) => [
|
|
@@ -60,31 +76,73 @@ export function findNpmCli(env = process.env) {
|
|
|
60
76
|
/* Try the next native Node/npm installation location. */
|
|
61
77
|
}
|
|
62
78
|
}
|
|
63
|
-
throw
|
|
79
|
+
throw updateError("NPM_NOT_FOUND");
|
|
64
80
|
}
|
|
65
|
-
function
|
|
81
|
+
export function runUpdateCommand(
|
|
82
|
+
args,
|
|
83
|
+
{
|
|
84
|
+
cwd,
|
|
85
|
+
env = process.env,
|
|
86
|
+
timeout = 600000,
|
|
87
|
+
log,
|
|
88
|
+
failureCode = "STEP_FAILED",
|
|
89
|
+
npmInstall = false,
|
|
90
|
+
} = {},
|
|
91
|
+
) {
|
|
66
92
|
return new Promise((resolve, reject) => {
|
|
67
93
|
const fd = log ? openSync(log, "a", 0o600) : null;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
94
|
+
let timer,
|
|
95
|
+
child,
|
|
96
|
+
settled = false,
|
|
97
|
+
timedOut = false,
|
|
98
|
+
outputError,
|
|
99
|
+
tail = "";
|
|
100
|
+
const finish = (error) => {
|
|
101
|
+
if (settled) return;
|
|
102
|
+
settled = true;
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
try {
|
|
105
|
+
if (fd !== null) closeSync(fd);
|
|
106
|
+
} catch (closeError) {
|
|
107
|
+
error ||= closeError;
|
|
108
|
+
}
|
|
109
|
+
if (error) reject(error);
|
|
110
|
+
else resolve();
|
|
111
|
+
};
|
|
112
|
+
try {
|
|
113
|
+
child = spawn(process.execPath, args, {
|
|
114
|
+
cwd,
|
|
115
|
+
env,
|
|
116
|
+
windowsHide: true,
|
|
117
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
118
|
+
});
|
|
119
|
+
} catch (error) {
|
|
120
|
+
finish(error);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const capture = (chunk) => {
|
|
124
|
+
if (settled) return;
|
|
125
|
+
tail = (tail + chunk.toString("utf8")).slice(-16384);
|
|
126
|
+
try {
|
|
127
|
+
if (fd !== null) writeSync(fd, chunk);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
outputError = error;
|
|
130
|
+
// Wait for exit before rollback can rename a migration database.
|
|
131
|
+
child.kill("SIGKILL");
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
child.stdout.on("data", capture);
|
|
135
|
+
child.stderr.on("data", capture);
|
|
136
|
+
timer = setTimeout(() => {
|
|
77
137
|
timedOut = true;
|
|
78
|
-
child.kill();
|
|
138
|
+
child.kill("SIGKILL");
|
|
79
139
|
}, timeout);
|
|
80
|
-
child.once("error",
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (timedOut) reject(Error("更新步骤超时"));
|
|
87
|
-
else code === 0 ? resolve() : reject(Error("更新程序校验失败"));
|
|
140
|
+
child.once("error", finish);
|
|
141
|
+
child.once("close", (code) => {
|
|
142
|
+
if (outputError) finish(outputError);
|
|
143
|
+
else if (timedOut) finish(updateError("STEP_TIMEOUT"));
|
|
144
|
+
else if (code === 0) finish();
|
|
145
|
+
else finish(updateError(npmInstall ? npmFailureCode(tail) : failureCode));
|
|
88
146
|
});
|
|
89
147
|
});
|
|
90
148
|
}
|
|
@@ -94,10 +152,11 @@ export function updateExecution(paths) {
|
|
|
94
152
|
const pause = () => new Promise((r) => setTimeout(r, 250));
|
|
95
153
|
return {
|
|
96
154
|
async install(slot, version) {
|
|
155
|
+
requireUpdateNode();
|
|
97
156
|
const cli = findNpmCli();
|
|
98
157
|
const userconfig = join(updateRoot(paths.home), "install.npmrc");
|
|
99
158
|
writeFileSync(userconfig, "", { mode: 0o600 });
|
|
100
|
-
await
|
|
159
|
+
await runUpdateCommand(
|
|
101
160
|
[
|
|
102
161
|
cli,
|
|
103
162
|
"install",
|
|
@@ -112,20 +171,26 @@ export function updateExecution(paths) {
|
|
|
112
171
|
"--engine-strict",
|
|
113
172
|
`luckytri@${version}`,
|
|
114
173
|
],
|
|
115
|
-
{
|
|
174
|
+
{
|
|
175
|
+
cwd: slot,
|
|
176
|
+
env: installEnvironment(process.env),
|
|
177
|
+
log: paths.log,
|
|
178
|
+
npmInstall: true,
|
|
179
|
+
},
|
|
116
180
|
);
|
|
117
181
|
},
|
|
118
182
|
async check(root, version) {
|
|
119
183
|
const script = `import fs from 'node:fs';import {createRequire} from 'node:module';const p=JSON.parse(fs.readFileSync(${JSON.stringify(join(root, "package.json"))},'utf8'));if(p.version!==${JSON.stringify(version)})process.exit(1);await import(${JSON.stringify(pathToFileURL(join(root, "server/app.js")).href)});const sharp=createRequire(${JSON.stringify(pathToFileURL(join(root, "package.json")).href)})('sharp');await sharp({create:{width:1,height:1,channels:3,background:'#fff'}}).png().toBuffer();`;
|
|
120
|
-
await
|
|
184
|
+
await runUpdateCommand(["--input-type=module", "-e", script], {
|
|
121
185
|
cwd: root,
|
|
122
186
|
env: installEnvironment(process.env),
|
|
123
187
|
timeout: 30000,
|
|
124
188
|
log: paths.log,
|
|
189
|
+
failureCode: "PACKAGE_CHECK_FAILED",
|
|
125
190
|
});
|
|
126
191
|
},
|
|
127
192
|
async migrate(root, database, { previous, version, databaseId }) {
|
|
128
|
-
await
|
|
193
|
+
await runUpdateCommand(
|
|
129
194
|
[
|
|
130
195
|
join(root, "scripts", "migrate-instance.js"),
|
|
131
196
|
database,
|
|
@@ -138,6 +203,7 @@ export function updateExecution(paths) {
|
|
|
138
203
|
env: installEnvironment(process.env),
|
|
139
204
|
timeout: 3600000,
|
|
140
205
|
log: paths.log,
|
|
206
|
+
failureCode: "MIGRATION_FAILED",
|
|
141
207
|
},
|
|
142
208
|
);
|
|
143
209
|
},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { oneTypedMessage } from "./response-validator.js";
|
|
1
2
|
import { effectivePersona, identityAliases } from "./persona-manager.js";
|
|
2
3
|
import { localClock, conversationCues } from "./conversation-cues.js";
|
|
3
4
|
import { estimateTokens } from "./model-manager.js";
|
|
@@ -102,7 +103,9 @@ export function buildContext(
|
|
|
102
103
|
localTime: localClock(m.time, policy.timeZone).local,
|
|
103
104
|
role: m.role,
|
|
104
105
|
...(m.artifact ? { artifact: m.artifact } : {}),
|
|
105
|
-
|
|
106
|
+
// Her own earlier messages are shown as typed lines: an old reply laid
|
|
107
|
+
// out a sentence per line would otherwise teach her that layout again.
|
|
108
|
+
text: m.role === "assistant" ? oneTypedMessage(m.text) : m.text,
|
|
106
109
|
mentions: m.mentions || [],
|
|
107
110
|
replyTo: m.replyTo,
|
|
108
111
|
replyChain: m.replyChain,
|
|
@@ -20,7 +20,7 @@ export const DIALOGUE_TASK = `读懂眼前这轮聊天,再用自己的话回
|
|
|
20
20
|
长短和口吻来自你自己:inner.ownVoice 是你自己一直以来打字的样子,inner.state 是此刻的心情和精力,再加上你和眼前这个人的关系、刚发生的事;别人怎么打字只是在场的情况,不去模仿。像在 QQ 里随手打一句,不写组织好的书面段落;答案不省,意思说够就停,不为活跃气氛加问题。问过的细节对方没接,就回应他继续说的内容,不立刻换词再问。如果这段对话里的人明确要求一段发完,同一话题后续都收进一条消息。列点也可以放在一条消息里换行。被明确要求停止就停止,新的实际问题仍可回应。
|
|
21
21
|
若一轮里同时有人提出表达偏好、另一人分享新内容,先简短确认偏好,再回应新内容;不能让确认格式代替对内容的回应。
|
|
22
22
|
停止的范围按对方原话理解:不要方案,就停止给方案;不要纠结某个细节,就放下那个细节,不把整个话题或交流关掉。对方明确说一条旁枝不是他问的,简短承认后回到主线;不要说“我没理清楚”或“先不乱说”,除非那条信息确实是回答主问题所必需的。
|
|
23
|
-
输出 JSON:{"understanding":{"kind":"question|proposal|feeling|sharing|correction|greeting|closing|other","messageIds":[本轮回应的序号],"point":"对方此刻的具体意思","tone":"serious|joking|teasing|ironic|venting","stance":"fact|guess|opinion|joke|hearsay"},"contribution":{"kind":"answer|reaction|question|idea|correction|none","point":"自己这次实际说出的意思,不是收住/接住之类动作"},"choice":"speak|react|decline|silent","targetMessageIds":[batchIds中的序号],"topic":"当前话题","say":"实际说的话","appraisal":"这件事对自己的实际影响,没有则空","reason":"为何这样选择,用名字不用QQ号","feelings":[{"feeling":"两三个字","intensity":0到1,"valence":-1到1,"arousal":0到1,"cause":[消息序号]}],"bonds":[{"userId":"speaker","change":"warmer|closer|trust_up|trust_down|friction|repair|distance","why":"具体变化","evidence":[消息序号]}],"crisis":{"clear":false,"messageIds":[]}}。silent 时 say 为空;react 是一句很短的话;say
|
|
23
|
+
输出 JSON:{"understanding":{"kind":"question|proposal|feeling|sharing|correction|greeting|closing|other","messageIds":[本轮回应的序号],"point":"对方此刻的具体意思","tone":"serious|joking|teasing|ironic|venting","stance":"fact|guess|opinion|joke|hearsay"},"contribution":{"kind":"answer|reaction|question|idea|correction|none","point":"自己这次实际说出的意思,不是收住/接住之类动作"},"choice":"speak|react|decline|silent","targetMessageIds":[batchIds中的序号],"topic":"当前话题","say":"实际说的话","appraisal":"这件事对自己的实际影响,没有则空","reason":"为何这样选择,用名字不用QQ号","feelings":[{"feeling":"两三个字","intensity":0到1,"valence":-1到1,"arousal":0到1,"cause":[消息序号]}],"bonds":[{"userId":"speaker","change":"warmer|closer|trust_up|trust_down|friction|repair|distance","why":"具体变化","evidence":[消息序号]}],"crisis":{"clear":false,"messageIds":[]}}。silent 时 say 为空;react 是一句很短的话;say 是你在输入框里打的字,换行就是按了发送、另起一条消息,说多少、发几次由你此刻想说什么决定;若对方已要求同一段发完,则把连贯补充收在一条消息里,列点也可在同一条里换行。真实自伤危机认真回应眼前安全,crisis.clear=true。understanding 里的 tone 和 stance 可选:所回应的话是认真、玩笑、调侃、反话还是发泄,说的是事实、猜测、观点、玩笑还是转述;拿不准就不写这两项。working.reading 只是字面提示,以前后文为准。choice、targetMessageIds 等其余字段仍在最外层。`;
|
|
24
24
|
|
|
25
25
|
export const DIALOGUE_REWRITE = `重新核对 exchange.current 和引用,修改 draft 中被指出的具体问题。decision.understanding 可能也有误,以原话为准。保留成立的内容,直接说这轮真正想说的话;不要把修改写成新的认错套话或内部状态报告。self.livingFor 只作背景,不必提起。输出 {"bubbles":["实际内容"],"reason":"为何这样说"}。`;
|
|
26
26
|
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
recentOwnMessages,
|
|
15
15
|
routineSocialReply,
|
|
16
16
|
} from "./conversation-grounding.js";
|
|
17
|
-
import { joinBubbleParts } from "./turn.js";
|
|
17
|
+
import { joinBubbleParts, typedSends } from "./turn.js";
|
|
18
18
|
const norm = (s) => s.toLowerCase().replace(/[\s\p{P}\p{S}]/gu, "");
|
|
19
19
|
export function wrongIdentityClaim(text, snapshot) {
|
|
20
20
|
const name = snapshot.persona?.name;
|
|
@@ -198,7 +198,7 @@ export function normalizeResponse(result, decision, fallback = "嗯") {
|
|
|
198
198
|
}
|
|
199
199
|
bubbles = bubbles
|
|
200
200
|
.filter((x) => typeof x === "string")
|
|
201
|
-
.
|
|
201
|
+
.flatMap((x) => typedSends(x.trim()))
|
|
202
202
|
.filter(Boolean);
|
|
203
203
|
if (bubbles.length > maxBubbles)
|
|
204
204
|
bubbles =
|
|
@@ -601,3 +601,19 @@ export function deniesPrivateKnowledge(bubbles, snapshot) {
|
|
|
601
601
|
/不知道|没听说|没听过|(没|不曾)(跟|和|对)我(说|提|讲)|没提过/.test(line),
|
|
602
602
|
);
|
|
603
603
|
}
|
|
604
|
+
|
|
605
|
+
// One message is typed as one line. A line break inside a message is only
|
|
606
|
+
// kept for a list (every line a "- " or "1." item); otherwise the lines are
|
|
607
|
+
// one thought she typed, joined the way she would type them. A sentence per
|
|
608
|
+
// line inside one chat bubble reads as a machine laying out text.
|
|
609
|
+
export function oneTypedMessage(text) {
|
|
610
|
+
const lines = String(text || "")
|
|
611
|
+
.split(/\r?\n/)
|
|
612
|
+
.map((line) => line.trim())
|
|
613
|
+
.filter(Boolean);
|
|
614
|
+
if (lines.length < 2) return lines[0] || "";
|
|
615
|
+
const item = /^(?:[-*•·]|\d+[.)、.]|[((]?\d+[))])\s*/u;
|
|
616
|
+
if (lines.filter((line) => item.test(line)).length >= 2)
|
|
617
|
+
return lines.join("\n");
|
|
618
|
+
return joinBubbleParts(lines);
|
|
619
|
+
}
|
package/server/core/turn.js
CHANGED
|
@@ -600,7 +600,7 @@ function turnGuides(snapshot) {
|
|
|
600
600
|
export function saidMessages(text) {
|
|
601
601
|
return String(text)
|
|
602
602
|
.split(/\n[ \t\u3000]*\n+/)
|
|
603
|
-
.
|
|
603
|
+
.flatMap((part) => typedSends(part.trim()))
|
|
604
604
|
.filter(Boolean);
|
|
605
605
|
}
|
|
606
606
|
|
|
@@ -610,3 +610,32 @@ export function withSaidMessages(raw) {
|
|
|
610
610
|
const { say, ...rest } = raw;
|
|
611
611
|
return { ...rest, bubbles: saidMessages(say) };
|
|
612
612
|
}
|
|
613
|
+
|
|
614
|
+
// In a chat box, Enter sends. A line break she typed is a message she sent;
|
|
615
|
+
// only a list stays together in one message.
|
|
616
|
+
export function typedSends(text) {
|
|
617
|
+
const lines = String(text || "")
|
|
618
|
+
.split(/\r?\n/)
|
|
619
|
+
.map((line) => line.trim())
|
|
620
|
+
.filter(Boolean);
|
|
621
|
+
if (lines.length < 2) return lines;
|
|
622
|
+
const item = /^(?:[-*•·]|\d+[.)、.]|[((]?\d+[))])\s*/u;
|
|
623
|
+
if (lines.filter((line) => item.test(line)).length >= 2)
|
|
624
|
+
return [lines.join("\n")];
|
|
625
|
+
// A line left mid-sentence, or one that only continues the last, was not
|
|
626
|
+
// a separate send: the thought goes out as one message.
|
|
627
|
+
const sends = [];
|
|
628
|
+
for (const line of lines) {
|
|
629
|
+
const last = sends.at(-1);
|
|
630
|
+
if (
|
|
631
|
+
last &&
|
|
632
|
+
(/[,、::,]$/u.test(last) ||
|
|
633
|
+
/^(?:[,、;;]|但|但是|不过|而且|所以|因为|然后|就是|或者|要是|如果|反正)/u.test(
|
|
634
|
+
line,
|
|
635
|
+
))
|
|
636
|
+
)
|
|
637
|
+
sends[sends.length - 1] = joinBubbleParts([last, line]);
|
|
638
|
+
else sends.push(line);
|
|
639
|
+
}
|
|
640
|
+
return sends;
|
|
641
|
+
}
|
package/server/i18n/errors.js
CHANGED
|
@@ -34,6 +34,42 @@ export const ERRORS = {
|
|
|
34
34
|
"The update did not complete. The original program and data were retained. Check the runtime log before trying again.",
|
|
35
35
|
"自动恢复未完成,更新前的校验备份已保留,请查看运行日志并手动恢复。":
|
|
36
36
|
"Automatic recovery did not complete. The verified pre-update backup was retained. Check the runtime log and recover manually.",
|
|
37
|
+
"Node.js 版本不满足新版本要求,请先升级 Node.js 后重试。":
|
|
38
|
+
"This Node.js version does not meet the new release requirements. Upgrade Node.js and try again.",
|
|
39
|
+
"当前服务找不到 npm,请安装 npm 并检查服务的 PATH 配置。":
|
|
40
|
+
"The service cannot find npm. Install npm and check the service PATH.",
|
|
41
|
+
"npm 暂时无法下载该版本或其运行依赖,请稍后重新检查更新。":
|
|
42
|
+
"npm cannot download this release or its runtime dependencies yet. Check for updates again later.",
|
|
43
|
+
"连接 npm 失败,请检查服务器的网络、DNS 和代理配置。":
|
|
44
|
+
"Cannot reach npm. Check the server network, DNS and proxy settings.",
|
|
45
|
+
"连接 npm 时证书校验失败,请检查服务器的证书和代理配置。":
|
|
46
|
+
"Certificate verification failed when connecting to npm. Check the server certificates and proxy settings.",
|
|
47
|
+
"npm 拒绝了下载请求,请检查服务器的网络或代理访问限制。":
|
|
48
|
+
"npm rejected the download request. Check network or proxy access restrictions.",
|
|
49
|
+
"磁盘空间不足,请为新程序、数据库快照和迁移副本留出空间。":
|
|
50
|
+
"There is not enough disk space for the new program, database snapshot and migration copy.",
|
|
51
|
+
"服务账号没有所需的读写权限,请检查实例和更新目录的权限。":
|
|
52
|
+
"The service account lacks read or write access. Check the instance and update directory permissions.",
|
|
53
|
+
"当前更新步骤超时,请检查运行日志后重试。":
|
|
54
|
+
"The update step timed out. Check the runtime log and try again.",
|
|
55
|
+
"新版本安装失败,旧实例仍在运行,请查看运行日志中的 npm 报错。":
|
|
56
|
+
"Installation failed while the previous instance remains running. Check the npm errors in the runtime log.",
|
|
57
|
+
"新程序或本机运行依赖校验失败,请查看运行日志。":
|
|
58
|
+
"The new program or its local runtime dependencies failed validation. Check the runtime log.",
|
|
59
|
+
"旧实例未能安全停止,请检查进程管理和实例端口。":
|
|
60
|
+
"The previous instance could not be stopped safely. Check process management and the instance port.",
|
|
61
|
+
"更新前的数据库快照未能完成或通过校验,请查看运行日志。":
|
|
62
|
+
"The pre-update database snapshot could not be completed or verified. Check the runtime log.",
|
|
63
|
+
"记忆与数据迁移未通过校验,请查看运行日志。":
|
|
64
|
+
"Memory and data migration failed validation. Check the runtime log.",
|
|
65
|
+
"新实例未能正常启动,请检查运行日志和端口占用。":
|
|
66
|
+
"The new instance could not start correctly. Check the runtime log and port usage.",
|
|
67
|
+
"更新进程意外退出,请检查服务管理器、进程退出记录和运行日志。":
|
|
68
|
+
"The update process exited unexpectedly. Check the service manager, process exit records and runtime log.",
|
|
69
|
+
"旧版本或数据库恢复未完成,请使用更新前的校验备份恢复。":
|
|
70
|
+
"The previous program or database could not be restored. Recover from the verified pre-update backup.",
|
|
71
|
+
"当前更新步骤未完成,请查看运行日志。":
|
|
72
|
+
"The update step did not complete. Check the runtime log.",
|
|
37
73
|
"缺少候选版本的数据迁移器,已停止更新":
|
|
38
74
|
"The candidate release has no data migrator. The update was stopped.",
|
|
39
75
|
迁移目标不是现有的绝对路径数据库文件:
|
package/server/mind/affect.js
CHANGED
|
@@ -196,7 +196,7 @@ export class Affect {
|
|
|
196
196
|
if (batchAt !== null && row.created !== batchAt) apply(batchAt);
|
|
197
197
|
batchAt = row.created;
|
|
198
198
|
batchV += row.valence * row.intensity;
|
|
199
|
-
batchA += (row.arousal - BASELINE.arousal) * row.intensity
|
|
199
|
+
batchA += (row.arousal - BASELINE.arousal) * row.intensity;
|
|
200
200
|
last = row;
|
|
201
201
|
}
|
|
202
202
|
apply(batchAt);
|
|
@@ -213,7 +213,9 @@ export class Affect {
|
|
|
213
213
|
.get(now - 2 * HOUR, now).n;
|
|
214
214
|
const energy = clamp(
|
|
215
215
|
phase.energy +
|
|
216
|
-
|
|
216
|
+
// How worn or wound up she feels is part of her energy: a night up
|
|
217
|
+
// must not read as "有精神" (round-19 probe).
|
|
218
|
+
(arousal - BASELINE.arousal) * 0.6 -
|
|
217
219
|
Math.min(0.2, talked * 0.006) -
|
|
218
220
|
Math.min(
|
|
219
221
|
0.12,
|
package/server/mind/view.js
CHANGED
|
@@ -405,7 +405,10 @@ export function ownTyping(mind, session, now) {
|
|
|
405
405
|
`SELECT text FROM messages WHERE role='assistant' AND is_demo=0 AND time<=? AND time>? ${where} ORDER BY id DESC LIMIT 40`,
|
|
406
406
|
)
|
|
407
407
|
.all(now + 60000, now - 14 * 86400000, ...args)
|
|
408
|
-
|
|
408
|
+
// An older reply laid out a sentence per line was several sends in
|
|
409
|
+
// one message; each line is counted as the message it would be now.
|
|
410
|
+
.flatMap((row) => String(row.text || "").split(/\n+/))
|
|
411
|
+
.map((t) => t.trim())
|
|
409
412
|
.filter((t) => t && !/^\[/.test(t));
|
|
410
413
|
let lines = pick("AND session_id=?", session);
|
|
411
414
|
if (lines.length < 4) lines = pick("");
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Only these descriptions reach the management UI. Subprocess output can
|
|
2
|
+
// contain local paths or credentials and belongs in the local log only.
|
|
3
|
+
export const UPDATE_FAILURES = Object.freeze({
|
|
4
|
+
NODE_UNSUPPORTED: "Node.js 版本不满足新版本要求,请先升级 Node.js 后重试。",
|
|
5
|
+
NPM_NOT_FOUND: "当前服务找不到 npm,请安装 npm 并检查服务的 PATH 配置。",
|
|
6
|
+
VERSION_UNAVAILABLE:
|
|
7
|
+
"npm 暂时无法下载该版本或其运行依赖,请稍后重新检查更新。",
|
|
8
|
+
NETWORK_ERROR: "连接 npm 失败,请检查服务器的网络、DNS 和代理配置。",
|
|
9
|
+
TLS_ERROR: "连接 npm 时证书校验失败,请检查服务器的证书和代理配置。",
|
|
10
|
+
REGISTRY_ACCESS: "npm 拒绝了下载请求,请检查服务器的网络或代理访问限制。",
|
|
11
|
+
DISK_FULL: "磁盘空间不足,请为新程序、数据库快照和迁移副本留出空间。",
|
|
12
|
+
PERMISSION_DENIED: "服务账号没有所需的读写权限,请检查实例和更新目录的权限。",
|
|
13
|
+
STEP_TIMEOUT: "当前更新步骤超时,请检查运行日志后重试。",
|
|
14
|
+
INSTALL_FAILED:
|
|
15
|
+
"新版本安装失败,旧实例仍在运行,请查看运行日志中的 npm 报错。",
|
|
16
|
+
PACKAGE_CHECK_FAILED: "新程序或本机运行依赖校验失败,请查看运行日志。",
|
|
17
|
+
STOP_FAILED: "旧实例未能安全停止,请检查进程管理和实例端口。",
|
|
18
|
+
BACKUP_FAILED: "更新前的数据库快照未能完成或通过校验,请查看运行日志。",
|
|
19
|
+
MIGRATION_FAILED: "记忆与数据迁移未通过校验,请查看运行日志。",
|
|
20
|
+
START_FAILED: "新实例未能正常启动,请检查运行日志和端口占用。",
|
|
21
|
+
UPDATE_INTERRUPTED:
|
|
22
|
+
"更新进程意外退出,请检查服务管理器、进程退出记录和运行日志。",
|
|
23
|
+
RECOVERY_FAILED: "旧版本或数据库恢复未完成,请使用更新前的校验备份恢复。",
|
|
24
|
+
STEP_FAILED: "当前更新步骤未完成,请查看运行日志。",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export function updateError(code) {
|
|
28
|
+
const known = Object.hasOwn(UPDATE_FAILURES, code) ? code : "STEP_FAILED";
|
|
29
|
+
return Object.assign(Error(UPDATE_FAILURES[known]), { updateCode: known });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function npmFailureCode(output) {
|
|
33
|
+
for (const [pattern, code] of [
|
|
34
|
+
[/\bEBADENGINE\b/, "NODE_UNSUPPORTED"],
|
|
35
|
+
[/\bENOSPC\b/, "DISK_FULL"],
|
|
36
|
+
[/\b(?:EACCES|EPERM)\b/, "PERMISSION_DENIED"],
|
|
37
|
+
[/\b(?:E404|ETARGET)\b/, "VERSION_UNAVAILABLE"],
|
|
38
|
+
[/\b(?:E401|E403)\b/, "REGISTRY_ACCESS"],
|
|
39
|
+
[
|
|
40
|
+
/\b(?:CERT_[A-Z_]+|[A-Z_]*CERTIFICATE[A-Z_]*|SELF_SIGNED_CERT_IN_CHAIN|UNABLE_TO_VERIFY_LEAF_SIGNATURE)\b/,
|
|
41
|
+
"TLS_ERROR",
|
|
42
|
+
],
|
|
43
|
+
[
|
|
44
|
+
/\b(?:ENOTFOUND|EAI_AGAIN|ECONNRESET|ECONNREFUSED|ETIMEDOUT|ESOCKETTIMEDOUT|ENETUNREACH|EHOSTUNREACH)\b/,
|
|
45
|
+
"NETWORK_ERROR",
|
|
46
|
+
],
|
|
47
|
+
])
|
|
48
|
+
if (pattern.test(output)) return code;
|
|
49
|
+
return "INSTALL_FAILED";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function updateFailure(error, stage) {
|
|
53
|
+
const osCode = {
|
|
54
|
+
ENOSPC: "DISK_FULL",
|
|
55
|
+
EACCES: "PERMISSION_DENIED",
|
|
56
|
+
EPERM: "PERMISSION_DENIED",
|
|
57
|
+
}[error?.code];
|
|
58
|
+
const fallback =
|
|
59
|
+
{
|
|
60
|
+
downloading: "INSTALL_FAILED",
|
|
61
|
+
checking: "PACKAGE_CHECK_FAILED",
|
|
62
|
+
stopping: "STOP_FAILED",
|
|
63
|
+
backup: "BACKUP_FAILED",
|
|
64
|
+
migrating: "MIGRATION_FAILED",
|
|
65
|
+
restarting: "START_FAILED",
|
|
66
|
+
verifying: "START_FAILED",
|
|
67
|
+
restoring: "RECOVERY_FAILED",
|
|
68
|
+
}[stage] || "STEP_FAILED";
|
|
69
|
+
const failureCode = Object.hasOwn(UPDATE_FAILURES, error?.updateCode)
|
|
70
|
+
? error.updateCode
|
|
71
|
+
: osCode || fallback;
|
|
72
|
+
return { failureCode, detail: UPDATE_FAILURES[failureCode] };
|
|
73
|
+
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "./update-manager.js";
|
|
16
16
|
import { backupDatabase } from "../../scripts/backup.js";
|
|
17
17
|
import { restoreToNewFile, verifyArchive } from "../storage/archive.js";
|
|
18
|
+
import { updateFailure } from "./update-errors.js";
|
|
18
19
|
|
|
19
20
|
// Executed outside the server so a large verified snapshot does not stall the
|
|
20
21
|
// WebUI. Dependencies are explicit for failure, restart and rollback tests.
|
|
@@ -105,12 +106,9 @@ export async function performUpdate(
|
|
|
105
106
|
return job;
|
|
106
107
|
} catch (error) {
|
|
107
108
|
const failedStage = job.status;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
failedStage,
|
|
112
|
-
error.code || error.cause?.code || "STEP_FAILED",
|
|
113
|
-
);
|
|
109
|
+
const failure = updateFailure(error, failedStage);
|
|
110
|
+
job = { ...job, failedStage, ...failure };
|
|
111
|
+
console.error("更新步骤未完成:", failedStage, failure.failureCode, error);
|
|
114
112
|
if (!stopped) {
|
|
115
113
|
save("error", { error: UPDATE_FAILED, finished: Date.now() });
|
|
116
114
|
return job;
|
|
@@ -150,6 +148,7 @@ export async function performUpdate(
|
|
|
150
148
|
`${paths.database}.update-failed-${id}.migration${suffix}`,
|
|
151
149
|
);
|
|
152
150
|
const restored = await start(paths.package);
|
|
151
|
+
save("restoring", { childPid: restored });
|
|
153
152
|
await health(restored, job.previous);
|
|
154
153
|
save("error", {
|
|
155
154
|
error: UPDATE_FAILED,
|
|
@@ -157,8 +156,14 @@ export async function performUpdate(
|
|
|
157
156
|
childPid: restored,
|
|
158
157
|
finished: Date.now(),
|
|
159
158
|
});
|
|
160
|
-
} catch {
|
|
161
|
-
|
|
159
|
+
} catch (recoveryError) {
|
|
160
|
+
const recovery = updateFailure(recoveryError, "restoring");
|
|
161
|
+
console.error("更新恢复未完成:", recovery.failureCode, recoveryError);
|
|
162
|
+
save("error", {
|
|
163
|
+
error: UPDATE_RECOVERY_FAILED,
|
|
164
|
+
recoveryCode: recovery.failureCode,
|
|
165
|
+
finished: Date.now(),
|
|
166
|
+
});
|
|
162
167
|
}
|
|
163
168
|
return job;
|
|
164
169
|
} finally {
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { mkdirSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
updateError,
|
|
6
|
+
updateFailure,
|
|
7
|
+
UPDATE_FAILURES,
|
|
8
|
+
} from "./update-errors.js";
|
|
4
9
|
import {
|
|
5
10
|
atomicJson,
|
|
6
11
|
UPDATE_ID,
|
|
@@ -11,7 +16,7 @@ export const UPDATE_FAILED =
|
|
|
11
16
|
"更新未完成,原程序和数据已保留,请查看运行日志后重试。";
|
|
12
17
|
export const UPDATE_RECOVERY_FAILED =
|
|
13
18
|
"自动恢复未完成,更新前的校验备份已保留,请查看运行日志并手动恢复。";
|
|
14
|
-
const
|
|
19
|
+
export const UPDATE_ACTIVE_STAGES = Object.freeze([
|
|
15
20
|
"queued",
|
|
16
21
|
"downloading",
|
|
17
22
|
"checking",
|
|
@@ -22,6 +27,12 @@ const ACTIVE = new Set([
|
|
|
22
27
|
"verifying",
|
|
23
28
|
"restoring",
|
|
24
29
|
]);
|
|
30
|
+
const ACTIVE = new Set(UPDATE_ACTIVE_STAGES);
|
|
31
|
+
export const isUpdateActive = (stage) => ACTIVE.has(stage);
|
|
32
|
+
export const isUpdateFinished = (stage) =>
|
|
33
|
+
stage === "done" || stage === "error";
|
|
34
|
+
const beforeStop = (stage) =>
|
|
35
|
+
["queued", "downloading", "checking"].includes(stage);
|
|
25
36
|
export const processAlive = (pid) => {
|
|
26
37
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
27
38
|
try {
|
|
@@ -70,18 +81,17 @@ export class InstanceUpdater {
|
|
|
70
81
|
return null;
|
|
71
82
|
}
|
|
72
83
|
if (
|
|
73
|
-
|
|
84
|
+
isUpdateActive(job.status) &&
|
|
74
85
|
job.workerPid &&
|
|
75
86
|
!processAlive(job.workerPid)
|
|
76
87
|
) {
|
|
88
|
+
const failedStage = job.status;
|
|
77
89
|
job = writeUpdateJob(this.paths.home, {
|
|
78
90
|
...job,
|
|
79
91
|
status: "error",
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
)
|
|
83
|
-
? UPDATE_FAILED
|
|
84
|
-
: UPDATE_RECOVERY_FAILED,
|
|
92
|
+
failedStage,
|
|
93
|
+
...updateFailure(updateError("UPDATE_INTERRUPTED"), failedStage),
|
|
94
|
+
error: beforeStop(failedStage) ? UPDATE_FAILED : UPDATE_RECOVERY_FAILED,
|
|
85
95
|
finished: this.now(),
|
|
86
96
|
});
|
|
87
97
|
}
|
|
@@ -95,10 +105,22 @@ export class InstanceUpdater {
|
|
|
95
105
|
finished: job.finished,
|
|
96
106
|
error: job.error || "",
|
|
97
107
|
restored: !!job.restored,
|
|
108
|
+
...(isUpdateActive(job.failedStage)
|
|
109
|
+
? { failedStage: job.failedStage }
|
|
110
|
+
: {}),
|
|
111
|
+
...(Object.hasOwn(UPDATE_FAILURES, job.failureCode)
|
|
112
|
+
? {
|
|
113
|
+
failureCode: job.failureCode,
|
|
114
|
+
detail: UPDATE_FAILURES[job.failureCode],
|
|
115
|
+
}
|
|
116
|
+
: {}),
|
|
117
|
+
...(Object.hasOwn(UPDATE_FAILURES, job.recoveryCode)
|
|
118
|
+
? { recoveryDetail: UPDATE_FAILURES[job.recoveryCode] }
|
|
119
|
+
: {}),
|
|
98
120
|
};
|
|
99
121
|
}
|
|
100
122
|
get busy() {
|
|
101
|
-
return this.starting ||
|
|
123
|
+
return this.starting || isUpdateActive(this.status()?.status);
|
|
102
124
|
}
|
|
103
125
|
async install(version) {
|
|
104
126
|
if (this.busy) return this.status();
|