sapbuddy-dsh 0.0.7 → 0.0.8
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 +17 -4
- package/dist/client.js +20 -5
- package/dist/index.js +85 -11
- package/dist/sap-tools/connections-store.js +12 -2
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
在 DSH 的插件管理页添加:
|
|
11
11
|
|
|
12
12
|
```text
|
|
13
|
-
sapbuddy-dsh@0.0.
|
|
13
|
+
sapbuddy-dsh@0.0.8
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
也可以通过 DSH 命令行安装:
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
dsh plugin --profile desktop add sapbuddy-dsh@0.0.
|
|
19
|
+
dsh plugin --profile desktop add sapbuddy-dsh@0.0.8
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
更新已安装插件后,完整退出并重新启动 DSH,再新建会话选择 **SapBuddy**。
|
|
@@ -32,6 +32,19 @@ dsh plugin --profile desktop add sapbuddy-dsh@0.0.7
|
|
|
32
32
|
|
|
33
33
|
源码不一致、相关对象仍未激活或核对查询失败时,继续保留失败。对象元数据中的 `active` 字段不能单独证明本次修改已生效。非源码对象沿用其激活接口结果。
|
|
34
34
|
|
|
35
|
+
## 连接配置与排查
|
|
36
|
+
|
|
37
|
+
地址必须写全协议,协议要和端口一致:
|
|
38
|
+
|
|
39
|
+
| 端口 | 协议 | 示例 |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| ICM HTTP(常见 8000) | `http://` | `http://192.168.111.85:8000` |
|
|
42
|
+
| ICM HTTPS(常见 44300) | `https://` | `https://10.8.202.35:44300` |
|
|
43
|
+
|
|
44
|
+
把 HTTP 端口写成 `https://`(或反过来)会在 TLS 阶段直接失败。客户端号就是登录用的 `sap-client`,必须与要登录的客户端一致——系统是 300 却填 200 会直接返回 401 认证失败;登录语言不确定时填 `EN`。内部系统若是自签名证书,编辑连接时保持「允许自签名证书」勾选。
|
|
45
|
+
|
|
46
|
+
设置页的 **测试** 按钮与真实 SAP 工具走同一套协议/客户端号/语言(早期版本把探活写死成 https 且只发 `X-sap-login-client` 头,会出现"账号密码都对却报 401"或"http 端口永远探测失败"的假阴性)。测试返回 401 时按提示核对客户端号、账号状态与登录语言;403/404 多为 ADT 服务未激活或用户无权限。
|
|
47
|
+
|
|
35
48
|
## 连接切换与会话自愈
|
|
36
49
|
|
|
37
50
|
切换或保存 SAP 连接后,插件会立刻作废进程内池化的 ADT 客户端,下一次调用按新配置重新登录,不会带着上一个系统的 cookie 与 CSRF 令牌去读写当前系统。
|
|
@@ -61,11 +74,11 @@ dsh plugin --profile desktop add sapbuddy-dsh@0.0.7
|
|
|
61
74
|
从 GitHub 克隆仓库后,在 `packages/sapbuddy-dsh` 下运行激活回归测试(需要 Node.js 24+,无需连接 SAP):
|
|
62
75
|
|
|
63
76
|
```bash
|
|
64
|
-
npm test # = test:activation + test:session + test:store
|
|
77
|
+
npm test # = test:activation + test:session + test:store + test:probe + test:route + test:bundle
|
|
65
78
|
npm pack --dry-run --ignore-scripts
|
|
66
79
|
```
|
|
67
80
|
|
|
68
|
-
`test:activation` 覆盖激活结果核对的误报恢复、失败计数、审计记录和核对失败边界;`test:session` 覆盖写前会话刷新、CSRF 失效自动重建,以及"已落库则绝不重试"的边界;`test:store`
|
|
81
|
+
`test:activation` 覆盖激活结果核对的误报恢复、失败计数、审计记录和核对失败边界;`test:session` 覆盖写前会话刷新、CSRF 失效自动重建,以及"已落库则绝不重试"的边界;`test:store` 用真实模块验证设置页保存/切换/删除连接确实会作废连接池;`test:probe` 覆盖连接探测的协议/客户端号/语言与 401 提示;`test:route` 起一个本地 http 服务端到端验证「测试」按钮(含 sap-client 传递与 401 提示);`test:bundle` 对设置页 bundle 做渲染冒烟。测试都使用模拟 SAP 响应,不能替代真实 SAP 联机验收。
|
|
69
82
|
|
|
70
83
|
## 许可证
|
|
71
84
|
|
package/dist/client.js
CHANGED
|
@@ -66,6 +66,7 @@ var S = {
|
|
|
66
66
|
badge: { border: "1px solid var(--dsw-alias-border-l3)", color: "var(--dsw-alias-label-secondary)", borderRadius: 4, padding: "1px 6px", fontSize: 11, lineHeight: "16px", flex: "none" },
|
|
67
67
|
badgeActive: { background: "var(--dsw-alias-state-success-primary)", borderColor: "var(--dsw-alias-state-success-primary)", color: "var(--dsw-alias-label-primary-foreground)" },
|
|
68
68
|
meta: { margin: 0, fontSize: 12, lineHeight: "18px", color: "var(--dsw-alias-label-tertiary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
69
|
+
testMsg: { margin: 0, fontSize: 12, lineHeight: "18px", color: "var(--dsw-alias-label-secondary)", whiteSpace: "pre-wrap" },
|
|
69
70
|
actions: { display: "flex", gap: 6, marginLeft: "auto", flex: "none" },
|
|
70
71
|
btn: { height: 28, padding: "0 10px", borderRadius: 14, fontSize: 12, lineHeight: "18px", cursor: "pointer", border: "1px solid var(--dsw-alias-border-l2)", background: "transparent", color: "var(--dsw-alias-label-primary)", font: "inherit" },
|
|
71
72
|
btnPrimary: { background: "var(--dsw-alias-button-primary-fill)", color: "var(--dsw-alias-label-primary-foreground)", border: "none" },
|
|
@@ -178,24 +179,38 @@ function Section() {
|
|
|
178
179
|
{ key: "m", style: S.meta },
|
|
179
180
|
c.url + (c.client ? " \xB7 Client " + c.client : "") + " \xB7 " + c.username + (c.hasPassword ? " \xB7 \u5BC6\u7801\u5DF2\u8BBE\u7F6E" : " \xB7 \u672A\u8BBE\u7F6E\u5BC6\u7801")
|
|
180
181
|
),
|
|
181
|
-
testResult[c.id] ? React.createElement("p", { key: "tr", style: S.
|
|
182
|
+
testResult[c.id] ? React.createElement("p", { key: "tr", style: S.testMsg }, testResult[c.id]) : null
|
|
182
183
|
]);
|
|
183
184
|
const editor = React.createElement("div", { key: "editor", style: S.editor }, [
|
|
184
185
|
React.createElement("div", { key: "g1", style: S.grid }, [
|
|
185
186
|
input("id", "\u8FDE\u63A5 ID\uFF08\u552F\u4E00\uFF0C\u5982 dev/prod\uFF09", "dev"),
|
|
186
187
|
input("name", "\u540D\u79F0", "\u5F00\u53D1\u7CFB\u7EDF"),
|
|
187
|
-
input("url", "\u5730\u5740\
|
|
188
|
-
input("client", "\u5BA2\u6237\u7AEF\u53F7", "100")
|
|
188
|
+
input("url", "\u5730\u5740\uFF08http(s)://host:port\uFF09", "http://10.8.202.35:8000 \u6216 https://10.8.202.35:44300"),
|
|
189
|
+
input("client", "\u5BA2\u6237\u7AEF\u53F7\uFF083 \u4F4D\uFF0C\u5FC5\u987B\u4E0E\u7CFB\u7EDF\u4E00\u81F4\uFF09", "100")
|
|
189
190
|
]),
|
|
190
191
|
React.createElement("div", { key: "g2", style: S.grid }, [
|
|
191
192
|
input("username", "\u7528\u6237\u540D", "YOUR_USER"),
|
|
192
193
|
input("password", "\u5BC6\u7801\uFF08\u7F16\u8F91\u65F6\u7559\u7A7A\u4FDD\u6301\u539F\u5BC6\u7801\uFF09", "\u2022\u2022\u2022\u2022\u2022\u2022"),
|
|
193
|
-
input("language", "\u8BED\u8A00", "ZH"),
|
|
194
|
+
input("language", "\u767B\u5F55\u8BED\u8A00\uFF08\u4E0D\u786E\u5B9A\u586B EN\uFF09", "ZH"),
|
|
194
195
|
input("authMethod", "\u8BA4\u8BC1\u65B9\u5F0F", "basic")
|
|
195
196
|
]),
|
|
196
197
|
React.createElement("div", { key: "g3", style: S.grid }, [
|
|
197
198
|
input("description", "\u63CF\u8FF0", "\u5F00\u53D1\u7CFB\u7EDF")
|
|
198
199
|
]),
|
|
200
|
+
React.createElement("label", { key: "ssl", style: { ...S.field, flexDirection: "row", alignItems: "center", gap: 8 } }, [
|
|
201
|
+
React.createElement("input", {
|
|
202
|
+
key: "i",
|
|
203
|
+
type: "checkbox",
|
|
204
|
+
checked: !draft.ssl || draft.ssl.allowSelfSigned !== false,
|
|
205
|
+
onChange: (evt) => setDraft((d) => ({ ...d, ssl: { ...(d.ssl || {}), allowSelfSigned: evt.target.checked } }))
|
|
206
|
+
}),
|
|
207
|
+
React.createElement("span", { key: "l", style: S.label }, "\u5141\u8BB8\u81EA\u7B7E\u540D\u8BC1\u4E66\uFF08\u5185\u90E8\u7CFB\u7EDF https \u7528\uFF09")
|
|
208
|
+
]),
|
|
209
|
+
React.createElement(
|
|
210
|
+
"p",
|
|
211
|
+
{ key: "hint", style: S.hint },
|
|
212
|
+
"\u7AEF\u53E3\u5BF9\u5E94\u534F\u8BAE\uFF1ASAP \u7684 ICM HTTP \u7AEF\u53E3\uFF08\u5E38\u89C1 8000\uFF09\u7528 http://\uFF0CHTTPS \u7AEF\u53E3\uFF08\u5E38\u89C1 44300\uFF09\u7528 https://\u3002\u5BA2\u6237\u7AEF\u53F7\u5C31\u662F\u767B\u5F55\u5BA2\u6237\u7AEF\uFF08sap-client\uFF09\uFF0C\u586B\u9519\u4F1A\u8FD4\u56DE 401\uFF1B\u767B\u5F55\u8BED\u8A00\u4E0D\u786E\u5B9A\u65F6\u586B EN\u3002"
|
|
213
|
+
),
|
|
199
214
|
React.createElement("div", { key: "acts", style: { display: "flex", gap: 8, justifyContent: "flex-end" } }, [
|
|
200
215
|
React.createElement("button", { key: "c", style: S.btn, onClick: () => {
|
|
201
216
|
setMode(null);
|
|
@@ -209,7 +224,7 @@ function Section() {
|
|
|
209
224
|
React.createElement(
|
|
210
225
|
"p",
|
|
211
226
|
{ key: "i", style: S.intro },
|
|
212
|
-
"\u914D\u7F6E\u4E00\u4E2A\u6216\u591A\u4E2A SAP \u7CFB\u7EDF\u8FDE\u63A5\uFF08ADT\uFF09\u3002\u5207\u6362\u4E3A\u542F\u7528\u540E\uFF0CSapBuddy \u4F1A\u8BDD\u7684 SAP \u5DE5\u5177\u9ED8\u8BA4\u4F7F\u7528\u8BE5\u8FDE\u63A5\uFF1B\u5207\u6362\u540E\u9700\u5728\u4F1A\u8BDD\u4E2D\u5148\u8C03\u7528 get_connected_systems \u786E\u8BA4\u3002\u5BC6\u7801\u4EC5\u4FDD\u5B58\u5728\u672C\u673A\u914D\u7F6E\u6587\u4EF6\u3002"
|
|
227
|
+
"\u914D\u7F6E\u4E00\u4E2A\u6216\u591A\u4E2A SAP \u7CFB\u7EDF\u8FDE\u63A5\uFF08ADT\uFF09\u3002\u5207\u6362\u4E3A\u542F\u7528\u540E\uFF0CSapBuddy \u4F1A\u8BDD\u7684 SAP \u5DE5\u5177\u9ED8\u8BA4\u4F7F\u7528\u8BE5\u8FDE\u63A5\uFF1B\u5207\u6362\u540E\u9700\u5728\u4F1A\u8BDD\u4E2D\u5148\u8C03\u7528 get_connected_systems \u786E\u8BA4\u3002\u5BC6\u7801\u4EC5\u4FDD\u5B58\u5728\u672C\u673A\u914D\u7F6E\u6587\u4EF6\u3002\u5730\u5740\u8981\u5199\u5168\u534F\u8BAE\uFF1Ahttp:// \u7528\u4E8E ICM HTTP \u7AEF\u53E3\uFF08\u5E38\u89C1 8000\uFF09\uFF0Chttps:// \u7528\u4E8E HTTPS \u7AEF\u53E3\uFF08\u5E38\u89C1 44300\uFF09\u3002"
|
|
213
228
|
),
|
|
214
229
|
msg ? React.createElement("p", { key: "m", style: S.msg }, msg) : null,
|
|
215
230
|
err ? React.createElement("p", { key: "e", style: S.err }, err) : null,
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import http from "node:http";
|
|
1
2
|
import https from "node:https";
|
|
2
3
|
import { listConnections, saveConnection, deleteConnection, activateConnection, getConnectionSecret, } from "./sap-tools/connections-store.js";
|
|
3
4
|
const ROUTES = [
|
|
@@ -33,22 +34,95 @@ function readBody(req) {
|
|
|
33
34
|
req.on("error", reject);
|
|
34
35
|
});
|
|
35
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* 把连接配置规范成一次 ADT 探活请求。
|
|
39
|
+
*
|
|
40
|
+
* 注意两件事(曾经踩过的坑):
|
|
41
|
+
* 1. 协议必须跟随配置里的 URL:SAP 的 ICM HTTP 端口(常见 8000)是 **http**,
|
|
42
|
+
* HTTPS 端口(常见 44300)才是 https。写死 https 会让 http 连接永远探测失败。
|
|
43
|
+
* 2. 客户端号/语言要与真实工具调用一致——通过查询参数 `sap-client` / `sap-language` 传
|
|
44
|
+
* (abap-adt-api 登录就是这么做的),同时保留 `X-sap-login-client` 头兼容只认该头的系统。
|
|
45
|
+
* 只发头不发参数时,部分系统会忽略头、用默认客户端登录,于是返回 401。
|
|
46
|
+
*
|
|
47
|
+
* @returns {{ error: string } | { scheme: "http" | "https", url: URL, headers: Record<string, string> }}
|
|
48
|
+
*/
|
|
49
|
+
export function buildProbeRequest(conf) {
|
|
50
|
+
const raw = String(conf?.url ?? "").trim().replace(/\/+$/, "");
|
|
51
|
+
if (!/^https?:\/\//i.test(raw))
|
|
52
|
+
return { error: `连接地址要以 http:// 或 https:// 开头(当前是 "${raw || "(空)"}")。` +
|
|
53
|
+
`例如 http://10.8.202.35:8000(ICM HTTP 端口)或 https://10.8.202.35:44300(HTTPS 端口)` };
|
|
54
|
+
let url;
|
|
55
|
+
try {
|
|
56
|
+
url = new URL(`${raw}/sap/bc/adt/compatibility/graph`);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return { error: `连接地址不合法: ${raw}` };
|
|
60
|
+
}
|
|
61
|
+
const scheme = url.protocol === "https:" ? "https" : "http";
|
|
62
|
+
const qs = new URLSearchParams();
|
|
63
|
+
if (conf.client)
|
|
64
|
+
qs.set("sap-client", String(conf.client));
|
|
65
|
+
if (conf.language)
|
|
66
|
+
qs.set("sap-language", String(conf.language));
|
|
67
|
+
if ([...qs.keys()].length > 0)
|
|
68
|
+
url.search = qs.toString();
|
|
69
|
+
const headers = {
|
|
70
|
+
Authorization: "Basic " + Buffer.from(`${conf.username}:${conf.password ?? ""}`).toString("base64"),
|
|
71
|
+
Accept: "*/*",
|
|
72
|
+
};
|
|
73
|
+
if (conf.client)
|
|
74
|
+
headers["X-sap-login-client"] = String(conf.client);
|
|
75
|
+
return { scheme, url, headers };
|
|
76
|
+
}
|
|
77
|
+
/** 把探活拿到的 HTTP 状态码翻译成能直接照做的排查建议;null 表示成功 */
|
|
78
|
+
export function describeProbeStatus(status, conf) {
|
|
79
|
+
if (status >= 200 && status < 300)
|
|
80
|
+
return null;
|
|
81
|
+
if (status === 401)
|
|
82
|
+
return "认证失败(HTTP 401):服务器拒绝了账号/密码。按顺序排查:\n" +
|
|
83
|
+
`① 客户端号是否与系统一致(当前填的是 ${conf.client || "空"},登录时作为 sap-client 发送);\n` +
|
|
84
|
+
`② 用户名/密码是否正确、账号是否被锁或过期(当前用户 ${conf.username || "空"});\n` +
|
|
85
|
+
`③ 语言 ${conf.language || "空"} 是否为该账号可用语言(不确定先填 EN);\n` +
|
|
86
|
+
"④ 该客户端是否允许 ADT 登录(SICF 里 /sap/bc/adt 的登录过程)。";
|
|
87
|
+
if (status === 403)
|
|
88
|
+
return "HTTP 403:账号认证通过但无权访问 ADT,或 /sap/bc/adt 服务未激活/被限制。请让 BASIS 检查 SICF 与用户 ADT 权限。";
|
|
89
|
+
if (status === 404)
|
|
90
|
+
return "HTTP 404:这个地址上没有 ADT 服务。确认端口是 SAP 的 HTTP/HTTPS 端口(8000/44300),且 /sap/bc/adt 已在 SICF 激活。";
|
|
91
|
+
if (status >= 500)
|
|
92
|
+
return `HTTP ${status}:SAP 服务端错误(ICM/ADT 异常),请稍后重试或联系 BASIS。`;
|
|
93
|
+
return `HTTP ${status}:未预期的响应,请检查地址与端口。`;
|
|
94
|
+
}
|
|
95
|
+
/** 网络层失败时给出"协议/端口写反"这类最常见的提示 */
|
|
96
|
+
function describeProbeFailure(message, scheme) {
|
|
97
|
+
if (/wrong version number|SSL routines|EPROTO|packet length too long|socket hang up/i.test(message))
|
|
98
|
+
return `连接失败: ${message}\n` +
|
|
99
|
+
`协议和端口对不上:这个端口很可能是 ${scheme === "https" ? "纯 HTTP(用 http://)" : "HTTPS(用 https://)"}。` +
|
|
100
|
+
"SAP 的 ICM HTTP 端口(常见 8000)要用 http://,HTTPS 端口(常见 44300)要用 https://。";
|
|
101
|
+
if (/certificate|self-signed|SELF_SIGNED|CERT_|unable to verify/i.test(message))
|
|
102
|
+
return `连接失败: ${message}\n证书不被信任:内部/自签名证书请在连接里开启「允许自签名证书」。`;
|
|
103
|
+
if (/ENOTFOUND|EAI_AGAIN/i.test(message))
|
|
104
|
+
return `连接失败: 主机名解析不了(${message})。检查地址拼写,或改用 IP。`;
|
|
105
|
+
if (/ECONNREFUSED/i.test(message))
|
|
106
|
+
return `连接失败: 端口没有监听(${message})。检查端口号与 SAP 实例状态。`;
|
|
107
|
+
if (/timeout|ETIMEDOUT|超时/i.test(message))
|
|
108
|
+
return `连接失败: 连接超时(${message})。检查网络/防火墙是否放通该端口。`;
|
|
109
|
+
return `连接失败: ${message}`;
|
|
110
|
+
}
|
|
36
111
|
/** 测试连接:用连接明文凭据做一次 ADT 请求(结果只回状态码,不回敏感信息)。
|
|
37
|
-
* 用 node:https 直连以支持 ssl.allowSelfSigned(Node fetch 不支持该选项)。 */
|
|
112
|
+
* 用 node:http/https 直连以支持 ssl.allowSelfSigned(Node fetch 不支持该选项)。 */
|
|
38
113
|
async function testConnection(id) {
|
|
39
114
|
const c = getConnectionSecret(id);
|
|
40
115
|
if (!c)
|
|
41
116
|
return { ok: false, status: 0, message: `连接 ${id} 不存在` };
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
headers["X-sap-login-client"] = c.client;
|
|
117
|
+
const probe = buildProbeRequest(c);
|
|
118
|
+
if (probe.error)
|
|
119
|
+
return { ok: false, status: 0, message: probe.error };
|
|
120
|
+
const transport = probe.scheme === "https" ? https : http;
|
|
47
121
|
try {
|
|
48
122
|
const status = await new Promise((resolve, reject) => {
|
|
49
|
-
const req =
|
|
123
|
+
const req = transport.request(probe.url, {
|
|
50
124
|
method: "GET",
|
|
51
|
-
headers,
|
|
125
|
+
headers: probe.headers,
|
|
52
126
|
rejectUnauthorized: !(c.ssl?.allowSelfSigned ?? false),
|
|
53
127
|
timeout: 15_000,
|
|
54
128
|
}, (res) => { res.resume(); resolve(res.statusCode ?? 0); });
|
|
@@ -56,11 +130,11 @@ async function testConnection(id) {
|
|
|
56
130
|
req.on("error", reject);
|
|
57
131
|
req.end();
|
|
58
132
|
});
|
|
59
|
-
const
|
|
60
|
-
return { ok, status, message:
|
|
133
|
+
const detail = describeProbeStatus(status, c);
|
|
134
|
+
return detail ? { ok: false, status, message: detail } : { ok: true, status, message: "连接成功" };
|
|
61
135
|
}
|
|
62
136
|
catch (e) {
|
|
63
|
-
return { ok: false, status: 0, message:
|
|
137
|
+
return { ok: false, status: 0, message: describeProbeFailure(e instanceof Error ? e.message : String(e), probe.scheme) };
|
|
64
138
|
}
|
|
65
139
|
}
|
|
66
140
|
/**
|
|
@@ -55,10 +55,19 @@ function validate(input) {
|
|
|
55
55
|
return "连接 ID 不能为空";
|
|
56
56
|
if (!/^[A-Za-z0-9_-]+$/.test(id))
|
|
57
57
|
return "连接 ID 只能包含字母/数字/下划线/连字符";
|
|
58
|
-
|
|
58
|
+
const url = String(input.url ?? "").trim();
|
|
59
|
+
if (!url)
|
|
59
60
|
return "连接地址(url)不能为空";
|
|
61
|
+
// 协议必须显式写:SAP 的 ICM HTTP 端口(常见 8000)是 http,HTTPS 端口(常见 44300)才是 https。
|
|
62
|
+
// 不写协议时真实工具与探测都会拿不到明确结论,干脆在保存时就拦下。
|
|
63
|
+
if (!/^https?:\/\//i.test(url))
|
|
64
|
+
return "连接地址要以 http:// 或 https:// 开头(ICM HTTP 端口常见 8000 用 http://,HTTPS 端口常见 44300 用 https://)";
|
|
60
65
|
if (!String(input.username ?? "").trim())
|
|
61
66
|
return "用户名不能为空";
|
|
67
|
+
// 客户端号就是登录时的 sap-client,填错(比如系统是 300 却填 200)会直接 401
|
|
68
|
+
const client = String(input.client ?? "").trim();
|
|
69
|
+
if (client && !/^\d{1,3}$/.test(client))
|
|
70
|
+
return "客户端号应为 1-3 位数字(如 100)";
|
|
62
71
|
return undefined;
|
|
63
72
|
}
|
|
64
73
|
/** 新建或更新连接;更新时密码为空/'***' 保持原密码。返回 {ok} 或 {ok:false,error} */
|
|
@@ -72,12 +81,13 @@ export function saveConnection(input) {
|
|
|
72
81
|
const password = input.password === undefined || input.password === "" || input.password === PASSWORD_MASK
|
|
73
82
|
? existing?.password ?? ""
|
|
74
83
|
: input.password;
|
|
84
|
+
const rawClient = String(input.client ?? existing?.client ?? "").trim();
|
|
75
85
|
const next = {
|
|
76
86
|
...existing,
|
|
77
87
|
...input,
|
|
78
88
|
id,
|
|
79
89
|
password,
|
|
80
|
-
client:
|
|
90
|
+
client: rawClient ? rawClient.padStart(3, "0") : rawClient,
|
|
81
91
|
language: input.language ?? existing?.language ?? "ZH",
|
|
82
92
|
authMethod: input.authMethod ?? existing?.authMethod ?? "basic",
|
|
83
93
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sapbuddy-dsh",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "SapBuddy —— SAP ABAP AI 全能助手(DeepSeek Harness 插件):41 个 SAP 工具 + 8 个 ABAP 技能 + SapBuddy preset + 连接设置页。一个包名装完。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -77,10 +77,13 @@
|
|
|
77
77
|
"typescript": "^5.6.0"
|
|
78
78
|
},
|
|
79
79
|
"scripts": {
|
|
80
|
-
"test": "npm run test:activation && npm run test:session && npm run test:store",
|
|
80
|
+
"test": "npm run test:activation && npm run test:session && npm run test:store && npm run test:probe && npm run test:route && npm run test:bundle",
|
|
81
81
|
"test:activation": "node --experimental-vm-modules --test --test-isolation=none test/activation.test.mjs",
|
|
82
82
|
"test:session": "node --experimental-vm-modules --test --test-isolation=none test/session-recovery.test.mjs",
|
|
83
|
-
"test:store": "node --test --test-isolation=none test/connections-store.test.mjs"
|
|
83
|
+
"test:store": "node --test --test-isolation=none test/connections-store.test.mjs",
|
|
84
|
+
"test:probe": "node --test --test-isolation=none test/connection-probe.test.mjs",
|
|
85
|
+
"test:route": "node --test --test-isolation=none test/connection-test-route.test.mjs",
|
|
86
|
+
"test:bundle": "node --test --test-isolation=none test/client-bundle.test.mjs"
|
|
84
87
|
},
|
|
85
88
|
"publishConfig": {
|
|
86
89
|
"access": "public"
|