dsh-weknora
Tencent/WeKnora把 WeKnora 知识库接入 dsh 的四个只读工具:列出知识库、混合检索原文片段、按顺序还原单篇文档,以及直接取用 WeKnora 自己带引用的 RAG 或 ReAct agent 回答(含可续聊的 session id)。
从 Web GUI 添加 WSL 工作区,无需在 WSL 之中再次安装 dsh 以及相关工具,bash 命令与文件读写运行在本机 WSL 发行版内,Windows 文件仍可访问。
English · 中文 · 日本語 · 한국어 · Français · Deutsch · Español · Português · Русский Add a WSL workspace from the DeepSeek Harness web GUI and run the whole agent session — bash commands and file reads/writes — inside a local WSL distribution with Linux paths. Nothing needs to be installed inside WSL. The session can reach both WSL and Windows at the same time: bash commands run inside the WSL distribution, while Windows files stay accessible via /mnt/ (for example /mnt/c/Users/...).
Pick one of the three ways below, then restart dsh web:
# 1) npm package
dsh plugin --profile web add dsh-wsl-workspace
# 2) GitHub repository (ships the prebuilt lib/, no local build required)
dsh plugin --profile web add https://github.com/6Mikao9/dsh-wsl-workspace
# 3) Local directory (development / self-hosted)
dsh plugin --profile web add D:\path\to\dsh-wsl-workspaceAfter restarting dsh web, a W button appears beside Settings at the sidebar foot.
Click the W button beside Settings at the sidebar foot to open the "Add WSL workspace" dialog. Pick a distribution from the list, then browse the directory tree or type an absolute Linux path (for example /home/me/proj) — use the Check button to verify the path exists before creating the workspace. The dialog follows the DeepSeek Harness UI language. The username field is optional: leave it empty to run commands as the distribution's default user, or name a Linux user of that distribution to run the session as that user instead (equivalent to wsl.exe -u ). The username only changes the bash tool's run identity — the file tools go through the Windows-side WSL share and are unaffected. Each workspace's username is kept in /wsl-workspaces.json; delete the entry (or recreate the workspace from the dialog) to return to the default user.
Click "Create & open" to start a new session in the workspace. In the new session the bash tool executes commands inside the chosen distribution and read/write/edit operate on WSL files, so every path the model sees is a Linux path. The mode picker keeps working as usual: Standard, PTC, Minimal and Creative each land on their WSL variant automatically (the WSL variant entries in the picker are bilingual, e.g. WSL · Standard mode(标准模式)), and Windows files stay reachable from inside the session under /mnt/ (for example /mnt/c/Users/...). The dialog's "?" button opens a panel with the DSH releases this build declares, how the plugin is used, and the limitations it cannot fix.
root), so it can read and write anywhere in the distro. The Windows ACL sandbox cannot wrap wsl.exe — its children run on the Linux kernel side — so WSL itself is the isolation boundary and the DSH file policy does not apply to bash.fs provider in the preset's isolate realm where the host's fs-sandbox wrapper is not in the call path. Symlinks: the share lists a Linux link but cannot resolve it, so a link path used to look like a missing file; resolve/lstat now ask the distribution (wsl.exe … readlink -f) and continue at the real path, and a link is never replaced by a regular file. The access mode: write/edit are fenced by ctx.sandboxPolicy exactly as the host backend fences them — the same writableRoots allow-list (plus the distribution's /tmp, the temp area of the world the session runs in), the same FS_SANDBOX_DENIED the tool layer renders as a denial, and the same sandboxMode fact it reads for escalation. Because the fence runs after resolution it judges the real path: a link out of the workspace is an outside write and is denied under workspace-write. A deployment that mounts no policy service fences nothing, as on the host.src/host/wsl-search.ts → lib/wsl-search.js), which runs the search inside the distribution: the tool names, parameter schemas, caps, output schema (Line N: grouping, found-count header, capped-result footer), search cards and formatted-result spill all come from @deepseek-ai/dsh-tool-fs-search's own exported pieces, so what the model sees matches the host. grep uses the distribution's GNU grep (-rnIEH -Z; POSIX ERE — \d, \w, \b and (?i) work, lookaround and backreferences do not), skips hidden files and directories plus node_modules like ripgrep's defaults do, does not read .gitignore, expands {a,b} into one --include per alternative, matches a include containing / in this process (ripgrep semantics), and fails loudly on a distribution without GNU grep rather than framing unreadable records. glob lists with GNU find (-printf gives each file's mtime without a stat per file) and matches gitignore-style patterns here (* never crosses a separator, ** does, ?, [...], {a,b} and a leading ! negation), ordering oldest-first exactly as rg --sort=modified does. Both search the Linux tree directly — symlinks, permissions and .gitignore-free traversal included — never the 9P share, both prune VCS metadata directories, and neither follows a symlink found during recursion, which is ripgrep's default too. Modes whose source preset mounts no search suite (Minimal) gain none..git ancestor (falling back to the cwd itself), then scanning downward for .dsh/skills / .agents/skills — including nested projects — bounded to 4 directory levels, 64 skill directories and 4096 visited directories. Register the workspace at the project root you work in; if the registered workspace itself sits inside a larger git repository, the scan starts at that repository's root (matching the host's own rule) and sibling projects may surface. A Linux symlink the Windows-side share cannot resolve is resolved through the distribution instead (wsl.exe … readlink -f, at most 32 per lookup, four in flight) and the walk continues at the real path, so a project linked in with ln -s — and nested projects below it — is discovered and deduplicated by that real path. Skill bodies always load live. The generated preset pins watch: false on the skill-filesystem row because watching a \\wsl.localhost... path fails, so the plugin polls instead, in two passes. The cheap pass runs every 3 seconds over the skills directories it published and re-stats each skill file: an added, removed or edited skill therefore reaches the model's next turn, and an edit is why the stamp matters — the model's catalog is rebuilt only when the registry's revision moves, and a directory listing cannot tell a rewritten SKILL.md from an untouched one. The full re-discovery walk runs every 30 seconds, because only a walk can find a skills directory that did not exist before (a new nested project's first .dsh/skills, say). Neither pass re-reads a skill body.bash is a stateful shell — cd, exported variables, activated virtualenvs and background jobs survive between calls. The world mounts the host's PTY registry and its config-driven backend (@deepseek-ai/dsh-terminal-bash) pointed at this plugin's relay (src/host/wsl-relay.ts → lib/wsl-relay.js, run by the host's own node), which hands the PTY to wsl.exe … bash: the distribution comes from the session's UNC cwd (else DSH_WSL_DISTRO), the optional username from DSH_WSL_USER, and the relay runs bash -lc 'cd … && exec bash -i' so the login environment is loaded while the session directory survives (a plain bash -l can be sent to $HOME by a profile). That tool registers the bash name, so it replaces the one-shot dsh-tool-bash row a non-WSL preset would use — the world also provides its own no-op sandbox capability (src/host/wsl-sandbox.ts), because the host's Windows ACL runner cannot read a \\wsl.localhost\… path's security descriptor and the PTY backend confines through it before spawning. Both shells run inside the distribution and are outside the DSH file policy — WSL is their isolation boundary. Two consequences of the host's own wrapping are stated in the tool's description (which this plugin overrides, because the host default mentions neither): the shell is one process for the whole Agent, so a cd in one call decides where the next call starts — use absolute paths or an explicit cd; and the host wraps each command as eval -- $'…', so a command ending in & backgrounds the *whole* wrapped command — the call then returns immediately with exit code 0 and no output while the real output arrives later, possibly inside the next call's. Write background work as ( long-job > log 2>&1 ) & on its own line, or use the background-job tool.bash_background starts one command in the background and returns a registry job id immediately; the host's job_list, job_output (incremental reads, status transitions, completion notices) and job_kill then work on it exactly as they do for the host's one-shot tool. The row exists because the persistent shell's schema declares only command: without a producer, job_list always answered "no background jobs" and a run_in_background: true argument passed to bash was silently ignored — the parameter schema does not forbid extra properties, so nothing reported the mistake. A real session found exactly that. The tool is mounted only alongside the persistent shell; a world that keeps the one-shot bash row already has run_in_background on that tool.0.1.0-rc.8 on — in 0.1.0-rc.7 spawnTerminal throws subprocess-local: terminal inspection is unsupported on platform win32 before any process starts, so every bash call in that release fails outright (grep/glob, which never touch the PTY, keep working). The plugin therefore *probes* the substrate at startup instead of assuming: it hands spawnTerminal a program that cannot exist, which reaches the inspector check and nothing else — no process is created either way, and the rejection says which half failed. When the answer is "no inspector", the generated world keeps the one-shot dsh-tool-bash row (this plugin's own ctx.shell provider, no PTY) and the model gets a working, stateless shell instead of an error on every call. 0.1.0-rc.7 is the only declared release in that state; every later one gets the persistent shell.localhost port-forwarding banner wsl.exe prints to stderr when the distro was not running yet is harmless.wraps child_process before the plugin is loaded: it installs plain exec/execFile wrappers and re-exports them with syncBuiltinESMExports(). That copy of execFile carries no util.promisify.custom, so promisify(execFile) fell back to the generic implementation, which resolves with the first callback argument only — the stdout string — and result.stdout was undefined on every call. The distro lookup read .stdout and threw Cannot read properties of undefined (reading 'includes'), the frontend caught it and rendered an empty picker: the dialog opened, the distribution list stayed empty, and "Create & open" could not be completed, while wsl.exe -l -q in a terminal listed the distribution normally. All three call sites now use the callback form of execFile, which no wrapper can reshape.
wsl.exe is tried on PATHfirst, then the absolute %SystemRoot%\System32\wsl.exe, and a lookup that fails now names every candidate it tried and the error each one produced, instead of a type error from inside the decoder.
tests/exec-shape.mjs reproduces the Desktop wrapper (plain wrappers plussyncBuiltinESMExports()), spawns a probe with and without --import, asserts the wrapped shape is broken and the new helpers are right, and runs the real listDistros/defaultDistro/resolveLinuxSymlink through both shapes. It is registered as the exec-shape check in scripts/compatibility/Run-Checks.ps1 so a future refactor cannot reintroduce the promisified call.
dsh.compatibility.dshReleases declares 0.1.7-rc.2 as well, so this buildclaims ten releases instead of nine.
(13/15 everywhere — the two failures are the documented baseline: typecheck and host-api, which needs a *running* frontend and passes 12/12 when pointed at one), plus the runbook's six-item frontend pass on all ten (dialog distro list, create & open, write, read, one-shot bash, persistent bash, skills) with web.err at 0 bytes and the file re-read independently on the Linux side. The Desktop-wrapper reproduction is in docs/compatibility-evidence.md, together with the pack-identity hash and the plain-npm install gate.
0.1.7 line renamed the host preset face — read() became readDocument(), which returns a document ({agentPreset, content, name, description}) instead of the composition text, and AgentPreset lost its path — so the variant generator threw agentPresets.read is not a function on every boot and no wsl-* mode ever reached the picker. The roster face is now probed by capability and both generations are served: readDocument() where it exists, read() behind it.
0.1.7 stopped scanning$DSH_HOME/.agent-presets/ — a preset there is a declarative @deepseek-ai/dsh-agent-preset row, and the directory a variant used to be written to is read by nothing at all. The generator now expands the composed variant back into an entry list and publishes it through ctx.agentPresets.register(); the plugin's effect owns the returned disposers, so an unload or hot reload retires the variants instead of leaving orphans the next apply could not replace (Duplicate agent preset: wsl-). Earlier releases keep the directory channel, unchanged.
mounted from a declaration is imported by the registry's entry tree, which — unlike the boot-time Include — does not turn an absolute path into a file: URL. Without the rewrite the provider rows never started, the audit reported each as never started, and the whole variant was refused as unusable.
shipped modes their bilingual WSL · … names carried the mode's older id (code) but not ptc, the id it has used since 0.1.1. That stayed invisible as long as the release published its own display name, because the lookup fell through to it — but 0.1.7 publishes none, so the mode reached the picker as WSL · ptc with the generic WSL execution world for ptc: … description. Both ids are covered now, and the variant reads WSL · PTC mode(PTC 模式) on every channel, like the other three shipped modes.
dsh.compatibility.dshReleases declares 0.1.7-rc.1. The two modules thedeclaration channel needs (@deepseek-ai/cordis-plugin-include, js-yaml) are resolved at call time and declared as optional peers, so a release that lacks them fails one variant rather than refusing to load the plugin.
The host rebuilds the catalog during a request and awaits each provider's list(), and this provider kept its own answer for only 10 s — so every time the catalog was re-collected (a new session or scope, or simply a lookup more than 10 s after the last one) that request paid a full walk of the workspace, one directory at a time: two stats and one readdir each. A readdir over the \\wsl.localhost\… 9P share measures 3-16 ms here and the walk's budget is 4096 directories, which is why a large workspace cost 20.4 s, on the request path. A published catalog is now served as-is, and only the provider's own change detector can drop it: a repeat lookup costs 1-3 ms and no filesystem traffic. The freshness contract is unchanged — a new nested skills directory still appears within 30 s, and an added, removed or edited skill within 3 s.
published in frontier order, so the catalog stays deterministic, and a directory's .dsh/skills / .agents/skills are probed only when its own listing showed that marker. The budget-sized walk went from 20.4 s to 4.8 s on the same machine; node's filesystem thread pool caps the real parallelism.
artifact turned up a regression this release introduced: npm auto-installs missing peer dependencies, and the @deepseek-ai/dsh-tool-fs-search peer added in 0.6.0 itself peers on @deepseek-ai/dsh-retention, which is not published — so a plain npm install died with E404 … @deepseek-ai/dsh-retention (0.4.3 installs fine, so it was ours). dsh plugin add uses pnpm, which only *warns* about unmet peers, which is why every harness run and real install passed. All ten host peers are now marked optional in peerDependenciesMeta: the package still declares what the host must provide, but npm no longer tries to fetch it.
The WSL world now matches the host everywhere a session can tell the difference, and the last two known issues are closed. Everything below ships together: a WSL variant gets Linux symlinks, the session's access mode, in-distribution search, a live skill catalog, a stateful shell, and tracked background jobs.
tool wraps each command as eval -- $'…', so a command ending in & backgrounds the *whole* wrapped command — the call returns immediately with exit code 0 and no output while the real output arrives later, possibly inside the next call's. And the shell is one process for the whole Agent, so a cd carries into the next call. The host default says neither, and DSH's own Minimal preset recommends the hazardous form (sleep 10 &). The world now overrides description (a supported key on every declared release) with both facts and the safe forms.
dsh-subprocess-local has no Windows process inspector, so the host's PTY-backed persistent shell cannot start on Windows at all — every bash call failed with subprocess-local: terminal inspection is unsupported on platform win32 (the host ships the same gap: its Minimal preset mounts persistent-bash there with no Windows guard). The plugin now *probes* the substrate instead of assuming — it hands spawnTerminal a program that cannot exist, which reaches the inspector check and nothing else — and when the answer is no, the world keeps the one-shot dsh-tool-bash row: a working, stateless shell rather than an error per call.
persistent one also removed the only thing that started a registry job, so job_list always answered "no background jobs" and a run_in_background: true argument handed to bash was silently ignored (the parameter schema allows extra properties, so nothing complained). The world now mounts bash_background (src/host/wsl-jobs.ts), a thin producer over the host's own ctx.jobs.start plus this plugin's ctx.shell.start: it returns a job id, and job_list/job_output/job_kill work on it as usual. It is mounted only where the source mode also mounts the job_* tools, and only alongside the persistent shell.
guard that also applied to an explicitly named file (grep path=.env returned nothing), a discarded find exit status (glob path=/nope-missing looked like an empty directory), a line-terminated glob header (a root whose name contains a newline came back truncated), untranslated Windows paths (grep path='D:\proj' failed where read worked), a closed spill schema (which would have failed the tool's own output validation on every capped search), and a catalog detector that could stack polls on a slow share. Plus two in the new producer: it was mounted in a mode with no job_* tools to read its ids, and it defaulted a job's working directory to the host process's rather than the session workspace.
(0.1.0-rc.7 … 0.1.5-rc.2) — search-real drives the real tools against a real distribution fixture — leaving only the two pre-existing baseline failures (typecheck, and host-api which needs a live server). 152 unit tests, including a parity check of every renderer against the host suite's own formatters. Real browser sessions on five releases for the tool behaviour, and a frontend pass on all eight (entry button, dialog, path check, create & open, mode picker, help panel with v0.7.0 and 8 release chips), with the session log as evidence for the tool set, the search results, the catalog replacement, the shell fallback and the background-job lifecycle.
tool-fs-search and left the model to grep through the shell — the last bullet of the panel's known issues. The world now mounts an in-distribution twin that keeps the host suite's contract: the same tool names, parameter schemas, inline caps (250 matches / 100 paths), output schema, Line N: grouping, found-count header, capped-result footer, search card and formatted-result spill — the rendering comes from @deepseek-ai/dsh-tool-fs-search's own exported formatters, and the two projections that package keeps private (the card metadata and the glob page) are reproduced and compared against it in unit tests. grep runs GNU grep inside the distribution (-rnIEH -Z, POSIX ERE, hidden entries and node_modules skipped like ripgrep's defaults, no .gitignore support), glob uses GNU find with in-process gitignore-style matching and ripgrep's oldest-first modification order. Model-controlled values travel as separate argv elemen