@yeaft/webchat-agent 1.0.665 → 1.0.667

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.
@@ -43,7 +43,11 @@ db.exec(`
43
43
  sidebar_projects_ratio REAL NOT NULL DEFAULT 0.5
44
44
  CHECK (sidebar_projects_ratio >= 0.15 AND sidebar_projects_ratio <= 0.85),
45
45
  sidebar_actions_compact INTEGER NOT NULL DEFAULT 0
46
- CHECK (sidebar_actions_compact IN (0, 1))
46
+ CHECK (sidebar_actions_compact IN (0, 1)),
47
+ sidebar_collapsed_projects TEXT NOT NULL DEFAULT '{}'
48
+ CHECK (json_valid(sidebar_collapsed_projects) AND json_type(sidebar_collapsed_projects) = 'object'),
49
+ sidebar_collapsed_sections TEXT NOT NULL DEFAULT '{}'
50
+ CHECK (json_valid(sidebar_collapsed_sections) AND json_type(sidebar_collapsed_sections) = 'object')
47
51
  );
48
52
 
49
53
  -- Finalized deletion survives removal of the users row. This prevents a
@@ -341,6 +345,10 @@ db.exec(`
341
345
  const migrations = [
342
346
  `ALTER TABLE user_preferences ADD COLUMN sidebar_actions_compact INTEGER NOT NULL DEFAULT 0
343
347
  CHECK (sidebar_actions_compact IN (0, 1))`,
348
+ `ALTER TABLE user_preferences ADD COLUMN sidebar_collapsed_projects TEXT NOT NULL DEFAULT '{}'
349
+ CHECK (json_valid(sidebar_collapsed_projects) AND json_type(sidebar_collapsed_projects) = 'object')`,
350
+ `ALTER TABLE user_preferences ADD COLUMN sidebar_collapsed_sections TEXT NOT NULL DEFAULT '{}'
351
+ CHECK (json_valid(sidebar_collapsed_sections) AND json_type(sidebar_collapsed_sections) = 'object')`,
344
352
  `ALTER TABLE sessions ADD COLUMN user_id TEXT REFERENCES users(id)`,
345
353
  `ALTER TABLE users ADD COLUMN totp_secret TEXT`,
346
354
  `ALTER TABLE users ADD COLUMN totp_enabled INTEGER DEFAULT 0`,
@@ -911,16 +919,25 @@ export const stmts = {
911
919
  `),
912
920
 
913
921
  getUserPreferences: db.prepare(`
914
- SELECT sidebar_projects_ratio, sidebar_actions_compact FROM user_preferences WHERE user_id = ?
922
+ SELECT sidebar_projects_ratio, sidebar_actions_compact, sidebar_collapsed_projects, sidebar_collapsed_sections
923
+ FROM user_preferences WHERE user_id = ?
915
924
  `),
916
925
 
917
926
  upsertUserPreferences: db.prepare(`
918
- INSERT INTO user_preferences (user_id, sidebar_projects_ratio, sidebar_actions_compact)
919
- VALUES (?, COALESCE(?, 0.5), COALESCE(?, 0))
927
+ INSERT INTO user_preferences (
928
+ user_id, sidebar_projects_ratio, sidebar_actions_compact, sidebar_collapsed_projects, sidebar_collapsed_sections
929
+ )
930
+ VALUES (?, COALESCE(?, 0.5), COALESCE(?, 0), ?, ?)
920
931
  ON CONFLICT(user_id) DO UPDATE SET
921
932
  sidebar_projects_ratio = COALESCE(?, user_preferences.sidebar_projects_ratio),
922
- sidebar_actions_compact = COALESCE(?, user_preferences.sidebar_actions_compact)
923
- RETURNING sidebar_projects_ratio, sidebar_actions_compact
933
+ sidebar_actions_compact = COALESCE(?, user_preferences.sidebar_actions_compact),
934
+ sidebar_collapsed_projects = json_patch(
935
+ user_preferences.sidebar_collapsed_projects, excluded.sidebar_collapsed_projects
936
+ ),
937
+ sidebar_collapsed_sections = json_patch(
938
+ user_preferences.sidebar_collapsed_sections, excluded.sidebar_collapsed_sections
939
+ )
940
+ RETURNING sidebar_projects_ratio, sidebar_actions_compact, sidebar_collapsed_projects, sidebar_collapsed_sections
924
941
  `),
925
942
 
926
943
  // Invitation 操作
@@ -4,6 +4,8 @@ function toPreferences(row) {
4
4
  return {
5
5
  sidebarProjectsRatio: row?.sidebar_projects_ratio ?? 0.5,
6
6
  sidebarActionsCompact: !!row?.sidebar_actions_compact,
7
+ sidebarCollapsedProjects: JSON.parse(row?.sidebar_collapsed_projects ?? '{}'),
8
+ sidebarCollapsedSections: JSON.parse(row?.sidebar_collapsed_sections ?? '{}'),
7
9
  };
8
10
  }
9
11
 
@@ -14,10 +16,15 @@ export const userPreferencesDb = {
14
16
  },
15
17
 
16
18
  // The HTTP boundary validates the patch. A single upsert preserves omitted
17
- // fields without a read/merge/write race between independent clients.
19
+ // fields and merges map keys without a read/merge/write race between clients.
20
+ // Empty maps are no-ops; explicit false values remain stored overrides.
18
21
  update(userId, patch) {
19
22
  const ratio = patch.sidebarProjectsRatio ?? null;
20
23
  const compact = patch.sidebarActionsCompact === undefined ? null : Number(patch.sidebarActionsCompact);
21
- return toPreferences(stmts.upsertUserPreferences.get(userId, ratio, compact, ratio, compact));
24
+ const projects = JSON.stringify(patch.sidebarCollapsedProjects ?? {});
25
+ const sections = JSON.stringify(patch.sidebarCollapsedSections ?? {});
26
+ return toPreferences(stmts.upsertUserPreferences.get(
27
+ userId, ratio, compact, projects, sections, ratio, compact,
28
+ ));
22
29
  },
23
30
  };
@@ -679,7 +679,7 @@ export async function handleAgentOutput(agentId, agent, msg) {
679
679
  id: event.sessionId,
680
680
  name: event.name != null ? event.name : (existing.name || event.sessionId),
681
681
  roster: Array.isArray(event.roster) ? event.roster : (existing.roster || []),
682
- defaultVpId: event.defaultVpId != null ? event.defaultVpId : (existing.defaultVpId || null),
682
+ defaultVpId: Object.hasOwn(event, 'defaultVpId') ? event.defaultVpId : (existing.defaultVpId || null),
683
683
  workDir: existing.workDir || '',
684
684
  config: existing.config || {},
685
685
  ...(existing.task ? { task: existing.task } : {}),
@@ -1047,7 +1047,7 @@ export async function handleAgentOutput(agentId, agent, msg) {
1047
1047
  roster: Array.isArray(msg.roster)
1048
1048
  ? msg.roster
1049
1049
  : (existing?.roster || []),
1050
- defaultVpId: msg.defaultVpId != null
1050
+ defaultVpId: Object.hasOwn(msg, 'defaultVpId')
1051
1051
  ? msg.defaultVpId
1052
1052
  : (existing?.defaultVpId || null),
1053
1053
  workDir: existing?.workDir || '',
@@ -51,6 +51,26 @@ function ensureAgentSecret(user) {
51
51
  return userDb.resetAgentSecret(user.id);
52
52
  }
53
53
 
54
+ function isPlainObject(value) {
55
+ if (!value || typeof value !== 'object') return false;
56
+ const prototype = Object.getPrototypeOf(value);
57
+ return prototype === Object.prototype || prototype === null;
58
+ }
59
+
60
+ const preferenceFields = new Set([
61
+ 'sidebarProjectsRatio', 'sidebarActionsCompact', 'sidebarCollapsedProjects', 'sidebarCollapsedSections',
62
+ ]);
63
+ const unsafeMapKeys = new Set(['__proto__', 'constructor', 'prototype']);
64
+
65
+ function isCollapseMap(value, sections = false) {
66
+ if (!isPlainObject(value)) return false;
67
+ const entries = Object.entries(value);
68
+ return (sections || entries.length <= 1000) && entries.every(([key, collapsed]) => (
69
+ typeof collapsed === 'boolean' && !unsafeMapKeys.has(key)
70
+ && (sections ? key === 'projects' || key === 'recents' : key.length > 0 && key.length <= 200)
71
+ ));
72
+ }
73
+
54
74
  /**
55
75
  * Register user profile, agent secret, and admin user management routes.
56
76
  */
@@ -104,17 +124,22 @@ export function registerUserRoutes(app, {
104
124
  }
105
125
 
106
126
  const body = req.body;
107
- if (!body || typeof body !== 'object' || Array.isArray(body)
127
+ if (!isPlainObject(body)
108
128
  || Object.keys(body).length === 0
109
- || Object.keys(body).some(key => key !== 'sidebarProjectsRatio' && key !== 'sidebarActionsCompact')
129
+ || Object.keys(body).some(key => !preferenceFields.has(key))
110
130
  || (Object.hasOwn(body, 'sidebarProjectsRatio') && (
111
131
  typeof body.sidebarProjectsRatio !== 'number'
112
132
  || !Number.isFinite(body.sidebarProjectsRatio)
113
133
  || body.sidebarProjectsRatio < 0.15 || body.sidebarProjectsRatio > 0.85
114
134
  ))
115
- || (Object.hasOwn(body, 'sidebarActionsCompact') && typeof body.sidebarActionsCompact !== 'boolean')) {
135
+ || (Object.hasOwn(body, 'sidebarActionsCompact') && typeof body.sidebarActionsCompact !== 'boolean')
136
+ || (Object.hasOwn(body, 'sidebarCollapsedProjects') && !isCollapseMap(body.sidebarCollapsedProjects))
137
+ || (Object.hasOwn(body, 'sidebarCollapsedSections') && !isCollapseMap(body.sidebarCollapsedSections, true))) {
116
138
  return res.status(400).json({
117
- error: 'Expected a nonempty patch with only sidebarProjectsRatio (finite number between 0.15 and 0.85) and/or sidebarActionsCompact (boolean)',
139
+ error: 'Expected a nonempty patch with only sidebarProjectsRatio (finite number between 0.15 and 0.85), '
140
+ + 'sidebarActionsCompact (boolean), sidebarCollapsedProjects (boolean map, up to 1000 nonempty project IDs '
141
+ + 'of at most 200 characters, excluding __proto__/constructor/prototype), and/or '
142
+ + 'sidebarCollapsedSections (boolean map with only projects/recents)',
118
143
  });
119
144
  }
120
145
 
@@ -1 +1 @@
1
- {"version":"1.0.665"}
1
+ {"version":"1.0.667"}