dsh-scnet 0.6.9 → 0.6.11
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 +8 -9
- package/README_CN.md +7 -6
- package/connector/callback.mjs +155 -0
- package/connector/index.mjs +4 -0
- package/package.json +1 -1
- package/skills/scnet-hpc/VERSION +1 -1
- package/skills/scnet-hpc/scripts/_common.sh +4 -2
- package/skills/scnet-hpc/scripts/scnet.py +20 -6
package/README.md
CHANGED
|
@@ -4,17 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
DSH-SCNet (`dsh-scnet` on npm) is a community-maintained DSH bundle for operating Supercomputing Network (SCNet). It packages the source [`scnet-hpc`](https://github.com/lql341/scnet-hpc) Skill, profile-aware utilities, and bounded tools for SSH, Slurm, OpenAPI files, regional resources, and Notebook discovery.
|
|
6
6
|
|
|
7
|
-
Current release: **0.6.
|
|
7
|
+
Current release: **0.6.11**
|
|
8
8
|
|
|
9
9
|
<!-- scnet-release:start -->
|
|
10
|
-
## 0.6.
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
unrelated configuration fields when settings are updated.
|
|
10
|
+
## 0.6.11 highlights
|
|
11
|
+
|
|
12
|
+
- Generate Slurm job scripts even when the optional SSH node-count probe fails, using
|
|
13
|
+
profile/cache metadata as a fallback; add deterministic coverage for probe outcomes.
|
|
14
|
+
- Correct Windows configuration-permission diagnostics and clarify WSL2 setup guidance.
|
|
15
|
+
- Keep shell scripts on LF line endings across all three repositories; synchronize downstream
|
|
16
|
+
versions and README release blocks after canonical main validation succeeds.
|
|
18
17
|
<!-- scnet-release:end -->
|
|
19
18
|
|
|
20
19
|
> This is an independent community project. It is compatible with DeepSeek Harness but is not an official DeepSeek product and does not imply endorsement, partnership, or authorization by DeepSeek.
|
package/README_CN.md
CHANGED
|
@@ -4,15 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
<p align="center">在 DeepSeek Harness 里操作超算互联网(SCNet):配置 SSH、管理 Slurm 作业、查询 OpenAPI 区域和文件、检查 Notebook 资源与实例,并保留海光 DCU/DTK 诊断能力。</p>
|
|
6
6
|
|
|
7
|
-
当前版本:**0.6.
|
|
7
|
+
当前版本:**0.6.11**
|
|
8
8
|
|
|
9
9
|
<!-- scnet-release:start -->
|
|
10
|
-
## 0.6.
|
|
10
|
+
## 0.6.11 更新
|
|
11
11
|
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
12
|
+
- SSH 实时节点数探测失败时,回退到 profile/cache,继续生成 Slurm 作业脚本;
|
|
13
|
+
使用模拟 SSH 覆盖各类探测结果,回归测试不依赖真实网络。
|
|
14
|
+
- 修复 Windows 配置文件权限诊断误报,补充 Windows 提示与 WSL2 使用指引。
|
|
15
|
+
- 三个仓库统一保持 shell 脚本使用 LF;源仓库 main 校验通过后,自动同步下游版本与
|
|
16
|
+
中英文 README 发布说明。
|
|
16
17
|
<!-- scnet-release:end -->
|
|
17
18
|
|
|
18
19
|
> 本项目是独立维护的社区项目,与 DeepSeek Harness 兼容,但不是 DeepSeek 官方产品,也不表示 DeepSeek 对本项目提供背书、合作或授权。
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { createServer } from 'node:http'
|
|
2
|
+
|
|
3
|
+
import { ConnectorError } from './errors.mjs'
|
|
4
|
+
|
|
5
|
+
const LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', '[::1]', 'localhost'])
|
|
6
|
+
const CALLBACK_TIMEOUT_MS = 120000
|
|
7
|
+
|
|
8
|
+
function requiredString(value, label) {
|
|
9
|
+
const normalized = String(value ?? '').trim()
|
|
10
|
+
if (!normalized) throw new TypeError(`${label} must be a non-empty string`)
|
|
11
|
+
return normalized
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseLoopbackRedirect(value) {
|
|
15
|
+
let redirect
|
|
16
|
+
try {
|
|
17
|
+
redirect = new URL(requiredString(value, 'redirectUri'))
|
|
18
|
+
} catch {
|
|
19
|
+
throw new TypeError('redirectUri must be a valid URL')
|
|
20
|
+
}
|
|
21
|
+
if (redirect.protocol !== 'http:' || !LOOPBACK_HOSTS.has(redirect.hostname)) {
|
|
22
|
+
throw new TypeError('redirectUri must use an http loopback address')
|
|
23
|
+
}
|
|
24
|
+
if (!redirect.pathname || redirect.pathname === '/') {
|
|
25
|
+
throw new TypeError('redirectUri must include a callback path')
|
|
26
|
+
}
|
|
27
|
+
if (redirect.username || redirect.password || redirect.hash) {
|
|
28
|
+
throw new TypeError('redirectUri must not include credentials or a fragment')
|
|
29
|
+
}
|
|
30
|
+
return redirect
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function responseBody(res, statusCode, body) {
|
|
34
|
+
const content = Buffer.from(body, 'utf8')
|
|
35
|
+
res.writeHead(statusCode, {
|
|
36
|
+
'content-type': 'text/html; charset=utf-8',
|
|
37
|
+
'content-length': content.byteLength,
|
|
38
|
+
connection: 'close',
|
|
39
|
+
})
|
|
40
|
+
res.end(content)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Start a one-shot local OAuth callback listener.
|
|
45
|
+
*
|
|
46
|
+
* The listener is protocol-neutral: the caller owns the authorization session
|
|
47
|
+
* and provides its callback consumer. Browser responses never contain callback
|
|
48
|
+
* parameters or private error details.
|
|
49
|
+
*/
|
|
50
|
+
export async function listenLoopbackCallback({
|
|
51
|
+
redirectUri,
|
|
52
|
+
consumeCallback,
|
|
53
|
+
timeoutMs = CALLBACK_TIMEOUT_MS,
|
|
54
|
+
} = {}) {
|
|
55
|
+
const requested = parseLoopbackRedirect(redirectUri)
|
|
56
|
+
if (typeof consumeCallback !== 'function') {
|
|
57
|
+
throw new TypeError('consumeCallback must be a function')
|
|
58
|
+
}
|
|
59
|
+
const timeout = Number(timeoutMs)
|
|
60
|
+
if (!Number.isFinite(timeout) || timeout <= 0) {
|
|
61
|
+
throw new TypeError('timeoutMs must be a positive number')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let settled = false
|
|
65
|
+
let timer
|
|
66
|
+
let actualRedirect
|
|
67
|
+
let resolveResult
|
|
68
|
+
let rejectResult
|
|
69
|
+
const result = new Promise((resolve, reject) => {
|
|
70
|
+
resolveResult = resolve
|
|
71
|
+
rejectResult = reject
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const server = createServer((req, res) => {
|
|
75
|
+
if (settled) {
|
|
76
|
+
responseBody(res, 410, '<!doctype html><title>Callback closed</title>')
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
const callbackUrl = actualRedirect && new URL(req.url || '/', actualRedirect.origin)
|
|
80
|
+
if (req.method !== 'GET' || !callbackUrl || callbackUrl.pathname !== requested.pathname) {
|
|
81
|
+
responseBody(res, 404, '<!doctype html><title>Not found</title>')
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
settle(callbackUrl.href, res)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
function closeServer() {
|
|
88
|
+
if (timer) clearTimeout(timer)
|
|
89
|
+
if (server.listening) server.close()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function settle(callbackUrl, res) {
|
|
93
|
+
if (settled) return
|
|
94
|
+
settled = true
|
|
95
|
+
try {
|
|
96
|
+
const value = consumeCallback(callbackUrl)
|
|
97
|
+
responseBody(res, 200, '<!doctype html><title>Authorization complete</title>')
|
|
98
|
+
resolveResult(value)
|
|
99
|
+
} catch (error) {
|
|
100
|
+
responseBody(res, 400, '<!doctype html><title>Authorization failed</title>')
|
|
101
|
+
rejectResult(
|
|
102
|
+
error instanceof ConnectorError
|
|
103
|
+
? error
|
|
104
|
+
: new ConnectorError('CALLBACK_MISMATCH', error?.message || 'OAuth callback rejected', {
|
|
105
|
+
operation: 'login',
|
|
106
|
+
}),
|
|
107
|
+
)
|
|
108
|
+
} finally {
|
|
109
|
+
closeServer()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await new Promise((resolve, reject) => {
|
|
114
|
+
server.once('error', reject)
|
|
115
|
+
server.listen({
|
|
116
|
+
host: requested.hostname,
|
|
117
|
+
port: requested.port,
|
|
118
|
+
exclusive: true,
|
|
119
|
+
}, resolve)
|
|
120
|
+
}).catch((error) => {
|
|
121
|
+
closeServer()
|
|
122
|
+
throw new ConnectorError('CALLBACK_MISMATCH', 'Unable to open the local OAuth callback listener', {
|
|
123
|
+
operation: 'login',
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const address = server.address()
|
|
128
|
+
if (!address || typeof address === 'string') {
|
|
129
|
+
closeServer()
|
|
130
|
+
throw new ConnectorError('CALLBACK_MISMATCH', 'Local OAuth callback listener returned no address', {
|
|
131
|
+
operation: 'login',
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
actualRedirect = new URL(requested.href)
|
|
135
|
+
actualRedirect.port = String(address.port)
|
|
136
|
+
|
|
137
|
+
timer = setTimeout(() => {
|
|
138
|
+
if (settled) return
|
|
139
|
+
settled = true
|
|
140
|
+
rejectResult(new ConnectorError('CALLBACK_MISMATCH', 'OAuth callback timed out', { operation: 'login' }))
|
|
141
|
+
closeServer()
|
|
142
|
+
}, timeout)
|
|
143
|
+
timer.unref?.()
|
|
144
|
+
|
|
145
|
+
return Object.freeze({
|
|
146
|
+
redirectUri: actualRedirect.href,
|
|
147
|
+
waitForCallback: () => result,
|
|
148
|
+
close() {
|
|
149
|
+
if (settled) return
|
|
150
|
+
settled = true
|
|
151
|
+
rejectResult(new ConnectorError('CALLBACK_MISMATCH', 'OAuth callback listener closed', { operation: 'login' }))
|
|
152
|
+
closeServer()
|
|
153
|
+
},
|
|
154
|
+
})
|
|
155
|
+
}
|
package/connector/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-scnet",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.11",
|
|
4
4
|
"description": "Community DeepSeek Harness bundle for SCNet SSH and OpenAPI jobs, files, Notebook resources, Slurm diagnostics, and Hygon DCU/DTK guidance.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|
package/skills/scnet-hpc/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.6.
|
|
1
|
+
0.6.11
|
|
@@ -53,10 +53,12 @@ refresh_live_node_count() {
|
|
|
53
53
|
[ -n "${CLUSTER_ID:-}" ] || return 0
|
|
54
54
|
[ -n "${PARTITION:-}" ] || return 0
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
# Live metadata is optional; a failed SSH probe must not trigger callers'
|
|
57
|
+
# errexit/pipefail settings or discard the existing profile/cache value.
|
|
58
|
+
local live=""
|
|
57
59
|
live=$(ssh -o BatchMode=yes -o ConnectTimeout=10 -o ServerAliveInterval=15 -o ServerAliveCountMax=2 \
|
|
58
60
|
"$CLUSTER_ID" "scontrol show partition ${PARTITION}" 2>/dev/null \
|
|
59
|
-
| tr ' ' '\n' | sed -n 's/^TotalNodes=//p' | head -1)
|
|
61
|
+
| tr ' ' '\n' | sed -n 's/^TotalNodes=//p' | head -1) || live=""
|
|
60
62
|
|
|
61
63
|
if [[ "$live" =~ ^[0-9]+$ ]] && [ "$live" -gt 0 ]; then
|
|
62
64
|
NODE_COUNT="$live"
|
|
@@ -706,6 +706,13 @@ def _print_terminal_help() -> None:
|
|
|
706
706
|
"Linux:使用 ↑/↓ 和 Enter,也可输入编号;"
|
|
707
707
|
"Ctrl+C 可取消。敏感输入不会回显。"
|
|
708
708
|
)
|
|
709
|
+
elif system == "Windows":
|
|
710
|
+
print("Windows:输入编号并按 Enter 选择;Ctrl+C 可取消。敏感输入不会回显。")
|
|
711
|
+
print("完整脚本环境建议使用 WSL2;Git Bash 尚未作为完整环境验证。")
|
|
712
|
+
print(
|
|
713
|
+
"当前实现未接入 Windows 凭据管理器;AK/SK 请通过环境变量注入:"
|
|
714
|
+
"SCNET_OPENAPI_USER / SCNET_OPENAPI_ACCESS_KEY / SCNET_OPENAPI_SECRET_KEY。"
|
|
715
|
+
)
|
|
709
716
|
print()
|
|
710
717
|
|
|
711
718
|
|
|
@@ -1411,12 +1418,19 @@ def doctor_report(
|
|
|
1411
1418
|
try:
|
|
1412
1419
|
path = config_path()
|
|
1413
1420
|
if path.is_file():
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1421
|
+
if os.name == "posix":
|
|
1422
|
+
mode = path.stat().st_mode & 0o777
|
|
1423
|
+
add(
|
|
1424
|
+
"user_config",
|
|
1425
|
+
mode & 0o077 == 0,
|
|
1426
|
+
f"{path} mode={mode:03o}",
|
|
1427
|
+
)
|
|
1428
|
+
else:
|
|
1429
|
+
add(
|
|
1430
|
+
"user_config",
|
|
1431
|
+
True,
|
|
1432
|
+
f"{path}(非 POSIX,跳过 mode 检查;未验证 ACL)",
|
|
1433
|
+
)
|
|
1420
1434
|
else:
|
|
1421
1435
|
add("user_config", False, f"{path} 不存在;先运行 setup")
|
|
1422
1436
|
except OSError as exc:
|