dsh-manage-session 0.5.1 → 0.5.3
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/CHANGELOG.ja.md +207 -242
- package/CHANGELOG.ko.md +207 -242
- package/CHANGELOG.md +337 -341
- package/INSTALL.ja.md +164 -179
- package/INSTALL.ko.md +164 -179
- package/INSTALL.md +164 -179
- package/INSTALL.zh.md +164 -179
- package/LICENSE +21 -21
- package/README.en.md +4 -12
- package/README.ja.md +0 -12
- package/README.ko.md +0 -12
- package/README.md +4 -14
- package/cordis.patch.yml +16 -11
- package/dsh.plugin.json +4 -4
- package/lib/client.js +3882 -690
- package/lib/index.d.ts +759 -29
- package/lib/index.js +2604 -93
- package/locale/de.json +6 -0
- package/locale/en.json +6 -0
- package/locale/es.json +6 -0
- package/locale/fr.json +6 -0
- package/locale/it.json +6 -0
- package/locale/ja.json +6 -0
- package/locale/ko.json +6 -0
- package/locale/ru.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +43 -32
- package/src/client/bridge-scope.ts +213 -0
- package/src/client/card.tsx +150 -150
- package/src/client/history-panel.tsx +319 -319
- package/src/client/index.ts +465 -354
- package/src/client/locales.ts +857 -698
- package/src/client/panel.tsx +139 -118
- package/src/client/search/card.tsx +563 -0
- package/src/client/search/host-api.ts +208 -0
- package/src/client/search/locales.ts +780 -0
- package/src/client/search/manage.tsx +400 -0
- package/src/client/search/panel.tsx +815 -0
- package/src/client/search/platform.ts +136 -0
- package/src/config.ts +82 -33
- package/src/host/health/attribution.ts +118 -118
- package/src/host/health/lossless.ts +128 -128
- package/src/host/health/source-kind.ts +350 -350
- package/src/host/history/archive-source.ts +250 -202
- package/src/host/history/archive.ts +249 -194
- package/src/host/history/favorites.ts +71 -0
- package/src/host/history/purge.ts +370 -370
- package/src/host/index/archive-source.ts +90 -0
- package/src/host/index/engine.ts +571 -0
- package/src/host/index/extract.ts +272 -0
- package/src/host/index/peers.ts +62 -0
- package/src/host/index/rebuild.ts +368 -0
- package/src/host/index/schema.ts +136 -0
- package/src/host/index/snapshot.ts +135 -0
- package/src/host/index/sync.ts +286 -0
- package/src/host/registry/archive-registry.ts +317 -0
- package/src/host/settings-bridge.ts +237 -0
- package/src/index.ts +1421 -532
- package/README.de.md +0 -120
- package/README.es.md +0 -120
- package/README.fr.md +0 -120
- package/README.it.md +0 -120
- package/README.ru.md +0 -120
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform detection and the keyboard-label vocabulary, for the client half.
|
|
3
|
+
*
|
|
4
|
+
* Why this module exists
|
|
5
|
+
* ----------------------
|
|
6
|
+
* A shortcut hint is the one place where a single hard-coded string is wrong
|
|
7
|
+
* for most users: `⌘K` is meaningless on Windows, `Ctrl K` is wrong on macOS.
|
|
8
|
+
* The obvious implementation — one `navigator.platform` test — is also the
|
|
9
|
+
* fragile one: that API is deprecated, and it returns an empty string in some
|
|
10
|
+
* privacy configurations, which silently degrades every Mac user to the
|
|
11
|
+
* Windows vocabulary without any error to notice.
|
|
12
|
+
*
|
|
13
|
+
* This module is the single owner of both the platform answer and every label
|
|
14
|
+
* derived from it. The hint rendered in the sidebar, the hint rendered in the
|
|
15
|
+
* panel footer, and the chord the keydown handler actually matches all read
|
|
16
|
+
* from here — so a platform fix cannot land in one of them and miss another,
|
|
17
|
+
* and the advertised key cannot drift away from the bound key.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** The navigator fields the detection reads (all optional, all untrusted). */
|
|
21
|
+
export interface NavigatorLike {
|
|
22
|
+
userAgentData?: { platform?: unknown }
|
|
23
|
+
platform?: unknown
|
|
24
|
+
userAgent?: unknown
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the running platform string, three tiers deep.
|
|
29
|
+
*
|
|
30
|
+
* 1. UA-CH (`navigator.userAgentData.platform`) — modern and precise.
|
|
31
|
+
* 2. `navigator.platform` — broadly available, deprecated, and empty in some
|
|
32
|
+
* privacy modes.
|
|
33
|
+
* 3. The UA string — last resort, still non-empty in practice.
|
|
34
|
+
*
|
|
35
|
+
* @param nav - the navigator to read; `undefined` is allowed (non-DOM host).
|
|
36
|
+
* @returns a lower-cased platform string, or `''` when nothing could be read.
|
|
37
|
+
*/
|
|
38
|
+
export function detectPlatform(nav: NavigatorLike | undefined): string {
|
|
39
|
+
const chPlatform = nav?.userAgentData?.platform
|
|
40
|
+
if (typeof chPlatform === 'string' && chPlatform !== '') return chPlatform.toLowerCase()
|
|
41
|
+
const legacy = nav?.platform
|
|
42
|
+
if (typeof legacy === 'string' && legacy !== '') return legacy.toLowerCase()
|
|
43
|
+
const agent = nav?.userAgent
|
|
44
|
+
if (typeof agent === 'string' && agent !== '') return agent.toLowerCase()
|
|
45
|
+
return ''
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The keyboard vocabulary for one platform. */
|
|
49
|
+
export interface PlatformLabels {
|
|
50
|
+
/** The lower-cased platform string these labels were derived from. */
|
|
51
|
+
platform: string
|
|
52
|
+
isMac: boolean
|
|
53
|
+
isWindows: boolean
|
|
54
|
+
/** Invoke-key modifier: `⌘` on macOS, `Ctrl` elsewhere. */
|
|
55
|
+
modLabel: string
|
|
56
|
+
/** Secondary modifier: `⌥` on macOS, `Alt` elsewhere. */
|
|
57
|
+
altLabel: string
|
|
58
|
+
/** Shift: `⇧` on macOS, `Shift` elsewhere. */
|
|
59
|
+
shiftLabel: string
|
|
60
|
+
/** Confirm: the arrow glyph is legible on every platform. */
|
|
61
|
+
enterLabel: string
|
|
62
|
+
/** Escape: `esc` is the macOS spelling, `Esc` the one everywhere else. */
|
|
63
|
+
escLabel: string
|
|
64
|
+
/**
|
|
65
|
+
* The whole invoke chord as one face, ready for a key chip: `⌘K` on macOS,
|
|
66
|
+
* `Ctrl K` on Windows and Linux. `Ctrl K` keeps the space because `CtrlK`
|
|
67
|
+
* reads as a word rather than as two keys.
|
|
68
|
+
*/
|
|
69
|
+
invokeLabel: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Derive the label vocabulary from a platform string.
|
|
74
|
+
*
|
|
75
|
+
* An unrecognised platform — an empty string, or a UA that names neither
|
|
76
|
+
* system — takes the non-Mac vocabulary on purpose: `Ctrl` is legible to
|
|
77
|
+
* everyone, while `⌘` is opaque to anyone who has never used a Mac.
|
|
78
|
+
*
|
|
79
|
+
* @param platform - the string produced by {@link detectPlatform}.
|
|
80
|
+
* @returns the label vocabulary for that platform.
|
|
81
|
+
*/
|
|
82
|
+
export function platformLabels(platform: string): PlatformLabels {
|
|
83
|
+
const isMac = /mac|iphone|ipad|ipod/u.test(platform)
|
|
84
|
+
const isWindows = /win/u.test(platform)
|
|
85
|
+
const modLabel = isMac ? '⌘' : 'Ctrl'
|
|
86
|
+
return {
|
|
87
|
+
platform,
|
|
88
|
+
isMac,
|
|
89
|
+
isWindows,
|
|
90
|
+
modLabel,
|
|
91
|
+
altLabel: isMac ? '⌥' : 'Alt',
|
|
92
|
+
shiftLabel: isMac ? '⇧' : 'Shift',
|
|
93
|
+
enterLabel: '↵',
|
|
94
|
+
escLabel: isMac ? 'esc' : 'Esc',
|
|
95
|
+
invokeLabel: isMac ? `${modLabel}K` : `${modLabel} K`,
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Read `globalThis.navigator` without assuming a DOM (or a DOM lib). */
|
|
100
|
+
function currentNavigator(): NavigatorLike | undefined {
|
|
101
|
+
const candidate = (globalThis as { navigator?: unknown }).navigator
|
|
102
|
+
if (candidate === null || typeof candidate !== 'object') return undefined
|
|
103
|
+
return candidate as NavigatorLike
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The labels for the platform this bundle is running on. */
|
|
107
|
+
export const LABELS: PlatformLabels = platformLabels(detectPlatform(currentNavigator()))
|
|
108
|
+
|
|
109
|
+
/** The minimal keydown face the chord test reads. */
|
|
110
|
+
export interface KeyChordEvent {
|
|
111
|
+
key: string
|
|
112
|
+
ctrlKey: boolean
|
|
113
|
+
metaKey: boolean
|
|
114
|
+
altKey: boolean
|
|
115
|
+
shiftKey: boolean
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Whether a keydown event is the invoke chord advertised by
|
|
120
|
+
* {@link PlatformLabels.invokeLabel}.
|
|
121
|
+
*
|
|
122
|
+
* The hint and the binding have to agree: a footer promising `⌘K` while the
|
|
123
|
+
* handler matches `Ctrl+K` is worse than showing no hint at all. Both sides
|
|
124
|
+
* call this, so neither can be changed alone. Requiring the other modifier to
|
|
125
|
+
* be *absent* keeps the two chords distinct instead of letting either one
|
|
126
|
+
* satisfy both platforms.
|
|
127
|
+
*
|
|
128
|
+
* @param event - the keydown payload.
|
|
129
|
+
* @param isMac - the running platform's macOS-ness, from {@link LABELS}.
|
|
130
|
+
* @returns `true` when the event is the invoke chord.
|
|
131
|
+
*/
|
|
132
|
+
export function isInvokeChord(event: KeyChordEvent, isMac: boolean): boolean {
|
|
133
|
+
if (event.key !== 'k' && event.key !== 'K') return false
|
|
134
|
+
if (event.altKey || event.shiftKey) return false
|
|
135
|
+
return isMac ? event.metaKey && !event.ctrlKey : event.ctrlKey && !event.metaKey
|
|
136
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -1,33 +1,82 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-manage-session
|
|
3
|
-
*
|
|
4
|
-
* 与 host 半身(schemastery schema 在 src/index.ts)以及浏览器半身共用同一形状,
|
|
5
|
-
* 使一处 admitted 的值在另一处也 admitted
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
export const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
1
|
+
/**
|
|
2
|
+
* dsh-manage-session 的共享配置面(合并包:管家子域 + 搜索索引子域)。
|
|
3
|
+
*
|
|
4
|
+
* 与 host 半身(schemastery schema 在 src/index.ts)以及浏览器半身共用同一形状,
|
|
5
|
+
* 使一处 admitted 的值在另一处也 admitted。
|
|
6
|
+
*
|
|
7
|
+
* 两个设置命名空间**并存且互不迁移**:
|
|
8
|
+
* - `session-steward` —— 管家子域(enabled/historyFiles/healthCheck/search 等共享键);
|
|
9
|
+
* - `switch-search` —— 搜索子域的历史命名空间,存储键承诺保持稳定(见
|
|
10
|
+
* dsh-search-index README「存储键保持稳定,不做迁移」);0.1.7 起两者实际
|
|
11
|
+
* 都落在同一 composition entry(configForms),旧宿主才按命名空间分流。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** 会话管家运行时配置(设置命名空间 + 组合入口)。 */
|
|
15
|
+
export interface StewardConfig {
|
|
16
|
+
/** 插件总开关(合并包唯一 master:关闭后不注册任何子域路由)。 */
|
|
17
|
+
enabled: boolean
|
|
18
|
+
/** 会话历史文件(归档浏览与清理)。关闭后不注册 history 路由、不渲染「养老院」页签。 */
|
|
19
|
+
historyFiles?: boolean
|
|
20
|
+
/** 会话健康检查(体检 → 处方 → 出院)。关闭后不注册 health 路由、不渲染「体检」页签。 */
|
|
21
|
+
healthCheck?: boolean
|
|
22
|
+
/** 搜索索引子域(`index-*` 路由与侧栏搜索入口)。关闭后索引子域整体停摆。 */
|
|
23
|
+
search?: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 缺省值。 */
|
|
27
|
+
export const DEFAULT_CONFIG: Required<StewardConfig> = {
|
|
28
|
+
enabled: true,
|
|
29
|
+
historyFiles: true,
|
|
30
|
+
healthCheck: true,
|
|
31
|
+
search: true,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** host 半身注册的设置命名空间(与 src/index.ts 保持一致)。 */
|
|
35
|
+
export const STEWARD_SETTINGS_NAMESPACE = 'session-steward'
|
|
36
|
+
|
|
37
|
+
/** 侧边栏入口 id(客户端注册 id)。 */
|
|
38
|
+
export const STEWARD_ENTRY_ID = 'dsh-manage-session'
|
|
39
|
+
|
|
40
|
+
/** host 路由前缀(管家子域;与搜索子域的 /switch-search/api 互不干扰)。 */
|
|
41
|
+
export const STEWARD_API_PREFIX = '/session-steward/api'
|
|
42
|
+
|
|
43
|
+
/** ------------------------------------------------------------------ 搜索子域 */
|
|
44
|
+
|
|
45
|
+
/** 搜索子域的历史设置命名空间(存储键稳定承诺,永不迁移)。 */
|
|
46
|
+
export const SWITCH_SEARCH_SETTINGS_NAMESPACE = 'switch-search'
|
|
47
|
+
|
|
48
|
+
/** 搜索子域路由前缀(历史值,浏览器旧 bundle 与快照文件名依赖它,保持不变)。 */
|
|
49
|
+
export const SWITCH_API_PREFIX = '/switch-search/api'
|
|
50
|
+
|
|
51
|
+
/** 搜索面板打开时的默认模式。 */
|
|
52
|
+
export type SwitchSearchDefaultMode = 'title' | 'content'
|
|
53
|
+
|
|
54
|
+
/** 搜索子域可调配置(独立于 StewardConfig 的历史形状,字段名保持稳定)。 */
|
|
55
|
+
export interface SwitchSearchConfig {
|
|
56
|
+
/**
|
|
57
|
+
* 搜索卡上的「启用」开关。合并包里它与管家 `enabled` 是**同一个**组合入口
|
|
58
|
+
* 字段(0.1.7 configForms 单 scope);保留在此处是为了 legacy settingsScope
|
|
59
|
+
* 回退路径(旧 switch-search 命名空间的存储形状)与设置卡类型不变。
|
|
60
|
+
*/
|
|
61
|
+
enabled?: boolean
|
|
62
|
+
/** 面板默认模式。 */
|
|
63
|
+
defaultMode: SwitchSearchDefaultMode
|
|
64
|
+
/** 独立索引是否后台自动同步(水位轮询)。 */
|
|
65
|
+
autoSync?: boolean
|
|
66
|
+
/** 增量同步间隔(ms)。 */
|
|
67
|
+
syncIntervalMs?: number
|
|
68
|
+
/** 整理索引后保留的归档份数。 */
|
|
69
|
+
archiveKeep?: number
|
|
70
|
+
/** 独立索引文件的可选绝对目录。 */
|
|
71
|
+
indexDir?: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 搜索子域缺省值。 */
|
|
75
|
+
export const SWITCH_DEFAULT_CONFIG: Required<SwitchSearchConfig> = {
|
|
76
|
+
enabled: true,
|
|
77
|
+
defaultMode: 'title',
|
|
78
|
+
autoSync: true,
|
|
79
|
+
syncIntervalMs: 30_000,
|
|
80
|
+
archiveKeep: 2,
|
|
81
|
+
indexDir: '',
|
|
82
|
+
}
|
|
@@ -1,118 +1,118 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 归属:把投影 key 指回产出它的插件包。
|
|
3
|
-
*
|
|
4
|
-
* 做法是静态扫描 profile 的 `node_modules`:`<pkg>/lib/index.js` 里
|
|
5
|
-
* `sessionProjections.register({ key: 'xxx' })`(或其定义工厂)会命中
|
|
6
|
-
* `key:\s*['"](\w+)['"]` 正则;据此建立 `投影 key → 包名` 索引。
|
|
7
|
-
* `@deepseek-ai/*` 视为内置(core);扫不到时如实返回 `unknown`,禁止臆测。
|
|
8
|
-
*/
|
|
9
|
-
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
10
|
-
import { homedir } from 'node:os'
|
|
11
|
-
import { join } from 'node:path'
|
|
12
|
-
import type { GateAttribution } from './gates.ts'
|
|
13
|
-
|
|
14
|
-
/** 一条归属记录。 */
|
|
15
|
-
export interface ProjectionOwner {
|
|
16
|
-
key: string
|
|
17
|
-
/** 包名;内置包以 `@deepseek-ai/` 前缀判定。 */
|
|
18
|
-
package: string
|
|
19
|
-
/** 'core' 表示内置包,'plugin' 表示第三方插件。 */
|
|
20
|
-
kind: 'core' | 'plugin'
|
|
21
|
-
/** 命中文件(相对 node_modules)。 */
|
|
22
|
-
entry: string
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** 默认扫描根:profile 的 node_modules。 */
|
|
26
|
-
export function defaultProfileNodeModules(dshHome?: string): string {
|
|
27
|
-
return join(dshHome ?? join(homedir(), '.dsh'), 'profiles', 'web', 'node_modules')
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** 收集候选包目录(含 @scope/pkg 一层)。 */
|
|
31
|
-
function candidatePackages(root: string): { name: string; dir: string }[] {
|
|
32
|
-
const out: { name: string; dir: string }[] = []
|
|
33
|
-
if (!existsSync(root)) return out
|
|
34
|
-
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
35
|
-
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue
|
|
36
|
-
if (entry.name.startsWith('.')) continue
|
|
37
|
-
const dir = join(root, entry.name)
|
|
38
|
-
if (entry.name.startsWith('@')) {
|
|
39
|
-
let scoped: string[] = []
|
|
40
|
-
try {
|
|
41
|
-
scoped = readdirSync(dir)
|
|
42
|
-
} catch { continue }
|
|
43
|
-
for (const child of scoped) {
|
|
44
|
-
out.push({ name: `${entry.name}/${child}`, dir: join(dir, child) })
|
|
45
|
-
}
|
|
46
|
-
continue
|
|
47
|
-
}
|
|
48
|
-
out.push({ name: entry.name, dir })
|
|
49
|
-
}
|
|
50
|
-
return out
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** 单个入口文件的最大扫描字节数(超大 bundle 直接跳过,避免无谓内存与耗时)。 */
|
|
54
|
-
const MAX_SCAN_BYTES = 12 << 20
|
|
55
|
-
|
|
56
|
-
/** 扫描若干候选入口文件里出现的投影 key。 */
|
|
57
|
-
function keysInFile(file: string): string[] {
|
|
58
|
-
if (!existsSync(file)) return []
|
|
59
|
-
let text: string
|
|
60
|
-
try {
|
|
61
|
-
if (statSync(file).size > MAX_SCAN_BYTES) return []
|
|
62
|
-
text = readFileSync(file, 'utf8')
|
|
63
|
-
} catch {
|
|
64
|
-
return []
|
|
65
|
-
}
|
|
66
|
-
const keys = new Set<string>()
|
|
67
|
-
const pattern = /key:\s*['"]([A-Za-z_$][\w$]*)['"]/g
|
|
68
|
-
let match: RegExpExecArray | null
|
|
69
|
-
while ((match = pattern.exec(text)) !== null) {
|
|
70
|
-
const key = match[1]
|
|
71
|
-
if (key !== undefined && key !== '') keys.add(key)
|
|
72
|
-
}
|
|
73
|
-
return [...keys]
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* 建立投影 key → 包 的索引。
|
|
78
|
-
* @param nodeModulesRoot - profile 的 node_modules 根。
|
|
79
|
-
* @returns 归属记录(同一 key 多包命中时保留首个并排序稳定)。
|
|
80
|
-
*/
|
|
81
|
-
export function buildProjectionOwnerIndex(nodeModulesRoot: string): ProjectionOwner[] {
|
|
82
|
-
const records: ProjectionOwner[] = []
|
|
83
|
-
const seen = new Set<string>()
|
|
84
|
-
for (const pack of candidatePackages(nodeModulesRoot)) {
|
|
85
|
-
const lib = join(pack.dir, 'lib')
|
|
86
|
-
let entries: string[] = []
|
|
87
|
-
try {
|
|
88
|
-
if (!statSync(lib).isDirectory()) continue
|
|
89
|
-
entries = readdirSync(lib).filter(name => name.endsWith('.js'))
|
|
90
|
-
} catch {
|
|
91
|
-
continue
|
|
92
|
-
}
|
|
93
|
-
for (const name of entries) {
|
|
94
|
-
const file = join(lib, name)
|
|
95
|
-
for (const key of keysInFile(file)) {
|
|
96
|
-
if (seen.has(key)) continue
|
|
97
|
-
seen.add(key)
|
|
98
|
-
records.push({
|
|
99
|
-
key,
|
|
100
|
-
package: pack.name,
|
|
101
|
-
kind: pack.name.startsWith('@deepseek-ai/') ? 'core' : 'plugin',
|
|
102
|
-
entry: `${pack.name}/lib/${name}`,
|
|
103
|
-
})
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return records.sort((left, right) => left.key.localeCompare(right.key))
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** 索引查询器。 */
|
|
111
|
-
export function createAttributor(index: readonly ProjectionOwner[]): (projection: string) => GateAttribution | undefined {
|
|
112
|
-
const byKey = new Map(index.map(record => [record.key, record]))
|
|
113
|
-
return (projection: string) => {
|
|
114
|
-
const owner = byKey.get(projection)
|
|
115
|
-
if (owner === undefined) return { projection, package: 'unknown' }
|
|
116
|
-
return { projection, package: owner.package }
|
|
117
|
-
}
|
|
118
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 归属:把投影 key 指回产出它的插件包。
|
|
3
|
+
*
|
|
4
|
+
* 做法是静态扫描 profile 的 `node_modules`:`<pkg>/lib/index.js` 里
|
|
5
|
+
* `sessionProjections.register({ key: 'xxx' })`(或其定义工厂)会命中
|
|
6
|
+
* `key:\s*['"](\w+)['"]` 正则;据此建立 `投影 key → 包名` 索引。
|
|
7
|
+
* `@deepseek-ai/*` 视为内置(core);扫不到时如实返回 `unknown`,禁止臆测。
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
10
|
+
import { homedir } from 'node:os'
|
|
11
|
+
import { join } from 'node:path'
|
|
12
|
+
import type { GateAttribution } from './gates.ts'
|
|
13
|
+
|
|
14
|
+
/** 一条归属记录。 */
|
|
15
|
+
export interface ProjectionOwner {
|
|
16
|
+
key: string
|
|
17
|
+
/** 包名;内置包以 `@deepseek-ai/` 前缀判定。 */
|
|
18
|
+
package: string
|
|
19
|
+
/** 'core' 表示内置包,'plugin' 表示第三方插件。 */
|
|
20
|
+
kind: 'core' | 'plugin'
|
|
21
|
+
/** 命中文件(相对 node_modules)。 */
|
|
22
|
+
entry: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 默认扫描根:profile 的 node_modules。 */
|
|
26
|
+
export function defaultProfileNodeModules(dshHome?: string): string {
|
|
27
|
+
return join(dshHome ?? join(homedir(), '.dsh'), 'profiles', 'web', 'node_modules')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 收集候选包目录(含 @scope/pkg 一层)。 */
|
|
31
|
+
function candidatePackages(root: string): { name: string; dir: string }[] {
|
|
32
|
+
const out: { name: string; dir: string }[] = []
|
|
33
|
+
if (!existsSync(root)) return out
|
|
34
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
35
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue
|
|
36
|
+
if (entry.name.startsWith('.')) continue
|
|
37
|
+
const dir = join(root, entry.name)
|
|
38
|
+
if (entry.name.startsWith('@')) {
|
|
39
|
+
let scoped: string[] = []
|
|
40
|
+
try {
|
|
41
|
+
scoped = readdirSync(dir)
|
|
42
|
+
} catch { continue }
|
|
43
|
+
for (const child of scoped) {
|
|
44
|
+
out.push({ name: `${entry.name}/${child}`, dir: join(dir, child) })
|
|
45
|
+
}
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
out.push({ name: entry.name, dir })
|
|
49
|
+
}
|
|
50
|
+
return out
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 单个入口文件的最大扫描字节数(超大 bundle 直接跳过,避免无谓内存与耗时)。 */
|
|
54
|
+
const MAX_SCAN_BYTES = 12 << 20
|
|
55
|
+
|
|
56
|
+
/** 扫描若干候选入口文件里出现的投影 key。 */
|
|
57
|
+
function keysInFile(file: string): string[] {
|
|
58
|
+
if (!existsSync(file)) return []
|
|
59
|
+
let text: string
|
|
60
|
+
try {
|
|
61
|
+
if (statSync(file).size > MAX_SCAN_BYTES) return []
|
|
62
|
+
text = readFileSync(file, 'utf8')
|
|
63
|
+
} catch {
|
|
64
|
+
return []
|
|
65
|
+
}
|
|
66
|
+
const keys = new Set<string>()
|
|
67
|
+
const pattern = /key:\s*['"]([A-Za-z_$][\w$]*)['"]/g
|
|
68
|
+
let match: RegExpExecArray | null
|
|
69
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
70
|
+
const key = match[1]
|
|
71
|
+
if (key !== undefined && key !== '') keys.add(key)
|
|
72
|
+
}
|
|
73
|
+
return [...keys]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 建立投影 key → 包 的索引。
|
|
78
|
+
* @param nodeModulesRoot - profile 的 node_modules 根。
|
|
79
|
+
* @returns 归属记录(同一 key 多包命中时保留首个并排序稳定)。
|
|
80
|
+
*/
|
|
81
|
+
export function buildProjectionOwnerIndex(nodeModulesRoot: string): ProjectionOwner[] {
|
|
82
|
+
const records: ProjectionOwner[] = []
|
|
83
|
+
const seen = new Set<string>()
|
|
84
|
+
for (const pack of candidatePackages(nodeModulesRoot)) {
|
|
85
|
+
const lib = join(pack.dir, 'lib')
|
|
86
|
+
let entries: string[] = []
|
|
87
|
+
try {
|
|
88
|
+
if (!statSync(lib).isDirectory()) continue
|
|
89
|
+
entries = readdirSync(lib).filter(name => name.endsWith('.js'))
|
|
90
|
+
} catch {
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
for (const name of entries) {
|
|
94
|
+
const file = join(lib, name)
|
|
95
|
+
for (const key of keysInFile(file)) {
|
|
96
|
+
if (seen.has(key)) continue
|
|
97
|
+
seen.add(key)
|
|
98
|
+
records.push({
|
|
99
|
+
key,
|
|
100
|
+
package: pack.name,
|
|
101
|
+
kind: pack.name.startsWith('@deepseek-ai/') ? 'core' : 'plugin',
|
|
102
|
+
entry: `${pack.name}/lib/${name}`,
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return records.sort((left, right) => left.key.localeCompare(right.key))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 索引查询器。 */
|
|
111
|
+
export function createAttributor(index: readonly ProjectionOwner[]): (projection: string) => GateAttribution | undefined {
|
|
112
|
+
const byKey = new Map(index.map(record => [record.key, record]))
|
|
113
|
+
return (projection: string) => {
|
|
114
|
+
const owner = byKey.get(projection)
|
|
115
|
+
if (owner === undefined) return { projection, package: 'unknown' }
|
|
116
|
+
return { projection, package: owner.package }
|
|
117
|
+
}
|
|
118
|
+
}
|