dsh-mega-chat-nav 0.1.1

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 (192) hide show
  1. package/LICENSE +201 -0
  2. package/README.en.md +149 -0
  3. package/README.md +149 -0
  4. package/cordis.patch.yml +4 -0
  5. package/lib/client/NavOverlay.js +35 -0
  6. package/lib/client/NavRail.js +12 -0
  7. package/lib/client/QuestionNavSettingsTab.js +26 -0
  8. package/lib/client/QuestionNavStrip.js +555 -0
  9. package/lib/client/components/ConfigPanel.js +14 -0
  10. package/lib/client/components/DotRail.js +62 -0
  11. package/lib/client/components/NavOverlay.js +121 -0
  12. package/lib/client/components/NavRail.js +97 -0
  13. package/lib/client/components/RailDispatch.js +22 -0
  14. package/lib/client/components/RailHost.js +79 -0
  15. package/lib/client/components/SearchBox.js +66 -0
  16. package/lib/client/components/SettingsPage.js +46 -0
  17. package/lib/client/components/StandaloneSettings.js +16 -0
  18. package/lib/client/components/codex/CodexRail.js +139 -0
  19. package/lib/client/components/deepseek/DeepseekRail.js +264 -0
  20. package/lib/client/components/minimal/MinimalRail.js +135 -0
  21. package/lib/client/components/minimal/cards.js +32 -0
  22. package/lib/client/components/minimal/dot.js +11 -0
  23. package/lib/client/components/minimal/hint.js +30 -0
  24. package/lib/client/components/minimal/paging.js +5 -0
  25. package/lib/client/components/minimal/quick-settings.js +28 -0
  26. package/lib/client/components/minimal/search.js +67 -0
  27. package/lib/client/components/minimal/useRail.js +716 -0
  28. package/lib/client/components/rail/cards.js +32 -0
  29. package/lib/client/components/rail/hint.js +30 -0
  30. package/lib/client/components/rail/paging.js +5 -0
  31. package/lib/client/components/rail/quick-settings.js +28 -0
  32. package/lib/client/components/rail/search.js +76 -0
  33. package/lib/client/components/rail/useRail.js +845 -0
  34. package/lib/client/components/settings/SettingsPage.js +46 -0
  35. package/lib/client/components/settings/SettingsTab.js +17 -0
  36. package/lib/client/components/settings/StandaloneSettings.js +16 -0
  37. package/lib/client/components/shared/Cascade.js +26 -0
  38. package/lib/client/components/shared/ConfigPanel.js +27 -0
  39. package/lib/client/components/shared/Dot.js +8 -0
  40. package/lib/client/components/shared/Hint.js +5 -0
  41. package/lib/client/components/shared/PagingButton.js +5 -0
  42. package/lib/client/components/shared/RailShell.js +78 -0
  43. package/lib/client/components/shared/SearchBox.js +63 -0
  44. package/lib/client/components/shared/hooks.js +427 -0
  45. package/lib/client/components/shared/types.js +1 -0
  46. package/lib/client/config.js +1 -0
  47. package/lib/client/engines/jump.js +34 -0
  48. package/lib/client/engines/useAtBottom.js +25 -0
  49. package/lib/client/engines/useJump.js +40 -0
  50. package/lib/client/engines/useScrollSpy.js +88 -0
  51. package/lib/client/hooks.js +11 -0
  52. package/lib/client/index.js +312 -0
  53. package/lib/client/jump.js +255 -0
  54. package/lib/client/locales.js +142 -0
  55. package/lib/client/pure.js +61 -0
  56. package/lib/client/settings.js +369 -0
  57. package/lib/client/virtual/VirtualList.js +21 -0
  58. package/lib/client.js +3213 -0
  59. package/lib/compat.js +145 -0
  60. package/lib/config.js +37 -0
  61. package/lib/core/active-dot.js +17 -0
  62. package/lib/core/align.js +9 -0
  63. package/lib/core/config.js +37 -0
  64. package/lib/core/focus.js +38 -0
  65. package/lib/core/jump.js +98 -0
  66. package/lib/core/logic/active-dot.js +16 -0
  67. package/lib/core/logic/focus.js +34 -0
  68. package/lib/core/logic/jump.js +188 -0
  69. package/lib/core/logic/nodes.js +59 -0
  70. package/lib/core/model/nav-item.js +1 -0
  71. package/lib/core/model/turns.js +73 -0
  72. package/lib/core/nav-item.js +1 -0
  73. package/lib/core/nodes.js +57 -0
  74. package/lib/core/page-size.js +7 -0
  75. package/lib/core/question-entry.js +1 -0
  76. package/lib/core/time.js +23 -0
  77. package/lib/core/turn-dots.js +85 -0
  78. package/lib/core/utils/measure.js +24 -0
  79. package/lib/core/utils/time.js +26 -0
  80. package/lib/index.js +64 -0
  81. package/lib/nav-item.js +1 -0
  82. package/lib/nodes.js +59 -0
  83. package/lib/projection.js +126 -0
  84. package/lib/schema.js +26 -0
  85. package/lib/search.js +285 -0
  86. package/lib/settings.js +89 -0
  87. package/lib/types/client/NavOverlay.d.ts +18 -0
  88. package/lib/types/client/NavRail.d.ts +7 -0
  89. package/lib/types/client/QuestionNavSettingsTab.d.ts +16 -0
  90. package/lib/types/client/QuestionNavStrip.d.ts +48 -0
  91. package/lib/types/client/components/ConfigPanel.d.ts +14 -0
  92. package/lib/types/client/components/DotRail.d.ts +15 -0
  93. package/lib/types/client/components/NavOverlay.d.ts +30 -0
  94. package/lib/types/client/components/NavRail.d.ts +16 -0
  95. package/lib/types/client/components/RailDispatch.d.ts +8 -0
  96. package/lib/types/client/components/RailHost.d.ts +27 -0
  97. package/lib/types/client/components/SearchBox.d.ts +9 -0
  98. package/lib/types/client/components/SettingsPage.d.ts +9 -0
  99. package/lib/types/client/components/StandaloneSettings.d.ts +13 -0
  100. package/lib/types/client/components/codex/CodexRail.d.ts +3 -0
  101. package/lib/types/client/components/deepseek/DeepseekRail.d.ts +3 -0
  102. package/lib/types/client/components/minimal/MinimalRail.d.ts +8 -0
  103. package/lib/types/client/components/minimal/cards.d.ts +19 -0
  104. package/lib/types/client/components/minimal/dot.d.ts +14 -0
  105. package/lib/types/client/components/minimal/hint.d.ts +11 -0
  106. package/lib/types/client/components/minimal/paging.d.ts +10 -0
  107. package/lib/types/client/components/minimal/quick-settings.d.ts +8 -0
  108. package/lib/types/client/components/minimal/search.d.ts +27 -0
  109. package/lib/types/client/components/minimal/useRail.d.ts +90 -0
  110. package/lib/types/client/components/rail/cards.d.ts +19 -0
  111. package/lib/types/client/components/rail/hint.d.ts +11 -0
  112. package/lib/types/client/components/rail/paging.d.ts +10 -0
  113. package/lib/types/client/components/rail/quick-settings.d.ts +8 -0
  114. package/lib/types/client/components/rail/search.d.ts +34 -0
  115. package/lib/types/client/components/rail/useRail.d.ts +132 -0
  116. package/lib/types/client/components/settings/SettingsPage.d.ts +9 -0
  117. package/lib/types/client/components/settings/SettingsTab.d.ts +6 -0
  118. package/lib/types/client/components/settings/StandaloneSettings.d.ts +13 -0
  119. package/lib/types/client/components/shared/Cascade.d.ts +14 -0
  120. package/lib/types/client/components/shared/ConfigPanel.d.ts +8 -0
  121. package/lib/types/client/components/shared/Dot.d.ts +17 -0
  122. package/lib/types/client/components/shared/Hint.d.ts +6 -0
  123. package/lib/types/client/components/shared/PagingButton.d.ts +10 -0
  124. package/lib/types/client/components/shared/RailShell.d.ts +43 -0
  125. package/lib/types/client/components/shared/SearchBox.d.ts +18 -0
  126. package/lib/types/client/components/shared/hooks.d.ts +64 -0
  127. package/lib/types/client/components/shared/types.d.ts +101 -0
  128. package/lib/types/client/config.d.ts +9 -0
  129. package/lib/types/client/engines/jump.d.ts +17 -0
  130. package/lib/types/client/engines/useAtBottom.d.ts +1 -0
  131. package/lib/types/client/engines/useJump.d.ts +16 -0
  132. package/lib/types/client/engines/useScrollSpy.d.ts +12 -0
  133. package/lib/types/client/hooks.d.ts +7 -0
  134. package/lib/types/client/index.d.ts +31 -0
  135. package/lib/types/client/jump.d.ts +47 -0
  136. package/lib/types/client/locales.d.ts +142 -0
  137. package/lib/types/client/pure.d.ts +21 -0
  138. package/lib/types/client/settings.d.ts +173 -0
  139. package/lib/types/client/virtual/VirtualList.d.ts +15 -0
  140. package/lib/types/compat.d.ts +44 -0
  141. package/lib/types/config.d.ts +44 -0
  142. package/lib/types/core/active-dot.d.ts +15 -0
  143. package/lib/types/core/align.d.ts +10 -0
  144. package/lib/types/core/config.d.ts +44 -0
  145. package/lib/types/core/focus.d.ts +24 -0
  146. package/lib/types/core/jump.d.ts +39 -0
  147. package/lib/types/core/logic/active-dot.d.ts +15 -0
  148. package/lib/types/core/logic/focus.d.ts +22 -0
  149. package/lib/types/core/logic/jump.d.ts +39 -0
  150. package/lib/types/core/logic/nodes.d.ts +48 -0
  151. package/lib/types/core/model/nav-item.d.ts +12 -0
  152. package/lib/types/core/model/turns.d.ts +30 -0
  153. package/lib/types/core/nav-item.d.ts +12 -0
  154. package/lib/types/core/nodes.d.ts +43 -0
  155. package/lib/types/core/page-size.d.ts +8 -0
  156. package/lib/types/core/question-entry.d.ts +12 -0
  157. package/lib/types/core/time.d.ts +4 -0
  158. package/lib/types/core/turn-dots.d.ts +29 -0
  159. package/lib/types/core/utils/measure.d.ts +12 -0
  160. package/lib/types/core/utils/time.d.ts +4 -0
  161. package/lib/types/index.d.ts +4 -0
  162. package/lib/types/nav-item.d.ts +12 -0
  163. package/lib/types/nodes.d.ts +48 -0
  164. package/lib/types/projection.d.ts +71 -0
  165. package/lib/types/schema.d.ts +37 -0
  166. package/lib/types/search.d.ts +42 -0
  167. package/lib/types/settings.d.ts +99 -0
  168. package/package.json +71 -0
  169. package/src/client/components/RailDispatch.tsx +27 -0
  170. package/src/client/components/codex/CodexRail.tsx +348 -0
  171. package/src/client/components/deepseek/DeepseekRail.tsx +522 -0
  172. package/src/client/components/minimal/MinimalRail.tsx +324 -0
  173. package/src/client/components/minimal/dot.tsx +32 -0
  174. package/src/client/components/rail/cards.tsx +102 -0
  175. package/src/client/components/rail/hint.tsx +50 -0
  176. package/src/client/components/rail/paging.tsx +26 -0
  177. package/src/client/components/rail/quick-settings.tsx +130 -0
  178. package/src/client/components/rail/search.tsx +147 -0
  179. package/src/client/components/rail/useRail.ts +906 -0
  180. package/src/client/components/settings/SettingsPage.tsx +198 -0
  181. package/src/client/components/settings/StandaloneSettings.tsx +28 -0
  182. package/src/client/components/shared/types.ts +99 -0
  183. package/src/client/index.ts +388 -0
  184. package/src/client/jump.ts +293 -0
  185. package/src/client/locales.ts +143 -0
  186. package/src/client/settings.ts +482 -0
  187. package/src/client/styles.css +1580 -0
  188. package/src/compat.ts +161 -0
  189. package/src/index.ts +66 -0
  190. package/src/projection.ts +174 -0
  191. package/src/search.ts +291 -0
  192. package/src/settings.ts +169 -0
package/src/search.ts ADDED
@@ -0,0 +1,291 @@
1
+ // host 半:全量搜索 lite 路由(读持久化日志,三级快路径 + LRU;不触客户端扩窗)
2
+ import type { Context } from '@deepseek-ai/cordis'
3
+
4
+ export interface SearchHit {
5
+ key: string
6
+ seq: number
7
+ time: number
8
+ role: string
9
+ /** 命中消息所在轮次(turn/start 计数;未定位为 0) */
10
+ turn: number
11
+ /** 命中是否来自更早历史(非当前已加载窗口) */
12
+ historic: boolean
13
+ snippet: string
14
+ hitStart: number
15
+ hitLen: number
16
+ }
17
+
18
+ interface EventLike {
19
+ type?: string
20
+ seq?: number
21
+ time?: number
22
+ data?: { id?: string; content?: unknown; message?: { id?: string; content?: unknown }; source?: { kind?: string } }
23
+ }
24
+
25
+ function eventText(ev: EventLike): string {
26
+ if (ev.type === 'tool/call') {
27
+ // 工具调用:name + 参数 JSON 前段(可被搜索命中)
28
+ const d = ev.data as { name?: unknown; arguments?: unknown } | undefined
29
+ const name = typeof d?.name === 'string' ? d.name : ''
30
+ const args = typeof d?.arguments === 'string' ? d.arguments : ''
31
+ return name.length === 0 && args.length === 0 ? '' : name + ' ' + args.slice(0, 200)
32
+ }
33
+ if (ev.type !== 'user/message' && ev.type !== 'assistant/message') return ''
34
+ const msg = ev.type === 'user/message' ? ev.data : ev.data?.message
35
+ const content = msg?.content
36
+ if (typeof content === 'string') return content
37
+ if (Array.isArray(content)) {
38
+ const parts: string[] = []
39
+ for (const b of content) {
40
+ if (b && typeof b === 'object' && (b as { type?: string }).type === 'text') {
41
+ const txt = (b as { text?: unknown }).text
42
+ if (typeof txt === 'string') parts.push(txt)
43
+ }
44
+ }
45
+ return parts.join(' ')
46
+ }
47
+ return ''
48
+ }
49
+
50
+ function roleOf(ev: EventLike): string {
51
+ if (ev.type === 'user/message') {
52
+ const kind = ev.data?.source?.kind
53
+ return kind === 'steering' ? 'steering' : 'user'
54
+ }
55
+ if (ev.type === 'tool/call') return 'tool'
56
+ return 'assistant'
57
+ }
58
+
59
+ function keyOf(ev: EventLike): string {
60
+ const id = ev.type === 'user/message' ? ev.data?.id : ev.data?.message?.id
61
+ return typeof id === 'string' && id.length > 0 ? id : 'seq:' + (ev.seq ?? 0)
62
+ }
63
+
64
+ function snippetAround(text: string, q: string, radius = 40): { snippet: string; hitStart: number; hitLen: number } | null {
65
+ const idx = text.toLowerCase().indexOf(q.toLowerCase())
66
+ if (idx < 0) return null
67
+ const start = Math.max(0, idx - radius)
68
+ const end = Math.min(text.length, idx + q.length + radius)
69
+ const pre = start > 0 ? '…' : ''
70
+ const post = end < text.length ? '…' : ''
71
+ return { snippet: pre + text.slice(start, end) + post, hitStart: idx - start + (start > 0 ? 1 : 0), hitLen: q.length }
72
+ }
73
+
74
+ function makeLru<T>(capacity = 32) {
75
+ const map = new Map<string, T>()
76
+ return {
77
+ get(k: string): T | undefined {
78
+ const v = map.get(k)
79
+ if (v !== undefined) { map.delete(k); map.set(k, v) }
80
+ return v
81
+ },
82
+ set(k: string, v: T): void {
83
+ map.delete(k); map.set(k, v)
84
+ if (map.size > capacity) {
85
+ const first = map.keys().next().value
86
+ if (first !== undefined) map.delete(first)
87
+ }
88
+ },
89
+ }
90
+ }
91
+
92
+ const searchCache = makeLru<{ hits: SearchHit[]; truncated: boolean }>()
93
+
94
+ /** 访问可选服务:未注入的 cordis 服务在属性读取时抛错,捕获后按缺失处理 */
95
+ function tryGet<T>(getter: () => T): T | undefined {
96
+ try { return getter() } catch { return undefined }
97
+ }
98
+
99
+ function readBody(req: unknown): Promise<string> {
100
+ return new Promise((resolve, reject) => {
101
+ const r = req as { on?(ev: 'data' | 'end' | 'error', cb: (chunk?: unknown) => void): void }
102
+ const chunks: Buffer[] = []
103
+ r.on?.('data', (chunk) => {
104
+ if (typeof chunk === 'string') chunks.push(Buffer.from(chunk))
105
+ else if (chunk instanceof Uint8Array) chunks.push(Buffer.from(chunk))
106
+ })
107
+ r.on?.('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
108
+ r.on?.('error', reject)
109
+ })
110
+ }
111
+
112
+ interface SessionsLike { get(id: string): { events?: readonly unknown[] } | undefined }
113
+ interface PersistenceLike { readRaw(id: string): unknown }
114
+ interface QueryLike { readSession(id: string): { events?: readonly unknown[] } }
115
+
116
+ /** 事件 + 来源标注 */
117
+ interface SourcedEvent {
118
+ ev: EventLike
119
+ /** 来自更早历史(持久化日志 / sessionQuery),而非当前已加载窗口 */
120
+ historic: boolean
121
+ }
122
+
123
+ /**
124
+ * 读取会话事件:合并「已加载窗口」(live)与「更早历史」(持久化日志 →
125
+ * sessionQuery 兜底),按 seq 去重(历史中与 live 重复的丢弃),整体按 seq 排序。
126
+ */
127
+ function readEventSources(
128
+ sessionId: string,
129
+ sessions: SessionsLike | undefined,
130
+ persistence: PersistenceLike | undefined,
131
+ query: QueryLike | undefined,
132
+ ): SourcedEvent[] {
133
+ const live = (sessions?.get(sessionId)?.events ?? []) as unknown as EventLike[]
134
+ const liveSeqs = new Set<number>()
135
+ for (const ev of live) {
136
+ if (typeof ev?.seq === 'number') liveSeqs.add(ev.seq)
137
+ }
138
+ let historic: readonly EventLike[] = []
139
+ try {
140
+ const raw = persistence?.readRaw(sessionId)
141
+ if (Array.isArray(raw)) historic = raw as unknown as EventLike[]
142
+ } catch { /* 下一级 */ }
143
+ if (historic.length === 0) {
144
+ const q = query?.readSession(sessionId)
145
+ if (q && Array.isArray(q.events)) historic = q.events as unknown as EventLike[]
146
+ }
147
+ const out: SourcedEvent[] = live.map((ev) => ({ ev, historic: false }))
148
+ for (const ev of historic) {
149
+ if (typeof ev?.seq === 'number' && liveSeqs.has(ev.seq)) continue
150
+ out.push({ ev, historic: true })
151
+ }
152
+ return out.sort((a, b) => (a.ev.seq ?? 0) - (b.ev.seq ?? 0))
153
+ }
154
+
155
+ /**
156
+ * 命中锚点:直接按消息 kind 拼出完整 DOM 锚点 key(对齐 conversationContextKey 公式):
157
+ * - 用户/steering 消息 → '13:input-message' + id
158
+ * - assistant 消息 → '14:assistant-step' + turn + ':' + step(assistant-step 定义见
159
+ * dsh-client-ui-conversation;id 为 turn:step 组合)
160
+ * AI 命中的方向判定 seq 取所属轮用户消息序号(AI 行紧跟其用户提问,位置近似)。
161
+ */
162
+ /** 事件 → 搜索内容范围 */
163
+ function scopeOf(ev: EventLike): 'user' | 'assistant' | 'tool' {
164
+ if (ev.type === 'tool/call') return 'tool'
165
+ if (ev.type === 'assistant/message') return 'assistant'
166
+ return 'user'
167
+ }
168
+
169
+ export function searchEvents(events: readonly SourcedEvent[], q: string, limit: number, scopes: readonly string[] = ['user', 'assistant', 'tool']): { hits: SearchHit[]; truncated: boolean } {
170
+ const hits: SearchHit[] = []
171
+ const needle = q.toLowerCase()
172
+ let turn = 0
173
+ let lastUserSeq = 0
174
+ for (const { ev, historic } of events) {
175
+ if (ev.type === 'turn/start') {
176
+ const t = (ev as { data?: { turn?: unknown } }).data?.turn
177
+ if (typeof t === 'number') turn = t
178
+ continue
179
+ }
180
+ if (ev.type === 'user/message') lastUserSeq = ev.seq ?? 0
181
+ const text = eventText(ev)
182
+ if (!text || !text.toLowerCase().includes(needle)) continue
183
+ if (!scopes.includes(scopeOf(ev))) continue
184
+ const role = roleOf(ev)
185
+ const isAssistant = role === 'assistant'
186
+ const isTool = ev.type === 'tool/call'
187
+ const anchorKey = isTool
188
+ ? '9:tool-call' + String((ev as { data?: { callId?: unknown } }).data?.callId ?? '')
189
+ : isAssistant
190
+ ? '14:assistant-step' + turn + ':' + (ev as { data?: { step?: unknown } }).data?.step
191
+ : '13:input-message' + keyOf(ev)
192
+ const anchorSeq = isAssistant || isTool ? lastUserSeq : ev.seq ?? 0
193
+ const snip = snippetAround(text, q)
194
+ hits.push({
195
+ key: anchorKey,
196
+ seq: anchorSeq,
197
+ time: ev.time ?? 0,
198
+ role,
199
+ turn,
200
+ historic,
201
+ snippet: snip ? snip.snippet : text.slice(0, 60),
202
+ hitStart: snip ? snip.hitStart : -1,
203
+ hitLen: snip ? snip.hitLen : 0,
204
+ })
205
+ if (limit > 0 && hits.length >= limit) break
206
+ }
207
+ return { hits, truncated: limit > 0 && hits.length >= limit }
208
+ }
209
+
210
+ export function registerSearchRoute(ctx: Context): void {
211
+ ctx.inject(['webServer', 'sessions'], (scoped) => {
212
+ const webServer = (scoped as unknown as {
213
+ webServer?: { register(opts: { kind: 'prefix'; path: string; handler(req: unknown, res: unknown): Promise<void> | void }): unknown }
214
+ }).webServer
215
+ const sessions = (scoped as unknown as { sessions?: SessionsLike }).sessions
216
+ if (!webServer || !sessions) return
217
+ ctx.inject(['settings'], (settingsCtx) => {
218
+ const settingsService = (settingsCtx as unknown as {
219
+ settings?: { update(ns: string, patch: object, expectedRevision?: number): Promise<void> }
220
+ }).settings
221
+ if (!settingsService) return
222
+ ctx.effect(() => {
223
+ const dispose = webServer.register({
224
+ kind: 'prefix',
225
+ path: '/mega-chat-nav',
226
+ handler: async (req, res) => {
227
+ const httpRes = res as { statusCode?: number; setHeader?(k: string, v: string): void; end(s: string): void }
228
+ const httpReq = req as { url?: string; method?: string }
229
+ const write = (code: number, body: unknown) => {
230
+ httpRes.statusCode = code
231
+ httpRes.setHeader?.('Content-Type', 'application/json; charset=utf-8')
232
+ httpRes.end(JSON.stringify(body))
233
+ }
234
+ try {
235
+ const url = new URL(httpReq.url ?? '/', 'http://localhost')
236
+ // 嵌套配置写回:scope.set 仅支持 section 内标量,嵌套 section 必须走 host 桥
237
+ if (url.pathname === '/mega-chat-nav/settings') {
238
+ if ((httpReq.method ?? 'GET') !== 'POST') { write(405, { ok: false, error: 'POST required' }); return }
239
+ const raw = await readBody(req)
240
+ let patch: unknown
241
+ let revision: unknown
242
+ try {
243
+ const body = JSON.parse(raw) as { patch?: unknown; revision?: unknown }
244
+ patch = body.patch
245
+ revision = body.revision
246
+ } catch {
247
+ write(400, { ok: false, error: 'bad json' }); return
248
+ }
249
+ if (typeof patch !== 'object' || patch === null || Array.isArray(patch)) {
250
+ write(400, { ok: false, error: 'patch must be an object' }); return
251
+ }
252
+ try {
253
+ await settingsService.update('mega-chat-nav', patch, typeof revision === 'number' ? revision : undefined)
254
+ } catch (e) {
255
+ write(409, { ok: false, error: 'settings conflict: ' + String(e) }); return
256
+ }
257
+ write(200, { ok: true })
258
+ return
259
+ }
260
+ if (url.pathname !== '/mega-chat-nav/search') { write(404, { ok: false, error: 'not found' }); return }
261
+ const sessionId = url.searchParams.get('session') ?? ''
262
+ const q = (url.searchParams.get('q') ?? '').trim()
263
+ // limit<=0(含未传)→ 不限条数,返回全部命中
264
+ const rawLimit = url.searchParams.get('limit')
265
+ const limit = rawLimit === null || rawLimit === ''
266
+ ? 0
267
+ : Math.max(0, Math.min(Number(rawLimit) || 0, 500))
268
+ if (!sessionId || q.length < 1) { write(400, { ok: false, error: 'session & q required' }); return }
269
+ // 搜索内容范围(多选);缺省全选;user 必选强制保留
270
+ const rawScopes = (url.searchParams.get('scopes') ?? '').split(',').map((s) => s.trim()).filter(Boolean)
271
+ const scopes = rawScopes.length > 0
272
+ ? [...new Set(['user', ...rawScopes])].filter((s) => s === 'user' || s === 'assistant' || s === 'tool')
273
+ : ['user', 'assistant', 'tool']
274
+ const cacheKey = sessionId + '|' + q + '|' + limit
275
+ const cached = searchCache.get(cacheKey)
276
+ if (cached) { write(200, { ok: true, ...cached }); return }
277
+ const persistence = tryGet(() => (scoped as unknown as { sessionPersistence?: PersistenceLike }).sessionPersistence)
278
+ const query = tryGet(() => (scoped as unknown as { sessionQuery?: QueryLike }).sessionQuery)
279
+ const result = searchEvents(readEventSources(sessionId, sessions, persistence, query), q, limit, scopes)
280
+ searchCache.set(cacheKey, result)
281
+ write(200, { ok: true, ...result })
282
+ } catch (e) {
283
+ write(500, { ok: false, error: String(e) })
284
+ }
285
+ },
286
+ })
287
+ return dispose as unknown as () => void
288
+ })
289
+ })
290
+ })
291
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Host-side durable settings for the mega-chat-nav plugin.
3
+ * 配置选项为本插件自有定义:host 半持有此处;client 半在
4
+ * src/client/settings.ts 持有同值副本(client bundle 不得依赖宿主包)。
5
+ *
6
+ * @module dsh-mega-chat-nav/settings
7
+ */
8
+
9
+ import z from '@deepseek-ai/schemastery'
10
+ import type { SettingsNamespace } from '@deepseek-ai/dsh-settings'
11
+
12
+ /** 风格选项:简约 / codex(时间线刻度)/ deepseek(用户消息面板,常态短横折叠) */
13
+ export const STYLE_OPTIONS = ['minimal', 'codex', 'deepseek'] as const
14
+ export type NavStyle = typeof STYLE_OPTIONS[number]
15
+
16
+ /** 停靠侧选项 */
17
+ export const ALIGN_OPTIONS = ['left', 'right'] as const
18
+ export type RailAlign = typeof ALIGN_OPTIONS[number]
19
+
20
+ /** 节点栏条带高度档位 */
21
+ export const BAND_OPTIONS = ['compact', 'standard', 'tall'] as const
22
+ export type BandHeight = typeof BAND_OPTIONS[number]
23
+
24
+ /** 跳转动画方式 */
25
+ export const SCROLL_OPTIONS = ['smooth', 'instant'] as const
26
+ export type ScrollMode = typeof SCROLL_OPTIONS[number]
27
+
28
+ /** 显示策略:常显 / 悬停浮现 / 隐藏 */
29
+ export const SHOW_OPTIONS = ['show', 'peek', 'hide'] as const
30
+ export type ShowMode = typeof SHOW_OPTIONS[number]
31
+
32
+ /** 翻页标记默认显示模式(按风格独立):minimal 常显(历史行为)/ codex 隐藏 */
33
+ export const DEFAULT_PAGING: Record<NavStyle, ShowMode> = {
34
+ minimal: 'show',
35
+ codex: 'hide',
36
+ deepseek: 'peek',
37
+ }
38
+
39
+ /** 搜索内容范围(多选;用户必选) */
40
+ export const SEARCH_SCOPE_OPTIONS = ['user', 'assistant', 'tool'] as const
41
+ export type SearchScope = typeof SEARCH_SCOPE_OPTIONS[number]
42
+ export const DEFAULT_SEARCH_SCOPES: SearchScope[] = ['user', 'assistant', 'tool']
43
+
44
+ /** 悬停卡片数量 */
45
+ export const CARD_OPTIONS = [1, 3, 5] as const
46
+ export type CardCount = typeof CARD_OPTIONS[number]
47
+
48
+ /** 悬浮卡默认数量(按风格独立):minimal 3 张 / codex 1 张 / deepseek 1 张(无悬浮卡能力,占位) */
49
+ export const DEFAULT_CARD_COUNT: Record<NavStyle, CardCount> = {
50
+ minimal: 3,
51
+ codex: 1,
52
+ deepseek: 1,
53
+ }
54
+
55
+ /** 悬浮卡内容块(多选) */
56
+ export const CARD_ITEM_OPTIONS = ['turn', 'duration', 'favorite'] as const
57
+ export type CardItem = typeof CARD_ITEM_OPTIONS[number]
58
+ export const DEFAULT_CARD_ITEMS: CardItem[] = ['turn', 'duration', 'favorite']
59
+
60
+ /** 刻度色调:柔和(低对比)/ 深邃(高对比、清晰) */
61
+ export const MARK_TONE_OPTIONS = ['soft', 'deep'] as const
62
+ export type MarkTone = typeof MARK_TONE_OPTIONS[number]
63
+ export const DEFAULT_MARK_TONE: MarkTone = 'deep'
64
+
65
+ export const DEFAULT_STYLE: NavStyle = 'minimal'
66
+ export const DEFAULT_ALIGN: RailAlign = 'left'
67
+ export const DEFAULT_BAND: BandHeight = 'standard'
68
+ export const DEFAULT_SCROLL: ScrollMode = 'smooth'
69
+ export const DEFAULT_SHOW: ShowMode = 'show'
70
+
71
+ /** 通用配置(全局,跨风格生效) */
72
+ export interface NavGeneralSettings {
73
+ /** 当前风格 */
74
+ style: NavStyle
75
+ /** 导航条停靠侧 */
76
+ align: RailAlign
77
+ /** 跳转动画方式 */
78
+ scrollBehavior: ScrollMode
79
+ /** 轮次总数徽标显示模式(兼容旧布尔:true→show / false→hide) */
80
+ showCount: ShowMode | boolean
81
+ /** 搜索按钮显示模式(兼容旧布尔) */
82
+ showSearch: ShowMode | boolean
83
+ /** 设置按钮显示模式(兼容旧布尔) */
84
+ showQuickSettings: ShowMode | boolean
85
+ /** 收藏按钮显示模式(兼容旧布尔) */
86
+ showFavorites: ShowMode | boolean
87
+ /** 搜索内容范围(多选;用户必选,其余可关) */
88
+ searchScopes: SearchScope[]
89
+ /** 导航栏与对话列边缘的偏移距离(px,0-32,随左右对齐自适应) */
90
+ railOffset: number
91
+ /** @deprecated 旧版字段(翻页标记曾置于此处)——仅兼容存量配置,新主源在 styles.<style>.showPaging */
92
+ showPaging?: ShowMode | boolean
93
+ }
94
+
95
+ /** 单风格配置(每风格独立一套) */
96
+ export interface StyleSettings {
97
+ /** 节点栏条带高度档位 */
98
+ bandHeight: BandHeight
99
+ /** 悬停卡片数量(1/3/5) */
100
+ cardCount: CardCount
101
+ /** 悬浮卡内容块(多选:轮次 / 用时 / 收藏) */
102
+ cardItems: CardItem[]
103
+ /** 刻度色调(codex 刻度轨使用:柔和 / 深邃) */
104
+ markTone: MarkTone
105
+ /** 翻页标记显示模式:show=常显 / peek=浮现 / hide=隐藏 */
106
+ showPaging: ShowMode
107
+ }
108
+
109
+ /** 风格专属配置(styles.<style> 每风格独立) */
110
+ export interface NavStyleSettings {
111
+ minimal: StyleSettings
112
+ codex: StyleSettings
113
+ deepseek: StyleSettings
114
+ }
115
+
116
+ /** 持久化设置文档结构 */
117
+ export interface NavSettings {
118
+ general: NavGeneralSettings
119
+ styles: NavStyleSettings
120
+ }
121
+
122
+ // 显示模式兼容旧布尔(true → show / false → hide):旧 user 段里的布尔值
123
+ // 不能导致 register 的 schema 校验失败,否则 namespace 注册失败、设置无法持久化
124
+ const ShowModeSchema = z.union([z.boolean(), ...SHOW_OPTIONS]).default(DEFAULT_SHOW)
125
+
126
+ const GeneralSchema = z.object({
127
+ style: z.union([...STYLE_OPTIONS]).default(DEFAULT_STYLE),
128
+ align: z.union([...ALIGN_OPTIONS]).default(DEFAULT_ALIGN),
129
+ scrollBehavior: z.union([...SCROLL_OPTIONS]).default(DEFAULT_SCROLL),
130
+ showCount: ShowModeSchema,
131
+ showSearch: ShowModeSchema,
132
+ showQuickSettings: ShowModeSchema,
133
+ showFavorites: ShowModeSchema,
134
+ /* 旧版 general.showPaging 残留兼容(0.9.x 曾置于此处);新主源已迁移 styles.<style> */
135
+ showPaging: ShowModeSchema,
136
+ searchScopes: z.array(z.union([...SEARCH_SCOPE_OPTIONS])).default([...DEFAULT_SEARCH_SCOPES]),
137
+ railOffset: z.natural().max(32).default(8),
138
+ })
139
+
140
+ /** 风格公共字段(两风格默认一致的配置) */
141
+ const StyleFields = {
142
+ bandHeight: z.union([...BAND_OPTIONS]).default(DEFAULT_BAND),
143
+ cardItems: z.array(z.union([...CARD_ITEM_OPTIONS])).default([...DEFAULT_CARD_ITEMS]),
144
+ markTone: z.union([...MARK_TONE_OPTIONS]).default(DEFAULT_MARK_TONE),
145
+ }
146
+
147
+ /** 每风格独立默认字段:悬浮卡数与翻页标记(minimal 常显 / codex 隐藏 / deepseek 浮现) */
148
+ const StyleDefaults: Record<NavStyle, { cardCount: CardCount; showPaging: ShowMode }> = {
149
+ minimal: { cardCount: DEFAULT_CARD_COUNT.minimal, showPaging: DEFAULT_PAGING.minimal },
150
+ codex: { cardCount: DEFAULT_CARD_COUNT.codex, showPaging: DEFAULT_PAGING.codex },
151
+ deepseek: { cardCount: DEFAULT_CARD_COUNT.deepseek, showPaging: DEFAULT_PAGING.deepseek },
152
+ }
153
+ const styleSchemaFields = (style: NavStyle) => ({
154
+ cardCount: z.union([...CARD_OPTIONS]).default(StyleDefaults[style].cardCount),
155
+ showPaging: z.union([...SHOW_OPTIONS]).default(StyleDefaults[style].showPaging),
156
+ })
157
+
158
+ /** 持久化设置 schema;也是浏览器 scope 校验的 wire 信封 */
159
+ export const NavSettingsSchema: z<NavSettings> = z.object({
160
+ general: GeneralSchema,
161
+ styles: z.object({
162
+ minimal: z.object({ ...StyleFields, ...styleSchemaFields('minimal') }),
163
+ codex: z.object({ ...StyleFields, ...styleSchemaFields('codex') }),
164
+ deepseek: z.object({ ...StyleFields, ...styleSchemaFields('deepseek') }),
165
+ }),
166
+ })
167
+
168
+ /** 本插件设置命名空间 */
169
+ export const megaChatNavSettingsNamespace = 'mega-chat-nav' as SettingsNamespace