From bde2d485579891c1438420f550d92da9a17d0c78 Mon Sep 17 00:00:00 2001 From: QQ597901027 Date: Mon, 19 Oct 2026 14:14:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=9B=AE=E7=9A=84=E5=9C=B0=E4=B8=AD?= =?UTF-8?q?=E6=96=87=E6=98=BE=E7=A4=BA=E5=90=8D=EF=BC=88labels=20=E6=98=A0?= =?UTF-8?q?=E5=B0=84=EF=BC=89+=20=E6=94=B9=E5=90=8D=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E9=BB=98=E8=AE=A4=E7=9B=AE=E7=9A=84=E5=9C=B0?= =?UTF-8?q?=20+=20=E4=BF=9D=E5=AD=98=E6=A0=A1=E9=AA=8C=E6=8F=90=E7=A4=BA?= =?UTF-8?q?=E5=8F=AF=E7=94=A8=E7=9B=AE=E7=9A=84=E5=9C=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 7 +++++++ client.js | 58 +++++++++++++++++++++++++++++++++++++++++-------------- index.js | 53 +++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 94 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 81e9e6d..dacbd56 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,13 @@ profile 的 `package.json`: - 只读/调试下白名单不生效但会保留,切回更改时复用; - 白名单存在 `/.dsh-abap-adt/ui-settings.json`(本插件私有,abap-adt 不会读取)。 +### 显示名(中文) + +- 每个目的地可设置**显示名**(如「广东开发」),只影响界面展示;工具调用仍使用稳定的「标识」(`name`); +- 显示名存在 `ui-settings.json` 的 `labels` 里,不影响 abap-adt 的 schema; +- 改名(标识)时默认目的地下拉会自动跟随,不会出现 `defaultDestination ... is not a destination name`; +- 会话状态徽章、目的地表格、默认目的地选择都会显示「显示名(标识)」。 + ### 安全 - 只读 ≠ 无害:仍能读取 SAP 源码与业务数据,建议连开发/测试系统 + 最小权限账号; diff --git a/client.js b/client.js index 93c2d32..5d16789 100644 --- a/client.js +++ b/client.js @@ -3,7 +3,7 @@ window.__ModuleLoader__.load({ factory(require) { const React = require('react') const h = React.createElement - const { useCallback, useEffect, useMemo, useState } = React + const { useCallback, useEffect, useState } = React const NS = 'abap-adt-ui' const API = '/abap-adt' @@ -19,7 +19,9 @@ window.__ModuleLoader__.load({ destinations: '目的地', empty: '还没有配置目的地。内置 demo 模拟系统始终可用。', add: '新增目的地', - name: '名称', + name: '标识(工具调用用)', + displayName: '显示名(中文)', + displayNameHint: '只用于界面展示;工具调用仍用「标识」', url: 'URL', client: '客户端', language: '语言', @@ -80,7 +82,9 @@ window.__ModuleLoader__.load({ destinations: 'Destinations', empty: 'No destinations configured yet. The built-in demo mock system is always available.', add: 'Add destination', - name: 'Name', + name: 'Identifier (for tool calls)', + displayName: 'Display name', + displayNameHint: 'Shown in the UI only; tool calls still use the identifier', url: 'URL', client: 'Client', language: 'Language', @@ -191,7 +195,7 @@ window.__ModuleLoader__.load({ } function blankDestination() { - return { name: '', url: '', client: '100', language: 'EN', username: '', passwordEnv: '', strictSSL: false, profile: 'dev', description: '' } + return { name: '', label: '', url: '', client: '100', language: 'EN', username: '', passwordEnv: '', strictSSL: false, profile: 'dev', description: '' } } function Field({ label, hint, children }) { @@ -273,13 +277,12 @@ window.__ModuleLoader__.load({ load() }, [load]) - const destinationNames = useMemo( - () => destinations.map((dest) => dest.name).filter((value) => value && value.trim()), - [destinations], - ) - const updateDestination = (index, key, value) => { - setDestinations((rows) => rows.map((row, rowIndex) => (rowIndex === index ? { ...row, [key]: value } : row))) + const row = destinations[index] + // Renaming the identifier keeps the default destination in sync, so a + // rename cannot leave defaultDestination pointing at a gone name. + if (key === 'name' && row && defaultDestination === row.name) setDefaultDestination(value) + setDestinations((rows) => rows.map((item, itemIndex) => (itemIndex === index ? { ...item, [key]: value } : item))) } const addDestination = () => { @@ -288,6 +291,8 @@ window.__ModuleLoader__.load({ } const removeDestination = (index) => { + const removed = destinations[index] + if (removed && removed.name && removed.name === defaultDestination) setDefaultDestination('') setDestinations((rows) => rows.filter((_, rowIndex) => rowIndex !== index)) setMessage('') } @@ -309,9 +314,16 @@ window.__ModuleLoader__.load({ strictSSL: dest.strictSSL, profile: dest.profile, })) + // Display labels are UI-only and keyed by the stable identifier. + const labels = {} + for (const dest of destinations) { + const name = String(dest.name || '').trim() + const label = String(dest.label || '').trim() + if (name && label) labels[name] = label + } const payload = await api('/save', { method: 'POST', - body: JSON.stringify({ workspace, defaultDestination, destinations: payloadDestinations }), + body: JSON.stringify({ workspace, defaultDestination, destinations: payloadDestinations, labels }), }) setMessage(`${DICTS.zh.saved}: ${payload.path}`) setMessageTone('ok') @@ -473,7 +485,8 @@ window.__ModuleLoader__.load({ }, h( 'div', - { style: { display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 10 } }, + { style: { display: 'grid', gridTemplateColumns: '1.2fr 1fr 1.6fr 0.55fr 0.55fr', gap: 10 } }, + h(Field, { label: DICTS.zh.displayName, hint: DICTS.zh.displayNameHint }, h('input', { style: inputStyle, value: dest.label || '', onChange: (e) => updateDestination(index, 'label', e.target.value), placeholder: '广东开发' })), h(Field, { label: DICTS.zh.name }, h('input', { style: inputStyle, value: dest.name || '', onChange: (e) => updateDestination(index, 'name', e.target.value), placeholder: 'dev' })), h(Field, { label: DICTS.zh.url }, h('input', { style: inputStyle, value: dest.url || '', onChange: (e) => updateDestination(index, 'url', e.target.value), placeholder: 'https://sap.example.com:44300' })), h(Field, { label: DICTS.zh.client }, h('input', { style: inputStyle, value: dest.client || '', onChange: (e) => updateDestination(index, 'client', e.target.value), placeholder: '100' })), @@ -628,7 +641,11 @@ window.__ModuleLoader__.load({ 'select', { style: inputStyle, value: defaultDestination, onChange: (e) => setDefaultDestination(e.target.value) }, h('option', { value: '' }, 'demo'), - destinationNames.map((value) => h('option', { key: value, value }, value)), + destinations + .filter((dest) => dest.name) + .map((dest) => + h('option', { key: dest.name, value: dest.name }, dest.label ? `${dest.label}(${dest.name})` : dest.name), + ), ), ), ), @@ -682,7 +699,12 @@ window.__ModuleLoader__.load({ const destinations = state.destinations || [] const names = destinations.map((dest) => dest.name).filter(Boolean) const selected = state.defaultDestination || 'demo' - const options = ['demo', ...names.filter((name) => name !== 'demo')] + const options = [ + { name: 'demo', label: DICTS.zh.chipDemo }, + ...destinations + .filter((dest) => dest.name && dest.name !== 'demo') + .map((dest) => ({ name: dest.name, label: dest.label || dest.name })), + ] const modeText = DICTS.zh.modes[state.mode] const modeBadge = modeText ? { text: modeText, tone: state.mode === 'change' ? 'err' : state.mode === 'debug' ? 'brand' : 'warn' } @@ -744,7 +766,13 @@ window.__ModuleLoader__.load({ maxWidth: 170, }, }, - options.map((name) => h('option', { key: name, value: name }, name)), + options.map((option) => + h( + 'option', + { key: option.name, value: option.name }, + option.label && option.label !== option.name ? `${option.label}(${option.name})` : option.name, + ), + ), ), names.length ? h('span', { style: { fontSize: 11, color: C.muted } }, `${names.length} ${DICTS.zh.chipDest}`) : null, modeBadge ? h(Badge, { tone: modeBadge.tone }, modeBadge.text) : null, diff --git a/index.js b/index.js index 233044d..a156e8b 100644 --- a/index.js +++ b/index.js @@ -358,15 +358,23 @@ export function apply(ctx, config) { } } - /** Read the workspace's UI-only settings (whitelists); defaults when absent. */ + /** Read the workspace's UI-only settings (whitelists + display labels). */ async function readUiSettings(workspace) { - const fallback = { ...WHITELIST_DEFAULTS } + const fallback = { ...WHITELIST_DEFAULTS, labels: {} } try { const raw = await readFile(join(workspace, CONFIG_DIR, UI_SETTINGS_FILE), 'utf8') const parsed = JSON.parse(raw) const pick = (key) => typeof parsed?.[key] === 'string' && parsed[key].trim() ? parsed[key].trim() : fallback[key] - return { allowedPackages: pick('allowedPackages'), allowedTransports: pick('allowedTransports') } + const labels = {} + if (parsed?.labels && typeof parsed.labels === 'object') { + for (const [name, label] of Object.entries(parsed.labels)) { + if (typeof name === 'string' && name && typeof label === 'string' && label.trim()) { + labels[name] = label.trim().slice(0, 80) + } + } + } + return { allowedPackages: pick('allowedPackages'), allowedTransports: pick('allowedTransports'), labels } } catch { return fallback } @@ -376,6 +384,7 @@ export function apply(ctx, config) { const payload = { allowedPackages: values.allowedPackages || WHITELIST_DEFAULTS.allowedPackages, allowedTransports: values.allowedTransports || WHITELIST_DEFAULTS.allowedTransports, + labels: values.labels && typeof values.labels === 'object' ? values.labels : {}, } await mkdir(join(workspace, CONFIG_DIR), { recursive: true }) await writeFile(join(workspace, CONFIG_DIR, UI_SETTINGS_FILE), `${JSON.stringify(payload, null, 2)}\n`, 'utf8') @@ -510,12 +519,21 @@ export function apply(ctx, config) { const presetInfo = presetInfoOf(sessionId) const mode = modeFromPolicy(parsed.policy) const whitelist = await readUiSettings(cwd) + // Display labels are UI-only: attach the Chinese label beside the stable + // destination name so the page/chip can show it without touching the plugin + // schema (which accepts only the known destination keys). + const labels = whitelist.labels || {} + const destinations = normalizeDestinations(parsed.destinations).map((dest) => ({ + ...dest, + label: labels[dest.name] || '', + })) return { workspace: cwd, workspaces: listWorkspaces(), file: { path: file, exists, bytes: raw.length }, chip: chipGate(cwd, exists), - destinations: normalizeDestinations(parsed.destinations), + destinations, + labels, defaultDestination: parsed.defaultDestination || '', policy: parsed.policy || {}, parser: parsed.parser, @@ -723,7 +741,10 @@ export function apply(ctx, config) { defaultDestination !== 'demo' && !destinations.some((dest) => dest.name === defaultDestination) ) { - sendJson(res, 400, { ok: false, error: `defaultDestination "${defaultDestination}" is not a destination name` }) + sendJson(res, 400, { + ok: false, + error: `defaultDestination "${defaultDestination}" is not a destination name (available: demo, ${destinations.map((dest) => dest.name).join(', ') || '(none)'})`, + }) return } const file = existingWorkspaceFile(workspace) @@ -737,10 +758,23 @@ export function apply(ctx, config) { } } await mkdir(join(workspace, CONFIG_DIR), { recursive: true }) - const whitelist = await readUiSettings(workspace) - const text = setModeInText(emitDestinations({ defaultDestination, destinations }), existingMode, whitelist) + const uiSettings = await readUiSettings(workspace) + // Display labels: keep only labels of destinations that still exist. + const names = new Set(destinations.map((dest) => dest.name)) + let labels = uiSettings.labels || {} + if (body.labels && typeof body.labels === 'object') { + labels = {} + for (const [name, label] of Object.entries(body.labels)) { + if (typeof name === 'string' && name && typeof label === 'string' && label.trim()) { + labels[name] = label.trim().slice(0, 80) + } + } + } + labels = Object.fromEntries(Object.entries(labels).filter(([name]) => names.has(name))) + await writeUiSettings(workspace, { ...uiSettings, labels }) + const text = setModeInText(emitDestinations({ defaultDestination, destinations }), existingMode, uiSettings) await writeFile(file, text, 'utf8') - sendJson(res, 200, { ok: true, path: file, destinations: destinations.length, mode: existingMode }) + sendJson(res, 200, { ok: true, path: file, destinations: destinations.length, mode: existingMode, labels }) return } @@ -781,7 +815,8 @@ export function apply(ctx, config) { } } if (body.allowedPackages !== undefined || body.allowedTransports !== undefined) { - await writeUiSettings(workspace, nextWhitelist) + // Preserve display labels when persisting whitelists. + await writeUiSettings(workspace, { ...current, ...nextWhitelist }) } await writeFile(file, setModeInText(raw, mode, nextWhitelist), 'utf8') sendJson(res, 200, {