dsh-manage-session 2.0.3 → 2.0.5
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 +33 -33
- package/CHANGELOG.ko.md +33 -33
- package/CHANGELOG.md +33 -33
- package/INSTALL.ja.md +164 -164
- package/INSTALL.ko.md +164 -164
- package/INSTALL.md +164 -164
- package/INSTALL.zh.md +164 -164
- package/LICENSE +21 -21
- package/README.en.md +76 -62
- package/README.ja.md +53 -39
- package/README.ko.md +52 -38
- package/README.md +18 -4
- package/cordis.patch.yml +19 -19
- package/dsh.plugin.json +2 -2
- package/locale/de.json +6 -6
- package/locale/en.json +6 -6
- package/locale/es.json +6 -6
- package/locale/fr.json +6 -6
- package/locale/it.json +6 -6
- package/locale/ja.json +6 -6
- package/locale/ko.json +6 -6
- package/locale/ru.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +9 -9
- package/screenshots.json +3 -3
- package/src/client/bridge-scope.ts +202 -202
- package/src/client/card.tsx +154 -154
- package/src/client/host-api.ts +61 -61
- package/src/client/index.ts +594 -594
- package/src/client/locales.ts +893 -893
- package/src/client/manage-locales.ts +178 -178
- package/src/client/manage.tsx +430 -430
- package/src/client/panel.tsx +131 -131
- package/src/config.ts +43 -43
- 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 -250
- package/src/host/history/archive.ts +249 -249
- package/src/host/history/favorites.ts +71 -71
- package/src/host/history/purge.ts +370 -370
- package/src/host/registry/archive-registry.ts +317 -317
- package/src/host/settings-bridge.ts +222 -222
package/src/client/panel.tsx
CHANGED
|
@@ -1,131 +1,131 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 会话管理面板壳:一个侧边栏入口,两个页签(管理=主页面 / 体检=备用)。
|
|
3
|
-
*
|
|
4
|
-
* 2026-10-20 页面调整(用户定标):「养老院」页签取消——管理台已覆盖其全部
|
|
5
|
-
* 动作面(archive/unarchive/purge + 归档域筛选),独立页签属重复入口;
|
|
6
|
-
* 历史文件组件(history-panel.tsx)随摘除,宿主侧 history 路由与配置字段
|
|
7
|
-
* (historyFiles)保留不动,字典键留用以备回迁。
|
|
8
|
-
*
|
|
9
|
-
* 页签可见性由开关决定:`search=false` 时「管理」消失(退到体检),
|
|
10
|
-
* `healthCheck=false` 时「体检」消失;两者都关时进入面板直接显示空壳提示
|
|
11
|
-
* (客户端也会隐藏入口)。
|
|
12
|
-
*/
|
|
13
|
-
import { createElement, useEffect, useState, type ReactElement } from 'react'
|
|
14
|
-
import { createPortal } from 'react-dom'
|
|
15
|
-
import { HealthPanel } from './health-panel.tsx'
|
|
16
|
-
import { ManageConsole } from './manage.tsx'
|
|
17
|
-
import { translate, type LocaleKey } from './locales.ts'
|
|
18
|
-
|
|
19
|
-
/** 面板字典面。 */
|
|
20
|
-
export type PanelTranslate = (key: LocaleKey, params?: Record<string, unknown>) => string
|
|
21
|
-
|
|
22
|
-
/** 页签 id(导出供测试断言)。 */
|
|
23
|
-
export const TAB_HEALTH = 'dms-tab-health'
|
|
24
|
-
export const TAB_MANAGE = 'dms-tab-manage'
|
|
25
|
-
|
|
26
|
-
/** 面板入参。 */
|
|
27
|
-
export interface StewardPanelProps {
|
|
28
|
-
t?: PanelTranslate
|
|
29
|
-
/** 健康检查子域是否可见(关掉只剩管理页签)。 */
|
|
30
|
-
healthCheck: boolean
|
|
31
|
-
/** 打开会话(管理台行点击;apply 侧经 openSessionThrough 解析)。 */
|
|
32
|
-
openSession?: (sessionId: string) => void
|
|
33
|
-
onClose: () => void
|
|
34
|
-
/**
|
|
35
|
-
* 壳形态:`dialog`(缺省)= 背景遮罩上的浮动对话框(底栏入口用);
|
|
36
|
-
* `page` = 填满主栏的整页(顶栏工具行入口用,无遮罩 / 无圆角阴影)。
|
|
37
|
-
*/
|
|
38
|
-
variant?: 'dialog' | 'page'
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** 会话管理对话框 / 整页。 */
|
|
42
|
-
export function StewardPanel({ t, healthCheck, openSession, onClose, variant = 'dialog' }: StewardPanelProps): ReactElement {
|
|
43
|
-
// 主页面=管理(会话管理台);体检是备用页签。
|
|
44
|
-
const [tab, setTab] = useState<'health' | 'manage'>('manage')
|
|
45
|
-
|
|
46
|
-
// 开关变化后收敛到仍然可见的页签(体检关掉→回管理)。
|
|
47
|
-
useEffect(() => {
|
|
48
|
-
if (tab === 'health' && !healthCheck) setTab('manage')
|
|
49
|
-
}, [tab, healthCheck])
|
|
50
|
-
|
|
51
|
-
useEffect(() => {
|
|
52
|
-
const onKey = (event: KeyboardEvent): void => { if (event.key === 'Escape') onClose() }
|
|
53
|
-
document.addEventListener('keydown', onKey)
|
|
54
|
-
return () => { document.removeEventListener('keydown', onKey) }
|
|
55
|
-
}, [onClose])
|
|
56
|
-
|
|
57
|
-
const tabs: ReactElement[] = []
|
|
58
|
-
tabs.push(createElement('button', {
|
|
59
|
-
key: TAB_MANAGE,
|
|
60
|
-
id: TAB_MANAGE,
|
|
61
|
-
type: 'button',
|
|
62
|
-
className: `dms_tab${tab === 'manage' ? ' dms_tabActive' : ''}`,
|
|
63
|
-
'aria-selected': tab === 'manage',
|
|
64
|
-
onClick: () => { setTab('manage') },
|
|
65
|
-
}, translate(t, 'panel.tab.manage')))
|
|
66
|
-
if (healthCheck) {
|
|
67
|
-
tabs.push(createElement('button', {
|
|
68
|
-
key: TAB_HEALTH,
|
|
69
|
-
id: TAB_HEALTH,
|
|
70
|
-
type: 'button',
|
|
71
|
-
className: `dms_tab${tab === 'health' ? ' dms_tabActive' : ''}`,
|
|
72
|
-
'aria-selected': tab === 'health',
|
|
73
|
-
onClick: () => { setTab('health') },
|
|
74
|
-
}, translate(t, 'panel.tab.health')))
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const body: ReactElement = tab === 'manage'
|
|
78
|
-
? createElement('div', { style: { display: 'flex', flexDirection: 'column', flex: '1 1 auto', minHeight: 0 } },
|
|
79
|
-
createElement(ManageConsole, { open: openSession ?? (() => {}) }))
|
|
80
|
-
: createElement(HealthPanel, { t, onClose })
|
|
81
|
-
|
|
82
|
-
const panelEl = createElement('div', {
|
|
83
|
-
key: 'panel',
|
|
84
|
-
className: variant === 'page' ? 'dms_panel dms_panelPage' : 'dms_panel',
|
|
85
|
-
role: variant === 'page' ? 'region' : 'dialog',
|
|
86
|
-
'aria-label': translate(t, 'panel.title'),
|
|
87
|
-
style: { display: 'flex', flexDirection: 'column' },
|
|
88
|
-
}, [
|
|
89
|
-
createElement('div', { key: 'head', className: 'dms_dialogHead' }, [
|
|
90
|
-
createElement('span', { key: 'title', className: 'dms_dialogTitle' }, translate(t, 'panel.title')),
|
|
91
|
-
createElement('span', { key: 'tabs', className: 'dms_tabRow' }, tabs),
|
|
92
|
-
createElement('button', {
|
|
93
|
-
key: 'close',
|
|
94
|
-
type: 'button',
|
|
95
|
-
className: 'dms_actBtn',
|
|
96
|
-
onClick: onClose,
|
|
97
|
-
}, translate(t, 'panel.close')),
|
|
98
|
-
]),
|
|
99
|
-
body,
|
|
100
|
-
])
|
|
101
|
-
|
|
102
|
-
// 整页形态直接占用主栏(宿主已给页容器,不再叠遮罩 / 传送门)。
|
|
103
|
-
if (variant === 'page') return panelEl
|
|
104
|
-
return createPortal(createElement('div', { key: 'steward-root' }, [
|
|
105
|
-
createElement('div', { key: 'backdrop', className: 'dms_backdrop', onClick: onClose }),
|
|
106
|
-
panelEl,
|
|
107
|
-
]), document.body)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** 侧边栏脚部入口:宽栏带文案成行控件,收起轨道退化为 36x36 图标钮。 */
|
|
111
|
-
export function StewardFooter(props: {
|
|
112
|
-
onClick?: () => void
|
|
113
|
-
title?: string
|
|
114
|
-
label?: string
|
|
115
|
-
wide?: boolean
|
|
116
|
-
}): ReactElement {
|
|
117
|
-
const wide = props.wide === true
|
|
118
|
-
return createElement('button', {
|
|
119
|
-
type: 'button',
|
|
120
|
-
className: wide ? 'dms_footerEntry' : 'dms_footerEntry dms_footerEntryRail',
|
|
121
|
-
title: props.title,
|
|
122
|
-
'aria-label': props.title,
|
|
123
|
-
onClick: props.onClick,
|
|
124
|
-
}, [
|
|
125
|
-
createElement('span', { key: 'icon', className: 'dms_footerIcon', 'aria-hidden': true }, '🧭'),
|
|
126
|
-
// 收起轨道里 36px 装不下文案,标签与宽栏同条件出现,与搜索入口一致。
|
|
127
|
-
wide && props.label !== undefined
|
|
128
|
-
? createElement('span', { key: 'label', className: 'dms_footerLabel' }, props.label)
|
|
129
|
-
: null,
|
|
130
|
-
])
|
|
131
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 会话管理面板壳:一个侧边栏入口,两个页签(管理=主页面 / 体检=备用)。
|
|
3
|
+
*
|
|
4
|
+
* 2026-10-20 页面调整(用户定标):「养老院」页签取消——管理台已覆盖其全部
|
|
5
|
+
* 动作面(archive/unarchive/purge + 归档域筛选),独立页签属重复入口;
|
|
6
|
+
* 历史文件组件(history-panel.tsx)随摘除,宿主侧 history 路由与配置字段
|
|
7
|
+
* (historyFiles)保留不动,字典键留用以备回迁。
|
|
8
|
+
*
|
|
9
|
+
* 页签可见性由开关决定:`search=false` 时「管理」消失(退到体检),
|
|
10
|
+
* `healthCheck=false` 时「体检」消失;两者都关时进入面板直接显示空壳提示
|
|
11
|
+
* (客户端也会隐藏入口)。
|
|
12
|
+
*/
|
|
13
|
+
import { createElement, useEffect, useState, type ReactElement } from 'react'
|
|
14
|
+
import { createPortal } from 'react-dom'
|
|
15
|
+
import { HealthPanel } from './health-panel.tsx'
|
|
16
|
+
import { ManageConsole } from './manage.tsx'
|
|
17
|
+
import { translate, type LocaleKey } from './locales.ts'
|
|
18
|
+
|
|
19
|
+
/** 面板字典面。 */
|
|
20
|
+
export type PanelTranslate = (key: LocaleKey, params?: Record<string, unknown>) => string
|
|
21
|
+
|
|
22
|
+
/** 页签 id(导出供测试断言)。 */
|
|
23
|
+
export const TAB_HEALTH = 'dms-tab-health'
|
|
24
|
+
export const TAB_MANAGE = 'dms-tab-manage'
|
|
25
|
+
|
|
26
|
+
/** 面板入参。 */
|
|
27
|
+
export interface StewardPanelProps {
|
|
28
|
+
t?: PanelTranslate
|
|
29
|
+
/** 健康检查子域是否可见(关掉只剩管理页签)。 */
|
|
30
|
+
healthCheck: boolean
|
|
31
|
+
/** 打开会话(管理台行点击;apply 侧经 openSessionThrough 解析)。 */
|
|
32
|
+
openSession?: (sessionId: string) => void
|
|
33
|
+
onClose: () => void
|
|
34
|
+
/**
|
|
35
|
+
* 壳形态:`dialog`(缺省)= 背景遮罩上的浮动对话框(底栏入口用);
|
|
36
|
+
* `page` = 填满主栏的整页(顶栏工具行入口用,无遮罩 / 无圆角阴影)。
|
|
37
|
+
*/
|
|
38
|
+
variant?: 'dialog' | 'page'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 会话管理对话框 / 整页。 */
|
|
42
|
+
export function StewardPanel({ t, healthCheck, openSession, onClose, variant = 'dialog' }: StewardPanelProps): ReactElement {
|
|
43
|
+
// 主页面=管理(会话管理台);体检是备用页签。
|
|
44
|
+
const [tab, setTab] = useState<'health' | 'manage'>('manage')
|
|
45
|
+
|
|
46
|
+
// 开关变化后收敛到仍然可见的页签(体检关掉→回管理)。
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (tab === 'health' && !healthCheck) setTab('manage')
|
|
49
|
+
}, [tab, healthCheck])
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const onKey = (event: KeyboardEvent): void => { if (event.key === 'Escape') onClose() }
|
|
53
|
+
document.addEventListener('keydown', onKey)
|
|
54
|
+
return () => { document.removeEventListener('keydown', onKey) }
|
|
55
|
+
}, [onClose])
|
|
56
|
+
|
|
57
|
+
const tabs: ReactElement[] = []
|
|
58
|
+
tabs.push(createElement('button', {
|
|
59
|
+
key: TAB_MANAGE,
|
|
60
|
+
id: TAB_MANAGE,
|
|
61
|
+
type: 'button',
|
|
62
|
+
className: `dms_tab${tab === 'manage' ? ' dms_tabActive' : ''}`,
|
|
63
|
+
'aria-selected': tab === 'manage',
|
|
64
|
+
onClick: () => { setTab('manage') },
|
|
65
|
+
}, translate(t, 'panel.tab.manage')))
|
|
66
|
+
if (healthCheck) {
|
|
67
|
+
tabs.push(createElement('button', {
|
|
68
|
+
key: TAB_HEALTH,
|
|
69
|
+
id: TAB_HEALTH,
|
|
70
|
+
type: 'button',
|
|
71
|
+
className: `dms_tab${tab === 'health' ? ' dms_tabActive' : ''}`,
|
|
72
|
+
'aria-selected': tab === 'health',
|
|
73
|
+
onClick: () => { setTab('health') },
|
|
74
|
+
}, translate(t, 'panel.tab.health')))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const body: ReactElement = tab === 'manage'
|
|
78
|
+
? createElement('div', { style: { display: 'flex', flexDirection: 'column', flex: '1 1 auto', minHeight: 0 } },
|
|
79
|
+
createElement(ManageConsole, { open: openSession ?? (() => {}) }))
|
|
80
|
+
: createElement(HealthPanel, { t, onClose })
|
|
81
|
+
|
|
82
|
+
const panelEl = createElement('div', {
|
|
83
|
+
key: 'panel',
|
|
84
|
+
className: variant === 'page' ? 'dms_panel dms_panelPage' : 'dms_panel',
|
|
85
|
+
role: variant === 'page' ? 'region' : 'dialog',
|
|
86
|
+
'aria-label': translate(t, 'panel.title'),
|
|
87
|
+
style: { display: 'flex', flexDirection: 'column' },
|
|
88
|
+
}, [
|
|
89
|
+
createElement('div', { key: 'head', className: 'dms_dialogHead' }, [
|
|
90
|
+
createElement('span', { key: 'title', className: 'dms_dialogTitle' }, translate(t, 'panel.title')),
|
|
91
|
+
createElement('span', { key: 'tabs', className: 'dms_tabRow' }, tabs),
|
|
92
|
+
createElement('button', {
|
|
93
|
+
key: 'close',
|
|
94
|
+
type: 'button',
|
|
95
|
+
className: 'dms_actBtn',
|
|
96
|
+
onClick: onClose,
|
|
97
|
+
}, translate(t, 'panel.close')),
|
|
98
|
+
]),
|
|
99
|
+
body,
|
|
100
|
+
])
|
|
101
|
+
|
|
102
|
+
// 整页形态直接占用主栏(宿主已给页容器,不再叠遮罩 / 传送门)。
|
|
103
|
+
if (variant === 'page') return panelEl
|
|
104
|
+
return createPortal(createElement('div', { key: 'steward-root' }, [
|
|
105
|
+
createElement('div', { key: 'backdrop', className: 'dms_backdrop', onClick: onClose }),
|
|
106
|
+
panelEl,
|
|
107
|
+
]), document.body)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 侧边栏脚部入口:宽栏带文案成行控件,收起轨道退化为 36x36 图标钮。 */
|
|
111
|
+
export function StewardFooter(props: {
|
|
112
|
+
onClick?: () => void
|
|
113
|
+
title?: string
|
|
114
|
+
label?: string
|
|
115
|
+
wide?: boolean
|
|
116
|
+
}): ReactElement {
|
|
117
|
+
const wide = props.wide === true
|
|
118
|
+
return createElement('button', {
|
|
119
|
+
type: 'button',
|
|
120
|
+
className: wide ? 'dms_footerEntry' : 'dms_footerEntry dms_footerEntryRail',
|
|
121
|
+
title: props.title,
|
|
122
|
+
'aria-label': props.title,
|
|
123
|
+
onClick: props.onClick,
|
|
124
|
+
}, [
|
|
125
|
+
createElement('span', { key: 'icon', className: 'dms_footerIcon', 'aria-hidden': true }, '🧭'),
|
|
126
|
+
// 收起轨道里 36px 装不下文案,标签与宽栏同条件出现,与搜索入口一致。
|
|
127
|
+
wide && props.label !== undefined
|
|
128
|
+
? createElement('span', { key: 'label', className: 'dms_footerLabel' }, props.label)
|
|
129
|
+
: null,
|
|
130
|
+
])
|
|
131
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-manage-session 的共享配置面(会话管理器:归档 + 体检)。
|
|
3
|
-
*
|
|
4
|
-
* 与 host 半身(schemastery schema 在 src/index.ts)以及浏览器半身共用同一形状,
|
|
5
|
-
* 使一处 admitted 的值在另一处也 admitted。
|
|
6
|
-
*
|
|
7
|
-
* 设置命名空间只有一个:`manage-session`(enabled/historyFiles/healthCheck/directDelete)。
|
|
8
|
-
* 搜索/索引子域已拆回 dsh-search-index,本包不再持有 `search` 开关、索引目录、
|
|
9
|
-
* 语料源(DRS)等字段——DRS 依赖也随搜索一起迁回那边。
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
/** 会话管理运行时配置(设置命名空间 + 组合入口)。 */
|
|
13
|
-
export interface StewardConfig {
|
|
14
|
-
/** 插件总开关(关闭后不注册任何子域路由)。 */
|
|
15
|
-
enabled: boolean
|
|
16
|
-
/** 会话历史文件(归档浏览与清理)。关闭后不注册 history 路由。 */
|
|
17
|
-
historyFiles?: boolean
|
|
18
|
-
/** 会话健康检查(体检 → 处方 → 出院)。关闭后不注册 health 路由、不渲染「体检」页签。 */
|
|
19
|
-
healthCheck?: boolean
|
|
20
|
-
/**
|
|
21
|
-
* 直接删除入口(2026-10-20 用户定标):开启后侧栏会话菜单出现「删除」行
|
|
22
|
-
* (两步确认,走管理端同款 session-history-purge 链路)。
|
|
23
|
-
* **缺省关**——破坏性入口不默认铺开;管理端批量删除不受本开关限制。
|
|
24
|
-
*/
|
|
25
|
-
directDelete?: boolean
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** 缺省值。 */
|
|
29
|
-
export const DEFAULT_CONFIG: Required<StewardConfig> = {
|
|
30
|
-
enabled: true,
|
|
31
|
-
historyFiles: true,
|
|
32
|
-
healthCheck: true,
|
|
33
|
-
directDelete: false,
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** host 半身注册的设置命名空间(与 src/index.ts 保持一致)。 */
|
|
37
|
-
export const STEWARD_SETTINGS_NAMESPACE = 'manage-session'
|
|
38
|
-
|
|
39
|
-
/** 侧边栏入口 id(客户端注册 id)。 */
|
|
40
|
-
export const STEWARD_ENTRY_ID = 'dsh-manage-session'
|
|
41
|
-
|
|
42
|
-
/** host 路由前缀(管理 + 体检,方法名一律 `session-*`)。 */
|
|
43
|
-
export const STEWARD_API_PREFIX = '/manage-session/api'
|
|
1
|
+
/**
|
|
2
|
+
* dsh-manage-session 的共享配置面(会话管理器:归档 + 体检)。
|
|
3
|
+
*
|
|
4
|
+
* 与 host 半身(schemastery schema 在 src/index.ts)以及浏览器半身共用同一形状,
|
|
5
|
+
* 使一处 admitted 的值在另一处也 admitted。
|
|
6
|
+
*
|
|
7
|
+
* 设置命名空间只有一个:`manage-session`(enabled/historyFiles/healthCheck/directDelete)。
|
|
8
|
+
* 搜索/索引子域已拆回 dsh-search-index,本包不再持有 `search` 开关、索引目录、
|
|
9
|
+
* 语料源(DRS)等字段——DRS 依赖也随搜索一起迁回那边。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** 会话管理运行时配置(设置命名空间 + 组合入口)。 */
|
|
13
|
+
export interface StewardConfig {
|
|
14
|
+
/** 插件总开关(关闭后不注册任何子域路由)。 */
|
|
15
|
+
enabled: boolean
|
|
16
|
+
/** 会话历史文件(归档浏览与清理)。关闭后不注册 history 路由。 */
|
|
17
|
+
historyFiles?: boolean
|
|
18
|
+
/** 会话健康检查(体检 → 处方 → 出院)。关闭后不注册 health 路由、不渲染「体检」页签。 */
|
|
19
|
+
healthCheck?: boolean
|
|
20
|
+
/**
|
|
21
|
+
* 直接删除入口(2026-10-20 用户定标):开启后侧栏会话菜单出现「删除」行
|
|
22
|
+
* (两步确认,走管理端同款 session-history-purge 链路)。
|
|
23
|
+
* **缺省关**——破坏性入口不默认铺开;管理端批量删除不受本开关限制。
|
|
24
|
+
*/
|
|
25
|
+
directDelete?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 缺省值。 */
|
|
29
|
+
export const DEFAULT_CONFIG: Required<StewardConfig> = {
|
|
30
|
+
enabled: true,
|
|
31
|
+
historyFiles: true,
|
|
32
|
+
healthCheck: true,
|
|
33
|
+
directDelete: false,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** host 半身注册的设置命名空间(与 src/index.ts 保持一致)。 */
|
|
37
|
+
export const STEWARD_SETTINGS_NAMESPACE = 'manage-session'
|
|
38
|
+
|
|
39
|
+
/** 侧边栏入口 id(客户端注册 id)。 */
|
|
40
|
+
export const STEWARD_ENTRY_ID = 'dsh-manage-session'
|
|
41
|
+
|
|
42
|
+
/** host 路由前缀(管理 + 体检,方法名一律 `session-*`)。 */
|
|
43
|
+
export const STEWARD_API_PREFIX = '/manage-session/api'
|
|
@@ -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
|
+
}
|