@yeaft/webchat-agent 1.0.188 → 1.0.190

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.
Files changed (121) hide show
  1. package/cli.js +12 -0
  2. package/index.js +10 -2
  3. package/local-run.js +218 -0
  4. package/local-runtime/server/.env.example +54 -0
  5. package/local-runtime/server/api.js +111 -0
  6. package/local-runtime/server/auth/aad.js +235 -0
  7. package/local-runtime/server/auth/login.js +156 -0
  8. package/local-runtime/server/auth/oauth-flow.js +277 -0
  9. package/local-runtime/server/auth/password-reset.js +134 -0
  10. package/local-runtime/server/auth/providers/alipay.js +125 -0
  11. package/local-runtime/server/auth/providers/github.js +82 -0
  12. package/local-runtime/server/auth/providers/google.js +60 -0
  13. package/local-runtime/server/auth/providers/microsoft.js +71 -0
  14. package/local-runtime/server/auth/providers/types.js +57 -0
  15. package/local-runtime/server/auth/providers/wechat.js +68 -0
  16. package/local-runtime/server/auth/register.js +91 -0
  17. package/local-runtime/server/auth/session-store.js +57 -0
  18. package/local-runtime/server/auth/token.js +85 -0
  19. package/local-runtime/server/auth/totp-auth.js +133 -0
  20. package/local-runtime/server/auth/utils.js +42 -0
  21. package/local-runtime/server/auth.js +8 -0
  22. package/local-runtime/server/check-node-version.js +74 -0
  23. package/local-runtime/server/config.js +298 -0
  24. package/local-runtime/server/context.js +140 -0
  25. package/local-runtime/server/create-user.js +59 -0
  26. package/local-runtime/server/database.js +12 -0
  27. package/local-runtime/server/db/connection.js +963 -0
  28. package/local-runtime/server/db/expert-db.js +171 -0
  29. package/local-runtime/server/db/identity-db.js +92 -0
  30. package/local-runtime/server/db/invitation-db.js +38 -0
  31. package/local-runtime/server/db/message-db.js +257 -0
  32. package/local-runtime/server/db/session-db.js +118 -0
  33. package/local-runtime/server/db/user-db.js +165 -0
  34. package/local-runtime/server/db/user-stats-db.js +185 -0
  35. package/local-runtime/server/db/yeaft-session-db.js +258 -0
  36. package/local-runtime/server/email.js +96 -0
  37. package/local-runtime/server/encryption.js +105 -0
  38. package/local-runtime/server/handlers/agent-conversation.js +347 -0
  39. package/local-runtime/server/handlers/agent-file-terminal.js +99 -0
  40. package/local-runtime/server/handlers/agent-output.js +854 -0
  41. package/local-runtime/server/handlers/agent-sync.js +399 -0
  42. package/local-runtime/server/handlers/agent-work-center.js +27 -0
  43. package/local-runtime/server/handlers/client-conversation.js +1182 -0
  44. package/local-runtime/server/handlers/client-misc.js +254 -0
  45. package/local-runtime/server/handlers/client-work-center.js +269 -0
  46. package/local-runtime/server/handlers/client-workbench.js +146 -0
  47. package/local-runtime/server/handlers/session-pin-router.js +61 -0
  48. package/local-runtime/server/heartbeat-policy.js +46 -0
  49. package/local-runtime/server/index.js +275 -0
  50. package/local-runtime/server/package.json +55 -0
  51. package/local-runtime/server/perf-trace.js +154 -0
  52. package/local-runtime/server/proxy.js +273 -0
  53. package/local-runtime/server/routes/admin-routes.js +207 -0
  54. package/local-runtime/server/routes/auth-routes.js +322 -0
  55. package/local-runtime/server/routes/expert-routes.js +117 -0
  56. package/local-runtime/server/routes/invitation-routes.js +60 -0
  57. package/local-runtime/server/routes/session-routes.js +112 -0
  58. package/local-runtime/server/routes/upload-routes.js +109 -0
  59. package/local-runtime/server/routes/user-routes.js +241 -0
  60. package/local-runtime/server/totp.js +74 -0
  61. package/local-runtime/server/work-item-attachment-policy.js +56 -0
  62. package/local-runtime/server/ws-agent.js +319 -0
  63. package/local-runtime/server/ws-client.js +214 -0
  64. package/local-runtime/server/ws-utils.js +394 -0
  65. package/local-runtime/server/yeaft-asset-store.js +339 -0
  66. package/local-runtime/version.json +1 -0
  67. package/local-runtime/web/app.bundle.js +7673 -0
  68. package/local-runtime/web/app.bundle.js.gz +0 -0
  69. package/local-runtime/web/assets/avatars/README.md +34 -0
  70. package/local-runtime/web/assets/avatars/ada.svg +1 -0
  71. package/local-runtime/web/assets/avatars/alan.svg +1 -0
  72. package/local-runtime/web/assets/avatars/alice.svg +1 -0
  73. package/local-runtime/web/assets/avatars/anders.svg +1 -0
  74. package/local-runtime/web/assets/avatars/bezos.svg +1 -0
  75. package/local-runtime/web/assets/avatars/borges.svg +1 -0
  76. package/local-runtime/web/assets/avatars/buffett.svg +1 -0
  77. package/local-runtime/web/assets/avatars/clausewitz.svg +1 -0
  78. package/local-runtime/web/assets/avatars/dalio.svg +1 -0
  79. package/local-runtime/web/assets/avatars/dieter.svg +1 -0
  80. package/local-runtime/web/assets/avatars/drucker.svg +1 -0
  81. package/local-runtime/web/assets/avatars/einstein.svg +1 -0
  82. package/local-runtime/web/assets/avatars/grace.svg +1 -0
  83. package/local-runtime/web/assets/avatars/harari.svg +1 -0
  84. package/local-runtime/web/assets/avatars/jung.svg +1 -0
  85. package/local-runtime/web/assets/avatars/kahneman.svg +1 -0
  86. package/local-runtime/web/assets/avatars/ken.svg +1 -0
  87. package/local-runtime/web/assets/avatars/kongzi.svg +1 -0
  88. package/local-runtime/web/assets/avatars/kubrick.svg +1 -0
  89. package/local-runtime/web/assets/avatars/linus.svg +1 -0
  90. package/local-runtime/web/assets/avatars/luxun.svg +1 -0
  91. package/local-runtime/web/assets/avatars/margaret.svg +1 -0
  92. package/local-runtime/web/assets/avatars/martin.svg +1 -0
  93. package/local-runtime/web/assets/avatars/miyazaki.svg +1 -0
  94. package/local-runtime/web/assets/avatars/munger.svg +1 -0
  95. package/local-runtime/web/assets/avatars/nietzsche.svg +1 -0
  96. package/local-runtime/web/assets/avatars/norman.svg +1 -0
  97. package/local-runtime/web/assets/avatars/shannon.svg +1 -0
  98. package/local-runtime/web/assets/avatars/simaqian.svg +1 -0
  99. package/local-runtime/web/assets/avatars/socrates.svg +1 -0
  100. package/local-runtime/web/assets/avatars/steve.svg +1 -0
  101. package/local-runtime/web/assets/avatars/sudongpo.svg +1 -0
  102. package/local-runtime/web/assets/avatars/sunzi.svg +1 -0
  103. package/local-runtime/web/docx-preview.min.js +2 -0
  104. package/local-runtime/web/docx-preview.min.js.gz +0 -0
  105. package/local-runtime/web/html-to-image.min.js +2 -0
  106. package/local-runtime/web/html-to-image.min.js.gz +0 -0
  107. package/local-runtime/web/index.html +21 -0
  108. package/local-runtime/web/jszip.min.js +13 -0
  109. package/local-runtime/web/jszip.min.js.gz +0 -0
  110. package/local-runtime/web/mermaid.min.js +2843 -0
  111. package/local-runtime/web/mermaid.min.js.gz +0 -0
  112. package/local-runtime/web/msal-browser.min.js +69 -0
  113. package/local-runtime/web/msal-browser.min.js.gz +0 -0
  114. package/local-runtime/web/style.bundle.css +10 -0
  115. package/local-runtime/web/style.bundle.css.gz +0 -0
  116. package/local-runtime/web/vendor.bundle.js +1522 -0
  117. package/local-runtime/web/vendor.bundle.js.gz +0 -0
  118. package/local-runtime/web/xlsx.min.js +24 -0
  119. package/local-runtime/web/xlsx.min.js.gz +0 -0
  120. package/package.json +13 -3
  121. package/scripts/prepare-local-runtime.js +25 -0
@@ -0,0 +1,322 @@
1
+ import { CONFIG, isEmailConfigured, isTotpEnabled, isAadEnabled, getEnabledSsoProviders, getUserByUsername } from '../config.js';
2
+ import { loginStep1, loginStep2, logout, verifyTotpStep, completeTotpSetup, register, loginWithAad } from '../auth.js';
3
+ import { buildAuthorizeUrl, handleCallback, peekStateMode, storePendingResult, consumePendingResult } from '../auth/oauth-flow.js';
4
+ import { verifyToken } from '../auth/token.js';
5
+ import { requestPasswordReset, verifyPasswordReset } from '../auth/password-reset.js';
6
+ import { identityDb, userDb } from '../database.js';
7
+
8
+ /**
9
+ * Register authentication-related API routes.
10
+ */
11
+ export function registerAuthRoutes(app, { requireAuth, checkRateLimit }) {
12
+ app.get('/api/auth/mode', (req, res) => {
13
+ const aadEnabled = isAadEnabled();
14
+ const ssoProviders = getEnabledSsoProviders();
15
+ res.json({
16
+ skipAuth: CONFIG.skipAuth,
17
+ emailVerification: isEmailConfigured(),
18
+ // Password reset shares the email infrastructure — gate the UI on this.
19
+ passwordResetEnabled: isEmailConfigured(),
20
+ totpEnabled: isTotpEnabled(),
21
+ registrationEnabled: !CONFIG.skipAuth,
22
+ aadEnabled,
23
+ ...(aadEnabled && {
24
+ aadClientId: CONFIG.aad.clientId,
25
+ aadTenantId: CONFIG.aad.tenantId
26
+ }),
27
+ // Server-driven SSO providers (each one handled via /api/auth/sso/:provider/start).
28
+ ssoProviders
29
+ });
30
+ });
31
+
32
+ app.post('/api/auth/login', async (req, res) => {
33
+ if (!checkRateLimit(req.ip)) {
34
+ return res.status(429).json({ success: false, error: 'Too many login attempts, please try again later' });
35
+ }
36
+ const { username, password } = req.body;
37
+ if (!username || !password) {
38
+ return res.status(400).json({ success: false, error: 'Username and password are required' });
39
+ }
40
+ try {
41
+ const result = await loginStep1(username, password);
42
+ res.json(result);
43
+ } catch (err) {
44
+ console.error('Login error:', err);
45
+ res.status(500).json({ success: false, error: 'Internal server error' });
46
+ }
47
+ });
48
+
49
+ app.post('/api/auth/verify', (req, res) => {
50
+ if (!checkRateLimit(req.ip)) {
51
+ return res.status(429).json({ success: false, error: 'Too many attempts, please try again later' });
52
+ }
53
+ const { tempToken, code } = req.body;
54
+ if (!tempToken || !code) {
55
+ return res.status(400).json({ success: false, error: 'Token and code are required' });
56
+ }
57
+ const result = loginStep2(tempToken, code);
58
+ res.json(result);
59
+ });
60
+
61
+ app.post('/api/auth/logout', (req, res) => {
62
+ const token = req.headers.authorization?.replace('Bearer ', '');
63
+ if (token) {
64
+ logout(token);
65
+ }
66
+ res.json({ success: true });
67
+ });
68
+
69
+ app.post('/api/auth/verify-totp', async (req, res) => {
70
+ if (!checkRateLimit(req.ip)) {
71
+ return res.status(429).json({ success: false, error: 'Too many attempts, please try again later' });
72
+ }
73
+ const { tempToken, totpCode } = req.body;
74
+ if (!tempToken || !totpCode) {
75
+ return res.status(400).json({ success: false, error: 'Token and TOTP code are required' });
76
+ }
77
+ try {
78
+ const result = await verifyTotpStep(tempToken, totpCode);
79
+ res.json(result);
80
+ } catch (err) {
81
+ console.error('TOTP verification error:', err);
82
+ res.status(500).json({ success: false, error: 'Internal server error' });
83
+ }
84
+ });
85
+
86
+ app.post('/api/auth/setup-totp', async (req, res) => {
87
+ const { setupToken, totpCode } = req.body;
88
+ if (!setupToken || !totpCode) {
89
+ return res.status(400).json({ success: false, error: 'Setup token and TOTP code are required' });
90
+ }
91
+ try {
92
+ const result = await completeTotpSetup(setupToken, totpCode);
93
+ res.json(result);
94
+ } catch (err) {
95
+ console.error('TOTP setup error:', err);
96
+ res.status(500).json({ success: false, error: 'Internal server error' });
97
+ }
98
+ });
99
+
100
+ // Registration (public - requires invitation code)
101
+ app.post('/api/auth/register', async (req, res) => {
102
+ if (!checkRateLimit(req.ip)) {
103
+ return res.status(429).json({ success: false, error: 'Too many attempts, please try again later' });
104
+ }
105
+ const { username, password, email, invitationCode } = req.body;
106
+ try {
107
+ const result = await register(username, password, email, invitationCode);
108
+ if (!result.success) {
109
+ return res.status(400).json(result);
110
+ }
111
+ res.json(result);
112
+ } catch (err) {
113
+ console.error('Registration error:', err);
114
+ res.status(500).json({ success: false, error: 'Internal server error' });
115
+ }
116
+ });
117
+
118
+ // Password reset — step 1: email me a code
119
+ app.post('/api/auth/password-reset/request', async (req, res) => {
120
+ if (!checkRateLimit(req.ip)) {
121
+ return res.status(429).json({ success: false, error: 'Too many attempts, please try again later' });
122
+ }
123
+ const { email } = req.body || {};
124
+ try {
125
+ const result = await requestPasswordReset(email);
126
+ res.json(result);
127
+ } catch (err) {
128
+ console.error('Password reset request error:', err);
129
+ res.status(500).json({ success: false, error: 'Internal server error' });
130
+ }
131
+ });
132
+
133
+ // Password reset — step 2: verify code, set new password
134
+ app.post('/api/auth/password-reset/verify', async (req, res) => {
135
+ if (!checkRateLimit(req.ip)) {
136
+ return res.status(429).json({ success: false, error: 'Too many attempts, please try again later' });
137
+ }
138
+ const { resetToken, code, newPassword } = req.body || {};
139
+ try {
140
+ const result = await verifyPasswordReset(resetToken, code, newPassword);
141
+ res.json(result);
142
+ } catch (err) {
143
+ console.error('Password reset verify error:', err);
144
+ res.status(500).json({ success: false, error: 'Internal server error' });
145
+ }
146
+ });
147
+
148
+ // Azure AD (Microsoft Entra ID) SSO login
149
+ app.post('/api/auth/aad', async (req, res) => {
150
+ if (!checkRateLimit(req.ip)) {
151
+ return res.status(429).json({ success: false, error: 'Too many attempts, please try again later' });
152
+ }
153
+ const { idToken } = req.body;
154
+ if (!idToken) {
155
+ return res.status(400).json({ success: false, error: 'id_token is required' });
156
+ }
157
+ try {
158
+ const result = await loginWithAad(idToken);
159
+ res.json(result);
160
+ } catch (err) {
161
+ console.error('AAD login error:', err);
162
+ res.status(500).json({ success: false, error: 'Internal server error' });
163
+ }
164
+ });
165
+
166
+ // ─── Server-driven SSO (GitHub / Google / WeChat / Alipay / Microsoft) ────
167
+ // Login flow: /start → 302 to provider → user consents → provider 302 to /callback
168
+ // Bind flow: same, but /start requires Authorization header so callback knows which user
169
+
170
+ app.get('/api/auth/sso/:provider/start', (req, res) => {
171
+ const { provider } = req.params;
172
+ const intent = req.query.intent === 'bind' ? 'bind' : 'login';
173
+ let userId = null;
174
+
175
+ if (intent === 'bind') {
176
+ // Bind requires the caller to be already logged in. The browser navigates
177
+ // here directly so we read the token from the `Authorization` query
178
+ // parameter (frontend hands it over explicitly because top-level navigation
179
+ // can't set headers).
180
+ const token = String(req.query.token || '');
181
+ const ver = verifyToken(token);
182
+ if (!ver.valid) {
183
+ return res.status(401).send('Authentication required to bind an identity');
184
+ }
185
+ const u = userDb.getByUsername(ver.username);
186
+ if (!u) return res.status(401).send('User not found');
187
+ userId = u.id;
188
+ }
189
+
190
+ try {
191
+ const { url } = buildAuthorizeUrl({ provider, intent, userId });
192
+ res.redirect(url);
193
+ } catch (err) {
194
+ console.error(`[SSO ${provider}] start error:`, err.message);
195
+ res.status(400).send(`SSO start failed: ${err.message}`);
196
+ }
197
+ });
198
+
199
+ // QR-flow start: returns the authorize URL + state in JSON instead of
200
+ // redirecting. The PC frontend renders the URL as a QR code, the user scans
201
+ // with their phone, and the PC polls /poll/:state for the result.
202
+ app.get('/api/auth/sso/:provider/start-qr', (req, res) => {
203
+ const { provider } = req.params;
204
+ const intent = req.query.intent === 'bind' ? 'bind' : 'login';
205
+ let userId = null;
206
+ if (intent === 'bind') {
207
+ const token = String(req.query.token || '');
208
+ const ver = verifyToken(token);
209
+ if (!ver.valid) return res.status(401).json({ success: false, error: 'auth required' });
210
+ const u = userDb.getByUsername(ver.username);
211
+ if (!u) return res.status(401).json({ success: false, error: 'user not found' });
212
+ userId = u.id;
213
+ }
214
+ try {
215
+ const { url, state } = buildAuthorizeUrl({ provider, intent, userId, mode: 'qr' });
216
+ res.json({ success: true, authorizeUrl: url, state });
217
+ } catch (err) {
218
+ console.error(`[SSO ${provider}] start-qr error:`, err.message);
219
+ res.status(400).json({ success: false, error: err.message });
220
+ }
221
+ });
222
+
223
+ // QR-flow poll: PC frontend hits this every ~2s with the state issued by
224
+ // /start-qr. Returns { status: 'pending' | 'login' | 'bind' | 'error', ... }.
225
+ app.get('/api/auth/sso/poll/:state', (req, res) => {
226
+ const r = consumePendingResult(req.params.state);
227
+ if (!r) return res.json({ status: 'pending' });
228
+ if (r.kind === 'login') {
229
+ return res.json({ status: 'login', token: r.token, sessionKey: r.sessionKey, role: r.role });
230
+ }
231
+ if (r.kind === 'bind') {
232
+ return res.json({ status: 'bind', provider: r.provider });
233
+ }
234
+ return res.json({ status: 'error', error: r.error || 'SSO failed', code: r.status || 400 });
235
+ });
236
+
237
+ app.get('/api/auth/sso/:provider/callback', async (req, res) => {
238
+ const { provider } = req.params;
239
+ // Alipay's web auth returns the code as `auth_code` (not the OAuth-standard
240
+ // `code`). Accept either to keep one route handling all providers.
241
+ const code = req.query.code || req.query.auth_code;
242
+ const { state } = req.query;
243
+ if (!code || !state) {
244
+ return res.status(400).send('Missing code or state');
245
+ }
246
+ // Was this state issued in QR mode? If so, the PC browser is polling for
247
+ // the result — we park it under the state and show a tiny success page on
248
+ // the device that scanned (typically the user's phone).
249
+ const stateMode = peekStateMode(String(state));
250
+ try {
251
+ const result = await handleCallback({ provider, code: String(code), state: String(state) });
252
+
253
+ if (stateMode === 'qr') {
254
+ storePendingResult(String(state), result);
255
+ if (result.kind === 'error') {
256
+ return res.status(result.status || 400).send(`扫码登录失败: ${result.error}`);
257
+ }
258
+ // Minimal success page for the scanning device. The real session lands
259
+ // on the original PC tab once it finishes polling.
260
+ return res.send(`<!doctype html><meta charset="utf-8"><title>登录成功</title><style>body{font-family:-apple-system,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;color:#222}.box{text-align:center;padding:0 24px}h1{font-size:20px;margin:0 0 8px}p{color:#666;margin:0;line-height:1.5}</style><div class=box><h1>✓ 登录成功</h1><p>请返回原先的浏览器页面,几秒内会自动登录。<br/>本页面可关闭。</p></div>`);
261
+ }
262
+
263
+ if (result.kind === 'login') {
264
+ const params = new URLSearchParams({
265
+ token: result.token,
266
+ sessionKey: result.sessionKey,
267
+ role: result.role
268
+ });
269
+ return res.redirect(`/#/sso-complete?${params.toString()}`);
270
+ }
271
+ if (result.kind === 'bind') {
272
+ return res.redirect(`/#/settings?ssoBound=${encodeURIComponent(provider)}`);
273
+ }
274
+ const status = result.status || 400;
275
+ if (status === 409) {
276
+ return res.redirect(`/#/settings?ssoError=conflict&provider=${encodeURIComponent(provider)}`);
277
+ }
278
+ return res.status(status).send(`SSO error: ${result.error}`);
279
+ } catch (err) {
280
+ console.error(`[SSO ${provider}] callback error:`, err);
281
+ res.status(500).send('Internal server error');
282
+ }
283
+ });
284
+
285
+ // List the current user's bound identities.
286
+ app.get('/api/auth/identities', requireAuth, (req, res) => {
287
+ const u = userDb.getByUsername(req.user.username);
288
+ if (!u) return res.status(404).json({ success: false, error: 'User not found' });
289
+ const rows = identityDb.listForUser(u.id).map(r => ({
290
+ provider: r.provider,
291
+ email: r.email,
292
+ displayName: r.display_name,
293
+ createdAt: Number(r.created_at),
294
+ lastLoginAt: r.last_login_at ? Number(r.last_login_at) : null
295
+ }));
296
+ // Whether the user has a password (i.e. can log in without any SSO identity).
297
+ const hasPassword = !!u.password_hash;
298
+ res.json({ success: true, identities: rows, hasPassword });
299
+ });
300
+
301
+ // Unbind an identity. Refuse if it would leave the user with no login method.
302
+ app.delete('/api/auth/identities/:provider', requireAuth, (req, res) => {
303
+ const { provider } = req.params;
304
+ const u = userDb.getByUsername(req.user.username);
305
+ if (!u) return res.status(404).json({ success: false, error: 'User not found' });
306
+ const total = identityDb.countForUser(u.id);
307
+ const hasPassword = !!u.password_hash;
308
+ if (!hasPassword && total <= 1) {
309
+ return res.status(400).json({
310
+ success: false,
311
+ error: 'Cannot remove the only login method. Set a password first.'
312
+ });
313
+ }
314
+ const removed = identityDb.removeForUser(u.id, provider);
315
+ if (!removed) return res.status(404).json({ success: false, error: 'No such linked identity' });
316
+ // Keep legacy users.aad_oid in sync if microsoft is unbound.
317
+ if (provider === 'microsoft' && u.aad_oid) {
318
+ try { userDb.updateAadOid(u.id, null); } catch { /* ignore */ }
319
+ }
320
+ res.json({ success: true });
321
+ });
322
+ }
@@ -0,0 +1,117 @@
1
+ import { CONFIG } from '../config.js';
2
+ import { expertDb, userDb } from '../database.js';
3
+
4
+ /**
5
+ * Resolve the effective userId from the request.
6
+ * In skipAuth mode, uses a default user id; otherwise resolves from JWT.
7
+ */
8
+ function resolveUserId(req) {
9
+ if (CONFIG.skipAuth) {
10
+ // In skipAuth mode, use a fixed default user ID
11
+ return req.query.userId || 'default-user';
12
+ }
13
+ const user = userDb.getByUsername(req.user.username);
14
+ return user?.id;
15
+ }
16
+
17
+ /**
18
+ * Register custom expert role REST API routes.
19
+ *
20
+ * GET /api/expert-roles/custom — list custom roles for current user
21
+ * POST /api/expert-roles/custom — create a custom role
22
+ * PUT /api/expert-roles/custom/:roleId — update a custom role
23
+ * DELETE /api/expert-roles/custom/:roleId — delete a custom role
24
+ */
25
+ export function registerExpertRoutes(app, { requireAuth }) {
26
+ // GET — list all custom roles for the current user
27
+ app.get('/api/expert-roles/custom', requireAuth, (req, res) => {
28
+ try {
29
+ const userId = resolveUserId(req);
30
+ if (!userId) return res.status(401).json({ error: 'User not found' });
31
+
32
+ const roles = expertDb.getCustomRolesByUser(userId);
33
+ res.json({ roles });
34
+ } catch (error) {
35
+ console.error('[ExpertRoutes] GET /api/expert-roles/custom error:', error);
36
+ res.status(500).json({ error: 'Internal server error' });
37
+ }
38
+ });
39
+
40
+ // POST — create a custom role
41
+ app.post('/api/expert-roles/custom', requireAuth, (req, res) => {
42
+ try {
43
+ const userId = resolveUserId(req);
44
+ if (!userId) return res.status(401).json({ error: 'User not found' });
45
+
46
+ const roleData = req.body;
47
+ if (!roleData.name || !roleData.title) {
48
+ return res.status(400).json({ error: 'name and title are required' });
49
+ }
50
+
51
+ // Check for duplicate role_id if provided
52
+ if (roleData.roleId && expertDb.exists(userId, roleData.roleId)) {
53
+ return res.status(409).json({ error: `Role ${roleData.roleId} already exists` });
54
+ }
55
+
56
+ const { rowId, roleId } = expertDb.createCustomRole(userId, roleData);
57
+ const roles = expertDb.getCustomRolesByUser(userId);
58
+ const created = roles.find(r => r.id === roleId);
59
+
60
+ res.status(201).json({ role: created });
61
+ } catch (error) {
62
+ console.error('[ExpertRoutes] POST /api/expert-roles/custom error:', error);
63
+ if (error.message?.includes('UNIQUE constraint')) {
64
+ return res.status(409).json({ error: 'Role with this ID already exists' });
65
+ }
66
+ res.status(500).json({ error: 'Internal server error' });
67
+ }
68
+ });
69
+
70
+ // PUT — update a custom role
71
+ app.put('/api/expert-roles/custom/:roleId', requireAuth, (req, res) => {
72
+ try {
73
+ const userId = resolveUserId(req);
74
+ if (!userId) return res.status(401).json({ error: 'User not found' });
75
+
76
+ const { roleId } = req.params;
77
+ const roleData = req.body;
78
+
79
+ if (!expertDb.exists(userId, roleId)) {
80
+ return res.status(404).json({ error: `Role ${roleId} not found` });
81
+ }
82
+
83
+ if (!roleData.name || !roleData.title) {
84
+ return res.status(400).json({ error: 'name and title are required' });
85
+ }
86
+
87
+ expertDb.updateCustomRole(userId, roleId, roleData);
88
+ const roles = expertDb.getCustomRolesByUser(userId);
89
+ const updated = roles.find(r => r.id === roleId);
90
+
91
+ res.json({ role: updated });
92
+ } catch (error) {
93
+ console.error('[ExpertRoutes] PUT /api/expert-roles/custom error:', error);
94
+ res.status(500).json({ error: 'Internal server error' });
95
+ }
96
+ });
97
+
98
+ // DELETE — delete a custom role
99
+ app.delete('/api/expert-roles/custom/:roleId', requireAuth, (req, res) => {
100
+ try {
101
+ const userId = resolveUserId(req);
102
+ if (!userId) return res.status(401).json({ error: 'User not found' });
103
+
104
+ const { roleId } = req.params;
105
+ const deleted = expertDb.deleteCustomRole(userId, roleId);
106
+
107
+ if (!deleted) {
108
+ return res.status(404).json({ error: `Role ${roleId} not found` });
109
+ }
110
+
111
+ res.json({ deleted: true, roleId });
112
+ } catch (error) {
113
+ console.error('[ExpertRoutes] DELETE /api/expert-roles/custom error:', error);
114
+ res.status(500).json({ error: 'Internal server error' });
115
+ }
116
+ });
117
+ }
@@ -0,0 +1,60 @@
1
+ import { userDb, invitationDb } from '../database.js';
2
+
3
+ /**
4
+ * Register invitation-related API routes.
5
+ */
6
+ export function registerInvitationRoutes(app, { requireAuth, requireAdmin }) {
7
+ // Create invitation code
8
+ app.post('/api/invitations', requireAuth, requireAdmin, (req, res) => {
9
+ try {
10
+ const user = userDb.getByUsername(req.user.username);
11
+ if (!user) {
12
+ return res.status(400).json({ error: 'User not found' });
13
+ }
14
+ const role = req.body.role || 'pro';
15
+ if (!['pro'].includes(role)) {
16
+ return res.status(400).json({ error: 'Invalid role. Must be "pro"' });
17
+ }
18
+ const expiresInDays = parseInt(req.body.expiresInDays, 10) || 7;
19
+ const expiresInMs = expiresInDays * 24 * 60 * 60 * 1000;
20
+ const invitation = invitationDb.create(user.id, role, expiresInMs);
21
+ res.json(invitation);
22
+ } catch (err) {
23
+ console.error('Create invitation error:', err);
24
+ res.status(500).json({ error: 'Failed to create invitation' });
25
+ }
26
+ });
27
+
28
+ // List my invitations
29
+ app.get('/api/invitations', requireAuth, requireAdmin, (req, res) => {
30
+ try {
31
+ const user = userDb.getByUsername(req.user.username);
32
+ if (!user) {
33
+ return res.status(400).json({ error: 'User not found' });
34
+ }
35
+ const invitations = invitationDb.getByUser(user.id);
36
+ res.json({ invitations });
37
+ } catch (err) {
38
+ console.error('List invitations error:', err);
39
+ res.status(500).json({ error: 'Failed to list invitations' });
40
+ }
41
+ });
42
+
43
+ // Delete unused invitation
44
+ app.delete('/api/invitations/:code', requireAuth, requireAdmin, (req, res) => {
45
+ try {
46
+ const user = userDb.getByUsername(req.user.username);
47
+ if (!user) {
48
+ return res.status(400).json({ error: 'User not found' });
49
+ }
50
+ const deleted = invitationDb.delete(req.params.code, user.id);
51
+ if (!deleted) {
52
+ return res.status(404).json({ error: 'Invitation not found or already used' });
53
+ }
54
+ res.json({ success: true });
55
+ } catch (err) {
56
+ console.error('Delete invitation error:', err);
57
+ res.status(500).json({ error: 'Failed to delete invitation' });
58
+ }
59
+ });
60
+ }
@@ -0,0 +1,112 @@
1
+ import { CONFIG } from '../config.js';
2
+ import { sessionDb, messageDb, userDb } from '../database.js';
3
+
4
+ // 转换数据库会话记录为前端期望的格式
5
+ function transformSession(session) {
6
+ return {
7
+ id: session.id,
8
+ agentId: session.agent_id,
9
+ agentName: session.agent_name,
10
+ claudeSessionId: session.claude_session_id,
11
+ workDir: session.work_dir,
12
+ title: session.title,
13
+ createdAt: session.created_at,
14
+ updatedAt: session.updated_at,
15
+ isActive: !!session.is_active,
16
+ userId: session.user_id
17
+ };
18
+ }
19
+
20
+ /**
21
+ * Register session history API routes.
22
+ */
23
+ export function registerSessionRoutes(app, { requireAuth }) {
24
+ app.get('/api/sessions', requireAuth, (req, res) => {
25
+ const limit = parseInt(req.query.limit, 10) || 100;
26
+ const agentId = req.query.agentId;
27
+
28
+ try {
29
+ let effectiveUserId;
30
+ if (CONFIG.skipAuth) {
31
+ effectiveUserId = req.query.userId;
32
+ } else if (req.user.role === 'admin' && req.query.userId) {
33
+ effectiveUserId = req.query.userId;
34
+ } else {
35
+ const user = userDb.getByUsername(req.user.username);
36
+ effectiveUserId = user?.id;
37
+ }
38
+
39
+ let sessions;
40
+ if (effectiveUserId && agentId) {
41
+ sessions = sessionDb.getByUserAndAgent(effectiveUserId, agentId, limit);
42
+ } else if (effectiveUserId) {
43
+ sessions = sessionDb.getByUser(effectiveUserId, limit);
44
+ } else if (agentId) {
45
+ sessions = sessionDb.getByAgent(agentId, limit);
46
+ } else {
47
+ sessions = sessionDb.getAll(limit);
48
+ }
49
+ res.json({ sessions: sessions.map(transformSession) });
50
+ } catch (e) {
51
+ console.error('Failed to get sessions:', e.message);
52
+ res.status(500).json({ error: 'Failed to get sessions' });
53
+ }
54
+ });
55
+
56
+ app.get('/api/sessions/:id', requireAuth, (req, res) => {
57
+ try {
58
+ const session = sessionDb.get(req.params.id);
59
+ if (!session) {
60
+ return res.status(404).json({ error: 'Session not found' });
61
+ }
62
+ if (!CONFIG.skipAuth) {
63
+ const user = userDb.getByUsername(req.user.username);
64
+ if (session.user_id && session.user_id !== user?.id) {
65
+ return res.status(403).json({ error: 'Permission denied' });
66
+ }
67
+ }
68
+ res.json({ session: transformSession(session) });
69
+ } catch (e) {
70
+ console.error('Failed to get session:', e.message);
71
+ res.status(500).json({ error: 'Failed to get session' });
72
+ }
73
+ });
74
+
75
+ app.get('/api/sessions/:id/messages', requireAuth, (req, res) => {
76
+ const limit = parseInt(req.query.limit, 10) || 100;
77
+ try {
78
+ if (!CONFIG.skipAuth) {
79
+ const session = sessionDb.get(req.params.id);
80
+ const user = userDb.getByUsername(req.user.username);
81
+ if (session && session.user_id && session.user_id !== user?.id) {
82
+ return res.status(403).json({ error: 'Permission denied' });
83
+ }
84
+ }
85
+ const messages = limit
86
+ ? messageDb.getRecent(req.params.id, limit)
87
+ : messageDb.getBySession(req.params.id);
88
+ res.json({ messages });
89
+ } catch (e) {
90
+ console.error('Failed to get messages:', e.message);
91
+ res.status(500).json({ error: 'Failed to get messages' });
92
+ }
93
+ });
94
+
95
+ app.delete('/api/sessions/:id', requireAuth, (req, res) => {
96
+ try {
97
+ if (!CONFIG.skipAuth) {
98
+ const session = sessionDb.get(req.params.id);
99
+ const user = userDb.getByUsername(req.user.username);
100
+ if (session && session.user_id && session.user_id !== user?.id) {
101
+ return res.status(403).json({ error: 'Permission denied' });
102
+ }
103
+ }
104
+ messageDb.deleteBySession(req.params.id);
105
+ sessionDb.delete(req.params.id);
106
+ res.json({ success: true });
107
+ } catch (e) {
108
+ console.error('Failed to delete session:', e.message);
109
+ res.status(500).json({ error: 'Failed to delete session' });
110
+ }
111
+ });
112
+ }