3 Commits

4 changed files with 85 additions and 17 deletions
+19
View File
@@ -58,6 +58,25 @@ profile 的 `package.json`:
改完执行 `pnpm install`(关闭 DSH 后执行最稳,避免 Windows 上正在使用的原生模块被占用),再启动。 改完执行 `pnpm install`(关闭 DSH 后执行最稳,避免 Windows 上正在使用的原生模块被占用),再启动。
### 方式三:DSH 桌面版(Electron)
桌面版也是 DSH,插件机制相同,但 profile 名是**保留的 `desktop`**,且由桌面应用自己管理。要点:
1. **前提版本**:桌面版需基于 DSH **≥ 0.2.1-rc.2**(本插件 `dsh.engines.dsh` 的要求;0.1.x 不支持预设声明行机制)。
2. **安装入口**:用**桌面版自带的插件管理界面**(设置 → 插件 / Plugin Manager)安装本仓库目录作为 bundle;
```text
install_bundle target = <本仓库克隆目录的绝对路径>
```
或安装一个可解析的包 / Git 仓库地址。
⚠️ 不要用 npm 全局安装的 `dsh` CLI 去管 `desktop` profile——CLI 会拒绝桌面版 profile 的启动与插件管理请求,必须使用桌面安装自带的命令运行时。
3. **依赖**:`@nefevcore/abap-adt-dsh-plugin` 会随包安装;桌面版的插件操作会使用其安装的包管理器(需能访问 npm registry)。
4. **重启桌面应用** → 新建会话选「ABAP 工作台」→ 设置 → ABAP ADT 配置目的地/模式(用法与 Web 版一致)。
5. **已知差异/注意**:
- 桌面版的 Web UI 同样加载本插件的 Client 半(设置页「ABAP ADT」+ 会话状态徽章),但 `dsh.client` 支持取决于桌面版是否加载第三方 client 插件;若设置页没有出现 ABAP ADT,先确认桌面版版本与插件是否真的激活。
- 内置 mock `demo` 会在 `127.0.0.1` 随机端口监听,桌面版可能弹一次防火墙提示(仅本机回环,放行即可/拒绝也不影响真实 SAP 目的地)。
- 工作区是普通目录,目的地/模式配置仍写在工作区 `.dsh-abap-adt/`,与 Web 版一致。
- Host 半只用 Node 内置模块;`@nefevcore` core 通过扫描 `~/.dsh/profiles/*` 定位,桌面版 profile 在 `~/.dsh/profiles/desktop` 下同样适用。
### 安装后 ### 安装后
1. **重启 DSH**; 1. **重启 DSH**;
+16 -11
View File
@@ -431,23 +431,33 @@ window.__ModuleLoader__.load({
) )
: null : null
const workspacePaths = workspaces.map((item) => item.path).filter(Boolean)
const workspaceOptions =
workspace && !workspacePaths.includes(workspace) ? [workspace, ...workspacePaths] : workspacePaths
const workspaceTitles = Object.fromEntries(workspaces.map((item) => [item.path, item.title]))
const workspaceRow = h( const workspaceRow = h(
'div', 'div',
{ style: { display: 'grid', gridTemplateColumns: '2fr 1fr', gap: 12, marginTop: 16 } }, { style: { display: 'grid', gridTemplateColumns: '2fr 1fr', gap: 12, marginTop: 16 } },
h( h(
Field, Field,
{ label: DICTS.zh.workspace, hint: DICTS.zh.workspaceHint }, { label: DICTS.zh.workspace, hint: DICTS.zh.workspaceHint },
h('input', { h(
'select',
{
style: inputStyle, style: inputStyle,
value: workspace, value: workspace,
list: 'abap-adt-workspaces', onChange: (event) => {
onChange: (event) => setWorkspace(event.target.value), const next = event.target.value
onBlur: () => { setWorkspace(next)
if (workspace && workspace !== file?.path?.replace(/[\\/]\.dsh-abap-adt[\\/].*$/, '')) load(workspace) if (next) load(next)
}, },
placeholder: 'D:\\AIagent\\SAP', },
workspaceOptions.map((path) => {
const title = workspaceTitles[path]
return h('option', { key: path, value: path }, title && title !== path ? title + ' — ' + path : path)
}), }),
), ),
),
h( h(
Field, Field,
{ label: DICTS.zh.fileLabel }, { label: DICTS.zh.fileLabel },
@@ -457,11 +467,6 @@ window.__ModuleLoader__.load({
file?.path ? `${file.path}${file.exists ? '' : DICTS.zh.notExists}` : DICTS.zh.never, file?.path ? `${file.path}${file.exists ? '' : DICTS.zh.notExists}` : DICTS.zh.never,
), ),
), ),
h(
'datalist',
{ id: 'abap-adt-workspaces' },
workspaces.map((item) => h('option', { key: item.id || item.path, value: item.path }, item.title)),
),
) )
const destinationCards = destinations.length const destinationCards = destinations.length
+46 -2
View File
@@ -16,7 +16,7 @@
* the DSH credential service (falling back to the process environment). * the DSH credential service (falling back to the process environment).
*/ */
import { existsSync, readdirSync } from 'node:fs' import { existsSync, readdirSync, statSync } from 'node:fs'
import { mkdir, readFile, writeFile } from 'node:fs/promises' import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { request as httpRequest } from 'node:http' import { request as httpRequest } from 'node:http'
import { request as httpsRequest } from 'node:https' import { request as httpsRequest } from 'node:https'
@@ -508,8 +508,52 @@ export function apply(ctx, config) {
} }
} }
/**
* The workspace whose destinations file was written most recently. An empty
* destinations list (a scratch config, or one where every system was removed)
* loses to any workspace that still lists systems, so the settings page never
* opens on an empty page while a usable config exists elsewhere.
*/
async function lastModifiedWorkspace() {
let best
let bestTime = -Infinity
let any
let anyTime = -Infinity
for (const row of listWorkspaces()) {
if (!row.path) continue
for (const fileName of FILE_NAMES) {
const candidate = join(row.path, CONFIG_DIR, fileName)
let mtimeMs
try {
mtimeMs = statSync(candidate).mtimeMs
} catch {
continue
}
if (mtimeMs > anyTime) {
anyTime = mtimeMs
any = row.path
}
if (mtimeMs <= bestTime) continue
try {
const parsed = await parseDestinations(await readFile(candidate, 'utf8'))
if (!normalizeDestinations(parsed.destinations).length) continue
} catch {
continue
}
bestTime = mtimeMs
best = row.path
}
}
return best || any
}
async function stateOf({ sessionId, workspace }) { async function stateOf({ sessionId, workspace }) {
const cwd = workspace || resolveCwd(sessionId) || listWorkspaces()[0]?.path || process.cwd() const cwd =
workspace ||
resolveCwd(sessionId) ||
(await lastModifiedWorkspace()) ||
listWorkspaces()[0]?.path ||
process.cwd()
const file = existingWorkspaceFile(cwd) const file = existingWorkspaceFile(cwd)
let raw = '' let raw = ''
let exists = false let exists = false
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "dsh-abap-workbench", "name": "dsh-abap-workbench",
"version": "0.1.2", "version": "0.1.3",
"private": false, "private": false,
"description": "SAP ABAP 工作台:一个 DSH bundle,内含 ABAP 预设(只读/调试/更改,按工作区策略切换)、可视化配置页(目的地、连接测试、运行模式、可写白名单)和会话状态徽章。依赖 @nefevcore/abap-adt-dsh-plugin 提供 adt_* 工具。", "description": "SAP ABAP 工作台:一个 DSH bundle,内含 ABAP 预设(只读/调试/更改,按工作区策略切换)、可视化配置页(目的地、连接测试、运行模式、可写白名单)和会话状态徽章。依赖 @nefevcore/abap-adt-dsh-plugin 提供 adt_* 工具。",
"type": "module", "type": "module",