@liuzw/dsh-web-ui-custom 0.0.0-stage → 1.0.0
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/LICENSE +21 -0
- package/README.md +50 -2
- package/assets/apple-touch-icon.png +0 -0
- package/assets/icon-192.png +0 -0
- package/assets/icon-512.png +0 -0
- package/assets/icon-maskable-512.png +0 -0
- package/client.js +144 -0
- package/cordis.patch.yml +4 -0
- package/index.js +86 -0
- package/package.json +53 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 liuzwZYW
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,51 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @liuzw/dsh-web-ui-custom
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
给 **DeepSeek Harness Web** 的两项界面定制,做成插件——升级 DSH 不会再被覆盖:
|
|
4
|
+
|
|
5
|
+
1. **PWA 应用图标**:深色圆角底 + 白色鲸鱼,替换上游白底灰鲸那一套;
|
|
6
|
+
2. **侧栏设置齿轮**:账户行右侧(收起时在头像上方)加一个齿轮,单击直接打开设置面板。
|
|
7
|
+
|
|
8
|
+
> 之前的做法是直接改 DSH 安装目录里的前端 `dist` 和客户端 bundle,每次升级都会被还原。这个包用 DSH 的插件接口重做了一遍,安装、卸载、升级互不影响。
|
|
9
|
+
|
|
10
|
+
## 安装
|
|
11
|
+
|
|
12
|
+
**从 npm(推荐)**——在 DSH 的插件市场里搜 `@liuzw/dsh-web-ui-custom` 安装,或手动:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
cd ~/.dsh/profiles/web
|
|
16
|
+
pnpm add @liuzw/dsh-web-ui-custom
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
然后把包名加进 `package.json` 的 `dsh.profile.bundles`:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
"bundles": ["...", "@liuzw/dsh-web-ui-custom"]
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
重启 `dsh web` 生效。
|
|
26
|
+
|
|
27
|
+
**源码方式**(本仓库 clone 后):
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
./install.sh # 写入 file: 依赖 + bundle,并 pnpm install --offline
|
|
31
|
+
./uninstall.sh # 撤销
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
两条脚本都要求先停掉 `dsh web/serve`,失败会自动回滚 profile 配置。
|
|
35
|
+
|
|
36
|
+
## 实现
|
|
37
|
+
|
|
38
|
+
| 部分 | 做法 |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| 图标(宿主 `index.js`) | `webServer.register` 以 **exact route** 接住 `/icon-192.png`、`/icon-512.png`、`/icon-maskable-512.png`、`/apple-touch-icon.png`,返回包内 `assets/` 的 PNG。上游 `manifest.webmanifest` 与 `index.html` 引用的正是这些路径,因此前端 `dist` 一个字节都不用改。 |
|
|
41
|
+
| 齿轮(浏览器 `client.js`) | 在侧栏账户行追加一个按钮;点击时沿 React fiber 向上找到启动器组件的 `openSettings` 属性并调用。React 重渲染会移除外来子节点,用 MutationObserver 补回。 |
|
|
42
|
+
|
|
43
|
+
## 行为与边界
|
|
44
|
+
|
|
45
|
+
- 图标路由带 `cache-control: no-cache`,DSH 静态资源本身也是 `no-cache`,刷新即可看到新图标;已安装的 PWA 若仍显示旧图标,是系统缓存,卸载重装 PWA 即可。
|
|
46
|
+
- 齿轮依赖 `_settingsArea` / `_triggerRow` 两个 CSS Module 后缀和启动器组件上的 `openSettings`。DSH 改这两处时齿轮会静默不显示(图标不受影响),届时只需调整 `client.js` 里的 `findRow`。
|
|
47
|
+
- 宿主以 exact match 匹配图标路径,DSH 若改文件名,需要同步更新 `index.js` 的 `ICONS` 表。
|
|
48
|
+
|
|
49
|
+
## 许可
|
|
50
|
+
|
|
51
|
+
MIT
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/client.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client half of the local Web UI kit: a settings gear beside the sidebar
|
|
3
|
+
* account launcher.
|
|
4
|
+
*
|
|
5
|
+
* The shipped account row opens a menu; the gear is a direct shortcut into the
|
|
6
|
+
* settings dialog. It is mounted as a sibling of the row's anchor and marks the
|
|
7
|
+
* row with `data-dsh-wuc-row`, so the stylesheet below can lay the pair out
|
|
8
|
+
* on one line when the sidebar is wide and stack the gear above the avatar in
|
|
9
|
+
* the 56px rail.
|
|
10
|
+
*
|
|
11
|
+
* How the settings dialog is opened: the row is the shipped launcher, and the
|
|
12
|
+
* settings shell hands that component an `openSettings` prop. React keeps the
|
|
13
|
+
* fiber for every host element under `__reactFiber$…`, so walking up from the
|
|
14
|
+
* row's own node reaches the component fiber that carries the callback. This
|
|
15
|
+
* uses no private module import and degrades quietly: when the prop is not
|
|
16
|
+
* found the gear is removed instead of becoming a dead button.
|
|
17
|
+
*
|
|
18
|
+
* React re-renders replace foreign children, so a MutationObserver re-mounts
|
|
19
|
+
* the gear after any reconciliation that drops it.
|
|
20
|
+
*
|
|
21
|
+
* @module dsh-web-ui-custom
|
|
22
|
+
*/
|
|
23
|
+
window.__ModuleLoader__.load({
|
|
24
|
+
id: '@liuzw/dsh-web-ui-custom',
|
|
25
|
+
factory() {
|
|
26
|
+
/** Marks the account row our gear belongs to. */
|
|
27
|
+
const ROW_ATTR = 'data-dsh-wuc-row'
|
|
28
|
+
/** Marks the gear button itself. */
|
|
29
|
+
const GEAR_ATTR = 'data-dsh-wuc-gear'
|
|
30
|
+
|
|
31
|
+
const LABEL = (typeof navigator !== 'undefined' && /^zh/i.test(navigator.language ?? '')) ? '设置' : 'Settings'
|
|
32
|
+
|
|
33
|
+
/** 16px outline gear, drawn with `currentColor` so it follows the row. */
|
|
34
|
+
const GEAR_SVG = [
|
|
35
|
+
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">',
|
|
36
|
+
'<circle cx="12" cy="12" r="3.2"/>',
|
|
37
|
+
'<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>',
|
|
38
|
+
'</svg>',
|
|
39
|
+
].join('')
|
|
40
|
+
|
|
41
|
+
const CSS = [
|
|
42
|
+
/* One line: anchor takes the space, gear keeps its box. */
|
|
43
|
+
`[${ROW_ATTR}]{display:flex;align-items:center;gap:4px}`,
|
|
44
|
+
`[${ROW_ATTR}]>:first-child{flex:1;min-width:0}`,
|
|
45
|
+
`[${GEAR_ATTR}]{flex:none;width:36px;height:36px;display:inline-flex;align-items:center;justify-content:center;padding:0;border:0;border-radius:8px;background:transparent;color:inherit;cursor:pointer}`,
|
|
46
|
+
`[${GEAR_ATTR}]:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.06))}`,
|
|
47
|
+
`[${GEAR_ATTR}]:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4f6ef7);outline-offset:2px}`,
|
|
48
|
+
/* Rail: stack the gear above the avatar, keeping the icon column centred. */
|
|
49
|
+
`[class*="_collapsed"] [${ROW_ATTR}]{flex-direction:column;gap:8px}`,
|
|
50
|
+
`[class*="_collapsed"] [${GEAR_ATTR}]{order:-1}`,
|
|
51
|
+
].join('')
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The shipped account launcher row.
|
|
55
|
+
* @returns the row element, or `undefined` while the sidebar is not mounted.
|
|
56
|
+
*/
|
|
57
|
+
function findRow() {
|
|
58
|
+
const area = document.querySelector('[class*="_settingsArea"]')
|
|
59
|
+
if (area === null) return undefined
|
|
60
|
+
const trigger = area.querySelector('[class*="_triggerRow"]')
|
|
61
|
+
const row = (trigger ?? area).firstElementChild
|
|
62
|
+
return row instanceof HTMLElement ? row : undefined
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Walk up the React fiber chain looking for the launcher's `openSettings`.
|
|
67
|
+
* @param node - a host element inside the launcher's subtree.
|
|
68
|
+
* @returns the callback the settings shell handed the launcher, if any.
|
|
69
|
+
*/
|
|
70
|
+
function resolveOpenSettings(node) {
|
|
71
|
+
const key = Object.keys(node).find((name) => name.startsWith('__reactFiber$'))
|
|
72
|
+
if (key === undefined) return undefined
|
|
73
|
+
let fiber = node[key]
|
|
74
|
+
for (let depth = 0; fiber && depth < 64; depth += 1) {
|
|
75
|
+
const props = fiber.memoizedProps
|
|
76
|
+
if (props && typeof props.openSettings === 'function') return props.openSettings
|
|
77
|
+
fiber = fiber.return
|
|
78
|
+
}
|
|
79
|
+
return undefined
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Mount the gear into the account row when both the row and its settings
|
|
84
|
+
* callback are present.
|
|
85
|
+
* @param row - the launcher row.
|
|
86
|
+
*/
|
|
87
|
+
function mount(row) {
|
|
88
|
+
if (row.querySelector(`:scope > [${GEAR_ATTR}]`) !== null) return
|
|
89
|
+
if (typeof resolveOpenSettings(row) !== 'function') return
|
|
90
|
+
|
|
91
|
+
row.setAttribute(ROW_ATTR, '')
|
|
92
|
+
const gear = document.createElement('button')
|
|
93
|
+
gear.type = 'button'
|
|
94
|
+
gear.dataset.dshWucGear = ''
|
|
95
|
+
gear.setAttribute('aria-label', LABEL)
|
|
96
|
+
gear.title = LABEL
|
|
97
|
+
gear.innerHTML = GEAR_SVG
|
|
98
|
+
gear.addEventListener('click', (event) => {
|
|
99
|
+
event.preventDefault()
|
|
100
|
+
event.stopPropagation()
|
|
101
|
+
const open = resolveOpenSettings(row)
|
|
102
|
+
if (typeof open === 'function') open()
|
|
103
|
+
})
|
|
104
|
+
row.append(gear)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
/**
|
|
109
|
+
* Install the stylesheet, the gear, and the observer that keeps it there.
|
|
110
|
+
* @param ctx - browser plugin context, for effect ownership.
|
|
111
|
+
*/
|
|
112
|
+
apply(ctx) {
|
|
113
|
+
ctx.effect(() => {
|
|
114
|
+
const style = document.createElement('style')
|
|
115
|
+
style.dataset.plugin = '@liuzw/dsh-web-ui-custom'
|
|
116
|
+
style.textContent = CSS
|
|
117
|
+
document.head.append(style)
|
|
118
|
+
|
|
119
|
+
let scheduled = false
|
|
120
|
+
const sync = () => {
|
|
121
|
+
scheduled = false
|
|
122
|
+
const row = findRow()
|
|
123
|
+
if (row !== undefined) mount(row)
|
|
124
|
+
}
|
|
125
|
+
const schedule = () => {
|
|
126
|
+
if (scheduled) return
|
|
127
|
+
scheduled = true
|
|
128
|
+
requestAnimationFrame(sync)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
sync()
|
|
132
|
+
const observer = new MutationObserver(schedule)
|
|
133
|
+
observer.observe(document.body, { childList: true, subtree: true })
|
|
134
|
+
return () => {
|
|
135
|
+
observer.disconnect()
|
|
136
|
+
style.remove()
|
|
137
|
+
for (const gear of document.querySelectorAll(`[${GEAR_ATTR}]`)) gear.remove()
|
|
138
|
+
for (const row of document.querySelectorAll(`[${ROW_ATTR}]`)) row.removeAttribute(ROW_ATTR)
|
|
139
|
+
}
|
|
140
|
+
}, '@liuzw/dsh-web-ui-custom: sidebar settings gear')
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
})
|
package/cordis.patch.yml
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host half of the local Web UI kit.
|
|
3
|
+
*
|
|
4
|
+
* The shipped Web frontend serves its PWA icons from its own `dist/`, so an
|
|
5
|
+
* upgrade replaces any replaced image files. This row claims the four icon
|
|
6
|
+
* paths as exact routes instead and answers with the brand set kept inside
|
|
7
|
+
* this package, which leaves the frontend untouched and survives upgrades.
|
|
8
|
+
*
|
|
9
|
+
* The manifest itself is not claimed here: it keeps pointing at
|
|
10
|
+
* `icon-192.png`, `icon-512.png`, `icon-maskable-512.png`, and the built
|
|
11
|
+
* `index.html` still links `./apple-touch-icon.png`. Exact routes win over the
|
|
12
|
+
* static file of the same path, so both keep resolving to these bytes.
|
|
13
|
+
*
|
|
14
|
+
* @module dsh-web-ui-custom
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync } from 'node:fs'
|
|
17
|
+
import { fileURLToPath } from 'node:url'
|
|
18
|
+
import { dirname, join } from 'node:path'
|
|
19
|
+
|
|
20
|
+
/** Stable Cordis plugin name. */
|
|
21
|
+
export const name = 'web-ui-custom'
|
|
22
|
+
|
|
23
|
+
/** The web server is required before any icon route can be claimed. */
|
|
24
|
+
export const inject = ['webServer']
|
|
25
|
+
|
|
26
|
+
/** Icon files served from this package, keyed by the path the frontend links. */
|
|
27
|
+
const ICONS = {
|
|
28
|
+
'/icon-192.png': 'icon-192.png',
|
|
29
|
+
'/icon-512.png': 'icon-512.png',
|
|
30
|
+
'/icon-maskable-512.png': 'icon-maskable-512.png',
|
|
31
|
+
'/apple-touch-icon.png': 'apple-touch-icon.png',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const ASSET_DIR = join(dirname(fileURLToPath(import.meta.url)), 'assets')
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Read one bundled icon once.
|
|
38
|
+
* @param file - file name inside `assets/`.
|
|
39
|
+
* @returns the file bytes, or `undefined` when the package is incomplete.
|
|
40
|
+
*/
|
|
41
|
+
function readIcon(file) {
|
|
42
|
+
try {
|
|
43
|
+
return readFileSync(join(ASSET_DIR, file))
|
|
44
|
+
} catch {
|
|
45
|
+
return undefined
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Serve the brand icons over the shipped asset paths.
|
|
51
|
+
* @param ctx - plugin context carrying the webServer service.
|
|
52
|
+
*/
|
|
53
|
+
export function apply(ctx) {
|
|
54
|
+
ctx.effect(() => {
|
|
55
|
+
const disposers = []
|
|
56
|
+
for (const [route, file] of Object.entries(ICONS)) {
|
|
57
|
+
const body = readIcon(file)
|
|
58
|
+
if (body === undefined) {
|
|
59
|
+
ctx.logger?.warn?.(`web-ui-custom: missing asset ${file}, keeping the shipped icon for ${route}`)
|
|
60
|
+
continue
|
|
61
|
+
}
|
|
62
|
+
disposers.push(ctx.webServer.register({
|
|
63
|
+
kind: 'exact',
|
|
64
|
+
path: route,
|
|
65
|
+
handler: (request, response) => {
|
|
66
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
67
|
+
response.writeHead(405, { allow: 'GET, HEAD' })
|
|
68
|
+
response.end()
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
response.writeHead(200, {
|
|
72
|
+
'content-type': 'image/png',
|
|
73
|
+
'content-length': body.byteLength,
|
|
74
|
+
/* Revalidate so a replaced icon reaches an installed app. */
|
|
75
|
+
'cache-control': 'no-cache',
|
|
76
|
+
})
|
|
77
|
+
if (request.method === 'HEAD') response.end()
|
|
78
|
+
else response.end(body)
|
|
79
|
+
},
|
|
80
|
+
}))
|
|
81
|
+
}
|
|
82
|
+
return () => {
|
|
83
|
+
for (const dispose of disposers) dispose()
|
|
84
|
+
}
|
|
85
|
+
}, 'web-ui-custom: icon routes')
|
|
86
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liuzw/dsh-web-ui-custom",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Custom PWA icons and a sidebar settings gear for the DeepSeek Harness Web app. 为 DSH Web 换上自制 PWA 图标,并在侧栏账户行加一个直开设置的齿轮。",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./index.js",
|
|
10
|
+
"./client": "./client.js",
|
|
11
|
+
"./package.json": "./package.json"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"index.js",
|
|
15
|
+
"client.js",
|
|
16
|
+
"cordis.patch.yml",
|
|
17
|
+
"assets/*.png"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"deepseek-harness",
|
|
21
|
+
"dsh",
|
|
22
|
+
"dsh-plugin",
|
|
23
|
+
"pwa",
|
|
24
|
+
"icons",
|
|
25
|
+
"sidebar",
|
|
26
|
+
"settings",
|
|
27
|
+
"web"
|
|
28
|
+
],
|
|
29
|
+
"author": "liuzwZYW",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/liuzwZYW/dsh-web-ui-custom.git"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/liuzwZYW/dsh-web-ui-custom#readme",
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/liuzwZYW/dsh-web-ui-custom/issues"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@deepseek-ai/dsh": ">=0.2.0 <0.3.0-0"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=22"
|
|
43
|
+
},
|
|
44
|
+
"dsh": {
|
|
45
|
+
"bundle": {
|
|
46
|
+
"patch": "./cordis.patch.yml"
|
|
47
|
+
},
|
|
48
|
+
"client": {
|
|
49
|
+
"platform": "web",
|
|
50
|
+
"immediately": true
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
}
|
|
6
56
|
}
|