luckytri 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/SECURITY.md +71 -0
- package/LICENSE +21 -0
- package/README.en.md +250 -0
- package/README.md +250 -0
- package/bin/luckytri.js +107 -0
- package/docs/brand/banner.png +0 -0
- package/docs/brand/palette.png +0 -0
- package/docs/brand/portrait.png +0 -0
- package/docs/en/CHANGELOG.md +113 -0
- package/docs/en/architecture.md +152 -0
- package/docs/en/chat-style.md +62 -0
- package/docs/en/connect.md +63 -0
- package/docs/en/guide.md +216 -0
- package/docs/en/her-life.md +189 -0
- package/docs/en/npm-release.md +25 -0
- package/docs/en/plugins.md +82 -0
- package/docs/en/recovery.md +28 -0
- package/docs/en/time.md +71 -0
- package/docs/en/webui.md +73 -0
- package/docs/zh/CHANGELOG.md +113 -0
- package/docs/zh/architecture.md +152 -0
- package/docs/zh/chat-style.md +62 -0
- package/docs/zh/connect.md +63 -0
- package/docs/zh/guide.md +216 -0
- package/docs/zh/her-life.md +189 -0
- package/docs/zh/npm-release.md +33 -0
- package/docs/zh/plugins.md +82 -0
- package/docs/zh/recovery.md +28 -0
- package/docs/zh/time.md +71 -0
- package/docs/zh/webui.md +73 -0
- package/package.json +108 -0
- package/plugins/sdk/index.d.ts +155 -0
- package/plugins/sdk/testing.js +58 -0
- package/plugins/weather/index.js +82 -0
- package/plugins/weather/luckytri-plugin.json +29 -0
- package/public/app/assets/ChatsPage-C-FcRapR.js +3 -0
- package/public/app/assets/ChatsPage-DfyPCS9N.css +1 -0
- package/public/app/assets/Empty-CpP5kQHH.js +2 -0
- package/public/app/assets/Empty-DmUB2-Bk.css +1 -0
- package/public/app/assets/Field-D4gY-INU.js +1 -0
- package/public/app/assets/HeartPage-Bft9wZp5.css +1 -0
- package/public/app/assets/HeartPage-CPy_thQd.js +1 -0
- package/public/app/assets/LifePage-BUi5euBF.css +1 -0
- package/public/app/assets/LifePage-CxWvwmSf.js +1 -0
- package/public/app/assets/MemoryPage-DC4rhQ68.js +1 -0
- package/public/app/assets/MemoryPage-cZvXfkXb.css +1 -0
- package/public/app/assets/NaturePage-CTXHCE2D.css +1 -0
- package/public/app/assets/NaturePage-IvYw04Zq.js +3 -0
- package/public/app/assets/PeoplePage-BmUxkrOU.css +1 -0
- package/public/app/assets/PeoplePage-Cqefm9-q.js +1 -0
- package/public/app/assets/PluginsPage-DKSaKvXb.js +2 -0
- package/public/app/assets/PluginsPage-HyBe5D8w.css +1 -0
- package/public/app/assets/SystemPage-CAMPoYqM.css +1 -0
- package/public/app/assets/SystemPage-CJmtJkrK.js +1 -0
- package/public/app/assets/TimePage-BOms_n4I.css +1 -0
- package/public/app/assets/TimePage-Cw44inM4.js +3 -0
- package/public/app/assets/formatters-ZmW_xFHw.js +1 -0
- package/public/app/assets/index-BheRW-1t.js +4 -0
- package/public/app/assets/index-YDz1ree1.css +1 -0
- package/public/app/assets/knowledge-DG0e0uKw.js +1 -0
- package/public/app/assets/liquid-garden-IMoHBwzc.png +0 -0
- package/public/app/assets/luckytri-home-95QKeMtc.png +0 -0
- package/public/app/assets/media-BzPDBm7e.js +1 -0
- package/public/app/index.html +27 -0
- package/public/guide.en.html +1053 -0
- package/public/guide.html +893 -0
- package/public/plugin-kit/v1/bridge.js +30 -0
- package/public/plugin-kit/v1/kit.css +14 -0
- package/public/tutorial.en.md +214 -0
- package/public/tutorial.md +214 -0
- package/scripts/backup.js +311 -0
- package/scripts/browser.js +41 -0
- package/scripts/launch.js +94 -0
- package/scripts/plugin.js +150 -0
- package/scripts/proxy-config.js +167 -0
- package/scripts/recover.js +26 -0
- package/scripts/runtime.js +25 -0
- package/scripts/service.js +47 -0
- package/scripts/setup.js +50 -0
- package/scripts/start-server.js +41 -0
- package/scripts/stop.js +36 -0
- package/server/app.js +96 -0
- package/server/channels/adapters.js +27 -0
- package/server/channels/contract.js +57 -0
- package/server/channels/hub.js +165 -0
- package/server/channels/index.js +25 -0
- package/server/channels/onebot/adapter.js +288 -0
- package/server/channels/onebot/index.js +336 -0
- package/server/channels/onebot/token.js +3 -0
- package/server/channels/qqbot/adapter.js +253 -0
- package/server/channels/qqbot/api.js +199 -0
- package/server/channels/qqbot/credentials.js +11 -0
- package/server/channels/qqbot/gateway.js +274 -0
- package/server/channels/qqbot/index.js +389 -0
- package/server/channels/segments.js +40 -0
- package/server/channels/session-key.js +153 -0
- package/server/channels/state.js +42 -0
- package/server/core/api.js +564 -0
- package/server/core/capabilities.js +208 -0
- package/server/core/context-builder.js +288 -0
- package/server/core/context-compactor.js +454 -0
- package/server/core/conversation-cues.js +325 -0
- package/server/core/conversation-manager.js +87 -0
- package/server/core/events.js +60 -0
- package/server/core/feedback.js +8 -0
- package/server/core/group-style.js +72 -0
- package/server/core/image-preprocessor.js +208 -0
- package/server/core/initiative-context.js +66 -0
- package/server/core/input-budget.js +32 -0
- package/server/core/local-demo.js +98 -0
- package/server/core/message-manager.js +118 -0
- package/server/core/message-scheduler.js +78 -0
- package/server/core/model-json.js +190 -0
- package/server/core/model-manager.js +1213 -0
- package/server/core/model-presets.js +900 -0
- package/server/core/name-call.js +29 -0
- package/server/core/net-guard.js +49 -0
- package/server/core/network.js +157 -0
- package/server/core/orchestrator.js +569 -0
- package/server/core/perceivers.js +91 -0
- package/server/core/person-name.js +22 -0
- package/server/core/persona-manager.js +243 -0
- package/server/core/reply-delivery.js +393 -0
- package/server/core/reply-target-resolver.js +50 -0
- package/server/core/repository.js +293 -0
- package/server/core/response-generator.js +50 -0
- package/server/core/response-validator.js +508 -0
- package/server/core/retired-prompts.js +51 -0
- package/server/core/sessions.js +109 -0
- package/server/core/speaker-names.js +92 -0
- package/server/core/topic-tracker.js +65 -0
- package/server/core/turn-processor.js +630 -0
- package/server/core/turn.js +236 -0
- package/server/core/vision-manager.js +633 -0
- package/server/http.js +18 -0
- package/server/i18n/decisions.js +206 -0
- package/server/i18n/errors.js +497 -0
- package/server/i18n/index.js +144 -0
- package/server/i18n/labels.js +653 -0
- package/server/i18n/models.js +111 -0
- package/server/index.js +102 -0
- package/server/knowledge/api.js +342 -0
- package/server/knowledge/embedding-profile.js +114 -0
- package/server/knowledge/index.js +13 -0
- package/server/knowledge/manager.js +386 -0
- package/server/knowledge/retrieval.js +93 -0
- package/server/knowledge/schema.js +154 -0
- package/server/knowledge/vector-index.js +90 -0
- package/server/mind/affect.js +265 -0
- package/server/mind/anticipations.js +507 -0
- package/server/mind/api.js +703 -0
- package/server/mind/attention.js +200 -0
- package/server/mind/bonds.js +558 -0
- package/server/mind/budget.js +152 -0
- package/server/mind/clock.js +83 -0
- package/server/mind/context-selection.js +61 -0
- package/server/mind/continuity.js +562 -0
- package/server/mind/conversation-origin.js +175 -0
- package/server/mind/day-planner.js +324 -0
- package/server/mind/days.js +210 -0
- package/server/mind/evidence-read.js +45 -0
- package/server/mind/evidence.js +148 -0
- package/server/mind/faces.js +138 -0
- package/server/mind/guard.js +60 -0
- package/server/mind/index.js +445 -0
- package/server/mind/initiative.js +333 -0
- package/server/mind/life/activities.js +21 -0
- package/server/mind/life/context.js +309 -0
- package/server/mind/life/diary.js +786 -0
- package/server/mind/life/growth.js +252 -0
- package/server/mind/life/index.js +1049 -0
- package/server/mind/life/presence.js +305 -0
- package/server/mind/meetings.js +1069 -0
- package/server/mind/memory-policy.js +48 -0
- package/server/mind/memory.js +888 -0
- package/server/mind/nature.js +262 -0
- package/server/mind/novelty.js +19 -0
- package/server/mind/observations.js +84 -0
- package/server/mind/own-day.js +324 -0
- package/server/mind/own-voice.js +318 -0
- package/server/mind/periods.js +118 -0
- package/server/mind/reading.js +120 -0
- package/server/mind/salience.js +201 -0
- package/server/mind/schema.js +562 -0
- package/server/mind/self.js +484 -0
- package/server/mind/senses.js +53 -0
- package/server/mind/thoughts.js +274 -0
- package/server/mind/time/activity-clock.js +196 -0
- package/server/mind/time/agenda.js +242 -0
- package/server/mind/time/api.js +200 -0
- package/server/mind/time/attention.js +34 -0
- package/server/mind/time/availability.js +69 -0
- package/server/mind/time/commitment-review.js +198 -0
- package/server/mind/time/delivery-links.js +130 -0
- package/server/mind/time/executor.js +512 -0
- package/server/mind/time/experiences.js +128 -0
- package/server/mind/time/games.js +562 -0
- package/server/mind/time/index.js +739 -0
- package/server/mind/time/intent.js +166 -0
- package/server/mind/time/kinds.js +171 -0
- package/server/mind/time/model-material.js +77 -0
- package/server/mind/time/plugin-activity.js +207 -0
- package/server/mind/time/presentation.js +51 -0
- package/server/mind/time/schedule.js +83 -0
- package/server/mind/time/schema.js +84 -0
- package/server/mind/time/search.js +383 -0
- package/server/mind/time/sharing.js +472 -0
- package/server/mind/time/task-links.js +311 -0
- package/server/mind/time/tasks.js +587 -0
- package/server/mind/time/works.js +432 -0
- package/server/mind/traits.js +191 -0
- package/server/mind/util.js +110 -0
- package/server/mind/view.js +273 -0
- package/server/paths.js +102 -0
- package/server/plugins/api.js +204 -0
- package/server/plugins/host.js +967 -0
- package/server/plugins/install.js +295 -0
- package/server/plugins/manifest.js +201 -0
- package/server/plugins/permissions.js +42 -0
- package/server/plugins/runner/guard.mjs +36 -0
- package/server/plugins/runner/worker.mjs +250 -0
- package/server/plugins/schema.js +91 -0
- package/server/plugins/zip.js +112 -0
- package/server/startup-lock.js +118 -0
- package/server/storage/archive.js +241 -0
- package/server/storage/backup-cleanup-worker.js +8 -0
- package/server/storage/backup-cleanup.js +414 -0
- package/server/storage/backup-lock.js +76 -0
- package/server/storage/backup-scheduler.js +131 -0
- package/server/storage/store.js +199 -0
- package/server/studio/display-names.js +138 -0
- package/server/studio/management.js +102 -0
- package/server/studio/readiness.js +190 -0
- package/server/studio/settings.js +236 -0
- package/server/version.js +10 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import net from "node:net";
|
|
3
|
+
|
|
4
|
+
const WINDOWS_INTERNET_SETTINGS =
|
|
5
|
+
"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings";
|
|
6
|
+
const LOCAL_BYPASS = ["localhost", "127.0.0.1", "::1"];
|
|
7
|
+
|
|
8
|
+
function environmentProxy(env) {
|
|
9
|
+
const http = env.http_proxy || env.HTTP_PROXY || "";
|
|
10
|
+
const https = env.https_proxy || env.HTTPS_PROXY || http;
|
|
11
|
+
return http || https ? { http, https } : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function readWindowsValue(name) {
|
|
15
|
+
try {
|
|
16
|
+
const output = execFileSync(
|
|
17
|
+
"reg.exe",
|
|
18
|
+
["query", WINDOWS_INTERNET_SETTINGS, "/v", name],
|
|
19
|
+
{
|
|
20
|
+
encoding: "utf8",
|
|
21
|
+
windowsHide: true,
|
|
22
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
return (
|
|
26
|
+
output
|
|
27
|
+
.match(new RegExp(`\\b${name}\\s+REG_\\w+\\s+(.+)$`, "im"))?.[1]
|
|
28
|
+
?.trim() || ""
|
|
29
|
+
);
|
|
30
|
+
} catch {
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseWindowsProxyServer(value) {
|
|
36
|
+
const routes = {};
|
|
37
|
+
for (const part of String(value || "").split(";")) {
|
|
38
|
+
const item = part.trim();
|
|
39
|
+
if (!item) continue;
|
|
40
|
+
const separator = item.indexOf("=");
|
|
41
|
+
if (separator < 0) routes.default = item;
|
|
42
|
+
else
|
|
43
|
+
routes[item.slice(0, separator).trim().toLowerCase()] = item
|
|
44
|
+
.slice(separator + 1)
|
|
45
|
+
.trim();
|
|
46
|
+
}
|
|
47
|
+
const normalize = (item) => {
|
|
48
|
+
if (!item) return "";
|
|
49
|
+
try {
|
|
50
|
+
const url = new URL(
|
|
51
|
+
/^[a-z][a-z\d+.-]*:\/\//i.test(item) ? item : `http://${item}`,
|
|
52
|
+
);
|
|
53
|
+
return ["http:", "https:"].includes(url.protocol) ? url.href : "";
|
|
54
|
+
} catch {
|
|
55
|
+
return "";
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const fallback = routes.default || routes.http || routes.https || "";
|
|
59
|
+
return {
|
|
60
|
+
http: normalize(routes.http || fallback || routes.https),
|
|
61
|
+
https: normalize(routes.https || fallback || routes.http),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isLoopbackProxy(url) {
|
|
66
|
+
try {
|
|
67
|
+
const host = new URL(url).hostname.toLowerCase();
|
|
68
|
+
return host === "localhost" || host === "::1" || host.startsWith("127.");
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isListening(url) {
|
|
75
|
+
return new Promise((resolve) => {
|
|
76
|
+
try {
|
|
77
|
+
const proxy = new URL(url);
|
|
78
|
+
const socket = net.createConnection({
|
|
79
|
+
host: proxy.hostname,
|
|
80
|
+
port: Number(proxy.port || (proxy.protocol === "https:" ? 443 : 80)),
|
|
81
|
+
});
|
|
82
|
+
const finish = (value) => {
|
|
83
|
+
socket.destroy();
|
|
84
|
+
resolve(value);
|
|
85
|
+
};
|
|
86
|
+
socket.setTimeout(700, () => finish(false));
|
|
87
|
+
socket.once("connect", () => finish(true));
|
|
88
|
+
socket.once("error", () => finish(false));
|
|
89
|
+
} catch {
|
|
90
|
+
resolve(false);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function mergeNoProxy(...values) {
|
|
96
|
+
const hosts = new Set(LOCAL_BYPASS);
|
|
97
|
+
for (const value of values) {
|
|
98
|
+
for (const host of String(value || "").split(/[;,]/)) {
|
|
99
|
+
const item = host.trim();
|
|
100
|
+
if (item && item.toLowerCase() !== "<local>") hosts.add(item);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return [...hosts].join(",");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function resolveProxyEnvironment(env = process.env) {
|
|
107
|
+
const configured = environmentProxy(env);
|
|
108
|
+
if (configured) {
|
|
109
|
+
return {
|
|
110
|
+
enabled: true,
|
|
111
|
+
source: "environment",
|
|
112
|
+
env: {
|
|
113
|
+
HTTP_PROXY: configured.http || configured.https,
|
|
114
|
+
HTTPS_PROXY: configured.https || configured.http,
|
|
115
|
+
NO_PROXY: mergeNoProxy(env.NO_PROXY, env.no_proxy),
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (process.platform !== "win32")
|
|
121
|
+
return { enabled: false, source: "none", env: {} };
|
|
122
|
+
|
|
123
|
+
const enabled = /^\s*(?:1|0x1)\s*$/i.test(readWindowsValue("ProxyEnable"));
|
|
124
|
+
const server = readWindowsValue("ProxyServer");
|
|
125
|
+
const routes = parseWindowsProxyServer(server);
|
|
126
|
+
const bypass = readWindowsValue("ProxyOverride");
|
|
127
|
+
let source = enabled ? "windows-system" : "none";
|
|
128
|
+
|
|
129
|
+
// Some proxy clients leave the Windows system-proxy toggle off while their
|
|
130
|
+
// local HTTP proxy remains active. Honor that live endpoint for LuckyTri.
|
|
131
|
+
if (
|
|
132
|
+
!enabled &&
|
|
133
|
+
routes.https &&
|
|
134
|
+
isLoopbackProxy(routes.https) &&
|
|
135
|
+
(await isListening(routes.https))
|
|
136
|
+
)
|
|
137
|
+
source = "windows-local-proxy";
|
|
138
|
+
|
|
139
|
+
if (source === "none")
|
|
140
|
+
return {
|
|
141
|
+
enabled: false,
|
|
142
|
+
source: readWindowsValue("AutoConfigURL")
|
|
143
|
+
? "windows-pac-unsupported"
|
|
144
|
+
: source,
|
|
145
|
+
env: {},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
if (!routes.http && !routes.https)
|
|
149
|
+
return { enabled: false, source: "windows-proxy-invalid", env: {} };
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
enabled: true,
|
|
153
|
+
source,
|
|
154
|
+
env: {
|
|
155
|
+
HTTP_PROXY: routes.http || routes.https,
|
|
156
|
+
HTTPS_PROXY: routes.https || routes.http,
|
|
157
|
+
NO_PROXY: mergeNoProxy(env.NO_PROXY, env.no_proxy, bypass),
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function supportsNodeEnvironmentProxy(version = process.versions.node) {
|
|
163
|
+
const [major, minor] = version.split(".").map(Number);
|
|
164
|
+
return (
|
|
165
|
+
major > 24 || (major === 24 && minor >= 5) || (major === 22 && minor >= 21)
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { verifyArchive, restoreToNewFile } from "../server/storage/archive.js";
|
|
2
|
+
const args = process.argv.slice(2);
|
|
3
|
+
const value = (flag) => {
|
|
4
|
+
const index = args.indexOf(flag);
|
|
5
|
+
return index >= 0 ? args[index + 1] : null;
|
|
6
|
+
};
|
|
7
|
+
try {
|
|
8
|
+
const source = value("--backup");
|
|
9
|
+
if (!source || !["verify", "restore"].includes(args[0]))
|
|
10
|
+
throw Error(
|
|
11
|
+
"用法:npm run recovery -- verify --backup 文件;或 restore --backup 文件 --to 新文件",
|
|
12
|
+
);
|
|
13
|
+
if (args[0] === "verify") {
|
|
14
|
+
const result = verifyArchive(source);
|
|
15
|
+
console.log(
|
|
16
|
+
`备份检查通过,数据库版本 ${result.schemaVersion},${result.manifest ? "SHA-256 清单匹配" : "旧备份无校验清单"}`,
|
|
17
|
+
);
|
|
18
|
+
} else {
|
|
19
|
+
const target = value("--to");
|
|
20
|
+
if (!target) throw Error("恢复必须指定 --to 新文件,现有数据库不会被覆盖");
|
|
21
|
+
console.log("恢复并校验完成:" + restoreToNewFile(source, target));
|
|
22
|
+
}
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error(error.message);
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { runtimePaths } from "../server/paths.js";
|
|
3
|
+
import { initializeEnvironment } from "./setup.js";
|
|
4
|
+
|
|
5
|
+
export function prepareRuntime({ initialize = true } = {}) {
|
|
6
|
+
let paths = runtimePaths();
|
|
7
|
+
process.env.LUCKYTRI_HOME = paths.home;
|
|
8
|
+
if (initialize) {
|
|
9
|
+
mkdirSync(paths.home, { recursive: true, mode: 0o700 });
|
|
10
|
+
if (initializeEnvironment(paths.home))
|
|
11
|
+
console.log(
|
|
12
|
+
`已创建本机配置 / Created local configuration: ${paths.config}\n首次访问请在本机编辑器查看 ADMIN_TOKEN;不要分享配置文件。`,
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
process.loadEnvFile(paths.config);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
if (error.code !== "ENOENT") throw error;
|
|
19
|
+
}
|
|
20
|
+
paths = runtimePaths();
|
|
21
|
+
// Child processes and backup workers inherit the same absolute paths.
|
|
22
|
+
process.env.DB_PATH = paths.database;
|
|
23
|
+
process.env.BACKUP_DIR = paths.backups;
|
|
24
|
+
return paths;
|
|
25
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { runtimePaths } from "../server/paths.js";
|
|
2
|
+
|
|
3
|
+
export function serviceUrl(env = process.env) {
|
|
4
|
+
const port = Number(env.PORT || 3210);
|
|
5
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
6
|
+
throw Error("PORT 必须是 1–65535 / PORT must be between 1 and 65535");
|
|
7
|
+
const raw = env.HOST || "127.0.0.1";
|
|
8
|
+
const host = raw === "0.0.0.0" ? "127.0.0.1" : raw === "::" ? "::1" : raw;
|
|
9
|
+
return new URL(`http://${host.includes(":") ? `[${host}]` : host}:${port}`)
|
|
10
|
+
.origin;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const serviceHeaders = () => ({
|
|
14
|
+
"Content-Type": "application/json",
|
|
15
|
+
Authorization: `Bearer ${process.env.ADMIN_TOKEN || ""}`,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export async function running({ base = serviceUrl(), lenient = false } = {}) {
|
|
19
|
+
let response;
|
|
20
|
+
try {
|
|
21
|
+
response = await fetch(base + "/api/service/status", {
|
|
22
|
+
headers: serviceHeaders(),
|
|
23
|
+
signal: AbortSignal.timeout(1500),
|
|
24
|
+
});
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (lenient || error.cause?.code === "ECONNREFUSED") return false;
|
|
27
|
+
throw Error("无法确认端口状态,请检查服务或网络;未重复启动。");
|
|
28
|
+
}
|
|
29
|
+
if (!response.ok)
|
|
30
|
+
throw Error("端口已被占用或管理令牌不匹配,请检查本机配置。");
|
|
31
|
+
let info;
|
|
32
|
+
try {
|
|
33
|
+
info = await response.json();
|
|
34
|
+
} catch {
|
|
35
|
+
throw Error("此端口运行的不是 LuckyTri。");
|
|
36
|
+
}
|
|
37
|
+
const paths = runtimePaths();
|
|
38
|
+
const normalize = (value) =>
|
|
39
|
+
process.platform === "win32" ? value.toLowerCase() : value;
|
|
40
|
+
if (
|
|
41
|
+
!["luckytri", "luckybot", "lucky", "xiaoman"].includes(info.app) ||
|
|
42
|
+
typeof (info.home ?? info.workspace) !== "string" ||
|
|
43
|
+
normalize(info.home ?? info.workspace) !== normalize(paths.home)
|
|
44
|
+
)
|
|
45
|
+
throw Error("此端口已被其他实例占用,未操作任何进程。");
|
|
46
|
+
return true;
|
|
47
|
+
}
|
package/scripts/setup.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { runtimePaths } from "../server/paths.js";
|
|
6
|
+
|
|
7
|
+
export function initializeEnvironment(directory = runtimePaths().home) {
|
|
8
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
9
|
+
const file = resolve(directory, ".env");
|
|
10
|
+
const token = () => randomBytes(32).toString("hex");
|
|
11
|
+
const content = [
|
|
12
|
+
"# LuckyTri 本机配置。修改后重启服务,不要公开此文件。",
|
|
13
|
+
"# LuckyTri local configuration. Restart after editing and never share this file.",
|
|
14
|
+
"HOST=127.0.0.1",
|
|
15
|
+
"PORT=3210",
|
|
16
|
+
`ADMIN_TOKEN=${token()}`,
|
|
17
|
+
"",
|
|
18
|
+
"# 连接 QQ 的方式二选一,也可以在管理界面「连接 QQ」里选择。",
|
|
19
|
+
"# Pick one way to connect QQ; it can also be chosen in the studio under Connect QQ.",
|
|
20
|
+
"# LUCKYTRI_CHANNEL=onebot # onebot | qqbot;设置后界面里不能再改 / locks the choice when set",
|
|
21
|
+
"",
|
|
22
|
+
"# OneBot 11",
|
|
23
|
+
`ONEBOT_TOKEN=${token()}`,
|
|
24
|
+
"",
|
|
25
|
+
"# QQ 官方机器人 / QQ official bot",
|
|
26
|
+
"# QQBOT_APP_ID=",
|
|
27
|
+
"# QQBOT_APP_SECRET=",
|
|
28
|
+
"",
|
|
29
|
+
"LLM_API_KEY=",
|
|
30
|
+
"",
|
|
31
|
+
].join("\n");
|
|
32
|
+
try {
|
|
33
|
+
writeFileSync(file, content, { flag: "wx", mode: 0o600 });
|
|
34
|
+
return true;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (error.code === "EEXIST") return false;
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (
|
|
41
|
+
process.argv[1] &&
|
|
42
|
+
import.meta.url === pathToFileURL(resolve(process.argv[1])).href
|
|
43
|
+
) {
|
|
44
|
+
const created = initializeEnvironment();
|
|
45
|
+
console.log(
|
|
46
|
+
created
|
|
47
|
+
? "已生成 .env 与两枚不同的随机令牌。请在本机编辑器中查看,修改后重启服务。\nCreated .env with two different random tokens. Open it in a local editor and restart after changes."
|
|
48
|
+
: "已有 .env,已保留原文件,不覆盖任何配置。\nA .env already exists and was left untouched.",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +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
ADDED
|
@@ -0,0 +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
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { onebot } from "./onebot/adapter.js";
|
|
2
|
+
import { qqbot } from "./qqbot/adapter.js";
|
|
3
|
+
import { parseSessionKey } from "./session-key.js";
|
|
4
|
+
|
|
5
|
+
// What the core may ask a platform without being connected to it, such as
|
|
6
|
+
// which media links are safe to download. One entry per channel type.
|
|
7
|
+
const adapters = new Map([
|
|
8
|
+
[onebot.type, onebot],
|
|
9
|
+
[qqbot.type, qqbot],
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
export function registerChannel(adapter) {
|
|
13
|
+
adapters.set(adapter.type, adapter);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function adapterFor(sessionOrType = "onebot") {
|
|
17
|
+
try {
|
|
18
|
+
const type = String(sessionOrType).includes(":")
|
|
19
|
+
? parseSessionKey(sessionOrType).channel
|
|
20
|
+
: sessionOrType;
|
|
21
|
+
return adapters.get(type) || onebot;
|
|
22
|
+
} catch {
|
|
23
|
+
return onebot;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { onebot, qqbot };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// A channel is the only part of LuckyTri that knows how a platform connects.
|
|
2
|
+
// Everything above it (sessions, attention, memory, time, her inner life) sees
|
|
3
|
+
// the same neutral message and talks back through the same few calls, so a
|
|
4
|
+
// new platform is one folder that fulfils this contract and one line in the
|
|
5
|
+
// registry (see channels/index.js).
|
|
6
|
+
//
|
|
7
|
+
// type stable id, also the first part of every session key
|
|
8
|
+
// capabilities what the platform can express: mention, quote,
|
|
9
|
+
// image, thread, sticker
|
|
10
|
+
// attach(server, chat) one-time wiring to the HTTP server and the ChatSystem
|
|
11
|
+
// start() begin accepting or opening the connection for the
|
|
12
|
+
// current settings; safe to call again after a change
|
|
13
|
+
// stop() drop the connection and stop timers; safe to repeat
|
|
14
|
+
// send(message, text) deliver one bubble; resolves { message_id }
|
|
15
|
+
// fetchQuoted(message) the message that `message.replyId` points to, or null
|
|
16
|
+
// fetchImage(file) a fresh URL or bytes for an expired image, or null
|
|
17
|
+
// fetchMedia(file, type) optional: a voice or file the same way, or null
|
|
18
|
+
// refreshDirectory() learn real group and friend names; resolves a count
|
|
19
|
+
// canReach(sessionId) whether she may start a conversation there now
|
|
20
|
+
// status() { online, ... } for the connection panel
|
|
21
|
+
// close() final shutdown
|
|
22
|
+
//
|
|
23
|
+
// The message every channel hands to `ChatSystem.receive` is neutral. Besides
|
|
24
|
+
// the fields the core already uses, an adapter fills platformId, accountId,
|
|
25
|
+
// time, mentions, replyId and attachments itself, so nothing above the
|
|
26
|
+
// channel ever has to parse a platform's wire format.
|
|
27
|
+
|
|
28
|
+
const REQUIRED = [
|
|
29
|
+
"start",
|
|
30
|
+
"stop",
|
|
31
|
+
"attach",
|
|
32
|
+
"send",
|
|
33
|
+
"fetchQuoted",
|
|
34
|
+
"fetchImage",
|
|
35
|
+
"refreshDirectory",
|
|
36
|
+
"canReach",
|
|
37
|
+
"status",
|
|
38
|
+
"close",
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
export function defineChannel(channel) {
|
|
42
|
+
if (!channel || typeof channel.type !== "string" || !channel.type)
|
|
43
|
+
throw new TypeError("channel needs a type");
|
|
44
|
+
for (const name of REQUIRED)
|
|
45
|
+
if (typeof channel[name] !== "function")
|
|
46
|
+
throw new TypeError(`channel ${channel.type} is missing ${name}()`);
|
|
47
|
+
return Object.freeze({
|
|
48
|
+
capabilities: {
|
|
49
|
+
mention: false,
|
|
50
|
+
quote: false,
|
|
51
|
+
image: false,
|
|
52
|
+
thread: false,
|
|
53
|
+
sticker: false,
|
|
54
|
+
},
|
|
55
|
+
...channel,
|
|
56
|
+
});
|
|
57
|
+
}
|