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,110 @@
|
|
|
1
|
+
import { localClock } from "../core/conversation-cues.js";
|
|
2
|
+
|
|
3
|
+
export const HOUR = 3600000;
|
|
4
|
+
export const DAY = 86400000;
|
|
5
|
+
|
|
6
|
+
export function parse(value, fallback) {
|
|
7
|
+
if (value == null) return fallback;
|
|
8
|
+
if (typeof value !== "string") return value;
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(value);
|
|
11
|
+
} catch {
|
|
12
|
+
return fallback;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function clamp(value, min = 0, max = 1) {
|
|
17
|
+
const n = Number(value);
|
|
18
|
+
if (!Number.isFinite(n)) return min;
|
|
19
|
+
return Math.min(max, Math.max(min, n));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function text(value, max) {
|
|
23
|
+
return String(value ?? "")
|
|
24
|
+
.trim()
|
|
25
|
+
.slice(0, max);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function normalized(value) {
|
|
29
|
+
return String(value ?? "")
|
|
30
|
+
.replace(/[\s\p{P}\p{S}]/gu, "")
|
|
31
|
+
.toLowerCase();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Bigram overlap on normalized text: cheap, deterministic, good enough to
|
|
35
|
+
// tell "the same thought again" from a genuinely new one.
|
|
36
|
+
export function similar(a, b, threshold = 0.8) {
|
|
37
|
+
const left = normalized(a);
|
|
38
|
+
const right = normalized(b);
|
|
39
|
+
if (!left || !right) return false;
|
|
40
|
+
if (left.includes(right) || right.includes(left)) return true;
|
|
41
|
+
const grams = (s) =>
|
|
42
|
+
new Set(
|
|
43
|
+
Array.from({ length: Math.max(0, s.length - 1) }, (_, i) =>
|
|
44
|
+
s.slice(i, i + 2),
|
|
45
|
+
),
|
|
46
|
+
);
|
|
47
|
+
const x = grams(left);
|
|
48
|
+
const y = grams(right);
|
|
49
|
+
const shared = [...x].filter((g) => y.has(g)).length;
|
|
50
|
+
return shared / Math.max(1, Math.min(x.size, y.size)) > threshold;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const CREDENTIAL =
|
|
54
|
+
/(?:sk-[a-zA-Z0-9_-]{16,}|Bearer\s+\S{12,}|(?:密码|验证码|口令|API.?Key|token|密钥)\s*(?:是|为|[::=])\s*\S{4,})/i;
|
|
55
|
+
|
|
56
|
+
export function hasCredential(value) {
|
|
57
|
+
return CREDENTIAL.test(String(value ?? ""));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function dayKey(time, timeZone) {
|
|
61
|
+
return localClock(time, timeZone).local.slice(0, 10);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// "2026-10-02" or "2026-10-02 14:30" as a moment in her time zone.
|
|
65
|
+
export function zonedTime(value, timeZone) {
|
|
66
|
+
const match = String(value ?? "")
|
|
67
|
+
.trim()
|
|
68
|
+
.match(/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}))?$/);
|
|
69
|
+
if (!match) return null;
|
|
70
|
+
const [, y, mo, d, h = "00", mi = "00"] = match;
|
|
71
|
+
const guess = Date.UTC(+y, +mo - 1, +d, +h, +mi);
|
|
72
|
+
if (!Number.isFinite(guess)) return null;
|
|
73
|
+
const shown = localClock(guess, timeZone).local;
|
|
74
|
+
const offset = Date.parse(`${shown.replace(" ", "T")}:00Z`) - guess;
|
|
75
|
+
const time = guess - offset;
|
|
76
|
+
return localClock(time, timeZone).local === `${y}-${mo}-${d} ${h}:${mi}`
|
|
77
|
+
? time
|
|
78
|
+
: null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Exponential return toward a baseline.
|
|
82
|
+
export function relax(value, baseline, elapsed, halfLife) {
|
|
83
|
+
if (!(elapsed > 0)) return value;
|
|
84
|
+
return baseline + (value - baseline) * Math.pow(0.5, elapsed / halfLife);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Evidence is stored as short typed strings so every kind of experience can
|
|
88
|
+
// be cited the same way: "m:<event seq>", "t:<thought id>", "d:<day>",
|
|
89
|
+
// "f:<decision id>", "r:<passage id>", "a:<anticipation id>",
|
|
90
|
+
// "g:<meeting id>" for what a meeting already meant to her,
|
|
91
|
+
// "s:<self thread>" for something she is living for.
|
|
92
|
+
export function evidence(list) {
|
|
93
|
+
const out = [];
|
|
94
|
+
for (const item of Array.isArray(list) ? list : []) {
|
|
95
|
+
if (Number.isSafeInteger(item) && item > 0) out.push(`m:${item}`);
|
|
96
|
+
else if (
|
|
97
|
+
typeof item === "string" &&
|
|
98
|
+
/^[mtdfrasgxoe]:[\w:.-]{1,80}$/.test(item)
|
|
99
|
+
)
|
|
100
|
+
out.push(item);
|
|
101
|
+
else if (/^\d+$/.test(String(item))) out.push(`m:${item}`);
|
|
102
|
+
}
|
|
103
|
+
return [...new Set(out)].slice(0, 24);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function messageSeqs(sources) {
|
|
107
|
+
return evidence(sources)
|
|
108
|
+
.filter((s) => s.startsWith("m:"))
|
|
109
|
+
.map((s) => Number(s.slice(2)));
|
|
110
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { summarizeGroupStyle } from "../core/group-style.js";
|
|
2
|
+
import { relevantContext, boundMindContext } from "./context-selection.js";
|
|
3
|
+
import { interestTerms } from "./attention.js";
|
|
4
|
+
import { agoLabel, elapsedLabel } from "./clock.js";
|
|
5
|
+
import { describeFace } from "./faces.js";
|
|
6
|
+
import { SELF_KINDS } from "./self.js";
|
|
7
|
+
import { DAY, text } from "./util.js";
|
|
8
|
+
|
|
9
|
+
const FORGOTTEN = "(很久没想起了)";
|
|
10
|
+
|
|
11
|
+
// One person's own words. Two people cannot be added together, and a line
|
|
12
|
+
// she already said does not count. A bare string is one utterance.
|
|
13
|
+
function speakerCues(cue) {
|
|
14
|
+
const groups = new Map();
|
|
15
|
+
let anon = 0;
|
|
16
|
+
for (const item of cue || []) {
|
|
17
|
+
if (item && typeof item === "object") {
|
|
18
|
+
if (item.role === "assistant") continue;
|
|
19
|
+
const id = String(item.userId || item.speaker || "");
|
|
20
|
+
if (!id) continue;
|
|
21
|
+
const texts = groups.get(id) || [];
|
|
22
|
+
texts.push(String(item.text || ""));
|
|
23
|
+
groups.set(id, texts);
|
|
24
|
+
} else groups.set(`\0${anon++}`, [String(item ?? "")]);
|
|
25
|
+
}
|
|
26
|
+
return [...groups.values()]
|
|
27
|
+
.map((texts) => interestTerms(texts))
|
|
28
|
+
.filter((terms) => terms.size);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const FEEDBACK = {
|
|
32
|
+
too_long: "太长了",
|
|
33
|
+
too_formal: "太端着",
|
|
34
|
+
too_meme: "梗太多",
|
|
35
|
+
natural: "挺自然",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// The way a room talks is read as of `now`, not as of the wall clock: a replay
|
|
39
|
+
// or a test that moves the clock must see the room as it was at that moment.
|
|
40
|
+
function groupStyle(mind, session, now) {
|
|
41
|
+
try {
|
|
42
|
+
const rows = mind.db
|
|
43
|
+
.prepare(
|
|
44
|
+
"SELECT * FROM (SELECT * FROM messages WHERE session_id=? AND is_demo=0 AND time<=? ORDER BY id DESC LIMIT 200) ORDER BY id",
|
|
45
|
+
)
|
|
46
|
+
.all(session, now + 60000);
|
|
47
|
+
const ids = rows.map((row) => row.event_id).filter(Boolean);
|
|
48
|
+
const away = ids.length
|
|
49
|
+
? new Set(
|
|
50
|
+
mind.db
|
|
51
|
+
.prepare(
|
|
52
|
+
`SELECT e.event_id FROM core_events e JOIN mind_unlived u ON u.seq=e.seq WHERE e.event_id IN (${ids.map(() => "?").join(",")})`,
|
|
53
|
+
)
|
|
54
|
+
.all(...ids)
|
|
55
|
+
.map((row) => row.event_id),
|
|
56
|
+
)
|
|
57
|
+
: new Set();
|
|
58
|
+
const profile = summarizeGroupStyle(
|
|
59
|
+
away.size ? rows.filter((row) => !away.has(row.event_id)) : rows,
|
|
60
|
+
now,
|
|
61
|
+
);
|
|
62
|
+
return profile.ready ? profile.summary : "";
|
|
63
|
+
} catch {
|
|
64
|
+
return "";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function feedback(mind, session, now) {
|
|
69
|
+
return mind.db
|
|
70
|
+
.prepare(
|
|
71
|
+
"SELECT f.tag,d.reply FROM reply_feedback f JOIN decisions d ON d.id=f.decision_id WHERE d.session_id=? AND d.is_demo=0 AND f.time>? AND f.time<=? ORDER BY f.time DESC LIMIT 2",
|
|
72
|
+
)
|
|
73
|
+
.all(session, now - 3 * DAY, now)
|
|
74
|
+
.filter((row) => FEEDBACK[row.tag] && row.tag !== "natural")
|
|
75
|
+
.map(
|
|
76
|
+
(row) => `有人觉得我说的「${text(row.reply, 30)}」${FEEDBACK[row.tag]}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Who she is right now, in a few hundred tokens. `self` changes a few times a
|
|
81
|
+
// day and sits in the cached head of the prompt; `inner` changes every turn,
|
|
82
|
+
// so anything this batch of messages brings back to mind belongs there.
|
|
83
|
+
export function innerView(
|
|
84
|
+
mind,
|
|
85
|
+
{ session, kind = "group", people = [], cue = [], now = Date.now() },
|
|
86
|
+
) {
|
|
87
|
+
const nature = mind.nature.current(now);
|
|
88
|
+
const affect = mind.affect.state(now, { nature, room: session });
|
|
89
|
+
const face = mind.faces.current(session, now);
|
|
90
|
+
const living = mind.self.living({ before: now, now, room: session });
|
|
91
|
+
const cues = speakerCues(cue);
|
|
92
|
+
const carries = (thread) => mind.meetings.stays(thread, session, now);
|
|
93
|
+
const visibleThreads = relevantContext(
|
|
94
|
+
mind.self
|
|
95
|
+
.annotated({ before: now, now })
|
|
96
|
+
.filter((t) => !t.faded && carries(t)),
|
|
97
|
+
{ cues, limit: 6, pinned: (t) => t.core || t.thread === living?.thread },
|
|
98
|
+
);
|
|
99
|
+
const shownLiving = living && carries(living) ? living : null;
|
|
100
|
+
const reminded = [
|
|
101
|
+
...mind.self
|
|
102
|
+
.reminded({ before: now, now, cues, limit: 2 })
|
|
103
|
+
.filter(carries)
|
|
104
|
+
.map((t) => `${SELF_KINDS[t.kind]}:${text(t.content, 80)}${FORGOTTEN}`),
|
|
105
|
+
...mind.thoughts
|
|
106
|
+
.reminded({ now, cues, session, limit: 1 })
|
|
107
|
+
.filter((t) => mind.meetings.sayable(t.content, session))
|
|
108
|
+
.map(
|
|
109
|
+
(t) =>
|
|
110
|
+
`${elapsedLabel(t.created, now, mind.timeZone())}想到:${text(t.content, 110)}${FORGOTTEN}`,
|
|
111
|
+
),
|
|
112
|
+
...mind.meetings
|
|
113
|
+
.reminded({ session, now, cues, limit: 1 })
|
|
114
|
+
.filter((line) => mind.meetings.sayable(line, session))
|
|
115
|
+
.map((line) => `${line}${FORGOTTEN}`),
|
|
116
|
+
];
|
|
117
|
+
const diary = mind.db
|
|
118
|
+
.prepare(
|
|
119
|
+
"SELECT day,content FROM mind_diary WHERE created<=? ORDER BY created DESC LIMIT 1",
|
|
120
|
+
)
|
|
121
|
+
.get(now);
|
|
122
|
+
const diaryHere =
|
|
123
|
+
diary &&
|
|
124
|
+
!mind.meetings.privateBeyond(diary.day, session, now) &&
|
|
125
|
+
mind.meetings.sayable(diary.content, session)
|
|
126
|
+
? `${diary.day}:${text(diary.content, 110)}`
|
|
127
|
+
: "";
|
|
128
|
+
const read = mind.reading
|
|
129
|
+
.recent({ now, limit: 2 })
|
|
130
|
+
.filter((r) => r.created > now - 7 * DAY);
|
|
131
|
+
const room = kind === "group" ? groupStyle(mind, session, now) : "";
|
|
132
|
+
const spoken = (value) =>
|
|
133
|
+
value && mind.meetings.sayable(value, session) ? value : "";
|
|
134
|
+
const self = {
|
|
135
|
+
...(face
|
|
136
|
+
? {
|
|
137
|
+
here: describeFace({
|
|
138
|
+
...face,
|
|
139
|
+
role: spoken(face.role),
|
|
140
|
+
tone: spoken(face.tone),
|
|
141
|
+
aspiration: spoken(face.aspiration),
|
|
142
|
+
content: spoken(face.content),
|
|
143
|
+
}),
|
|
144
|
+
}
|
|
145
|
+
: {}),
|
|
146
|
+
...(visibleThreads.length
|
|
147
|
+
? {
|
|
148
|
+
threads: visibleThreads.map(
|
|
149
|
+
(t) =>
|
|
150
|
+
`${SELF_KINDS[t.kind]}:${text(t.content, 80)}${t.status === "emerging" ? "(刚开始有这种感觉)" : ""}`,
|
|
151
|
+
),
|
|
152
|
+
}
|
|
153
|
+
: {}),
|
|
154
|
+
...(shownLiving ? { livingFor: text(shownLiving.content, 80) } : {}),
|
|
155
|
+
...(diaryHere ? { lastDiary: diaryHere } : {}),
|
|
156
|
+
...(read.length
|
|
157
|
+
? {
|
|
158
|
+
readLately: read.map((r) => {
|
|
159
|
+
const note =
|
|
160
|
+
r.note && mind.meetings.sayable(r.note, session)
|
|
161
|
+
? text(r.note, 60)
|
|
162
|
+
: "";
|
|
163
|
+
return `读过《${r.title}》第 ${r.ordinal + 1} 段${note ? `:${note}` : ""}`;
|
|
164
|
+
}),
|
|
165
|
+
}
|
|
166
|
+
: {}),
|
|
167
|
+
};
|
|
168
|
+
if (!self.here) delete self.here;
|
|
169
|
+
const persons = [];
|
|
170
|
+
for (const id of [...new Set(people.map(String))].slice(0, 6)) {
|
|
171
|
+
const bond = mind.bonds.person(id, now, { room: session });
|
|
172
|
+
// A recorded feeling counts even before anyone has spoken. Merely being
|
|
173
|
+
// seen does not.
|
|
174
|
+
if (
|
|
175
|
+
!bond ||
|
|
176
|
+
(!bond.interactions &&
|
|
177
|
+
!bond.impression &&
|
|
178
|
+
!bond.lastChange &&
|
|
179
|
+
bond.tension < 0.15)
|
|
180
|
+
)
|
|
181
|
+
continue;
|
|
182
|
+
persons.push({
|
|
183
|
+
id,
|
|
184
|
+
name: bond.name,
|
|
185
|
+
feel: bond.feel,
|
|
186
|
+
...(bond.impression && mind.meetings.sayable(bond.impression, session)
|
|
187
|
+
? { impression: bond.impression }
|
|
188
|
+
: {}),
|
|
189
|
+
// Someone coming back after days away, or someone who has been around
|
|
190
|
+
// without talking with her for a while.
|
|
191
|
+
...(bond.awayDays >= 3
|
|
192
|
+
? { away: `上次见到是 ${agoLabel(now - bond.seenAt)}` }
|
|
193
|
+
: bond.absentDays >= 7
|
|
194
|
+
? { lastTalked: `上次说上话是 ${agoLabel(now - bond.lastTalkedAt)}` }
|
|
195
|
+
: {}),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
const group = kind === "group" ? mind.bonds.group(session, now) : null;
|
|
199
|
+
const thoughts = relevantContext(
|
|
200
|
+
mind.thoughts
|
|
201
|
+
.open({ now, limit: 400, session })
|
|
202
|
+
.filter((t) => mind.meetings.sayable(t.content, session)),
|
|
203
|
+
{ cues, limit: 3 },
|
|
204
|
+
);
|
|
205
|
+
const heard = feedback(mind, session, now);
|
|
206
|
+
const continuity = mind.continuity.recall({ session, people, cue, now });
|
|
207
|
+
const expecting = mind.anticipations
|
|
208
|
+
.upcoming({ now, people, session, limit: 3 })
|
|
209
|
+
.map((a) => a.text);
|
|
210
|
+
const withWhom = mind.meetings.recall({
|
|
211
|
+
session,
|
|
212
|
+
people,
|
|
213
|
+
cue,
|
|
214
|
+
now,
|
|
215
|
+
limit: 2,
|
|
216
|
+
});
|
|
217
|
+
const wills = mind.meetings.traces({ session, before: now, limit: 2 });
|
|
218
|
+
const will = wills
|
|
219
|
+
.map((row) =>
|
|
220
|
+
row.living ? row.text : `${text(row.content, 28)}:${row.text}`,
|
|
221
|
+
)
|
|
222
|
+
.join(";");
|
|
223
|
+
const stood = mind.meetings.stood({
|
|
224
|
+
session,
|
|
225
|
+
people,
|
|
226
|
+
cue,
|
|
227
|
+
now,
|
|
228
|
+
limit: 2,
|
|
229
|
+
});
|
|
230
|
+
const cause =
|
|
231
|
+
affect.cause && mind.meetings.sayable(affect.cause, session)
|
|
232
|
+
? text(affect.cause, 40)
|
|
233
|
+
: "";
|
|
234
|
+
const withLines = withWhom.filter((line) =>
|
|
235
|
+
mind.meetings.sayable(line, session),
|
|
236
|
+
);
|
|
237
|
+
const expectingLines = expecting.filter((line) =>
|
|
238
|
+
mind.meetings.sayable(line, session),
|
|
239
|
+
);
|
|
240
|
+
const senses = mind.senses?.lines({ kind, now }) || [];
|
|
241
|
+
const body = (mind.body?.({ kind, now }) || [])
|
|
242
|
+
.map((line) => text(line, 80))
|
|
243
|
+
.filter(Boolean)
|
|
244
|
+
.slice(0, 8);
|
|
245
|
+
return boundMindContext({
|
|
246
|
+
self,
|
|
247
|
+
affect: cause ? affect : { ...affect, cause: "" },
|
|
248
|
+
inner: {
|
|
249
|
+
currentLife: mind.time.view({ session, now, cue }),
|
|
250
|
+
state: `${affect.phaseLabel},精力${affect.energyLabel},心情${affect.mood}${cause ? `(${cause})` : ""}${affect.lately ? `,${affect.lately}` : ""}`,
|
|
251
|
+
...(persons.length ? { people: persons } : {}),
|
|
252
|
+
...(continuity ? { continuity } : {}),
|
|
253
|
+
...(group ? { thisGroup: group.feel } : {}),
|
|
254
|
+
...(thoughts.length
|
|
255
|
+
? {
|
|
256
|
+
onMind: thoughts.map(
|
|
257
|
+
(t) =>
|
|
258
|
+
`${elapsedLabel(t.created, now, mind.timeZone())}想到:${text(t.content, 110)}`,
|
|
259
|
+
),
|
|
260
|
+
}
|
|
261
|
+
: {}),
|
|
262
|
+
...(expectingLines.length ? { expecting: expectingLines } : {}),
|
|
263
|
+
...(withLines.length ? { with: withLines } : {}),
|
|
264
|
+
...(will ? { will } : {}),
|
|
265
|
+
...(stood.length ? { stood } : {}),
|
|
266
|
+
...(reminded.length ? { reminded } : {}),
|
|
267
|
+
...(room ? { room } : {}),
|
|
268
|
+
...(heard.length ? { heard } : {}),
|
|
269
|
+
...(senses.length ? { senses } : {}),
|
|
270
|
+
...(body.length ? { body } : {}),
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
}
|
package/server/paths.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
// Code and static assets belong to the package; all mutable state belongs to
|
|
7
|
+
// an instance. Neither is ever resolved against the caller's working directory.
|
|
8
|
+
export const PACKAGE_ROOT = fileURLToPath(new URL("../", import.meta.url));
|
|
9
|
+
export const SOURCE_CHECKOUT = existsSync(join(PACKAGE_ROOT, ".git"));
|
|
10
|
+
|
|
11
|
+
export function defaultHome({
|
|
12
|
+
platform = process.platform,
|
|
13
|
+
env = process.env,
|
|
14
|
+
userHome = homedir(),
|
|
15
|
+
} = {}) {
|
|
16
|
+
if (platform === "win32")
|
|
17
|
+
return join(
|
|
18
|
+
env.LOCALAPPDATA && isAbsolute(env.LOCALAPPDATA)
|
|
19
|
+
? env.LOCALAPPDATA
|
|
20
|
+
: join(userHome, "AppData", "Local"),
|
|
21
|
+
"LuckyTri",
|
|
22
|
+
);
|
|
23
|
+
if (platform === "darwin")
|
|
24
|
+
return join(userHome, "Library", "Application Support", "LuckyTri");
|
|
25
|
+
return join(
|
|
26
|
+
env.XDG_DATA_HOME && isAbsolute(env.XDG_DATA_HOME)
|
|
27
|
+
? env.XDG_DATA_HOME
|
|
28
|
+
: join(userHome, ".local", "share"),
|
|
29
|
+
"luckytri",
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function inside(file, directory) {
|
|
34
|
+
const part = relative(directory, file);
|
|
35
|
+
return (
|
|
36
|
+
!part ||
|
|
37
|
+
(part !== ".." && !part.startsWith(`..${sep}`) && !isAbsolute(part))
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Resolve through existing ancestors as well: a symlink must not route user
|
|
42
|
+
// data back into node_modules, where an npm update would delete it.
|
|
43
|
+
function canonical(file) {
|
|
44
|
+
if (existsSync(file)) return realpathSync(file);
|
|
45
|
+
const parent = dirname(file);
|
|
46
|
+
return parent === file
|
|
47
|
+
? file
|
|
48
|
+
: join(canonical(parent), relative(parent, file));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function runtimePaths(env = process.env) {
|
|
52
|
+
if (env.LUCKYTRI_HOME && !isAbsolute(env.LUCKYTRI_HOME))
|
|
53
|
+
throw Error(
|
|
54
|
+
"LUCKYTRI_HOME / --home 必须是绝对路径 (must be an absolute path)",
|
|
55
|
+
);
|
|
56
|
+
const home = canonical(
|
|
57
|
+
resolve(
|
|
58
|
+
env.LUCKYTRI_HOME ||
|
|
59
|
+
(SOURCE_CHECKOUT ? PACKAGE_ROOT : defaultHome({ env })),
|
|
60
|
+
),
|
|
61
|
+
);
|
|
62
|
+
const data = canonical(join(home, "data"));
|
|
63
|
+
const config = canonical(join(home, ".env"));
|
|
64
|
+
const log = canonical(join(data, "launcher.log"));
|
|
65
|
+
const knowledge = canonical(join(data, "knowledge"));
|
|
66
|
+
const plugins = canonical(join(data, "plugins"));
|
|
67
|
+
const database =
|
|
68
|
+
env.DB_PATH === ":memory:"
|
|
69
|
+
? ":memory:"
|
|
70
|
+
: canonical(resolve(home, env.DB_PATH || join("data", "friend.db")));
|
|
71
|
+
const backups = canonical(
|
|
72
|
+
resolve(home, env.BACKUP_DIR || join(dirname(database), "backups")),
|
|
73
|
+
);
|
|
74
|
+
if (!SOURCE_CHECKOUT) {
|
|
75
|
+
for (const file of [
|
|
76
|
+
home,
|
|
77
|
+
config,
|
|
78
|
+
data,
|
|
79
|
+
database,
|
|
80
|
+
backups,
|
|
81
|
+
log,
|
|
82
|
+
knowledge,
|
|
83
|
+
plugins,
|
|
84
|
+
])
|
|
85
|
+
if (file !== ":memory:" && inside(file, canonical(PACKAGE_ROOT)))
|
|
86
|
+
throw Error(
|
|
87
|
+
"用户数据不能存放在 npm 安装目录内 (user data cannot live inside the npm package)",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
package: PACKAGE_ROOT,
|
|
92
|
+
home,
|
|
93
|
+
config,
|
|
94
|
+
data,
|
|
95
|
+
database,
|
|
96
|
+
backups,
|
|
97
|
+
log,
|
|
98
|
+
knowledge,
|
|
99
|
+
plugins,
|
|
100
|
+
public: join(PACKAGE_ROOT, "public"),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { createReadStream, existsSync, statSync } from "node:fs";
|
|
2
|
+
import { join, normalize } from "node:path";
|
|
3
|
+
import { PERMISSIONS } from "./permissions.js";
|
|
4
|
+
|
|
5
|
+
const SANDBOX =
|
|
6
|
+
"sandbox allow-scripts allow-forms; default-src 'none'; img-src data: https:; style-src 'unsafe-inline' *; script-src 'unsafe-inline' *; connect-src 'none'";
|
|
7
|
+
|
|
8
|
+
function safeFile(root, requestPath) {
|
|
9
|
+
const relative = normalize(requestPath).replace(/^(\.\.[/\\])+/, "");
|
|
10
|
+
const file = join(root, relative);
|
|
11
|
+
if (!file.startsWith(root) || !existsSync(file) || !statSync(file).isFile())
|
|
12
|
+
return "";
|
|
13
|
+
return file;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function mountPlugins(app, plugins) {
|
|
17
|
+
if (!plugins) return;
|
|
18
|
+
app.get("/api/plugins", (req, res) => {
|
|
19
|
+
res.json({ plugins: plugins.list(), permissions: PERMISSIONS });
|
|
20
|
+
});
|
|
21
|
+
app.get("/api/plugins/market", async (req, res) => {
|
|
22
|
+
try {
|
|
23
|
+
res.json(await plugins.install.catalog());
|
|
24
|
+
} catch (error) {
|
|
25
|
+
res.status(400).json({ error: error.message });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
app.post("/api/plugins/market/stage", async (req, res) => {
|
|
29
|
+
try {
|
|
30
|
+
res.json(await plugins.install.stageRegistry(req.body));
|
|
31
|
+
} catch (error) {
|
|
32
|
+
res.status(400).json({ error: error.message });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
app.post("/api/plugins/import/url", async (req, res) => {
|
|
36
|
+
try {
|
|
37
|
+
res.json(await plugins.install.stageUrl(req.body));
|
|
38
|
+
} catch (error) {
|
|
39
|
+
res.status(400).json({ error: error.message });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
app.post("/api/plugins/import/github", async (req, res) => {
|
|
43
|
+
try {
|
|
44
|
+
res.json(await plugins.install.stageGithub(req.body.repository));
|
|
45
|
+
} catch (error) {
|
|
46
|
+
res.status(400).json({ error: error.message });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
app.post("/api/plugins/import/dir", (req, res) => {
|
|
50
|
+
try {
|
|
51
|
+
res.json(plugins.install.stageDir(req.body.dir));
|
|
52
|
+
} catch (error) {
|
|
53
|
+
res.status(400).json({ error: error.message });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
app.post("/api/plugins/import/commit", (req, res) => {
|
|
57
|
+
try {
|
|
58
|
+
res.json(plugins.install.commit(req.body.token));
|
|
59
|
+
} catch (error) {
|
|
60
|
+
res.status(400).json({ error: error.message });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
app.post("/api/plugins/sources", (req, res) => {
|
|
64
|
+
try {
|
|
65
|
+
plugins.install.saveRegistry(req.body.url);
|
|
66
|
+
res.json({ ok: true });
|
|
67
|
+
} catch (error) {
|
|
68
|
+
res.status(400).json({ error: error.message });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
app.delete("/api/plugins/sources/:source", (req, res) => {
|
|
72
|
+
plugins.install.removeRegistry(req.params.source);
|
|
73
|
+
res.json({ ok: true });
|
|
74
|
+
});
|
|
75
|
+
app.post("/api/plugins/:id/rollback", (req, res) => {
|
|
76
|
+
try {
|
|
77
|
+
res.json(plugins.install.rollback(req.params.id));
|
|
78
|
+
} catch (error) {
|
|
79
|
+
res.status(400).json({ error: error.message });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
app.delete("/api/plugins/:id", async (req, res) => {
|
|
83
|
+
try {
|
|
84
|
+
res.json(
|
|
85
|
+
await plugins.install.remove(req.params.id, { data: !!req.body?.data }),
|
|
86
|
+
);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
res.status(400).json({ error: error.message });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
app.post("/api/plugins/:id/grant", (req, res) => {
|
|
92
|
+
try {
|
|
93
|
+
res.json(plugins.grant(req.params.id, req.body.permissions || []));
|
|
94
|
+
} catch (error) {
|
|
95
|
+
res.status(400).json({ error: error.message });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
app.post("/api/plugins/:id/enable", async (req, res) => {
|
|
99
|
+
try {
|
|
100
|
+
res.json(await plugins.enable(req.params.id));
|
|
101
|
+
} catch (error) {
|
|
102
|
+
res.status(400).json({ error: error.message });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
app.post("/api/plugins/:id/disable", async (req, res) => {
|
|
106
|
+
try {
|
|
107
|
+
res.json(await plugins.disable(req.params.id));
|
|
108
|
+
} catch (error) {
|
|
109
|
+
res.status(400).json({ error: error.message });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
app.get("/api/plugins/:id/settings", (req, res) => {
|
|
113
|
+
try {
|
|
114
|
+
res.json(plugins.settings(req.params.id));
|
|
115
|
+
} catch (error) {
|
|
116
|
+
res.status(404).json({ error: error.message });
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
app.put("/api/plugins/:id/settings", (req, res) => {
|
|
120
|
+
try {
|
|
121
|
+
res.json(
|
|
122
|
+
plugins.saveSettings(req.params.id, req.body.values || req.body),
|
|
123
|
+
);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
res.status(400).json({ error: error.message });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
app.get("/api/plugins/:id/log", (req, res) => {
|
|
129
|
+
const rows = plugins.db
|
|
130
|
+
.prepare(
|
|
131
|
+
"SELECT id,time,kind,level,message FROM plugin_events WHERE plugin_id=? ORDER BY id DESC LIMIT 80",
|
|
132
|
+
)
|
|
133
|
+
.all(req.params.id);
|
|
134
|
+
res.json({ events: rows });
|
|
135
|
+
});
|
|
136
|
+
app.get("/api/plugins/actions/pending", (req, res) => {
|
|
137
|
+
res.json({ actions: plugins.chat.capabilities.pending() });
|
|
138
|
+
});
|
|
139
|
+
app.post("/api/plugins/actions/:action/approve", async (req, res) => {
|
|
140
|
+
try {
|
|
141
|
+
res.json(await plugins.chat.capabilities.approve(req.params.action));
|
|
142
|
+
} catch (error) {
|
|
143
|
+
res.status(400).json({ error: error.message });
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
app.post("/api/plugins/actions/:action/reject", (req, res) => {
|
|
147
|
+
try {
|
|
148
|
+
res.json(
|
|
149
|
+
plugins.chat.capabilities.reject(req.params.action, req.body.reason),
|
|
150
|
+
);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
res.status(400).json({ error: error.message });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
app.use("/api/plugins/:id/x", async (req, res) => {
|
|
156
|
+
try {
|
|
157
|
+
const path = req.path && req.path !== "/" ? req.path : "/";
|
|
158
|
+
const result = await plugins.handleHttp(
|
|
159
|
+
req.params.id,
|
|
160
|
+
req.method,
|
|
161
|
+
path,
|
|
162
|
+
req.body,
|
|
163
|
+
);
|
|
164
|
+
if (result == null)
|
|
165
|
+
return res.status(404).json({ error: "插件接口不存在" });
|
|
166
|
+
res.json(result && typeof result === "object" ? result : { result });
|
|
167
|
+
} catch (error) {
|
|
168
|
+
res.status(400).json({ error: error.message });
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
app.use("/hook/:id", async (req, res) => {
|
|
172
|
+
try {
|
|
173
|
+
const path = req.path && req.path !== "/" ? req.path : "/";
|
|
174
|
+
const route = plugins.routes.find(
|
|
175
|
+
(item) => item.id === req.params.id && item.open && item.path === path,
|
|
176
|
+
);
|
|
177
|
+
if (!route) return res.status(404).end();
|
|
178
|
+
const result = await plugins.handleHttp(
|
|
179
|
+
req.params.id,
|
|
180
|
+
req.method,
|
|
181
|
+
path,
|
|
182
|
+
req.body || {},
|
|
183
|
+
);
|
|
184
|
+
res.json(result && typeof result === "object" ? result : { result });
|
|
185
|
+
} catch {
|
|
186
|
+
res.status(400).end();
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
app.use("/plugin-ui/:id", (req, res) => {
|
|
190
|
+
const row = plugins.list().find((item) => item.id === req.params.id);
|
|
191
|
+
const page = row?.manifest.contributes?.page;
|
|
192
|
+
if (!row?.enabled || !page) return res.status(404).end();
|
|
193
|
+
const root = join(row.path, "ui");
|
|
194
|
+
const relative = req.path.replace(/^\//, "") || "index.html";
|
|
195
|
+
const file = safeFile(root, relative);
|
|
196
|
+
if (!file) return res.status(404).end();
|
|
197
|
+
if (file.endsWith(".html")) {
|
|
198
|
+
res.setHeader("Content-Security-Policy", SANDBOX);
|
|
199
|
+
res.setHeader("Cache-Control", "no-store");
|
|
200
|
+
}
|
|
201
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
202
|
+
createReadStream(file).pipe(res);
|
|
203
|
+
});
|
|
204
|
+
}
|