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.
- package/LICENSE +201 -0
- package/README.en.md +149 -0
- package/README.md +149 -0
- package/cordis.patch.yml +4 -0
- package/lib/client/NavOverlay.js +35 -0
- package/lib/client/NavRail.js +12 -0
- package/lib/client/QuestionNavSettingsTab.js +26 -0
- package/lib/client/QuestionNavStrip.js +555 -0
- package/lib/client/components/ConfigPanel.js +14 -0
- package/lib/client/components/DotRail.js +62 -0
- package/lib/client/components/NavOverlay.js +121 -0
- package/lib/client/components/NavRail.js +97 -0
- package/lib/client/components/RailDispatch.js +22 -0
- package/lib/client/components/RailHost.js +79 -0
- package/lib/client/components/SearchBox.js +66 -0
- package/lib/client/components/SettingsPage.js +46 -0
- package/lib/client/components/StandaloneSettings.js +16 -0
- package/lib/client/components/codex/CodexRail.js +139 -0
- package/lib/client/components/deepseek/DeepseekRail.js +264 -0
- package/lib/client/components/minimal/MinimalRail.js +135 -0
- package/lib/client/components/minimal/cards.js +32 -0
- package/lib/client/components/minimal/dot.js +11 -0
- package/lib/client/components/minimal/hint.js +30 -0
- package/lib/client/components/minimal/paging.js +5 -0
- package/lib/client/components/minimal/quick-settings.js +28 -0
- package/lib/client/components/minimal/search.js +67 -0
- package/lib/client/components/minimal/useRail.js +716 -0
- package/lib/client/components/rail/cards.js +32 -0
- package/lib/client/components/rail/hint.js +30 -0
- package/lib/client/components/rail/paging.js +5 -0
- package/lib/client/components/rail/quick-settings.js +28 -0
- package/lib/client/components/rail/search.js +76 -0
- package/lib/client/components/rail/useRail.js +845 -0
- package/lib/client/components/settings/SettingsPage.js +46 -0
- package/lib/client/components/settings/SettingsTab.js +17 -0
- package/lib/client/components/settings/StandaloneSettings.js +16 -0
- package/lib/client/components/shared/Cascade.js +26 -0
- package/lib/client/components/shared/ConfigPanel.js +27 -0
- package/lib/client/components/shared/Dot.js +8 -0
- package/lib/client/components/shared/Hint.js +5 -0
- package/lib/client/components/shared/PagingButton.js +5 -0
- package/lib/client/components/shared/RailShell.js +78 -0
- package/lib/client/components/shared/SearchBox.js +63 -0
- package/lib/client/components/shared/hooks.js +427 -0
- package/lib/client/components/shared/types.js +1 -0
- package/lib/client/config.js +1 -0
- package/lib/client/engines/jump.js +34 -0
- package/lib/client/engines/useAtBottom.js +25 -0
- package/lib/client/engines/useJump.js +40 -0
- package/lib/client/engines/useScrollSpy.js +88 -0
- package/lib/client/hooks.js +11 -0
- package/lib/client/index.js +312 -0
- package/lib/client/jump.js +255 -0
- package/lib/client/locales.js +142 -0
- package/lib/client/pure.js +61 -0
- package/lib/client/settings.js +369 -0
- package/lib/client/virtual/VirtualList.js +21 -0
- package/lib/client.js +3213 -0
- package/lib/compat.js +145 -0
- package/lib/config.js +37 -0
- package/lib/core/active-dot.js +17 -0
- package/lib/core/align.js +9 -0
- package/lib/core/config.js +37 -0
- package/lib/core/focus.js +38 -0
- package/lib/core/jump.js +98 -0
- package/lib/core/logic/active-dot.js +16 -0
- package/lib/core/logic/focus.js +34 -0
- package/lib/core/logic/jump.js +188 -0
- package/lib/core/logic/nodes.js +59 -0
- package/lib/core/model/nav-item.js +1 -0
- package/lib/core/model/turns.js +73 -0
- package/lib/core/nav-item.js +1 -0
- package/lib/core/nodes.js +57 -0
- package/lib/core/page-size.js +7 -0
- package/lib/core/question-entry.js +1 -0
- package/lib/core/time.js +23 -0
- package/lib/core/turn-dots.js +85 -0
- package/lib/core/utils/measure.js +24 -0
- package/lib/core/utils/time.js +26 -0
- package/lib/index.js +64 -0
- package/lib/nav-item.js +1 -0
- package/lib/nodes.js +59 -0
- package/lib/projection.js +126 -0
- package/lib/schema.js +26 -0
- package/lib/search.js +285 -0
- package/lib/settings.js +89 -0
- package/lib/types/client/NavOverlay.d.ts +18 -0
- package/lib/types/client/NavRail.d.ts +7 -0
- package/lib/types/client/QuestionNavSettingsTab.d.ts +16 -0
- package/lib/types/client/QuestionNavStrip.d.ts +48 -0
- package/lib/types/client/components/ConfigPanel.d.ts +14 -0
- package/lib/types/client/components/DotRail.d.ts +15 -0
- package/lib/types/client/components/NavOverlay.d.ts +30 -0
- package/lib/types/client/components/NavRail.d.ts +16 -0
- package/lib/types/client/components/RailDispatch.d.ts +8 -0
- package/lib/types/client/components/RailHost.d.ts +27 -0
- package/lib/types/client/components/SearchBox.d.ts +9 -0
- package/lib/types/client/components/SettingsPage.d.ts +9 -0
- package/lib/types/client/components/StandaloneSettings.d.ts +13 -0
- package/lib/types/client/components/codex/CodexRail.d.ts +3 -0
- package/lib/types/client/components/deepseek/DeepseekRail.d.ts +3 -0
- package/lib/types/client/components/minimal/MinimalRail.d.ts +8 -0
- package/lib/types/client/components/minimal/cards.d.ts +19 -0
- package/lib/types/client/components/minimal/dot.d.ts +14 -0
- package/lib/types/client/components/minimal/hint.d.ts +11 -0
- package/lib/types/client/components/minimal/paging.d.ts +10 -0
- package/lib/types/client/components/minimal/quick-settings.d.ts +8 -0
- package/lib/types/client/components/minimal/search.d.ts +27 -0
- package/lib/types/client/components/minimal/useRail.d.ts +90 -0
- package/lib/types/client/components/rail/cards.d.ts +19 -0
- package/lib/types/client/components/rail/hint.d.ts +11 -0
- package/lib/types/client/components/rail/paging.d.ts +10 -0
- package/lib/types/client/components/rail/quick-settings.d.ts +8 -0
- package/lib/types/client/components/rail/search.d.ts +34 -0
- package/lib/types/client/components/rail/useRail.d.ts +132 -0
- package/lib/types/client/components/settings/SettingsPage.d.ts +9 -0
- package/lib/types/client/components/settings/SettingsTab.d.ts +6 -0
- package/lib/types/client/components/settings/StandaloneSettings.d.ts +13 -0
- package/lib/types/client/components/shared/Cascade.d.ts +14 -0
- package/lib/types/client/components/shared/ConfigPanel.d.ts +8 -0
- package/lib/types/client/components/shared/Dot.d.ts +17 -0
- package/lib/types/client/components/shared/Hint.d.ts +6 -0
- package/lib/types/client/components/shared/PagingButton.d.ts +10 -0
- package/lib/types/client/components/shared/RailShell.d.ts +43 -0
- package/lib/types/client/components/shared/SearchBox.d.ts +18 -0
- package/lib/types/client/components/shared/hooks.d.ts +64 -0
- package/lib/types/client/components/shared/types.d.ts +101 -0
- package/lib/types/client/config.d.ts +9 -0
- package/lib/types/client/engines/jump.d.ts +17 -0
- package/lib/types/client/engines/useAtBottom.d.ts +1 -0
- package/lib/types/client/engines/useJump.d.ts +16 -0
- package/lib/types/client/engines/useScrollSpy.d.ts +12 -0
- package/lib/types/client/hooks.d.ts +7 -0
- package/lib/types/client/index.d.ts +31 -0
- package/lib/types/client/jump.d.ts +47 -0
- package/lib/types/client/locales.d.ts +142 -0
- package/lib/types/client/pure.d.ts +21 -0
- package/lib/types/client/settings.d.ts +173 -0
- package/lib/types/client/virtual/VirtualList.d.ts +15 -0
- package/lib/types/compat.d.ts +44 -0
- package/lib/types/config.d.ts +44 -0
- package/lib/types/core/active-dot.d.ts +15 -0
- package/lib/types/core/align.d.ts +10 -0
- package/lib/types/core/config.d.ts +44 -0
- package/lib/types/core/focus.d.ts +24 -0
- package/lib/types/core/jump.d.ts +39 -0
- package/lib/types/core/logic/active-dot.d.ts +15 -0
- package/lib/types/core/logic/focus.d.ts +22 -0
- package/lib/types/core/logic/jump.d.ts +39 -0
- package/lib/types/core/logic/nodes.d.ts +48 -0
- package/lib/types/core/model/nav-item.d.ts +12 -0
- package/lib/types/core/model/turns.d.ts +30 -0
- package/lib/types/core/nav-item.d.ts +12 -0
- package/lib/types/core/nodes.d.ts +43 -0
- package/lib/types/core/page-size.d.ts +8 -0
- package/lib/types/core/question-entry.d.ts +12 -0
- package/lib/types/core/time.d.ts +4 -0
- package/lib/types/core/turn-dots.d.ts +29 -0
- package/lib/types/core/utils/measure.d.ts +12 -0
- package/lib/types/core/utils/time.d.ts +4 -0
- package/lib/types/index.d.ts +4 -0
- package/lib/types/nav-item.d.ts +12 -0
- package/lib/types/nodes.d.ts +48 -0
- package/lib/types/projection.d.ts +71 -0
- package/lib/types/schema.d.ts +37 -0
- package/lib/types/search.d.ts +42 -0
- package/lib/types/settings.d.ts +99 -0
- package/package.json +71 -0
- package/src/client/components/RailDispatch.tsx +27 -0
- package/src/client/components/codex/CodexRail.tsx +348 -0
- package/src/client/components/deepseek/DeepseekRail.tsx +522 -0
- package/src/client/components/minimal/MinimalRail.tsx +324 -0
- package/src/client/components/minimal/dot.tsx +32 -0
- package/src/client/components/rail/cards.tsx +102 -0
- package/src/client/components/rail/hint.tsx +50 -0
- package/src/client/components/rail/paging.tsx +26 -0
- package/src/client/components/rail/quick-settings.tsx +130 -0
- package/src/client/components/rail/search.tsx +147 -0
- package/src/client/components/rail/useRail.ts +906 -0
- package/src/client/components/settings/SettingsPage.tsx +198 -0
- package/src/client/components/settings/StandaloneSettings.tsx +28 -0
- package/src/client/components/shared/types.ts +99 -0
- package/src/client/index.ts +388 -0
- package/src/client/jump.ts +293 -0
- package/src/client/locales.ts +143 -0
- package/src/client/settings.ts +482 -0
- package/src/client/styles.css +1580 -0
- package/src/compat.ts +161 -0
- package/src/index.ts +66 -0
- package/src/projection.ts +174 -0
- package/src/search.ts +291 -0
- 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
|
+
}
|
package/src/settings.ts
ADDED
|
@@ -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
|