@yeaft/webchat-agent 1.0.188 → 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,165 @@
|
|
|
1
|
+
import { stmts, generateUserId, generateAgentSecret, transaction } from './connection.js';
|
|
2
|
+
|
|
3
|
+
export const userDb = {
|
|
4
|
+
getOrCreate(username, displayName = null) {
|
|
5
|
+
let user = stmts.getUserByUsername.get(username);
|
|
6
|
+
if (!user) {
|
|
7
|
+
const id = generateUserId();
|
|
8
|
+
const now = Date.now();
|
|
9
|
+
stmts.insertUser.run(id, username, displayName || username, now);
|
|
10
|
+
user = { id, username, display_name: displayName || username, created_at: now };
|
|
11
|
+
}
|
|
12
|
+
return user;
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
createFull(username, passwordHash, email = null, role = 'user') {
|
|
16
|
+
const id = generateUserId();
|
|
17
|
+
const now = Date.now();
|
|
18
|
+
const agentSecret = generateAgentSecret();
|
|
19
|
+
stmts.insertUserFull.run(id, username, username, passwordHash, email, agentSecret, role, now);
|
|
20
|
+
return { id, username, display_name: username, password_hash: passwordHash, email, agent_secret: agentSecret, role, created_at: now };
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
migrateUser(username, passwordHash, email, role = 'admin') {
|
|
24
|
+
const existing = stmts.getUserByUsername.get(username);
|
|
25
|
+
if (existing) {
|
|
26
|
+
if (existing.password_hash) {
|
|
27
|
+
return existing;
|
|
28
|
+
}
|
|
29
|
+
const newSecret = generateAgentSecret();
|
|
30
|
+
stmts.updateUserMigrate.run(passwordHash, email, role, newSecret, existing.id);
|
|
31
|
+
return { ...existing, password_hash: passwordHash, email, role, agent_secret: existing.agent_secret || newSecret };
|
|
32
|
+
}
|
|
33
|
+
return this.createFull(username, passwordHash, email, role);
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
get(id) {
|
|
37
|
+
return stmts.getUserById.get(id);
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
getByUsername(username) {
|
|
41
|
+
return stmts.getUserByUsername.get(username);
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
getUserByAgentSecret(secret) {
|
|
45
|
+
if (!secret) return null;
|
|
46
|
+
return stmts.getUserByAgentSecret.get(secret) || null;
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
getAll() {
|
|
50
|
+
return stmts.getAllUsers.all();
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
updateLogin(id) {
|
|
54
|
+
stmts.updateUserLogin.run(Date.now(), id);
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
updatePassword(userId, passwordHash) {
|
|
58
|
+
stmts.updateUserPassword.run(passwordHash, userId);
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
updateEmail(userId, email) {
|
|
62
|
+
stmts.updateUserEmail.run(email, userId);
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
updateDisplayName(userId, displayName) {
|
|
66
|
+
if (!displayName) return;
|
|
67
|
+
stmts.updateUserDisplayName.run(displayName, userId);
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
getAgentSecret(userId) {
|
|
71
|
+
const user = stmts.getUserById.get(userId);
|
|
72
|
+
return user?.agent_secret || null;
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
resetAgentSecret(userId) {
|
|
76
|
+
const newSecret = generateAgentSecret();
|
|
77
|
+
stmts.updateUserAgentSecret.run(newSecret, userId);
|
|
78
|
+
return newSecret;
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
updateRole(userId, role) {
|
|
82
|
+
stmts.updateUserRole.run(role, userId);
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
getTotp(username) {
|
|
86
|
+
const result = stmts.getUserTotp.get(username);
|
|
87
|
+
if (result) {
|
|
88
|
+
return {
|
|
89
|
+
totpSecret: result.totp_secret,
|
|
90
|
+
totpEnabled: !!result.totp_enabled
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
updateTotp(username, totpSecret, totpEnabled) {
|
|
97
|
+
let user = stmts.getUserByUsername.get(username);
|
|
98
|
+
if (!user) {
|
|
99
|
+
const id = generateUserId();
|
|
100
|
+
const now = Date.now();
|
|
101
|
+
stmts.insertUser.run(id, username, username, now);
|
|
102
|
+
}
|
|
103
|
+
stmts.updateUserTotp.run(totpSecret, totpEnabled ? 1 : 0, username);
|
|
104
|
+
return true;
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
getByAadOid(aadOid) {
|
|
108
|
+
if (!aadOid) return null;
|
|
109
|
+
return stmts.getUserByAadOid.get(aadOid) || null;
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
updateAadOid(userId, aadOid) {
|
|
113
|
+
stmts.updateUserAadOid.run(aadOid, userId);
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Create a user from AAD profile (no password, linked by aad_oid).
|
|
118
|
+
* `displayName` is used as a friendlier label (e.g. the Alipay nickname);
|
|
119
|
+
* falls back to the username when not provided.
|
|
120
|
+
*/
|
|
121
|
+
createFromAad(username, email, aadOid, role = 'pro', displayName = null) {
|
|
122
|
+
const id = generateUserId();
|
|
123
|
+
const now = Date.now();
|
|
124
|
+
const agentSecret = generateAgentSecret();
|
|
125
|
+
const display = displayName || username;
|
|
126
|
+
stmts.insertUserFull.run(id, username, display, null, email, agentSecret, role, now);
|
|
127
|
+
stmts.updateUserAadOid.run(aadOid, id);
|
|
128
|
+
return { id, username, display_name: display, email, aad_oid: aadOid, agent_secret: agentSecret, role, created_at: now };
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Permanently delete a user and ALL data scoped to that user.
|
|
133
|
+
*
|
|
134
|
+
* Tables touched (all inside one transaction):
|
|
135
|
+
* - user_identities hard delete (also covered by FK CASCADE, belt-and-braces)
|
|
136
|
+
* - sessions hard delete (messages cascade via FK)
|
|
137
|
+
* - user_stats hard delete
|
|
138
|
+
* - daily_stats hard delete
|
|
139
|
+
* - agent_metric_watermarks cascade delete
|
|
140
|
+
* - custom_expert_roles hard delete (custom_expert_actions cascade via FK)
|
|
141
|
+
* - invitations.created_by rows deleted (codes the user issued)
|
|
142
|
+
* - invitations.used_by set NULL (preserve history of *who consumed what* — but we lose the link)
|
|
143
|
+
* - users row deleted last
|
|
144
|
+
*
|
|
145
|
+
* Caller is responsible for revoking JWT sessions (we don't import the
|
|
146
|
+
* session store from here to keep this layer pure).
|
|
147
|
+
*/
|
|
148
|
+
deleteUser(userId) {
|
|
149
|
+
// No inner try/catch — any failure must bubble out so the transaction
|
|
150
|
+
// rolls back. A partial delete (e.g. users row gone but daily_stats
|
|
151
|
+
// orphaned) is worse than the operation failing outright.
|
|
152
|
+
const run = transaction((id) => {
|
|
153
|
+
stmts.deleteIdentitiesForUser.run(id);
|
|
154
|
+
stmts.deleteUserSessionsByUser.run(id);
|
|
155
|
+
stmts.deleteUserStats.run(id);
|
|
156
|
+
stmts.deleteDailyStatsForUser.run(id);
|
|
157
|
+
stmts.deleteCustomExpertRolesForUser.run(id);
|
|
158
|
+
stmts.deleteInvitationsCreatedBy.run(id);
|
|
159
|
+
stmts.clearInvitationUsedBy.run(id);
|
|
160
|
+
const result = stmts.deleteUserById.run(id);
|
|
161
|
+
return result.changes > 0;
|
|
162
|
+
});
|
|
163
|
+
return run(userId);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import db from './connection.js';
|
|
2
|
+
import { stmts, transaction } from './connection.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get today's date string in YYYY-MM-DD format (local time).
|
|
6
|
+
*/
|
|
7
|
+
function todayStr() {
|
|
8
|
+
const d = new Date();
|
|
9
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get the start date string for a given period.
|
|
14
|
+
*/
|
|
15
|
+
function periodStartDate(period) {
|
|
16
|
+
const now = new Date();
|
|
17
|
+
switch (period) {
|
|
18
|
+
case 'today':
|
|
19
|
+
return todayStr();
|
|
20
|
+
case 'week': {
|
|
21
|
+
const d = new Date(now);
|
|
22
|
+
d.setDate(d.getDate() - 6); // last 7 days
|
|
23
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
24
|
+
}
|
|
25
|
+
case 'month': {
|
|
26
|
+
const d = new Date(now);
|
|
27
|
+
d.setDate(d.getDate() - 29); // last 30 days
|
|
28
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
29
|
+
}
|
|
30
|
+
default: // 'all'
|
|
31
|
+
return '1970-01-01';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const userStatsDb = {
|
|
36
|
+
/**
|
|
37
|
+
* Batch flush in-memory deltas to DB.
|
|
38
|
+
* Writes to both user_stats (cumulative) and daily_stats (per-day).
|
|
39
|
+
* @param {Map<string, {requests: number, bytesSent: number, bytesReceived: number, messages: number, sessions: number}>} deltaMap
|
|
40
|
+
*/
|
|
41
|
+
flushDeltas(deltaMap) {
|
|
42
|
+
if (deltaMap.size === 0) return;
|
|
43
|
+
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
const today = todayStr();
|
|
46
|
+
const flush = transaction(() => {
|
|
47
|
+
for (const [userId, delta] of deltaMap) {
|
|
48
|
+
// Cumulative stats
|
|
49
|
+
stmts.upsertUserStats.run(
|
|
50
|
+
userId,
|
|
51
|
+
delta.messages || 0,
|
|
52
|
+
delta.sessions || 0,
|
|
53
|
+
delta.requests || 0,
|
|
54
|
+
delta.bytesSent || 0,
|
|
55
|
+
delta.bytesReceived || 0,
|
|
56
|
+
delta.inputTokens || 0,
|
|
57
|
+
delta.outputTokens || 0,
|
|
58
|
+
delta.cacheReadTokens || 0,
|
|
59
|
+
delta.cacheWriteTokens || 0,
|
|
60
|
+
delta.totalTokens || 0,
|
|
61
|
+
now
|
|
62
|
+
);
|
|
63
|
+
// Daily stats
|
|
64
|
+
stmts.upsertDailyStats.run(
|
|
65
|
+
userId,
|
|
66
|
+
today,
|
|
67
|
+
delta.messages || 0,
|
|
68
|
+
delta.sessions || 0,
|
|
69
|
+
delta.requests || 0,
|
|
70
|
+
delta.bytesSent || 0,
|
|
71
|
+
delta.bytesReceived || 0,
|
|
72
|
+
delta.inputTokens || 0,
|
|
73
|
+
delta.outputTokens || 0,
|
|
74
|
+
delta.cacheReadTokens || 0,
|
|
75
|
+
delta.cacheWriteTokens || 0,
|
|
76
|
+
delta.totalTokens || 0
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
flush();
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
getAll() {
|
|
84
|
+
return stmts.getUserStats.all();
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get user stats aggregated by period.
|
|
89
|
+
* @param {'today'|'week'|'month'|'all'} period
|
|
90
|
+
*/
|
|
91
|
+
getByPeriod(period) {
|
|
92
|
+
if (period === 'all') {
|
|
93
|
+
return this.getAll();
|
|
94
|
+
}
|
|
95
|
+
const startDate = periodStartDate(period);
|
|
96
|
+
return stmts.getDailyStatsAll.all(startDate);
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
getByUserId(userId) {
|
|
100
|
+
return stmts.getUserStatsById.get(userId) || null;
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Convert one cumulative agent snapshot into an idempotent user delta.
|
|
105
|
+
* A new metric epoch means the agent process restarted and counters began at
|
|
106
|
+
* zero. Repeated or lower snapshots in the same epoch never add usage.
|
|
107
|
+
*/
|
|
108
|
+
recordAgentTokenSnapshot(userId, agentInstanceId, metrics = {}) {
|
|
109
|
+
const metricEpoch = typeof metrics.metricEpoch === 'string' ? metrics.metricEpoch : '';
|
|
110
|
+
if (!userId || !agentInstanceId || !metricEpoch) return null;
|
|
111
|
+
|
|
112
|
+
const fields = [
|
|
113
|
+
['inputTokens', 'input_tokens'],
|
|
114
|
+
['outputTokens', 'output_tokens'],
|
|
115
|
+
['cacheReadTokens', 'cache_read_tokens'],
|
|
116
|
+
['cacheWriteTokens', 'cache_write_tokens'],
|
|
117
|
+
['totalTokens', 'total_tokens'],
|
|
118
|
+
];
|
|
119
|
+
const current = {};
|
|
120
|
+
for (const [metricKey] of fields) {
|
|
121
|
+
current[metricKey] = Math.max(0, Number(metrics[metricKey]) || 0);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
return transaction(() => {
|
|
126
|
+
const previous = stmts.getAgentMetricWatermark.get(userId, agentInstanceId, metricEpoch);
|
|
127
|
+
const delta = {};
|
|
128
|
+
const watermark = {};
|
|
129
|
+
for (const [metricKey, column] of fields) {
|
|
130
|
+
const previousValue = Number(previous?.[column]) || 0;
|
|
131
|
+
delta[metricKey] = Math.max(0, current[metricKey] - previousValue);
|
|
132
|
+
watermark[metricKey] = Math.max(previousValue, current[metricKey]);
|
|
133
|
+
}
|
|
134
|
+
stmts.upsertAgentMetricWatermark.run(
|
|
135
|
+
userId,
|
|
136
|
+
agentInstanceId,
|
|
137
|
+
metricEpoch,
|
|
138
|
+
watermark.inputTokens,
|
|
139
|
+
watermark.outputTokens,
|
|
140
|
+
watermark.cacheReadTokens,
|
|
141
|
+
watermark.cacheWriteTokens,
|
|
142
|
+
watermark.totalTokens,
|
|
143
|
+
now
|
|
144
|
+
);
|
|
145
|
+
if (Object.values(delta).some(value => value > 0)) {
|
|
146
|
+
stmts.upsertUserStats.run(
|
|
147
|
+
userId, 0, 0, 0, 0, 0,
|
|
148
|
+
delta.inputTokens,
|
|
149
|
+
delta.outputTokens,
|
|
150
|
+
delta.cacheReadTokens,
|
|
151
|
+
delta.cacheWriteTokens,
|
|
152
|
+
delta.totalTokens,
|
|
153
|
+
now
|
|
154
|
+
);
|
|
155
|
+
stmts.upsertDailyStats.run(
|
|
156
|
+
userId, todayStr(), 0, 0, 0, 0, 0,
|
|
157
|
+
delta.inputTokens,
|
|
158
|
+
delta.outputTokens,
|
|
159
|
+
delta.cacheReadTokens,
|
|
160
|
+
delta.cacheWriteTokens,
|
|
161
|
+
delta.totalTokens
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
return delta;
|
|
165
|
+
})();
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
getDashboardTotals() {
|
|
169
|
+
return stmts.getDashboardTotals.get();
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
getDashboardTokenTotals() {
|
|
173
|
+
return stmts.getDashboardTokenTotals.get();
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
getTodayActiveUsers() {
|
|
177
|
+
const row = stmts.getTodayActiveUsers.get(todayStr());
|
|
178
|
+
return row?.count || 0;
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
getTodayMessages() {
|
|
182
|
+
const row = stmts.getTodayMessages.get(todayStr());
|
|
183
|
+
return row?.count || 0;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* yeaft-session-db.js — server-side persistence for yeaft sessions.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors session-db.js but for the yeaft engine's per-user multi-VP
|
|
5
|
+
* sessions. The agent owns the canonical session state on disk (under
|
|
6
|
+
* `~/.yeaft/sessions/<id>/`); this table is a shadow registry the
|
|
7
|
+
* server uses to:
|
|
8
|
+
*
|
|
9
|
+
* 1. Show the user's yeaft sessions in the sidebar BEFORE any agent
|
|
10
|
+
* comes online (so a reload doesn't blank the list).
|
|
11
|
+
* 2. List sessions across ALL of the user's agents in one place
|
|
12
|
+
* (cross-agent unified sidebar) — same way chat conversations
|
|
13
|
+
* pull from the `sessions` table regardless of which agent is
|
|
14
|
+
* currently selected.
|
|
15
|
+
*
|
|
16
|
+
* Rows are upserted whenever the agent emits a `group_list_updated`
|
|
17
|
+
* snapshot (full state) or a `group_roster_changed` delta. They are
|
|
18
|
+
* deleted when the agent emits a `session_crud_result` with op=delete
|
|
19
|
+
* or op=archive.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { stmts } from './connection.js';
|
|
23
|
+
|
|
24
|
+
function safeJsonParse(s, fallback) {
|
|
25
|
+
if (s == null || s === '') return fallback;
|
|
26
|
+
try { return JSON.parse(s); }
|
|
27
|
+
catch (_) { return fallback; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function safeJsonStringify(v) {
|
|
31
|
+
if (v == null) return null;
|
|
32
|
+
try { return JSON.stringify(v); }
|
|
33
|
+
catch (_) { return null; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sessionOrderKey(agentId, sessionId) {
|
|
37
|
+
return `${agentId || ''}\u001f${sessionId || ''}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function mapRow(row) {
|
|
41
|
+
if (!row) return row;
|
|
42
|
+
return {
|
|
43
|
+
id: row.id,
|
|
44
|
+
userId: row.user_id || null,
|
|
45
|
+
agentId: row.agent_id,
|
|
46
|
+
name: row.name || row.id,
|
|
47
|
+
roster: safeJsonParse(row.roster_json, []),
|
|
48
|
+
defaultVpId: row.default_vp_id || null,
|
|
49
|
+
workDir: row.work_dir || '',
|
|
50
|
+
config: safeJsonParse(row.config_json, {}),
|
|
51
|
+
announcement: row.announcement || '',
|
|
52
|
+
createdAt: row.created_at || null,
|
|
53
|
+
updatedAt: row.updated_at,
|
|
54
|
+
isArchived: row.is_archived === 1,
|
|
55
|
+
// fix-yeaft-session-list-and-menu: persisted pin state. Decorated
|
|
56
|
+
// onto outgoing snapshots in server/handlers/agent-output.js so the
|
|
57
|
+
// web sees `pinned: true/false` on each session row.
|
|
58
|
+
// Keep `isPinned` for existing server-side callers, and expose `pinned`
|
|
59
|
+
// because the web session store consumes neutral session metadata using
|
|
60
|
+
// that field name during DB hydration.
|
|
61
|
+
isPinned: row.is_pinned === 1,
|
|
62
|
+
pinned: row.is_pinned === 1,
|
|
63
|
+
sortOrder: Number.isFinite(row.sort_order) ? row.sort_order : null,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const yeaftSessionDb = {
|
|
68
|
+
/**
|
|
69
|
+
* Upsert one session from a snapshot row. Returns nothing.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} userId
|
|
72
|
+
* @param {string} agentId
|
|
73
|
+
* @param {object} session Shape from `web-bridge.snapshotSessions()`:
|
|
74
|
+
* { id, name, roster[], defaultVpId, workDir, config, announcement, createdAt }
|
|
75
|
+
*/
|
|
76
|
+
upsertFromSnapshot(userId, agentId, session) {
|
|
77
|
+
if (!session || !session.id || !agentId) return;
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
stmts.upsertYeaftSession.run(
|
|
80
|
+
session.id,
|
|
81
|
+
userId || null,
|
|
82
|
+
agentId,
|
|
83
|
+
session.name || session.id,
|
|
84
|
+
safeJsonStringify(Array.isArray(session.roster) ? session.roster : []),
|
|
85
|
+
session.defaultVpId || null,
|
|
86
|
+
session.workDir || '',
|
|
87
|
+
safeJsonStringify(session.config && typeof session.config === 'object' ? session.config : {}),
|
|
88
|
+
typeof session.announcement === 'string' ? session.announcement : '',
|
|
89
|
+
session.createdAt || now,
|
|
90
|
+
now,
|
|
91
|
+
0,
|
|
92
|
+
);
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Bulk reconciliation from a full snapshot. Sessions not present in
|
|
97
|
+
* the incoming array (but currently in the DB for this user+agent)
|
|
98
|
+
* are deleted — the agent has authoritatively said "these are my
|
|
99
|
+
* sessions right now". Behaviour matches the web `applySnapshot`
|
|
100
|
+
* per-agent replacement so server + client stay in sync.
|
|
101
|
+
*
|
|
102
|
+
* @param {string} userId
|
|
103
|
+
* @param {string} agentId
|
|
104
|
+
* @param {object[]} sessions
|
|
105
|
+
*/
|
|
106
|
+
reconcileFromSnapshot(userId, agentId, sessions) {
|
|
107
|
+
const arr = Array.isArray(sessions) ? sessions : [];
|
|
108
|
+
const incomingIds = new Set();
|
|
109
|
+
for (const s of arr) {
|
|
110
|
+
if (s && s.id) {
|
|
111
|
+
incomingIds.add(s.id);
|
|
112
|
+
this.upsertFromSnapshot(userId, agentId, s);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Drop rows for this (user, agent) not in the snapshot.
|
|
116
|
+
const existing = stmts.getYeaftSessionsByAgent.all(agentId);
|
|
117
|
+
for (const row of existing) {
|
|
118
|
+
// Only touch rows belonging to this user — guards against
|
|
119
|
+
// accidentally wiping another user's rows if agent ownership is
|
|
120
|
+
// ever shared (currently it isn't, but be defensive).
|
|
121
|
+
// Treat NULL user_id as foreign too — never cross-delete.
|
|
122
|
+
if (userId && row.user_id !== userId) continue;
|
|
123
|
+
if (!incomingIds.has(row.id)) {
|
|
124
|
+
stmts.deleteYeaftSessionForAgent.run(row.id, userId, agentId);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
/** All non-archived rows for this user (across all agents). */
|
|
130
|
+
getByUser(userId) {
|
|
131
|
+
if (!userId) return [];
|
|
132
|
+
return stmts.getYeaftSessionsByUser.all(userId).map(mapRow);
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
getByAgent(agentId) {
|
|
136
|
+
return stmts.getYeaftSessionsByAgent.all(agentId).map(mapRow);
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
get(id) {
|
|
140
|
+
return mapRow(stmts.getYeaftSession.get(id));
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
getForAgent(userId, agentId, id) {
|
|
144
|
+
if (!userId || !agentId || !id) return null;
|
|
145
|
+
return mapRow(stmts.getYeaftSessionForAgent.get(id, userId, agentId));
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
delete(id) {
|
|
149
|
+
stmts.deleteYeaftSession.run(id);
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
deleteForAgent(userId, agentId, id) {
|
|
153
|
+
if (!userId || !agentId || !id) return;
|
|
154
|
+
stmts.deleteYeaftSessionForAgent.run(id, userId, agentId);
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
setArchived(id, archived) {
|
|
158
|
+
stmts.setYeaftSessionArchived.run(archived ? 1 : 0, Date.now(), id);
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
setArchivedForAgent(userId, agentId, id, archived) {
|
|
162
|
+
if (!userId || !agentId || !id) return;
|
|
163
|
+
stmts.setYeaftSessionArchivedForAgent.run(archived ? 1 : 0, Date.now(), id, userId, agentId);
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Persist the per-session pin state. fix-yeaft-session-list-and-menu:
|
|
168
|
+
* yeaft sidebar's "置顶" menu item flips this — the existing
|
|
169
|
+
* `pin_session` / `unpin_session` WebSocket handler in
|
|
170
|
+
* server/handlers/client-conversation.js detects yeaft-owned ids and
|
|
171
|
+
* routes here (chat-owned ids fall back to sessionDb.setPinned).
|
|
172
|
+
*
|
|
173
|
+
* upsertFromSnapshot does NOT touch is_pinned (the snapshot is
|
|
174
|
+
* agent-authored and the agent has no notion of pin); the column is
|
|
175
|
+
* preserved across snapshot upserts naturally because is_pinned is
|
|
176
|
+
* absent from the ON CONFLICT update set in connection.js.
|
|
177
|
+
*/
|
|
178
|
+
setPinned(id, pinned) {
|
|
179
|
+
stmts.setYeaftSessionPinned.run(pinned ? 1 : 0, Date.now(), id);
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
setPinnedForAgentRow(userId, agentId, id, pinned) {
|
|
183
|
+
if (!userId || !agentId || !id) return;
|
|
184
|
+
stmts.setYeaftSessionPinnedForAgent.run(pinned ? 1 : 0, Date.now(), id, userId, agentId);
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
setOrderForAgent(userId, agentId, sessionIds) {
|
|
188
|
+
if (!userId || !agentId || !Array.isArray(sessionIds)) return false;
|
|
189
|
+
const owned = new Set(
|
|
190
|
+
stmts.getYeaftSessionsByAgent.all(agentId)
|
|
191
|
+
.filter(row => row && row.user_id === userId)
|
|
192
|
+
.map(row => row.id),
|
|
193
|
+
);
|
|
194
|
+
const seen = new Set();
|
|
195
|
+
const ordered = [];
|
|
196
|
+
for (const rawId of sessionIds) {
|
|
197
|
+
const id = typeof rawId === 'string' ? rawId : String(rawId || '');
|
|
198
|
+
if (!id || seen.has(id) || !owned.has(id)) continue;
|
|
199
|
+
seen.add(id);
|
|
200
|
+
ordered.push(id);
|
|
201
|
+
}
|
|
202
|
+
if (ordered.length === 0) return false;
|
|
203
|
+
const now = Date.now();
|
|
204
|
+
ordered.forEach((id, index) => {
|
|
205
|
+
stmts.setYeaftSessionSortOrder.run(index, now, id, userId, agentId);
|
|
206
|
+
});
|
|
207
|
+
return true;
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
setOrderForUser(userId, sessions) {
|
|
211
|
+
if (!userId || !Array.isArray(sessions)) return false;
|
|
212
|
+
const owned = new Set(
|
|
213
|
+
stmts.getYeaftSessionsByUser.all(userId)
|
|
214
|
+
.filter(row => row && row.user_id === userId && row.agent_id && row.id)
|
|
215
|
+
.map(row => sessionOrderKey(row.agent_id, row.id)),
|
|
216
|
+
);
|
|
217
|
+
const seen = new Set();
|
|
218
|
+
const ordered = [];
|
|
219
|
+
for (const item of sessions) {
|
|
220
|
+
const agentId = typeof item?.agentId === 'string' ? item.agentId : '';
|
|
221
|
+
const sessionId = typeof item?.sessionId === 'string'
|
|
222
|
+
? item.sessionId
|
|
223
|
+
: (typeof item?.id === 'string' ? item.id : '');
|
|
224
|
+
const key = sessionOrderKey(agentId, sessionId);
|
|
225
|
+
if (!agentId || !sessionId || seen.has(key) || !owned.has(key)) continue;
|
|
226
|
+
seen.add(key);
|
|
227
|
+
ordered.push({ agentId, sessionId });
|
|
228
|
+
}
|
|
229
|
+
if (ordered.length === 0) return false;
|
|
230
|
+
const now = Date.now();
|
|
231
|
+
ordered.forEach(({ agentId, sessionId }, index) => {
|
|
232
|
+
stmts.setYeaftSessionSortOrder.run(index, now, sessionId, userId, agentId);
|
|
233
|
+
});
|
|
234
|
+
return true;
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Persist pin state for a Yeaft Session even if the agent snapshot has not
|
|
239
|
+
* reached the server-side shadow table yet. This keeps the UI pin action
|
|
240
|
+
* durable across reloads in the bootstrap race where the user pins a row
|
|
241
|
+
* that exists in the web/agent session list but not in `yeaft_sessions`.
|
|
242
|
+
*/
|
|
243
|
+
setPinnedForAgent(userId, agentId, session, pinned) {
|
|
244
|
+
const id = typeof session === 'string' ? session : session?.id;
|
|
245
|
+
if (!id || !agentId) return false;
|
|
246
|
+
const existing = this.getForAgent(userId, agentId, id);
|
|
247
|
+
if (existing && existing.userId && userId && existing.userId !== userId) return false;
|
|
248
|
+
if (!existing) {
|
|
249
|
+
this.upsertFromSnapshot(userId, agentId, {
|
|
250
|
+
...(session && typeof session === 'object' ? session : {}),
|
|
251
|
+
id,
|
|
252
|
+
name: (session && typeof session === 'object' && session.name) ? session.name : id,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
this.setPinnedForAgentRow(userId, agentId, id, pinned);
|
|
256
|
+
return true;
|
|
257
|
+
},
|
|
258
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import nodemailer from 'nodemailer';
|
|
2
|
+
import { CONFIG } from './config.js';
|
|
3
|
+
|
|
4
|
+
let transporter = null;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get or create email transporter
|
|
8
|
+
* @returns {nodemailer.Transporter}
|
|
9
|
+
*/
|
|
10
|
+
function getTransporter() {
|
|
11
|
+
if (!transporter) {
|
|
12
|
+
transporter = nodemailer.createTransport({
|
|
13
|
+
host: CONFIG.smtp.host,
|
|
14
|
+
port: CONFIG.smtp.port,
|
|
15
|
+
secure: CONFIG.smtp.secure,
|
|
16
|
+
auth: {
|
|
17
|
+
user: CONFIG.smtp.user,
|
|
18
|
+
pass: CONFIG.smtp.pass
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return transporter;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Send verification code email
|
|
27
|
+
* @param {string} to - Recipient email address
|
|
28
|
+
* @param {string} code - Verification code
|
|
29
|
+
* @param {string} username - Username for personalization
|
|
30
|
+
* @returns {Promise<void>}
|
|
31
|
+
*/
|
|
32
|
+
export async function sendVerificationCode(to, code, username) {
|
|
33
|
+
const transport = getTransporter();
|
|
34
|
+
|
|
35
|
+
const htmlContent = `
|
|
36
|
+
<!DOCTYPE html>
|
|
37
|
+
<html>
|
|
38
|
+
<head>
|
|
39
|
+
<meta charset="utf-8">
|
|
40
|
+
<style>
|
|
41
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
|
|
42
|
+
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
43
|
+
.code { font-size: 32px; font-weight: bold; letter-spacing: 8px; color: #2563eb; padding: 20px; background: #f0f9ff; border-radius: 8px; text-align: center; margin: 20px 0; }
|
|
44
|
+
.footer { color: #6b7280; font-size: 12px; margin-top: 20px; }
|
|
45
|
+
</style>
|
|
46
|
+
</head>
|
|
47
|
+
<body>
|
|
48
|
+
<div class="container">
|
|
49
|
+
<h2>WebChat Login Verification</h2>
|
|
50
|
+
<p>Hello ${username},</p>
|
|
51
|
+
<p>Your verification code is:</p>
|
|
52
|
+
<div class="code">${code}</div>
|
|
53
|
+
<p>This code will expire in 5 minutes.</p>
|
|
54
|
+
<p>If you did not request this code, please ignore this email.</p>
|
|
55
|
+
<div class="footer">
|
|
56
|
+
<p>This email was sent by WebChat. Do not reply to this email.</p>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</body>
|
|
60
|
+
</html>
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
const textContent = `
|
|
64
|
+
WebChat Login Verification
|
|
65
|
+
|
|
66
|
+
Hello ${username},
|
|
67
|
+
|
|
68
|
+
Your verification code is: ${code}
|
|
69
|
+
|
|
70
|
+
This code will expire in 5 minutes.
|
|
71
|
+
|
|
72
|
+
If you did not request this code, please ignore this email.
|
|
73
|
+
`.trim();
|
|
74
|
+
|
|
75
|
+
await transport.sendMail({
|
|
76
|
+
from: CONFIG.smtp.from,
|
|
77
|
+
to,
|
|
78
|
+
subject: `WebChat Verification Code: ${code}`,
|
|
79
|
+
text: textContent,
|
|
80
|
+
html: htmlContent
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Test email configuration
|
|
86
|
+
* @returns {Promise<{success: boolean, error?: string}>}
|
|
87
|
+
*/
|
|
88
|
+
export async function testEmailConfig() {
|
|
89
|
+
try {
|
|
90
|
+
const transport = getTransporter();
|
|
91
|
+
await transport.verify();
|
|
92
|
+
return { success: true };
|
|
93
|
+
} catch (err) {
|
|
94
|
+
return { success: false, error: err.message };
|
|
95
|
+
}
|
|
96
|
+
}
|