Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 29 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
> 面向 AI 编程协作者:开始任何工作前,请先阅读本目录的 `AI_AGENTS.md`,并遵循其中的同步/检查/上报流程。

<p align="center">
<img src="https://img.shields.io/badge/version-v1.0.0-6c5ce7" alt="version" />
<img src="https://img.shields.io/badge/version-v1.0.1-6c5ce7" alt="version" />
<img src="https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-2d3436" alt="platform" />
<img src="https://img.shields.io/badge/license-MIT-00b894" alt="license" />
<img src="https://img.shields.io/badge/stack-WebView2%20%2B%20C%23%20WinForms-0984e3" alt="stack" />
Expand All @@ -27,14 +27,14 @@ A hot-plug package manager for [DeepSeek Harness (dsh)](https://github.com/deeps

| | |
|---|---|
| **Release** | **v1.0.0** |
| Releases page | <https://github.com/ARFCON/dsh-hotplug-hub/releases/tag/v1.0.0> |
| Windows installer | `DSH-Hotplug-Hub-win-x64-setup-v1.0.0.exe` |
| Windows portable | `DSH-Hotplug-Hub-win-x64-portable-v1.0.0.zip` |
| Linux installer | `DSH-Hotplug-Hub-linux-x64-setup-v1.0.0.sh` |
| Linux portable | `DSH-Hotplug-Hub-linux-x64-portable-v1.0.0.tar.gz` |
| macOS installer | `DSH-Hotplug-Hub-macos-x64-setup-v1.0.0.command` |
| macOS portable | `DSH-Hotplug-Hub-macos-x64-portable-v1.0.0.zip` |
| **Release** | **v1.0.1** |
| Releases page | <https://github.com/ARFCON/dsh-hotplug-hub/releases/tag/v1.0.1> |
| Windows installer | `DSH-Hotplug-Hub-win-x64-setup-v1.0.1.exe` |
| Windows portable | `DSH-Hotplug-Hub-win-x64-portable-v1.0.1.zip` |
| Linux installer | `DSH-Hotplug-Hub-linux-x64-setup-v1.0.1.sh` |
| Linux portable | `DSH-Hotplug-Hub-linux-x64-portable-v1.0.1.tar.gz` |
| macOS installer | `DSH-Hotplug-Hub-macos-x64-setup-v1.0.1.command` |
| macOS portable | `DSH-Hotplug-Hub-macos-x64-portable-v1.0.1.zip` |

## Key Features

Expand All @@ -61,22 +61,22 @@ A hot-plug package manager for [DeepSeek Harness (dsh)](https://github.com/deeps

### Option 1 — Installer (recommended)

1. Download `DSH-Hotplug-Hub-win-x64-setup-v1.0.0.exe` from the Releases page.
1. Download `DSH-Hotplug-Hub-win-x64-setup-v1.0.1.exe` from the Releases page.
2. Double-click and choose an install location (default `%LOCALAPPDATA%\Programs\DseamWorld`).
3. The installer auto-creates desktop / start-menu shortcuts and launches when finished.

Silent install:

```powershell
DSH-Hotplug-Hub-win-x64-setup-v1.0.0.exe --silent
DSH-Hotplug-Hub-win-x64-setup-v1.0.0.exe --silent --dir "D:\MyApps\DseamWorld"
DSH-Hotplug-Hub-win-x64-setup-v1.0.1.exe --silent
DSH-Hotplug-Hub-win-x64-setup-v1.0.1.exe --silent --dir "D:\MyApps\DseamWorld"
```

### Option 2 — Portable

- **Windows**: unzip `DSH-Hotplug-Hub-win-x64-portable-v1.0.0.zip`, then double-click `DSH-Hotplug-Hub.exe` (WebView2 runtime DLLs are included in the same directory).
- **Linux**: `tar -xzf DSH-Hotplug-Hub-linux-x64-portable-v1.0.0.tar.gz`, then run `./dsh-hotplug-hub`.
- **macOS**: unzip `DSH-Hotplug-Hub-macos-x64-portable-v1.0.0.zip`, then double-click `Start-DSH-Hotplug-Hub.command`.
- **Windows**: unzip `DSH-Hotplug-Hub-win-x64-portable-v1.0.1.zip`, then double-click `DSH-Hotplug-Hub.exe` (WebView2 runtime DLLs are included in the same directory).
- **Linux**: `tar -xzf DSH-Hotplug-Hub-linux-x64-portable-v1.0.1.tar.gz`, then run `./dsh-hotplug-hub`.
- **macOS**: unzip `DSH-Hotplug-Hub-macos-x64-portable-v1.0.1.zip`, then double-click `Start-DSH-Hotplug-Hub.command`.

### Option 3 — From source

Expand Down Expand Up @@ -169,14 +169,14 @@ Full change history: [`开发文档/开发历史.md`](开发文档/开发历史.

| | |
|---|---|
| **正式版** | **v1.0.0** |
| 发布页 | <https://github.com/ARFCON/dsh-hotplug-hub/releases/tag/v1.0.0> |
| Windows 安装版 | `DSH-Hotplug-Hub-win-x64-setup-v1.0.0.exe` |
| Windows 便携版 | `DSH-Hotplug-Hub-win-x64-portable-v1.0.0.zip` |
| Linux 安装版 | `DSH-Hotplug-Hub-linux-x64-setup-v1.0.0.sh` |
| Linux 便携版 | `DSH-Hotplug-Hub-linux-x64-portable-v1.0.0.tar.gz` |
| macOS 安装版 | `DSH-Hotplug-Hub-macos-x64-setup-v1.0.0.command` |
| macOS 便携版 | `DSH-Hotplug-Hub-macos-x64-portable-v1.0.0.zip` |
| **正式版** | **v1.0.1** |
| 发布页 | <https://github.com/ARFCON/dsh-hotplug-hub/releases/tag/v1.0.1> |
| Windows 安装版 | `DSH-Hotplug-Hub-win-x64-setup-v1.0.1.exe` |
| Windows 便携版 | `DSH-Hotplug-Hub-win-x64-portable-v1.0.1.zip` |
| Linux 安装版 | `DSH-Hotplug-Hub-linux-x64-setup-v1.0.1.sh` |
| Linux 便携版 | `DSH-Hotplug-Hub-linux-x64-portable-v1.0.1.tar.gz` |
| macOS 安装版 | `DSH-Hotplug-Hub-macos-x64-setup-v1.0.1.command` |
| macOS 便携版 | `DSH-Hotplug-Hub-macos-x64-portable-v1.0.1.zip` |

## 主要功能

Expand All @@ -203,22 +203,22 @@ Full change history: [`开发文档/开发历史.md`](开发文档/开发历史.

### 方式一:安装包(推荐)

1. 在 Release 页面下载 `DSH-Hotplug-Hub-win-x64-setup-v1.0.0.exe`。
1. 在 Release 页面下载 `DSH-Hotplug-Hub-win-x64-setup-v1.0.1.exe`。
2. 双击运行,选择安装位置(默认 `%LOCALAPPDATA%\Programs\DseamWorld`)。
3. 安装包自动创建桌面 / 开始菜单快捷方式,完成后直接启动。

静默安装:

```powershell
DSH-Hotplug-Hub-win-x64-setup-v1.0.0.exe --silent
DSH-Hotplug-Hub-win-x64-setup-v1.0.0.exe --silent --dir "D:\MyApps\DseamWorld"
DSH-Hotplug-Hub-win-x64-setup-v1.0.1.exe --silent
DSH-Hotplug-Hub-win-x64-setup-v1.0.1.exe --silent --dir "D:\MyApps\DseamWorld"
```

### 方式二:便携版

- **Windows**:解压 `DSH-Hotplug-Hub-win-x64-portable-v1.0.0.zip`,双击 `DSH-Hotplug-Hub.exe`(WebView2 运行时 DLL 已内置同目录)。
- **Linux**:`tar -xzf DSH-Hotplug-Hub-linux-x64-portable-v1.0.0.tar.gz`,运行 `./dsh-hotplug-hub`。
- **macOS**:解压 `DSH-Hotplug-Hub-macos-x64-portable-v1.0.0.zip`,双击 `Start-DSH-Hotplug-Hub.command`。
- **Windows**:解压 `DSH-Hotplug-Hub-win-x64-portable-v1.0.1.zip`,双击 `DSH-Hotplug-Hub.exe`(WebView2 运行时 DLL 已内置同目录)。
- **Linux**:`tar -xzf DSH-Hotplug-Hub-linux-x64-portable-v1.0.1.tar.gz`,运行 `./dsh-hotplug-hub`。
- **macOS**:解压 `DSH-Hotplug-Hub-macos-x64-portable-v1.0.1.zip`,双击 `Start-DSH-Hotplug-Hub.command`。

### 方式三:源码安装

Expand Down
3 changes: 2 additions & 1 deletion dsh-hotplug-hub/dsh-memory-hub/lib/store.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,8 @@ export class MemoryStore {
try {
return fn()
} finally {
releaseLock(nodeFsPort, lockPath, { pid: process.pid, fd: a.fd })
// 审计修复:传入 refresh 句柄清理心跳(Worker 线程),否则释放后仍持续写锁文件。
releaseLock(nodeFsPort, lockPath, { pid: process.pid, fd: a.fd, refresh: a.refresh })
}
}

Expand Down
2 changes: 2 additions & 0 deletions dsh-hotplug-hub/dsh-memory-hub/vendor-shared/CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
| `PACK_ID_RE` | `^[a-z0-9][a-z0-9._-]{0,63}$`(大小写不敏感) | 包 id / CLI id:字母数字开头,允许 `. _ -`,1..64 字符 |
| `PLUGIN_NAME_RE` | `^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$` | npm 包名(可 scoped) |
| `EXACT_VERSION_RE` | `^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$` | 精确版本号(**须再经 semver 双检**:`semver.valid(v) !== null`,拒绝 `1.02.3` 等) |
| `REPO_RE` | `^[0-9A-Za-z][0-9A-Za-z._-]*\/[0-9A-Za-z][0-9A-Za-z._-]*$` | GitHub 仓库 `owner/repo`(两段、字母数字开头;拒绝前导 `. -` 与 `..` 段,防 URL/git clone 路径穿越) |
| `RESERVED_WIN_NAMES` | `CON PRN AUX NUL COM1..COM9 LPT1..LPT9` | Windows 保留设备名(任何单段名均拒绝,含首段匹配) |
| 长度预算 | id ≤ 64;patch id ≤ 64;source.path ≤ 4096;source.repo ≤ 512;source.ref ≤ 256 | |

Expand All @@ -25,6 +26,7 @@
- 尾随 `.` 或空格拒绝;
- 插件名每个 `/` 段均过 Windows 安全名检查;
- `source.path` 必须绝对、拒绝 UNC、拒绝 `.`/`..` 段;
- `source.repo` 必须匹配 `REPO_RE`(`owner/repo` 两段、字母数字开头;拒绝空白/元字符/`..` 段);
- `source.ref` 拒绝 `..`、纯点、控制字符、超长(阶段 2 起允许合法 `/`)。

## 2. 错误码与结果契约
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ const os = require('os');
const PACK_ID_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
const PLUGIN_NAME_RE = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
const EXACT_VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
// GitHub 仓库 owner/repo 格式:两段、各段字母数字开头(拒绝前导 . - 与 .. 段,
// 防止 repo 进入 URL/git clone 时产生路径穿越或畸形 URL)。
const REPO_RE = /^[0-9A-Za-z][0-9A-Za-z._-]*\/[0-9A-Za-z][0-9A-Za-z._-]*$/;

// Windows 保留设备名(N43:CON/NUL/COM1… 全部拒绝)
const RESERVED_WIN_NAMES = new Set([
Expand Down Expand Up @@ -45,9 +48,10 @@ const LOCK_REFRESH_MS = 10000;
const LAUNCH_ALIVE_CHECK_MS = 500;
const LAUNCH_WAIT_TIMEOUT_MS = 120000;

// 崩溃循环判定(3 次 / 30s 窗口)
// 崩溃循环判定:连续 CRASH_LOOP_THRESHOLD 次非零退出(retries 连续失败计数,
// 成功即清零;与 classifyStateSignals / healplan 触发文案一致)。历史曾声明
// "30s 窗口"语义(CRASH_LOOP_WINDOW_MS),但从未被实现或消费——已删除该死常量。
const CRASH_LOOP_THRESHOLD = 3;
const CRASH_LOOP_WINDOW_MS = 30000;

// 自愈默认重试预算
const DEFAULT_RETRY_BUDGET = 3;
Expand Down Expand Up @@ -133,6 +137,7 @@ module.exports = {
PACK_ID_RE,
PLUGIN_NAME_RE,
EXACT_VERSION_RE,
REPO_RE,
RESERVED_WIN_NAMES,
GITHUB_MIRRORS,
SCHEMA_VERSION,
Expand All @@ -148,7 +153,6 @@ module.exports = {
LAUNCH_ALIVE_CHECK_MS,
LAUNCH_WAIT_TIMEOUT_MS,
CRASH_LOOP_THRESHOLD,
CRASH_LOOP_WINDOW_MS,
DEFAULT_RETRY_BUDGET,
STATE_FILE,
RUN_LOG_FILE,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use strict';
// fs/lock-heartbeat.js — 锁 token 心跳(Worker 线程,独立事件循环)
//
// 背景(P1 修复):主线程持锁期间若执行阻塞式 spawnSync(npm/git 安装 >30s),
// 事件循环冻结,setInterval 无法触发 → token 时间戳不再刷新 → 第二写者按
// "存活 pid + token 超龄"判定陈旧并接管锁,破坏并发写互斥。
// 本 Worker 运行在独立线程,其事件循环与主线程互不阻塞,故即使主线程卡在
// spawnSync,token 仍按 refreshMs 周期刷新,锁的陈旧判定契约(CONTRACT.md §5)
// 得以维持。
//
// 审计修复(P1b,跨线程覆盖竞态):此前每 tick 用 fs.openSync(lockPath, 'r+')
// 按【路径】重开锁文件写 token——release 后主线程 unlink 旧锁、另一进程在同路径
// 重建新锁,若本 Worker 尚未被 terminate(terminate 异步、存在窗口),下一次 tick
// 会打开【新持有者的锁文件】并用旧 pid 覆盖其 token,击穿跨进程互斥(已实证复现)。
//
// 根因修复:Worker 不按路径重开,改经【主线程传入的持有 fd】写——该 fd 由
// acquireLock 用 'wx' 独占创建、绑定获取锁时创建的固定 inode;release 后主线程
// unlink + 重建同名文件,本 fd 仍指向旧的(已 unlink)inode,写入不影响新持有者。
//
// 同步停止握手:主线程 releaseLock 先 Atomics.store(ctrl[0]=1) 请求停止并
// Atomics.notify 唤醒本 Worker,再 Atomics.wait(ctrl[1]==1) 等待确认;本 Worker
// 仅在 while 条件(ctrl[0]==0)为真时写,收到停止请求后【绝不再写】,随后置
// ctrl[1]=1 确认并退出。主线程在收到确认前不关闭 fd → 不存在"写已关闭/被复用 fd"
// 的窗口(fd 复用竞态一并消除)。
const { workerData } = require('worker_threads');
const fs = require('fs');

const { fd, pid, refreshMs, ctrl } = workerData;
const c = new Int32Array(ctrl); // c[0]=停止请求(主线程写);c[1]=已停止确认(本 Worker 写)

while (Atomics.load(c, 0) === 0) {
try {
fs.ftruncateSync(fd, 0);
fs.writeSync(fd, `${pid}\n${Date.now()}\n`, 0, 'utf8');
} catch (_) { /* 刷新失败:下次重试;陈旧接管兜底 */ }
// 阻塞至 refreshMs 或主线程请求停止(Atomics.notify 立即唤醒)
Atomics.wait(c, 0, 0, refreshMs);
}

// 确认停止并退出(主线程据此决定关闭 fd 的时机)
Atomics.store(c, 1, 1);
Atomics.notify(c, 1);
process.exit(0);
86 changes: 72 additions & 14 deletions dsh-hotplug-hub/dsh-memory-hub/vendor-shared/fs/lock.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
// | EEXIST + pid 死 | 立即接管 |
// | EEXIST + EACCES/EPERM(他用户) | 等待至超时,不接管 |
// | token 缺失/损坏 | 按文件 mtime 判陈旧 |
const { dirname } = require('path');
const { dirname, join } = require('path');
const {
LOCK_WAIT_MS,
LOCK_STALE_MS,
Expand Down Expand Up @@ -160,6 +160,60 @@ function checkV1DirectoryLock(fsPort, lockPath, opts) {
return { held: false };
}

/**
* 启动锁 token 心跳。
* P1 修复:优先用 Worker 线程(独立事件循环)——主线程执行阻塞式 spawnSync 时
* setInterval 无法触发,token 会陈旧被第二写者接管;Worker 线程不受主线程阻塞影响。
* Worker 不可用(环境限制)时回退主线程 setInterval(尽力而为,阻塞期间仍可能陈旧)。
*
* P1b 修复(跨线程覆盖竞态):Worker 经【主线程传入的持有 fd】写 token,绝不按路径
* 重开——按路径重开会在 release→reacquire 窗口打开新持有者的锁文件并用旧 pid 覆盖其
* token。fd 绑定获取锁时创建的 inode,release 后重建同名文件也不受影响。
* 同步停止握手:返回 heartbeat 句柄含 stop(),其置停止请求 + 等 Worker 确认后再让
* 调用方关 fd(见 releaseLock)。
* @param {object} fsPort fs 端口(回退路径用)
* @param {number} pid 持有者 pid
* @param {number} refreshMs 刷新周期(0=不刷新,返回 null)
* @param {number} fd 主线程已打开的锁 fd(Worker 经此写;回退路径重写 token 用)
* @returns {{stop: Function}|object|null} heartbeat 句柄或 interval 句柄
*/
function startHeartbeat(fsPort, pid, refreshMs, fd) {
if (!(refreshMs > 0)) return null;
try {
const { Worker } = require('worker_threads');
const ctrl = new SharedArrayBuffer(8); // 2×Int32:c[0]=停止请求,c[1]=已停止确认
const worker = new Worker(join(__dirname, 'lock-heartbeat.js'), {
workerData: { fd, pid, refreshMs, ctrl }
});
if (typeof worker.unref === 'function') worker.unref();
const c = new Int32Array(ctrl);
return {
stop() {
// 请求停止并等待 Worker 确认:确认后 Worker 绝不再写,调用方可安全关 fd。
Atomics.store(c, 0, 1);
Atomics.notify(c, 0);
const deadline = Date.now() + 1000;
while (Atomics.load(c, 1) === 0 && Date.now() < deadline) {
// Worker 收到 notify 后应立即置 c[1]=1;此处短暂自旋兜底(Worker 崩溃/未启动)
Atomics.wait(c, 1, 0, 50);
}
try { worker.terminate(); } catch (_) { /* 忽略 */ }
}
};
} catch (_) {
// Worker 不可用(worker_threads 缺失/创建失败)→ 回退主线程 setInterval
if (typeof setInterval !== 'function') return null;
const timer = setInterval(() => {
try {
fsPort.ftruncateSync(fd, 0);
fsPort.writeSync(fd, formatToken(pid, Date.now()), 0, 'utf8');
} catch (_) { /* 刷新失败:陈旧接管兜底 */ }
}, refreshMs);
if (typeof timer.unref === 'function') timer.unref();
return { stop() { clearInterval(timer); } };
}
}

/**
* 获取文件锁。
* @param {object} fsPort fs 端口
Expand Down Expand Up @@ -210,19 +264,15 @@ function acquireLock(fsPort, lockPath, opts = {}) {
try { if (fsPort.existsSync(lockPath)) fsPort.unlinkSync(lockPath); } catch (_) { /* 忽略 */ }
return { ok: false, error: makeError('ERR_LOCK_ACQUIRE', `写入锁 token 失败 ${lockPath}:${writeErr.message}`) };
}
// 持锁期刷新:经已打开 fd 重写 token(防止长任务被误判陈旧)
let refresh = null;
if (refreshMs > 0 && typeof setInterval === 'function') {
refresh = setInterval(() => {
try {
fsPort.ftruncateSync(fd, 0);
fsPort.writeSync(fd, formatToken(pid, Date.now()), 0, 'utf8');
} catch (_) { /* 刷新失败:下次仍会尝试;陈旧接管兜底 */ }
}, refreshMs);
if (typeof refresh.unref === 'function') refresh.unref();
}
// 持锁期刷新:Worker 线程心跳(独立事件循环)——主线程阻塞 spawnSync 时
// setInterval 无法触发,token 会陈旧被第二写者接管(P1);Worker 不受影响。
const refresh = startHeartbeat(fsPort, pid, refreshMs, fd);
const release = () => releaseLock(fsPort, lockPath, { owner, pid, fd, refresh });
return { ok: true, fd, owner, token, release };
// 审计修复:返回 refresh 句柄——此前调用方(pipeline/index 等)直接 releaseLock
// {owner,pid,fd} 而不传 refresh,导致持锁期 setInterval 定时器在释放后泄漏、
// 每 10s 对已关闭 fd 写 token(EBADF 被吞)。返回后调用方须在 releaseLock 时
// 一并传入 refresh 以清理定时器。
return { ok: true, fd, owner, token, release, refresh };
} catch (e) {
if (fd !== null) {
try { fsPort.closeSync(fd); } catch (_) { /* 忽略 */ }
Expand Down Expand Up @@ -297,7 +347,15 @@ function releaseLock(fsPort, lockPath, opts = {}) {
const owner = opts.owner || `pid-${pid}`;
try {
if (opts.refresh) {
try { clearInterval(opts.refresh); } catch (_) { /* 忽略 */ }
// P1b 修复:refresh 统一为 heartbeat/interval 句柄(含 stop())。
// stop() 先停 Worker 并等其确认(heartbeat)或 clearInterval(interval),
// 确保返回后不再有 token 写,调用方随后关 fd / unlink 才安全。
// 兼容旧形态(裸 Worker/interval)以防调用方仍传旧值。
try {
if (typeof opts.refresh.stop === 'function') opts.refresh.stop();
else if (typeof opts.refresh.terminate === 'function') opts.refresh.terminate();
else clearInterval(opts.refresh);
} catch (_) { /* 忽略 */ }
}
const token = readToken(fsPort, lockPath);
if (token && token.pid !== pid) {
Expand Down
7 changes: 7 additions & 0 deletions dsh-hotplug-hub/dsh-memory-hub/vendor-shared/ids.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const {
PACK_ID_RE,
PLUGIN_NAME_RE,
EXACT_VERSION_RE,
REPO_RE,
RESERVED_WIN_NAMES,
MAX_ID_LENGTH,
MAX_SOURCE_PATH_LENGTH
Expand Down Expand Up @@ -214,6 +215,12 @@ function validateSourceRepo(repo) {
if (CONTROL_CHAR_RE.test(repo)) {
return { ok: false, error: makeError('ERR_ASSEMBLY_FIELD', 'source.repo 不得包含控制字符') };
}
// 审计修复:repo 必须是 owner/repo 格式(两段、字母数字开头、拒绝 .. 段/空白/元字符)。
// 此前仅查长度+控制字符,`../../etc/passwd`、含空格或 `?query` 的串会进入
// codeload/git clone URL 拼装,产生畸形 URL 或路径穿越风险。
if (!REPO_RE.test(repo)) {
return { ok: false, error: makeError('ERR_ASSEMBLY_FIELD', `source.repo 必须是 owner/repo 格式:${JSON.stringify(repo)}`) };
}
return { ok: true };
}

Expand Down
4 changes: 2 additions & 2 deletions dsh-hotplug-hub/dsh-memory-hub/vendor-shared/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ export const {

// contracts/constants
export const {
PACK_ID_RE, PLUGIN_NAME_RE, EXACT_VERSION_RE, RESERVED_WIN_NAMES, GITHUB_MIRRORS,
PACK_ID_RE, PLUGIN_NAME_RE, EXACT_VERSION_RE, REPO_RE, RESERVED_WIN_NAMES, GITHUB_MIRRORS,
SCHEMA_VERSION, HOTPACK_VERSION, MAX_ID_LENGTH, MAX_PATCH_ID_LENGTH, MAX_SOURCE_PATH_LENGTH,
RUNLOG_MAX_BYTES, LOCK_WAIT_MS, LOCK_STALE_MS, LOCK_POLL_MS, LOCK_REFRESH_MS,
LAUNCH_ALIVE_CHECK_MS, LAUNCH_WAIT_TIMEOUT_MS, CRASH_LOOP_THRESHOLD, CRASH_LOOP_WINDOW_MS,
LAUNCH_ALIVE_CHECK_MS, LAUNCH_WAIT_TIMEOUT_MS, CRASH_LOOP_THRESHOLD,
DEFAULT_RETRY_BUDGET, STATE_FILE, RUN_LOG_FILE, PATCH_FILE, PROFILE_MANIFEST,
SNAPSHOT_INLINE_MAX_BYTES, PROFILES_DIR, STORE_DIR, MEMORY_DIR, HOTPLUG_DIR, PATCH_LOCK_FILE,
resolveDshRoot, dshRootPaths, defaultRoots,
Expand Down
Loading
Loading