@yeaft/webchat-agent 1.0.186 → 1.0.189
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/cli.js +12 -0
- package/index.js +10 -2
- package/local-run.js +218 -0
- package/local-runtime/server/.env.example +54 -0
- package/local-runtime/server/api.js +111 -0
- package/local-runtime/server/auth/aad.js +235 -0
- package/local-runtime/server/auth/login.js +156 -0
- package/local-runtime/server/auth/oauth-flow.js +277 -0
- package/local-runtime/server/auth/password-reset.js +134 -0
- package/local-runtime/server/auth/providers/alipay.js +125 -0
- package/local-runtime/server/auth/providers/github.js +82 -0
- package/local-runtime/server/auth/providers/google.js +60 -0
- package/local-runtime/server/auth/providers/microsoft.js +71 -0
- package/local-runtime/server/auth/providers/types.js +57 -0
- package/local-runtime/server/auth/providers/wechat.js +68 -0
- package/local-runtime/server/auth/register.js +91 -0
- package/local-runtime/server/auth/session-store.js +57 -0
- package/local-runtime/server/auth/token.js +85 -0
- package/local-runtime/server/auth/totp-auth.js +133 -0
- package/local-runtime/server/auth/utils.js +42 -0
- package/local-runtime/server/auth.js +8 -0
- package/local-runtime/server/check-node-version.js +74 -0
- package/local-runtime/server/config.js +298 -0
- package/local-runtime/server/context.js +140 -0
- package/local-runtime/server/create-user.js +59 -0
- package/local-runtime/server/database.js +12 -0
- package/local-runtime/server/db/connection.js +963 -0
- package/local-runtime/server/db/expert-db.js +171 -0
- package/local-runtime/server/db/identity-db.js +92 -0
- package/local-runtime/server/db/invitation-db.js +38 -0
- package/local-runtime/server/db/message-db.js +257 -0
- package/local-runtime/server/db/session-db.js +118 -0
- package/local-runtime/server/db/user-db.js +165 -0
- package/local-runtime/server/db/user-stats-db.js +185 -0
- package/local-runtime/server/db/yeaft-session-db.js +258 -0
- package/local-runtime/server/email.js +96 -0
- package/local-runtime/server/encryption.js +105 -0
- package/local-runtime/server/handlers/agent-conversation.js +347 -0
- package/local-runtime/server/handlers/agent-file-terminal.js +99 -0
- package/local-runtime/server/handlers/agent-output.js +854 -0
- package/local-runtime/server/handlers/agent-sync.js +399 -0
- package/local-runtime/server/handlers/agent-work-center.js +27 -0
- package/local-runtime/server/handlers/client-conversation.js +1182 -0
- package/local-runtime/server/handlers/client-misc.js +254 -0
- package/local-runtime/server/handlers/client-work-center.js +269 -0
- package/local-runtime/server/handlers/client-workbench.js +146 -0
- package/local-runtime/server/handlers/session-pin-router.js +61 -0
- package/local-runtime/server/heartbeat-policy.js +46 -0
- package/local-runtime/server/index.js +275 -0
- package/local-runtime/server/package.json +55 -0
- package/local-runtime/server/perf-trace.js +154 -0
- package/local-runtime/server/proxy.js +273 -0
- package/local-runtime/server/routes/admin-routes.js +207 -0
- package/local-runtime/server/routes/auth-routes.js +322 -0
- package/local-runtime/server/routes/expert-routes.js +117 -0
- package/local-runtime/server/routes/invitation-routes.js +60 -0
- package/local-runtime/server/routes/session-routes.js +112 -0
- package/local-runtime/server/routes/upload-routes.js +109 -0
- package/local-runtime/server/routes/user-routes.js +241 -0
- package/local-runtime/server/totp.js +74 -0
- package/local-runtime/server/work-item-attachment-policy.js +56 -0
- package/local-runtime/server/ws-agent.js +319 -0
- package/local-runtime/server/ws-client.js +214 -0
- package/local-runtime/server/ws-utils.js +394 -0
- package/local-runtime/server/yeaft-asset-store.js +339 -0
- package/local-runtime/version.json +1 -0
- package/local-runtime/web/app.bundle.js +7673 -0
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/assets/avatars/README.md +34 -0
- package/local-runtime/web/assets/avatars/ada.svg +1 -0
- package/local-runtime/web/assets/avatars/alan.svg +1 -0
- package/local-runtime/web/assets/avatars/alice.svg +1 -0
- package/local-runtime/web/assets/avatars/anders.svg +1 -0
- package/local-runtime/web/assets/avatars/bezos.svg +1 -0
- package/local-runtime/web/assets/avatars/borges.svg +1 -0
- package/local-runtime/web/assets/avatars/buffett.svg +1 -0
- package/local-runtime/web/assets/avatars/clausewitz.svg +1 -0
- package/local-runtime/web/assets/avatars/dalio.svg +1 -0
- package/local-runtime/web/assets/avatars/dieter.svg +1 -0
- package/local-runtime/web/assets/avatars/drucker.svg +1 -0
- package/local-runtime/web/assets/avatars/einstein.svg +1 -0
- package/local-runtime/web/assets/avatars/grace.svg +1 -0
- package/local-runtime/web/assets/avatars/harari.svg +1 -0
- package/local-runtime/web/assets/avatars/jung.svg +1 -0
- package/local-runtime/web/assets/avatars/kahneman.svg +1 -0
- package/local-runtime/web/assets/avatars/ken.svg +1 -0
- package/local-runtime/web/assets/avatars/kongzi.svg +1 -0
- package/local-runtime/web/assets/avatars/kubrick.svg +1 -0
- package/local-runtime/web/assets/avatars/linus.svg +1 -0
- package/local-runtime/web/assets/avatars/luxun.svg +1 -0
- package/local-runtime/web/assets/avatars/margaret.svg +1 -0
- package/local-runtime/web/assets/avatars/martin.svg +1 -0
- package/local-runtime/web/assets/avatars/miyazaki.svg +1 -0
- package/local-runtime/web/assets/avatars/munger.svg +1 -0
- package/local-runtime/web/assets/avatars/nietzsche.svg +1 -0
- package/local-runtime/web/assets/avatars/norman.svg +1 -0
- package/local-runtime/web/assets/avatars/shannon.svg +1 -0
- package/local-runtime/web/assets/avatars/simaqian.svg +1 -0
- package/local-runtime/web/assets/avatars/socrates.svg +1 -0
- package/local-runtime/web/assets/avatars/steve.svg +1 -0
- package/local-runtime/web/assets/avatars/sudongpo.svg +1 -0
- package/local-runtime/web/assets/avatars/sunzi.svg +1 -0
- package/local-runtime/web/docx-preview.min.js +2 -0
- package/local-runtime/web/docx-preview.min.js.gz +0 -0
- package/local-runtime/web/html-to-image.min.js +2 -0
- package/local-runtime/web/html-to-image.min.js.gz +0 -0
- package/local-runtime/web/index.html +21 -0
- package/local-runtime/web/jszip.min.js +13 -0
- package/local-runtime/web/jszip.min.js.gz +0 -0
- package/local-runtime/web/mermaid.min.js +2843 -0
- package/local-runtime/web/mermaid.min.js.gz +0 -0
- package/local-runtime/web/msal-browser.min.js +69 -0
- package/local-runtime/web/msal-browser.min.js.gz +0 -0
- package/local-runtime/web/style.bundle.css +10 -0
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/local-runtime/web/vendor.bundle.js +1522 -0
- package/local-runtime/web/vendor.bundle.js.gz +0 -0
- package/local-runtime/web/xlsx.min.js +24 -0
- package/local-runtime/web/xlsx.min.js.gz +0 -0
- package/package.json +13 -3
- package/scripts/prepare-local-runtime.js +25 -0
|
@@ -0,0 +1,963 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { randomBytes, randomUUID } from 'crypto';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
|
|
9
|
+
// 数据库文件位置
|
|
10
|
+
const DATA_DIR = process.env.SERVER_DATA_DIR || process.env.TEST_DB_DIR || join(__dirname, '../../data');
|
|
11
|
+
const DB_PATH = process.env.SERVER_DATA_DIR
|
|
12
|
+
? join(DATA_DIR, 'webchat.db')
|
|
13
|
+
: (process.env.TEST_DB_PATH || join(DATA_DIR, 'webchat.db'));
|
|
14
|
+
|
|
15
|
+
// 确保数据目录存在
|
|
16
|
+
if (!existsSync(DATA_DIR)) {
|
|
17
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 创建数据库连接
|
|
21
|
+
const db = new DatabaseSync(DB_PATH);
|
|
22
|
+
|
|
23
|
+
// 启用 WAL 模式提高性能
|
|
24
|
+
db.exec('PRAGMA journal_mode = WAL');
|
|
25
|
+
db.exec('PRAGMA foreign_keys = ON');
|
|
26
|
+
|
|
27
|
+
// 初始化表结构(不包含索引,索引在迁移后创建)
|
|
28
|
+
db.exec(`
|
|
29
|
+
-- 用户表
|
|
30
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
31
|
+
id TEXT PRIMARY KEY,
|
|
32
|
+
username TEXT UNIQUE NOT NULL,
|
|
33
|
+
display_name TEXT,
|
|
34
|
+
created_at INTEGER NOT NULL,
|
|
35
|
+
last_login_at INTEGER
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
-- 会话表
|
|
39
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
40
|
+
id TEXT PRIMARY KEY,
|
|
41
|
+
agent_id TEXT NOT NULL,
|
|
42
|
+
agent_name TEXT,
|
|
43
|
+
claude_session_id TEXT,
|
|
44
|
+
work_dir TEXT,
|
|
45
|
+
title TEXT,
|
|
46
|
+
created_at INTEGER NOT NULL,
|
|
47
|
+
updated_at INTEGER NOT NULL,
|
|
48
|
+
is_active INTEGER DEFAULT 1
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
-- 消息表
|
|
52
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
53
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
54
|
+
session_id TEXT NOT NULL,
|
|
55
|
+
role TEXT NOT NULL,
|
|
56
|
+
content TEXT NOT NULL,
|
|
57
|
+
message_type TEXT,
|
|
58
|
+
tool_name TEXT,
|
|
59
|
+
tool_input TEXT,
|
|
60
|
+
created_at INTEGER NOT NULL,
|
|
61
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
-- 邀请码表
|
|
65
|
+
CREATE TABLE IF NOT EXISTS invitations (
|
|
66
|
+
id TEXT PRIMARY KEY,
|
|
67
|
+
created_by TEXT NOT NULL,
|
|
68
|
+
created_at INTEGER NOT NULL,
|
|
69
|
+
used_by TEXT,
|
|
70
|
+
used_at INTEGER,
|
|
71
|
+
expires_at INTEGER NOT NULL,
|
|
72
|
+
role TEXT DEFAULT 'user',
|
|
73
|
+
FOREIGN KEY (created_by) REFERENCES users(id),
|
|
74
|
+
FOREIGN KEY (used_by) REFERENCES users(id)
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
-- 用户统计表
|
|
78
|
+
CREATE TABLE IF NOT EXISTS user_stats (
|
|
79
|
+
user_id TEXT PRIMARY KEY REFERENCES users(id),
|
|
80
|
+
message_count INTEGER DEFAULT 0,
|
|
81
|
+
session_count INTEGER DEFAULT 0,
|
|
82
|
+
request_count INTEGER DEFAULT 0,
|
|
83
|
+
bytes_sent INTEGER DEFAULT 0,
|
|
84
|
+
bytes_received INTEGER DEFAULT 0,
|
|
85
|
+
input_tokens INTEGER DEFAULT 0,
|
|
86
|
+
output_tokens INTEGER DEFAULT 0,
|
|
87
|
+
cache_read_tokens INTEGER DEFAULT 0,
|
|
88
|
+
cache_write_tokens INTEGER DEFAULT 0,
|
|
89
|
+
total_tokens INTEGER DEFAULT 0,
|
|
90
|
+
updated_at INTEGER NOT NULL DEFAULT 0
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
-- 每日统计表(按天聚合用户用量)
|
|
94
|
+
CREATE TABLE IF NOT EXISTS daily_stats (
|
|
95
|
+
user_id TEXT NOT NULL REFERENCES users(id),
|
|
96
|
+
date TEXT NOT NULL,
|
|
97
|
+
message_count INTEGER DEFAULT 0,
|
|
98
|
+
session_count INTEGER DEFAULT 0,
|
|
99
|
+
request_count INTEGER DEFAULT 0,
|
|
100
|
+
bytes_sent INTEGER DEFAULT 0,
|
|
101
|
+
bytes_received INTEGER DEFAULT 0,
|
|
102
|
+
input_tokens INTEGER DEFAULT 0,
|
|
103
|
+
output_tokens INTEGER DEFAULT 0,
|
|
104
|
+
cache_read_tokens INTEGER DEFAULT 0,
|
|
105
|
+
cache_write_tokens INTEGER DEFAULT 0,
|
|
106
|
+
total_tokens INTEGER DEFAULT 0,
|
|
107
|
+
PRIMARY KEY (user_id, date)
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
-- Agent token metric watermarks. Agent snapshots are cumulative within one
|
|
111
|
+
-- process epoch; this table makes reconnects and server restarts idempotent.
|
|
112
|
+
CREATE TABLE IF NOT EXISTS agent_metric_watermarks (
|
|
113
|
+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
114
|
+
agent_instance_id TEXT NOT NULL,
|
|
115
|
+
metric_epoch TEXT NOT NULL,
|
|
116
|
+
input_tokens INTEGER DEFAULT 0,
|
|
117
|
+
output_tokens INTEGER DEFAULT 0,
|
|
118
|
+
cache_read_tokens INTEGER DEFAULT 0,
|
|
119
|
+
cache_write_tokens INTEGER DEFAULT 0,
|
|
120
|
+
total_tokens INTEGER DEFAULT 0,
|
|
121
|
+
updated_at INTEGER NOT NULL,
|
|
122
|
+
PRIMARY KEY (user_id, agent_instance_id, metric_epoch)
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
-- 基本索引(不依赖迁移列)
|
|
126
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent_id);
|
|
127
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_updated ON sessions(updated_at DESC);
|
|
128
|
+
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
|
|
129
|
+
CREATE INDEX IF NOT EXISTS idx_messages_created ON messages(created_at);
|
|
130
|
+
-- feat-chat-load-perf: composite covering index for the role='user' filter
|
|
131
|
+
-- used by getRecentUserMessageIds / getLastUserMessage / getUserMessageIdsBeforeId.
|
|
132
|
+
-- Pre-fix: SQLite seeks by (session_id) then post-filters every row by role,
|
|
133
|
+
-- which is hundreds of ms on sessions with thousands of messages. Post-fix:
|
|
134
|
+
-- direct index seek + reverse scan limited to N user rows. Built in the
|
|
135
|
+
-- base block (not postMigrationIndexes) because session_id / role / id all
|
|
136
|
+
-- live in the base CREATE TABLE — there's no migration-column dependency.
|
|
137
|
+
-- IF NOT EXISTS makes this a one-shot cost on first startup post-deploy; on
|
|
138
|
+
-- a messages table with hundreds of thousands of rows the build can take
|
|
139
|
+
-- several seconds, so we time it just below. WAL mode (line 22) lets
|
|
140
|
+
-- concurrent reads continue during the build; writers will block briefly.
|
|
141
|
+
CREATE INDEX IF NOT EXISTS idx_messages_session_role_id ON messages(session_id, role, id DESC);
|
|
142
|
+
CREATE INDEX IF NOT EXISTS idx_daily_stats_date ON daily_stats(date);
|
|
143
|
+
`);
|
|
144
|
+
|
|
145
|
+
// 数据库迁移 - 添加缺失的列
|
|
146
|
+
const migrations = [
|
|
147
|
+
`ALTER TABLE sessions ADD COLUMN user_id TEXT REFERENCES users(id)`,
|
|
148
|
+
`ALTER TABLE users ADD COLUMN totp_secret TEXT`,
|
|
149
|
+
`ALTER TABLE users ADD COLUMN totp_enabled INTEGER DEFAULT 0`,
|
|
150
|
+
`ALTER TABLE users ADD COLUMN password_hash TEXT`,
|
|
151
|
+
`ALTER TABLE users ADD COLUMN email TEXT`,
|
|
152
|
+
`ALTER TABLE users ADD COLUMN agent_secret TEXT`,
|
|
153
|
+
`ALTER TABLE users ADD COLUMN role TEXT DEFAULT 'user'`,
|
|
154
|
+
`ALTER TABLE messages ADD COLUMN metadata TEXT`,
|
|
155
|
+
`ALTER TABLE sessions ADD COLUMN is_pinned INTEGER DEFAULT 0`,
|
|
156
|
+
`ALTER TABLE users ADD COLUMN aad_oid TEXT`,
|
|
157
|
+
// fix-chat-title-sticky: persist the "user manually renamed this session"
|
|
158
|
+
// bit so it survives agent reconnect / server restart / DB rehydration.
|
|
159
|
+
// Before this column existed, the bit lived only on the in-memory
|
|
160
|
+
// `convInfo.customTitle` flag, and every rebuild path
|
|
161
|
+
// (agent-conversation handlers, agent-sync, get_agents) wiped it —
|
|
162
|
+
// letting the per-message auto-title write at
|
|
163
|
+
// `client-conversation.js:351` clobber the user's renamed title.
|
|
164
|
+
`ALTER TABLE sessions ADD COLUMN is_custom_title INTEGER DEFAULT 0`,
|
|
165
|
+
// feat-chat-load-perf: one-shot rebuild sentinel for the `bulkAddHistory`
|
|
166
|
+
// timestamp-range heuristic in server/db/message-db.js. The heuristic
|
|
167
|
+
// (when count > 5 and (max_ts - min_ts) < 1000ms) was designed to repair
|
|
168
|
+
// sessions whose timestamps got bunched by an old anchor-detection bug,
|
|
169
|
+
// by deleting all rows for the session and re-inserting from the agent's
|
|
170
|
+
// historyMessages payload. Without a sentinel, every subsequent resume
|
|
171
|
+
// re-triggers the rebuild (because the rebuild itself produces tightly
|
|
172
|
+
// spaced `ts = lastTs + 1` values that re-pass the < 1000ms test), so the
|
|
173
|
+
// user pays a full delete+rebuild on EVERY session open. The sentinel
|
|
174
|
+
// (Unix-ms stamp at the time of the one and only repair) lets the
|
|
175
|
+
// heuristic fire exactly once per session for the lifetime of the row.
|
|
176
|
+
`ALTER TABLE sessions ADD COLUMN ts_rebuilt_at INTEGER DEFAULT 0`,
|
|
177
|
+
// fix-copilot-provider-persist: persist the conversation's PROVIDER
|
|
178
|
+
// (claude-code / copilot / ...) so it survives an agent process restart.
|
|
179
|
+
// Before this column the provider lived ONLY in the agent's in-memory
|
|
180
|
+
// ctx.conversations Map (state.providerName). On restart that Map is empty,
|
|
181
|
+
// so the agent reported no provider, the server rebuilt convs from DB
|
|
182
|
+
// without one → the UI lost the "copilot" marker AND sends mis-routed to
|
|
183
|
+
// Claude (handleUserInput resolved providerName to the default). The send
|
|
184
|
+
// forward now reads this column so the agent can self-heal its ACP child.
|
|
185
|
+
`ALTER TABLE sessions ADD COLUMN provider TEXT`,
|
|
186
|
+
`ALTER TABLE user_stats ADD COLUMN input_tokens INTEGER DEFAULT 0`,
|
|
187
|
+
`ALTER TABLE user_stats ADD COLUMN output_tokens INTEGER DEFAULT 0`,
|
|
188
|
+
`ALTER TABLE user_stats ADD COLUMN cache_read_tokens INTEGER DEFAULT 0`,
|
|
189
|
+
`ALTER TABLE user_stats ADD COLUMN cache_write_tokens INTEGER DEFAULT 0`,
|
|
190
|
+
`ALTER TABLE user_stats ADD COLUMN total_tokens INTEGER DEFAULT 0`,
|
|
191
|
+
`ALTER TABLE daily_stats ADD COLUMN input_tokens INTEGER DEFAULT 0`,
|
|
192
|
+
`ALTER TABLE daily_stats ADD COLUMN output_tokens INTEGER DEFAULT 0`,
|
|
193
|
+
`ALTER TABLE daily_stats ADD COLUMN cache_read_tokens INTEGER DEFAULT 0`,
|
|
194
|
+
`ALTER TABLE daily_stats ADD COLUMN cache_write_tokens INTEGER DEFAULT 0`,
|
|
195
|
+
`ALTER TABLE daily_stats ADD COLUMN total_tokens INTEGER DEFAULT 0`
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
// Yeaft sessions table — server-side persistence so the unified sidebar
|
|
199
|
+
// can list yeaft sessions across all the user's agents (online or not)
|
|
200
|
+
// and survive reload, mirroring how chat conversations work via the
|
|
201
|
+
// `sessions` table. Schema is deliberately separate because the
|
|
202
|
+
// lifecycle and metadata diverge (roster, defaultVpId, per-session
|
|
203
|
+
// config overrides — none of which are chat concerns).
|
|
204
|
+
const yeaftSessionsTable = `
|
|
205
|
+
CREATE TABLE IF NOT EXISTS yeaft_sessions (
|
|
206
|
+
id TEXT NOT NULL,
|
|
207
|
+
user_id TEXT REFERENCES users(id),
|
|
208
|
+
agent_id TEXT NOT NULL,
|
|
209
|
+
name TEXT,
|
|
210
|
+
roster_json TEXT,
|
|
211
|
+
default_vp_id TEXT,
|
|
212
|
+
work_dir TEXT,
|
|
213
|
+
config_json TEXT,
|
|
214
|
+
announcement TEXT,
|
|
215
|
+
created_at INTEGER,
|
|
216
|
+
updated_at INTEGER NOT NULL,
|
|
217
|
+
is_archived INTEGER DEFAULT 0,
|
|
218
|
+
is_pinned INTEGER DEFAULT 0,
|
|
219
|
+
sort_order INTEGER,
|
|
220
|
+
PRIMARY KEY (user_id, agent_id, id)
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
CREATE INDEX IF NOT EXISTS idx_yeaft_sessions_user ON yeaft_sessions(user_id);
|
|
224
|
+
CREATE INDEX IF NOT EXISTS idx_yeaft_sessions_agent ON yeaft_sessions(agent_id);
|
|
225
|
+
CREATE INDEX IF NOT EXISTS idx_yeaft_sessions_id ON yeaft_sessions(id);
|
|
226
|
+
CREATE INDEX IF NOT EXISTS idx_yeaft_sessions_updated ON yeaft_sessions(updated_at DESC);
|
|
227
|
+
`;
|
|
228
|
+
db.exec(yeaftSessionsTable);
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const tableInfo = db.prepare(`PRAGMA table_info(yeaft_sessions)`).all();
|
|
232
|
+
const idColumn = tableInfo.find(col => col && col.name === 'id');
|
|
233
|
+
if (idColumn && Number(idColumn.pk) === 1) {
|
|
234
|
+
db.exec(`
|
|
235
|
+
CREATE TABLE IF NOT EXISTS yeaft_sessions_composite (
|
|
236
|
+
id TEXT NOT NULL,
|
|
237
|
+
user_id TEXT REFERENCES users(id),
|
|
238
|
+
agent_id TEXT NOT NULL,
|
|
239
|
+
name TEXT,
|
|
240
|
+
roster_json TEXT,
|
|
241
|
+
default_vp_id TEXT,
|
|
242
|
+
work_dir TEXT,
|
|
243
|
+
config_json TEXT,
|
|
244
|
+
announcement TEXT,
|
|
245
|
+
created_at INTEGER,
|
|
246
|
+
updated_at INTEGER NOT NULL,
|
|
247
|
+
is_archived INTEGER DEFAULT 0,
|
|
248
|
+
is_pinned INTEGER DEFAULT 0,
|
|
249
|
+
sort_order INTEGER,
|
|
250
|
+
PRIMARY KEY (user_id, agent_id, id)
|
|
251
|
+
);
|
|
252
|
+
INSERT OR REPLACE INTO yeaft_sessions_composite
|
|
253
|
+
(id, user_id, agent_id, name, roster_json, default_vp_id, work_dir,
|
|
254
|
+
config_json, announcement, created_at, updated_at, is_archived, is_pinned, sort_order)
|
|
255
|
+
SELECT id, user_id, agent_id, name, roster_json, default_vp_id, work_dir,
|
|
256
|
+
config_json, announcement, created_at, updated_at, is_archived, is_pinned, sort_order
|
|
257
|
+
FROM yeaft_sessions;
|
|
258
|
+
DROP TABLE yeaft_sessions;
|
|
259
|
+
ALTER TABLE yeaft_sessions_composite RENAME TO yeaft_sessions;
|
|
260
|
+
CREATE INDEX IF NOT EXISTS idx_yeaft_sessions_user ON yeaft_sessions(user_id);
|
|
261
|
+
CREATE INDEX IF NOT EXISTS idx_yeaft_sessions_agent ON yeaft_sessions(agent_id);
|
|
262
|
+
CREATE INDEX IF NOT EXISTS idx_yeaft_sessions_id ON yeaft_sessions(id);
|
|
263
|
+
CREATE INDEX IF NOT EXISTS idx_yeaft_sessions_updated ON yeaft_sessions(updated_at DESC);
|
|
264
|
+
`);
|
|
265
|
+
}
|
|
266
|
+
} catch (e) {
|
|
267
|
+
console.warn('[DB] yeaft_sessions composite-key migration failed:', e?.message || e);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Yeaft session schema additions (separate from `migrations` above so the
|
|
271
|
+
// table existence in the CREATE block above is guaranteed before we try
|
|
272
|
+
// to ALTER it). Same try/swallow pattern: ignores "column exists" on
|
|
273
|
+
// fresh DBs that already got the column from CREATE TABLE.
|
|
274
|
+
const yeaftMigrations = [
|
|
275
|
+
// fix-yeaft-session-list-and-menu: per-session pin state. Lives on the
|
|
276
|
+
// server so it survives reload / cross-device / agent restart; mirrored
|
|
277
|
+
// into chatStore.pinnedSessions on the web so sort logic stays unified
|
|
278
|
+
// between chat and yeaft.
|
|
279
|
+
`ALTER TABLE yeaft_sessions ADD COLUMN is_pinned INTEGER DEFAULT 0`,
|
|
280
|
+
`ALTER TABLE yeaft_sessions ADD COLUMN sort_order INTEGER`,
|
|
281
|
+
];
|
|
282
|
+
for (const migration of yeaftMigrations) {
|
|
283
|
+
try { db.exec(migration); } catch (_) { /* column exists */ }
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
for (const migration of migrations) {
|
|
287
|
+
try {
|
|
288
|
+
db.exec(migration);
|
|
289
|
+
} catch (e) {
|
|
290
|
+
// 列已存在,忽略错误
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// User identities table (multi-provider SSO + account binding)
|
|
295
|
+
// One user can have multiple identities (microsoft / github / google / wechat / alipay).
|
|
296
|
+
// UNIQUE(provider, subject) enforces "this provider account is bound to one user only".
|
|
297
|
+
const identityTable = `
|
|
298
|
+
CREATE TABLE IF NOT EXISTS user_identities (
|
|
299
|
+
id TEXT PRIMARY KEY,
|
|
300
|
+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
301
|
+
provider TEXT NOT NULL,
|
|
302
|
+
subject TEXT NOT NULL,
|
|
303
|
+
email TEXT,
|
|
304
|
+
display_name TEXT,
|
|
305
|
+
created_at INTEGER NOT NULL,
|
|
306
|
+
last_login_at INTEGER,
|
|
307
|
+
UNIQUE(provider, subject)
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
CREATE INDEX IF NOT EXISTS idx_identities_user ON user_identities(user_id);
|
|
311
|
+
CREATE INDEX IF NOT EXISTS idx_identities_provider ON user_identities(provider);
|
|
312
|
+
`;
|
|
313
|
+
try { db.exec(identityTable); } catch (e) { /* tables already exist */ }
|
|
314
|
+
|
|
315
|
+
// One-time backfill: copy existing users.aad_oid into user_identities so
|
|
316
|
+
// legacy AAD users automatically participate in the new identity model.
|
|
317
|
+
try {
|
|
318
|
+
const aadUsers = db.prepare("SELECT id, email FROM users WHERE aad_oid IS NOT NULL AND aad_oid != ''").all();
|
|
319
|
+
if (aadUsers.length > 0) {
|
|
320
|
+
const checkStmt = db.prepare(
|
|
321
|
+
"SELECT id FROM user_identities WHERE provider = 'microsoft' AND user_id = ?"
|
|
322
|
+
);
|
|
323
|
+
const insertStmt = db.prepare(
|
|
324
|
+
`INSERT OR IGNORE INTO user_identities (id, user_id, provider, subject, email, display_name, created_at)
|
|
325
|
+
VALUES (?, ?, 'microsoft', ?, ?, NULL, ?)`
|
|
326
|
+
);
|
|
327
|
+
const getOidStmt = db.prepare("SELECT aad_oid FROM users WHERE id = ?");
|
|
328
|
+
for (const u of aadUsers) {
|
|
329
|
+
if (checkStmt.get(u.id)) continue;
|
|
330
|
+
const oidRow = getOidStmt.get(u.id);
|
|
331
|
+
const oid = oidRow?.aad_oid;
|
|
332
|
+
if (!oid) continue;
|
|
333
|
+
const idVal = `idn_${randomUUID()}`;
|
|
334
|
+
try { insertStmt.run(idVal, u.id, oid, u.email || null, Date.now()); } catch (e) { /* unique conflict */ }
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
} catch (e) { /* table missing or migration error — non-fatal */ }
|
|
338
|
+
|
|
339
|
+
// Custom expert roles tables (帮帮团自定义角色)
|
|
340
|
+
const customExpertTables = `
|
|
341
|
+
CREATE TABLE IF NOT EXISTS custom_expert_roles (
|
|
342
|
+
id TEXT PRIMARY KEY,
|
|
343
|
+
user_id TEXT NOT NULL REFERENCES users(id),
|
|
344
|
+
role_id TEXT NOT NULL,
|
|
345
|
+
name TEXT NOT NULL,
|
|
346
|
+
full_name TEXT,
|
|
347
|
+
title TEXT NOT NULL,
|
|
348
|
+
title_en TEXT,
|
|
349
|
+
group_id TEXT NOT NULL DEFAULT 'custom',
|
|
350
|
+
icon TEXT,
|
|
351
|
+
message_prefix TEXT,
|
|
352
|
+
message_prefix_en TEXT,
|
|
353
|
+
created_at INTEGER NOT NULL,
|
|
354
|
+
updated_at INTEGER NOT NULL,
|
|
355
|
+
UNIQUE(user_id, role_id)
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
CREATE TABLE IF NOT EXISTS custom_expert_actions (
|
|
359
|
+
id TEXT PRIMARY KEY,
|
|
360
|
+
role_row_id TEXT NOT NULL REFERENCES custom_expert_roles(id) ON DELETE CASCADE,
|
|
361
|
+
action_id TEXT NOT NULL,
|
|
362
|
+
name TEXT NOT NULL,
|
|
363
|
+
name_en TEXT,
|
|
364
|
+
message_template TEXT,
|
|
365
|
+
message_template_en TEXT,
|
|
366
|
+
default_message TEXT,
|
|
367
|
+
default_message_en TEXT,
|
|
368
|
+
UNIQUE(role_row_id, action_id)
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
CREATE INDEX IF NOT EXISTS idx_custom_expert_roles_user ON custom_expert_roles(user_id);
|
|
372
|
+
CREATE INDEX IF NOT EXISTS idx_custom_expert_actions_role ON custom_expert_actions(role_row_id);
|
|
373
|
+
`;
|
|
374
|
+
try { db.exec(customExpertTables); } catch (e) { /* tables already exist */ }
|
|
375
|
+
|
|
376
|
+
// 创建依赖迁移列的索引(在迁移后)
|
|
377
|
+
const postMigrationIndexes = [
|
|
378
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id)`,
|
|
379
|
+
`CREATE INDEX IF NOT EXISTS idx_users_agent_secret ON users(agent_secret)`,
|
|
380
|
+
`CREATE INDEX IF NOT EXISTS idx_users_aad_oid ON users(aad_oid)`
|
|
381
|
+
];
|
|
382
|
+
// Time the post-migration index pass so the operational signal lands in the
|
|
383
|
+
// deploy log on first startup after composite-index addition — a multi-second
|
|
384
|
+
// index build over the messages table should not look like "the server hung".
|
|
385
|
+
// (The composite idx_messages_session_role_id itself was moved into the base
|
|
386
|
+
// CREATE INDEX block above; its first-time cost shows up in the engine init.)
|
|
387
|
+
console.time('[db] postMigrationIndexes');
|
|
388
|
+
for (const idx of postMigrationIndexes) {
|
|
389
|
+
try { db.exec(idx); } catch (e) { /* 索引已存在 */ }
|
|
390
|
+
}
|
|
391
|
+
console.timeEnd('[db] postMigrationIndexes');
|
|
392
|
+
|
|
393
|
+
// 生成用户级 Agent 密钥
|
|
394
|
+
export function generateAgentSecret() {
|
|
395
|
+
return randomBytes(32).toString('hex');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// 生成用户 ID
|
|
399
|
+
export function generateUserId() {
|
|
400
|
+
return `user_${randomUUID()}`;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// 准备常用语句
|
|
404
|
+
export const stmts = {
|
|
405
|
+
// User 操作
|
|
406
|
+
insertUser: db.prepare(`
|
|
407
|
+
INSERT INTO users (id, username, display_name, created_at)
|
|
408
|
+
VALUES (?, ?, ?, ?)
|
|
409
|
+
`),
|
|
410
|
+
|
|
411
|
+
insertUserFull: db.prepare(`
|
|
412
|
+
INSERT INTO users (id, username, display_name, password_hash, email, agent_secret, role, created_at)
|
|
413
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
414
|
+
`),
|
|
415
|
+
|
|
416
|
+
updateUserLogin: db.prepare(`
|
|
417
|
+
UPDATE users SET last_login_at = ? WHERE id = ?
|
|
418
|
+
`),
|
|
419
|
+
|
|
420
|
+
updateUserPassword: db.prepare(`
|
|
421
|
+
UPDATE users SET password_hash = ? WHERE id = ?
|
|
422
|
+
`),
|
|
423
|
+
|
|
424
|
+
updateUserEmail: db.prepare(`
|
|
425
|
+
UPDATE users SET email = ? WHERE id = ?
|
|
426
|
+
`),
|
|
427
|
+
|
|
428
|
+
updateUserDisplayName: db.prepare(`
|
|
429
|
+
UPDATE users SET display_name = ? WHERE id = ?
|
|
430
|
+
`),
|
|
431
|
+
|
|
432
|
+
updateUserAgentSecret: db.prepare(`
|
|
433
|
+
UPDATE users SET agent_secret = ? WHERE id = ?
|
|
434
|
+
`),
|
|
435
|
+
|
|
436
|
+
updateUserRole: db.prepare(`
|
|
437
|
+
UPDATE users SET role = ? WHERE id = ?
|
|
438
|
+
`),
|
|
439
|
+
|
|
440
|
+
updateUserMigrate: db.prepare(`
|
|
441
|
+
UPDATE users SET password_hash = ?, email = ?, role = ?, agent_secret = COALESCE(agent_secret, ?) WHERE id = ?
|
|
442
|
+
`),
|
|
443
|
+
|
|
444
|
+
getUserById: db.prepare(`
|
|
445
|
+
SELECT * FROM users WHERE id = ?
|
|
446
|
+
`),
|
|
447
|
+
|
|
448
|
+
getUserByUsername: db.prepare(`
|
|
449
|
+
SELECT * FROM users WHERE username = ?
|
|
450
|
+
`),
|
|
451
|
+
|
|
452
|
+
getUserByAgentSecret: db.prepare(`
|
|
453
|
+
SELECT * FROM users WHERE agent_secret = ?
|
|
454
|
+
`),
|
|
455
|
+
|
|
456
|
+
getAllUsers: db.prepare(`
|
|
457
|
+
SELECT * FROM users ORDER BY created_at DESC
|
|
458
|
+
`),
|
|
459
|
+
|
|
460
|
+
updateUserTotp: db.prepare(`
|
|
461
|
+
UPDATE users SET totp_secret = ?, totp_enabled = ? WHERE username = ?
|
|
462
|
+
`),
|
|
463
|
+
|
|
464
|
+
getUserTotp: db.prepare(`
|
|
465
|
+
SELECT totp_secret, totp_enabled FROM users WHERE username = ?
|
|
466
|
+
`),
|
|
467
|
+
|
|
468
|
+
getUserByAadOid: db.prepare(`
|
|
469
|
+
SELECT * FROM users WHERE aad_oid = ?
|
|
470
|
+
`),
|
|
471
|
+
|
|
472
|
+
updateUserAadOid: db.prepare(`
|
|
473
|
+
UPDATE users SET aad_oid = ? WHERE id = ?
|
|
474
|
+
`),
|
|
475
|
+
|
|
476
|
+
// Invitation 操作
|
|
477
|
+
insertInvitation: db.prepare(`
|
|
478
|
+
INSERT INTO invitations (id, created_by, created_at, expires_at, role)
|
|
479
|
+
VALUES (?, ?, ?, ?, ?)
|
|
480
|
+
`),
|
|
481
|
+
|
|
482
|
+
getInvitation: db.prepare(`
|
|
483
|
+
SELECT * FROM invitations WHERE id = ?
|
|
484
|
+
`),
|
|
485
|
+
|
|
486
|
+
useInvitation: db.prepare(`
|
|
487
|
+
UPDATE invitations SET used_by = ?, used_at = ? WHERE id = ?
|
|
488
|
+
`),
|
|
489
|
+
|
|
490
|
+
getInvitationsByUser: db.prepare(`
|
|
491
|
+
SELECT i.*, u.username AS used_by_username
|
|
492
|
+
FROM invitations i
|
|
493
|
+
LEFT JOIN users u ON i.used_by = u.id
|
|
494
|
+
WHERE i.created_by = ?
|
|
495
|
+
ORDER BY i.created_at DESC
|
|
496
|
+
`),
|
|
497
|
+
|
|
498
|
+
deleteInvitation: db.prepare(`
|
|
499
|
+
DELETE FROM invitations WHERE id = ? AND created_by = ? AND used_by IS NULL
|
|
500
|
+
`),
|
|
501
|
+
|
|
502
|
+
cleanupExpiredInvitations: db.prepare(`
|
|
503
|
+
DELETE FROM invitations WHERE expires_at < ? AND used_by IS NULL
|
|
504
|
+
`),
|
|
505
|
+
|
|
506
|
+
// Session 操作
|
|
507
|
+
insertSession: db.prepare(`
|
|
508
|
+
INSERT INTO sessions (id, user_id, agent_id, agent_name, claude_session_id, work_dir, title, provider, created_at, updated_at)
|
|
509
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
510
|
+
`),
|
|
511
|
+
|
|
512
|
+
updateSession: db.prepare(`
|
|
513
|
+
UPDATE sessions SET
|
|
514
|
+
claude_session_id = COALESCE(?, claude_session_id),
|
|
515
|
+
title = COALESCE(?, title),
|
|
516
|
+
is_custom_title = COALESCE(?, is_custom_title),
|
|
517
|
+
updated_at = ?
|
|
518
|
+
WHERE id = ?
|
|
519
|
+
`),
|
|
520
|
+
|
|
521
|
+
updateSessionActive: db.prepare(`
|
|
522
|
+
UPDATE sessions SET is_active = ?, updated_at = ? WHERE id = ?
|
|
523
|
+
`),
|
|
524
|
+
|
|
525
|
+
// fix-session-dup: transfer a session row to a new owning agent.
|
|
526
|
+
// Needed when the user resumes a conversation against a different
|
|
527
|
+
// agent than the one that originally created it — without this,
|
|
528
|
+
// DB.agent_id keeps pointing at the old agent and on the next
|
|
529
|
+
// `get_agents` restore (client-conversation.js:`get_agents`) the
|
|
530
|
+
// conv gets reseated into the OLD agent's in-memory Map alongside
|
|
531
|
+
// the new owner, which is the server-side root of Bug 2 (one conv
|
|
532
|
+
// rendered as two sidebar rows with different agent badges).
|
|
533
|
+
updateSessionAgent: db.prepare(`
|
|
534
|
+
UPDATE sessions SET agent_id = ?, agent_name = ?, updated_at = ? WHERE id = ?
|
|
535
|
+
`),
|
|
536
|
+
|
|
537
|
+
updateSessionPinned: db.prepare(`
|
|
538
|
+
UPDATE sessions SET is_pinned = ?, updated_at = ? WHERE id = ?
|
|
539
|
+
`),
|
|
540
|
+
|
|
541
|
+
// fix-copilot-provider-persist: persist the conversation's code-agent
|
|
542
|
+
// provider so it survives an agent process restart. Mirrors the pinned/
|
|
543
|
+
// agent update shape. Only written when a non-default provider is known
|
|
544
|
+
// (the create/resume handlers pass msg.provider through).
|
|
545
|
+
updateSessionProvider: db.prepare(`
|
|
546
|
+
UPDATE sessions SET provider = ?, updated_at = ? WHERE id = ?
|
|
547
|
+
`),
|
|
548
|
+
|
|
549
|
+
// feat-chat-load-perf: one-shot sentinel for the bulkAddHistory timestamp-
|
|
550
|
+
// rebuild repair path. ts_rebuilt_at = 0 means "never repaired"; non-zero
|
|
551
|
+
// means "repair already ran at this Unix-ms". The repair is destructive
|
|
552
|
+
// (DELETE + re-INSERT all rows for the session) so it must run at most once
|
|
553
|
+
// per session over its lifetime. Statements live in the Session block
|
|
554
|
+
// because they SELECT/UPDATE the `sessions` table — the bulkAddHistory
|
|
555
|
+
// call site in server/db/message-db.js is the only consumer today.
|
|
556
|
+
getSessionTsRebuiltAt: db.prepare(`
|
|
557
|
+
SELECT ts_rebuilt_at FROM sessions WHERE id = ?
|
|
558
|
+
`),
|
|
559
|
+
|
|
560
|
+
markSessionTsRebuilt: db.prepare(`
|
|
561
|
+
UPDATE sessions SET ts_rebuilt_at = ? WHERE id = ?
|
|
562
|
+
`),
|
|
563
|
+
|
|
564
|
+
getSession: db.prepare(`
|
|
565
|
+
SELECT * FROM sessions WHERE id = ?
|
|
566
|
+
`),
|
|
567
|
+
|
|
568
|
+
getSessionsByAgent: db.prepare(`
|
|
569
|
+
SELECT * FROM sessions WHERE agent_id = ? ORDER BY updated_at DESC LIMIT ?
|
|
570
|
+
`),
|
|
571
|
+
|
|
572
|
+
getSessionsByUser: db.prepare(`
|
|
573
|
+
SELECT * FROM sessions WHERE user_id = ? ORDER BY updated_at DESC LIMIT ?
|
|
574
|
+
`),
|
|
575
|
+
|
|
576
|
+
getSessionsByUserAndAgent: db.prepare(`
|
|
577
|
+
SELECT * FROM sessions WHERE user_id = ? AND agent_id = ? ORDER BY updated_at DESC LIMIT ?
|
|
578
|
+
`),
|
|
579
|
+
|
|
580
|
+
getAllSessions: db.prepare(`
|
|
581
|
+
SELECT * FROM sessions ORDER BY updated_at DESC LIMIT ?
|
|
582
|
+
`),
|
|
583
|
+
|
|
584
|
+
getActiveSessions: db.prepare(`
|
|
585
|
+
SELECT * FROM sessions WHERE is_active = 1 ORDER BY updated_at DESC
|
|
586
|
+
`),
|
|
587
|
+
|
|
588
|
+
getActiveSessionsByUser: db.prepare(`
|
|
589
|
+
SELECT * FROM sessions WHERE (user_id = ? OR user_id IS NULL) AND is_active = 1 ORDER BY updated_at DESC
|
|
590
|
+
`),
|
|
591
|
+
|
|
592
|
+
deleteSession: db.prepare(`
|
|
593
|
+
DELETE FROM sessions WHERE id = ?
|
|
594
|
+
`),
|
|
595
|
+
|
|
596
|
+
// Message 操作
|
|
597
|
+
insertMessage: db.prepare(`
|
|
598
|
+
INSERT INTO messages (session_id, role, content, message_type, tool_name, tool_input, created_at, metadata)
|
|
599
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
600
|
+
`),
|
|
601
|
+
|
|
602
|
+
getRecentUserMessageIds: db.prepare(`
|
|
603
|
+
SELECT id FROM messages WHERE session_id = ? AND role = 'user'
|
|
604
|
+
ORDER BY id DESC LIMIT ?
|
|
605
|
+
`),
|
|
606
|
+
|
|
607
|
+
getMessagesFromId: db.prepare(`
|
|
608
|
+
SELECT * FROM messages WHERE session_id = ? AND id >= ?
|
|
609
|
+
ORDER BY id ASC
|
|
610
|
+
`),
|
|
611
|
+
|
|
612
|
+
getUserMessageIdsBeforeId: db.prepare(`
|
|
613
|
+
SELECT id FROM messages WHERE session_id = ? AND role = 'user' AND id < ?
|
|
614
|
+
ORDER BY id DESC LIMIT ?
|
|
615
|
+
`),
|
|
616
|
+
|
|
617
|
+
getMessagesBetweenIds: db.prepare(`
|
|
618
|
+
SELECT * FROM messages WHERE session_id = ? AND id >= ? AND id < ?
|
|
619
|
+
ORDER BY id ASC
|
|
620
|
+
`),
|
|
621
|
+
|
|
622
|
+
getMessagesBySession: db.prepare(`
|
|
623
|
+
SELECT * FROM messages WHERE session_id = ? ORDER BY id ASC
|
|
624
|
+
`),
|
|
625
|
+
|
|
626
|
+
getRecentMessages: db.prepare(`
|
|
627
|
+
SELECT * FROM messages WHERE session_id = ? ORDER BY id DESC LIMIT ?
|
|
628
|
+
`),
|
|
629
|
+
|
|
630
|
+
getMessagesAfterId: db.prepare(`
|
|
631
|
+
SELECT * FROM messages WHERE session_id = ? AND id > ? ORDER BY id ASC
|
|
632
|
+
`),
|
|
633
|
+
|
|
634
|
+
getMessagesBeforeId: db.prepare(`
|
|
635
|
+
SELECT * FROM messages WHERE session_id = ? AND id < ?
|
|
636
|
+
ORDER BY id DESC LIMIT ?
|
|
637
|
+
`),
|
|
638
|
+
|
|
639
|
+
getMessageCount: db.prepare(`
|
|
640
|
+
SELECT COUNT(*) as count FROM messages WHERE session_id = ?
|
|
641
|
+
`),
|
|
642
|
+
|
|
643
|
+
getTimestampRange: db.prepare(`
|
|
644
|
+
SELECT MIN(created_at) as min_ts, MAX(created_at) as max_ts, COUNT(*) as count
|
|
645
|
+
FROM messages WHERE session_id = ?
|
|
646
|
+
`),
|
|
647
|
+
|
|
648
|
+
// feat-chat-load-perf: one-shot sentinel for the bulkAddHistory timestamp-
|
|
649
|
+
// rebuild repair path. ts_rebuilt_at = 0 means "never repaired"; non-zero
|
|
650
|
+
// means "repair already ran at this Unix-ms". The repair is destructive
|
|
651
|
+
// (DELETE + re-INSERT all rows for the session) so it must run at most once
|
|
652
|
+
// per session over its lifetime.
|
|
653
|
+
getSessionTsRebuiltAt: db.prepare(`
|
|
654
|
+
SELECT ts_rebuilt_at FROM sessions WHERE id = ?
|
|
655
|
+
`),
|
|
656
|
+
|
|
657
|
+
markSessionTsRebuilt: db.prepare(`
|
|
658
|
+
UPDATE sessions SET ts_rebuilt_at = ? WHERE id = ?
|
|
659
|
+
`),
|
|
660
|
+
|
|
661
|
+
getLastUserMessage: db.prepare(`
|
|
662
|
+
SELECT * FROM messages WHERE session_id = ? AND role = 'user'
|
|
663
|
+
ORDER BY id DESC LIMIT 1
|
|
664
|
+
`),
|
|
665
|
+
|
|
666
|
+
deleteMessagesBySession: db.prepare(`
|
|
667
|
+
DELETE FROM messages WHERE session_id = ?
|
|
668
|
+
`),
|
|
669
|
+
|
|
670
|
+
deleteMessagesAfterId: db.prepare(`
|
|
671
|
+
DELETE FROM messages WHERE session_id = ? AND id > ?
|
|
672
|
+
`),
|
|
673
|
+
|
|
674
|
+
updateMessageMetadata: db.prepare(`
|
|
675
|
+
UPDATE messages SET metadata = ? WHERE id = ?
|
|
676
|
+
`),
|
|
677
|
+
|
|
678
|
+
// UserStats 操作
|
|
679
|
+
upsertUserStats: db.prepare(`
|
|
680
|
+
INSERT INTO user_stats (
|
|
681
|
+
user_id, message_count, session_count, request_count, bytes_sent, bytes_received,
|
|
682
|
+
input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, total_tokens, updated_at
|
|
683
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
684
|
+
ON CONFLICT(user_id) DO UPDATE SET
|
|
685
|
+
message_count = message_count + excluded.message_count,
|
|
686
|
+
session_count = session_count + excluded.session_count,
|
|
687
|
+
request_count = request_count + excluded.request_count,
|
|
688
|
+
bytes_sent = bytes_sent + excluded.bytes_sent,
|
|
689
|
+
bytes_received = bytes_received + excluded.bytes_received,
|
|
690
|
+
input_tokens = input_tokens + excluded.input_tokens,
|
|
691
|
+
output_tokens = output_tokens + excluded.output_tokens,
|
|
692
|
+
cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
|
|
693
|
+
cache_write_tokens = cache_write_tokens + excluded.cache_write_tokens,
|
|
694
|
+
total_tokens = total_tokens + excluded.total_tokens,
|
|
695
|
+
updated_at = excluded.updated_at
|
|
696
|
+
`),
|
|
697
|
+
|
|
698
|
+
// DailyStats 操作
|
|
699
|
+
upsertDailyStats: db.prepare(`
|
|
700
|
+
INSERT INTO daily_stats (
|
|
701
|
+
user_id, date, message_count, session_count, request_count, bytes_sent, bytes_received,
|
|
702
|
+
input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, total_tokens
|
|
703
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
704
|
+
ON CONFLICT(user_id, date) DO UPDATE SET
|
|
705
|
+
message_count = message_count + excluded.message_count,
|
|
706
|
+
session_count = session_count + excluded.session_count,
|
|
707
|
+
request_count = request_count + excluded.request_count,
|
|
708
|
+
bytes_sent = bytes_sent + excluded.bytes_sent,
|
|
709
|
+
bytes_received = bytes_received + excluded.bytes_received,
|
|
710
|
+
input_tokens = input_tokens + excluded.input_tokens,
|
|
711
|
+
output_tokens = output_tokens + excluded.output_tokens,
|
|
712
|
+
cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
|
|
713
|
+
cache_write_tokens = cache_write_tokens + excluded.cache_write_tokens,
|
|
714
|
+
total_tokens = total_tokens + excluded.total_tokens
|
|
715
|
+
`),
|
|
716
|
+
|
|
717
|
+
getDailyStatsAll: db.prepare(`
|
|
718
|
+
SELECT ds.user_id, u.username, u.display_name, u.role, u.last_login_at,
|
|
719
|
+
SUM(ds.message_count) as message_count, SUM(ds.session_count) as session_count,
|
|
720
|
+
SUM(ds.request_count) as request_count, SUM(ds.bytes_sent) as bytes_sent,
|
|
721
|
+
SUM(ds.bytes_received) as bytes_received,
|
|
722
|
+
SUM(ds.input_tokens) as input_tokens, SUM(ds.output_tokens) as output_tokens,
|
|
723
|
+
SUM(ds.cache_read_tokens) as cache_read_tokens,
|
|
724
|
+
SUM(ds.cache_write_tokens) as cache_write_tokens,
|
|
725
|
+
SUM(ds.total_tokens) as total_tokens
|
|
726
|
+
FROM daily_stats ds
|
|
727
|
+
JOIN users u ON ds.user_id = u.id
|
|
728
|
+
WHERE ds.date >= ?
|
|
729
|
+
GROUP BY ds.user_id
|
|
730
|
+
ORDER BY message_count DESC
|
|
731
|
+
`),
|
|
732
|
+
|
|
733
|
+
getTodayActiveUsers: db.prepare(`
|
|
734
|
+
SELECT COUNT(DISTINCT user_id) as count FROM daily_stats WHERE date = ?
|
|
735
|
+
`),
|
|
736
|
+
|
|
737
|
+
getTodayMessages: db.prepare(`
|
|
738
|
+
SELECT COALESCE(SUM(message_count), 0) as count FROM daily_stats WHERE date = ?
|
|
739
|
+
`),
|
|
740
|
+
|
|
741
|
+
getUserStats: db.prepare(`
|
|
742
|
+
SELECT us.*, u.username, u.display_name, u.role, u.last_login_at
|
|
743
|
+
FROM user_stats us
|
|
744
|
+
JOIN users u ON us.user_id = u.id
|
|
745
|
+
ORDER BY us.message_count DESC
|
|
746
|
+
`),
|
|
747
|
+
|
|
748
|
+
getUserStatsById: db.prepare(`
|
|
749
|
+
SELECT * FROM user_stats WHERE user_id = ?
|
|
750
|
+
`),
|
|
751
|
+
|
|
752
|
+
getAgentMetricWatermark: db.prepare(`
|
|
753
|
+
SELECT * FROM agent_metric_watermarks
|
|
754
|
+
WHERE user_id = ? AND agent_instance_id = ? AND metric_epoch = ?
|
|
755
|
+
`),
|
|
756
|
+
|
|
757
|
+
upsertAgentMetricWatermark: db.prepare(`
|
|
758
|
+
INSERT INTO agent_metric_watermarks (
|
|
759
|
+
user_id, agent_instance_id, metric_epoch, input_tokens, output_tokens,
|
|
760
|
+
cache_read_tokens, cache_write_tokens, total_tokens, updated_at
|
|
761
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
762
|
+
ON CONFLICT(user_id, agent_instance_id, metric_epoch) DO UPDATE SET
|
|
763
|
+
input_tokens = excluded.input_tokens,
|
|
764
|
+
output_tokens = excluded.output_tokens,
|
|
765
|
+
cache_read_tokens = excluded.cache_read_tokens,
|
|
766
|
+
cache_write_tokens = excluded.cache_write_tokens,
|
|
767
|
+
total_tokens = excluded.total_tokens,
|
|
768
|
+
updated_at = excluded.updated_at
|
|
769
|
+
`),
|
|
770
|
+
|
|
771
|
+
// Identity 操作 (multi-provider SSO)
|
|
772
|
+
insertIdentity: db.prepare(`
|
|
773
|
+
INSERT INTO user_identities (id, user_id, provider, subject, email, display_name, created_at, last_login_at)
|
|
774
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
775
|
+
`),
|
|
776
|
+
|
|
777
|
+
getIdentityBySubject: db.prepare(`
|
|
778
|
+
SELECT * FROM user_identities WHERE provider = ? AND subject = ?
|
|
779
|
+
`),
|
|
780
|
+
|
|
781
|
+
getIdentitiesByUser: db.prepare(`
|
|
782
|
+
SELECT * FROM user_identities WHERE user_id = ? ORDER BY created_at ASC
|
|
783
|
+
`),
|
|
784
|
+
|
|
785
|
+
getIdentityForUser: db.prepare(`
|
|
786
|
+
SELECT * FROM user_identities WHERE user_id = ? AND provider = ?
|
|
787
|
+
`),
|
|
788
|
+
|
|
789
|
+
countIdentitiesByUser: db.prepare(`
|
|
790
|
+
SELECT COUNT(*) as count FROM user_identities WHERE user_id = ?
|
|
791
|
+
`),
|
|
792
|
+
|
|
793
|
+
updateIdentityLogin: db.prepare(`
|
|
794
|
+
UPDATE user_identities SET last_login_at = ? WHERE id = ?
|
|
795
|
+
`),
|
|
796
|
+
|
|
797
|
+
deleteIdentityForUser: db.prepare(`
|
|
798
|
+
DELETE FROM user_identities WHERE user_id = ? AND provider = ?
|
|
799
|
+
`),
|
|
800
|
+
|
|
801
|
+
// Hard-delete cascade for account deletion.
|
|
802
|
+
// user_identities and messages cascade automatically; the rest are explicit.
|
|
803
|
+
deleteUserSessionsByUser: db.prepare(`
|
|
804
|
+
DELETE FROM sessions WHERE user_id = ?
|
|
805
|
+
`),
|
|
806
|
+
deleteYeaftSessionsByUserCascade: db.prepare(`
|
|
807
|
+
DELETE FROM yeaft_sessions WHERE user_id = ?
|
|
808
|
+
`),
|
|
809
|
+
deleteIdentitiesForUser: db.prepare(`
|
|
810
|
+
DELETE FROM user_identities WHERE user_id = ?
|
|
811
|
+
`),
|
|
812
|
+
deleteUserStats: db.prepare(`
|
|
813
|
+
DELETE FROM user_stats WHERE user_id = ?
|
|
814
|
+
`),
|
|
815
|
+
deleteDailyStatsForUser: db.prepare(`
|
|
816
|
+
DELETE FROM daily_stats WHERE user_id = ?
|
|
817
|
+
`),
|
|
818
|
+
deleteCustomExpertRolesForUser: db.prepare(`
|
|
819
|
+
DELETE FROM custom_expert_roles WHERE user_id = ?
|
|
820
|
+
`),
|
|
821
|
+
// Invitations: keep history but null-out the FK so it doesn't block deletion.
|
|
822
|
+
// (created_by is NOT NULL, so for invitations the user created we just delete them.)
|
|
823
|
+
deleteInvitationsCreatedBy: db.prepare(`
|
|
824
|
+
DELETE FROM invitations WHERE created_by = ?
|
|
825
|
+
`),
|
|
826
|
+
clearInvitationUsedBy: db.prepare(`
|
|
827
|
+
UPDATE invitations SET used_by = NULL WHERE used_by = ?
|
|
828
|
+
`),
|
|
829
|
+
deleteUserById: db.prepare(`
|
|
830
|
+
DELETE FROM users WHERE id = ?
|
|
831
|
+
`),
|
|
832
|
+
|
|
833
|
+
// Yeaft session 操作
|
|
834
|
+
upsertYeaftSession: db.prepare(`
|
|
835
|
+
INSERT INTO yeaft_sessions
|
|
836
|
+
(id, user_id, agent_id, name, roster_json, default_vp_id, work_dir,
|
|
837
|
+
config_json, announcement, created_at, updated_at, is_archived)
|
|
838
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
839
|
+
ON CONFLICT(user_id, agent_id, id) DO UPDATE SET
|
|
840
|
+
user_id = COALESCE(excluded.user_id, user_id),
|
|
841
|
+
name = excluded.name,
|
|
842
|
+
roster_json = excluded.roster_json,
|
|
843
|
+
default_vp_id = excluded.default_vp_id,
|
|
844
|
+
work_dir = excluded.work_dir,
|
|
845
|
+
config_json = excluded.config_json,
|
|
846
|
+
announcement = excluded.announcement,
|
|
847
|
+
created_at = COALESCE(yeaft_sessions.created_at, excluded.created_at),
|
|
848
|
+
updated_at = excluded.updated_at,
|
|
849
|
+
is_archived = excluded.is_archived
|
|
850
|
+
`),
|
|
851
|
+
|
|
852
|
+
getYeaftSession: db.prepare(`
|
|
853
|
+
SELECT * FROM yeaft_sessions WHERE id = ? ORDER BY updated_at DESC LIMIT 1
|
|
854
|
+
`),
|
|
855
|
+
|
|
856
|
+
getYeaftSessionForAgent: db.prepare(`
|
|
857
|
+
SELECT * FROM yeaft_sessions WHERE id = ? AND user_id = ? AND agent_id = ?
|
|
858
|
+
`),
|
|
859
|
+
|
|
860
|
+
getYeaftSessionsByUser: db.prepare(`
|
|
861
|
+
SELECT * FROM yeaft_sessions
|
|
862
|
+
WHERE user_id = ? AND is_archived = 0
|
|
863
|
+
ORDER BY CASE WHEN sort_order IS NULL THEN 1 ELSE 0 END, sort_order ASC, updated_at DESC
|
|
864
|
+
`),
|
|
865
|
+
|
|
866
|
+
getYeaftSessionsByAgent: db.prepare(`
|
|
867
|
+
SELECT * FROM yeaft_sessions
|
|
868
|
+
WHERE agent_id = ? AND is_archived = 0
|
|
869
|
+
ORDER BY CASE WHEN sort_order IS NULL THEN 1 ELSE 0 END, sort_order ASC, updated_at DESC
|
|
870
|
+
`),
|
|
871
|
+
|
|
872
|
+
deleteYeaftSession: db.prepare(`
|
|
873
|
+
DELETE FROM yeaft_sessions WHERE id = ?
|
|
874
|
+
`),
|
|
875
|
+
|
|
876
|
+
deleteYeaftSessionForAgent: db.prepare(`
|
|
877
|
+
DELETE FROM yeaft_sessions WHERE id = ? AND user_id = ? AND agent_id = ?
|
|
878
|
+
`),
|
|
879
|
+
|
|
880
|
+
deleteYeaftSessionsByUser: db.prepare(`
|
|
881
|
+
DELETE FROM yeaft_sessions WHERE user_id = ?
|
|
882
|
+
`),
|
|
883
|
+
|
|
884
|
+
setYeaftSessionArchived: db.prepare(`
|
|
885
|
+
UPDATE yeaft_sessions SET is_archived = ?, updated_at = ? WHERE id = ?
|
|
886
|
+
`),
|
|
887
|
+
|
|
888
|
+
setYeaftSessionArchivedForAgent: db.prepare(`
|
|
889
|
+
UPDATE yeaft_sessions SET is_archived = ?, updated_at = ? WHERE id = ? AND user_id = ? AND agent_id = ?
|
|
890
|
+
`),
|
|
891
|
+
|
|
892
|
+
setYeaftSessionPinned: db.prepare(`
|
|
893
|
+
UPDATE yeaft_sessions SET is_pinned = ?, updated_at = ? WHERE id = ?
|
|
894
|
+
`),
|
|
895
|
+
|
|
896
|
+
setYeaftSessionPinnedForAgent: db.prepare(`
|
|
897
|
+
UPDATE yeaft_sessions SET is_pinned = ?, updated_at = ? WHERE id = ? AND user_id = ? AND agent_id = ?
|
|
898
|
+
`),
|
|
899
|
+
|
|
900
|
+
setYeaftSessionSortOrder: db.prepare(`
|
|
901
|
+
UPDATE yeaft_sessions
|
|
902
|
+
SET sort_order = ?, updated_at = ?
|
|
903
|
+
WHERE id = ? AND user_id = ? AND agent_id = ?
|
|
904
|
+
`),
|
|
905
|
+
|
|
906
|
+
// Dashboard 聚合
|
|
907
|
+
getDashboardTotals: db.prepare(`
|
|
908
|
+
SELECT
|
|
909
|
+
(SELECT COUNT(*) FROM users) as total_users,
|
|
910
|
+
(SELECT COUNT(*) FROM sessions) as total_sessions,
|
|
911
|
+
(SELECT COUNT(*) FROM messages) as total_messages
|
|
912
|
+
`),
|
|
913
|
+
|
|
914
|
+
getDashboardTokenTotals: db.prepare(`
|
|
915
|
+
SELECT
|
|
916
|
+
COALESCE(SUM(input_tokens), 0) as input_tokens,
|
|
917
|
+
COALESCE(SUM(output_tokens), 0) as output_tokens,
|
|
918
|
+
COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens,
|
|
919
|
+
COALESCE(SUM(cache_write_tokens), 0) as cache_write_tokens,
|
|
920
|
+
COALESCE(SUM(total_tokens), 0) as total_tokens
|
|
921
|
+
FROM user_stats
|
|
922
|
+
`)
|
|
923
|
+
};
|
|
924
|
+
|
|
925
|
+
// 关闭数据库连接(用于优雅退出)
|
|
926
|
+
let dbClosed = false;
|
|
927
|
+
export function closeDb() {
|
|
928
|
+
if (dbClosed) return;
|
|
929
|
+
db.close();
|
|
930
|
+
dbClosed = true;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* Run a function inside a SQLite transaction.
|
|
935
|
+
* node:sqlite (DatabaseSync) does not provide better-sqlite3's `db.transaction(fn)`
|
|
936
|
+
* helper, so we wrap BEGIN/COMMIT/ROLLBACK manually.
|
|
937
|
+
*
|
|
938
|
+
* Returns a function with the same signature as `fn` (call it with the same args)
|
|
939
|
+
* to mirror the better-sqlite3 API.
|
|
940
|
+
*
|
|
941
|
+
* @template T
|
|
942
|
+
* @template {any[]} A
|
|
943
|
+
* @param {(...args: A) => T} fn
|
|
944
|
+
* @returns {(...args: A) => T}
|
|
945
|
+
*/
|
|
946
|
+
export function transaction(fn) {
|
|
947
|
+
return (...args) => {
|
|
948
|
+
db.exec('BEGIN');
|
|
949
|
+
try {
|
|
950
|
+
const result = fn(...args);
|
|
951
|
+
db.exec('COMMIT');
|
|
952
|
+
return result;
|
|
953
|
+
} catch (err) {
|
|
954
|
+
try { db.exec('ROLLBACK'); } catch { /* ignore rollback errors */ }
|
|
955
|
+
throw err;
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// 进程退出时关闭数据库(兜底)
|
|
961
|
+
process.on('exit', closeDb);
|
|
962
|
+
|
|
963
|
+
export default db;
|