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,564 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
normalizeModels,
|
|
4
|
+
publicModel,
|
|
5
|
+
storedModels,
|
|
6
|
+
validateModel,
|
|
7
|
+
} from "./model-manager.js";
|
|
8
|
+
import { prompts, PROMPTS } from "./persona-manager.js";
|
|
9
|
+
import { publicSession, parseSessionKey } from "../channels/session-key.js";
|
|
10
|
+
import { qqbotCredentials } from "../channels/qqbot/credentials.js";
|
|
11
|
+
import { wrap } from "../http.js";
|
|
12
|
+
import {
|
|
13
|
+
parseNewSession,
|
|
14
|
+
renameSession,
|
|
15
|
+
setSessionEnabled,
|
|
16
|
+
upsertSession,
|
|
17
|
+
} from "./sessions.js";
|
|
18
|
+
import {
|
|
19
|
+
applySpeakerNames,
|
|
20
|
+
invalidateSpeakerNames,
|
|
21
|
+
speakerNames,
|
|
22
|
+
} from "./speaker-names.js";
|
|
23
|
+
import { displayNames, displaySession } from "../studio/display-names.js";
|
|
24
|
+
|
|
25
|
+
export function mountCore(app, system) {
|
|
26
|
+
const { repo } = system;
|
|
27
|
+
app.get("/api/core/health", (req, res) => {
|
|
28
|
+
const row = repo.db
|
|
29
|
+
.prepare(
|
|
30
|
+
"SELECT time,status,json_extract(data,'$.error') error FROM core_traces WHERE mode='live' AND status!='running' AND json_array_length(data,'$.calls')>0 ORDER BY rowid DESC LIMIT 1",
|
|
31
|
+
)
|
|
32
|
+
.get();
|
|
33
|
+
res.json({
|
|
34
|
+
modelError: row?.status === "error" ? row.error : null,
|
|
35
|
+
time: row?.time || null,
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
app.get("/api/core/usage", (req, res) => {
|
|
39
|
+
const row = repo.db
|
|
40
|
+
.prepare(
|
|
41
|
+
`SELECT COUNT(*) calls,
|
|
42
|
+
COALESCE(SUM(input),0) input,
|
|
43
|
+
COALESCE(SUM(cached),0) cached,
|
|
44
|
+
COALESCE(SUM(output),0) output,
|
|
45
|
+
COALESCE(SUM(estimated),0) estimatedCalls,
|
|
46
|
+
MIN(time) since,
|
|
47
|
+
COALESCE(SUM(CASE WHEN estimated=0 THEN input+output ELSE 0 END),0) reportedTokens,
|
|
48
|
+
COALESCE(SUM(CASE WHEN estimated>0 THEN input+output ELSE 0 END),0) estimatedTokens
|
|
49
|
+
FROM mind_usage`,
|
|
50
|
+
)
|
|
51
|
+
.get();
|
|
52
|
+
const input = Number(row.input) || 0;
|
|
53
|
+
const output = Number(row.output) || 0;
|
|
54
|
+
res.json({
|
|
55
|
+
total: input + output,
|
|
56
|
+
input,
|
|
57
|
+
output,
|
|
58
|
+
cached: Number(row.cached) || 0,
|
|
59
|
+
calls: Number(row.calls) || 0,
|
|
60
|
+
estimatedCalls: Number(row.estimatedCalls) || 0,
|
|
61
|
+
reportedTokens: Number(row.reportedTokens) || 0,
|
|
62
|
+
estimatedTokens: Number(row.estimatedTokens) || 0,
|
|
63
|
+
since: row.since == null ? null : Number(row.since),
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
app.get("/api/core/state", (req, res) => {
|
|
67
|
+
const names = displayNames(repo.db, system.now());
|
|
68
|
+
res.json({
|
|
69
|
+
models: normalizeModels(storedModels(repo)).map(publicModel),
|
|
70
|
+
persona: system.mind.nature.current(),
|
|
71
|
+
prompts: prompts(repo),
|
|
72
|
+
sessions: repo.db
|
|
73
|
+
.prepare("SELECT * FROM sessions")
|
|
74
|
+
.all()
|
|
75
|
+
.map((s) => ({
|
|
76
|
+
...displaySession(publicSession(s), names),
|
|
77
|
+
policy: system.policy(s.id),
|
|
78
|
+
})),
|
|
79
|
+
revision: repo.store.revision,
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
app.post(
|
|
83
|
+
"/api/core/sessions",
|
|
84
|
+
wrap((req, res) => {
|
|
85
|
+
const parsed = parseNewSession(req.body || {}, {
|
|
86
|
+
qqbotAppId: qqbotCredentials(repo.store).appId,
|
|
87
|
+
});
|
|
88
|
+
upsertSession(repo.db, parsed);
|
|
89
|
+
repo.store.revision++;
|
|
90
|
+
res.json({ ok: true, sessionId: parsed.sessionId });
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
app.patch(
|
|
94
|
+
"/api/core/sessions/:id/archive",
|
|
95
|
+
wrap((req, res) => {
|
|
96
|
+
const row = repo.db
|
|
97
|
+
.prepare("SELECT id FROM sessions WHERE id=?")
|
|
98
|
+
.get(req.params.id);
|
|
99
|
+
if (!row) throw Error("会话不存在");
|
|
100
|
+
if (typeof req.body.archived !== "boolean") throw Error("归档开关无效");
|
|
101
|
+
repo.db
|
|
102
|
+
// `archived` already makes a session ineligible for processing. Keep
|
|
103
|
+
// its participation switch untouched so restoring it returns to the
|
|
104
|
+
// state the user configured before moving it out of the panel.
|
|
105
|
+
.prepare("UPDATE sessions SET archived=? WHERE id=?")
|
|
106
|
+
.run(+req.body.archived, req.params.id);
|
|
107
|
+
repo.store.revision++;
|
|
108
|
+
res.json({ ok: true });
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
app.patch(
|
|
112
|
+
"/api/core/sessions/:id",
|
|
113
|
+
wrap((req, res) => {
|
|
114
|
+
const { enabled, name } = req.body || {};
|
|
115
|
+
if (enabled === undefined && name === undefined)
|
|
116
|
+
throw Error("没有要修改的内容");
|
|
117
|
+
// The official bot has no way to look up a group's name, so naming a
|
|
118
|
+
// room is something the person does, here, for either channel.
|
|
119
|
+
if (name !== undefined) renameSession(repo.db, req.params.id, name);
|
|
120
|
+
if (enabled !== undefined)
|
|
121
|
+
setSessionEnabled(repo.db, req.params.id, enabled);
|
|
122
|
+
repo.store.revision++;
|
|
123
|
+
res.json({ ok: true });
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
app.delete(
|
|
127
|
+
"/api/core/sessions/:id",
|
|
128
|
+
wrap((req, res) => {
|
|
129
|
+
const id = String(req.params.id || ""),
|
|
130
|
+
row = repo.db
|
|
131
|
+
.prepare("SELECT id,archived FROM sessions WHERE id=?")
|
|
132
|
+
.get(id);
|
|
133
|
+
if (!row) throw Error("会话不存在");
|
|
134
|
+
if (!row.archived) throw Error("请先移出面板,再永久删除会话");
|
|
135
|
+
system.cancelSession(id);
|
|
136
|
+
repo.db.exec("BEGIN IMMEDIATE");
|
|
137
|
+
try {
|
|
138
|
+
const memoryIds = repo.db
|
|
139
|
+
.prepare("SELECT id FROM core_memories WHERE session_id=?")
|
|
140
|
+
.all(id)
|
|
141
|
+
.map((r) => r.id);
|
|
142
|
+
if (memoryIds.length) {
|
|
143
|
+
const placeholders = memoryIds.map(() => "?").join(",");
|
|
144
|
+
repo.db
|
|
145
|
+
.prepare(
|
|
146
|
+
`DELETE FROM core_memory_versions WHERE memory_id IN (${placeholders})`,
|
|
147
|
+
)
|
|
148
|
+
.run(...memoryIds);
|
|
149
|
+
}
|
|
150
|
+
repo.db
|
|
151
|
+
.prepare(
|
|
152
|
+
"DELETE FROM reply_feedback WHERE decision_id IN (SELECT id FROM decisions WHERE session_id=?)",
|
|
153
|
+
)
|
|
154
|
+
.run(id);
|
|
155
|
+
// What she lived through only in this conversation leaves with it;
|
|
156
|
+
// thoughts that also belong to other places stay.
|
|
157
|
+
repo.db
|
|
158
|
+
.prepare(
|
|
159
|
+
"DELETE FROM mind_thoughts WHERE sessions=? OR outreach_session=?",
|
|
160
|
+
)
|
|
161
|
+
.run(JSON.stringify([id]), id);
|
|
162
|
+
repo.db
|
|
163
|
+
.prepare(
|
|
164
|
+
"DELETE FROM mind_bond_events WHERE session_id=? OR (subject_kind='group' AND subject_id=?)",
|
|
165
|
+
)
|
|
166
|
+
.run(id, id);
|
|
167
|
+
for (const table of [
|
|
168
|
+
"mind_attention",
|
|
169
|
+
"mind_choices",
|
|
170
|
+
"mind_faces",
|
|
171
|
+
"mind_affect",
|
|
172
|
+
"mind_self",
|
|
173
|
+
"core_topics",
|
|
174
|
+
"core_memories",
|
|
175
|
+
"core_stages",
|
|
176
|
+
"core_context_summaries",
|
|
177
|
+
"core_cursors",
|
|
178
|
+
"core_jobs",
|
|
179
|
+
"core_outbox",
|
|
180
|
+
"core_references",
|
|
181
|
+
"core_traces",
|
|
182
|
+
"core_events",
|
|
183
|
+
"decisions",
|
|
184
|
+
"send_attempts",
|
|
185
|
+
"messages",
|
|
186
|
+
])
|
|
187
|
+
repo.db.prepare(`DELETE FROM ${table} WHERE session_id=?`).run(id);
|
|
188
|
+
repo.db.prepare("DELETE FROM memory_candidates WHERE scope=?").run(id);
|
|
189
|
+
repo.db
|
|
190
|
+
.prepare("DELETE FROM core_config WHERE id=?")
|
|
191
|
+
.run("session:" + id);
|
|
192
|
+
repo.db.prepare("DELETE FROM sessions WHERE id=?").run(id);
|
|
193
|
+
repo.db.exec("COMMIT");
|
|
194
|
+
} catch (error) {
|
|
195
|
+
repo.db.exec("ROLLBACK");
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
invalidateSpeakerNames(repo.db, id);
|
|
199
|
+
repo.store.revision++;
|
|
200
|
+
res.json({ ok: true, deleted: id });
|
|
201
|
+
}),
|
|
202
|
+
);
|
|
203
|
+
app.delete(
|
|
204
|
+
"/api/core/sessions/:id/context",
|
|
205
|
+
wrap((req, res) => {
|
|
206
|
+
const removed = system.clearContext(req.params.id);
|
|
207
|
+
res.json({ ok: true, removed });
|
|
208
|
+
}),
|
|
209
|
+
);
|
|
210
|
+
app.get(
|
|
211
|
+
"/api/core/sessions/:id/summaries",
|
|
212
|
+
wrap((req, res) => {
|
|
213
|
+
const id = String(req.params.id || "");
|
|
214
|
+
if (!repo.db.prepare("SELECT id FROM sessions WHERE id=?").get(id))
|
|
215
|
+
throw Error("会话不存在");
|
|
216
|
+
res.json(
|
|
217
|
+
system.compactor.list(id, { timeZone: system.policy(id).timeZone }),
|
|
218
|
+
);
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
app.put(
|
|
222
|
+
"/api/core/models",
|
|
223
|
+
wrap((req, res) => {
|
|
224
|
+
if (!Array.isArray(req.body.models) || req.body.models.length > 30)
|
|
225
|
+
throw Error("模型档案数量应为 0–30");
|
|
226
|
+
const old = storedModels(repo);
|
|
227
|
+
const profiles = normalizeModels(
|
|
228
|
+
req.body.models.map((m) =>
|
|
229
|
+
validateModel({
|
|
230
|
+
...m,
|
|
231
|
+
isDefault: !!m.isDefault,
|
|
232
|
+
apiKey: m.apiKey || old.find((x) => x.id === m.id)?.apiKey || "",
|
|
233
|
+
}),
|
|
234
|
+
),
|
|
235
|
+
);
|
|
236
|
+
if (new Set(profiles.map((m) => m.id)).size !== profiles.length)
|
|
237
|
+
throw Error("模型 ID 重复");
|
|
238
|
+
repo.saveConfig("models", profiles);
|
|
239
|
+
res.json({ ok: true });
|
|
240
|
+
}),
|
|
241
|
+
);
|
|
242
|
+
app.delete(
|
|
243
|
+
"/api/core/models/:id",
|
|
244
|
+
wrap((req, res) => {
|
|
245
|
+
const id = String(req.params.id || "");
|
|
246
|
+
if (!id) throw Error("模型档案不存在");
|
|
247
|
+
const old = storedModels(repo);
|
|
248
|
+
if (!old.some((m) => m.id === id)) throw Error("模型档案不存在");
|
|
249
|
+
const profiles = normalizeModels(old.filter((m) => m.id !== id));
|
|
250
|
+
const fallback = profiles.find((m) => m.isDefault)?.id || "";
|
|
251
|
+
const sessionConfigs = repo.db
|
|
252
|
+
.prepare("SELECT id,value FROM core_config WHERE id LIKE 'session:%'")
|
|
253
|
+
.all()
|
|
254
|
+
.map((row) => ({ id: row.id, value: JSON.parse(row.value) }))
|
|
255
|
+
.filter(
|
|
256
|
+
({ value }) =>
|
|
257
|
+
value.modelId === id ||
|
|
258
|
+
value.visionModelId === id ||
|
|
259
|
+
value.fallbackModelId === id,
|
|
260
|
+
);
|
|
261
|
+
repo.db.exec("BEGIN IMMEDIATE");
|
|
262
|
+
try {
|
|
263
|
+
repo.db
|
|
264
|
+
.prepare(
|
|
265
|
+
"UPDATE core_config SET value=?,version=version+1 WHERE id='models'",
|
|
266
|
+
)
|
|
267
|
+
.run(JSON.stringify(profiles));
|
|
268
|
+
for (const row of sessionConfigs) {
|
|
269
|
+
const next = { ...row.value };
|
|
270
|
+
if (next.modelId === id) next.modelId = fallback;
|
|
271
|
+
if (next.visionModelId === id) delete next.visionModelId;
|
|
272
|
+
if (
|
|
273
|
+
next.fallbackModelId === id ||
|
|
274
|
+
next.fallbackModelId === next.modelId
|
|
275
|
+
)
|
|
276
|
+
delete next.fallbackModelId;
|
|
277
|
+
repo.db
|
|
278
|
+
.prepare(
|
|
279
|
+
"UPDATE core_config SET value=?,version=version+1 WHERE id=?",
|
|
280
|
+
)
|
|
281
|
+
.run(JSON.stringify(next), row.id);
|
|
282
|
+
}
|
|
283
|
+
repo.db.exec("COMMIT");
|
|
284
|
+
} catch (error) {
|
|
285
|
+
repo.db.exec("ROLLBACK");
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
288
|
+
repo.store.revision++;
|
|
289
|
+
res.json({
|
|
290
|
+
ok: true,
|
|
291
|
+
deleted: id,
|
|
292
|
+
resetSessions: sessionConfigs.map(({ id: session }) =>
|
|
293
|
+
session.slice(8),
|
|
294
|
+
),
|
|
295
|
+
});
|
|
296
|
+
}),
|
|
297
|
+
);
|
|
298
|
+
// Kept for scripts written against the old persona API: it records a new
|
|
299
|
+
// version of her nature.
|
|
300
|
+
app.put(
|
|
301
|
+
"/api/core/persona",
|
|
302
|
+
wrap((req, res) => {
|
|
303
|
+
const { version: _version, mood: _mood, ...p } = req.body || {};
|
|
304
|
+
system.mind.nature.save(
|
|
305
|
+
{ ...system.mind.nature.current(), ...p },
|
|
306
|
+
"通过人格接口修改",
|
|
307
|
+
);
|
|
308
|
+
res.json({ ok: true });
|
|
309
|
+
}),
|
|
310
|
+
);
|
|
311
|
+
app.put(
|
|
312
|
+
"/api/core/prompts",
|
|
313
|
+
wrap((req, res) => {
|
|
314
|
+
const next = {};
|
|
315
|
+
for (const k of Object.keys(PROMPTS)) {
|
|
316
|
+
if (
|
|
317
|
+
typeof req.body[k] !== "string" ||
|
|
318
|
+
!req.body[k].trim() ||
|
|
319
|
+
req.body[k].length > 50000
|
|
320
|
+
)
|
|
321
|
+
throw Error(`${k} Prompt 无效`);
|
|
322
|
+
next[k] = req.body[k];
|
|
323
|
+
}
|
|
324
|
+
repo.saveConfig("prompts-history:" + Date.now(), prompts(repo));
|
|
325
|
+
repo.saveConfig("prompts", next);
|
|
326
|
+
res.json({ ok: true });
|
|
327
|
+
}),
|
|
328
|
+
);
|
|
329
|
+
app.get("/api/core/prompt-versions", (req, res) =>
|
|
330
|
+
res.json(
|
|
331
|
+
repo.db
|
|
332
|
+
.prepare(
|
|
333
|
+
"SELECT id,version FROM core_config WHERE id LIKE 'prompts-history:%' ORDER BY id DESC LIMIT 100",
|
|
334
|
+
)
|
|
335
|
+
.all(),
|
|
336
|
+
),
|
|
337
|
+
);
|
|
338
|
+
app.post(
|
|
339
|
+
"/api/core/prompt-versions/:id/restore",
|
|
340
|
+
wrap((req, res) => {
|
|
341
|
+
if (!req.params.id.startsWith("prompts-history:"))
|
|
342
|
+
throw Error("版本无效");
|
|
343
|
+
const v = repo.config(req.params.id);
|
|
344
|
+
if (!v) throw Error("版本不存在");
|
|
345
|
+
repo.saveConfig("prompts-history:" + Date.now(), prompts(repo));
|
|
346
|
+
repo.saveConfig("prompts", v);
|
|
347
|
+
res.json({ ok: true });
|
|
348
|
+
}),
|
|
349
|
+
);
|
|
350
|
+
app.put(
|
|
351
|
+
"/api/core/sessions/:id",
|
|
352
|
+
wrap((req, res) => {
|
|
353
|
+
if (
|
|
354
|
+
!repo.db
|
|
355
|
+
.prepare("SELECT id FROM sessions WHERE id=?")
|
|
356
|
+
.get(req.params.id)
|
|
357
|
+
)
|
|
358
|
+
throw Error("会话不存在");
|
|
359
|
+
const p = req.body;
|
|
360
|
+
for (const [k, min, max] of [
|
|
361
|
+
["aggregateMs", 0, 30000],
|
|
362
|
+
["maxWaitMs", 0, 60000],
|
|
363
|
+
["contextMessages", 0, 500],
|
|
364
|
+
["maxReply", 1, 2000],
|
|
365
|
+
])
|
|
366
|
+
if (!Number.isInteger(p[k]) || p[k] < min || p[k] > max)
|
|
367
|
+
throw Error(`${k} 超出范围`);
|
|
368
|
+
// 0 is what older saves stored; it now means the default window.
|
|
369
|
+
if (p.contextMessages > 0 && p.contextMessages < 10)
|
|
370
|
+
throw Error("近期原文条数应在 10–500 之间");
|
|
371
|
+
if (p.compaction !== undefined && typeof p.compaction !== "boolean")
|
|
372
|
+
throw Error("上下文压缩开关无效");
|
|
373
|
+
if (p.maxWaitMs < p.aggregateMs)
|
|
374
|
+
throw Error("最大聚合时间不能小于基础窗口");
|
|
375
|
+
for (const retired of [
|
|
376
|
+
"probability",
|
|
377
|
+
"cooldown",
|
|
378
|
+
"comfortOnDistress",
|
|
379
|
+
"persona",
|
|
380
|
+
])
|
|
381
|
+
if (p[retired] !== undefined)
|
|
382
|
+
throw Error(
|
|
383
|
+
"开不开口由 TA 自己决定,会话里不再设置概率、冷却、主动安慰或群人格;TA 在各群的样子见「人际」",
|
|
384
|
+
);
|
|
385
|
+
if (
|
|
386
|
+
p.selectiveVision !== undefined &&
|
|
387
|
+
typeof p.selectiveVision !== "boolean"
|
|
388
|
+
)
|
|
389
|
+
throw Error("节能看图开关无效");
|
|
390
|
+
if (typeof p.memory !== "boolean" || typeof p.deepCheck !== "boolean")
|
|
391
|
+
throw Error("开关无效");
|
|
392
|
+
const primary = system.models.profile(p.modelId);
|
|
393
|
+
if (p.visionModelId && !system.models.profile(p.visionModelId).vision)
|
|
394
|
+
throw Error("视觉兼容模型必须开启视觉能力");
|
|
395
|
+
if (
|
|
396
|
+
p.fallbackModelId !== undefined &&
|
|
397
|
+
typeof p.fallbackModelId !== "string"
|
|
398
|
+
)
|
|
399
|
+
throw Error("备用模型无效");
|
|
400
|
+
if (p.fallbackModelId) {
|
|
401
|
+
if (p.fallbackModelId === "default")
|
|
402
|
+
throw Error("请从模型库中选择具体的备用模型");
|
|
403
|
+
if (system.models.profile(p.fallbackModelId).id === primary.id)
|
|
404
|
+
throw Error("备用模型不能和主模型相同");
|
|
405
|
+
} else delete p.fallbackModelId;
|
|
406
|
+
if (p.name !== undefined) renameSession(repo.db, req.params.id, p.name);
|
|
407
|
+
const {
|
|
408
|
+
topicBoost: _deprecatedTopicBoost,
|
|
409
|
+
name: _name,
|
|
410
|
+
enabled: _enabled,
|
|
411
|
+
archived: _archived,
|
|
412
|
+
...clean
|
|
413
|
+
} = p;
|
|
414
|
+
repo.saveConfig("session:" + req.params.id, clean);
|
|
415
|
+
res.json({ ok: true });
|
|
416
|
+
}),
|
|
417
|
+
);
|
|
418
|
+
app.get(
|
|
419
|
+
"/api/core/events",
|
|
420
|
+
wrap((req, res) => {
|
|
421
|
+
const before = Number(req.query.before) || Number.MAX_SAFE_INTEGER;
|
|
422
|
+
const session = String(req.query.session || "");
|
|
423
|
+
const after =
|
|
424
|
+
req.query.after === undefined ? null : Number(req.query.after);
|
|
425
|
+
const names = speakerNames(repo.db, [session]);
|
|
426
|
+
const labels = displayNames(repo.db, system.now());
|
|
427
|
+
const rows =
|
|
428
|
+
Number.isSafeInteger(after) && after >= 0
|
|
429
|
+
? repo.db
|
|
430
|
+
.prepare(
|
|
431
|
+
"SELECT seq,session_id,time,role,payload FROM core_events WHERE session_id=? AND seq>? ORDER BY seq ASC LIMIT 100",
|
|
432
|
+
)
|
|
433
|
+
.all(session, after)
|
|
434
|
+
: repo.db
|
|
435
|
+
.prepare(
|
|
436
|
+
"SELECT seq,session_id,time,role,payload FROM core_events WHERE session_id=? AND seq<? ORDER BY seq DESC LIMIT 100",
|
|
437
|
+
)
|
|
438
|
+
.all(session, before);
|
|
439
|
+
res.json(
|
|
440
|
+
rows.map((r) => {
|
|
441
|
+
const payload = JSON.parse(r.payload);
|
|
442
|
+
const label =
|
|
443
|
+
labels.get(String(payload.userId)) ||
|
|
444
|
+
names.get(String(payload.userId));
|
|
445
|
+
if (label) payload.name = label;
|
|
446
|
+
return { ...r, payload };
|
|
447
|
+
}),
|
|
448
|
+
);
|
|
449
|
+
}),
|
|
450
|
+
);
|
|
451
|
+
app.get("/api/core/traces", (req, res) => {
|
|
452
|
+
const mode = repo.store.settings().demo ? "demo" : "live";
|
|
453
|
+
const session = String(req.query.session || "");
|
|
454
|
+
const requestedLimit = Number(req.query.limit);
|
|
455
|
+
const limit = Number.isSafeInteger(requestedLimit)
|
|
456
|
+
? Math.max(1, Math.min(100, requestedLimit))
|
|
457
|
+
: 100;
|
|
458
|
+
const compact = req.query.compact === "1";
|
|
459
|
+
const fields = compact
|
|
460
|
+
? "id,session_id,time,mode,status,json_extract(data,'$.reason') reason"
|
|
461
|
+
: "id,session_id,time,mode,status,json_extract(data,'$.reason') reason,json_extract(data,'$.error') error";
|
|
462
|
+
const rows = session
|
|
463
|
+
? repo.db
|
|
464
|
+
.prepare(
|
|
465
|
+
`SELECT ${fields} FROM core_traces WHERE session_id=? AND mode IN (?,'memory','summary','replay') ORDER BY time DESC LIMIT ?`,
|
|
466
|
+
)
|
|
467
|
+
.all(session, mode, limit)
|
|
468
|
+
: repo.db
|
|
469
|
+
.prepare(
|
|
470
|
+
`SELECT ${fields} FROM core_traces WHERE mode IN (?,'memory','summary','replay') ORDER BY time DESC LIMIT ?`,
|
|
471
|
+
)
|
|
472
|
+
.all(mode, limit);
|
|
473
|
+
const names = speakerNames(
|
|
474
|
+
repo.db,
|
|
475
|
+
rows.map((row) => row.session_id),
|
|
476
|
+
);
|
|
477
|
+
res.json(
|
|
478
|
+
rows.map((row) => ({
|
|
479
|
+
...row,
|
|
480
|
+
reason: row.reason ? applySpeakerNames(row.reason, names) : row.reason,
|
|
481
|
+
})),
|
|
482
|
+
);
|
|
483
|
+
});
|
|
484
|
+
app.get(
|
|
485
|
+
"/api/core/traces/:id",
|
|
486
|
+
wrap((req, res) => {
|
|
487
|
+
const t = repo.db
|
|
488
|
+
.prepare("SELECT * FROM core_traces WHERE id=?")
|
|
489
|
+
.get(req.params.id);
|
|
490
|
+
if (!t) throw Error("日志不存在");
|
|
491
|
+
const data = JSON.parse(t.data);
|
|
492
|
+
const names = speakerNames(repo.db, [t.session_id]);
|
|
493
|
+
if (typeof data.reason === "string")
|
|
494
|
+
data.reason = applySpeakerNames(data.reason, names);
|
|
495
|
+
res.json({ ...t, data });
|
|
496
|
+
}),
|
|
497
|
+
);
|
|
498
|
+
let replaying = false;
|
|
499
|
+
app.post(
|
|
500
|
+
"/api/core/replay",
|
|
501
|
+
wrap(async (req, res) => {
|
|
502
|
+
if (replaying) throw Error("已有回放正在运行");
|
|
503
|
+
const { session, from, to } = req.body;
|
|
504
|
+
if (!Number.isSafeInteger(from) || !Number.isSafeInteger(to) || from > to)
|
|
505
|
+
throw Error("回放消息序号范围无效");
|
|
506
|
+
const rows = repo
|
|
507
|
+
.events(session, to, { simulated: false })
|
|
508
|
+
.filter((m) => m.seq >= from && m.role === "user");
|
|
509
|
+
if (!rows.length || rows.length > 100)
|
|
510
|
+
throw Error("请选择 1–100 条人类消息");
|
|
511
|
+
replaying = true;
|
|
512
|
+
try {
|
|
513
|
+
res.json(await system.process(session, rows, { replay: true }));
|
|
514
|
+
} finally {
|
|
515
|
+
replaying = false;
|
|
516
|
+
}
|
|
517
|
+
}),
|
|
518
|
+
);
|
|
519
|
+
app.post("/api/simulate", async (req, res) => {
|
|
520
|
+
if (!storeDemo(system))
|
|
521
|
+
return res.status(400).json({ error: "请先开启模拟模式" });
|
|
522
|
+
const { sessionId, userId, text, mentioned } = req.body;
|
|
523
|
+
if (
|
|
524
|
+
typeof text !== "string" ||
|
|
525
|
+
!text.trim() ||
|
|
526
|
+
text.length > 4000 ||
|
|
527
|
+
typeof userId !== "string" ||
|
|
528
|
+
!/^[0-9A-Za-z_-]{4,64}$/.test(userId) ||
|
|
529
|
+
typeof sessionId !== "string" ||
|
|
530
|
+
!repo.db.prepare("SELECT id FROM sessions WHERE id=?").get(sessionId)
|
|
531
|
+
)
|
|
532
|
+
return res
|
|
533
|
+
.status(400)
|
|
534
|
+
.json({ error: "请选择会话并输入消息和有效的用户 ID" });
|
|
535
|
+
const trace = await system.receive({
|
|
536
|
+
sessionId,
|
|
537
|
+
kind: (() => {
|
|
538
|
+
try {
|
|
539
|
+
return parseSessionKey(sessionId).kind;
|
|
540
|
+
} catch {
|
|
541
|
+
return sessionId.split(":")[0];
|
|
542
|
+
}
|
|
543
|
+
})(),
|
|
544
|
+
userId,
|
|
545
|
+
name: "体验用户",
|
|
546
|
+
text,
|
|
547
|
+
mentioned: !!mentioned,
|
|
548
|
+
eventId: randomUUID(),
|
|
549
|
+
simulated: true,
|
|
550
|
+
});
|
|
551
|
+
res.json({
|
|
552
|
+
speak: Array.isArray(trace.sent) && trace.sent.length > 0,
|
|
553
|
+
reply: Array.isArray(trace.sent) ? trace.sent.join("\n") : "",
|
|
554
|
+
reason: trace.reason || "",
|
|
555
|
+
choice: trace.decision?.choice || "silent",
|
|
556
|
+
emotion: trace.decision?.topic || "模拟",
|
|
557
|
+
status: trace.status,
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function storeDemo(system) {
|
|
563
|
+
return system.store.settings().demo;
|
|
564
|
+
}
|