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/compat.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh 版本校验器(纯函数,零依赖:宿主侧 / 单测均可用)。
|
|
3
|
+
*
|
|
4
|
+
* 语义(对齐官方 deepseek-harness tag 线,如 dsh-v0.1.1-rc.2):
|
|
5
|
+
* - 比较按语义化版本:主/次/修订号数值比较优先;
|
|
6
|
+
* - 同基线预发布排序:alpha < rc(标识符字典序),数字标识按数值比较;
|
|
7
|
+
* - 正式版(无预发布)高于同基线任意预发布(0.1.1 > 0.1.1-rc.2);
|
|
8
|
+
* - 支持操作符:> < =(另附 >= <=);
|
|
9
|
+
* - 通配:仅 '=' 支持,如 '0.1.1-*' 匹配该基线的任意预发布(0.1.1-rc.1 / 0.1.1-rc.2 …);
|
|
10
|
+
* '*' 作为标识段时吞掉其后的全部剩余标识。
|
|
11
|
+
*/
|
|
12
|
+
export type DshCmpOp = '>' | '>=' | '<' | '<=' | '='
|
|
13
|
+
|
|
14
|
+
/** 兼容策略:当前 dsh 版本与目标的比较约束。 */
|
|
15
|
+
export interface DshCompatPolicy {
|
|
16
|
+
op: DshCmpOp
|
|
17
|
+
/**
|
|
18
|
+
* 目标版本(可含通配,如 0.1.1-*,仅 op='=' 生效)。
|
|
19
|
+
* 支持数组:命中其中任一版本即通过,如 ['0.1.1-*', '0.1.2-rc.1']。
|
|
20
|
+
*/
|
|
21
|
+
target: string | readonly string[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DshVersionParts {
|
|
25
|
+
/** [major, minor, patch] */
|
|
26
|
+
base: [number, number, number]
|
|
27
|
+
/** 预发布标识数组(rc、alpha、数字…);无预发布 = null */
|
|
28
|
+
pre: string[] | null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const VERSION_RE = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/
|
|
32
|
+
|
|
33
|
+
/** 解析 dsh 版本串;非法返回 null(不支持 x.y / 纯数字 etc.)。 */
|
|
34
|
+
export function parseDshVersion(v: string): DshVersionParts | null {
|
|
35
|
+
const m = VERSION_RE.exec(v.trim())
|
|
36
|
+
if (!m) return null
|
|
37
|
+
const pre = m[4] !== undefined ? m[4].split('.') : null
|
|
38
|
+
if (pre !== null && pre.some((s) => s === '')) return null
|
|
39
|
+
return { base: [Number(m[1]), Number(m[2]), Number(m[3])], pre }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function compareIdent(a: string, b: string): number {
|
|
43
|
+
const na = /^\d+$/.test(a)
|
|
44
|
+
const nb = /^\d+$/.test(b)
|
|
45
|
+
if (na && nb) {
|
|
46
|
+
const x = Number(a)
|
|
47
|
+
const y = Number(b)
|
|
48
|
+
return x === y ? 0 : x < y ? -1 : 1
|
|
49
|
+
}
|
|
50
|
+
if (na) return -1 // 数字标识 < 字母标识
|
|
51
|
+
if (nb) return 1
|
|
52
|
+
return a === b ? 0 : a < b ? -1 : 1
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function comparePre(a: string[], b: string[]): number {
|
|
56
|
+
const n = Math.min(a.length, b.length)
|
|
57
|
+
for (let i = 0; i < n; i++) {
|
|
58
|
+
const c = compareIdent(a[i], b[i])
|
|
59
|
+
if (c !== 0) return c
|
|
60
|
+
}
|
|
61
|
+
return a.length === b.length ? 0 : a.length < b.length ? -1 : 1
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 语义化比较:a < b → -1;相等 → 0;a > b → 1;任一无法解析 → NaN。
|
|
66
|
+
*/
|
|
67
|
+
export function compareDshVersions(a: string, b: string): number {
|
|
68
|
+
const pa = parseDshVersion(a)
|
|
69
|
+
const pb = parseDshVersion(b)
|
|
70
|
+
if (pa === null || pb === null) return NaN
|
|
71
|
+
for (let i = 0; i < 3; i++) {
|
|
72
|
+
if (pa.base[i] !== pb.base[i]) return pa.base[i] < pb.base[i] ? -1 : 1
|
|
73
|
+
}
|
|
74
|
+
if (pa.pre === null && pb.pre === null) return 0
|
|
75
|
+
if (pa.pre === null) return 1 // 正式版高于同基线任意预发布
|
|
76
|
+
if (pb.pre === null) return -1
|
|
77
|
+
return comparePre(pa.pre, pb.pre)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 操作符比较(非法版本一律 false)。 */
|
|
81
|
+
export function checkDshVersion(current: string, op: DshCmpOp, target: string): boolean {
|
|
82
|
+
const c = compareDshVersions(current, target)
|
|
83
|
+
if (Number.isNaN(c)) return false
|
|
84
|
+
switch (op) {
|
|
85
|
+
case '>':
|
|
86
|
+
return c > 0
|
|
87
|
+
case '>=':
|
|
88
|
+
return c >= 0
|
|
89
|
+
case '<':
|
|
90
|
+
return c < 0
|
|
91
|
+
case '<=':
|
|
92
|
+
return c <= 0
|
|
93
|
+
default:
|
|
94
|
+
return c === 0
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 标识段匹配:'*' 吞掉 ≥1 个剩余标识;静态段全等(全数字按数值比较)。 */
|
|
99
|
+
function matchIdents(cur: string[], pat: string[]): boolean {
|
|
100
|
+
const f = (i: number, j: number): boolean => {
|
|
101
|
+
if (i === cur.length && j === pat.length) return true
|
|
102
|
+
if (j === pat.length) return false
|
|
103
|
+
if (pat[j] === '*') {
|
|
104
|
+
if (i >= cur.length) return false // * 至少匹配 1 个
|
|
105
|
+
for (let k = i + 1; k <= cur.length; k++) {
|
|
106
|
+
if (f(k, j + 1)) return true
|
|
107
|
+
}
|
|
108
|
+
return false
|
|
109
|
+
}
|
|
110
|
+
if (i >= cur.length) return false
|
|
111
|
+
if (compareIdent(pat[j], cur[i]) !== 0) return false
|
|
112
|
+
return f(i + 1, j + 1)
|
|
113
|
+
}
|
|
114
|
+
return f(0, 0)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 通配相等(仅配合 op='='):pattern 形如 0.1.1-*(任意预发布)、0.1.1-rc.*、
|
|
119
|
+
* 0.1.*(任意小版本)等;'*' 匹配 ≥1 个标识。例:0.1.1-* 命中 0.1.1-rc.1 / 0.1.1-rc.2。
|
|
120
|
+
*/
|
|
121
|
+
export function wildcardEqual(version: string, pattern: string): boolean {
|
|
122
|
+
const pv = parseDshVersion(version)
|
|
123
|
+
if (pv === null) return false
|
|
124
|
+
const dash = pattern.indexOf('-')
|
|
125
|
+
const basePat = (dash >= 0 ? pattern.slice(0, dash) : pattern).split('.')
|
|
126
|
+
const prePat = dash >= 0 ? pattern.slice(dash + 1).split('.') : null
|
|
127
|
+
if (basePat.length !== 3) return false
|
|
128
|
+
const baseHasStar = basePat.some((t) => t === '*')
|
|
129
|
+
for (let i = 0; i < 3; i++) {
|
|
130
|
+
const t = basePat[i]
|
|
131
|
+
if (t === '*') continue
|
|
132
|
+
if (!/^\d+$/.test(t)) return false
|
|
133
|
+
if (Number(t) !== pv.base[i]) return false
|
|
134
|
+
}
|
|
135
|
+
if (prePat === null) {
|
|
136
|
+
// 无预发布子句:基线精确时要求同为正式版;基线含通配(0.1.*)则任意预发布也算命中
|
|
137
|
+
return baseHasStar || pv.pre === null
|
|
138
|
+
}
|
|
139
|
+
if (pv.pre === null) return false // 模式含预发布通配:当前必须有预发布
|
|
140
|
+
return matchIdents(pv.pre, prePat)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** 单项命中:target 含 '*' 时仅接受 op='=' 的通配比较,否则按操作符数值比较。 */
|
|
144
|
+
function entryPasses(current: string, op: DshCmpOp, target: string): boolean {
|
|
145
|
+
if (target.includes('*')) {
|
|
146
|
+
if (op !== '=') return false
|
|
147
|
+
return wildcardEqual(current, target)
|
|
148
|
+
}
|
|
149
|
+
return checkDshVersion(current, op, target)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** 策略校验:target 为数组时命中任一即通过;通配(如 0.1.1-*)仅在 op='=' 时参与。 */
|
|
153
|
+
export function checkDshPolicy(current: string, policy: DshCompatPolicy): boolean {
|
|
154
|
+
const targets = Array.isArray(policy.target) ? policy.target : [policy.target]
|
|
155
|
+
return targets.some((t) => entryPasses(current, policy.op, t))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** 不兼容时的控制台提示文案(xxx = 检测到的当前 dsh 版本)。 */
|
|
159
|
+
export function dshCompatMessage(pluginName: string, currentDshVersion: string): string {
|
|
160
|
+
return pluginName + ' 可能不适配 dsh ' + currentDshVersion + ' 版本,请慎重使用'
|
|
161
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host half of the dsh-mega-chat-nav plugin — runs in the DSH host process.
|
|
3
|
+
* Registers the `msgNavMessages` session projection unit (the ordered list
|
|
4
|
+
* of user questions, each tagged with its turn) and the plugin's durable
|
|
5
|
+
* settings namespace. Both registries are optional capabilities, so each
|
|
6
|
+
* registration rides `ctx.inject`: without them the host half contributes
|
|
7
|
+
* nothing and the browser strip falls back to live-window questions and
|
|
8
|
+
* default rail alignment.
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync } from 'node:fs'
|
|
11
|
+
import { createRequire } from 'node:module'
|
|
12
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
13
|
+
import { msgNavProjectionDefinition } from './projection.ts'
|
|
14
|
+
import { registerSearchRoute } from './search.ts'
|
|
15
|
+
import { megaChatNavSettingsNamespace, NavSettingsSchema } from './settings.ts'
|
|
16
|
+
import { checkDshPolicy, dshCompatMessage, type DshCompatPolicy } from './compat.ts'
|
|
17
|
+
|
|
18
|
+
/** Cordis plugin name. */
|
|
19
|
+
export const name = 'dsh-mega-chat-nav'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 兼容策略(按需修改):
|
|
23
|
+
* - '= 0.1.1-*' = 支持 dsh-v0.1.1 整条预发布线(0.1.1-rc.1 / rc.2);
|
|
24
|
+
* - target 支持数组:命中任一即通过,如 ['0.1.1', '0.1.2-rc.1'];
|
|
25
|
+
* - 关系约束示例:{ op: '>=', target: '0.1.1-rc.2' }。
|
|
26
|
+
*/
|
|
27
|
+
const DSCH_COMPAT_POLICY: DshCompatPolicy = { op: '=', target: '0.1.1-*' }
|
|
28
|
+
|
|
29
|
+
const dshRequire = createRequire(import.meta.url)
|
|
30
|
+
|
|
31
|
+
/** 探测当前 dsh 版本:@deepseek-ai/dsh 本体 → dsh → dsh-client-runtime(与 tag 线 lockstep) */
|
|
32
|
+
function detectDshVersion(): string | null {
|
|
33
|
+
for (const pkg of ['@deepseek-ai/dsh', 'dsh', '@deepseek-ai/dsh-client-runtime']) {
|
|
34
|
+
try {
|
|
35
|
+
const pkgJson = dshRequire.resolve(pkg + '/package.json')
|
|
36
|
+
const parsed = JSON.parse(readFileSync(pkgJson, 'utf8')) as { version?: string }
|
|
37
|
+
if (typeof parsed.version === 'string' && parsed.version.length > 0) return parsed.version
|
|
38
|
+
} catch {
|
|
39
|
+
/* 尝试下一候选 */
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function apply(ctx: Context): void {
|
|
46
|
+
// dsh 版本兼容校验:检测失败或不合规只打印警示,不阻断插件加载
|
|
47
|
+
try {
|
|
48
|
+
const dshVer = detectDshVersion()
|
|
49
|
+
if (dshVer !== null && !checkDshPolicy(dshVer, DSCH_COMPAT_POLICY)) {
|
|
50
|
+
console.warn(dshCompatMessage(name, dshVer))
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
/* 校验器自身异常不应影响插件 */
|
|
54
|
+
}
|
|
55
|
+
ctx.inject(['sessionProjections'], (inner) => {
|
|
56
|
+
inner.sessionProjections.register(msgNavProjectionDefinition)
|
|
57
|
+
})
|
|
58
|
+
ctx.inject(['settings'], (settingsCtx) => {
|
|
59
|
+
settingsCtx.settings.register(megaChatNavSettingsNamespace, NavSettingsSchema)
|
|
60
|
+
})
|
|
61
|
+
try {
|
|
62
|
+
registerSearchRoute(ctx)
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.error('[dsh-mega-chat-nav] search route failed:', e)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `msgNavMessages` session projection unit: a pure fold of the session
|
|
3
|
+
* event log into the ordered list of user questions, each tagged with the
|
|
4
|
+
* turn that claimed it. Registered on `ctx.sessionProjections` by the host
|
|
5
|
+
* half (src/index.ts); persistence, replay, and client delivery are the
|
|
6
|
+
* projection seam's (session-projection-cache checkpoints the state, the
|
|
7
|
+
* api-proxy carriers seed + push the wire view).
|
|
8
|
+
*
|
|
9
|
+
* Fold rules mirror the chat messageDefinition classification: an
|
|
10
|
+
* append-origin `user/message` with a human (`user`) source is a question;
|
|
11
|
+
* replacement copies (compaction checkpoints) and injected context are not.
|
|
12
|
+
* Turns come from `turn/start` boundaries, so retry/goal-continuation turns
|
|
13
|
+
* without a question simply produce no entry — dots may skip turn numbers,
|
|
14
|
+
* staying exactly aligned with the Trajectory view's turn labels.
|
|
15
|
+
*
|
|
16
|
+
* @module dsh-mega-chat-nav/projection
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { z } from 'zod'
|
|
20
|
+
import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection'
|
|
21
|
+
|
|
22
|
+
/** 注册本投影单元到框架的合并扩展表(接口声明于 /types 出口,须在声明处合并) */
|
|
23
|
+
declare module '@deepseek-ai/dsh-session-projection/types' {
|
|
24
|
+
interface SessionProjectionStateMap {
|
|
25
|
+
msgNavMessages: QuestionIndexState
|
|
26
|
+
}
|
|
27
|
+
interface SessionProjectionMap {
|
|
28
|
+
msgNavMessages: NavItem[]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 投影条目:一条 user/message 提问的轻量索引(正文零驻留) */
|
|
33
|
+
export interface NavItem {
|
|
34
|
+
/** 归属回合号(turn/start 之后的第一个提问) */
|
|
35
|
+
turn: number
|
|
36
|
+
/** 消息稳定 id(跳转锚点推导源) */
|
|
37
|
+
id: string
|
|
38
|
+
/** 事件 seq(排序与锚点序号) */
|
|
39
|
+
seq: number
|
|
40
|
+
/** Unix 毫秒时间戳 */
|
|
41
|
+
time: number
|
|
42
|
+
/** 首文本块(悬停/级联卡正文) */
|
|
43
|
+
text: string
|
|
44
|
+
/** 该轮性能指标(view 附加;缺数据时省略) */
|
|
45
|
+
metrics?: { durationMs: number; firstTokenMs: number; tokensPerSec: number }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Fold state: the last opened turn plus every question recorded so far. */
|
|
49
|
+
/** 一轮的性能指标(由 turn/start、assistant/chunk、assistant/message 折叠) */
|
|
50
|
+
interface TurnMetric {
|
|
51
|
+
/** 轮开始时间(turn/start) */
|
|
52
|
+
startedAt: number
|
|
53
|
+
/** 首个内容增量块时间(assistant/chunk 首 text/reasoning delta);未流式时为 null */
|
|
54
|
+
firstTokenAt: number | null
|
|
55
|
+
/** 轮结束时间(assistant/message);未完成时为 null */
|
|
56
|
+
finishedAt: number | null
|
|
57
|
+
/** 累计输出 token(assistant/message 的 usage.outputTokens 求和) */
|
|
58
|
+
outputTokens: number
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface QuestionIndexState {
|
|
62
|
+
/** Turn of the last `turn/start` (0 before any; turns are 1-based). */
|
|
63
|
+
turn: number
|
|
64
|
+
/** Every recorded question, in event order. */
|
|
65
|
+
questions: NavItem[]
|
|
66
|
+
/** 每轮性能指标(turn 号 → 指标) */
|
|
67
|
+
metrics: Record<number, TurnMetric>
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const navEntrySchema = z.object({
|
|
71
|
+
turn: z.number(),
|
|
72
|
+
id: z.string(),
|
|
73
|
+
seq: z.number(),
|
|
74
|
+
time: z.number(),
|
|
75
|
+
text: z.string(),
|
|
76
|
+
metrics: z.object({
|
|
77
|
+
durationMs: z.number(),
|
|
78
|
+
firstTokenMs: z.number(),
|
|
79
|
+
tokensPerSec: z.number(),
|
|
80
|
+
}).optional(),
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const turnMetricSchema = z.object({
|
|
84
|
+
startedAt: z.number(),
|
|
85
|
+
firstTokenAt: z.number().nullable(),
|
|
86
|
+
finishedAt: z.number().nullable(),
|
|
87
|
+
outputTokens: z.number(),
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const msgNavMessagesStateSchema = z.object({
|
|
91
|
+
turn: z.number(),
|
|
92
|
+
questions: z.array(navEntrySchema),
|
|
93
|
+
metrics: z.record(z.number(), turnMetricSchema),
|
|
94
|
+
}).strict()
|
|
95
|
+
|
|
96
|
+
/** Validates the wire payload before it leaves the host. */
|
|
97
|
+
const msgNavMessagesViewSchema = z.array(navEntrySchema)
|
|
98
|
+
|
|
99
|
+
/** First text block of a user message; empty string when absent. */
|
|
100
|
+
function messageText(content: readonly { type?: string; text?: string }[] | undefined): string {
|
|
101
|
+
const block = content?.find((part) => typeof part?.text === 'string')
|
|
102
|
+
return block?.text ?? ''
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The `msgNavMessages` unit registered on `ctx.sessionProjections`. */
|
|
106
|
+
export const msgNavProjectionDefinition: Omit<ProjectionDefinition<'msgNavMessages', QuestionIndexState>, 'wire'> & {
|
|
107
|
+
wire: NonNullable<ProjectionDefinition<'msgNavMessages', QuestionIndexState>['wire']>
|
|
108
|
+
} = {
|
|
109
|
+
key: 'msgNavMessages',
|
|
110
|
+
stateVersion: 2,
|
|
111
|
+
stateSchema: msgNavMessagesStateSchema,
|
|
112
|
+
init: () => ({ turn: 0, questions: [], metrics: {} }),
|
|
113
|
+
apply: (state: QuestionIndexState, event) => {
|
|
114
|
+
// Every uninteresting event returns the same reference (Object.is gates
|
|
115
|
+
// the change feed and the persisted-cache dirty check).
|
|
116
|
+
switch (event.type) {
|
|
117
|
+
case 'turn/start': {
|
|
118
|
+
if (event.data.turn === state.turn) return state
|
|
119
|
+
const metrics = { ...state.metrics }
|
|
120
|
+
metrics[event.data.turn] = { startedAt: event.time, firstTokenAt: null, finishedAt: null, outputTokens: 0 }
|
|
121
|
+
return { ...state, turn: event.data.turn, metrics }
|
|
122
|
+
}
|
|
123
|
+
case 'user/message': {
|
|
124
|
+
if (event.surfaceOp !== 'append') return state
|
|
125
|
+
if (event.data.source?.kind !== 'user') return state
|
|
126
|
+
const entry: NavItem = {
|
|
127
|
+
turn: state.turn,
|
|
128
|
+
id: String(event.data.id),
|
|
129
|
+
seq: event.seq,
|
|
130
|
+
time: event.time,
|
|
131
|
+
text: messageText(event.data.content),
|
|
132
|
+
}
|
|
133
|
+
return { ...state, questions: [...state.questions, entry] }
|
|
134
|
+
}
|
|
135
|
+
case 'assistant/chunk': {
|
|
136
|
+
// 只记录每轮首个内容增量块(首 token 时间);后续增量块不产生新 state
|
|
137
|
+
const turn = event.data.turn
|
|
138
|
+
const chunk = event.data.chunk
|
|
139
|
+
const current = state.metrics[turn]
|
|
140
|
+
if (current === undefined) return state
|
|
141
|
+
if (current.firstTokenAt !== null) return state
|
|
142
|
+
if (chunk?.type !== 'text-delta' && chunk?.type !== 'reasoning-delta') return state
|
|
143
|
+
const metrics = { ...state.metrics, [turn]: { ...current, firstTokenAt: event.time } }
|
|
144
|
+
return { ...state, metrics }
|
|
145
|
+
}
|
|
146
|
+
case 'assistant/message': {
|
|
147
|
+
const turn = event.data.turn
|
|
148
|
+
const current = state.metrics[turn]
|
|
149
|
+
if (current === undefined) return state
|
|
150
|
+
const output = typeof event.data.usage?.outputTokens === 'number' ? event.data.usage.outputTokens : 0
|
|
151
|
+
if (output === 0 && current.finishedAt !== null) return state
|
|
152
|
+
const metrics = {
|
|
153
|
+
...state.metrics,
|
|
154
|
+
[turn]: { ...current, finishedAt: event.time, outputTokens: current.outputTokens + output },
|
|
155
|
+
}
|
|
156
|
+
return { ...state, metrics }
|
|
157
|
+
}
|
|
158
|
+
default:
|
|
159
|
+
return state
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
wire: {
|
|
163
|
+
viewSchema: msgNavMessagesViewSchema,
|
|
164
|
+
view: (state: QuestionIndexState) => state.questions.map((q) => {
|
|
165
|
+
const m = state.metrics[q.turn]
|
|
166
|
+
if (m === undefined || m.finishedAt === null || m.startedAt <= 0) return q
|
|
167
|
+
const durationMs = m.finishedAt - m.startedAt
|
|
168
|
+
if (durationMs <= 0) return q
|
|
169
|
+
const firstTokenMs = m.firstTokenAt !== null ? m.firstTokenAt - m.startedAt : 0
|
|
170
|
+
const tokensPerSec = durationMs > 0 ? Math.round((m.outputTokens * 1000) / durationMs) : 0
|
|
171
|
+
return { ...q, metrics: { durationMs, firstTokenMs, tokensPerSec } }
|
|
172
|
+
}),
|
|
173
|
+
},
|
|
174
|
+
}
|