luckytri 1.0.3 → 1.0.4

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 (67) hide show
  1. package/bin/luckytri.js +5 -1
  2. package/docs/en/CHANGELOG.md +7 -0
  3. package/docs/en/guide.md +3 -1
  4. package/docs/en/webui.md +3 -1
  5. package/docs/zh/CHANGELOG.md +7 -0
  6. package/docs/zh/guide.md +3 -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 +23 -8
  29. package/public/guide.html +11 -5
  30. package/public/tutorial.en.md +3 -1
  31. package/public/tutorial.md +3 -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 +10 -0
  41. package/server/core/reply-delivery.js +10 -0
  42. package/server/core/response-validator.js +20 -0
  43. package/server/i18n/errors.js +25 -0
  44. package/server/index.js +45 -2
  45. package/server/mind/api.js +25 -0
  46. package/server/mind/bonds.js +9 -2
  47. package/server/mind/index.js +2 -0
  48. package/server/mind/initiative.js +6 -1
  49. package/server/mind/life/context.js +14 -3
  50. package/server/mind/life/growth.js +2 -1
  51. package/server/mind/nature.js +23 -2
  52. package/server/mind/relationships.js +279 -0
  53. package/server/mind/schema.js +2 -0
  54. package/server/mind/time/tasks.js +2 -1
  55. package/server/mind/view.js +2 -0
  56. package/server/runtime/installation.js +116 -0
  57. package/server/runtime/update-execution.js +127 -0
  58. package/server/runtime/update-manager.js +140 -0
  59. package/server/startup-lock.js +12 -12
  60. package/server/storage/archive.js +10 -2
  61. package/server/storage/backup-lock.js +23 -5
  62. package/server/storage/data-transfer.js +2 -1
  63. package/server/studio/display-names.js +1 -0
  64. package/server/studio/settings.js +1 -0
  65. package/server/studio/updates.js +14 -3
  66. package/public/app/assets/PeoplePage-D12cvuy7.js +0 -1
  67. package/public/app/assets/mind-DYRfFMwh.js +0 -1
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,7 @@ export function replyPrompt(p, custom = PROMPTS, stage = "generation") {
177
186
  : []),
178
187
  "【任务】",
179
188
  PROMPTS.system,
189
+ "【相互联系的人】人物中的 relationship 是按账号绑定的人际关系,peerRole 是对方相对于你的称呼,selfRole 是你相对于对方的称呼,kind=bot 表示另一位独立的机器人。它可以影响你的关心、打算、感受与交流,你仍有自己的选择。不必每句话叫亲属称呼;名字相同不代表同一个账号。origin=owner 表示管理者安排的关系,不能据此编造共同童年、旧约定、已经建立的信任或对方的同意。关系不会扩大隐私权限或执行权限;真实相处形成的印象、信任与边界继续依据经历变化。",
180
190
  ...([
181
191
  "expression",
182
192
  "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 =
@@ -7,6 +7,22 @@ import { gentlePersona } from "./persona-manager.js";
7
7
  import { initiativeContext } from "./initiative-context.js";
8
8
  import { interestTerms } from "../mind/attention.js";
9
9
  const norm = (s) => s.toLowerCase().replace(/[\s\p{P}\p{S}]/gu, "");
10
+ export function wrongIdentityClaim(text, snapshot) {
11
+ const name = snapshot.persona?.name;
12
+ if (!name) return false;
13
+ const peers = (snapshot.inner?.people || [])
14
+ .filter((p) => p.relationship?.kind === "bot")
15
+ .map((p) => p.relationship.name || p.name);
16
+ return ["LuckyTri", "LuckyBot", ...peers]
17
+ .filter((p) => p && p !== name)
18
+ .some((peer) => {
19
+ const escaped = peer.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
20
+ return new RegExp(
21
+ `(?:^|[。!?!?\\n])\\s*我(?:叫|是|的名字是)\\s*${escaped}\\s*(?:[,,。!?!?;;]|$)`,
22
+ "i",
23
+ ).test(text);
24
+ });
25
+ }
10
26
  export function contradictoryOwnWords(snapshot, decision = {}) {
11
27
  const targets = (snapshot.messages || []).filter((message) =>
12
28
  decision.targetMessageIds?.includes(message.id),
@@ -140,6 +156,10 @@ export function normalizeResponse(result, decision, fallback = "嗯") {
140
156
  }
141
157
  export function validateResponse(result, snapshot, decision, maxChars = 180) {
142
158
  const issues = [];
159
+ if (result.bubbles?.some((line) => wrongIdentityClaim(line, snapshot)))
160
+ issues.push(
161
+ "自称与当前实例名字不一致,使用本轮天性中的名字,不把另一位机器人或软件名称当成自己",
162
+ );
143
163
  const maxBubbles = bubbleLimit(decision);
144
164
  if (
145
165
  !Array.isArray(result.bubbles) ||
@@ -2,6 +2,31 @@
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
+ 关系对象账号无效: "Invalid relationship account",
6
+ 官方机器人关系需要指定应用账号:
7
+ "Official bot relationships require an application account",
8
+ 不能把当前实例绑定为自己的关系对象:
9
+ "This instance cannot be bound as its own peer",
10
+ 关系设置无效: "Invalid relationship settings",
11
+ 关系称呼或备注无效: "Invalid relationship label or note",
12
+ "关系已经变化,请刷新后再修改":
13
+ "The relationship has changed. Refresh before editing.",
14
+ 关系不存在或已解除: "The relationship does not exist or has ended",
15
+ 更新任务无效: "Invalid update operation",
16
+ 更新目录无效: "Invalid update directory",
17
+ 更新程序校验失败: "The update program did not pass validation",
18
+ 更新任务已经处理: "The update operation has already been processed",
19
+ 另一次启动正在进行: "Another launch is in progress",
20
+ 启动锁尚未释放: "The startup lock has not been released",
21
+ 当前实例不支持自动更新: "This instance does not support automatic updates",
22
+ 请重新检查并选择可用的新版本:
23
+ "Check again and select an available new release",
24
+ "版本更新正在进行,请稍后再试":
25
+ "A version update is in progress. Try again later.",
26
+ "更新未完成,原程序和数据已保留,请查看运行日志后重试。":
27
+ "The update did not complete. The original program and data were retained. Check the runtime log before trying again.",
28
+ "自动恢复未完成,更新前的校验备份已保留,请查看运行日志并手动恢复。":
29
+ "Automatic recovery did not complete. The verified pre-update backup was retained. Check the runtime log and recover manually.",
5
30
  修改机会需要明确的记录与原因:
6
31
  "An edit opportunity needs an explicit record and reason",
7
32
  修改机会记录无效: "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}`);
@@ -1,5 +1,6 @@
1
1
  import { wrap } from "../http.js";
2
2
  import { mountTime } from "./time/api.js";
3
+ import { RELATIONSHIP_ROLES } from "./relationships.js";
3
4
  import { localClock } from "../core/conversation-cues.js";
4
5
  import { agoLabel, elapsedLabel, JUST_NOW } from "./clock.js";
5
6
  import { diffSnapshots } from "./index.js";
@@ -77,6 +78,29 @@ function activityOf({ db, life, now, affect, words, names }) {
77
78
  export function mountMind(app, chat, life) {
78
79
  const { mind } = chat;
79
80
  mountTime(app, life);
81
+ app.get(
82
+ "/api/mind/relationships",
83
+ wrap((_req, res) =>
84
+ res.json({
85
+ roles: RELATIONSHIP_ROLES,
86
+ items: mind.relationships
87
+ .list(life.now())
88
+ .map((r) => mind.relationships.view(r)),
89
+ }),
90
+ ),
91
+ );
92
+ app.put(
93
+ "/api/mind/relationships",
94
+ wrap((req, res) => res.json(mind.relationships.save(req.body, life.now()))),
95
+ );
96
+ app.delete(
97
+ "/api/mind/relationships/:id",
98
+ wrap((req, res) =>
99
+ res.json(
100
+ mind.relationships.end(String(req.params.id), req.body, life.now()),
101
+ ),
102
+ ),
103
+ );
80
104
  // A small, cheap picture of TA for the studio: asked for on every change.
81
105
  app.get(
82
106
  "/api/mind/presence",
@@ -287,6 +311,7 @@ export function mountMind(app, chat, life) {
287
311
  })),
288
312
  },
289
313
  changes: mind.bonds.changes("person", id, 60),
314
+ relationshipHistory: mind.relationships.history(id),
290
315
  memories: db
291
316
  .prepare(
292
317
  "SELECT id,session_id,content,type,confidence,importance,status,discretion,locked,created,superseded_by FROM core_memories WHERE subject=? AND status IN ('confirmed','superseded') ORDER BY CASE status WHEN 'confirmed' THEN 0 ELSE 1 END, created DESC LIMIT 80",