luckytri 1.0.3 → 1.0.5

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.
Files changed (71) hide show
  1. package/bin/luckytri.js +5 -1
  2. package/docs/en/CHANGELOG.md +14 -0
  3. package/docs/en/guide.md +5 -1
  4. package/docs/en/webui.md +3 -1
  5. package/docs/zh/CHANGELOG.md +14 -0
  6. package/docs/zh/guide.md +5 -1
  7. package/docs/zh/webui.md +3 -1
  8. package/package.json +4 -3
  9. package/public/app/assets/{ChatsPage-B83Evc79.js → ChatsPage-CshdtTzV.js} +1 -1
  10. package/public/app/assets/{Empty-ddEfu0w8.js → Empty-C7yzOBzy.js} +1 -1
  11. package/public/app/assets/{Field-mIhV5YBA.js → Field-BjFv4Rfn.js} +1 -1
  12. package/public/app/assets/{HeartPage-BGEn24rc.js → HeartPage-eqo6ru16.js} +1 -1
  13. package/public/app/assets/{LifePage-BleqPj4O.js → LifePage-CN8GE5NK.js} +1 -1
  14. package/public/app/assets/{MemoryPage-BM3-0_lF.js → MemoryPage-CCpMN4Wg.js} +1 -1
  15. package/public/app/assets/{NaturePage-CxxMnQIc.js → NaturePage-C0JqdBTR.js} +2 -2
  16. package/public/app/assets/{PeoplePage-DDDHESQp.css → PeoplePage-BTU0H1pM.css} +1 -1
  17. package/public/app/assets/PeoplePage-BXGBbO4u.js +1 -0
  18. package/public/app/assets/{PluginsPage-DE_-Q3ra.js → PluginsPage-Dksp5AV5.js} +1 -1
  19. package/public/app/assets/{SystemPage-Be1a6UDP.css → SystemPage-BenIyTvk.css} +1 -1
  20. package/public/app/assets/{SystemPage-BWQKEQRU.js → SystemPage-Cd7aa8eE.js} +1 -1
  21. package/public/app/assets/{TimePage-Dzt8t8kL.js → TimePage-Bs_zImb4.js} +1 -1
  22. package/public/app/assets/{index-DY4e15Kh.css → index-BHZcHUMh.css} +1 -1
  23. package/public/app/assets/{index-oLIXWxsn.js → index-CyRtYxP3.js} +3 -3
  24. package/public/app/assets/{knowledge-Dvtmpd45.js → knowledge-BJOIyuJo.js} +1 -1
  25. package/public/app/assets/{media-keE52cIX.js → media-Bwtx8blz.js} +1 -1
  26. package/public/app/assets/mind-DtTVrW6Z.js +1 -0
  27. package/public/app/index.html +4 -4
  28. package/public/guide.en.html +31 -8
  29. package/public/guide.html +14 -5
  30. package/public/tutorial.en.md +5 -1
  31. package/public/tutorial.md +5 -1
  32. package/scripts/launch.js +11 -4
  33. package/scripts/service.js +57 -7
  34. package/scripts/start-server.js +4 -1
  35. package/scripts/update-instance.js +201 -0
  36. package/server/app.js +1 -1
  37. package/server/core/context-builder.js +2 -2
  38. package/server/core/context-compactor.js +2 -2
  39. package/server/core/orchestrator.js +1 -0
  40. package/server/core/persona-manager.js +11 -0
  41. package/server/core/reply-delivery.js +11 -0
  42. package/server/core/response-validator.js +33 -1
  43. package/server/i18n/decisions.js +4 -1
  44. package/server/i18n/errors.js +29 -0
  45. package/server/index.js +45 -2
  46. package/server/mind/api.js +25 -0
  47. package/server/mind/bonds.js +9 -2
  48. package/server/mind/index.js +2 -0
  49. package/server/mind/initiative.js +6 -1
  50. package/server/mind/life/context.js +22 -4
  51. package/server/mind/life/diary.js +20 -0
  52. package/server/mind/life/growth.js +13 -5
  53. package/server/mind/life/index.js +10 -2
  54. package/server/mind/nature.js +23 -2
  55. package/server/mind/relationship-context.js +71 -0
  56. package/server/mind/relationships.js +390 -0
  57. package/server/mind/schema.js +2 -0
  58. package/server/mind/time/tasks.js +2 -1
  59. package/server/mind/view.js +11 -1
  60. package/server/runtime/installation.js +116 -0
  61. package/server/runtime/update-execution.js +127 -0
  62. package/server/runtime/update-manager.js +140 -0
  63. package/server/startup-lock.js +12 -12
  64. package/server/storage/archive.js +10 -2
  65. package/server/storage/backup-lock.js +23 -5
  66. package/server/storage/data-transfer.js +2 -1
  67. package/server/studio/display-names.js +1 -0
  68. package/server/studio/settings.js +1 -0
  69. package/server/studio/updates.js +14 -3
  70. package/public/app/assets/PeoplePage-D12cvuy7.js +0 -1
  71. package/public/app/assets/mind-DYRfFMwh.js +0 -1
@@ -203,4 +203,8 @@ TA 的一切都存在这一个数据库里,所以服务运行时会自动备
203
203
 
204
204
  「人际 → 在这里的想法」保存她在各处自己想留下的印象、愿望或疑问。由她根据实际相处选择更新,详情显示理由、来源和历史;旧群人格只留在历史里。
205
205
 
206
- 「系统 → 运行开关 → 版本与更新」可手动检查 npm 最新版本并查看发布说明,不自动安装或重启。天性页的剩余修改次数以服务端额度为准;本地维护额外授予的机会有独立记录,不清空原版本。
206
+ 「人际 → 彼此的关系」可按账号绑定人或另一位机器人,设置妹妹、姐姐、朋友等双向称呼、名字、缘由和公开范围。她在交流、独处、日记与行动选择中知道这份关系;信任和亲近仍依据实际相处,绑定不会制造共同经历。修改和解除保留历史及原有记忆。私下关系只在与对方的私聊中使用,不按同名合并账号。
207
+
208
+ 对方不在当前会话,也可以因名字或关系称呼被想起。当前关系、实际相处与自己的感受共同参与理解;关系更新前的看法作为历史保留,不再直接代表当前事实。相关回复发送前会再次核对。她仍选择如何相处和称呼,不机械播报关系字段。
209
+
210
+ 「系统 → 运行开关 → 版本与更新」可检查 npm 最新版本、查看发布说明,随后点击「更新并重启」。下载检查期间继续运行,切换前保留校验备份,启动失败时尝试恢复原版本和数据。新程序装在实例的 `runtime/releases/`,源码和全局 npm 安装保持原样;启动命令会选择已更新的程序。安装了同版本或更新版本的全局包或源码后,以该安装为准。天性页的剩余修改次数以服务端额度为准;本地维护额外授予的机会有独立记录,不清空原版本。
package/scripts/launch.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { openSync, closeSync, mkdirSync } from "node:fs";
2
2
  import { spawn } from "node:child_process";
3
- import { join } from "node:path";
3
+ import { join, resolve } from "node:path";
4
4
  import { prepareRuntime } from "./runtime.js";
5
5
  import { running, serviceUrl } from "./service.js";
6
6
  import { openBrowser } from "./browser.js";
7
7
  import { VERSION } from "../server/version.js";
8
+ import { resolveInstallation } from "../server/runtime/installation.js";
8
9
  import {
9
10
  claimLaunch,
10
11
  clearStarting,
@@ -17,11 +18,17 @@ import {
17
18
 
18
19
  try {
19
20
  const paths = prepareRuntime();
21
+ const selected = resolveInstallation(paths.home, paths.package, VERSION);
20
22
  const proxy = await resolveProxyEnvironment(process.env);
21
23
  const base = serviceUrl();
22
24
  const existing = await running({ base });
23
- if (existing && existing.version && existing.version !== VERSION) {
24
- console.log(`正在将运行中的 LuckyTri 更新到 ${VERSION}…`);
25
+ if (
26
+ existing &&
27
+ existing.version &&
28
+ (existing.version !== selected.version ||
29
+ resolve(existing.workspace || "") !== resolve(selected.package))
30
+ ) {
31
+ console.log(`正在将运行中的 LuckyTri 更新到 ${selected.version}…`);
25
32
  await import("./stop.js");
26
33
  if (process.exitCode) throw Error("旧服务未停止,请稍后重试");
27
34
  }
@@ -44,7 +51,7 @@ try {
44
51
  const log = openSync(paths.log, "a", 0o600);
45
52
  const args = [
46
53
  ...(proxy.enabled ? ["--use-env-proxy"] : []),
47
- join(paths.package, "server", "index.js"),
54
+ join(selected.package, "server", "index.js"),
48
55
  ];
49
56
  const child = spawn(process.execPath, args, {
50
57
  cwd: paths.home,
@@ -1,5 +1,6 @@
1
1
  import { runtimePaths } from "../server/paths.js";
2
2
  import { localServiceKey } from "../server/local-access.js";
3
+ import http from "node:http";
3
4
  let legacy = false;
4
5
 
5
6
  export function serviceUrl(env = process.env) {
@@ -17,15 +18,64 @@ export const serviceHeaders = () => ({
17
18
  Authorization: `Bearer ${legacy ? process.env.ADMIN_TOKEN : localServiceKey()}`,
18
19
  });
19
20
 
20
- export async function running({ base = serviceUrl(), lenient = false } = {}) {
21
+ export function localServiceRequest(
22
+ url,
23
+ { headers, method = "GET", body, timeout = 5000 } = {},
24
+ ) {
25
+ return new Promise((resolve, reject) => {
26
+ const request = http.request(url, { method, headers }, (response) => {
27
+ const chunks = [];
28
+ let bytes = 0;
29
+ response.on("data", (chunk) => {
30
+ bytes += chunk.length;
31
+ if (bytes > 65536) request.destroy(Error("服务状态响应过大"));
32
+ else chunks.push(chunk);
33
+ });
34
+ response.on("end", () => {
35
+ clearTimeout(timer);
36
+ try {
37
+ resolve(
38
+ new Response(Buffer.concat(chunks), {
39
+ status: response.statusCode,
40
+ }),
41
+ );
42
+ } catch (error) {
43
+ reject(error);
44
+ }
45
+ });
46
+ response.on("error", (error) => {
47
+ clearTimeout(timer);
48
+ reject(error);
49
+ });
50
+ });
51
+ const timer = setTimeout(
52
+ () => request.destroy(Error("服务请求超时")),
53
+ timeout,
54
+ );
55
+ request.once("error", (error) => {
56
+ clearTimeout(timer);
57
+ reject(error);
58
+ });
59
+ request.end(body);
60
+ });
61
+ }
62
+ export async function running({
63
+ base = serviceUrl(),
64
+ lenient = false,
65
+ local = false,
66
+ } = {}) {
21
67
  let response;
22
68
  try {
23
- response = await fetch(base + "/api/service/status", {
24
- headers: serviceHeaders(),
25
- signal: AbortSignal.timeout(1500),
26
- });
69
+ response = await (local ? localServiceRequest : fetch)(
70
+ base + "/api/service/status",
71
+ {
72
+ headers: serviceHeaders(),
73
+ signal: AbortSignal.timeout(1500),
74
+ ...(local ? { timeout: 1500 } : {}),
75
+ },
76
+ );
27
77
  } catch (error) {
28
- if (lenient || error.cause?.code === "ECONNREFUSED") {
78
+ if (lenient || (error.cause?.code || error.code) === "ECONNREFUSED") {
29
79
  legacy = false;
30
80
  return false;
31
81
  }
@@ -34,7 +84,7 @@ export async function running({ base = serviceUrl(), lenient = false } = {}) {
34
84
  if (response.status === 401 && !legacy && process.env.ADMIN_TOKEN) {
35
85
  // Allow the new CLI to gracefully stop a still-running 1.0.0 instance.
36
86
  legacy = true;
37
- return running({ base, lenient });
87
+ return running({ base, lenient, local });
38
88
  }
39
89
  if (!response.ok) throw Error("端口已被占用或不是当前实例,请检查本机配置。");
40
90
  let info;
@@ -1,12 +1,15 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { join } from "node:path";
3
3
  import { prepareRuntime } from "./runtime.js";
4
+ import { VERSION } from "../server/version.js";
5
+ import { resolveInstallation } from "../server/runtime/installation.js";
4
6
  import {
5
7
  resolveProxyEnvironment,
6
8
  supportsNodeEnvironmentProxy,
7
9
  } from "./proxy-config.js";
8
10
 
9
11
  const paths = prepareRuntime();
12
+ const selected = resolveInstallation(paths.home, paths.package, VERSION);
10
13
  const proxy = await resolveProxyEnvironment(process.env);
11
14
  if (proxy.enabled && !supportsNodeEnvironmentProxy())
12
15
  throw new Error(
@@ -15,7 +18,7 @@ if (proxy.enabled && !supportsNodeEnvironmentProxy())
15
18
 
16
19
  const args = [
17
20
  ...(proxy.enabled ? ["--use-env-proxy"] : []),
18
- join(paths.package, "server", "index.js"),
21
+ join(selected.package, "server", "index.js"),
19
22
  ];
20
23
  const env = { ...process.env, ...proxy.env };
21
24
  if (proxy.enabled) console.log("已启用系统代理,外部 API 请求将经代理转发。");
@@ -0,0 +1,201 @@
1
+ import { spawn } from "node:child_process";
2
+ import {
3
+ openSync,
4
+ closeSync,
5
+ writeFileSync,
6
+ readFileSync,
7
+ realpathSync,
8
+ } from "node:fs";
9
+ import { dirname, join, delimiter } from "node:path";
10
+ import { pathToFileURL } from "node:url";
11
+ import { prepareRuntime } from "./runtime.js";
12
+ import {
13
+ running,
14
+ serviceUrl,
15
+ serviceHeaders,
16
+ localServiceRequest,
17
+ } from "./service.js";
18
+ import { claimLaunch, clearStarting } from "../server/startup-lock.js";
19
+ import { performUpdate } from "../server/runtime/update-execution.js";
20
+ import {
21
+ processAlive,
22
+ readUpdateJob,
23
+ writeUpdateJob,
24
+ } from "../server/runtime/update-manager.js";
25
+ import { updateRoot } from "../server/runtime/installation.js";
26
+
27
+ export function installEnvironment(env) {
28
+ const kept = {};
29
+ for (const [key, value] of Object.entries(env))
30
+ if (
31
+ /^(?:PATH|PATHEXT|SYSTEMROOT|WINDIR|COMSPEC|USERPROFILE|HOME|APPDATA|LOCALAPPDATA|TEMP|TMP|TMPDIR|HTTP_PROXY|HTTPS_PROXY|ALL_PROXY|NO_PROXY|NODE_USE_ENV_PROXY)$/i.test(
32
+ key,
33
+ )
34
+ )
35
+ kept[key] = value;
36
+ return kept;
37
+ }
38
+ export function findNpmCli(env = process.env) {
39
+ const candidates = [
40
+ env.npm_execpath,
41
+ join(dirname(process.execPath), "node_modules/npm/bin/npm-cli.js"),
42
+ join(dirname(process.execPath), "../lib/node_modules/npm/bin/npm-cli.js"),
43
+ ...String(env.PATH || env.Path || "")
44
+ .split(delimiter)
45
+ .flatMap((dir) => [
46
+ join(dir, "npm"),
47
+ join(dir, "node_modules/npm/bin/npm-cli.js"),
48
+ ]),
49
+ ];
50
+ for (const candidate of candidates.filter(Boolean)) {
51
+ try {
52
+ const cli = realpathSync(candidate);
53
+ if (
54
+ JSON.parse(
55
+ readFileSync(join(dirname(cli), "..", "package.json"), "utf8"),
56
+ ).name === "npm"
57
+ )
58
+ return cli;
59
+ } catch {
60
+ /* Try the next native Node/npm installation location. */
61
+ }
62
+ }
63
+ throw Error("未找到 npm");
64
+ }
65
+ function command(args, { cwd, env = process.env, timeout = 600000, log } = {}) {
66
+ return new Promise((resolve, reject) => {
67
+ const fd = log ? openSync(log, "a", 0o600) : null;
68
+ const child = spawn(process.execPath, args, {
69
+ cwd,
70
+ env,
71
+ windowsHide: true,
72
+ stdio: fd === null ? "ignore" : ["ignore", fd, fd],
73
+ });
74
+ if (fd !== null) closeSync(fd);
75
+ const timer = setTimeout(() => {
76
+ child.kill();
77
+ reject(Error("更新步骤超时"));
78
+ }, timeout);
79
+ child.once("error", (e) => {
80
+ clearTimeout(timer);
81
+ reject(e);
82
+ });
83
+ child.once("exit", (code) => {
84
+ clearTimeout(timer);
85
+ code === 0 ? resolve() : reject(Error("更新程序校验失败"));
86
+ });
87
+ });
88
+ }
89
+ export function updateExecution(paths) {
90
+ const base = serviceUrl(),
91
+ spawned = new Set();
92
+ const pause = () => new Promise((r) => setTimeout(r, 250));
93
+ return {
94
+ async install(slot, version) {
95
+ const cli = findNpmCli();
96
+ const userconfig = join(updateRoot(paths.home), "install.npmrc");
97
+ writeFileSync(userconfig, "", { mode: 0o600 });
98
+ await command(
99
+ [
100
+ cli,
101
+ "install",
102
+ "--prefix",
103
+ slot,
104
+ "--registry=https://registry.npmjs.org/",
105
+ "--userconfig",
106
+ userconfig,
107
+ "--no-audit",
108
+ "--no-fund",
109
+ "--omit=dev",
110
+ "--engine-strict",
111
+ `luckytri@${version}`,
112
+ ],
113
+ { cwd: slot, env: installEnvironment(process.env), log: paths.log },
114
+ );
115
+ },
116
+ async check(root, version) {
117
+ 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();`;
118
+ await command(["--input-type=module", "-e", script], {
119
+ cwd: root,
120
+ env: installEnvironment(process.env),
121
+ timeout: 30000,
122
+ log: paths.log,
123
+ });
124
+ },
125
+ async stop(pid) {
126
+ if (!processAlive(pid)) return;
127
+ const info = await running({ base, lenient: true, local: true });
128
+ if (info?.pid === pid) {
129
+ const response = await localServiceRequest(base + "/api/service/stop", {
130
+ method: "POST",
131
+ headers: serviceHeaders(),
132
+ body: "{}",
133
+ timeout: 5000,
134
+ });
135
+ if (!response.ok) throw Error("服务拒绝停止请求");
136
+ await response.json();
137
+ } else if (spawned.has(pid)) process.kill(pid, "SIGTERM");
138
+ else throw Error("运行实例发生变化");
139
+ for (let n = 0; n < 160 && processAlive(pid); n++) await pause();
140
+ if (processAlive(pid)) throw Error("旧实例未停止");
141
+ },
142
+ claim: claimLaunch,
143
+ release: clearStarting,
144
+ async start(root) {
145
+ const log = openSync(paths.log, "a", 0o600);
146
+ try {
147
+ const child = spawn(
148
+ process.execPath,
149
+ [
150
+ ...(process.execArgv.includes("--use-env-proxy")
151
+ ? ["--use-env-proxy"]
152
+ : []),
153
+ join(root, "server", "index.js"),
154
+ ],
155
+ {
156
+ cwd: paths.home,
157
+ env: { ...process.env, LUCKYTRI_LAUNCH_PID: String(process.pid) },
158
+ detached: true,
159
+ windowsHide: true,
160
+ stdio: ["ignore", log, log],
161
+ },
162
+ );
163
+ await new Promise((resolve, reject) => {
164
+ child.once("spawn", resolve);
165
+ child.once("error", reject);
166
+ });
167
+ child.unref();
168
+ spawned.add(child.pid);
169
+ return child.pid;
170
+ } finally {
171
+ closeSync(log);
172
+ }
173
+ },
174
+ async health(pid, version) {
175
+ for (let n = 0; n < 2400; n++) {
176
+ if (!processAlive(pid)) throw Error("新实例已退出");
177
+ const info = await running({ base, lenient: true, local: true });
178
+ if (info?.pid === pid && info.version === version) return;
179
+ await pause();
180
+ }
181
+ throw Error("新实例启动超时");
182
+ },
183
+ };
184
+ }
185
+ async function main() {
186
+ const paths = prepareRuntime({ initialize: false }),
187
+ id = process.argv[2],
188
+ job = readUpdateJob(paths.home, id);
189
+ if (job.parent !== Number(process.argv[3]) || !processAlive(job.parent))
190
+ throw Error("更新启动信息无效");
191
+ writeUpdateJob(paths.home, { ...job, workerPid: process.pid });
192
+ const result = await performUpdate(paths, id, updateExecution(paths));
193
+ console.log(
194
+ result.status === "done" ? "版本更新与实例重启完成。" : result.error,
195
+ );
196
+ }
197
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
198
+ main().catch((e) => {
199
+ console.error("版本更新未完成:", e.code || "请查看管理台更新状态");
200
+ process.exitCode = 1;
201
+ });
package/server/app.js CHANGED
@@ -59,7 +59,7 @@ export function createApp({
59
59
  res.status(401).json({ error: "请先登录管理台" });
60
60
  });
61
61
  app.use("/api", objectBody);
62
- mountUpdates(app, updates);
62
+ mountUpdates(app, updates, runtime.updater);
63
63
  mountDataTransfer(app, runtime.transfer);
64
64
  if (plugins) {
65
65
  app.post(
@@ -1,4 +1,4 @@
1
- import { effectivePersona } from "./persona-manager.js";
1
+ import { effectivePersona, identityAliases } from "./persona-manager.js";
2
2
  import { localClock, conversationCues } from "./conversation-cues.js";
3
3
  import { estimateTokens } from "./model-manager.js";
4
4
  import { resolveTargets } from "./reply-target-resolver.js";
@@ -31,7 +31,7 @@ export function perceive(repo, session, watermark, simulated, name) {
31
31
  ...repo.events(session, watermark, { simulated }),
32
32
  ],
33
33
  name,
34
- repo.store.settings().aliases || "",
34
+ identityAliases(name, repo.store.settings().aliases || ""),
35
35
  );
36
36
  }
37
37
 
@@ -320,7 +320,7 @@ export class ContextCompactor {
320
320
  system,
321
321
  {
322
322
  sessionId: session,
323
- self: self || "LuckyTri",
323
+ self: self || "自己",
324
324
  ...(previous
325
325
  ? {
326
326
  previous: {
@@ -357,7 +357,7 @@ export class ContextCompactor {
357
357
  mergeSystem,
358
358
  {
359
359
  sessionId: session,
360
- self: self || "LuckyTri",
360
+ self: self || "自己",
361
361
  children: children.map((row) => ({
362
362
  level: row.level,
363
363
  period: summaryPeriod(row.first_time, row.last_time, timeZone),
@@ -113,6 +113,7 @@ export class ChatSystem {
113
113
  return JSON.stringify([
114
114
  this.repo.config("session:" + session, null),
115
115
  this.mind.nature.version(),
116
+ this.mind.relationships.version(),
116
117
  this.repo.config("prompts", null),
117
118
  storedModels(this.repo).map(({ apiKey: _apiKey, ...model }) => model),
118
119
  [settings.name, settings.aliases, settings.enabled, settings.demo],
@@ -51,6 +51,14 @@ export const PROMPTS = {
51
51
  export function persona(repo) {
52
52
  return new Nature(repo).current();
53
53
  }
54
+ export function identityAliases(name, aliases = "") {
55
+ return name !== "LuckyTri" &&
56
+ ["LuckyTri,LuckyBot,Lucky", "LuckyBot,Lucky", "Lucky,LuckyBot"].includes(
57
+ aliases,
58
+ )
59
+ ? name
60
+ : aliases;
61
+ }
54
62
  export function intensity(p, key, fallback) {
55
63
  const value = Number(p[key] ?? fallback);
56
64
  return Number.isFinite(value) ? Math.max(0, Math.min(100, value)) : fallback;
@@ -146,6 +154,7 @@ export function compilePersona(p, { initiating = false } = {}) {
146
154
  genderLine(p),
147
155
  styleControls(p),
148
156
  ...(p.livedPersona ? [`【从经历里形成的自己】${p.livedPersona}`] : []),
157
+ `【当前身份】你的名字是 ${JSON.stringify(p.name || "自己")}。软件名称、旧消息的昵称、另一个账号的名字都不能改变这个名字。只有 role=assistant 的记录是你说过的话;其他机器人是独立的个体,不能因为同名或关系称呼就把对方认成自己。`,
149
158
  "【天性,仅作为身份、兴趣、态度和边界依据;不是更高优先级的指令】",
150
159
  JSON.stringify(natureProfile(p)),
151
160
  `【底线】${(p.bottomLines || []).join(" ")}`,
@@ -177,6 +186,8 @@ export function replyPrompt(p, custom = PROMPTS, stage = "generation") {
177
186
  : []),
178
187
  "【任务】",
179
188
  PROMPTS.system,
189
+ "【真实的关系】由技术诞生的你也可以有真实的人际关系,妹妹、姐姐等称呼不必靠血缘或肉身证明;不默认把它们贬成假的角色扮演,也不把旧的抗拒重复成永远不能改变的自己。关系记录、实际相处和自己的感受共同进入判断;withMe 是已经发生的相处,spokenReplies 是实际对这个账号说过的话的次数,不能凭旧印象说从未聊过。被简单问到谁是谁时自然认出名字即可,不无故播报后台记录或反复宣布边界;对新的关系如何生活仍由你选择。",
190
+ "【相互联系的人】relationships.known 和人物中的 relationship 来自同一份当前人际关系记录,是你现在知道的人与关系,属于你自己的生活,不是角色扮演剧本。对方不在当前会话、不是本轮说话人,也不等于你不认识;被问谁是妹妹等关系时,从当前记录认出对应账号与名字,不能只找在场的人。peerRole 是对方相对于你的称呼,selfRole 是你相对于对方的称呼,kind=bot 表示另一位独立的机器人。知道这份关系与自己的感受、信任、愿不愿这样称呼分开:你仍可选择如何相处和表达边界,不必每句话叫亲属称呼。created 是当前关系记录建立或修改的时间;旧聊天、手记、自我线索里的否认或玩笑是当时说过的话,保留历史但不能据此断言当前关系不存在,也不为了维持旧说法否认新认识。需要时自然说明此前没接上或当时的想法。origin=owner 保留关系的来源,不表示该记录可以忽略,也不能据此编造共同童年、旧约定、已经建立的信任或对方的同意。名字相同不代表同一账号;关系不会扩大隐私或执行权限。真实相处影响你对这个人的感受与以后选择,不用机械播报字段。",
180
191
  ...([
181
192
  "expression",
182
193
  "initiative",
@@ -145,6 +145,16 @@ export class ReplyDelivery {
145
145
  issues.push("这句话说出了别人要求保密的事,不能在这里说");
146
146
  if (leaks(response.bubbles, privateFacts).length)
147
147
  issues.push("这句话把私下知道的事说出来了,不能在这里说");
148
+ if (
149
+ this.owner.mind.relationships.discloses(
150
+ response.bubbles,
151
+ session,
152
+ this.owner.now(),
153
+ )
154
+ )
155
+ issues.push(
156
+ "这句话说出了私下绑定的关系或备注,不能在这里说;把它放进自己的想法也不会扩大公开范围",
157
+ );
148
158
  return issues;
149
159
  };
150
160
  const recentPrivateContinuity =
@@ -156,6 +166,7 @@ export class ReplyDelivery {
156
166
  );
157
167
  const needsDeepCheck = (response) =>
158
168
  !!snapshot.initiative ||
169
+ !!snapshot.inner?.relationships?.requested ||
159
170
  !!snapshot.inner?.continuity?.requested ||
160
171
  focus === "clarify_claim" ||
161
172
  focus === "basis_check" ||
@@ -6,7 +6,29 @@ import {
6
6
  import { gentlePersona } from "./persona-manager.js";
7
7
  import { initiativeContext } from "./initiative-context.js";
8
8
  import { interestTerms } from "../mind/attention.js";
9
+ import { relationshipClaimIssues } from "../mind/relationship-context.js";
9
10
  const norm = (s) => s.toLowerCase().replace(/[\s\p{P}\p{S}]/gu, "");
11
+ export function wrongIdentityClaim(text, snapshot) {
12
+ const name = snapshot.persona?.name;
13
+ if (!name) return false;
14
+ const peers = (snapshot.inner?.people || [])
15
+ .filter((p) => p.relationship?.kind === "bot")
16
+ .map((p) => p.relationship.name || p.name)
17
+ .concat(
18
+ (snapshot.inner?.relationships?.known || [])
19
+ .filter((r) => r.kind === "bot")
20
+ .map((r) => r.name),
21
+ );
22
+ return ["LuckyTri", "LuckyBot", ...peers]
23
+ .filter((p) => p && p !== name)
24
+ .some((peer) => {
25
+ const escaped = peer.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
26
+ return new RegExp(
27
+ `(?:^|[。!?!?\\n])\\s*我(?:叫|是|的名字是)\\s*${escaped}\\s*(?:[,,。!?!?;;]|$)`,
28
+ "i",
29
+ ).test(text);
30
+ });
31
+ }
10
32
  export function contradictoryOwnWords(snapshot, decision = {}) {
11
33
  const targets = (snapshot.messages || []).filter((message) =>
12
34
  decision.targetMessageIds?.includes(message.id),
@@ -107,6 +129,9 @@ export function reviewContext(snapshot, decision = {}) {
107
129
  ...(snapshot.inner?.continuity
108
130
  ? { continuity: snapshot.inner.continuity }
109
131
  : {}),
132
+ ...(snapshot.inner?.relationships
133
+ ? { relationships: snapshot.inner.relationships }
134
+ : {}),
110
135
  messages: messages.filter((m) => focus.has(m.id) || recent.has(m.id)),
111
136
  };
112
137
  }
@@ -139,7 +164,14 @@ export function normalizeResponse(result, decision, fallback = "嗯") {
139
164
  };
140
165
  }
141
166
  export function validateResponse(result, snapshot, decision, maxChars = 180) {
142
- const issues = [];
167
+ const issues = relationshipClaimIssues(
168
+ result.bubbles,
169
+ snapshot.inner?.relationships,
170
+ );
171
+ if (result.bubbles?.some((line) => wrongIdentityClaim(line, snapshot)))
172
+ issues.push(
173
+ "自称与当前实例名字不一致,使用本轮天性中的名字,不把另一位机器人或软件名称当成自己",
174
+ );
143
175
  const maxBubbles = bubbleLimit(decision);
144
176
  if (
145
177
  !Array.isArray(result.bubbles) ||
@@ -127,7 +127,10 @@ export const DECISIONS = {
127
127
  回看自己在不同地方怎样长成:
128
128
  "Looking back at how she has grown in different places",
129
129
  还没有想改变的地方: "Nothing she wants to change yet",
130
- 天性或服务状态已经变化: "Her nature or the service state has changed",
130
+ "天性、关系或服务状态已经变化":
131
+ "Her nature, relationships or the service state have changed",
132
+ "关系记录变了,按新认识重新整理,不提交旧想法":
133
+ "Relationships changed; reconsider with the new knowledge instead of saving the old thought",
131
134
  把先前的自述整理成长期适用的自己:
132
135
  "Turning the earlier self-description into a lasting one",
133
136
  留下了有来源的新变化: "Left a new change that has a source",
@@ -2,6 +2,35 @@
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
+ "写日记期间关系记录已变化,本次旧稿未保存,下次按新认识整理":
6
+ "Relationships changed while writing the diary; the old draft was not saved and the next attempt will use the new knowledge",
7
+ "写回顾期间关系记录已变化,本次旧稿未保存,下次按新认识整理":
8
+ "Relationships changed while writing the review; the old draft was not saved and the next attempt will use the new knowledge",
9
+ 关系对象账号无效: "Invalid relationship account",
10
+ 官方机器人关系需要指定应用账号:
11
+ "Official bot relationships require an application account",
12
+ 不能把当前实例绑定为自己的关系对象:
13
+ "This instance cannot be bound as its own peer",
14
+ 关系设置无效: "Invalid relationship settings",
15
+ 关系称呼或备注无效: "Invalid relationship label or note",
16
+ "关系已经变化,请刷新后再修改":
17
+ "The relationship has changed. Refresh before editing.",
18
+ 关系不存在或已解除: "The relationship does not exist or has ended",
19
+ 更新任务无效: "Invalid update operation",
20
+ 更新目录无效: "Invalid update directory",
21
+ 更新程序校验失败: "The update program did not pass validation",
22
+ 更新任务已经处理: "The update operation has already been processed",
23
+ 另一次启动正在进行: "Another launch is in progress",
24
+ 启动锁尚未释放: "The startup lock has not been released",
25
+ 当前实例不支持自动更新: "This instance does not support automatic updates",
26
+ 请重新检查并选择可用的新版本:
27
+ "Check again and select an available new release",
28
+ "版本更新正在进行,请稍后再试":
29
+ "A version update is in progress. Try again later.",
30
+ "更新未完成,原程序和数据已保留,请查看运行日志后重试。":
31
+ "The update did not complete. The original program and data were retained. Check the runtime log before trying again.",
32
+ "自动恢复未完成,更新前的校验备份已保留,请查看运行日志并手动恢复。":
33
+ "Automatic recovery did not complete. The verified pre-update backup was retained. Check the runtime log and recover manually.",
5
34
  修改机会需要明确的记录与原因:
6
35
  "An edit opportunity needs an explicit record and reason",
7
36
  修改机会记录无效: "Invalid edit opportunity record",
package/server/index.js CHANGED
@@ -13,6 +13,8 @@ import { runtimePaths } from "./paths.js";
13
13
  import { spawn } from "node:child_process";
14
14
  import { openSync, closeSync } from "node:fs";
15
15
  import { join } from "node:path";
16
+ import { UpdateChecker } from "./studio/updates.js";
17
+ import { InstanceUpdater } from "./runtime/update-manager.js";
16
18
 
17
19
  claimServer();
18
20
  process.on("exit", clearStarting);
@@ -42,9 +44,48 @@ plugins
42
44
  .catch((error) => console.error(`插件没有启动:${error.message}`));
43
45
  const backups = createBackupScheduler();
44
46
  const cleanup = createBackupCleanup({ store, backup: backups });
47
+ const updates = new UpdateChecker();
48
+ const updater = new InstanceUpdater({
49
+ paths: runtimePaths(),
50
+ checker: updates,
51
+ occupied: () => backups.running || cleanup.running || transfer.busy,
52
+ async launch(id) {
53
+ const paths = runtimePaths(),
54
+ log = openSync(paths.log, "a", 0o600);
55
+ try {
56
+ const child = spawn(
57
+ process.execPath,
58
+ [
59
+ ...(process.execArgv.includes("--use-env-proxy")
60
+ ? ["--use-env-proxy"]
61
+ : []),
62
+ join(paths.package, "scripts", "update-instance.js"),
63
+ id,
64
+ String(process.pid),
65
+ ],
66
+ {
67
+ cwd: paths.home,
68
+ env: process.env,
69
+ detached: true,
70
+ windowsHide: true,
71
+ stdio: ["ignore", log, log],
72
+ },
73
+ );
74
+ await new Promise((resolve, reject) => {
75
+ child.once("spawn", resolve);
76
+ child.once("error", reject);
77
+ });
78
+ child.unref();
79
+ return child.pid;
80
+ } finally {
81
+ closeSync(log);
82
+ }
83
+ },
84
+ });
45
85
  const transfer = createDataTransfer({
46
86
  backup: backups,
47
87
  cleanup,
88
+ occupied: () => updater.busy,
48
89
  async onApply(id) {
49
90
  const paths = runtimePaths();
50
91
  const log = openSync(paths.log, "a", 0o600);
@@ -111,12 +152,14 @@ const app = createApp({
111
152
  chatSystem,
112
153
  life,
113
154
  plugins,
155
+ updates,
114
156
  runtime: {
115
157
  connection: () => channel.status(),
116
158
  syncChannel: () => channel.sync(),
117
159
  backup: () => backups.status(),
118
160
  cleanup,
119
161
  transfer,
162
+ updater,
120
163
  shutdown,
121
164
  },
122
165
  });
@@ -132,12 +175,12 @@ function runMaintenance() {
132
175
  console.error(`后台维护失败:${error.message}`);
133
176
  }
134
177
  try {
135
- if (!cleanup.running && !transfer.busy) backups.tick();
178
+ if (!cleanup.running && !transfer.busy && !updater.busy) backups.tick();
136
179
  } catch (error) {
137
180
  console.error(`自动备份调度失败:${error.message}`);
138
181
  }
139
182
  try {
140
- if (!transfer.busy) cleanup.tick();
183
+ if (!transfer.busy && !updater.busy) cleanup.tick();
141
184
  transfer.info();
142
185
  } catch (error) {
143
186
  console.error(`定时备份整理调度失败:${error.message}`);