dsh-mobile 0.1.0-alpha.2 → 0.1.0-alpha.21
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.en.md +123 -0
- package/README.md +86 -116
- package/SECURITY.md +5 -3
- package/assets/brand/app-icon-master.png +0 -0
- package/assets/brand/repository-hero.png +0 -0
- package/cordis.patch.yml +10 -0
- package/lib/cli.js +317 -80
- package/lib/client.js +623 -206
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +31 -1
- package/lib/index.mjs +716 -23
- package/lib/mobile-layout.js +227 -0
- package/lib/mobile-layout.js.map +1 -0
- package/package.json +19 -13
- package/README.zh.md +0 -206
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@deepseek-ai/dsh-client-ui-layout",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
//#region src/mobile-layout.ts
|
|
9
|
+
var MobileLayoutController = class {
|
|
10
|
+
snapshot = Object.freeze({
|
|
11
|
+
sidebarOpen: false,
|
|
12
|
+
detailsOpen: false
|
|
13
|
+
});
|
|
14
|
+
listeners = /* @__PURE__ */ new Set();
|
|
15
|
+
subscribe = (listener) => {
|
|
16
|
+
this.listeners.add(listener);
|
|
17
|
+
return () => {
|
|
18
|
+
this.listeners.delete(listener);
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
getSnapshot = () => this.snapshot;
|
|
22
|
+
toggleSidebar() {
|
|
23
|
+
this.update({ sidebarOpen: !this.snapshot.sidebarOpen });
|
|
24
|
+
}
|
|
25
|
+
openDetails() {
|
|
26
|
+
this.update({ detailsOpen: true });
|
|
27
|
+
}
|
|
28
|
+
closeDetails() {
|
|
29
|
+
this.update({ detailsOpen: false });
|
|
30
|
+
}
|
|
31
|
+
closeSidebar() {
|
|
32
|
+
this.update({ sidebarOpen: false });
|
|
33
|
+
}
|
|
34
|
+
update(next) {
|
|
35
|
+
const snapshot = Object.freeze({
|
|
36
|
+
...this.snapshot,
|
|
37
|
+
...next
|
|
38
|
+
});
|
|
39
|
+
if (snapshot.sidebarOpen === this.snapshot.sidebarOpen && snapshot.detailsOpen === this.snapshot.detailsOpen) return;
|
|
40
|
+
this.snapshot = snapshot;
|
|
41
|
+
for (const listener of this.listeners) listener();
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var ThemePresenter = class {
|
|
45
|
+
appliedTokens = [];
|
|
46
|
+
meta = document.createElement("meta");
|
|
47
|
+
constructor() {
|
|
48
|
+
this.meta.name = "theme-color";
|
|
49
|
+
}
|
|
50
|
+
apply(snapshot) {
|
|
51
|
+
const scheme = snapshot.active.colorScheme;
|
|
52
|
+
document.documentElement.style.colorScheme = scheme;
|
|
53
|
+
document.body.toggleAttribute("data-ds-dark-theme", scheme === "dark");
|
|
54
|
+
for (const name of this.appliedTokens) document.body.style.removeProperty(name);
|
|
55
|
+
this.appliedTokens = [];
|
|
56
|
+
for (const [name, value] of Object.entries(snapshot.active.tokens)) {
|
|
57
|
+
document.body.style.setProperty(name, value);
|
|
58
|
+
this.appliedTokens.push(name);
|
|
59
|
+
}
|
|
60
|
+
this.meta.content = getComputedStyle(document.body).backgroundColor;
|
|
61
|
+
if (!this.meta.isConnected) document.head.append(this.meta);
|
|
62
|
+
}
|
|
63
|
+
dispose() {
|
|
64
|
+
document.documentElement.style.removeProperty("color-scheme");
|
|
65
|
+
document.body.removeAttribute("data-ds-dark-theme");
|
|
66
|
+
for (const name of this.appliedTokens) document.body.style.removeProperty(name);
|
|
67
|
+
this.meta.remove();
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const MOBILE_LAYOUT_STYLES = `
|
|
71
|
+
html,body,#root{width:100%;height:100%;overflow:hidden}
|
|
72
|
+
.dshm-shell{position:relative;display:grid;width:100%;height:100dvh;min-width:0;overflow:hidden;background:var(--dsw-alias-bg-base,#fff)}
|
|
73
|
+
.dshm-main{grid-area:1/1;min-width:0;min-height:0;overflow:hidden}
|
|
74
|
+
.dshm-main>*,.dshm-main>*>*{min-width:0}
|
|
75
|
+
.dshm-menu{position:fixed;z-index:62;top:max(4px,env(safe-area-inset-top));left:4px;display:grid;place-items:center;width:44px;height:44px;padding:0;border:0;border-radius:12px;background:transparent;color:var(--dsw-alias-label-primary,#171a21);cursor:pointer;-webkit-tap-highlight-color:transparent}
|
|
76
|
+
.dshm-menu:hover,.dshm-menu:active{background:var(--dsw-alias-interactive-bg-hover,#f1f3f6)}
|
|
77
|
+
.dshm-menu:focus-visible,.dshm-close:focus-visible{outline:2px solid var(--dsw-alias-border-focus,#2563eb);outline-offset:2px}
|
|
78
|
+
.dshm-menu svg,.dshm-close svg{width:22px;height:22px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
79
|
+
.dshm-drawer{position:fixed;z-index:70;inset:0 auto 0 0;box-sizing:border-box;width:min(88vw,340px);max-width:100%;padding-top:env(safe-area-inset-top);overflow:hidden;background:var(--dsw-alias-bg-layer-1,#f8fafc);box-shadow:18px 0 46px rgb(15 23 42 / 18%);left:-104%;transition:left 190ms cubic-bezier(.22,1,.36,1)}
|
|
80
|
+
.dshm-drawer[data-open=true]{left:0}
|
|
81
|
+
.dshm-drawer[data-open=false]{pointer-events:none;visibility:hidden;transition:left 190ms cubic-bezier(.22,1,.36,1),visibility 0s linear 190ms}
|
|
82
|
+
.dshm-drawer>*{width:100%!important;height:100%!important}
|
|
83
|
+
.dshm-details{position:fixed;z-index:80;inset:0 0 0 auto;box-sizing:border-box;width:min(94vw,460px);max-width:100%;padding-top:env(safe-area-inset-top);overflow:hidden;background:var(--dsw-alias-bg-layer-1,#fff);box-shadow:-18px 0 46px rgb(15 23 42 / 18%);transform:translateX(104%);transition:transform 190ms cubic-bezier(.22,1,.36,1)}
|
|
84
|
+
.dshm-details[data-open=true]{transform:translateX(0)}
|
|
85
|
+
.dshm-details[data-open=false]{pointer-events:none;visibility:hidden;transition:transform 190ms cubic-bezier(.22,1,.36,1),visibility 0s linear 190ms}
|
|
86
|
+
.dshm-scrim{position:fixed;z-index:65;inset:0;border:0;background:rgb(15 23 42 / 40%);opacity:0;pointer-events:none;transition:opacity 180ms ease-out}
|
|
87
|
+
.dshm-scrim[data-open=true]{opacity:1;pointer-events:auto}
|
|
88
|
+
.dshm-overlay{position:fixed;z-index:90;inset:0;pointer-events:none}.dshm-overlay>*{pointer-events:auto}
|
|
89
|
+
.dshm-shell header{min-width:0;padding-left:52px}
|
|
90
|
+
.dshm-shell textarea{font-size:16px}
|
|
91
|
+
.dshm-shell table{display:block;max-width:100%;overflow-x:auto}
|
|
92
|
+
.dshm-shell pre{max-width:100%;overflow-x:auto}
|
|
93
|
+
.dshm-shell img,.dshm-shell video,.dshm-shell canvas,.dshm-shell svg{max-width:100%}
|
|
94
|
+
.dshm-shell [data-disclosure-row]{min-width:0;max-width:100%}
|
|
95
|
+
.dshm-shell [data-disclosure-row]>*{min-width:0;overflow-wrap:anywhere}
|
|
96
|
+
.dshm-shell [data-context-fields]>*{min-width:0}
|
|
97
|
+
.dshm-shell [class*="_body"]{max-width:100%;overflow-wrap:anywhere}
|
|
98
|
+
@media(max-width:420px){.dshm-shell [data-context-fields]>*{display:grid;grid-template-columns:1fr!important;gap:4px}.dshm-shell [class*="_ioSection"]{grid-template-columns:1fr!important}}
|
|
99
|
+
@media(prefers-reduced-motion:reduce){.dshm-drawer,.dshm-details,.dshm-scrim{transition:none!important}}
|
|
100
|
+
`;
|
|
101
|
+
function icon(paths) {
|
|
102
|
+
return (0, react.createElement)("svg", {
|
|
103
|
+
"aria-hidden": true,
|
|
104
|
+
viewBox: "0 0 24 24"
|
|
105
|
+
}, ...paths.map((path, index) => (0, react.createElement)("path", {
|
|
106
|
+
d: path,
|
|
107
|
+
key: index
|
|
108
|
+
})));
|
|
109
|
+
}
|
|
110
|
+
function MobileAppFrame(props) {
|
|
111
|
+
const state = (0, react.useSyncExternalStore)(props.controller.subscribe, props.controller.getSnapshot);
|
|
112
|
+
const hasSession = props.useSessions((session) => {
|
|
113
|
+
const current = session.current;
|
|
114
|
+
return current !== void 0 && session.byId[current]?.blank === false;
|
|
115
|
+
});
|
|
116
|
+
(0, react.useEffect)(() => {
|
|
117
|
+
if (!hasSession) props.controller.closeDetails();
|
|
118
|
+
}, [hasSession, props.controller]);
|
|
119
|
+
const closeDrawerAfterAction = (event) => {
|
|
120
|
+
if (!(event.target instanceof Element)) return;
|
|
121
|
+
const action = event.target.closest("button,a,[role=\"button\"],[role=\"option\"],[role=\"treeitem\"]");
|
|
122
|
+
if (action === null || action.closest("[data-dshm-keep-open],[aria-haspopup=\"dialog\"]") !== null) return;
|
|
123
|
+
window.setTimeout(() => {
|
|
124
|
+
props.controller.closeSidebar();
|
|
125
|
+
}, 80);
|
|
126
|
+
};
|
|
127
|
+
return (0, react.createElement)("div", { className: "dshm-shell" }, (0, react.createElement)("main", { className: "dshm-main" }, props.renderSlot("conversation", {})), (0, react.createElement)("button", {
|
|
128
|
+
"aria-expanded": state.sidebarOpen,
|
|
129
|
+
"aria-label": state.sidebarOpen ? "关闭导航" : "打开导航",
|
|
130
|
+
className: "dshm-menu",
|
|
131
|
+
onClick: () => {
|
|
132
|
+
props.controller.toggleSidebar();
|
|
133
|
+
},
|
|
134
|
+
type: "button"
|
|
135
|
+
}, icon([
|
|
136
|
+
"M4 6h16",
|
|
137
|
+
"M4 12h16",
|
|
138
|
+
"M4 18h16"
|
|
139
|
+
])), (0, react.createElement)("button", {
|
|
140
|
+
"aria-label": "关闭浮层",
|
|
141
|
+
className: "dshm-scrim",
|
|
142
|
+
"data-open": state.sidebarOpen || state.detailsOpen,
|
|
143
|
+
onClick: () => {
|
|
144
|
+
state.detailsOpen ? props.controller.closeDetails() : props.controller.closeSidebar();
|
|
145
|
+
},
|
|
146
|
+
tabIndex: state.sidebarOpen || state.detailsOpen ? 0 : -1,
|
|
147
|
+
type: "button"
|
|
148
|
+
}), (0, react.createElement)("aside", {
|
|
149
|
+
"aria-hidden": !state.sidebarOpen,
|
|
150
|
+
className: "dshm-drawer",
|
|
151
|
+
"data-open": state.sidebarOpen,
|
|
152
|
+
onClickCapture: closeDrawerAfterAction,
|
|
153
|
+
...state.sidebarOpen ? {} : { inert: "" }
|
|
154
|
+
}, props.renderSlot("sidebar", {
|
|
155
|
+
collapsed: false,
|
|
156
|
+
width: 340
|
|
157
|
+
})), (0, react.createElement)("aside", {
|
|
158
|
+
"aria-hidden": !state.detailsOpen,
|
|
159
|
+
className: "dshm-details",
|
|
160
|
+
"data-open": state.detailsOpen,
|
|
161
|
+
...state.detailsOpen ? {} : { inert: "" }
|
|
162
|
+
}, hasSession ? props.renderSlot("details", {}) : void 0), (0, react.createElement)("div", {
|
|
163
|
+
className: "dshm-overlay",
|
|
164
|
+
"data-shell-overlay": true
|
|
165
|
+
}, props.renderSlot("shell.overlay", {})));
|
|
166
|
+
}
|
|
167
|
+
/** Replace the desktop layout module on the authenticated mobile surface. */
|
|
168
|
+
function apply(ctx) {
|
|
169
|
+
const controller = new MobileLayoutController();
|
|
170
|
+
ctx.effect(() => {
|
|
171
|
+
const style = document.createElement("style");
|
|
172
|
+
style.dataset.plugin = "dsh-mobile-layout";
|
|
173
|
+
style.textContent = MOBILE_LAYOUT_STYLES;
|
|
174
|
+
document.head.append(style);
|
|
175
|
+
const disposeService = ctx.reflect.provide("layout", controller);
|
|
176
|
+
const disposeRoot = ctx.slots.register({
|
|
177
|
+
name: "root",
|
|
178
|
+
children: {
|
|
179
|
+
sidebar: {
|
|
180
|
+
kind: "single",
|
|
181
|
+
scope: "root"
|
|
182
|
+
},
|
|
183
|
+
conversation: {
|
|
184
|
+
kind: "single",
|
|
185
|
+
scope: "session-maybe"
|
|
186
|
+
},
|
|
187
|
+
details: {
|
|
188
|
+
kind: "single",
|
|
189
|
+
scope: "session"
|
|
190
|
+
},
|
|
191
|
+
"shell.overlay": {
|
|
192
|
+
kind: "list",
|
|
193
|
+
scope: "root"
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}, (props) => (0, react.createElement)(MobileAppFrame, {
|
|
197
|
+
...props,
|
|
198
|
+
controller
|
|
199
|
+
}));
|
|
200
|
+
return () => {
|
|
201
|
+
disposeRoot();
|
|
202
|
+
disposeService();
|
|
203
|
+
style.remove();
|
|
204
|
+
};
|
|
205
|
+
}, "dsh-mobile: dedicated root layout");
|
|
206
|
+
ctx.effect(() => {
|
|
207
|
+
const presenter = new ThemePresenter();
|
|
208
|
+
presenter.apply(ctx.theme.getTheme());
|
|
209
|
+
const off = ctx.on("theme/change", (snapshot) => {
|
|
210
|
+
presenter.apply(snapshot);
|
|
211
|
+
});
|
|
212
|
+
return () => {
|
|
213
|
+
off();
|
|
214
|
+
presenter.dispose();
|
|
215
|
+
};
|
|
216
|
+
}, "dsh-mobile: theme presenter");
|
|
217
|
+
}
|
|
218
|
+
/** Preserve the official layout module's dependency ordering. */
|
|
219
|
+
const inject = ["slots", "theme"];
|
|
220
|
+
//#endregion
|
|
221
|
+
exports.apply = apply;
|
|
222
|
+
exports.inject = inject;
|
|
223
|
+
return module.exports;
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
//# sourceMappingURL=mobile-layout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mobile-layout.js","names":["createElement","useSyncExternalStore"],"sources":["../src/mobile-layout.ts"],"sourcesContent":["import { createElement, useEffect, useSyncExternalStore } from 'react'\nimport type { ReactNode } from 'react'\n\ninterface SessionState {\n readonly current?: string\n readonly byId: Readonly<Record<string, { readonly blank?: boolean } | undefined>>\n}\n\ninterface MobileRootProps {\n readonly renderSlot: (name: string, owner: Record<string, unknown>) => ReactNode\n readonly useSessions: <T>(selector: (state: SessionState) => T) => T\n}\n\ninterface MobileClientContext {\n readonly effect: (effect: () => void | (() => void), label?: string) => void\n readonly on: (event: string, listener: (value: ThemeSnapshot) => void) => () => void\n readonly reflect: { provide: (name: string, value: unknown) => () => void | Promise<void> }\n readonly slots: {\n register: (options: Record<string, unknown>, component: (props: MobileRootProps) => ReactNode) => () => void\n }\n readonly theme: { getTheme: () => ThemeSnapshot }\n}\n\ninterface ThemeSnapshot {\n readonly active: {\n readonly colorScheme: 'dark' | 'light'\n readonly tokens: Readonly<Record<string, string>>\n }\n}\n\ninterface LayoutSnapshot {\n readonly sidebarOpen: boolean\n readonly detailsOpen: boolean\n}\n\nclass MobileLayoutController {\n private snapshot: LayoutSnapshot = Object.freeze({ sidebarOpen: false, detailsOpen: false })\n private readonly listeners = new Set<() => void>()\n\n readonly subscribe = (listener: () => void): (() => void) => {\n this.listeners.add(listener)\n return () => { this.listeners.delete(listener) }\n }\n\n readonly getSnapshot = (): LayoutSnapshot => this.snapshot\n\n toggleSidebar(): void {\n this.update({ sidebarOpen: !this.snapshot.sidebarOpen })\n }\n\n openDetails(): void {\n this.update({ detailsOpen: true })\n }\n\n closeDetails(): void {\n this.update({ detailsOpen: false })\n }\n\n closeSidebar(): void {\n this.update({ sidebarOpen: false })\n }\n\n private update(next: Partial<LayoutSnapshot>): void {\n const snapshot = Object.freeze({ ...this.snapshot, ...next })\n if (snapshot.sidebarOpen === this.snapshot.sidebarOpen && snapshot.detailsOpen === this.snapshot.detailsOpen) return\n this.snapshot = snapshot\n for (const listener of this.listeners) listener()\n }\n}\n\nclass ThemePresenter {\n private appliedTokens: string[] = []\n private readonly meta = document.createElement('meta')\n\n constructor() {\n this.meta.name = 'theme-color'\n }\n\n apply(snapshot: ThemeSnapshot): void {\n const scheme = snapshot.active.colorScheme\n document.documentElement.style.colorScheme = scheme\n document.body.toggleAttribute('data-ds-dark-theme', scheme === 'dark')\n for (const name of this.appliedTokens) document.body.style.removeProperty(name)\n this.appliedTokens = []\n for (const [name, value] of Object.entries(snapshot.active.tokens)) {\n document.body.style.setProperty(name, value)\n this.appliedTokens.push(name)\n }\n this.meta.content = getComputedStyle(document.body).backgroundColor\n if (!this.meta.isConnected) document.head.append(this.meta)\n }\n\n dispose(): void {\n document.documentElement.style.removeProperty('color-scheme')\n document.body.removeAttribute('data-ds-dark-theme')\n for (const name of this.appliedTokens) document.body.style.removeProperty(name)\n this.meta.remove()\n }\n}\n\nconst MOBILE_LAYOUT_STYLES = `\nhtml,body,#root{width:100%;height:100%;overflow:hidden}\n.dshm-shell{position:relative;display:grid;width:100%;height:100dvh;min-width:0;overflow:hidden;background:var(--dsw-alias-bg-base,#fff)}\n.dshm-main{grid-area:1/1;min-width:0;min-height:0;overflow:hidden}\n.dshm-main>*,.dshm-main>*>*{min-width:0}\n.dshm-menu{position:fixed;z-index:62;top:max(4px,env(safe-area-inset-top));left:4px;display:grid;place-items:center;width:44px;height:44px;padding:0;border:0;border-radius:12px;background:transparent;color:var(--dsw-alias-label-primary,#171a21);cursor:pointer;-webkit-tap-highlight-color:transparent}\n.dshm-menu:hover,.dshm-menu:active{background:var(--dsw-alias-interactive-bg-hover,#f1f3f6)}\n.dshm-menu:focus-visible,.dshm-close:focus-visible{outline:2px solid var(--dsw-alias-border-focus,#2563eb);outline-offset:2px}\n.dshm-menu svg,.dshm-close svg{width:22px;height:22px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}\n.dshm-drawer{position:fixed;z-index:70;inset:0 auto 0 0;box-sizing:border-box;width:min(88vw,340px);max-width:100%;padding-top:env(safe-area-inset-top);overflow:hidden;background:var(--dsw-alias-bg-layer-1,#f8fafc);box-shadow:18px 0 46px rgb(15 23 42 / 18%);left:-104%;transition:left 190ms cubic-bezier(.22,1,.36,1)}\n.dshm-drawer[data-open=true]{left:0}\n.dshm-drawer[data-open=false]{pointer-events:none;visibility:hidden;transition:left 190ms cubic-bezier(.22,1,.36,1),visibility 0s linear 190ms}\n.dshm-drawer>*{width:100%!important;height:100%!important}\n.dshm-details{position:fixed;z-index:80;inset:0 0 0 auto;box-sizing:border-box;width:min(94vw,460px);max-width:100%;padding-top:env(safe-area-inset-top);overflow:hidden;background:var(--dsw-alias-bg-layer-1,#fff);box-shadow:-18px 0 46px rgb(15 23 42 / 18%);transform:translateX(104%);transition:transform 190ms cubic-bezier(.22,1,.36,1)}\n.dshm-details[data-open=true]{transform:translateX(0)}\n.dshm-details[data-open=false]{pointer-events:none;visibility:hidden;transition:transform 190ms cubic-bezier(.22,1,.36,1),visibility 0s linear 190ms}\n.dshm-scrim{position:fixed;z-index:65;inset:0;border:0;background:rgb(15 23 42 / 40%);opacity:0;pointer-events:none;transition:opacity 180ms ease-out}\n.dshm-scrim[data-open=true]{opacity:1;pointer-events:auto}\n.dshm-overlay{position:fixed;z-index:90;inset:0;pointer-events:none}.dshm-overlay>*{pointer-events:auto}\n.dshm-shell header{min-width:0;padding-left:52px}\n.dshm-shell textarea{font-size:16px}\n.dshm-shell table{display:block;max-width:100%;overflow-x:auto}\n.dshm-shell pre{max-width:100%;overflow-x:auto}\n.dshm-shell img,.dshm-shell video,.dshm-shell canvas,.dshm-shell svg{max-width:100%}\n.dshm-shell [data-disclosure-row]{min-width:0;max-width:100%}\n.dshm-shell [data-disclosure-row]>*{min-width:0;overflow-wrap:anywhere}\n.dshm-shell [data-context-fields]>*{min-width:0}\n.dshm-shell [class*=\"_body\"]{max-width:100%;overflow-wrap:anywhere}\n@media(max-width:420px){.dshm-shell [data-context-fields]>*{display:grid;grid-template-columns:1fr!important;gap:4px}.dshm-shell [class*=\"_ioSection\"]{grid-template-columns:1fr!important}}\n@media(prefers-reduced-motion:reduce){.dshm-drawer,.dshm-details,.dshm-scrim{transition:none!important}}\n`\n\nfunction icon(paths: readonly string[]): ReactNode {\n return createElement('svg', { 'aria-hidden': true, viewBox: '0 0 24 24' },\n ...paths.map((path, index) => createElement('path', { d: path, key: index })))\n}\n\nfunction MobileAppFrame(props: MobileRootProps & { readonly controller: MobileLayoutController }): ReactNode {\n const state = useSyncExternalStore(props.controller.subscribe, props.controller.getSnapshot)\n const hasSession = props.useSessions(session => {\n const current = session.current\n return current !== undefined && session.byId[current]?.blank === false\n })\n\n useEffect(() => {\n if (!hasSession) props.controller.closeDetails()\n }, [hasSession, props.controller])\n\n const closeDrawerAfterAction = (event: { readonly target: EventTarget | null; readonly currentTarget: EventTarget & HTMLElement }): void => {\n if (!(event.target instanceof Element)) return\n const action = event.target.closest('button,a,[role=\"button\"],[role=\"option\"],[role=\"treeitem\"]')\n if (action === null || action.closest('[data-dshm-keep-open],[aria-haspopup=\"dialog\"]') !== null) return\n window.setTimeout(() => { props.controller.closeSidebar() }, 80)\n }\n\n return createElement('div', { className: 'dshm-shell' },\n createElement('main', { className: 'dshm-main' }, props.renderSlot('conversation', {})),\n createElement('button', {\n 'aria-expanded': state.sidebarOpen,\n 'aria-label': state.sidebarOpen ? '关闭导航' : '打开导航',\n className: 'dshm-menu',\n onClick: () => { props.controller.toggleSidebar() },\n type: 'button',\n }, icon(['M4 6h16', 'M4 12h16', 'M4 18h16'])),\n createElement('button', {\n 'aria-label': '关闭浮层',\n className: 'dshm-scrim',\n 'data-open': state.sidebarOpen || state.detailsOpen,\n onClick: () => { state.detailsOpen ? props.controller.closeDetails() : props.controller.closeSidebar() },\n tabIndex: state.sidebarOpen || state.detailsOpen ? 0 : -1,\n type: 'button',\n }),\n createElement('aside', {\n 'aria-hidden': !state.sidebarOpen,\n className: 'dshm-drawer',\n 'data-open': state.sidebarOpen,\n onClickCapture: closeDrawerAfterAction,\n ...(state.sidebarOpen ? {} : { inert: '' }),\n }, props.renderSlot('sidebar', { collapsed: false, width: 340 })),\n createElement('aside', {\n 'aria-hidden': !state.detailsOpen,\n className: 'dshm-details',\n 'data-open': state.detailsOpen,\n ...(state.detailsOpen ? {} : { inert: '' }),\n }, hasSession ? props.renderSlot('details', {}) : undefined),\n createElement('div', { className: 'dshm-overlay', 'data-shell-overlay': true }, props.renderSlot('shell.overlay', {})),\n )\n}\n\n/** Replace the desktop layout module on the authenticated mobile surface. */\nexport function apply(ctx: MobileClientContext): void {\n const controller = new MobileLayoutController()\n ctx.effect(() => {\n const style = document.createElement('style')\n style.dataset.plugin = 'dsh-mobile-layout'\n style.textContent = MOBILE_LAYOUT_STYLES\n document.head.append(style)\n const disposeService = ctx.reflect.provide('layout', controller)\n const disposeRoot = ctx.slots.register({\n name: 'root',\n children: {\n sidebar: { kind: 'single', scope: 'root' },\n conversation: { kind: 'single', scope: 'session-maybe' },\n details: { kind: 'single', scope: 'session' },\n 'shell.overlay': { kind: 'list', scope: 'root' },\n },\n }, props => createElement(MobileAppFrame, { ...props, controller }))\n return () => {\n disposeRoot()\n void disposeService()\n style.remove()\n }\n }, 'dsh-mobile: dedicated root layout')\n\n ctx.effect(() => {\n const presenter = new ThemePresenter()\n presenter.apply(ctx.theme.getTheme())\n const off = ctx.on('theme/change', snapshot => { presenter.apply(snapshot) })\n return () => { off(); presenter.dispose() }\n }, 'dsh-mobile: theme presenter')\n}\n\n/** Preserve the official layout module's dependency ordering. */\nexport const inject: readonly string[] = ['slots', 'theme']\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mobile",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.21",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "DeepSeek Harness 移动端适配与安全局域网访问插件,支持 Android App 和手机浏览器。",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
|
-
"dsh-mobile": "lib/cli.js"
|
|
8
|
+
"dsh-mobile": "lib/cli.js"
|
|
9
9
|
},
|
|
10
10
|
"main": "./lib/index.mjs",
|
|
11
11
|
"types": "./lib/index.d.mts",
|
|
@@ -20,14 +20,16 @@
|
|
|
20
20
|
"./package.json": "./package.json"
|
|
21
21
|
},
|
|
22
22
|
"files": [
|
|
23
|
-
"lib/**/*.mjs",
|
|
24
|
-
"lib/cli.js",
|
|
23
|
+
"lib/**/*.mjs",
|
|
24
|
+
"lib/cli.js",
|
|
25
25
|
"lib/**/*.d.mts",
|
|
26
|
-
"lib/client.js",
|
|
27
|
-
"lib/client.js.map",
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
26
|
+
"lib/client.js",
|
|
27
|
+
"lib/client.js.map",
|
|
28
|
+
"lib/mobile-layout.js",
|
|
29
|
+
"lib/mobile-layout.js.map",
|
|
30
|
+
"cordis.patch.yml",
|
|
31
|
+
"README.md",
|
|
32
|
+
"README.en.md",
|
|
31
33
|
"assets/brand/*.png",
|
|
32
34
|
"LICENSE",
|
|
33
35
|
"SECURITY.md"
|
|
@@ -38,7 +40,9 @@
|
|
|
38
40
|
},
|
|
39
41
|
"client": {
|
|
40
42
|
"platform": "web",
|
|
41
|
-
"inject": [
|
|
43
|
+
"inject": [
|
|
44
|
+
"@deepseek-ai/dsh-client-connection",
|
|
45
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
42
46
|
"@deepseek-ai/dsh-client-ui-sidebar"
|
|
43
47
|
],
|
|
44
48
|
"immediately": true
|
|
@@ -46,7 +50,8 @@
|
|
|
46
50
|
},
|
|
47
51
|
"scripts": {
|
|
48
52
|
"build": "tsdown",
|
|
49
|
-
"check:mobile-release": "node ./scripts/check-mobile-release.mjs",
|
|
53
|
+
"check:mobile-release": "node ./scripts/check-mobile-release.mjs",
|
|
54
|
+
"check:dsh-compatibility": "node ./scripts/check-dsh-compatibility.mjs",
|
|
50
55
|
"check:release-tag": "node ./scripts/check-release-version.mjs --tag-env",
|
|
51
56
|
"check:version": "node ./scripts/check-release-version.mjs",
|
|
52
57
|
"clean": "node ./scripts/clean.mjs",
|
|
@@ -84,7 +89,7 @@
|
|
|
84
89
|
"license": "Apache-2.0",
|
|
85
90
|
"peerDependencies": {
|
|
86
91
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
87
|
-
"@deepseek-ai/dsh-host-webserver": "
|
|
92
|
+
"@deepseek-ai/dsh-host-webserver": "0.1.0-rc.5 || 0.1.0-rc.6",
|
|
88
93
|
"react": "^18.2.0"
|
|
89
94
|
},
|
|
90
95
|
"peerDependenciesMeta": {
|
|
@@ -100,6 +105,7 @@
|
|
|
100
105
|
},
|
|
101
106
|
"dependencies": {
|
|
102
107
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
108
|
+
"bonjour-service": "^1.4.4",
|
|
103
109
|
"selfsigned": "^5.5.0"
|
|
104
110
|
},
|
|
105
111
|
"devDependencies": {
|
package/README.zh.md
DELETED
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
<p align="center">
|
|
2
|
-
<img src="https://unpkg.com/dsh-mobile@0.1.0-alpha.2/assets/brand/repository-hero.png" alt="鲸鱼娘拿着手机连接桌面上的 DSH" width="100%">
|
|
3
|
-
</p>
|
|
4
|
-
|
|
5
|
-
<h1 align="center">DSH Mobile</h1>
|
|
6
|
-
|
|
7
|
-
<p align="center">让手机通过受保护的局域网连接直接控制原生 DeepSeek Harness Web UI。</p>
|
|
8
|
-
|
|
9
|
-
<p align="center">
|
|
10
|
-
<a href="https://www.npmjs.com/package/dsh-mobile"><img src="https://img.shields.io/npm/v/dsh-mobile?label=npm&color=CB3837" alt="npm 版本"></a>
|
|
11
|
-
<a href="https://www.npmjs.com/package/dsh-mobile"><img src="https://img.shields.io/npm/dm/dsh-mobile?color=2563EB" alt="npm 下载量"></a>
|
|
12
|
-
<img src="https://img.shields.io/badge/build-verified-059669" alt="构建已验证">
|
|
13
|
-
<a href="apps/mobile/README.zh-CN.md"><img src="https://img.shields.io/badge/platform-Android-3DDC84?logo=android&logoColor=white" alt="Android"></a>
|
|
14
|
-
<a href="#为什么符合-dsh-的插件理念"><img src="https://img.shields.io/badge/DSH-stock%20compatible-2563EB" alt="兼容原生 DSH"></a>
|
|
15
|
-
<a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-0F172A" alt="Apache-2.0 许可证"></a>
|
|
16
|
-
</p>
|
|
17
|
-
|
|
18
|
-
<p align="center">
|
|
19
|
-
<a href="README.md">English</a> ·
|
|
20
|
-
<a href="#快速开始">快速开始</a> ·
|
|
21
|
-
<a href="#自定义移动端-web-ui">自定义 UI</a> ·
|
|
22
|
-
<a href="#卸载">卸载</a>
|
|
23
|
-
</p>
|
|
24
|
-
|
|
25
|
-
> 当前为公开 npm Alpha,源码仓库在测试阶段暂时保持私有,仅支持 Android。它是社区项目,不是 DeepSeek 官方产品。
|
|
26
|
-
|
|
27
|
-
DSH Mobile 不是另一套远程控制面板。插件直接复用 DSH 自己的 Web 应用、会话和插件槽位,在外层增加 HTTPS、设备配对、会话撤销和移动端样式。你可以用 Android App 获得无浏览器栏的体验,也可以直接用手机浏览器访问同一个局域网地址。
|
|
28
|
-
|
|
29
|
-
移动端客户端可以“自己改自己”:直接在普通 DSH 对话中要求 DSH 修改负责外观的 `mobile.css` 或负责功能的 `mobile.js`,手机页面保持打开,Android App 和手机浏览器通常会在一秒内自动应用结果。
|
|
30
|
-
|
|
31
|
-
## 主要能力
|
|
32
|
-
|
|
33
|
-
| 能力 | Android App | 手机浏览器 |
|
|
34
|
-
| --- | :---: | :---: |
|
|
35
|
-
| 实时访问 DSH Web UI | 支持 | 支持 |
|
|
36
|
-
| HTTPS 局域网入口 | 支持 | 支持 |
|
|
37
|
-
| 一次性设备配对 | 支持 | 支持 |
|
|
38
|
-
| 在 DSH 对话中扩展移动 UI与网页功能并实时预览 | 支持 | 支持 |
|
|
39
|
-
| 文件选择与下载 | 支持 | 由浏览器决定 |
|
|
40
|
-
| iOS | 暂不支持 | 可用 Safari 访问网页 |
|
|
41
|
-
|
|
42
|
-
宿主文件选择器、打开本机文档等动作仍受原生 DSH 和操作系统限制。配对设备应视为电脑上的完全可信操作者,而不是低权限访客。
|
|
43
|
-
|
|
44
|
-
## 快速开始
|
|
45
|
-
|
|
46
|
-
终端中可以直接运行 `dsh` 时,使用第一组命令:
|
|
47
|
-
|
|
48
|
-
```powershell
|
|
49
|
-
dsh plugin --profile web add dsh-mobile
|
|
50
|
-
dsh plugin --profile web exec dsh-mobile setup
|
|
51
|
-
dsh --profile web
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
如果你直接克隆了 DSH 源码,则在 DSH 源码根目录使用第二组命令:
|
|
55
|
-
|
|
56
|
-
```powershell
|
|
57
|
-
corepack enable
|
|
58
|
-
pnpm install
|
|
59
|
-
pnpm dsh plugin --profile web add dsh-mobile
|
|
60
|
-
pnpm dsh plugin --profile web exec dsh-mobile setup
|
|
61
|
-
pnpm dsh --profile web
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
两种方式都会把公开 npm 包安装到原生 DSH,不修改 DSH 源码。设置向导会选择局域网地址、生成本地 TLS 证书,并把插件数据保存到 `$DSH_HOME/mobile-access/`;需要手动指定网卡或端口时,通过同一个 `plugin exec` 命令运行 `dsh-mobile setup --help`。
|
|
65
|
-
|
|
66
|
-
1. 将向导输出的 `dsh-mobile-ca.cer` 复制到 Android,并在系统设置中安装为用户 CA 证书。
|
|
67
|
-
2. 在电脑 DSH 左下角打开“移动端”卡片。
|
|
68
|
-
3. 确认服务已开启,点击“生成配对链接”。链接会复制到剪贴板。
|
|
69
|
-
4. 在 Android App 中粘贴链接,或直接在手机浏览器中打开链接。
|
|
70
|
-
|
|
71
|
-
配对链接两分钟内有效且只能使用一次。App 与浏览器拥有各自的 Cookie 存储,因此需要分别配对。
|
|
72
|
-
|
|
73
|
-
## 日常使用
|
|
74
|
-
|
|
75
|
-
安装后,“移动端”卡片会常驻 DSH 左下角。关闭移动访问只会停止外部 HTTPS 监听并断开移动会话,不会停止 DSH 本身;重新开启不需要再次安装插件。
|
|
76
|
-
|
|
77
|
-
手机有两种等价入口:
|
|
78
|
-
|
|
79
|
-
- Android App:使用系统 WebView 打开同一 DSH 页面,没有浏览器地址栏和标签栏。
|
|
80
|
-
- 手机浏览器:直接访问向导显示的 `https://局域网地址:端口`,无需安装 App。
|
|
81
|
-
|
|
82
|
-
## 自定义移动端 Web UI
|
|
83
|
-
|
|
84
|
-
这不是一个只能换颜色的主题编辑器,而是与 DSH 页面同源的 Web 扩展面。电脑上的 DSH 可以编辑两个文件:
|
|
85
|
-
|
|
86
|
-
- `mobile.css` 控制布局、视觉层级、安全区、字体和响应式行为。
|
|
87
|
-
- `mobile.js` 挂载移动端专属 DOM 与交互,可以使用浏览器能力,也可以用已配对页面的身份调用同源 DSH Web API。
|
|
88
|
-
|
|
89
|
-
自由度并不限于几个预设组件。你可以让 DSH 创建单手指令坞、手势命令面板、会话仪表盘、终端快捷键、悬浮上下文面板、语音输入、相机或二维码工作流,乃至完全不同的移动端导航。相机、麦克风、通知等能力仍需要浏览器或 Android 授权。
|
|
90
|
-
|
|
91
|
-
最简单的方式是不离开 DSH。打开一个具备文件写入权限的 DSH 对话,给它一个更大胆的目标:
|
|
92
|
-
|
|
93
|
-
```text
|
|
94
|
-
请编辑 $DSH_HOME/mobile-access/mobile.css 和 mobile.js,
|
|
95
|
-
把 DSH Mobile 改造成适合单手操作的现场工作台。
|
|
96
|
-
|
|
97
|
-
增加底部指令坞、左右滑动切换会话、浏览器支持时可用的按住说话按钮,
|
|
98
|
-
以及可折叠的会话监控面板。所有功能只在移动端出现;JavaScript 通过
|
|
99
|
-
window.dshMobile.register() 注册并返回清理函数,不要修改 DSH 源码。
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
DSH 修改文件时让手机页面保持打开。Android App 和手机浏览器每秒检查一次这两个已认证文件,保存后的变化会自动应用,不需要重启 DSH、重新配对或手动刷新页面。JavaScript 写错时会保留上一次成功挂载的功能。
|
|
103
|
-
|
|
104
|
-
设置向导会创建:
|
|
105
|
-
|
|
106
|
-
```text
|
|
107
|
-
$DSH_HOME/mobile-access/mobile.css
|
|
108
|
-
$DSH_HOME/mobile-access/mobile.js
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
它们只通过已认证的移动网关加载,不修改 DSH 安装目录,升级或卸载 DSH 时也不会覆盖。你可以继续在同一个对话中说“把指令坞改成环形菜单”“增加全屏监控视图”或“用相机准备附件”,并直接在手机上观察每次修改。
|
|
112
|
-
|
|
113
|
-
`mobile.js` 只有一个很小的生命周期入口:
|
|
114
|
-
|
|
115
|
-
```js
|
|
116
|
-
window.dshMobile.register(({ root, document, request }) => {
|
|
117
|
-
const workspace = document.createElement('section')
|
|
118
|
-
workspace.ariaLabel = '我的移动工作台'
|
|
119
|
-
root.append(workspace)
|
|
120
|
-
|
|
121
|
-
// 在 workspace 中构建任意移动 Web UI 和交互。调用同源 DSH API 时使用
|
|
122
|
-
// request(),它会携带已配对 Session 和 CSRF 防护信息。
|
|
123
|
-
|
|
124
|
-
return () => workspace.remove()
|
|
125
|
-
})
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
传入的 `root` 用来承载移动端专属 DOM;`request()` 会拒绝跨源目标,并自动携带已配对会话和网关 CSRF 请求头。需要设备权限的能力仍可直接使用安全上下文中的标准浏览器 API。
|
|
129
|
-
|
|
130
|
-
### 最简单的主题调整
|
|
131
|
-
|
|
132
|
-
```css
|
|
133
|
-
:root {
|
|
134
|
-
--dsh-mobile-accent: #2563eb;
|
|
135
|
-
--dsh-mobile-font-scale: 1.05;
|
|
136
|
-
--dsh-mobile-radius: 16px;
|
|
137
|
-
}
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
### 只调整窄屏布局
|
|
141
|
-
|
|
142
|
-
```css
|
|
143
|
-
@media (max-width: 720px) {
|
|
144
|
-
textarea {
|
|
145
|
-
min-height: 88px;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
button,
|
|
149
|
-
[role='button'] {
|
|
150
|
-
min-height: 48px;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
建议优先使用稳定的 Web API 和 DSH 扩展点,不要依赖构建后生成的类名。删除 `mobile.css` 可恢复默认外观;将 `mobile.js` 替换为空的注册函数即可移除自定义功能,打开的手机页面会自动更新。
|
|
156
|
-
|
|
157
|
-
## 为什么符合 DSH 的插件理念
|
|
158
|
-
|
|
159
|
-
DSH Mobile 通过一个标准 bundle 同时贡献两张 face:Host face 提供局域网网关和本地控制接口,Client face 提供移动端样式与左下角控制卡。它只注入原生 `webServer` 服务,通过 `dsh plugin add/remove` 安装和卸载,不修改 `deepseek-harness` 文件。
|
|
160
|
-
|
|
161
|
-
```mermaid
|
|
162
|
-
flowchart LR
|
|
163
|
-
Phone["Android App 或手机浏览器"] -->|"HTTPS + 已配对会话"| Gateway["DSH Mobile 插件"]
|
|
164
|
-
Gateway -->|"仅回环代理"| DSH["原生 DSH Web UI"]
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
## 安全模型
|
|
168
|
-
|
|
169
|
-
- 网关只接受设置向导选定的私有局域网 CIDR、精确 Host 和同源浏览器请求。
|
|
170
|
-
- Android App 不跳过 TLS 校验;证书必须覆盖实际访问的局域网 IP。
|
|
171
|
-
- 配对设备获得短期 HttpOnly Session,设备和会话都可在电脑端撤销。
|
|
172
|
-
- 管理接口只允许从电脑回环地址访问,移动端无法打开、关闭网关或管理其他设备。
|
|
173
|
-
- 外层网关在认证后才代理到原生 DSH 的回环端口,不会把 DSH 自身绑定到 `0.0.0.0`。
|
|
174
|
-
- `mobile.js` 会作为应用代码在已配对的 DSH 页面中运行。只允许可信的 DSH 对话编辑它,并在长期保留前检查生成的集成。
|
|
175
|
-
|
|
176
|
-
不要在公共 Wi-Fi、端口转发、公网 IP 或不可信 VPN 中开放本插件。更多说明见 [SECURITY.md](SECURITY.md)。
|
|
177
|
-
|
|
178
|
-
## 卸载
|
|
179
|
-
|
|
180
|
-
普通卸载保留证书、已配对设备和自定义 CSS,方便以后重新安装:
|
|
181
|
-
|
|
182
|
-
```powershell
|
|
183
|
-
dsh plugin --profile web remove dsh-mobile
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
如果希望彻底删除本地数据,必须先运行清理,再卸载包:
|
|
187
|
-
|
|
188
|
-
```powershell
|
|
189
|
-
dsh plugin --profile web exec dsh-mobile purge --yes
|
|
190
|
-
dsh plugin --profile web remove dsh-mobile
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
## Android App
|
|
194
|
-
|
|
195
|
-
当前仅维护 Android 薄壳,源码位于 [`apps/mobile/android`](apps/mobile/android)。App 不复制 DSH 前端,只渲染插件提供的同一 HTTPS 页面;浏览器访问始终是一等能力。构建与安全边界见 [Android 文档](apps/mobile/README.zh-CN.md)。
|
|
196
|
-
|
|
197
|
-
## 开发
|
|
198
|
-
|
|
199
|
-
```powershell
|
|
200
|
-
npm ci
|
|
201
|
-
npm run verify
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
真实安装验证应从 `npm pack` 生成的 tarball 开始,并在隔离的原生 DSH Web profile 中完成安装、设置、启动、配对和卸载。
|
|
205
|
-
|
|
206
|
-
Apache-2.0 licensed. See [LICENSE](LICENSE).
|