luckytri 1.0.6 → 1.0.8

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 (168) hide show
  1. package/LICENSE +21 -21
  2. package/README.en.md +7 -3
  3. package/README.md +7 -3
  4. package/docs/en/CHANGELOG.md +35 -16
  5. package/docs/en/chat-style.md +28 -9
  6. package/docs/en/guide.md +5 -5
  7. package/docs/en/her-life.md +2 -2
  8. package/docs/en/webui.md +1 -1
  9. package/docs/zh/CHANGELOG.md +35 -16
  10. package/docs/zh/chat-style.md +30 -9
  11. package/docs/zh/guide.md +5 -5
  12. package/docs/zh/her-life.md +2 -2
  13. package/docs/zh/webui.md +1 -1
  14. package/package.json +1 -1
  15. package/public/app/assets/{ChatsPage-DCYMkJ91.js → ChatsPage-CshdtTzV.js} +2 -2
  16. package/public/app/assets/ChatsPage-DfyPCS9N.css +1 -0
  17. package/public/app/assets/{Empty-CLyZkxR5.js → Empty-C7yzOBzy.js} +1 -1
  18. package/public/app/assets/{Empty-DIobYG3x.css → Empty-DmUB2-Bk.css} +1 -1
  19. package/public/app/assets/{Field-BpaRwLI_.js → Field-BjFv4Rfn.js} +1 -1
  20. package/public/app/assets/HeartPage-Bft9wZp5.css +1 -0
  21. package/public/app/assets/{HeartPage-BIOMTiwr.js → HeartPage-eqo6ru16.js} +1 -1
  22. package/public/app/assets/LifePage-BUi5euBF.css +1 -0
  23. package/public/app/assets/{LifePage-BCgM4JkR.js → LifePage-CN8GE5NK.js} +1 -1
  24. package/public/app/assets/{MemoryPage-vShvRqMt.js → MemoryPage-CCpMN4Wg.js} +1 -1
  25. package/public/app/assets/MemoryPage-cZvXfkXb.css +1 -0
  26. package/public/app/assets/{NaturePage-Cy9ptr4G.js → NaturePage-C0JqdBTR.js} +2 -2
  27. package/public/app/assets/NaturePage-CHkpb5NK.css +1 -0
  28. package/public/app/assets/PeoplePage-BTU0H1pM.css +1 -0
  29. package/public/app/assets/{PeoplePage-CUZekB2E.js → PeoplePage-BXGBbO4u.js} +1 -1
  30. package/public/app/assets/{PluginsPage-1RM4rmJP.js → PluginsPage-Dksp5AV5.js} +1 -1
  31. package/public/app/assets/{SystemPage-BI2CB5Ig.css → SystemPage-BenIyTvk.css} +1 -1
  32. package/public/app/assets/{SystemPage-BeT5TmIV.js → SystemPage-Cd7aa8eE.js} +1 -1
  33. package/public/app/assets/TimePage-BOms_n4I.css +1 -0
  34. package/public/app/assets/{TimePage-taTmPMM8.js → TimePage-Bs_zImb4.js} +2 -2
  35. package/public/app/assets/{index-DY5AM0Hz.css → index-BHZcHUMh.css} +1 -1
  36. package/public/app/assets/{index-DdYwC5ZU.js → index-CyRtYxP3.js} +3 -3
  37. package/public/app/assets/{knowledge-DJmfu2KN.js → knowledge-BJOIyuJo.js} +1 -1
  38. package/public/app/assets/{media-BEtnE0Gv.js → media-Bwtx8blz.js} +1 -1
  39. package/public/app/assets/{mind-Dtiw0-QK.js → mind-DtTVrW6Z.js} +1 -1
  40. package/public/app/index.html +5 -6
  41. package/public/guide.en.html +3 -3
  42. package/public/guide.html +3 -3
  43. package/scripts/import-data.js +128 -128
  44. package/scripts/proxy-config.js +167 -167
  45. package/scripts/update-instance.js +201 -201
  46. package/server/channels/adapters.js +27 -27
  47. package/server/channels/onebot/adapter.js +288 -288
  48. package/server/channels/qqbot/adapter.js +253 -253
  49. package/server/channels/qqbot/api.js +199 -199
  50. package/server/channels/qqbot/credentials.js +11 -11
  51. package/server/channels/qqbot/gateway.js +274 -274
  52. package/server/channels/qqbot/index.js +389 -389
  53. package/server/channels/segments.js +40 -40
  54. package/server/channels/session-key.js +153 -153
  55. package/server/channels/state.js +42 -42
  56. package/server/core/api.js +564 -564
  57. package/server/core/context-builder.js +288 -288
  58. package/server/core/context-compactor.js +454 -454
  59. package/server/core/conversation-cues.js +385 -325
  60. package/server/core/conversation-grounding.js +304 -0
  61. package/server/core/conversation-manager.js +87 -87
  62. package/server/core/dialogue-context.js +244 -0
  63. package/server/core/dialogue-contract.js +35 -0
  64. package/server/core/feedback.js +8 -8
  65. package/server/core/group-style.js +72 -72
  66. package/server/core/initiative-context.js +68 -66
  67. package/server/core/input-budget.js +32 -32
  68. package/server/core/local-demo.js +98 -98
  69. package/server/core/message-manager.js +118 -118
  70. package/server/core/message-scheduler.js +78 -78
  71. package/server/core/model-json.js +190 -190
  72. package/server/core/model-manager.js +5 -8
  73. package/server/core/model-presets.js +900 -900
  74. package/server/core/name-call.js +29 -29
  75. package/server/core/network.js +157 -157
  76. package/server/core/person-name.js +22 -22
  77. package/server/core/persona-manager.js +359 -269
  78. package/server/core/reader-check.js +62 -0
  79. package/server/core/reply-delivery.js +537 -404
  80. package/server/core/reply-target-resolver.js +50 -50
  81. package/server/core/response-generator.js +92 -50
  82. package/server/core/response-validator.js +53 -32
  83. package/server/core/retired-prompts.js +51 -51
  84. package/server/core/sessions.js +109 -109
  85. package/server/core/speaker-names.js +92 -92
  86. package/server/core/topic-tracker.js +65 -65
  87. package/server/core/turn.js +82 -15
  88. package/server/http.js +18 -18
  89. package/server/i18n/decisions.js +209 -209
  90. package/server/i18n/errors.js +2 -0
  91. package/server/i18n/index.js +144 -144
  92. package/server/i18n/labels.js +6 -10
  93. package/server/i18n/models.js +111 -111
  94. package/server/knowledge/api.js +342 -342
  95. package/server/knowledge/embedding-profile.js +114 -114
  96. package/server/knowledge/index.js +13 -13
  97. package/server/knowledge/retrieval.js +93 -93
  98. package/server/knowledge/schema.js +158 -158
  99. package/server/knowledge/vector-index.js +90 -90
  100. package/server/mind/affect.js +265 -265
  101. package/server/mind/attention.js +200 -200
  102. package/server/mind/bonds.js +565 -565
  103. package/server/mind/clock.js +83 -83
  104. package/server/mind/context-selection.js +16 -4
  105. package/server/mind/continuity.js +562 -562
  106. package/server/mind/conversation-origin.js +175 -175
  107. package/server/mind/days.js +210 -210
  108. package/server/mind/evidence-read.js +45 -45
  109. package/server/mind/expression-grounding.js +21 -0
  110. package/server/mind/faces.js +373 -373
  111. package/server/mind/guard.js +68 -60
  112. package/server/mind/initiative.js +338 -338
  113. package/server/mind/life/activities.js +21 -21
  114. package/server/mind/life/context.js +330 -330
  115. package/server/mind/life/growth.js +261 -261
  116. package/server/mind/life/presence.js +305 -305
  117. package/server/mind/meetings.js +1069 -1069
  118. package/server/mind/memory-policy.js +48 -48
  119. package/server/mind/memory.js +888 -888
  120. package/server/mind/nature.js +358 -358
  121. package/server/mind/novelty.js +19 -19
  122. package/server/mind/own-voice.js +35 -0
  123. package/server/mind/periods.js +118 -118
  124. package/server/mind/reading.js +132 -120
  125. package/server/mind/relationship-context.js +71 -71
  126. package/server/mind/relationships.js +390 -390
  127. package/server/mind/salience.js +201 -201
  128. package/server/mind/schema.js +567 -567
  129. package/server/mind/self.js +496 -484
  130. package/server/mind/thoughts.js +274 -274
  131. package/server/mind/time/agenda.js +242 -242
  132. package/server/mind/time/api.js +200 -200
  133. package/server/mind/time/attention.js +34 -34
  134. package/server/mind/time/availability.js +23 -1
  135. package/server/mind/time/executor.js +4 -2
  136. package/server/mind/time/games.js +20 -11
  137. package/server/mind/time/index.js +9 -2
  138. package/server/mind/time/kinds.js +29 -2
  139. package/server/mind/time/material-relevance.js +19 -0
  140. package/server/mind/time/model-material.js +77 -77
  141. package/server/mind/time/presentation.js +35 -51
  142. package/server/mind/time/reading-input.js +31 -0
  143. package/server/mind/time/schedule.js +83 -83
  144. package/server/mind/time/schema.js +84 -84
  145. package/server/mind/time/search.js +383 -383
  146. package/server/mind/time/sharing.js +482 -472
  147. package/server/mind/time/task-links.js +311 -311
  148. package/server/mind/time/tasks.js +41 -3
  149. package/server/mind/time/works.js +527 -432
  150. package/server/mind/traits.js +191 -191
  151. package/server/mind/view.js +62 -12
  152. package/server/runtime/installation.js +116 -116
  153. package/server/runtime/update-manager.js +140 -140
  154. package/server/storage/data-package.js +298 -298
  155. package/server/storage/data-transfer-api.js +50 -50
  156. package/server/storage/data-transfer-worker.js +18 -18
  157. package/server/storage/data-transfer.js +416 -416
  158. package/server/studio/display-names.js +139 -139
  159. package/server/studio/management.js +102 -102
  160. package/server/studio/updates.js +144 -144
  161. package/server/version.js +10 -10
  162. package/public/app/assets/ChatsPage-CFMVozoG.css +0 -1
  163. package/public/app/assets/HeartPage-C-2jvgzF.css +0 -1
  164. package/public/app/assets/LifePage-CG1OHbRK.css +0 -1
  165. package/public/app/assets/MemoryPage-C_Bu4rfb.css +0 -1
  166. package/public/app/assets/NaturePage-CPZlImT0.css +0 -1
  167. package/public/app/assets/PeoplePage-3RRUITDz.css +0 -1
  168. package/public/app/assets/TimePage-C-guwPk2.css +0 -1
@@ -1,116 +1,116 @@
1
- import {
2
- existsSync,
3
- lstatSync,
4
- mkdirSync,
5
- readFileSync,
6
- realpathSync,
7
- readdirSync,
8
- renameSync,
9
- rmdirSync,
10
- writeFileSync,
11
- unlinkSync,
12
- } from "node:fs";
13
- import { join, relative, isAbsolute } from "node:path";
14
- import { randomUUID } from "node:crypto";
15
- import { compareVersions } from "../studio/updates.js";
16
- export const UPDATE_ID =
17
- /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
18
- export const updateRoot = (home) => join(home, "runtime");
19
- export function updatePath(home, id) {
20
- if (!UPDATE_ID.test(String(id))) throw Error("更新任务无效");
21
- const root = updateRoot(home),
22
- path = join(root, "releases", id);
23
- // Managed paths cannot be symlinked into an unrelated installation.
24
- for (const p of [root, join(root, "releases"), path])
25
- if (existsSync(p) && lstatSync(p).isSymbolicLink())
26
- throw Error("更新目录无效");
27
- return path;
28
- }
29
- export function readActive(home) {
30
- try {
31
- return JSON.parse(
32
- readFileSync(join(updateRoot(home), "active.json"), "utf8"),
33
- );
34
- } catch {
35
- return null;
36
- }
37
- }
38
- export function atomicJson(file, value) {
39
- const tmp = `${file}.${randomUUID()}.partial`;
40
- try {
41
- writeFileSync(tmp, JSON.stringify(value), {
42
- mode: 0o600,
43
- flag: "wx",
44
- flush: true,
45
- });
46
- renameSync(tmp, file);
47
- } finally {
48
- if (existsSync(tmp)) unlinkSync(tmp);
49
- }
50
- }
51
- export function writeActive(home, value) {
52
- const root = updateRoot(home);
53
- if (value) updatePath(home, value.id);
54
- mkdirSync(root, { recursive: true, mode: 0o700 });
55
- if (value) atomicJson(join(root, "active.json"), value);
56
- else if (existsSync(join(root, "active.json")))
57
- unlinkSync(join(root, "active.json"));
58
- }
59
- export function installedRelease(home, id, version) {
60
- const slot = updatePath(home, id),
61
- root = join(slot, "node_modules", "luckytri");
62
- const part = relative(realpathSync(slot), realpathSync(root));
63
- if (part.startsWith("..") || isAbsolute(part)) throw Error("更新目录无效");
64
- const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
65
- if (
66
- pkg.name !== "luckytri" ||
67
- pkg.version !== version ||
68
- !existsSync(join(root, "server", "index.js")) ||
69
- !existsSync(join(root, "public", "app", "index.html"))
70
- )
71
- throw Error("更新程序校验失败");
72
- return root;
73
- }
74
- export function resolveInstallation(home, basePackage, baseVersion) {
75
- const active = readActive(home);
76
- if (active) {
77
- try {
78
- // An explicit newer/equal global npm or source install takes precedence.
79
- if (compareVersions(active.version, baseVersion) > 0)
80
- return {
81
- package: installedRelease(home, active.id, active.version),
82
- version: active.version,
83
- };
84
- } catch {
85
- /* Invalid receipts never route the launcher to arbitrary code. */
86
- }
87
- }
88
- return { package: basePackage, version: baseVersion };
89
- }
90
- export function pruneInstallations(home, keep) {
91
- const root = join(updateRoot(home), "releases");
92
- if (!existsSync(root)) return;
93
- for (const entry of readdirSync(root, { withFileTypes: true })) {
94
- if (
95
- !entry.isDirectory() ||
96
- !UPDATE_ID.test(entry.name) ||
97
- keep.includes(entry.name)
98
- )
99
- continue;
100
- const path = updatePath(home, entry.name),
101
- part = relative(realpathSync(root), realpathSync(path));
102
- if (!part || part.startsWith("..") || isAbsolute(part))
103
- throw Error("更新目录无效");
104
- // File removal uses unlink, including Unicode/short Windows paths where
105
- // recursive rm can return without actually removing a lock or directory.
106
- const remove = (directory) => {
107
- for (const child of readdirSync(directory, { withFileTypes: true })) {
108
- const target = join(directory, child.name);
109
- if (child.isDirectory() && !child.isSymbolicLink()) remove(target);
110
- else unlinkSync(target);
111
- }
112
- rmdirSync(directory);
113
- };
114
- remove(path);
115
- }
116
- }
1
+ import {
2
+ existsSync,
3
+ lstatSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ realpathSync,
7
+ readdirSync,
8
+ renameSync,
9
+ rmdirSync,
10
+ writeFileSync,
11
+ unlinkSync,
12
+ } from "node:fs";
13
+ import { join, relative, isAbsolute } from "node:path";
14
+ import { randomUUID } from "node:crypto";
15
+ import { compareVersions } from "../studio/updates.js";
16
+ export const UPDATE_ID =
17
+ /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
18
+ export const updateRoot = (home) => join(home, "runtime");
19
+ export function updatePath(home, id) {
20
+ if (!UPDATE_ID.test(String(id))) throw Error("更新任务无效");
21
+ const root = updateRoot(home),
22
+ path = join(root, "releases", id);
23
+ // Managed paths cannot be symlinked into an unrelated installation.
24
+ for (const p of [root, join(root, "releases"), path])
25
+ if (existsSync(p) && lstatSync(p).isSymbolicLink())
26
+ throw Error("更新目录无效");
27
+ return path;
28
+ }
29
+ export function readActive(home) {
30
+ try {
31
+ return JSON.parse(
32
+ readFileSync(join(updateRoot(home), "active.json"), "utf8"),
33
+ );
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+ export function atomicJson(file, value) {
39
+ const tmp = `${file}.${randomUUID()}.partial`;
40
+ try {
41
+ writeFileSync(tmp, JSON.stringify(value), {
42
+ mode: 0o600,
43
+ flag: "wx",
44
+ flush: true,
45
+ });
46
+ renameSync(tmp, file);
47
+ } finally {
48
+ if (existsSync(tmp)) unlinkSync(tmp);
49
+ }
50
+ }
51
+ export function writeActive(home, value) {
52
+ const root = updateRoot(home);
53
+ if (value) updatePath(home, value.id);
54
+ mkdirSync(root, { recursive: true, mode: 0o700 });
55
+ if (value) atomicJson(join(root, "active.json"), value);
56
+ else if (existsSync(join(root, "active.json")))
57
+ unlinkSync(join(root, "active.json"));
58
+ }
59
+ export function installedRelease(home, id, version) {
60
+ const slot = updatePath(home, id),
61
+ root = join(slot, "node_modules", "luckytri");
62
+ const part = relative(realpathSync(slot), realpathSync(root));
63
+ if (part.startsWith("..") || isAbsolute(part)) throw Error("更新目录无效");
64
+ const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
65
+ if (
66
+ pkg.name !== "luckytri" ||
67
+ pkg.version !== version ||
68
+ !existsSync(join(root, "server", "index.js")) ||
69
+ !existsSync(join(root, "public", "app", "index.html"))
70
+ )
71
+ throw Error("更新程序校验失败");
72
+ return root;
73
+ }
74
+ export function resolveInstallation(home, basePackage, baseVersion) {
75
+ const active = readActive(home);
76
+ if (active) {
77
+ try {
78
+ // An explicit newer/equal global npm or source install takes precedence.
79
+ if (compareVersions(active.version, baseVersion) > 0)
80
+ return {
81
+ package: installedRelease(home, active.id, active.version),
82
+ version: active.version,
83
+ };
84
+ } catch {
85
+ /* Invalid receipts never route the launcher to arbitrary code. */
86
+ }
87
+ }
88
+ return { package: basePackage, version: baseVersion };
89
+ }
90
+ export function pruneInstallations(home, keep) {
91
+ const root = join(updateRoot(home), "releases");
92
+ if (!existsSync(root)) return;
93
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
94
+ if (
95
+ !entry.isDirectory() ||
96
+ !UPDATE_ID.test(entry.name) ||
97
+ keep.includes(entry.name)
98
+ )
99
+ continue;
100
+ const path = updatePath(home, entry.name),
101
+ part = relative(realpathSync(root), realpathSync(path));
102
+ if (!part || part.startsWith("..") || isAbsolute(part))
103
+ throw Error("更新目录无效");
104
+ // File removal uses unlink, including Unicode/short Windows paths where
105
+ // recursive rm can return without actually removing a lock or directory.
106
+ const remove = (directory) => {
107
+ for (const child of readdirSync(directory, { withFileTypes: true })) {
108
+ const target = join(directory, child.name);
109
+ if (child.isDirectory() && !child.isSymbolicLink()) remove(target);
110
+ else unlinkSync(target);
111
+ }
112
+ rmdirSync(directory);
113
+ };
114
+ remove(path);
115
+ }
116
+ }
@@ -1,140 +1,140 @@
1
- import { randomUUID } from "node:crypto";
2
- import { mkdirSync, readFileSync } from "node:fs";
3
- import { join } from "node:path";
4
- import {
5
- atomicJson,
6
- UPDATE_ID,
7
- updatePath,
8
- updateRoot,
9
- } from "./installation.js";
10
- export const UPDATE_FAILED =
11
- "更新未完成,原程序和数据已保留,请查看运行日志后重试。";
12
- export const UPDATE_RECOVERY_FAILED =
13
- "自动恢复未完成,更新前的校验备份已保留,请查看运行日志并手动恢复。";
14
- const ACTIVE = new Set([
15
- "queued",
16
- "downloading",
17
- "checking",
18
- "stopping",
19
- "backup",
20
- "restarting",
21
- "verifying",
22
- "restoring",
23
- ]);
24
- export const processAlive = (pid) => {
25
- if (!Number.isInteger(pid) || pid <= 0) return false;
26
- try {
27
- process.kill(pid, 0);
28
- return true;
29
- } catch (e) {
30
- return e.code !== "ESRCH";
31
- }
32
- };
33
- export function readUpdateJob(home, id) {
34
- updatePath(home, id);
35
- const job = JSON.parse(
36
- readFileSync(join(updateRoot(home), "job.json"), "utf8"),
37
- );
38
- if (job.id !== id || !UPDATE_ID.test(job.id)) throw Error("更新任务无效");
39
- return job;
40
- }
41
- export const writeUpdateJob = (home, job) => {
42
- updatePath(home, job.id);
43
- atomicJson(join(updateRoot(home), "job.json"), job);
44
- return job;
45
- };
46
- export class InstanceUpdater {
47
- constructor({
48
- paths,
49
- checker,
50
- launch,
51
- occupied = () => false,
52
- now = Date.now,
53
- }) {
54
- this.paths = paths;
55
- this.checker = checker;
56
- this.launch = launch;
57
- this.occupied = occupied;
58
- this.now = now;
59
- this.starting = false;
60
- }
61
- status() {
62
- let job;
63
- try {
64
- job = JSON.parse(
65
- readFileSync(join(updateRoot(this.paths.home), "job.json"), "utf8"),
66
- );
67
- if (!UPDATE_ID.test(job.id)) return null;
68
- } catch {
69
- return null;
70
- }
71
- if (
72
- ACTIVE.has(job.status) &&
73
- job.workerPid &&
74
- !processAlive(job.workerPid)
75
- ) {
76
- job = writeUpdateJob(this.paths.home, {
77
- ...job,
78
- status: "error",
79
- error: ["queued", "downloading", "checking"].includes(job.status)
80
- ? UPDATE_FAILED
81
- : UPDATE_RECOVERY_FAILED,
82
- finished: this.now(),
83
- });
84
- }
85
- // Public progress is deliberately free of filesystem paths and process data.
86
- return {
87
- id: job.id,
88
- version: job.version,
89
- previous: job.previous,
90
- status: job.status,
91
- started: job.started,
92
- finished: job.finished,
93
- error: job.error || "",
94
- restored: !!job.restored,
95
- };
96
- }
97
- get busy() {
98
- return this.starting || ACTIVE.has(this.status()?.status);
99
- }
100
- async install(version) {
101
- if (this.busy) return this.status();
102
- if (this.occupied()) throw Error("数据处理或备份正在进行,请稍后再试");
103
- if (!this.launch || this.paths.database === ":memory:")
104
- throw Error("当前实例不支持自动更新");
105
- this.starting = true;
106
- try {
107
- const info = await this.checker.check();
108
- if (info.status !== "available" || version !== info.latest)
109
- throw Error("请重新检查并选择可用的新版本");
110
- updatePath(this.paths.home, randomUUID());
111
- mkdirSync(updateRoot(this.paths.home), { recursive: true, mode: 0o700 });
112
- const job = {
113
- id: randomUUID(),
114
- version,
115
- previous: info.current,
116
- status: "queued",
117
- started: this.now(),
118
- parent: process.pid,
119
- };
120
- writeUpdateJob(this.paths.home, job);
121
- try {
122
- const pid = await this.launch(job.id);
123
- writeUpdateJob(this.paths.home, {
124
- ...readUpdateJob(this.paths.home, job.id),
125
- workerPid: pid,
126
- });
127
- } catch {
128
- writeUpdateJob(this.paths.home, {
129
- ...job,
130
- status: "error",
131
- error: UPDATE_FAILED,
132
- });
133
- throw Error(UPDATE_FAILED);
134
- }
135
- return this.status();
136
- } finally {
137
- this.starting = false;
138
- }
139
- }
140
- }
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdirSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import {
5
+ atomicJson,
6
+ UPDATE_ID,
7
+ updatePath,
8
+ updateRoot,
9
+ } from "./installation.js";
10
+ export const UPDATE_FAILED =
11
+ "更新未完成,原程序和数据已保留,请查看运行日志后重试。";
12
+ export const UPDATE_RECOVERY_FAILED =
13
+ "自动恢复未完成,更新前的校验备份已保留,请查看运行日志并手动恢复。";
14
+ const ACTIVE = new Set([
15
+ "queued",
16
+ "downloading",
17
+ "checking",
18
+ "stopping",
19
+ "backup",
20
+ "restarting",
21
+ "verifying",
22
+ "restoring",
23
+ ]);
24
+ export const processAlive = (pid) => {
25
+ if (!Number.isInteger(pid) || pid <= 0) return false;
26
+ try {
27
+ process.kill(pid, 0);
28
+ return true;
29
+ } catch (e) {
30
+ return e.code !== "ESRCH";
31
+ }
32
+ };
33
+ export function readUpdateJob(home, id) {
34
+ updatePath(home, id);
35
+ const job = JSON.parse(
36
+ readFileSync(join(updateRoot(home), "job.json"), "utf8"),
37
+ );
38
+ if (job.id !== id || !UPDATE_ID.test(job.id)) throw Error("更新任务无效");
39
+ return job;
40
+ }
41
+ export const writeUpdateJob = (home, job) => {
42
+ updatePath(home, job.id);
43
+ atomicJson(join(updateRoot(home), "job.json"), job);
44
+ return job;
45
+ };
46
+ export class InstanceUpdater {
47
+ constructor({
48
+ paths,
49
+ checker,
50
+ launch,
51
+ occupied = () => false,
52
+ now = Date.now,
53
+ }) {
54
+ this.paths = paths;
55
+ this.checker = checker;
56
+ this.launch = launch;
57
+ this.occupied = occupied;
58
+ this.now = now;
59
+ this.starting = false;
60
+ }
61
+ status() {
62
+ let job;
63
+ try {
64
+ job = JSON.parse(
65
+ readFileSync(join(updateRoot(this.paths.home), "job.json"), "utf8"),
66
+ );
67
+ if (!UPDATE_ID.test(job.id)) return null;
68
+ } catch {
69
+ return null;
70
+ }
71
+ if (
72
+ ACTIVE.has(job.status) &&
73
+ job.workerPid &&
74
+ !processAlive(job.workerPid)
75
+ ) {
76
+ job = writeUpdateJob(this.paths.home, {
77
+ ...job,
78
+ status: "error",
79
+ error: ["queued", "downloading", "checking"].includes(job.status)
80
+ ? UPDATE_FAILED
81
+ : UPDATE_RECOVERY_FAILED,
82
+ finished: this.now(),
83
+ });
84
+ }
85
+ // Public progress is deliberately free of filesystem paths and process data.
86
+ return {
87
+ id: job.id,
88
+ version: job.version,
89
+ previous: job.previous,
90
+ status: job.status,
91
+ started: job.started,
92
+ finished: job.finished,
93
+ error: job.error || "",
94
+ restored: !!job.restored,
95
+ };
96
+ }
97
+ get busy() {
98
+ return this.starting || ACTIVE.has(this.status()?.status);
99
+ }
100
+ async install(version) {
101
+ if (this.busy) return this.status();
102
+ if (this.occupied()) throw Error("数据处理或备份正在进行,请稍后再试");
103
+ if (!this.launch || this.paths.database === ":memory:")
104
+ throw Error("当前实例不支持自动更新");
105
+ this.starting = true;
106
+ try {
107
+ const info = await this.checker.check();
108
+ if (info.status !== "available" || version !== info.latest)
109
+ throw Error("请重新检查并选择可用的新版本");
110
+ updatePath(this.paths.home, randomUUID());
111
+ mkdirSync(updateRoot(this.paths.home), { recursive: true, mode: 0o700 });
112
+ const job = {
113
+ id: randomUUID(),
114
+ version,
115
+ previous: info.current,
116
+ status: "queued",
117
+ started: this.now(),
118
+ parent: process.pid,
119
+ };
120
+ writeUpdateJob(this.paths.home, job);
121
+ try {
122
+ const pid = await this.launch(job.id);
123
+ writeUpdateJob(this.paths.home, {
124
+ ...readUpdateJob(this.paths.home, job.id),
125
+ workerPid: pid,
126
+ });
127
+ } catch {
128
+ writeUpdateJob(this.paths.home, {
129
+ ...job,
130
+ status: "error",
131
+ error: UPDATE_FAILED,
132
+ });
133
+ throw Error(UPDATE_FAILED);
134
+ }
135
+ return this.status();
136
+ } finally {
137
+ this.starting = false;
138
+ }
139
+ }
140
+ }