dsh-workspace-plus 0.6.1 → 0.7.1
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 -5
- package/cordis.patch.yml +1 -1
- package/lib/client.js +47 -11
- package/lib/index.js +63 -6
- package/lib/notice/click-host.cs +515 -0
- package/lib/notice/index.js +114 -20
- package/lib/notice/launcher.js +455 -0
- package/lib/notice/win32.js +26 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/dsh-workspace-plus)
|
|
4
4
|
[](https://dsh-plugin.org/plugins/sen70s/dsh-workspace-plus)
|
|
5
5
|
[](https://github.com/Sen70s/dsh-workspace-plus/blob/main/LICENSE)
|
|
6
|
-
|
|
7
|
-
**当前版本 `v0.6.1`** · 要求 DSH `>=0.1.7-rc.2`
|
|
8
|
-
|
|
6
|
+
**当前版本 `v0.7.1`** · 要求 DSH `>=0.1.7-rc.2`
|
|
9
7
|
DeepSeek Harness(DSH)的插件。一个插件,两个子功能,插件页里各占一行、可单独启停。
|
|
10
8
|
|
|
9
|
+
DeepSeek Harness(DSH)的插件。一个插件,三个子功能,插件页里各占一行、可单独启停。
|
|
10
|
+
|
|
11
11
|
| 子功能 | 说明 |
|
|
12
12
|
| --- | --- |
|
|
13
13
|
| **workspace+** | 一个工作区登记多个目录,每个目录一个标签。标签表注入每次请求,输入框打 `@` 也能直接引用 |
|
|
@@ -58,6 +58,8 @@ agent 直接去 D:\proj\backend,不会因为旁边有个 backend-legacy 而改
|
|
|
58
58
|
|
|
59
59
|
装好即用。请求权限、询问用户、任务完成、中断、出错都会弹 toast,点一下就打开对应的对话。
|
|
60
60
|
|
|
61
|
+
正常情况下,点通知不会再先闪一个终端窗口。首次注册 `dsh-notice:` 协议时(插件加载后就会发生),宿主会用系统自带的 `csc.exe` 把一个小助手(约 10 KB 的 `click-host.exe`)编译到 `~/.dsh/notice/` 并缓存;之后点击由这个助手在无窗口的情况下完成,窗口被别的程序挡住、最小化甚至收进托盘,它都能恢复并切到前台(最大化状态保持不变)。编译不可用时自动退化为无窗口 PowerShell(改由 Harness 自己把窗口切到前台),再不行就退化为普通 PowerShell 兼容模式(可能有窗口闪烁);当前用的是哪一种,设置 → 通知 的状态区会直接写明。
|
|
62
|
+
|
|
61
63
|
设置 → 通知 里可以选通知类型、声音、时长、注册协议、发一条测试通知。这里还有一个「启用桌面通知」开关,不动插件页也能静音。
|
|
62
64
|
|
|
63
65
|
设置存在 `~/.dsh/notice/`,协议 scheme 是 `dsh-notice:` —— 从独立 dsh-notice 插件升级过来时,已有设置与协议注册自动生效。
|
|
@@ -70,12 +72,13 @@ agent 直接去 D:\proj\backend,不会因为旁边有个 backend-legacy 而改
|
|
|
70
72
|
node tools/verify.mjs
|
|
71
73
|
```
|
|
72
74
|
|
|
73
|
-
|
|
75
|
+
521 项断言,覆盖 manifest、patch 层、宿主半(含 `/workspaceplus/api` 在认证服务缺失时的拒绝)、浏览器半、工作区行按钮、`@` 候选源、弹窗形状,以及通知点击启动器的编译与回退(含真实 `csc.exe` 编译一次并自检)。
|
|
74
76
|
|
|
75
77
|
## 其它
|
|
76
78
|
|
|
77
|
-
- `lib/`
|
|
79
|
+
- `lib/` 是手写源码,**不需要打包步骤**(没有 bundler):从 npm、GitHub、本地目录三种方式装到的都是可直接加载的代码。唯一的例外是通知点击助手 —— 它由宿主在首次注册协议跳转时(插件加载后就会发生)用系统自带的 `csc.exe` 从 `lib/notice/click-host.cs` 现编到 `~/.dsh/notice/` 并缓存;编译不可用时自动退化为 PowerShell 启动器,插件其余部分不受影响。
|
|
78
80
|
- workspace+ 的状态文件在 `~/.dsh/workspaceplus/directories.json`,可以直接手改,改完刷新页面即可。
|
|
81
|
+
- workspace+ 的浏览器接口 `/workspaceplus/api` 每次请求都要过宿主的 `connection.admit` 认证。认证服务缺失、`admit` 不存在或报错时一律 **503**,读写都不放行——这个接口写下的目录备注会进入模型上下文,不能让未认证的调用方有机会修改它。
|
|
79
82
|
- [LICENSE](./LICENSE) · MIT
|
|
80
83
|
|
|
81
84
|
## Star History
|
package/cordis.patch.yml
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
# regardless and degrades silently when this row is off.
|
|
27
27
|
#
|
|
28
28
|
# Turning one row off removes exactly that half from the host while the bundle
|
|
29
|
-
# and the other sub-
|
|
29
|
+
# and the other sub-features keep working. The 通知 settings page also carries a
|
|
30
30
|
# runtime "启用桌面通知" switch, so notifications can be silenced without
|
|
31
31
|
# touching the row at all.
|
|
32
32
|
- insert:
|
package/lib/client.js
CHANGED
|
@@ -388,8 +388,36 @@ window.__ModuleLoader__.load({
|
|
|
388
388
|
} AUMID 发言:${toast.appId ?? '(未知)'}`
|
|
389
389
|
})()
|
|
390
390
|
|
|
391
|
+
// How a click is actually delivered, as the host reports it after
|
|
392
|
+
// registering the protocol. The host prefers a compiled windowless
|
|
393
|
+
// helper and falls back to windowless PowerShell, then to plain
|
|
394
|
+
// PowerShell, so the status line names the mode instead of promising the
|
|
395
|
+
// best case. The helper restores and focuses the window itself, but only
|
|
396
|
+
// when `focusOnClick` is on — with the switch off the host registers the
|
|
397
|
+
// same helper without that step — so the sentence must not claim a
|
|
398
|
+
// foreground window in that state. A host that predates `launcher` has no
|
|
399
|
+
// value here, and then this returns '' so the sentence reads exactly as
|
|
400
|
+
// it did before.
|
|
401
|
+
const launcherNote = (() => {
|
|
402
|
+
const focus = settings.focusOnClick === true
|
|
403
|
+
if (protocol.launcher === 'helper') {
|
|
404
|
+
return focus ? '点击助手会在无窗口的情况下把 Harness 切到前台' : '由无窗口的点击助手处理,不改变窗口的前后顺序'
|
|
405
|
+
}
|
|
406
|
+
if (protocol.launcher === 'windowless-powershell') {
|
|
407
|
+
return focus
|
|
408
|
+
? '使用无窗口 PowerShell 助手,由 Harness 负责切到前台;首次点击可能稍慢'
|
|
409
|
+
: '使用无窗口 PowerShell 助手;首次点击可能稍慢'
|
|
410
|
+
}
|
|
411
|
+
if (protocol.launcher === 'powershell') return '兼容模式:由 PowerShell 启动,可能有窗口闪烁'
|
|
412
|
+
return ''
|
|
413
|
+
})()
|
|
414
|
+
|
|
391
415
|
const protocolLine = (() => {
|
|
392
|
-
if (protocol.registered === true)
|
|
416
|
+
if (protocol.registered === true) {
|
|
417
|
+
return launcherNote
|
|
418
|
+
? `点击通知可打开对应对话(已注册 ${protocol.scheme}:,${launcherNote})。`
|
|
419
|
+
: `点击通知可打开对应对话(已注册 ${protocol.scheme}:)。`
|
|
420
|
+
}
|
|
393
421
|
if (settings.registerProtocol === false) return '已关闭协议注册:点击通知只会把 Harness 窗口切到前台。'
|
|
394
422
|
return `尚未注册 ${protocol.scheme ?? 'dsh-notice'}:,点击通知只会把 Harness 窗口切到前台。`
|
|
395
423
|
})()
|
|
@@ -434,7 +462,7 @@ window.__ModuleLoader__.load({
|
|
|
434
462
|
}),
|
|
435
463
|
React.createElement(NoticeSwitchRow, {
|
|
436
464
|
title: '点击时把 Harness 窗口切到前台',
|
|
437
|
-
description: '
|
|
465
|
+
description: '开启后点击通知会尽量把窗口从最小化、被遮挡或收进托盘的状态恢复并切到前台;关闭则只切换对话,不改变窗口的前后顺序。',
|
|
438
466
|
checked: settings.focusOnClick === true,
|
|
439
467
|
disabled,
|
|
440
468
|
onChange: (next) => void send({ op: 'patch', patch: { focusOnClick: next } }),
|
|
@@ -827,6 +855,7 @@ window.__ModuleLoader__.load({
|
|
|
827
855
|
document.head.appendChild(tag)
|
|
828
856
|
}
|
|
829
857
|
|
|
858
|
+
|
|
830
859
|
ensureStyles()
|
|
831
860
|
|
|
832
861
|
// The notice page's stylesheet follows the same one-tag rule; calling it in
|
|
@@ -1871,11 +1900,16 @@ window.__ModuleLoader__.load({
|
|
|
1871
1900
|
const bodyBlocks = (() => {
|
|
1872
1901
|
if (state.status === 'error') {
|
|
1873
1902
|
const unauthorized = state.message.includes('401')
|
|
1903
|
+
const unavailable = state.message.includes('503')
|
|
1874
1904
|
const unrouted = state.message.includes('404')
|
|
1875
1905
|
/**
|
|
1876
|
-
* The
|
|
1906
|
+
* The four failures need different advice:
|
|
1877
1907
|
* - 401: this document never completed the token exchange, so the
|
|
1878
1908
|
* connection layer has no authority to admit the request.
|
|
1909
|
+
* - 503: the Host has no usable authentication service, so the route
|
|
1910
|
+
* refused the request instead of accepting an unauthenticated
|
|
1911
|
+
* change. Restarting is the ordinary fix; the route stays shut until
|
|
1912
|
+
* the connection service is mounted.
|
|
1879
1913
|
* - 404: the request reached the Host and nothing claimed the path.
|
|
1880
1914
|
* The browser half hot-reloads from the served bundle, the host half
|
|
1881
1915
|
* does not, so a stale Host process is the ordinary cause.
|
|
@@ -1883,14 +1917,16 @@ window.__ModuleLoader__.load({
|
|
|
1883
1917
|
*/
|
|
1884
1918
|
const diagnosis = unauthorized
|
|
1885
1919
|
? '这个页面没有带授权的会话凭据。请用 `dsh` 启动时打印的带 token 的地址重新打开 GUI(它会写入 cookie,然后跳转到干净地址)。'
|
|
1886
|
-
:
|
|
1887
|
-
? '
|
|
1888
|
-
|
|
1889
|
-
'
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
'
|
|
1920
|
+
: unavailable
|
|
1921
|
+
? '宿主进程没有可用的认证服务,接口按安全默认拒绝了这次请求(503)。重启 dsh 通常即可恢复;如果一直出现,说明这个宿主没有挂载 connection 认证服务,这个接口在它上面不会放行任何读写。'
|
|
1922
|
+
: unrouted
|
|
1923
|
+
? '请求到达了 DSH 宿主进程,但宿主里没有注册 ' +
|
|
1924
|
+
API_PATH +
|
|
1925
|
+
' 这条路由——通常意味着宿主进程还在跑旧版插件代码。浏览器半会随 bundle 热更新,宿主半不会:重启 dsh 即可。' +
|
|
1926
|
+
'重启后终端会出现 [dsh-workspace-plus] host half loaded 一行;若没有,请到 设置 → 插件 查看该插件行的状态。'
|
|
1927
|
+
: '这个面板通过 ' +
|
|
1928
|
+
API_PATH +
|
|
1929
|
+
' 与宿主进程通信,而当前页面没有可用的 HTTP 通道(例如页面不是 dsh 提供的、或宿主进程还没起来)。'
|
|
1894
1930
|
return [
|
|
1895
1931
|
noticeBlock(
|
|
1896
1932
|
'error',
|
package/lib/index.js
CHANGED
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
* agent sees the mapping without spending a tool call
|
|
13
13
|
* 3. a `workspace_dirs` Tool the agent uses to inspect or edit the mapping
|
|
14
14
|
* when the user asks in natural language
|
|
15
|
-
* 4. a small JSON HTTP route the browser half uses to manage the same store
|
|
15
|
+
* 4. a small JSON HTTP route the browser half uses to manage the same store —
|
|
16
|
+
* every request must pass the Host's `connection.admit` check, and the
|
|
17
|
+
* route refuses (503) when that service is unavailable
|
|
16
18
|
*
|
|
17
19
|
* The whole point is routing by label instead of by guesswork: the injected
|
|
18
20
|
* table gives absolute paths, so a similar-looking directory name elsewhere on
|
|
@@ -362,6 +364,55 @@ export function validateLabel(label) {
|
|
|
362
364
|
return { ok: true, label: text }
|
|
363
365
|
}
|
|
364
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Run the Host's browser trust check for one plugin request.
|
|
369
|
+
*
|
|
370
|
+
* `connection.admit(req)` is the only authority a plugin route has: the Host
|
|
371
|
+
* fence plus the GUI session cookie. This route both publishes the mapping and
|
|
372
|
+
* lets the caller rewrite it, and whatever it stores is rendered into every
|
|
373
|
+
* request's model context — so it fails closed when that authority is absent
|
|
374
|
+
* instead of falling through to "admitted":
|
|
375
|
+
*
|
|
376
|
+
* - no `connection` service mounted, or a service without `admit` — the host
|
|
377
|
+
* composition has no auth layer (or its API drifted): refuse with 503;
|
|
378
|
+
* - `admit` throwing, rejecting, or failing to answer: the decision is
|
|
379
|
+
* unavailable, so refuse with 503;
|
|
380
|
+
* - a rejection from `admit`: pass its 401 through, everything else is 403.
|
|
381
|
+
*
|
|
382
|
+
* The result is awaited, so a synchronous or an asynchronous `admit` is honored
|
|
383
|
+
* the same way — but only an answer that actually carries no rejection counts
|
|
384
|
+
* as an admission.
|
|
385
|
+
*
|
|
386
|
+
* The notice sub-feature deliberately grants a loopback exemption. This route
|
|
387
|
+
* does not: an unauthenticated writer could otherwise plant arbitrary text in
|
|
388
|
+
* the agent's prompt through a directory note.
|
|
389
|
+
*
|
|
390
|
+
* @param connection - the resolved `connection` service, or undefined.
|
|
391
|
+
* @param req - incoming request.
|
|
392
|
+
* @returns a promise of `{ ok: true }` or `{ ok: false, status, message }`.
|
|
393
|
+
*/
|
|
394
|
+
export async function checkAdmission(connection, req) {
|
|
395
|
+
const unavailable = (message) => ({ ok: false, status: 503, message })
|
|
396
|
+
if (connection === undefined || connection === null) {
|
|
397
|
+
return unavailable('host authentication service is not mounted; refusing to answer')
|
|
398
|
+
}
|
|
399
|
+
if (typeof connection.admit !== 'function') {
|
|
400
|
+
return unavailable('host authentication service has no admit(); refusing to answer')
|
|
401
|
+
}
|
|
402
|
+
let admission
|
|
403
|
+
try {
|
|
404
|
+
admission = await connection.admit(req)
|
|
405
|
+
} catch (error) {
|
|
406
|
+
return unavailable(`host authentication service failed: ${String(error?.message ?? error)}`)
|
|
407
|
+
}
|
|
408
|
+
const rejection = admission !== null && typeof admission === 'object' ? admission.rejection : undefined
|
|
409
|
+
if (rejection !== undefined) {
|
|
410
|
+
const status = rejection === 401 ? 401 : 403
|
|
411
|
+
return { ok: false, status, message: status === 401 ? 'unauthorized' : 'forbidden' }
|
|
412
|
+
}
|
|
413
|
+
return { ok: true }
|
|
414
|
+
}
|
|
415
|
+
|
|
365
416
|
/**
|
|
366
417
|
* Apply one store operation. Every branch validates first and returns a
|
|
367
418
|
* discriminated result, so both the Tool and the HTTP route share one behavior
|
|
@@ -709,11 +760,17 @@ export function apply(ctx) {
|
|
|
709
760
|
kind: 'exact',
|
|
710
761
|
path: API_PATH,
|
|
711
762
|
handler: async (req, res) => {
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
763
|
+
// Fail closed. A request that cannot be admitted by the Host's
|
|
764
|
+
// connection service gets no answer at all, read or write: the
|
|
765
|
+
// mapping this route returns — and the notes it stores — become
|
|
766
|
+
// model context, so an unauthenticated caller must never reach it.
|
|
767
|
+
const admission = await checkAdmission(ctx.get('connection'), req)
|
|
768
|
+
if (!admission.ok) {
|
|
769
|
+
res.writeHead(admission.status, {
|
|
770
|
+
'content-type': 'application/json; charset=utf-8',
|
|
771
|
+
'cache-control': 'no-store',
|
|
772
|
+
})
|
|
773
|
+
res.end(JSON.stringify({ ok: false, message: admission.message }))
|
|
717
774
|
return
|
|
718
775
|
}
|
|
719
776
|
const send = (status, payload) => {
|