dsh-vscode-mode 0.14.2 → 0.14.4
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/README.md +12 -9
- package/lib/client.js +906 -395
- package/lib/client.js.map +1 -1
- package/lib/index.js +1594 -607
- package/lib/index.js.map +1 -1
- package/package.json +7 -7
- package/skills/dsh-vscodemode-mcp/SKILL.md +9 -3
- package/src/archiveDb.ts +476 -0
- package/src/capture.ts +8 -1
- package/src/client/diffDockStore.ts +18 -0
- package/src/client/mcpForm.ts +79 -0
- package/src/client/mcpState.ts +179 -0
- package/src/client/monaco/diffRender.ts +3 -0
- package/src/client/state/regions.ts +21 -1
- package/src/client/styles/editor.css +12 -0
- package/src/client/styles/mcp.css +6 -3
- package/src/client/ui/ConversationDiffDock.ts +2 -2
- package/src/client/ui/DiffLauncher.ts +76 -17
- package/src/client/ui/EditorView.ts +90 -7
- package/src/client/ui/McpSettings.ts +174 -137
- package/src/dshVersion.ts +4 -3
- package/src/index.ts +2 -0
- package/src/mcp.ts +131 -23
- package/src/mcpIsolation.ts +2 -1
- package/src/mcpProject.ts +74 -14
- package/src/mcpRuntime.ts +18 -3
- package/src/paths.ts +11 -0
- package/src/perf.ts +17 -24
- package/src/revert.ts +93 -1
- package/src/rpc.ts +243 -68
- package/src/shared/diff.ts +69 -20
- package/src/shared/mcpStatus.ts +30 -0
- package/src/shared/rpc.ts +10 -1
- package/src/store.ts +154 -55
|
@@ -11,7 +11,7 @@ import React from 'react'
|
|
|
11
11
|
import { useOpenReceipt } from '../useOpenReceipt.js'
|
|
12
12
|
import { acceptsLoad, receiptErrorFor, receiptPath, receiptReadyFor, receiptTabPath } from '../receiptState.js'
|
|
13
13
|
import { bindPanelCommand } from '../panelCommands.js'
|
|
14
|
-
import { dbg, rpc } from '../rpc.js'
|
|
14
|
+
import { dbg, isDebug, rpc } from '../rpc.js'
|
|
15
15
|
import { emitFileChanged, emitRefresh } from '../events.js'
|
|
16
16
|
import { langOf, loadMonaco, snippetLanguageOf } from '../monaco/loader.js'
|
|
17
17
|
import { clampZoom, clearZoomMem, dataUrlOf, isImagePath, isSvgPath, recallZoom, rememberZoom, zoomKeyOf, zoomStepOf } from '../imagePreview.js'
|
|
@@ -44,7 +44,8 @@ import { CommandPalette } from './CommandPalette.js'
|
|
|
44
44
|
import { closeCommandPalette } from '../commandPaletteStore.js'
|
|
45
45
|
import { DiffLauncher } from './DiffLauncher.js'
|
|
46
46
|
import { SidebarView } from '../sidebar/SidebarView.js'
|
|
47
|
-
import { clearDiffDock, publishDiffDock } from '../diffDockStore.js'
|
|
47
|
+
import { clearDiffDock, dockInLayout, publishDiffDock } from '../diffDockStore.js'
|
|
48
|
+
import { DiffBox } from './DiffBox.js'
|
|
48
49
|
import { displayDiffTotal, editorDockMode } from '../diffDock.js'
|
|
49
50
|
import { editorHeight } from '../editorLayout.js'
|
|
50
51
|
import { revealInExplorer as revealPathInExplorer } from '../fileReveal.js'
|
|
@@ -479,7 +480,19 @@ export function EditorView(props) {
|
|
|
479
480
|
// 当前 tab 是否为可预览的 Markdown,以及是否正处于预览态(预览态替换 Monaco host,只读)
|
|
480
481
|
const isMdActive = !!active && isMarkdownPath(active)
|
|
481
482
|
const mdPreviewing = isMdActive && mdPreviewRef.current.has(active)
|
|
482
|
-
const regions = React.useMemo(() =>
|
|
483
|
+
const regions = React.useMemo(() => {
|
|
484
|
+
// #region debug log
|
|
485
|
+
const trace = isDebug() && contentReady && currentRecords.length > 0
|
|
486
|
+
const start = trace ? performance.now() : 0
|
|
487
|
+
// #endregion
|
|
488
|
+
const next = diffRegions(currentRecords, contentReady ? content : null).filter((r) => !r.superseded)
|
|
489
|
+
// #region debug log
|
|
490
|
+
if (trace) {
|
|
491
|
+
try { dbg(sessionId, `[DEBUG diffRegions] records=${currentRecords.length} bytes=${content.length} regions=${next.length} ms=${(performance.now() - start).toFixed(1)}`) } catch (e) { /* 诊断不可影响渲染 */ }
|
|
492
|
+
}
|
|
493
|
+
// #endregion
|
|
494
|
+
return next
|
|
495
|
+
}, [currentRecords, content, contentPath, active])
|
|
483
496
|
// useMemo 稳定引用:否则 hover 等重渲染会让 view zone effect 反复重建(- 号闪烁)
|
|
484
497
|
const pendingRegions = React.useMemo(() => regions.filter((r) => r.status === ST.PENDING && !r.stale), [regions])
|
|
485
498
|
const staleRegions = React.useMemo(() => regions.filter((r) => r.status === ST.PENDING && r.stale), [regions])
|
|
@@ -802,14 +815,35 @@ export function EditorView(props) {
|
|
|
802
815
|
|
|
803
816
|
const refreshRecords = (skipStale) => {
|
|
804
817
|
if (!sessionId) return
|
|
818
|
+
// #region debug log
|
|
819
|
+
const trace = isDebug()
|
|
820
|
+
const start = trace ? performance.now() : 0
|
|
821
|
+
// #endregion
|
|
805
822
|
rpc('edrv.list', { sessionId, ...(skipStale ? { skipStale: true } : {}) }).then((res) => {
|
|
823
|
+
// #region debug log
|
|
824
|
+
const received = trace ? performance.now() : 0
|
|
825
|
+
// #endregion
|
|
806
826
|
if (!res || !res.ok || !Array.isArray(res.records)) return
|
|
807
827
|
const map = {}
|
|
808
828
|
for (const r of res.records) {
|
|
809
829
|
if (r.archived === true) continue
|
|
810
830
|
map[r.callId] = r
|
|
811
831
|
}
|
|
812
|
-
setRecords((prev) =>
|
|
832
|
+
setRecords((prev) => {
|
|
833
|
+
// #region debug log
|
|
834
|
+
const compareStart = trace ? performance.now() : 0
|
|
835
|
+
// #endregion
|
|
836
|
+
const equal = sameRecords(prev, map)
|
|
837
|
+
// #region debug log
|
|
838
|
+
if (trace) {
|
|
839
|
+
try {
|
|
840
|
+
const compareMs = performance.now() - compareStart
|
|
841
|
+
if (compareMs > 20 || received - start > 100) dbg(sessionId, `[DEBUG diffList] count=${res.records.length} skipStale=${skipStale === true} rpcMs=${(received - start).toFixed(1)} compareMs=${compareMs.toFixed(1)} changed=${!equal}`)
|
|
842
|
+
} catch (e) { /* 诊断不可影响轮询 */ }
|
|
843
|
+
}
|
|
844
|
+
// #endregion
|
|
845
|
+
return equal ? prev : map
|
|
846
|
+
})
|
|
813
847
|
}).catch((e) => setError('list异常:' + String(e)))
|
|
814
848
|
}
|
|
815
849
|
|
|
@@ -942,7 +976,15 @@ export function EditorView(props) {
|
|
|
942
976
|
return
|
|
943
977
|
}
|
|
944
978
|
setLoadStage({ progress: monaco ? 72 : 12, message: '读取文件内容…' })
|
|
979
|
+
// #region debug log
|
|
980
|
+
const readStart = isDebug() ? performance.now() : 0
|
|
981
|
+
// #endregion
|
|
945
982
|
rpc('edrv.read', { sessionId: sid, path }).then((res) => {
|
|
983
|
+
// #region debug log
|
|
984
|
+
if (readStart) {
|
|
985
|
+
try { dbg(sid, `[DEBUG diffRead] force=${force === true} ok=${res?.ok === true} bytes=${typeof res?.content === 'string' ? res.content.length : 0} rpcMs=${(performance.now() - readStart).toFixed(1)}`) } catch (e) { /* 诊断不可影响文件读取 */ }
|
|
986
|
+
}
|
|
987
|
+
// #endregion
|
|
946
988
|
if (!loadCurrent(path, seq)) return
|
|
947
989
|
if (res && res.ok) {
|
|
948
990
|
mark(res.version, true)
|
|
@@ -2704,7 +2746,20 @@ export function EditorView(props) {
|
|
|
2704
2746
|
*/
|
|
2705
2747
|
const actHunk = (region, reject, silent) => {
|
|
2706
2748
|
const method = reject ? 'edrv.reject' : 'edrv.accept'
|
|
2749
|
+
// #region debug log
|
|
2750
|
+
const trace = isDebug()
|
|
2751
|
+
const start = trace ? performance.now() : 0
|
|
2752
|
+
const traceId = trace ? `${Date.now()}-${Math.floor(Math.random() * 1000)}` : ''
|
|
2753
|
+
if (trace) {
|
|
2754
|
+
try { dbg(sessionId, `[DEBUG diffAction] stage=start trace=${traceId} method=${method} scope=${region.create ? 'call' : 'hunk'}`) } catch (e) { /* 诊断不可影响决策 */ }
|
|
2755
|
+
}
|
|
2756
|
+
// #endregion
|
|
2707
2757
|
return rpc(method, { sessionId, callId: region.callId, scope: region.create ? 'call' : 'hunk', hunkIndex: region.idx }).then((res) => {
|
|
2758
|
+
// #region debug log
|
|
2759
|
+
if (trace) {
|
|
2760
|
+
try { dbg(sessionId, `[DEBUG diffAction] stage=reply trace=${traceId} method=${method} ok=${res?.ok === true} rpcMs=${(performance.now() - start).toFixed(1)}`) } catch (e) { /* 诊断不可影响决策 */ }
|
|
2761
|
+
}
|
|
2762
|
+
// #endregion
|
|
2708
2763
|
if (res && res.ok) {
|
|
2709
2764
|
setRecords((prev) => Object.assign({}, prev, { [region.callId]: res.record }))
|
|
2710
2765
|
if (!silent) { reloadFile(); emitRefresh() }
|
|
@@ -2727,7 +2782,21 @@ export function EditorView(props) {
|
|
|
2727
2782
|
*/
|
|
2728
2783
|
const actMany = (items) => {
|
|
2729
2784
|
if (!items.length) return Promise.resolve({ ok: 0, fail: 0, stale: 0 })
|
|
2785
|
+
// #region debug log
|
|
2786
|
+
const trace = isDebug()
|
|
2787
|
+
const start = trace ? performance.now() : 0
|
|
2788
|
+
const rejected = trace ? items.filter((item) => item.decision === 'rejected').length : 0
|
|
2789
|
+
const traceId = trace ? `${Date.now()}-${Math.floor(Math.random() * 1000)}` : ''
|
|
2790
|
+
if (trace) {
|
|
2791
|
+
try { dbg(sessionId, `[DEBUG diffAction] stage=start trace=${traceId} method=decideBatch items=${items.length} rejected=${rejected}`) } catch (e) { /* 诊断不可影响决策 */ }
|
|
2792
|
+
}
|
|
2793
|
+
// #endregion
|
|
2730
2794
|
return rpc('edrv.decideBatch', { sessionId, items }).then((res) => {
|
|
2795
|
+
// #region debug log
|
|
2796
|
+
if (trace) {
|
|
2797
|
+
try { dbg(sessionId, `[DEBUG diffAction] stage=reply trace=${traceId} method=decideBatch items=${items.length} rpcMs=${(performance.now() - start).toFixed(1)} ok=${res?.ok === true}`) } catch (e) { /* 诊断不可影响决策 */ }
|
|
2798
|
+
}
|
|
2799
|
+
// #endregion
|
|
2731
2800
|
if (!res || !res.ok || !Array.isArray(res.results)) {
|
|
2732
2801
|
setError(res?.error ? String(res.error) : '批量处理失败')
|
|
2733
2802
|
return { ok: 0, fail: items.length, stale: 0 }
|
|
@@ -3700,9 +3769,14 @@ export function EditorView(props) {
|
|
|
3700
3769
|
React.createElement('button', { className: 'edrv-pill edrv-pill-undo', onClick: () => { dismissHover(); actHunk(hoverAct.region, true) } }, '↩ Undo'))
|
|
3701
3770
|
: null
|
|
3702
3771
|
|
|
3703
|
-
/**
|
|
3772
|
+
/**
|
|
3773
|
+
* 组装当前编辑器的差异上下文,供侧栏本地与会话 dock 共用。
|
|
3774
|
+
* @author ddj 2026年09月29号
|
|
3775
|
+
* @returns 含布局及操作回调的差异快照
|
|
3776
|
+
*/
|
|
3704
3777
|
const buildDockSnapshot = () => ({
|
|
3705
3778
|
mode: editorDockMode(active),
|
|
3779
|
+
layout,
|
|
3706
3780
|
pendingRegions,
|
|
3707
3781
|
staleRegions,
|
|
3708
3782
|
onAct: actHunk,
|
|
@@ -3729,9 +3803,10 @@ export function EditorView(props) {
|
|
|
3729
3803
|
onOpenNextFile: openNextFile,
|
|
3730
3804
|
})
|
|
3731
3805
|
|
|
3806
|
+
const dockSnapshot = buildDockSnapshot()
|
|
3732
3807
|
React.useEffect(() => {
|
|
3733
3808
|
if (!sessionId) return
|
|
3734
|
-
publishDiffDock(sessionId,
|
|
3809
|
+
publishDiffDock(sessionId, dockSnapshot, dockSourceRef.current)
|
|
3735
3810
|
// 侧栏形态同步 Tab 角标计数(仅本形态写入;页签形态清零避免残留)
|
|
3736
3811
|
if (layout === 'side') setSidePending(sessionId, sum.totalFiles)
|
|
3737
3812
|
else setSidePending(sessionId, 0)
|
|
@@ -3930,12 +4005,20 @@ export function EditorView(props) {
|
|
|
3930
4005
|
}, '添加配置…')
|
|
3931
4006
|
: null
|
|
3932
4007
|
|
|
3933
|
-
//
|
|
4008
|
+
// 侧栏差异栏悬浮在编辑区底部;中央页签仍使用对话 dock。
|
|
4009
|
+
const sideDock = dockInLayout(dockSnapshot, 'side')
|
|
4010
|
+
? React.createElement('div', {
|
|
4011
|
+
className: 'edrv-side-diff-dock edrv-diff-dock-shell',
|
|
4012
|
+
'data-edrv-diff-dock-shell': '1',
|
|
4013
|
+
}, React.createElement(DiffBox, { ...dockSnapshot, dock: true }))
|
|
4014
|
+
: null
|
|
4015
|
+
// 主编辑列(侧边栏右侧):pathBar + tabRow + 编辑/差异区。
|
|
3934
4016
|
const editorArea = React.createElement('div', { className: 'edrv-editor-area' },
|
|
3935
4017
|
dapBar,
|
|
3936
4018
|
addCfgBtn,
|
|
3937
4019
|
body,
|
|
3938
4020
|
hoverEl,
|
|
4021
|
+
sideDock,
|
|
3939
4022
|
overlay)
|
|
3940
4023
|
// 侧边栏引导条(仅旧页签形态且未装 betterSidebar 时显示;可复制安装命令、可关闭)
|
|
3941
4024
|
const dismissHint = () => {
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
import React from 'react'
|
|
10
10
|
import { rpc } from '../rpc.js'
|
|
11
11
|
import type { MpcProject, MpcServer } from '../../shared/mcp.js'
|
|
12
|
+
import { configOf } from '../mcpForm.js'
|
|
13
|
+
import { McpState } from '../mcpState.js'
|
|
14
|
+
import { statusOf, statusTone } from '../../shared/mcpStatus.js'
|
|
12
15
|
import '../styles/mcp.css'
|
|
13
16
|
import { availableOpeners, AUTO_OPEN_TOOL } from '../fileOpeners.js'
|
|
14
17
|
import { SettingsContext } from '../settingsContext.js'
|
|
@@ -24,44 +27,15 @@ import { AiSettings } from './AiSettings.js'
|
|
|
24
27
|
|
|
25
28
|
const EMPTY = { serverName: '', transport: 'stdio', command: '', args: '', cwd: '', url: '', headers: '' }
|
|
26
29
|
|
|
27
|
-
/**
|
|
28
|
-
function
|
|
29
|
-
const config = { serverName: draft.serverName.trim(), transport: draft.transport }
|
|
30
|
-
if (draft.transport === 'stdio') {
|
|
31
|
-
config.command = draft.command.trim()
|
|
32
|
-
config.args = draft.args.split(/\r?\n|\s+/).filter(Boolean)
|
|
33
|
-
if (draft.cwd.trim()) config.cwd = draft.cwd.trim()
|
|
34
|
-
} else {
|
|
35
|
-
config.url = draft.url.trim()
|
|
36
|
-
config.headers = parsePairs(draft.headers)
|
|
37
|
-
}
|
|
38
|
-
return config
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** 解析每行 key=value 的表单字段。 */
|
|
42
|
-
function parsePairs(text) {
|
|
43
|
-
return Object.fromEntries(text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
44
|
-
const idx = line.indexOf('=')
|
|
45
|
-
return idx > 0 ? [line.slice(0, idx).trim(), line.slice(idx + 1).trim()] : [line, '']
|
|
46
|
-
}))
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** @author ddj 2026年09月28号 @param server Server lifecycle snapshot. @returns User-facing status. */
|
|
50
|
-
function statusOf(server) {
|
|
51
|
-
if (!server.enabled || server.status === 'disabled') return '已禁用'
|
|
52
|
-
const labels = { connected: '在线', connecting: '装配中', configured: '已配置,等待会话', unverified: '已加载,连接未确认', error: '错误' }
|
|
53
|
-
return labels[server.status] ?? '状态未知'
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** MCP 服务卡片(全局与项目共用)。 */
|
|
57
|
-
function ServerCard({ server, onRefresh, onToggle, onRemove }) {
|
|
30
|
+
/** @private @author ddj 2026年10月08号 @param props Server snapshot, page-wide busy state and actions. @returns Lifecycle-evidence card; enabled does not imply online. */
|
|
31
|
+
function ServerCard({ server, busy, onRefresh, onToggle, onRemove }) {
|
|
58
32
|
return React.createElement('article', { className: 'vsm-mcp-card' },
|
|
59
33
|
React.createElement('div', { className: 'vsm-mcp-card-head' },
|
|
60
|
-
React.createElement('div', { className: 'vsm-mcp-title' }, React.createElement('span', { className: 'vsm-mcp-dot ' + (server
|
|
34
|
+
React.createElement('div', { className: 'vsm-mcp-title' }, React.createElement('span', { className: 'vsm-mcp-dot ' + statusTone(server) }), server.serverName),
|
|
61
35
|
React.createElement('div', { className: 'vsm-mcp-actions' },
|
|
62
|
-
React.createElement('button', { onClick: () => onRefresh(server.id), title: '
|
|
63
|
-
React.createElement('button', { className: 'vsm-danger', onClick: () => onRemove(server.id), title: '删除 MCP' }, '⌫'),
|
|
64
|
-
React.createElement('button', { className: 'vsm-switch ' + (server.enabled ? 'on' : ''), onClick: () => onToggle(server), 'aria-label': server.enabled ? '
|
|
36
|
+
React.createElement('button', { disabled: !!busy, onClick: () => onRefresh(server.id), title: '重新装配会话实例' }, '↻'),
|
|
37
|
+
React.createElement('button', { disabled: !!busy, className: 'vsm-danger', onClick: () => onRemove(server.id), title: '删除 MCP' }, '⌫'),
|
|
38
|
+
React.createElement('button', { disabled: !!busy, className: 'vsm-switch ' + (server.enabled ? 'on' : ''), onClick: () => onToggle(server), title: '配置启用不代表连接在线', 'aria-label': server.enabled ? '禁用配置(不代表在线)' : '启用配置(不代表在线)', 'aria-pressed': !!server.enabled }, server.enabled ? '●' : '○'),
|
|
65
39
|
),
|
|
66
40
|
),
|
|
67
41
|
React.createElement('div', { className: 'vsm-mcp-meta' }, statusOf(server), ' · ', server.toolCount, ' 个工具 · ', server.transport,
|
|
@@ -119,17 +93,17 @@ function ProjectPicker({ projects, value, onChange }) {
|
|
|
119
93
|
)
|
|
120
94
|
}
|
|
121
95
|
|
|
122
|
-
/**
|
|
96
|
+
/** @private @author ddj 2026年10月08号 @param props Project view, shared busy lock and actions. @returns Project cards with actions disabled for any active mutation. */
|
|
123
97
|
function ProjectGroup({ project, busy, onAdd, onRefresh, onToggle, onRemove }) {
|
|
124
98
|
const head = React.createElement('div', { className: 'vsm-project-head' },
|
|
125
99
|
React.createElement('div', { className: 'vsm-project-title' }, React.createElement('span', { className: 'vsm-project-icon' }, '▣'), React.createElement('div', null, React.createElement('span', { className: 'vsm-project-name' }, project.title), React.createElement('span', { className: 'vsm-project-path' }, project.workspacePath))),
|
|
126
|
-
React.createElement('button', { className: 'vsm-primary vsm-small', onClick: () => onAdd(project) }, '+ 添加 MCP'),
|
|
100
|
+
React.createElement('button', { className: 'vsm-primary vsm-small', disabled: !!busy, onClick: () => onAdd(project) }, '+ 添加 MCP'),
|
|
127
101
|
)
|
|
128
102
|
let body
|
|
129
103
|
if (project.missingDir) body = React.createElement('div', { className: 'vsm-project-empty' }, '项目目录已不存在,无法管理 MCP')
|
|
130
104
|
else if (project.fileError) body = React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, project.fileError)
|
|
131
105
|
else if (!project.servers.length) body = React.createElement('div', { className: 'vsm-project-empty' }, '此项目未配置 MCP')
|
|
132
|
-
else body = project.servers.map((server) => React.createElement(ServerCard, { key: server.serverName, server, onRefresh: (id) => onRefresh(project, server, id), onToggle: () => onToggle(project, server), onRemove: (id) => onRemove(project, server, id) }))
|
|
106
|
+
else body = project.servers.map((server) => React.createElement(ServerCard, { key: server.serverName, server, busy, onRefresh: (id) => onRefresh(project, server, id), onToggle: () => onToggle(project, server), onRemove: (id) => onRemove(project, server, id) }))
|
|
133
107
|
return React.createElement('section', { className: 'vsm-project' }, head, body)
|
|
134
108
|
}
|
|
135
109
|
|
|
@@ -462,8 +436,10 @@ const MCP_TABS = [
|
|
|
462
436
|
* @param {object} draft 新增表单草稿
|
|
463
437
|
* @param {Function} edit 草稿字段变更回调
|
|
464
438
|
* @param {Function} resetDraft 清空草稿回调
|
|
439
|
+
* @author ddj 2026年10月08号
|
|
465
440
|
*/
|
|
466
|
-
function McpManagePanel(
|
|
441
|
+
function McpManagePanel(props) {
|
|
442
|
+
const { projects, busy, draft, edit, resetDraft, saveGlobal, saveProject } = props
|
|
467
443
|
const [mcpTab, setMcpTab] = React.useState('mine')
|
|
468
444
|
const [showForm, setShowForm] = React.useState(false)
|
|
469
445
|
const [projectForm, setProjectForm] = React.useState(null)
|
|
@@ -477,26 +453,26 @@ function McpManagePanel({ servers, projects, busy, draft, edit, resetDraft, refr
|
|
|
477
453
|
if (!projects.some((project) => project.workspacePath === selectedPath)) setSelectedPath(projects[0].workspacePath)
|
|
478
454
|
}, [projects, selectedPath])
|
|
479
455
|
|
|
456
|
+
/** @author ddj 2026年10月08号 @description Close the successful global form. */
|
|
480
457
|
const closeGlobalForm = () => setShowForm(false)
|
|
458
|
+
/** @author ddj 2026年10月08号 @description Close the successful project form. */
|
|
481
459
|
const closeProjectForm = () => setProjectForm(null)
|
|
460
|
+
/** @author ddj 2026年10月08号 @param project Target project. @description Open a project draft only when idle. */
|
|
482
461
|
const addProject = (project) => {
|
|
462
|
+
if (busy) return
|
|
483
463
|
resetDraft?.()
|
|
464
|
+
setShowForm(false)
|
|
484
465
|
setProjectForm({ workspacePath: project.workspacePath, title: project.title })
|
|
485
466
|
}
|
|
467
|
+
/** @author ddj 2026年10月08号 @description Open a global draft only when idle. */
|
|
468
|
+
const addGlobal = () => {
|
|
469
|
+
if (busy) return
|
|
470
|
+
resetDraft?.()
|
|
471
|
+
setProjectForm(null)
|
|
472
|
+
setShowForm(true)
|
|
473
|
+
}
|
|
486
474
|
|
|
487
|
-
const
|
|
488
|
-
const mineBody = servers.length
|
|
489
|
-
? servers.map((s) => React.createElement(ServerCard, { key: s.id, server: s, onRefresh: refreshGlobal, onToggle: toggleGlobal, onRemove: removeGlobal }))
|
|
490
|
-
: React.createElement('div', { className: 'vsm-mcp-empty' }, '还没有配置全局 MCP')
|
|
491
|
-
const projectBody = projects.length ? React.createElement(React.Fragment, null,
|
|
492
|
-
React.createElement(ProjectPicker, { projects, value: selectedPath, onChange: setSelectedPath }),
|
|
493
|
-
selectedProject ? React.createElement(ProjectGroup, { project: selectedProject, busy, onAdd: addProject, onRefresh: refreshProject, onToggle: toggleProject, onRemove: removeProject }) : React.createElement('div', { className: 'vsm-mcp-empty' }, '请选择项目'),
|
|
494
|
-
) : React.createElement('div', { className: 'vsm-mcp-empty' }, '还没有项目')
|
|
495
|
-
|
|
496
|
-
let body
|
|
497
|
-
if (mcpTab === 'projects') body = projectBody
|
|
498
|
-
else if (mcpTab === 'market') body = React.createElement('div', { className: 'vsm-mcp-empty' }, 'MCP 市场暂未接入')
|
|
499
|
-
else body = mineBody
|
|
475
|
+
const body = manageBody(props, mcpTab, selectedPath, setSelectedPath, addProject)
|
|
500
476
|
|
|
501
477
|
const form = showForm
|
|
502
478
|
? React.createElement(McpForm, { title: '添加全局 MCP', draft, busy, edit, save: () => saveGlobal(closeGlobalForm), close: closeGlobalForm })
|
|
@@ -506,108 +482,151 @@ function McpManagePanel({ servers, projects, busy, draft, edit, resetDraft, refr
|
|
|
506
482
|
return React.createElement(React.Fragment, null,
|
|
507
483
|
React.createElement('div', { className: 'vsm-mcp-subhead' },
|
|
508
484
|
React.createElement('nav', { className: 'vsm-mcp-subtabs' }, MCP_TABS.map((item) => React.createElement('button', { key: item.id, className: mcpTab === item.id ? 'active' : '', onClick: () => setMcpTab(item.id) }, item.label))),
|
|
509
|
-
React.createElement('button', { className: 'vsm-primary vsm-small',
|
|
485
|
+
React.createElement('button', { className: 'vsm-primary vsm-small', disabled: !!busy, onClick: addGlobal }, '+ 添加全局 MCP'),
|
|
510
486
|
),
|
|
511
487
|
body,
|
|
512
488
|
form,
|
|
513
489
|
)
|
|
514
490
|
}
|
|
515
491
|
|
|
516
|
-
/** MCP
|
|
517
|
-
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
const [busy, setBusy] = React.useState('')
|
|
525
|
-
const [error, setError] = React.useState('')
|
|
526
|
-
const [tab, setTab] = React.useState('general')
|
|
527
|
-
const [draft, setDraft] = React.useState(EMPTY)
|
|
528
|
-
|
|
529
|
-
const loadAll = React.useCallback(() => {
|
|
530
|
-
setLoading(true)
|
|
531
|
-
setError('')
|
|
532
|
-
Promise.all([rpc('mcp.list', {}), rpc('mcp.projects', {})]).then(([list, prj]) => {
|
|
533
|
-
if (!list.ok) throw new Error(list.error)
|
|
534
|
-
if (!prj.ok) throw new Error(prj.error)
|
|
535
|
-
setServers(list.servers)
|
|
536
|
-
setProjects(prj.projects)
|
|
537
|
-
}).catch((e) => setError(String(e))).finally(() => setLoading(false))
|
|
538
|
-
}, [])
|
|
539
|
-
|
|
540
|
-
React.useEffect(loadAll, [loadAll])
|
|
541
|
-
|
|
542
|
-
const finish = (result) => {
|
|
543
|
-
if (!result.ok) throw new Error(result.error)
|
|
544
|
-
return result
|
|
492
|
+
/** @private @author ddj 2026年10月08号 @param props Current view and actions. @param tab Selected MCP subtab. @param path Selected project. @param select Project setter. @param add Project form opener. @returns Selected global/project/market content. */
|
|
493
|
+
function manageBody(props, tab, path, select, add) {
|
|
494
|
+
const { servers, projects, busy, refreshGlobal, toggleGlobal, removeGlobal, refreshProject, toggleProject, removeProject } = props
|
|
495
|
+
if (tab === 'market') return React.createElement('div', { className: 'vsm-mcp-empty' }, 'MCP 市场暂未接入')
|
|
496
|
+
if (tab !== 'projects') {
|
|
497
|
+
return servers.length
|
|
498
|
+
? servers.map((server) => React.createElement(ServerCard, { key: server.id, server, busy, onRefresh: refreshGlobal, onToggle: toggleGlobal, onRemove: removeGlobal }))
|
|
499
|
+
: React.createElement('div', { className: 'vsm-mcp-empty' }, '还没有配置全局 MCP')
|
|
545
500
|
}
|
|
501
|
+
if (!projects.length) return React.createElement('div', { className: 'vsm-mcp-empty' }, '还没有项目')
|
|
502
|
+
const project = projects.find((item) => item.workspacePath === path)
|
|
503
|
+
return React.createElement(React.Fragment, null,
|
|
504
|
+
React.createElement(ProjectPicker, { projects, value: path, onChange: select }),
|
|
505
|
+
project ? React.createElement(ProjectGroup, { project, busy, onAdd: add, onRefresh: refreshProject, onToggle: toggleProject, onRemove: removeProject }) : React.createElement('div', { className: 'vsm-mcp-empty' }, '请选择项目'),
|
|
506
|
+
)
|
|
507
|
+
}
|
|
546
508
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
509
|
+
//region MCP page state and locked actions
|
|
510
|
+
/** @private @author ddj 2026年10月08号 @returns Mounted controller reference and its published view. */
|
|
511
|
+
function useMcpState() {
|
|
512
|
+
const ref = React.useRef(null)
|
|
513
|
+
const [view, setView] = React.useState({ servers: [], projects: [], loading: true, busy: '', error: '' })
|
|
514
|
+
/** @author ddj 2026年10月08号 @description Subscribe to document visibility and initialize MCP data. @returns Unmount cleanup. */
|
|
515
|
+
const mount = () => {
|
|
516
|
+
const state = new McpState(rpc, setView)
|
|
517
|
+
ref.current = state
|
|
518
|
+
/** @author ddj 2026年10月08号 @description Gate snapshots on actual document visibility. */
|
|
519
|
+
const visibility = () => state.setVisible(document.visibilityState === 'visible')
|
|
520
|
+
/** @author ddj 2026年10月08号 @description Stop controller and remove page listener. */
|
|
521
|
+
const cleanup = () => {
|
|
522
|
+
document.removeEventListener('visibilitychange', visibility)
|
|
523
|
+
state.dispose()
|
|
524
|
+
if (ref.current === state) ref.current = null
|
|
525
|
+
}
|
|
526
|
+
visibility()
|
|
527
|
+
document.addEventListener('visibilitychange', visibility)
|
|
528
|
+
void state.load()
|
|
529
|
+
return cleanup
|
|
553
530
|
}
|
|
531
|
+
React.useEffect(mount, [])
|
|
532
|
+
return { ref, view }
|
|
533
|
+
}
|
|
554
534
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
if (patch) patch(result)
|
|
565
|
-
}).catch((e) => setError(String(e))).finally(() => setBusy(''))
|
|
535
|
+
/** @private @author ddj 2026年10月08号 @param state Mounted controller. @param id Server identity. @param method Mutation RPC. @param args Request fields. @returns Locked completion. */
|
|
536
|
+
function globalAction(state, id, method, args) {
|
|
537
|
+
/** @author ddj 2026年10月08号 @returns Global mutation result. */
|
|
538
|
+
const request = () => rpc(method, args)
|
|
539
|
+
/** @author ddj 2026年10月08号 @param result Global mutation result. @description Apply only successful mutation to the latest controller view. */
|
|
540
|
+
const commit = (result) => {
|
|
541
|
+
state.view.servers = method === 'mcp.remove'
|
|
542
|
+
? state.view.servers.filter((server) => server.id !== id)
|
|
543
|
+
: state.view.servers.map((server) => server.id === id ? result.server : server)
|
|
566
544
|
}
|
|
545
|
+
return state?.mutate(id, request, commit)
|
|
546
|
+
}
|
|
567
547
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
/**
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
* @param {Function} close 关闭新增表单回调
|
|
576
|
-
*/
|
|
577
|
-
const saveGlobal = (close) => {
|
|
578
|
-
setBusy('save')
|
|
579
|
-
setError('')
|
|
580
|
-
rpc('mcp.save', { config: configOf(draft) }).then(finish).then((result) => {
|
|
581
|
-
setServers((old) => old.some((s) => s.id === result.server.id) ? old.map((s) => s.id === result.server.id ? result.server : s) : old.concat(result.server))
|
|
582
|
-
setDraft(EMPTY)
|
|
583
|
-
close()
|
|
584
|
-
}).catch((e) => setError(String(e))).finally(() => setBusy(''))
|
|
548
|
+
/** @private @author ddj 2026年10月08号 @param state Mounted controller. @param path Project path. @param name Server identity. @param method Mutation RPC. @param extra Optional request fields. @returns Locked completion. */
|
|
549
|
+
function projectAction(state, path, name, method, extra = {}) {
|
|
550
|
+
/** @author ddj 2026年10月08号 @returns Project mutation result. */
|
|
551
|
+
const request = () => rpc(method, { workspacePath: path, serverName: name, ...extra })
|
|
552
|
+
/** @author ddj 2026年10月08号 @param result Project mutation result. @description Replace project in the current view. */
|
|
553
|
+
const commit = (result) => {
|
|
554
|
+
state.view.projects = state.view.projects.map((project) => project.workspacePath === path ? result.project : project)
|
|
585
555
|
}
|
|
556
|
+
return state?.mutate(path + ':' + name, request, commit)
|
|
557
|
+
}
|
|
586
558
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
559
|
+
/** @private @author ddj 2026年10月08号 @param ref Mounted controller reference. @returns Global/project card actions sharing a synchronous lock. */
|
|
560
|
+
function cardActions(ref) {
|
|
561
|
+
/** @author ddj 2026年10月08号 @param id Global identity. @returns Locked refresh. */
|
|
562
|
+
const refreshGlobal = (id) => globalAction(ref.current, id, 'mcp.refresh', { id })
|
|
563
|
+
/** @author ddj 2026年10月08号 @param server Current configuration. @returns Locked enable change. */
|
|
564
|
+
const toggleGlobal = (server) => globalAction(ref.current, server.id, 'mcp.toggle', { id: server.id, enabled: !server.enabled })
|
|
565
|
+
/** @author ddj 2026年10月08号 @param id Global identity from ServerCard. @returns Confirmed locked deletion. */
|
|
566
|
+
const removeGlobal = (id) => !ref.current?.view.busy && window.confirm('确认删除此 MCP 服务?') && globalAction(ref.current, id, 'mcp.remove', { id })
|
|
567
|
+
/** @author ddj 2026年10月08号 @param project Project. @param server Configuration. @returns Locked refresh. */
|
|
568
|
+
const refreshProject = (project, server) => projectAction(ref.current, project.workspacePath, server.serverName, 'mcp.projectRefresh')
|
|
569
|
+
/** @author ddj 2026年10月08号 @param project Project. @param server Configuration. @returns Locked enable change. */
|
|
570
|
+
const toggleProject = (project, server) => projectAction(ref.current, project.workspacePath, server.serverName, 'mcp.projectToggle', { enabled: !server.enabled })
|
|
571
|
+
/** @author ddj 2026年10月08号 @param project Project. @param server Configuration. @returns Confirmed locked deletion. */
|
|
572
|
+
const removeProject = (project, server) => !ref.current?.view.busy && window.confirm('确认删除此项目的 MCP「' + server.serverName + '」?') && projectAction(ref.current, project.workspacePath, server.serverName, 'mcp.projectRemove')
|
|
573
|
+
return { refreshGlobal, toggleGlobal, removeGlobal, refreshProject, toggleProject, removeProject }
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** @private @author ddj 2026年10月08号 @param state Controller. @param draft Editable fields. @param setDraft Draft setter. @param path Optional project target. @param close Success-only close callback. @returns Locked save, validation included. */
|
|
577
|
+
function saveDraft(state, draft, setDraft, path, close) {
|
|
578
|
+
/** @author ddj 2026年10月08号 @returns Validated save result. @throws Malformed arguments/headers. */
|
|
579
|
+
const request = () => {
|
|
596
580
|
const config = configOf(draft)
|
|
597
|
-
rpc('mcp.projectSave', { workspacePath, serverName: config.serverName, config })
|
|
598
|
-
setProjects((old) => old.map((p) => p.workspacePath === workspacePath ? result.project : p))
|
|
599
|
-
setDraft(EMPTY)
|
|
600
|
-
close()
|
|
601
|
-
}).catch((e) => setError(String(e))).finally(() => setBusy(''))
|
|
581
|
+
return path === null ? rpc('mcp.save', { config }) : rpc('mcp.projectSave', { workspacePath: path, serverName: config.serverName, config })
|
|
602
582
|
}
|
|
583
|
+
/** @author ddj 2026年10月08号 @param result Save result. @description Update current view and only then reset/close the form. */
|
|
584
|
+
const commit = (result) => {
|
|
585
|
+
if (path !== null) state.view.projects = state.view.projects.map((project) => project.workspacePath === path ? result.project : project)
|
|
586
|
+
else {
|
|
587
|
+
const servers = state.view.servers
|
|
588
|
+
state.view.servers = servers.some((server) => server.id === result.server.id)
|
|
589
|
+
? servers.map((server) => server.id === result.server.id ? result.server : server) : servers.concat(result.server)
|
|
590
|
+
}
|
|
591
|
+
setDraft(EMPTY)
|
|
592
|
+
close()
|
|
593
|
+
}
|
|
594
|
+
return state?.mutate(path === null ? 'save' : 'project-save', request, commit)
|
|
595
|
+
}
|
|
596
|
+
//endregion
|
|
603
597
|
|
|
604
|
-
|
|
605
|
-
|
|
598
|
+
/** @public @author ddj 2026年10月08号 @param props Settings integrations. @returns MCP management page with visible-tab-only snapshot polling. */
|
|
599
|
+
export function McpSettings(props) {
|
|
600
|
+
const openerRegistry = props?.openerRegistry ?? { list: () => [] }
|
|
601
|
+
const compatSummary = props?.compatSummary ?? (() => [])
|
|
602
|
+
const getShortcuts = props?.getShortcuts ?? (() => null)
|
|
603
|
+
const { ref, view } = useMcpState()
|
|
604
|
+
const { servers, projects, loading, busy, error } = view
|
|
605
|
+
const [tab, setTab] = React.useState('general')
|
|
606
|
+
const [draft, setDraft] = React.useState(EMPTY)
|
|
607
|
+
/** @author ddj 2026年10月08号 @description Select polling scope and invalidate late responses on tab leave. @returns Tab cleanup. */
|
|
608
|
+
const selectTab = () => {
|
|
609
|
+
const state = ref.current
|
|
610
|
+
state?.setActive(tab === 'mcp')
|
|
611
|
+
/** @author ddj 2026年10月08号 @description Cancel reads on tab switch. */
|
|
612
|
+
const cleanup = () => state?.setActive(false)
|
|
613
|
+
return cleanup
|
|
614
|
+
}
|
|
615
|
+
React.useEffect(selectTab, [tab])
|
|
616
|
+
const { refreshGlobal, toggleGlobal, removeGlobal, refreshProject, toggleProject, removeProject } = cardActions(ref)
|
|
617
|
+
/** @author ddj 2026年10月08号 @param close Success-only close callback. @returns Global save completion. */
|
|
618
|
+
const saveGlobal = (close) => saveDraft(ref.current, draft, setDraft, null, close)
|
|
619
|
+
/** @author ddj 2026年10月08号 @param path Target project. @param close Success-only close callback. @returns Project save completion. */
|
|
620
|
+
const saveProject = (path, close) => saveDraft(ref.current, draft, setDraft, path, close)
|
|
621
|
+
/** @author ddj 2026年10月08号 @param key Draft field. @param value User input. @description Keep draft edits unavailable during a save. */
|
|
622
|
+
const edit = (key, value) => { if (!ref.current?.view.busy) setDraft((old) => ({ ...old, [key]: value })) }
|
|
623
|
+
/** @author ddj 2026年10月08号 @description Reset the draft when no mutation is pending. */
|
|
624
|
+
const resetDraft = () => { if (!ref.current?.view.busy) setDraft(EMPTY) }
|
|
606
625
|
let body
|
|
607
626
|
if (loading) body = React.createElement('div', { className: 'vsm-mcp-empty' }, '正在读取 MCP 服务…')
|
|
608
627
|
else if (tab === 'general') body = React.createElement(GeneralSettings, { registry: openerRegistry, getShortcuts })
|
|
609
628
|
else if (tab === 'mcp') body = React.createElement(McpManagePanel, { servers, projects, busy, draft, edit, resetDraft,
|
|
610
|
-
saveGlobal
|
|
629
|
+
saveGlobal, saveProject,
|
|
611
630
|
refreshGlobal, toggleGlobal, removeGlobal, refreshProject, toggleProject, removeProject })
|
|
612
631
|
else if (tab === 'compat') body = React.createElement(CompatSection, { getSummary: compatSummary })
|
|
613
632
|
else if (tab === 'lsp') body = React.createElement(LspSettings, null)
|
|
@@ -621,9 +640,27 @@ export function McpSettings(props) {
|
|
|
621
640
|
)
|
|
622
641
|
}
|
|
623
642
|
|
|
624
|
-
/**
|
|
643
|
+
/** @private @author ddj 2026年10月08号 @param props Draft, shared busy lock, edit/save/close callbacks. @returns Lossless argument/header form; failed save stays open. */
|
|
625
644
|
function McpForm({ title, draft, busy, edit, save, close }) {
|
|
626
|
-
|
|
627
|
-
const
|
|
628
|
-
|
|
645
|
+
/** @author ddj 2026年10月08号 @param key Draft field. @returns Short input change callback. */
|
|
646
|
+
const change = (key) => (event) => edit(key, event.target.value)
|
|
647
|
+
/** @author ddj 2026年10月08号 @param key Draft field. @param placeholder Input hint. @returns Disabled-while-busy input. */
|
|
648
|
+
const input = (key, placeholder) => React.createElement('input', { disabled: !!busy, value: draft[key], onChange: change(key), placeholder })
|
|
649
|
+
/** @author ddj 2026年10月08号 @param key Draft field. @returns Disabled-while-busy text area. */
|
|
650
|
+
const textarea = (key) => React.createElement('textarea', { disabled: !!busy, value: draft[key], onChange: change(key) })
|
|
651
|
+
const fields = draft.transport === 'stdio' ? React.createElement(React.Fragment, null,
|
|
652
|
+
React.createElement('label', null, '命令', input('command', 'npx')),
|
|
653
|
+
React.createElement('label', null, '参数(JSON string[] 或每行一个;保留空格,空参数用 JSON)', textarea('args')),
|
|
654
|
+
React.createElement('label', null, '工作目录(可选)', input('cwd', '')),
|
|
655
|
+
) : React.createElement(React.Fragment, null,
|
|
656
|
+
React.createElement('label', null, 'URL', input('url', 'http://localhost:3000/mcp')),
|
|
657
|
+
React.createElement('label', null, '请求头(每行 key=value,键不可重复)', textarea('headers')),
|
|
658
|
+
)
|
|
659
|
+
return React.createElement('div', { className: 'vsm-mcp-modal' }, React.createElement('div', { className: 'vsm-mcp-dialog' },
|
|
660
|
+
React.createElement('h3', null, title),
|
|
661
|
+
React.createElement('label', null, '名称', input('serverName', '例如 codegraph')),
|
|
662
|
+
React.createElement('label', null, '传输方式', React.createElement('select', { disabled: !!busy, value: draft.transport, onChange: change('transport') }, React.createElement('option', { value: 'stdio' }, 'stdio'), React.createElement('option', { value: 'streamable-http' }, 'streamable-http'))),
|
|
663
|
+
fields,
|
|
664
|
+
React.createElement('div', { className: 'vsm-mcp-dialog-actions' }, React.createElement('button', { disabled: !!busy, onClick: close }, '取消'), React.createElement('button', { className: 'vsm-primary', disabled: !!busy, onClick: save }, '保存配置')),
|
|
665
|
+
))
|
|
629
666
|
}
|
package/src/dshVersion.ts
CHANGED
|
@@ -84,16 +84,17 @@ export function inDshRange(version: DshVersion | null | undefined, range: DshRan
|
|
|
84
84
|
|
|
85
85
|
/**
|
|
86
86
|
* 版本线标签:报告与文档展示版本归属;不可解析返回 '未知'。
|
|
87
|
-
*
|
|
88
|
-
* @author ddj 2026年09月02号 / 2026年09月18号 / 2026年09月22号 / 2026年09月29号
|
|
87
|
+
* 逐线自新到旧匹配,先命中先返回;仅已静态审计的 0.2.0-rc.1/rc.2 精确标记。
|
|
88
|
+
* @author ddj 2026年09月02号 / 2026年09月18号 / 2026年09月22号 / 2026年09月29号 / 2026年10月08号
|
|
89
89
|
* @param input 版本串(如 '0.1.6-alpha.2')
|
|
90
90
|
* @returns 版本线标签
|
|
91
91
|
*/
|
|
92
92
|
export function familyLabel(input: string): string {
|
|
93
93
|
const version = parseDshVersion(input)
|
|
94
94
|
if (!version) return '未知'
|
|
95
|
+
if (inDshRange(version, { from: '0.2.0-rc.2', to: '0.2.0-rc.2' })) return '0.2.0-rc.2(设置=profile Config;按能力探测)'
|
|
95
96
|
if (inDshRange(version, { from: '0.2.0-rc.1', to: '0.2.0-rc.1' })) return '0.2.0-rc.1(设置=profile Config;按能力探测)'
|
|
96
|
-
if (inDshRange(version, { from: '0.2.0-0' })) return '0.2.0 及更新(除 rc.1 外未经逐版适配;按能力探测)'
|
|
97
|
+
if (inDshRange(version, { from: '0.2.0-0' })) return '0.2.0 及更新(除 rc.1/rc.2 外未经逐版适配;按能力探测)'
|
|
97
98
|
if (inDshRange(version, { from: '0.1.7-alpha.1' })) return '0.1.7-alpha.1 及更新(设置=profile Config + configForms 客户端面 + 会话 V4)'
|
|
98
99
|
if (inDshRange(version, { from: '0.1.6-alpha.2' })) return '0.1.6-alpha.2 及更新(会话多实例共存 + 回合改动卡片 + Office 侧栏预览 + 侧栏浏览器)'
|
|
99
100
|
if (inDshRange(version, { from: '0.1.6-alpha.1' })) return '0.1.6-alpha 及更新(MCP SDK v2 + Web 侧边栏终端 + 文件链接默认侧栏预览)'
|