luckytri 1.0.0 → 1.0.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.
Files changed (139) hide show
  1. package/.github/SECURITY.md +4 -4
  2. package/README.en.md +252 -250
  3. package/README.md +252 -250
  4. package/bin/luckytri.js +5 -1
  5. package/docs/en/CHANGELOG.md +8 -0
  6. package/docs/en/connect.md +18 -18
  7. package/docs/en/guide.md +202 -216
  8. package/docs/en/npm-release.md +25 -25
  9. package/docs/zh/CHANGELOG.md +8 -0
  10. package/docs/zh/connect.md +17 -17
  11. package/docs/zh/guide.md +202 -216
  12. package/docs/zh/npm-release.md +33 -33
  13. package/package.json +110 -108
  14. package/plugins/sdk/index.d.ts +155 -155
  15. package/plugins/sdk/testing.js +58 -58
  16. package/plugins/weather/index.js +82 -82
  17. package/plugins/weather/luckytri-plugin.json +29 -29
  18. package/public/app/assets/ChatsPage-BhlJHseG.js +3 -0
  19. package/public/app/assets/Empty-DP6WMGHi.js +2 -0
  20. package/public/app/assets/Field-CtbdqaiB.js +1 -0
  21. package/public/app/assets/HeartPage-32BeTPn2.js +1 -0
  22. package/public/app/assets/LifePage-CxSm0XKI.js +1 -0
  23. package/public/app/assets/MemoryPage-BeKBcPCY.js +1 -0
  24. package/public/app/assets/NaturePage-NEPYl2HS.js +3 -0
  25. package/public/app/assets/PeoplePage-DxtpekwM.js +1 -0
  26. package/public/app/assets/PluginsPage-CVVQwEBI.css +1 -0
  27. package/public/app/assets/PluginsPage-DksEZbUU.js +2 -0
  28. package/public/app/assets/{SystemPage-CAMPoYqM.css → SystemPage-BkRu0Rvx.css} +1 -1
  29. package/public/app/assets/SystemPage-BvzJlHuY.js +1 -0
  30. package/public/app/assets/TimePage-7-awH5XL.js +3 -0
  31. package/public/app/assets/index-4rItIkD-.js +4 -0
  32. package/public/app/assets/{index-YDz1ree1.css → index-DY4e15Kh.css} +1 -1
  33. package/public/app/assets/{knowledge-DG0e0uKw.js → knowledge-C6_HvoyB.js} +1 -1
  34. package/public/app/assets/media-SMfTT-yc.js +1 -0
  35. package/public/app/index.html +3 -3
  36. package/public/guide.en.html +72 -99
  37. package/public/guide.html +44 -86
  38. package/public/tutorial.en.md +25 -39
  39. package/public/tutorial.md +25 -39
  40. package/scripts/backup.js +311 -311
  41. package/scripts/browser.js +41 -41
  42. package/scripts/import-data.js +128 -0
  43. package/scripts/launch.js +101 -94
  44. package/scripts/plugin.js +150 -150
  45. package/scripts/reset-password.js +22 -0
  46. package/scripts/runtime.js +25 -25
  47. package/scripts/service.js +56 -47
  48. package/scripts/setup.js +50 -50
  49. package/scripts/start-server.js +41 -41
  50. package/scripts/stop.js +36 -36
  51. package/server/app.js +112 -96
  52. package/server/auth.js +217 -0
  53. package/server/channels/contract.js +57 -57
  54. package/server/channels/hub.js +165 -165
  55. package/server/channels/index.js +28 -25
  56. package/server/channels/onebot/index.js +335 -336
  57. package/server/channels/onebot/token.js +22 -3
  58. package/server/core/capabilities.js +208 -208
  59. package/server/core/events.js +64 -60
  60. package/server/core/image-preprocessor.js +208 -208
  61. package/server/core/model-manager.js +1213 -1213
  62. package/server/core/net-guard.js +49 -49
  63. package/server/core/orchestrator.js +569 -569
  64. package/server/core/perceivers.js +91 -91
  65. package/server/core/persona-manager.js +243 -243
  66. package/server/core/repository.js +293 -293
  67. package/server/core/response-validator.js +508 -508
  68. package/server/core/turn-processor.js +630 -630
  69. package/server/core/turn.js +236 -236
  70. package/server/core/vision-manager.js +633 -633
  71. package/server/i18n/errors.js +548 -497
  72. package/server/i18n/labels.js +649 -653
  73. package/server/index.js +151 -102
  74. package/server/knowledge/manager.js +386 -386
  75. package/server/knowledge/schema.js +12 -8
  76. package/server/local-access.js +32 -0
  77. package/server/mind/anticipations.js +507 -507
  78. package/server/mind/api.js +703 -703
  79. package/server/mind/budget.js +152 -152
  80. package/server/mind/context-selection.js +61 -61
  81. package/server/mind/day-planner.js +324 -324
  82. package/server/mind/evidence.js +148 -148
  83. package/server/mind/index.js +445 -445
  84. package/server/mind/life/diary.js +786 -786
  85. package/server/mind/life/index.js +1049 -1049
  86. package/server/mind/observations.js +84 -84
  87. package/server/mind/own-day.js +324 -324
  88. package/server/mind/own-voice.js +318 -318
  89. package/server/mind/senses.js +53 -53
  90. package/server/mind/time/activity-clock.js +196 -196
  91. package/server/mind/time/availability.js +69 -69
  92. package/server/mind/time/commitment-review.js +198 -198
  93. package/server/mind/time/executor.js +512 -512
  94. package/server/mind/time/experiences.js +128 -128
  95. package/server/mind/time/games.js +562 -562
  96. package/server/mind/time/index.js +739 -739
  97. package/server/mind/time/intent.js +166 -166
  98. package/server/mind/time/kinds.js +171 -171
  99. package/server/mind/time/plugin-activity.js +207 -207
  100. package/server/mind/time/tasks.js +587 -587
  101. package/server/mind/util.js +110 -110
  102. package/server/mind/view.js +273 -273
  103. package/server/paths.js +102 -102
  104. package/server/plugins/api.js +204 -204
  105. package/server/plugins/host.js +967 -967
  106. package/server/plugins/install.js +295 -295
  107. package/server/plugins/manifest.js +201 -201
  108. package/server/plugins/permissions.js +42 -42
  109. package/server/plugins/runner/guard.mjs +36 -36
  110. package/server/plugins/runner/worker.mjs +250 -250
  111. package/server/plugins/schema.js +91 -91
  112. package/server/plugins/zip.js +112 -112
  113. package/server/startup-lock.js +118 -118
  114. package/server/storage/archive.js +242 -241
  115. package/server/storage/backup-cleanup-worker.js +8 -8
  116. package/server/storage/backup-cleanup.js +415 -414
  117. package/server/storage/backup-lock.js +76 -76
  118. package/server/storage/backup-scheduler.js +131 -131
  119. package/server/storage/data-package.js +298 -0
  120. package/server/storage/data-transfer-api.js +50 -0
  121. package/server/storage/data-transfer-worker.js +18 -0
  122. package/server/storage/data-transfer.js +415 -0
  123. package/server/storage/store.js +202 -199
  124. package/server/studio/readiness.js +181 -190
  125. package/server/studio/settings.js +236 -236
  126. package/public/app/assets/ChatsPage-C-FcRapR.js +0 -3
  127. package/public/app/assets/Empty-CpP5kQHH.js +0 -2
  128. package/public/app/assets/Field-D4gY-INU.js +0 -1
  129. package/public/app/assets/HeartPage-CPy_thQd.js +0 -1
  130. package/public/app/assets/LifePage-CxWvwmSf.js +0 -1
  131. package/public/app/assets/MemoryPage-DC4rhQ68.js +0 -1
  132. package/public/app/assets/NaturePage-IvYw04Zq.js +0 -3
  133. package/public/app/assets/PeoplePage-Cqefm9-q.js +0 -1
  134. package/public/app/assets/PluginsPage-DKSaKvXb.js +0 -2
  135. package/public/app/assets/PluginsPage-HyBe5D8w.css +0 -1
  136. package/public/app/assets/SystemPage-CJmtJkrK.js +0 -1
  137. package/public/app/assets/TimePage-Cw44inM4.js +0 -3
  138. package/public/app/assets/index-BheRW-1t.js +0 -4
  139. package/public/app/assets/media-BzPDBm7e.js +0 -1
@@ -1,41 +1,41 @@
1
- import { spawn } from "node:child_process";
2
- import { join } from "node:path";
3
- import { prepareRuntime } from "./runtime.js";
4
- import {
5
- resolveProxyEnvironment,
6
- supportsNodeEnvironmentProxy,
7
- } from "./proxy-config.js";
8
-
9
- const paths = prepareRuntime();
10
- const proxy = await resolveProxyEnvironment(process.env);
11
- if (proxy.enabled && !supportsNodeEnvironmentProxy())
12
- throw new Error(
13
- "系统代理转发需要 Node.js 24.5 或更高版本。请升级 Node.js 后重启 LuckyTri。",
14
- );
15
-
16
- const args = [
17
- ...(proxy.enabled ? ["--use-env-proxy"] : []),
18
- join(paths.package, "server", "index.js"),
19
- ];
20
- const env = { ...process.env, ...proxy.env };
21
- if (proxy.enabled) console.log("已启用系统代理,外部 API 请求将经代理转发。");
22
- else if (proxy.source === "windows-pac-unsupported")
23
- console.log(
24
- "检测到 PAC 自动代理;LuckyTri 暂不解析 PAC,请改用本机 HTTP 代理或配置 HTTPS_PROXY。",
25
- );
26
- else console.log("未检测到系统代理,外部 API 请求将直连。");
27
-
28
- const child = spawn(process.execPath, args, {
29
- cwd: paths.home,
30
- env,
31
- stdio: "inherit",
32
- });
33
- for (const signal of ["SIGINT", "SIGTERM"])
34
- process.on(signal, () => child.kill(signal));
35
- child.once("error", (error) => {
36
- console.error(`LuckyTri 启动失败:${error.message}`);
37
- process.exitCode = 1;
38
- });
39
- child.once("exit", (code, signal) => {
40
- process.exitCode = signal ? 1 : (code ?? 1);
41
- });
1
+ import { spawn } from "node:child_process";
2
+ import { join } from "node:path";
3
+ import { prepareRuntime } from "./runtime.js";
4
+ import {
5
+ resolveProxyEnvironment,
6
+ supportsNodeEnvironmentProxy,
7
+ } from "./proxy-config.js";
8
+
9
+ const paths = prepareRuntime();
10
+ const proxy = await resolveProxyEnvironment(process.env);
11
+ if (proxy.enabled && !supportsNodeEnvironmentProxy())
12
+ throw new Error(
13
+ "系统代理转发需要 Node.js 24.5 或更高版本。请升级 Node.js 后重启 LuckyTri。",
14
+ );
15
+
16
+ const args = [
17
+ ...(proxy.enabled ? ["--use-env-proxy"] : []),
18
+ join(paths.package, "server", "index.js"),
19
+ ];
20
+ const env = { ...process.env, ...proxy.env };
21
+ if (proxy.enabled) console.log("已启用系统代理,外部 API 请求将经代理转发。");
22
+ else if (proxy.source === "windows-pac-unsupported")
23
+ console.log(
24
+ "检测到 PAC 自动代理;LuckyTri 暂不解析 PAC,请改用本机 HTTP 代理或配置 HTTPS_PROXY。",
25
+ );
26
+ else console.log("未检测到系统代理,外部 API 请求将直连。");
27
+
28
+ const child = spawn(process.execPath, args, {
29
+ cwd: paths.home,
30
+ env,
31
+ stdio: "inherit",
32
+ });
33
+ for (const signal of ["SIGINT", "SIGTERM"])
34
+ process.on(signal, () => child.kill(signal));
35
+ child.once("error", (error) => {
36
+ console.error(`LuckyTri 启动失败:${error.message}`);
37
+ process.exitCode = 1;
38
+ });
39
+ child.once("exit", (code, signal) => {
40
+ process.exitCode = signal ? 1 : (code ?? 1);
41
+ });
package/scripts/stop.js CHANGED
@@ -1,36 +1,36 @@
1
- import { prepareRuntime } from "./runtime.js";
2
- import { running, serviceHeaders, serviceUrl } from "./service.js";
3
- import { starting } from "../server/startup-lock.js";
4
- try {
5
- prepareRuntime({ initialize: false });
6
- const base = serviceUrl();
7
- if (!(await running({ base }))) {
8
- console.log("LuckyTri 已经停止 / LuckyTri is already stopped");
9
- } else {
10
- const stopped = await fetch(base + "/api/service/stop", {
11
- method: "POST",
12
- headers: serviceHeaders(),
13
- body: "{}",
14
- signal: AbortSignal.timeout(5000),
15
- });
16
- if (!stopped.ok) throw new Error("服务拒绝停止请求");
17
- let stoppedConfirmed = false;
18
- for (let attempt = 0; attempt < 40; attempt++) {
19
- await new Promise((resolve) => setTimeout(resolve, 150));
20
- if (!(await running({ base, lenient: true })) && !starting()) {
21
- stoppedConfirmed = true;
22
- break;
23
- }
24
- }
25
- if (!stoppedConfirmed)
26
- throw Error("服务仍在停止中,请稍后再检查 / Service is still stopping");
27
- console.log("LuckyTri 已停止 / LuckyTri stopped");
28
- }
29
- } catch (error) {
30
- console.error(
31
- error instanceof TypeError
32
- ? "未连接到 LuckyTri,可能已经停止"
33
- : error.message,
34
- );
35
- process.exitCode = 1;
36
- }
1
+ import { prepareRuntime } from "./runtime.js";
2
+ import { running, serviceHeaders, serviceUrl } from "./service.js";
3
+ import { starting } from "../server/startup-lock.js";
4
+ try {
5
+ prepareRuntime({ initialize: false });
6
+ const base = serviceUrl();
7
+ if (!(await running({ base }))) {
8
+ console.log("LuckyTri 已经停止 / LuckyTri is already stopped");
9
+ } else {
10
+ const stopped = await fetch(base + "/api/service/stop", {
11
+ method: "POST",
12
+ headers: serviceHeaders(),
13
+ body: "{}",
14
+ signal: AbortSignal.timeout(5000),
15
+ });
16
+ if (!stopped.ok) throw new Error("服务拒绝停止请求");
17
+ let stoppedConfirmed = false;
18
+ for (let attempt = 0; attempt < 40; attempt++) {
19
+ await new Promise((resolve) => setTimeout(resolve, 150));
20
+ if (!(await running({ base, lenient: true })) && !starting()) {
21
+ stoppedConfirmed = true;
22
+ break;
23
+ }
24
+ }
25
+ if (!stoppedConfirmed)
26
+ throw Error("服务仍在停止中,请稍后再检查 / Service is still stopping");
27
+ console.log("LuckyTri 已停止 / LuckyTri stopped");
28
+ }
29
+ } catch (error) {
30
+ console.error(
31
+ error instanceof TypeError
32
+ ? "未连接到 LuckyTri,可能已经停止"
33
+ : error.message,
34
+ );
35
+ process.exitCode = 1;
36
+ }
package/server/app.js CHANGED
@@ -1,96 +1,112 @@
1
- import express from "express";
2
- import { existsSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { tokenEqual } from "./http.js";
5
- import { localizeApi } from "./i18n/index.js";
6
- import { mountStudio } from "./studio/settings.js";
7
- import { mountManagement } from "./studio/management.js";
8
- import { mountCore } from "./core/api.js";
9
- import { mountKnowledge } from "./knowledge/api.js";
10
- import { mountEvents } from "./core/events.js";
11
- import { mountMind } from "./mind/api.js";
12
- import { mountPlugins } from "./plugins/api.js";
13
- import { Life } from "./mind/life/index.js";
14
- import { runtimePaths } from "./paths.js";
15
-
16
- export function createApp({ store, chatSystem, runtime, life, plugins }) {
17
- const app = express();
18
- app.use("/api", localizeApi);
19
- app.use("/api", (req, res, next) => {
20
- res.setHeader("Cache-Control", "no-store");
21
- if (
22
- req.headers.origin &&
23
- req.headers.origin !== `${req.protocol}://${req.headers.host}`
24
- )
25
- return res.status(403).json({ error: "不允许跨站访问" });
26
- if (
27
- process.env.ADMIN_TOKEN &&
28
- !tokenEqual(
29
- req.headers.authorization,
30
- `Bearer ${process.env.ADMIN_TOKEN}`,
31
- )
32
- )
33
- return res.status(401).json({ error: "请输入管理令牌" });
34
- next();
35
- });
36
- if (plugins) {
37
- app.post(
38
- "/api/plugins/import/upload",
39
- express.raw({ type: "*/*", limit: "21mb" }),
40
- (req, res) => {
41
- try {
42
- res.json(plugins.install.stageUpload(req.body));
43
- } catch (error) {
44
- res.status(400).json({ error: error.message });
45
- }
46
- },
47
- );
48
- }
49
- app.use(express.json({ limit: "4mb" }));
50
- app.use("/api", (req, res, next) => {
51
- if (
52
- ["POST", "PATCH", "PUT"].includes(req.method) &&
53
- (!req.body || typeof req.body !== "object" || Array.isArray(req.body))
54
- )
55
- return res.status(400).json({ error: "请提交 JSON 对象" });
56
- next();
57
- });
58
- mountStudio(app, store, runtime, chatSystem);
59
- mountManagement(app, store, chatSystem);
60
- mountCore(app, chatSystem);
61
- mountMind(app, chatSystem, life || new Life(chatSystem));
62
- mountKnowledge(app, chatSystem);
63
- mountEvents(app, chatSystem);
64
- mountPlugins(app, plugins);
65
- // Built assets carry a content hash; the page that names them must not be
66
- // cached, or an upgrade leaves the browser asking for files that are gone.
67
- app.use(
68
- "/app",
69
- express.static(join(runtimePaths().public, "app"), {
70
- setHeaders(res, path) {
71
- res.setHeader(
72
- "Cache-Control",
73
- path.endsWith(".html")
74
- ? "no-cache"
75
- : "public, max-age=31536000, immutable",
76
- );
77
- },
78
- }),
79
- );
80
- const studioIndex = join(runtimePaths().public, "app", "index.html");
81
- app.get(["/", "/index.html", "/app", "/app/"], (req, res, next) => {
82
- if (!existsSync(studioIndex)) return next();
83
- res.setHeader("Cache-Control", "no-cache");
84
- res.sendFile(studioIndex);
85
- });
86
- app.use(express.static(runtimePaths().public));
87
- app.use((err, req, res, next) => {
88
- console.error(err.message);
89
- if (err.type === "entity.too.large")
90
- return res.status(413).json({ error: "请求内容过大" });
91
- if (err.type === "entity.parse.failed")
92
- return res.status(400).json({ error: "JSON 格式无效" });
93
- res.status(500).json({ error: "请求失败,请检查输入或服务日志" });
94
- });
95
- return app;
96
- }
1
+ import express from "express";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { createManagementAuth } from "./auth.js";
5
+ import { localizeApi } from "./i18n/index.js";
6
+ import { mountStudio } from "./studio/settings.js";
7
+ import { mountManagement } from "./studio/management.js";
8
+ import { mountCore } from "./core/api.js";
9
+ import { mountKnowledge } from "./knowledge/api.js";
10
+ import { mountEvents } from "./core/events.js";
11
+ import { mountMind } from "./mind/api.js";
12
+ import { mountPlugins } from "./plugins/api.js";
13
+ import { Life } from "./mind/life/index.js";
14
+ import { runtimePaths } from "./paths.js";
15
+ import { mountDataTransfer } from "./storage/data-transfer-api.js";
16
+
17
+ export function createApp({
18
+ store,
19
+ chatSystem,
20
+ runtime,
21
+ life,
22
+ plugins,
23
+ auth = createManagementAuth(store),
24
+ }) {
25
+ const app = express();
26
+ app.use("/api", localizeApi);
27
+ app.use("/api", (req, res, next) => {
28
+ res.setHeader("Cache-Control", "no-store");
29
+ if (
30
+ req.headers.origin &&
31
+ req.headers.origin !== `${req.protocol}://${req.headers.host}`
32
+ )
33
+ return res.status(403).json({ error: "不允许跨站访问" });
34
+ if (req.headers["sec-fetch-site"] === "cross-site")
35
+ return res.status(403).json({ error: "不允许跨站访问" });
36
+ next();
37
+ });
38
+ app.use(express.json({ limit: "4mb" }));
39
+ const objectBody = (req, res, next) => {
40
+ if (
41
+ ["POST", "PATCH", "PUT"].includes(req.method) &&
42
+ (!req.body || typeof req.body !== "object" || Array.isArray(req.body)) &&
43
+ !["/plugins/import/upload", "/storage/transfer/upload"].includes(req.path)
44
+ )
45
+ return res.status(400).json({ error: "请提交 JSON 对象" });
46
+ next();
47
+ };
48
+ app.use("/api/auth", objectBody);
49
+ auth.mount(app);
50
+ app.use("/api", (req, res, next) => {
51
+ if (
52
+ auth.authorized(req) ||
53
+ (["/service/status", "/service/stop"].includes(req.path) &&
54
+ auth.service(req))
55
+ )
56
+ return next();
57
+ res.status(401).json({ error: "请先登录管理台" });
58
+ });
59
+ app.use("/api", objectBody);
60
+ mountDataTransfer(app, runtime.transfer);
61
+ if (plugins) {
62
+ app.post(
63
+ "/api/plugins/import/upload",
64
+ express.raw({ type: "*/*", limit: "21mb" }),
65
+ (req, res) => {
66
+ try {
67
+ res.json(plugins.install.stageUpload(req.body));
68
+ } catch (error) {
69
+ res.status(400).json({ error: error.message });
70
+ }
71
+ },
72
+ );
73
+ }
74
+ mountStudio(app, store, runtime, chatSystem);
75
+ mountManagement(app, store, chatSystem);
76
+ mountCore(app, chatSystem);
77
+ mountMind(app, chatSystem, life || new Life(chatSystem));
78
+ mountKnowledge(app, chatSystem);
79
+ mountEvents(app, chatSystem, auth.authorized);
80
+ mountPlugins(app, plugins);
81
+ // Built assets carry a content hash; the page that names them must not be
82
+ // cached, or an upgrade leaves the browser asking for files that are gone.
83
+ app.use(
84
+ "/app",
85
+ express.static(join(runtimePaths().public, "app"), {
86
+ setHeaders(res, path) {
87
+ res.setHeader(
88
+ "Cache-Control",
89
+ path.endsWith(".html")
90
+ ? "no-cache"
91
+ : "public, max-age=31536000, immutable",
92
+ );
93
+ },
94
+ }),
95
+ );
96
+ const studioIndex = join(runtimePaths().public, "app", "index.html");
97
+ app.get(["/", "/index.html", "/app", "/app/"], (req, res, next) => {
98
+ if (!existsSync(studioIndex)) return next();
99
+ res.setHeader("Cache-Control", "no-cache");
100
+ res.sendFile(studioIndex);
101
+ });
102
+ app.use(express.static(runtimePaths().public));
103
+ app.use((err, req, res, next) => {
104
+ console.error(err.message);
105
+ if (err.type === "entity.too.large")
106
+ return res.status(413).json({ error: "请求内容过大" });
107
+ if (err.type === "entity.parse.failed")
108
+ return res.status(400).json({ error: "JSON 格式无效" });
109
+ res.status(500).json({ error: "请求失败,请检查输入或服务日志" });
110
+ });
111
+ return app;
112
+ }
package/server/auth.js ADDED
@@ -0,0 +1,217 @@
1
+ import { createHash, randomBytes, scrypt, timingSafeEqual } from "node:crypto";
2
+ import { promisify } from "node:util";
3
+ import { isLoopback, localServiceKey } from "./local-access.js";
4
+ import { tokenEqual, wrap } from "./http.js";
5
+
6
+ const derive = promisify(scrypt);
7
+ const COOKIE = "luckytri_session";
8
+ const MONTH = 30 * 86400000;
9
+ const sessionHash = (value) => createHash("sha256").update(value).digest("hex");
10
+ const digest = async (password, salt) =>
11
+ (await derive(password, salt, 64)).toString("hex");
12
+ function validate(password) {
13
+ if (
14
+ typeof password !== "string" ||
15
+ password.length < 8 ||
16
+ password.length > 256
17
+ )
18
+ throw Error("密码长度应为 8–256 个字符");
19
+ }
20
+
21
+ export function createManagementAuth(
22
+ store,
23
+ { key = localServiceKey(), now = Date.now } = {},
24
+ ) {
25
+ const db = store.db;
26
+ db.exec(`CREATE TABLE IF NOT EXISTS management_auth(id INTEGER PRIMARY KEY CHECK(id=1),salt TEXT NOT NULL,hash TEXT NOT NULL);
27
+ CREATE TABLE IF NOT EXISTS management_sessions(id TEXT PRIMARY KEY,expires INTEGER NOT NULL);`);
28
+ const record = () =>
29
+ db.prepare("SELECT salt,hash FROM management_auth WHERE id=1").get();
30
+ const failures = new Map();
31
+ function throttle(req) {
32
+ const address = req.socket.remoteAddress || "unknown";
33
+ const entry = failures.get(address);
34
+ if (entry && now() - entry.at < 300000 && entry.count >= 8) {
35
+ const error = Error("尝试次数过多,请稍后再试");
36
+ error.status = 429;
37
+ throw error;
38
+ }
39
+ if (!entry || now() - entry.at >= 300000)
40
+ failures.set(address, { count: 0, at: now() });
41
+ if (failures.size > 1000) failures.delete(failures.keys().next().value);
42
+ return failures.get(address);
43
+ }
44
+ async function verifyPassword(password) {
45
+ const saved = record();
46
+ if (!saved || typeof password !== "string" || password.length > 256)
47
+ return false;
48
+ const result = Buffer.from(await digest(password, saved.salt), "hex");
49
+ const expected = Buffer.from(saved.hash, "hex");
50
+ return (
51
+ result.length === expected.length &&
52
+ timingSafeEqual(result, expected) &&
53
+ record()?.hash === saved.hash
54
+ );
55
+ }
56
+ function cookie(req) {
57
+ return (
58
+ String(req.headers.cookie || "")
59
+ .split(";")
60
+ .map((part) => part.trim())
61
+ .find((part) => part.startsWith(COOKIE + "="))
62
+ ?.slice(COOKIE.length + 1) || ""
63
+ );
64
+ }
65
+ function authorized(req) {
66
+ const id = cookie(req);
67
+ if (!/^[\w-]{43}$/.test(id)) return false;
68
+ return !!db
69
+ .prepare("SELECT 1 FROM management_sessions WHERE id=? AND expires>?")
70
+ .get(sessionHash(id), now());
71
+ }
72
+ function service(req) {
73
+ return (
74
+ isLoopback(req.socket.remoteAddress) &&
75
+ tokenEqual(req.headers.authorization, `Bearer ${key}`)
76
+ );
77
+ }
78
+ function session(req, res) {
79
+ db.prepare("DELETE FROM management_sessions WHERE expires<=?").run(now());
80
+ const id = randomBytes(32).toString("base64url");
81
+ db.prepare("INSERT INTO management_sessions(id,expires) VALUES (?,?)").run(
82
+ sessionHash(id),
83
+ now() + MONTH,
84
+ );
85
+ db.exec(
86
+ "DELETE FROM management_sessions WHERE id NOT IN (SELECT id FROM management_sessions ORDER BY expires DESC LIMIT 100)",
87
+ );
88
+ res.cookie(COOKIE, id, {
89
+ httpOnly: true,
90
+ sameSite: "strict",
91
+ secure: req.secure,
92
+ maxAge: MONTH,
93
+ path: "/",
94
+ });
95
+ }
96
+ function mount(app) {
97
+ app.post("/api/auth/reset", (req, res) => {
98
+ if (!service(req))
99
+ return res.status(401).json({ error: "请使用本机命令重置管理密码" });
100
+ db.exec(
101
+ "BEGIN IMMEDIATE; DELETE FROM management_auth; DELETE FROM management_sessions; COMMIT;",
102
+ );
103
+ res.clearCookie(COOKIE, {
104
+ httpOnly: true,
105
+ sameSite: "strict",
106
+ secure: req.secure,
107
+ path: "/",
108
+ });
109
+ res.json({ ok: true });
110
+ });
111
+ app.get("/api/auth/status", (req, res) =>
112
+ res.json({ configured: !!record(), authenticated: authorized(req) }),
113
+ );
114
+ app.post(
115
+ "/api/auth/setup",
116
+ wrap(async (req, res) => {
117
+ if (
118
+ !isLoopback(req.socket.remoteAddress) ||
119
+ !["localhost", "127.0.0.1", "[::1]"].includes(req.hostname)
120
+ ) {
121
+ const error = Error("首次设置密码请在本机打开管理台");
122
+ error.status = 403;
123
+ throw error;
124
+ }
125
+ if (record()) {
126
+ const error = Error("管理密码已设置,请登录");
127
+ error.status = 409;
128
+ throw error;
129
+ }
130
+ validate(req.body.password);
131
+ const salt = randomBytes(16).toString("hex");
132
+ const hash = await digest(req.body.password, salt);
133
+ const inserted = db
134
+ .prepare(
135
+ "INSERT OR IGNORE INTO management_auth(id,salt,hash) VALUES (1,?,?)",
136
+ )
137
+ .run(salt, hash);
138
+ if (!inserted.changes) {
139
+ const error = Error("管理密码已设置,请登录");
140
+ error.status = 409;
141
+ throw error;
142
+ }
143
+ session(req, res);
144
+ res.json({ ok: true });
145
+ }),
146
+ );
147
+ app.post(
148
+ "/api/auth/login",
149
+ wrap(async (req, res) => {
150
+ const entry = throttle(req);
151
+ entry.count++;
152
+ if (!(await verifyPassword(req.body.password))) {
153
+ const error = Error("密码不正确");
154
+ error.status = 401;
155
+ throw error;
156
+ }
157
+ entry.count = 0;
158
+ session(req, res);
159
+ res.json({ ok: true });
160
+ }),
161
+ );
162
+ app.post("/api/auth/logout", (req, res) => {
163
+ db.prepare("DELETE FROM management_sessions WHERE id=?").run(
164
+ sessionHash(cookie(req)),
165
+ );
166
+ res.clearCookie(COOKIE, {
167
+ httpOnly: true,
168
+ sameSite: "strict",
169
+ secure: req.secure,
170
+ path: "/",
171
+ });
172
+ res.json({ ok: true });
173
+ });
174
+ app.post(
175
+ "/api/auth/password",
176
+ wrap(async (req, res) => {
177
+ if (!authorized(req)) {
178
+ const error = Error("请先登录管理台");
179
+ error.status = 401;
180
+ throw error;
181
+ }
182
+ const entry = throttle(req);
183
+ entry.count++;
184
+ const previous = record();
185
+ if (!(await verifyPassword(req.body.currentPassword)))
186
+ throw Error("当前密码不正确");
187
+ validate(req.body.password);
188
+ const salt = randomBytes(16).toString("hex"),
189
+ hash = await digest(req.body.password, salt);
190
+ const changed = db
191
+ .prepare(
192
+ "UPDATE management_auth SET salt=?,hash=? WHERE id=1 AND hash=?",
193
+ )
194
+ .run(salt, hash, previous.hash);
195
+ if (!changed.changes) throw Error("密码已变化,请重新登录");
196
+ db.exec("DELETE FROM management_sessions");
197
+ entry.count = 0;
198
+ session(req, res);
199
+ res.json({ ok: true });
200
+ }),
201
+ );
202
+ }
203
+ return {
204
+ mount,
205
+ authorized,
206
+ service,
207
+ configured: () => !!record(),
208
+ verifyPassword,
209
+ async verifyConnectionPassword(password, address) {
210
+ const entry = throttle({ socket: { remoteAddress: address } });
211
+ entry.count++;
212
+ const valid = await verifyPassword(password);
213
+ if (valid) entry.count = 0;
214
+ return valid;
215
+ },
216
+ };
217
+ }