dsh-crate-web 0.1.2-preview.5 → 0.1.2-preview.6
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 -3
- package/core/dsh_pack/_version.py +1 -1
- package/package.json +6 -2
- package/packaging/windows-terminal/README.md +43 -0
- package/packaging/windows-terminal/build.py +258 -0
- package/packaging/windows-terminal/dsh__node_modules__@deepseek-ai__dsh-sandbox-local__lib__index.js.patch +21 -0
- package/packaging/windows-terminal/dsh__node_modules__@deepseek-ai__dsh-subprocess-local__lib__index.js.patch +51 -0
- package/packaging/windows-terminal/dsh__node_modules__@deepseek-ai__dsh-terminal-bash__lib__index.js.patch +38 -0
- package/packaging/windows-terminal/dsh__node_modules__node-pty__lib__windowsPtyAgent.js.patch +31 -0
- package/packaging/windows-terminal/dsh__node_modules__node-pty__lib__windowsTerminal.js.patch +107 -0
- package/packaging/windows-terminal/manifest.json +577 -0
- package/skills/dsh-crate-troubleshooting/compatibility.json +1 -1
- package/skills/dsh-crate-troubleshooting/reference/diagnostics.generated.md +1 -1
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# DSH Crate — Preview
|
|
2
2
|
|
|
3
|
-
DSH Crate 为 DeepSeek Harness 的设置页提供环境包导出、检查、恢复、验证和历史记录。正式 npm 包名是 `dsh-crate-web`;本候选为 `0.1.2-preview.
|
|
3
|
+
DSH Crate 为 DeepSeek Harness 的设置页提供环境包导出、检查、恢复、验证和历史记录。正式 npm 包名是 `dsh-crate-web`;本候选为 `0.1.2-preview.6`,使用 `preview` 标签,保留既有 `latest`。
|
|
4
4
|
|
|
5
5
|
可通过官方插件入口安装明确版本:
|
|
6
6
|
|
|
7
7
|
```powershell
|
|
8
|
-
dsh plugin --profile web add dsh-crate-web@0.1.2-preview.
|
|
8
|
+
dsh plugin --profile web add dsh-crate-web@0.1.2-preview.6
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
重启后打开 Settings → DSH Crate。离线环境可从同版本 tgz 安装。Windows x64 随包保留 Python 3.14.6,无需另外安装系统 Python。其他系统未完成本轮桌面验收。
|
|
@@ -14,7 +14,16 @@ Import 默认恢复到新 Profile,先执行 Preflight 和计划检查。覆盖
|
|
|
14
14
|
|
|
15
15
|
默认 Verify 使用独立验证桌面并隔离 Home、工作区与缓存,不调用模型。模型、原生工具和各插件的真实业务验收必须有单独的执行证据;报告中的 UNTESTED 不能视为 PASS。
|
|
16
16
|
|
|
17
|
-
此 npm
|
|
17
|
+
此 npm 包包含 Crate 插件、Core、必要运行文件,以及供用户显式调用的 Windows 终端独立副本构建入口,不包含完整 DSH 桌面。安装插件不会向官方安装目录写入 Windows 终端或 NARB 补丁。历史业务证据来自配套的 Windows x64 / DSH 0.2.1-rc.5 独立修补桌面;尚未完成当前完整组合的模型及插件业务复测,不能宣称任意官方桌面均已兼容。
|
|
18
|
+
|
|
19
|
+
`0.1.2-preview.6` 新增随包的 [Windows 终端宿主构建入口](./packaging/windows-terminal/README.md)。它针对 manifest 锁定的官方输入,使用随包控制台 Node 修复 runner 空 stdin/退出 127,并补齐退出码和终端生命周期;匹配的独立副本已通过恢复后、重启后、移除 Crate 后的真实终端及正常清理验证。这些宿主证据来自升版前同字节补丁,不等于新 npm 版本完成了模型业务验收。必须由用户显式运行脚本,目标为不存在的新目录;原安装保持不变:
|
|
20
|
+
|
|
21
|
+
```powershell
|
|
22
|
+
# <包目录> 是已安装 dsh-crate-web 的目录;Windows x64 可使用包内 Python。
|
|
23
|
+
& "<包目录>\runtime\python\python.exe" "<包目录>\packaging\windows-terminal\build.py" --source "<匹配的官方桌面目录>" --destination "<不存在的新副本目录>"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
输入 EXE、ASAR 和控制台 Node 哈希不匹配时会停止;构建不关闭完整性检查,但新副本 EXE 的原代码签名不再有效。该入口不包含 Inspector/NARB 补丁,也不会自动启动新副本或调用模型。
|
|
18
27
|
|
|
19
28
|
已知限制:Windows 上 Import 偶发在临时 Profile 改为正式目录名时返回 WinError 5。界面会提示文件可能仍被占用或权限不足,并保留完整原始错误与回滚结果。请关闭正在使用目标目录的文件预览、编辑器或终端,核实权限与残留状态后手动重新导入;程序不会自动重试。历史故障的实际占用者尚未定位,当前提示不代表根因已修复。
|
|
20
29
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.1.2-preview.
|
|
1
|
+
__version__ = "0.1.2-preview.6"
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-crate-web",
|
|
3
3
|
"description": "DSH Web Settings surface for DSH Crate export, import, inspect, and history",
|
|
4
|
-
"version": "0.1.2-preview.
|
|
4
|
+
"version": "0.1.2-preview.6",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/index.d.ts",
|
|
@@ -30,7 +30,11 @@
|
|
|
30
30
|
"skills/dsh-crate-troubleshooting/compatibility.json",
|
|
31
31
|
"skills/dsh-crate-troubleshooting/reference/*.md",
|
|
32
32
|
"THIRD-PARTY-LICENSE.txt",
|
|
33
|
-
"runtime-inventory.mjs"
|
|
33
|
+
"runtime-inventory.mjs",
|
|
34
|
+
"packaging/windows-terminal/build.py",
|
|
35
|
+
"packaging/windows-terminal/manifest.json",
|
|
36
|
+
"packaging/windows-terminal/README.md",
|
|
37
|
+
"packaging/windows-terminal/*.patch"
|
|
34
38
|
],
|
|
35
39
|
"publishConfig": {
|
|
36
40
|
"access": "public",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Windows 终端宿主修复
|
|
2
|
+
|
|
3
|
+
该目录保存 DSH 0.2.1-rc.5 / Windows x64 的精确输入补丁和独立副本构建入口。它属于桌面宿主修复,**不随 npm 插件静默应用**,不修改用户原安装、Home、PowerShell Profile 或注册表。
|
|
4
|
+
|
|
5
|
+
已确认的原始故障是:打包 GUI EXE 作为 ConPTY 的 ACL runner 宿主时,runner 内 `GetStdHandle(-10)` 返回 NULL,受限 PowerShell 创建前即退出 127。管道关闭通知先于原生退出回调,旧 node-pty 又把退出码传播成 undefined,掩盖了原始错误。
|
|
6
|
+
|
|
7
|
+
补丁让独立桌面使用其随附的控制台 Node 运行原字节 ACL runner,保留原 sandbox 策略及显式 runner 覆盖优先级;同时修复退出事件、有效 PID 就绪、连接取消及 Windows PTY 收尾,并保留已采集的启动错误详情。不删除 required 插件,不放宽 ACL。
|
|
8
|
+
|
|
9
|
+
## 构建新副本
|
|
10
|
+
|
|
11
|
+
需要 Python 3.12 或更新版本,仅使用标准库。提供合法取得、与 manifest 中 EXE / ASAR / 控制台 Node 哈希完全一致的桌面目录;版本名相同不足以匹配。此仓库不分发官方桌面二进制或完整官方模块。
|
|
12
|
+
|
|
13
|
+
```powershell
|
|
14
|
+
python packaging/windows-terminal/build.py --source "<官方桌面目录>" --destination "<不存在的新目录>"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
输入和输出不能嵌套,拒绝已有目标及重解析点。所有补丁与 ACL 依赖先核对哈希,再复制;不使用硬链接。EXE 仅更新与 ASAR 对应的完整性资源,不关闭完整性检查、不改 fuse;代码签名不会因此保持有效。失败时保留新副本现场,不自动删除或重试。
|
|
18
|
+
|
|
19
|
+
输出中的 `crate-terminal-fix-receipt.json` 记录输入 manifest 与输出身份。`BUILT_NOT_TESTED` 只表示构建完成,必须在独立 DSH_HOME、workspace、Electron userData、TEMP 和缓存中另做业务验收。
|
|
20
|
+
|
|
21
|
+
## 验证
|
|
22
|
+
|
|
23
|
+
```powershell
|
|
24
|
+
python -m unittest discover -s tests -p test_windows_terminal_build.py
|
|
25
|
+
|
|
26
|
+
# 可选:读取本地官方输入,应用实际补丁并执行真实模块的受控事件回归。
|
|
27
|
+
$env:DSH_CRATE_TEST_DESKTOP = "<官方桌面目录>"
|
|
28
|
+
python -m unittest discover -s tests -p test_windows_terminal_build.py
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
受控回归不启动桌面、Worker 或模型;没有指定本地桌面时,实际模块用例标为 skipped,不能据此声称真实终端通过。真实业务验收还需覆盖:打开终端、命令输出及文件标记、关闭、自然退出码、计划重启和移除 Crate 后继续可用,以及进程/Job/端点释放。
|
|
32
|
+
|
|
33
|
+
### 隔离目录的权限前提
|
|
34
|
+
|
|
35
|
+
验收夹具 `tests/windows_terminal_directory.py` 只创建明确 scope 内的新目录,并给原所有者显式保留 Modify + WRITE_OWNER,其他主体、owner、继承和官方沙箱策略不变。必须先准备整棵隔离运行目录的全新根目录,再在其中创建 Home、workspace、TEMP 和缓存,使子目录继承明确的所有者权限。只处理 workspace / TEMP 叶目录,仍可能因祖先目录不可访问而得到错误的工作目录。不要把它用于已有用户目录。仅继承 `OWNER RIGHTS` 的私有临时目录,可能在官方受限令牌下无法读取或写入;只补 WRITE_OWNER 不足以证明可用。
|
|
36
|
+
|
|
37
|
+
已观察到的失败链路是:沙箱子进程不能写入指定 TEMP → PowerShell 进入受限语言模式 → 编码/提示符初始化无法工作 → 最后报告就绪超时。应检查实际 PowerShell 语言模式、工作目录、指定 TEMP 写入及目录外写入仍被拒绝,不能通过关闭语言限制、增加 Everyone 权限或提高超时来制造通过。
|
|
38
|
+
|
|
39
|
+
在 manifest 锁定的 DSH 0.2.1-rc.5 / Windows x64 / 随包 Node 24.21.0 独立副本中,已实际验证恢复后、重启后和移除 Crate 后的 `terminal_open → terminal_send → terminal_close`:命令输出和文件标记正确、工作目录正确、目录外写入被拒绝,桌面正常退出,受控 Job 活动进程归零,相关 TCP 连接释放。自然退出 0 / 127 的事件传播另有实际 ConPTY 回归。调用通过显式隔离测试插件执行真实注册工具,不经过外部模型;逐名称管道释放没有独立探测。
|
|
40
|
+
|
|
41
|
+
默认零模型 Verify 的 composition、boot、surface_ready、new_session 和 cleanup 已实际通过;其模型、核心工具及插件业务步骤仍保留 `UNTESTED`,不能据此宣称完整业务验收通过。移除后的 API 检查应核对官方回退层语义:该版本静态回退层对未匹配的 POST 返回 405,不能把“必须是 404”用作卸载成功条件。
|
|
42
|
+
|
|
43
|
+
此补丁不包含 Inspector / NARB 的历史候选修复,不代表完整桌面产品交付,也不宣称适用所有 rc.5 制品或后续版本。node-pty 的变更基于其 MIT 许可模块;构建保留原安装内许可证,其他官方模块使用者仍须遵守其适用许可。
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""对精确匹配的 Windows 桌面创建独立修复副本;只用 Python 标准库。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import copy
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path, PurePosixPath
|
|
9
|
+
import re
|
|
10
|
+
import shutil
|
|
11
|
+
import struct
|
|
12
|
+
|
|
13
|
+
ROOT = Path(__file__).resolve().parent
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def digest(data):
|
|
17
|
+
return hashlib.sha256(data).hexdigest()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def file_hash(path):
|
|
21
|
+
with Path(path).open('rb') as stream:
|
|
22
|
+
return hashlib.file_digest(stream, 'sha256').hexdigest()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def safe_name(name):
|
|
26
|
+
if not isinstance(name, str) or not name or '\\' in name:
|
|
27
|
+
raise ValueError('非法资源相对路径')
|
|
28
|
+
path = PurePosixPath(name)
|
|
29
|
+
if path.is_absolute() or ':' in name or any(p in ('', '.', '..') for p in name.split('/')):
|
|
30
|
+
raise ValueError('非法资源相对路径')
|
|
31
|
+
return name
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def leaves(header, prefix=''):
|
|
35
|
+
for key, node in header.get('files', {}).items():
|
|
36
|
+
name = prefix + key
|
|
37
|
+
safe_name(name)
|
|
38
|
+
if 'files' in node:
|
|
39
|
+
yield from leaves(node, name + '/')
|
|
40
|
+
else:
|
|
41
|
+
yield name, node
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Asar:
|
|
45
|
+
def __init__(self, path):
|
|
46
|
+
self.path = Path(path)
|
|
47
|
+
self.file = self.path.open('rb')
|
|
48
|
+
try:
|
|
49
|
+
a, n, c, size = struct.unpack('<4I', self.file.read(16))
|
|
50
|
+
if a != 4 or size > 64 * 1024 * 1024 or n < 8 + size or c < 4 + size:
|
|
51
|
+
raise ValueError('不支持的 ASAR 头')
|
|
52
|
+
raw = self.file.read(size)
|
|
53
|
+
if len(raw) != size:
|
|
54
|
+
raise ValueError('ASAR 头不完整')
|
|
55
|
+
self.header_hash = digest(raw)
|
|
56
|
+
self.header = json.loads(raw)
|
|
57
|
+
self.base = 8 + n
|
|
58
|
+
except BaseException:
|
|
59
|
+
self.close()
|
|
60
|
+
raise
|
|
61
|
+
|
|
62
|
+
def entry(self, name):
|
|
63
|
+
node = self.header
|
|
64
|
+
for part in safe_name(name).split('/'):
|
|
65
|
+
node = node['files'][part]
|
|
66
|
+
return node
|
|
67
|
+
|
|
68
|
+
def read(self, name, seen=()):
|
|
69
|
+
if name in seen or len(seen) > 64:
|
|
70
|
+
raise ValueError('ASAR 链接循环')
|
|
71
|
+
entry = self.entry(name)
|
|
72
|
+
if 'link' in entry:
|
|
73
|
+
return self.read(entry['link'], (*seen, name))
|
|
74
|
+
if entry.get('unpacked'):
|
|
75
|
+
return Path(str(self.path) + '.unpacked').joinpath(*name.split('/')).read_bytes()
|
|
76
|
+
self.file.seek(self.base + int(entry['offset']))
|
|
77
|
+
data = self.file.read(entry['size'])
|
|
78
|
+
if len(data) != entry['size']:
|
|
79
|
+
raise ValueError('ASAR 内容不完整')
|
|
80
|
+
return data
|
|
81
|
+
|
|
82
|
+
def close(self):
|
|
83
|
+
self.file.close()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def apply_patch(source, patch):
|
|
87
|
+
"""精确应用统一 diff;行号、旧内容和 hunk 数量均核对,不模糊匹配。"""
|
|
88
|
+
original = source.splitlines(keepends=True)
|
|
89
|
+
result, position, index, hunks = [], 0, 0, 0
|
|
90
|
+
lines = patch.splitlines(keepends=True)
|
|
91
|
+
while index < len(lines):
|
|
92
|
+
line = lines[index]
|
|
93
|
+
if not line.startswith('@@ '):
|
|
94
|
+
index += 1
|
|
95
|
+
continue
|
|
96
|
+
match = re.fullmatch(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[^\r\n]*\n?', line)
|
|
97
|
+
if not match:
|
|
98
|
+
raise ValueError('非法补丁 hunk')
|
|
99
|
+
start = int(match[1]) - 1
|
|
100
|
+
if start < position or start > len(original):
|
|
101
|
+
raise ValueError('补丁 hunk 越界或重叠')
|
|
102
|
+
result.extend(original[position:start])
|
|
103
|
+
position, index, old_count, new_count = start, index + 1, 0, 0
|
|
104
|
+
hunks += 1
|
|
105
|
+
while index < len(lines) and not lines[index].startswith('@@ '):
|
|
106
|
+
line = lines[index]
|
|
107
|
+
index += 1
|
|
108
|
+
if not line or line[0] not in ' +-':
|
|
109
|
+
raise ValueError('非法补丁内容')
|
|
110
|
+
kind, text = line[0], line[1:]
|
|
111
|
+
if kind in ' -':
|
|
112
|
+
if position >= len(original) or original[position] != text:
|
|
113
|
+
raise ValueError('补丁上下文不匹配')
|
|
114
|
+
position += 1
|
|
115
|
+
old_count += 1
|
|
116
|
+
if kind in ' +':
|
|
117
|
+
result.append(text)
|
|
118
|
+
new_count += 1
|
|
119
|
+
if old_count != int(match[2] or 1) or new_count != int(match[4] or 1):
|
|
120
|
+
raise ValueError('补丁行数不匹配')
|
|
121
|
+
if not hunks:
|
|
122
|
+
raise ValueError('空补丁')
|
|
123
|
+
result.extend(original[position:])
|
|
124
|
+
return ''.join(result)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def patched_modules(reader, manifest, asset_root=ROOT):
|
|
128
|
+
changes = {}
|
|
129
|
+
for row in manifest['patches']:
|
|
130
|
+
name = safe_name(row['path'])
|
|
131
|
+
if name in changes or reader.entry(name).get('unpacked') or 'link' in reader.entry(name):
|
|
132
|
+
raise ValueError('补丁目标重复或不是 packed 普通文件')
|
|
133
|
+
original = reader.read(name)
|
|
134
|
+
patch = (asset_root / safe_name(row['patch'])).read_bytes()
|
|
135
|
+
if digest(original) != row['beforeSha256'] or digest(patch) != row['patchSha256']:
|
|
136
|
+
raise ValueError('模块或补丁身份不匹配: ' + name)
|
|
137
|
+
normalized = original.decode('utf8').replace('\r\n', '\n')
|
|
138
|
+
text = apply_patch(normalized if normalized.endswith('\n') else normalized + '\n', patch.decode('utf8'))
|
|
139
|
+
if not row['outputFinalNewline']:
|
|
140
|
+
text = text.removesuffix('\n')
|
|
141
|
+
if row['outputNewline'] == 'CRLF':
|
|
142
|
+
text = text.replace('\n', '\r\n')
|
|
143
|
+
elif row['outputNewline'] != 'LF':
|
|
144
|
+
raise ValueError('未知输出换行格式')
|
|
145
|
+
raw = text.encode('utf8')
|
|
146
|
+
if digest(raw) != row['afterSha256']:
|
|
147
|
+
raise ValueError('补丁输出身份不匹配: ' + name)
|
|
148
|
+
changes[name] = raw
|
|
149
|
+
return changes
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def rewrite_asar(reader, destination, changes):
|
|
153
|
+
header = copy.deepcopy(reader.header)
|
|
154
|
+
packed = []
|
|
155
|
+
for name, node in leaves(header):
|
|
156
|
+
if name in changes:
|
|
157
|
+
data = changes[name]
|
|
158
|
+
node['size'] = len(data)
|
|
159
|
+
if 'integrity' in node:
|
|
160
|
+
integrity = node['integrity']
|
|
161
|
+
block = integrity['blockSize']
|
|
162
|
+
if block <= 0:
|
|
163
|
+
raise ValueError('非法 ASAR blockSize')
|
|
164
|
+
integrity['hash'] = digest(data)
|
|
165
|
+
integrity['blocks'] = [digest(data[i:i + block]) for i in range(0, len(data), block)]
|
|
166
|
+
if 'offset' in node:
|
|
167
|
+
packed.append((int(reader.entry(name)['offset']), name, node))
|
|
168
|
+
packed.sort()
|
|
169
|
+
offset = 0
|
|
170
|
+
for _, name, node in packed:
|
|
171
|
+
node['offset'] = str(offset)
|
|
172
|
+
offset += node['size']
|
|
173
|
+
raw = json.dumps(header, ensure_ascii=False, separators=(',', ':')).encode()
|
|
174
|
+
padded = (len(raw) + 3) & ~3
|
|
175
|
+
with Path(destination).open('wb') as output:
|
|
176
|
+
output.write(struct.pack('<4I', 4, 8 + padded, 4 + padded, len(raw)) + raw + b'\0' * (padded - len(raw)))
|
|
177
|
+
for _, name, node in packed:
|
|
178
|
+
output.write(changes[name] if name in changes else reader.read(name))
|
|
179
|
+
return digest(raw)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def patch_integrity(executable, spec, new_hash):
|
|
183
|
+
"""精确输入已锁定,唯一更新已核对的 ELECTRONASAR 完整性资源。"""
|
|
184
|
+
raw = executable.read_bytes()
|
|
185
|
+
offset, size = spec['resourceOffset'], spec['resourceBytes']
|
|
186
|
+
old = spec['oldHeaderSha256'].encode()
|
|
187
|
+
section = raw[offset:offset + size]
|
|
188
|
+
if len(section) != size or section.count(old) != 1 or len(new_hash) != len(old):
|
|
189
|
+
raise ValueError('EXE/ASAR 完整性资源不匹配')
|
|
190
|
+
executable.write_bytes(raw[:offset] + section.replace(old, new_hash.encode()) + raw[offset + size:])
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def plain_tree(path):
|
|
194
|
+
for item in [path, *path.rglob('*')]:
|
|
195
|
+
if item.is_symlink() or item.is_junction():
|
|
196
|
+
raise ValueError('目录含重解析点')
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def materialize(source, destination):
|
|
200
|
+
source_arg, destination_arg = Path(source).absolute(), Path(destination).absolute()
|
|
201
|
+
for path in (source_arg, destination_arg):
|
|
202
|
+
for ancestor in (path, *path.parents):
|
|
203
|
+
if ancestor.is_symlink() or ancestor.is_junction():
|
|
204
|
+
raise ValueError('输入或输出路径含重解析点')
|
|
205
|
+
source, destination = source_arg.resolve(), destination_arg.resolve()
|
|
206
|
+
if destination.exists() or destination.is_relative_to(source) or source.is_relative_to(destination):
|
|
207
|
+
raise ValueError('目标已存在或与输入嵌套,拒绝覆盖')
|
|
208
|
+
manifest = json.loads((ROOT / 'manifest.json').read_text('utf8'))
|
|
209
|
+
if manifest['schemaVersion'] != 1:
|
|
210
|
+
raise ValueError('不支持的 manifest 版本')
|
|
211
|
+
if file_hash(source / 'DeepSeek Harness.exe') != manifest['inputExeSha256'] or file_hash(source / 'resources/app.asar') != manifest['inputAsarSha256']:
|
|
212
|
+
raise ValueError('官方输入身份不匹配;同一版本名不能代替文件身份')
|
|
213
|
+
if file_hash(source / safe_name(manifest['consoleNode']['path'])) != manifest['consoleNode']['sha256']:
|
|
214
|
+
raise ValueError('随包控制台 Node 身份不匹配')
|
|
215
|
+
plain_tree(source)
|
|
216
|
+
reader = Asar(source / 'resources/app.asar')
|
|
217
|
+
try:
|
|
218
|
+
if reader.header_hash != manifest['exeIntegrity']['oldHeaderSha256']:
|
|
219
|
+
raise ValueError('原 ASAR header 身份不匹配')
|
|
220
|
+
changes = patched_modules(reader, manifest)
|
|
221
|
+
resources = {}
|
|
222
|
+
for name, expected in manifest['aclFiles'].items():
|
|
223
|
+
safe_name(name)
|
|
224
|
+
raw = reader.read('dsh/' + name)
|
|
225
|
+
if digest(raw) != expected:
|
|
226
|
+
raise ValueError('官方 ACL 依赖身份不匹配')
|
|
227
|
+
resources[name] = raw
|
|
228
|
+
# 所有内容先核对;新副本不使用硬链接,不修改原安装或用户 Home。
|
|
229
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
230
|
+
shutil.copytree(source, destination, symlinks=False)
|
|
231
|
+
header_hash = rewrite_asar(reader, destination / 'resources/app.asar', changes)
|
|
232
|
+
patch_integrity(destination / 'DeepSeek Harness.exe', manifest['exeIntegrity'], header_hash)
|
|
233
|
+
resource_root = destination / 'resources/runtime' / safe_name(manifest['aclResourceDirectory'])
|
|
234
|
+
resource_root.mkdir(exist_ok=False)
|
|
235
|
+
for name, raw in resources.items():
|
|
236
|
+
target = resource_root / name
|
|
237
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
238
|
+
with target.open('xb') as stream:
|
|
239
|
+
stream.write(raw)
|
|
240
|
+
receipt = {'status': 'BUILT_NOT_TESTED', 'manifestSha256': file_hash(ROOT / 'manifest.json'),
|
|
241
|
+
'changedModules': list(changes), 'testHooksIncluded': False,
|
|
242
|
+
'outputExeSha256': file_hash(destination / 'DeepSeek Harness.exe'),
|
|
243
|
+
'outputAsarSha256': file_hash(destination / 'resources/app.asar'),
|
|
244
|
+
'integrityDisabled': False, 'originalInstallationChanged': False,
|
|
245
|
+
'modelsRequested': 0, 'desktopStarted': False}
|
|
246
|
+
with (destination / 'crate-terminal-fix-receipt.json').open('x', encoding='utf8') as stream:
|
|
247
|
+
json.dump(receipt, stream, ensure_ascii=False, indent=2)
|
|
248
|
+
return receipt
|
|
249
|
+
finally:
|
|
250
|
+
reader.close()
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
if __name__ == '__main__':
|
|
254
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
255
|
+
parser.add_argument('--source', required=True, type=Path)
|
|
256
|
+
parser.add_argument('--destination', required=True, type=Path)
|
|
257
|
+
args = parser.parse_args()
|
|
258
|
+
print(json.dumps(materialize(args.source, args.destination), ensure_ascii=False))
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
--- a/dsh/node_modules/@deepseek-ai/dsh-sandbox-local/lib/index.js
|
|
2
|
+
+++ b/dsh/node_modules/@deepseek-ai/dsh-sandbox-local/lib/index.js
|
|
3
|
+
@@ -540,7 +540,17 @@
|
|
4
|
+
const override = this.internals.windowsAclRunnerArgs;
|
|
5
|
+
if (override !== void 0) return override;
|
|
6
|
+
const builtEntry = this.internals.windowsAclRunnerEntry ?? fileURLToPath(import.meta.resolve("@deepseek-ai/dsh-sandbox-windows-acl/runner"));
|
|
7
|
+
- if (existsSync(builtEntry)) return [process.execPath, builtEntry];
|
|
8
|
+
+ if (existsSync(builtEntry)) {
|
|
9
|
+
+ // 打包 Windows 桌面的 GUI EXE 不作为继承控制台句柄的 runner 宿主。
|
|
10
|
+
+ // 使用随附控制台 Node 和原字节官方 ACL runner,保持原沙箱协议。
|
|
11
|
+
+ if (process.platform === "win32" && process.versions.electron && import.meta.url.includes("/app.asar/")) {
|
|
12
|
+
+ const node = fileURLToPath(new URL("../../../../../../runtime/primary-runtime/dependencies/node/bin/node.exe", import.meta.url));
|
|
13
|
+
+ const runner = fileURLToPath(new URL("../../../../../../runtime/windows-acl-runner-v127/node_modules/@deepseek-ai/dsh-sandbox-windows-acl/lib/runner.js", import.meta.url));
|
|
14
|
+
+ if (!existsSync(node) || !existsSync(runner)) throw new Error("sandbox-local: Windows ACL console host resources are missing");
|
|
15
|
+
+ return [node, runner];
|
|
16
|
+
+ }
|
|
17
|
+
+ return [process.execPath, builtEntry];
|
|
18
|
+
+ }
|
|
19
|
+
const sourceEntry = fileURLToPath(import.meta.resolve("@deepseek-ai/dsh-sandbox-windows-acl/src/runner.ts"));
|
|
20
|
+
const sourceConfig = fileURLToPath(new URL("../../../../tsconfig.base.json", import.meta.url));
|
|
21
|
+
const registration = `import { register } from ${JSON.stringify(import.meta.resolve("tsx/esm/api"))}; register({ tsconfig: ${JSON.stringify(sourceConfig)} });`;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
--- a/dsh/node_modules/@deepseek-ai/dsh-subprocess-local/lib/index.js
|
|
2
|
+
+++ b/dsh/node_modules/@deepseek-ai/dsh-subprocess-local/lib/index.js
|
|
3
|
+
@@ -1226,6 +1226,8 @@
|
|
4
|
+
survivors = await this.stopDescendants();
|
|
5
|
+
if (survivors.length > 0) throw new Error(`terminal cleanup failed; surviving pids: ${survivors.map((member) => member.pid).join(", ")}`);
|
|
6
|
+
this.settleExitIfGone();
|
|
7
|
+
+ // shell及后代都已退出后释放 Windows PTY 传输端点。
|
|
8
|
+
+ if (this.platform === "win32") this.terminal.kill();
|
|
9
|
+
this.dataDisposable.dispose();
|
|
10
|
+
this.exitDisposable.dispose();
|
|
11
|
+
}
|
|
12
|
+
@@ -1643,6 +1645,7 @@
|
|
13
|
+
const linuxScope = containmentMode === "linux-scope" ? this.linuxScopeDecision : void 0;
|
|
14
|
+
let scope;
|
|
15
|
+
let terminal;
|
|
16
|
+
+ let cancelConnectingTerminal;
|
|
17
|
+
try {
|
|
18
|
+
scope = linuxScope === void 0 ? void 0 : prepareLinuxTerminalScope(launch, {
|
|
19
|
+
...activity?.env ?? env,
|
|
20
|
+
@@ -1654,10 +1657,31 @@
|
|
21
|
+
options.env = scope.env;
|
|
22
|
+
}
|
|
23
|
+
terminal = requireNodePty().spawn(scope?.command ?? file, scope?.args ?? [...launch.argv.slice(1)], options);
|
|
24
|
+
+ if (platform === "win32") {
|
|
25
|
+
+ if (terminal.processReady === void 0) throw new Error("subprocess-local: Windows PTY process readiness contract is missing");
|
|
26
|
+
+ if (spec.signal !== void 0) {
|
|
27
|
+
+ cancelConnectingTerminal = () => {
|
|
28
|
+
+ // 取消事件回调不抛出到 Host;kill 缓存失败,随后 catch 会保留清理错误。
|
|
29
|
+
+ try { terminal.kill(); } catch (_) { /* 由连接错误收尾路径读取并报告 */ }
|
|
30
|
+
+ };
|
|
31
|
+
+ spec.signal.addEventListener("abort", cancelConnectingTerminal, { once: true });
|
|
32
|
+
+ if (spec.signal.aborted) cancelConnectingTerminal();
|
|
33
|
+
+ }
|
|
34
|
+
+ await terminal.processReady;
|
|
35
|
+
+ spec.signal?.throwIfAborted();
|
|
36
|
+
+ if (!Number.isSafeInteger(terminal.pid) || terminal.pid <= 0) throw new Error("subprocess-local: Windows PTY returned an invalid child pid");
|
|
37
|
+
+ }
|
|
38
|
+
} catch (error) {
|
|
39
|
+
+ let cleanupError;
|
|
40
|
+
+ if (platform === "win32" && terminal !== void 0) {
|
|
41
|
+
+ try { terminal.kill(); } catch (failure) { cleanupError = failure; }
|
|
42
|
+
+ }
|
|
43
|
+
scope?.cleanup();
|
|
44
|
+
activity?.dispose();
|
|
45
|
+
+ if (cleanupError !== void 0) throw new AggregateError([error, cleanupError], "PTY connection and cleanup both failed");
|
|
46
|
+
throw error;
|
|
47
|
+
+ } finally {
|
|
48
|
+
+ if (cancelConnectingTerminal !== void 0) spec.signal?.removeEventListener("abort", cancelConnectingTerminal);
|
|
49
|
+
}
|
|
50
|
+
let handle;
|
|
51
|
+
const directSettlement = Promise.withResolvers();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
--- a/dsh/node_modules/@deepseek-ai/dsh-terminal-bash/lib/index.js
|
|
2
|
+
+++ b/dsh/node_modules/@deepseek-ai/dsh-terminal-bash/lib/index.js
|
|
3
|
+
@@ -498,7 +498,7 @@
|
|
4
|
+
submit: false,
|
|
5
|
+
...signal !== void 0 ? { signal } : {}
|
|
6
|
+
}).done;
|
|
7
|
+
- if (result.waitReason === "session_exit") throw new Error("PTY shell exited during startup");
|
|
8
|
+
+ if (result.waitReason === "session_exit") throw startupExitError(result);
|
|
9
|
+
if (result.waitReason === "timeout") throw new Error("PTY shell did not reach readiness before startup timeout");
|
|
10
|
+
this.motd = result.viewport;
|
|
11
|
+
} catch (error) {
|
|
12
|
+
@@ -971,6 +971,17 @@
|
|
13
|
+
mode: policy.mode
|
|
14
|
+
}, signal)).argv;
|
|
15
|
+
}
|
|
16
|
+
+/** 保留实际退出结果和已收集的启动输出;不将缺失退出码伪装为0。 */
|
|
17
|
+
+function startupExitError(result) {
|
|
18
|
+
+ const status = result.sessionStatus;
|
|
19
|
+
+ const exitCode = status.kind === "exited" && typeof status.exitCode === "number" ? status.exitCode : null;
|
|
20
|
+
+ const signal = status.kind === "exited" ? status.signal ?? null : null;
|
|
21
|
+
+ const output = result.viewport;
|
|
22
|
+
+ const truncated = result.truncated === true;
|
|
23
|
+
+ const error = new Error(`PTY shell exited during startup (exitCode=${exitCode ?? "unknown"}, signal=${signal ?? "none"})${output.length ? `\n${output}` : ""}${truncated ? "\n[startup output reached the configured capture limit]" : ""}`);
|
|
24
|
+
+ Object.assign(error, { exitCode, signal, startupOutput: output, startupOutputTruncated: truncated });
|
|
25
|
+
+ return error;
|
|
26
|
+
+}
|
|
27
|
+
async function startupSession(session, dialect, timeoutMs, signal) {
|
|
28
|
+
let startupOperation;
|
|
29
|
+
const start = async () => {
|
|
30
|
+
@@ -987,7 +998,7 @@
|
|
31
|
+
...signal !== void 0 ? { signal } : {}
|
|
32
|
+
});
|
|
33
|
+
const result = await startupOperation.done;
|
|
34
|
+
- if (result.waitReason === "session_exit") throw new Error("PTY shell exited during startup");
|
|
35
|
+
+ if (result.waitReason === "session_exit") throw startupExitError(result);
|
|
36
|
+
if (result.waitReason === "timeout") throw new Error("PTY shell did not reach readiness before startup timeout");
|
|
37
|
+
viewport = result.viewport;
|
|
38
|
+
if (result.waitReason === "stdin_read") break;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
--- a/dsh/node_modules/node-pty/lib/windowsPtyAgent.js
|
|
2
|
+
+++ b/dsh/node_modules/node-pty/lib/windowsPtyAgent.js
|
|
3
|
+
@@ -38,6 +38,8 @@
|
|
4
|
+
this._useConptyDll = _useConptyDll;
|
|
5
|
+
this._innerPid = 0;
|
|
6
|
+
this._onError = new eventEmitter2_1.EventEmitter2();
|
|
7
|
+
+ // 原生进程退出与输出管道关闭分别通知,避免将管道 EOF 当作完整退出结果。
|
|
8
|
+
+ this._onProcessExit = new eventEmitter2_1.EventEmitter2();
|
|
9
|
+
if (!conptyNative) {
|
|
10
|
+
conptyNative = (0, utils_1.loadNativeModule)('conpty').module;
|
|
11
|
+
}
|
|
12
|
+
@@ -91,6 +93,11 @@
|
|
13
|
+
});
|
|
14
|
+
this._inSocket.setEncoding('utf8');
|
|
15
|
+
}
|
|
16
|
+
+ Object.defineProperty(WindowsPtyAgent.prototype, "onProcessExit", {
|
|
17
|
+
+ get: function () { return this._onProcessExit.event; },
|
|
18
|
+
+ enumerable: false,
|
|
19
|
+
+ configurable: true
|
|
20
|
+
+ });
|
|
21
|
+
Object.defineProperty(WindowsPtyAgent.prototype, "onError", {
|
|
22
|
+
get: function () { return this._onError.event; },
|
|
23
|
+
enumerable: false,
|
|
24
|
+
@@ -248,6 +255,7 @@
|
|
25
|
+
WindowsPtyAgent.prototype._$onProcessExit = function (exitCode) {
|
|
26
|
+
var _this = this;
|
|
27
|
+
this._exitCode = exitCode;
|
|
28
|
+
+ this._onProcessExit.fire(exitCode);
|
|
29
|
+
if (!this._useConptyDll) {
|
|
30
|
+
this._flushDataAndCleanUp();
|
|
31
|
+
this._outSocket.on('data', function () { return _this._flushDataAndCleanUp(); });
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
--- a/dsh/node_modules/node-pty/lib/windowsTerminal.js
|
|
2
|
+
+++ b/dsh/node_modules/node-pty/lib/windowsTerminal.js
|
|
3
|
+
@@ -49,14 +49,32 @@
|
|
4
|
+
_this._isReady = false;
|
|
5
|
+
// Functions that need to run after `ready` event is emitted.
|
|
6
|
+
_this._deferreds = [];
|
|
7
|
+
+ _this._outputClosed = false;
|
|
8
|
+
+ _this._exitPublished = false;
|
|
9
|
+
+ _this._killRequested = false;
|
|
10
|
+
+ _this._processReadySettled = false;
|
|
11
|
+
+ _this.processReady = new Promise(function (resolve, reject) {
|
|
12
|
+
+ _this._resolveProcessReady = resolve;
|
|
13
|
+
+ _this._rejectProcessReady = reject;
|
|
14
|
+
+ });
|
|
15
|
+
+ // 原始 node-pty 调用方可能不消费该附加 Promise;保留拒绝结果但防止未处理拒绝。
|
|
16
|
+
+ _this.processReady.catch(function () {});
|
|
17
|
+
// Create new termal.
|
|
18
|
+
_this._agent = new windowsPtyAgent_1.WindowsPtyAgent(file, args, parsedEnv, cwd, _this._cols, _this._rows, false, opt.useConptyDll, opt.conptyInheritCursor);
|
|
19
|
+
_this._socket = _this._agent.outSocket;
|
|
20
|
+
// The socket 'close' handler that drives 'exit' is only attached after
|
|
21
|
+
// ready_datapipe, which never fires when connect() fails.
|
|
22
|
+
- _this._agent.onError(function () {
|
|
23
|
+
+ _this._agent.onError(function (error) {
|
|
24
|
+
+ if (!_this._processReadySettled) {
|
|
25
|
+
+ _this._processReadySettled = true;
|
|
26
|
+
+ _this._rejectProcessReady(error);
|
|
27
|
+
+ }
|
|
28
|
+
_this._close();
|
|
29
|
+
- _this.emit('exit', _this._agent.exitCode);
|
|
30
|
+
+ // 创建/连接失败可能没有原生子进程;保留失败码,禁止等待不会到来的回调。
|
|
31
|
+
+ _this._emitExitOnce(typeof _this._agent.exitCode === 'number' ? _this._agent.exitCode : -1);
|
|
32
|
+
+ });
|
|
33
|
+
+ _this._processExitDisposable = _this._agent.onProcessExit(function () {
|
|
34
|
+
+ _this._publishExitWhenComplete();
|
|
35
|
+
});
|
|
36
|
+
// Not available until `ready` event emitted.
|
|
37
|
+
_this._pid = _this._agent.innerPid;
|
|
38
|
+
@@ -67,6 +85,11 @@
|
|
39
|
+
_this._socket.on('ready_datapipe', function () {
|
|
40
|
+
// Update pid now that the agent has connected
|
|
41
|
+
_this._pid = _this._agent.innerPid;
|
|
42
|
+
+ if (!_this._processReadySettled) {
|
|
43
|
+
+ _this._processReadySettled = true;
|
|
44
|
+
+ if (_this._pid > 0) _this._resolveProcessReady(_this._pid);
|
|
45
|
+
+ else _this._rejectProcessReady(new Error('ConPTY connected without a valid child pid'));
|
|
46
|
+
+ }
|
|
47
|
+
// Run deferreds and set ready state once the first data event is received.
|
|
48
|
+
_this._socket.once('data', function () {
|
|
49
|
+
// Wait until the first data event is fired then we can run deferreds.
|
|
50
|
+
@@ -105,8 +128,9 @@
|
|
51
|
+
});
|
|
52
|
+
// Cleanup after the socket is closed.
|
|
53
|
+
_this._socket.on('close', function () {
|
|
54
|
+
- _this.emit('exit', _this._agent.exitCode);
|
|
55
|
+
+ _this._outputClosed = true;
|
|
56
|
+
_this._close();
|
|
57
|
+
+ _this._publishExitWhenComplete();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
_this._file = file;
|
|
61
|
+
@@ -116,6 +140,17 @@
|
|
62
|
+
_this._forwardEvents();
|
|
63
|
+
return _this;
|
|
64
|
+
}
|
|
65
|
+
+ WindowsTerminal.prototype._publishExitWhenComplete = function () {
|
|
66
|
+
+ // 两个顺序均允许:进程先退出,或输出管道先关闭;不设置猜测性延时或退出码。
|
|
67
|
+
+ if (!this._outputClosed || typeof this._agent.exitCode !== 'number') return;
|
|
68
|
+
+ this._emitExitOnce(this._agent.exitCode);
|
|
69
|
+
+ };
|
|
70
|
+
+ WindowsTerminal.prototype._emitExitOnce = function (exitCode) {
|
|
71
|
+
+ if (this._exitPublished) return;
|
|
72
|
+
+ this._exitPublished = true;
|
|
73
|
+
+ if (this._processExitDisposable) this._processExitDisposable.dispose();
|
|
74
|
+
+ this.emit('exit', exitCode);
|
|
75
|
+
+ };
|
|
76
|
+
WindowsTerminal.prototype._write = function (data) {
|
|
77
|
+
this._defer(this._doWrite, data);
|
|
78
|
+
};
|
|
79
|
+
@@ -155,14 +190,20 @@
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
WindowsTerminal.prototype.kill = function (signal) {
|
|
83
|
+
- var _this = this;
|
|
84
|
+
- this._deferNoArgs(function () {
|
|
85
|
+
- if (signal) {
|
|
86
|
+
- throw new Error('Signals not supported on windows.');
|
|
87
|
+
- }
|
|
88
|
+
- _this._close();
|
|
89
|
+
- _this._agent.kill();
|
|
90
|
+
- });
|
|
91
|
+
+ if (signal) throw new Error('Signals not supported on windows.');
|
|
92
|
+
+ if (this._killRequested) {
|
|
93
|
+
+ if (this._killError) throw this._killError;
|
|
94
|
+
+ return;
|
|
95
|
+
+ }
|
|
96
|
+
+ this._killRequested = true;
|
|
97
|
+
+ // 释放连接中的 PTY 不依赖首屏数据,避免连接失败或取消时留下延迟 kill。
|
|
98
|
+
+ if (!this._processReadySettled) {
|
|
99
|
+
+ this._processReadySettled = true;
|
|
100
|
+
+ this._rejectProcessReady(new Error('Terminal was closed before its process connected'));
|
|
101
|
+
+ }
|
|
102
|
+
+ this._close();
|
|
103
|
+
+ try { this._agent.kill(); }
|
|
104
|
+
+ catch (error) { this._killError = error; throw error; }
|
|
105
|
+
};
|
|
106
|
+
WindowsTerminal.prototype._deferNoArgs = function (deferredFn) {
|
|
107
|
+
var _this = this;
|