@tianyulinfeng1/dsh-plugin-deepseek-chat 1.1.2 → 1.1.6
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/CHANGELOG.md +144 -0
- package/README.en.md +34 -13
- package/README.md +36 -14
- package/docs/RELEASE_NOTES_v1.1.5.md +114 -0
- package/docs/login-persistence.md +1 -1
- package/lib/client.js +707 -364
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +3 -2
package/lib/client.js
CHANGED
|
@@ -1,41 +1,69 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Browser half of dsh-plugin-deepseek-chat (v1.1.
|
|
2
|
+
* Browser half of dsh-plugin-deepseek-chat (v1.1.6).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Three contributions, all showing chat.deepseek.com in an Electron <webview>:
|
|
5
|
+
* - the global left-sidebar panel (a `sidebar.panellist` icon row + the root
|
|
6
|
+
* `main` keyed slot), the same mechanism Plugins / Scheduled tasks use;
|
|
7
|
+
* - a conversation top-bar tab registered in the `conversation.view` list
|
|
8
|
+
* slot, so the web chat is reachable from the 对话 / 轨迹 / 上下文 … tab row
|
|
9
|
+
* without leaving the current Session (this one can be switched off);
|
|
10
|
+
* - a Settings page (`settings.section`) with two preferences: whether the
|
|
11
|
+
* top-bar tab is shown, and the tab's display name (default "DeepSeek 聊天",
|
|
12
|
+
* user-customizable).
|
|
8
13
|
*
|
|
9
|
-
*
|
|
10
|
-
* `content-security-policy: frame-ancestors 'none'`, so an iframe cannot carry
|
|
11
|
-
* it; the desktop shell exposes `globalThis.dshDesktop.browser`, the same guest
|
|
12
|
-
* lease bridge the built-in sidebar browser uses: acquire a lease → create a
|
|
13
|
-
* webview named by the lease with its approved partition → load the URL.
|
|
14
|
-
*
|
|
15
|
-
* Stable login session (v1.1.1)
|
|
14
|
+
* Filling the conversation area
|
|
16
15
|
* -----------------------------
|
|
16
|
+
* The conversation renderer places each view in `viewArea`, a sibling of the
|
|
17
|
+
* sticky `composerSeat`. In the `active` phase `viewArea` becomes
|
|
18
|
+
* `flex:1 0 auto`, so `height:100%` on the panel has no definite parent height
|
|
19
|
+
* and the Session's own messages/composer stay visible underneath. The tab is
|
|
20
|
+
* therefore wrapped in an absolutely positioned overlay: its containing block
|
|
21
|
+
* is the conversation body (`position:relative`), `inset:0` covers exactly
|
|
22
|
+
* that body, an opaque background plus a z-index above the composer's 9 hide
|
|
23
|
+
* the Session UI below, and the header with the tab row sits above the body so
|
|
24
|
+
* switching back to 对话 remains possible.
|
|
25
|
+
*
|
|
26
|
+
* The site sends `content-security-policy: frame-ancestors 'none'`, so an
|
|
27
|
+
* iframe cannot carry it; the desktop shell exposes
|
|
28
|
+
* `globalThis.dshDesktop.browser`, the same guest lease bridge the built-in
|
|
29
|
+
* sidebar browser uses: acquire a lease → create a webview named by the lease
|
|
30
|
+
* with its approved partition → load the URL.
|
|
31
|
+
*
|
|
32
|
+
* Stable login session
|
|
33
|
+
* --------------------
|
|
17
34
|
* The desktop shell caches ONE process-lifetime session per `workspace` string
|
|
18
35
|
* passed to acquire(), and release() destroys only the guest, never the
|
|
19
36
|
* partition's cookies. So the workspace MUST be an absolute constant:
|
|
20
|
-
* `panel:deepseek-chat`.
|
|
21
|
-
*
|
|
22
|
-
* cookie jar and forced another QR scan. That regression is fixed here.
|
|
37
|
+
* `panel:deepseek-chat`. Changing it hands the panel a fresh cookie jar and
|
|
38
|
+
* forces another QR scan.
|
|
23
39
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* the
|
|
30
|
-
*
|
|
40
|
+
* Settings persistence (v1.1.4)
|
|
41
|
+
* -----------------------------
|
|
42
|
+
* Settings live in window.localStorage under CONFIG_KEY, separate from the
|
|
43
|
+
* last-URL memory. The Settings page writes through writeConfig(), which
|
|
44
|
+
* notifies module-level subscribers. apply() uses that notification to dispose
|
|
45
|
+
* and re-inject the `conversation.view` entry: the tab row only recomputes its
|
|
46
|
+
* labels when the slot registry changes, so a label thunk alone cannot reflect
|
|
47
|
+
* a renamed tab. Re-registering also drives the showTab on/off switch.
|
|
31
48
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
49
|
+
* About & update check (v1.1.5)
|
|
50
|
+
* ----------------------------
|
|
51
|
+
* The Settings page also shows the plugin's metadata (version, author, license,
|
|
52
|
+
* a short description and links to npm/GitHub) and a "检查更新" button. The
|
|
53
|
+
* latest published version is read directly from the two public JSON endpoints
|
|
54
|
+
* (npm registry first, GitHub raw package.json as a fallback) using the
|
|
55
|
+
* renderer's fetch — the main window sets no connect-src CSP restriction and
|
|
56
|
+
* both hosts answer with permissive CORS. No Host-side route is added, so the
|
|
57
|
+
* empty host half stays untouched. compareVersion() does a numeric semver
|
|
58
|
+
* comparison rather than string equality, because a local unreleased build
|
|
59
|
+
* (e.g. 1.1.6) can already be ahead of the published "latest" (1.1.2).
|
|
60
|
+
*
|
|
61
|
+
* No account awareness (v1.1.3)
|
|
62
|
+
* -----------------------------
|
|
63
|
+
* Earlier versions probed the DSH account and rendered a status strip. That
|
|
64
|
+
* strip and the whole account sync store (polling / probes / listeners) are
|
|
65
|
+
* removed; the chat uses its own site-owned login, and no DSH account state is
|
|
66
|
+
* read, displayed, or fed into the guest lifecycle.
|
|
39
67
|
*
|
|
40
68
|
* Failure containment is still the hard rule of this file. DSH treats any
|
|
41
69
|
* uncaught error thrown by a plugin as a crash of its slot entry, and a crash
|
|
@@ -61,19 +89,34 @@ window.__ModuleLoader__.load({
|
|
|
61
89
|
const react = require("react");
|
|
62
90
|
|
|
63
91
|
const PANEL_ID = "deepseek-chat";
|
|
92
|
+
/** Cell key of the conversation top-bar tab (conversation.view list). */
|
|
93
|
+
const VIEW_ID = "deepseek-chat-view";
|
|
94
|
+
/** Position after the shipped views (chat 0, trajectory 10, stats …). */
|
|
95
|
+
const VIEW_ORDER = 100;
|
|
96
|
+
/** Cell key / position of the Settings page (settings.section list). */
|
|
97
|
+
const SETTINGS_ID = "deepseek-chat-settings";
|
|
98
|
+
const SETTINGS_ORDER = 48;
|
|
99
|
+
const CHAT_URL = "https://chat.deepseek.com/";
|
|
100
|
+
|
|
64
101
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
102
|
+
* Plugin metadata shown in Settings. Kept as plain constants (the client
|
|
103
|
+
* bundle cannot import package.json) and asserted against package.json by
|
|
104
|
+
* the offline self-test — the same pattern the factory id uses.
|
|
68
105
|
*/
|
|
69
|
-
const
|
|
70
|
-
const
|
|
106
|
+
const PLUGIN_NAME = "@tianyulinfeng1/dsh-plugin-deepseek-chat";
|
|
107
|
+
const PLUGIN_VERSION = "1.1.6";
|
|
108
|
+
const PLUGIN_AUTHOR = "sweet560";
|
|
109
|
+
const PLUGIN_LICENSE = "MIT";
|
|
110
|
+
const NPM_PAGE_URL = "https://www.npmjs.com/package/@tianyulinfeng1/dsh-plugin-deepseek-chat";
|
|
111
|
+
const GITHUB_PAGE_URL = "https://github.com/sweet560/dsh-plugin-deepseek-chat";
|
|
112
|
+
/** Public JSON endpoints used by "检查更新" (npm first, GitHub fallback). */
|
|
113
|
+
const NPM_VERSION_URL = "https://registry.npmjs.org/-/package/@tianyulinfeng1%2fdsh-plugin-deepseek-chat/dist-tags";
|
|
114
|
+
const GITHUB_VERSION_URL = "https://raw.githubusercontent.com/sweet560/dsh-plugin-deepseek-chat/main/package.json";
|
|
71
115
|
/**
|
|
72
116
|
* Constant storage identities for the guest partition and the
|
|
73
117
|
* last-conversation memory. The main process keeps one partition per
|
|
74
|
-
* `workspace` string for the process lifetime;
|
|
75
|
-
*
|
|
76
|
-
* cookie jar and force a fresh QR scan. v1.1.1 regression fix.
|
|
118
|
+
* `workspace` string for the process lifetime; these MUST NOT change,
|
|
119
|
+
* or a fresh cookie jar would force a fresh QR scan.
|
|
77
120
|
*/
|
|
78
121
|
const PARTITION_BASE = "panel:deepseek-chat";
|
|
79
122
|
const STORAGE_BASE = "dsh-plugin-deepseek-chat:last-url";
|
|
@@ -89,11 +132,33 @@ window.__ModuleLoader__.load({
|
|
|
89
132
|
crashed: "The embedded chat stopped responding",
|
|
90
133
|
timeout: "The embedded chat did not start",
|
|
91
134
|
retry: "Retry",
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
135
|
+
settingsNav: "DeepSeek Chat",
|
|
136
|
+
settingsTitle: "DeepSeek Chat settings",
|
|
137
|
+
settingsEnableTab: "Show the conversation top-bar tab",
|
|
138
|
+
settingsEnableHint: "Add a web-chat entry to the 对话 / 轨迹 / 上下文 … tab row. Turn this off to keep only the left-sidebar panel.",
|
|
139
|
+
settingsTabLabel: "Top-bar tab name",
|
|
140
|
+
settingsTabLabelHint: "Default: DeepSeek 聊天. Leave the box empty to restore the default.",
|
|
141
|
+
settingsPlaceholder: "DeepSeek 聊天",
|
|
142
|
+
aboutTitle: "About this plugin",
|
|
143
|
+
infoVersion: "Version",
|
|
144
|
+
infoAuthor: "Author",
|
|
145
|
+
infoLicense: "License",
|
|
146
|
+
infoDescription: "Description",
|
|
147
|
+
infoSummary: "Opens chat.deepseek.com from the sidebar and the conversation tab row: a switchable, renameable top-bar tab, a full-area web chat, and last-conversation memory.",
|
|
148
|
+
infoLinks: "Links",
|
|
149
|
+
linkNpm: "npm package page",
|
|
150
|
+
linkGithub: "GitHub repository",
|
|
151
|
+
updateTitle: "Check for updates",
|
|
152
|
+
checkUpdate: "Check for updates",
|
|
153
|
+
updateIdle: "Click the button to read the latest published version from npm or GitHub.",
|
|
154
|
+
updateChecking: "Checking for updates…",
|
|
155
|
+
updateUpToDate: "You're up to date",
|
|
156
|
+
updateFound: "New version available",
|
|
157
|
+
updateFailed: "Update check failed",
|
|
158
|
+
updateNoFetch: "This environment cannot reach the network (fetch is unavailable).",
|
|
159
|
+
updateSource: "Source",
|
|
160
|
+
updateLatest: "Latest",
|
|
161
|
+
updateCurrent: "Current"
|
|
97
162
|
} : {
|
|
98
163
|
label: "DeepSeek 聊天",
|
|
99
164
|
loading: "正在加载 DeepSeek 聊天…",
|
|
@@ -102,29 +167,247 @@ window.__ModuleLoader__.load({
|
|
|
102
167
|
crashed: "内嵌聊天进程已退出",
|
|
103
168
|
timeout: "内嵌聊天页面没有启动",
|
|
104
169
|
retry: "重试",
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
170
|
+
settingsNav: "DeepSeek 聊天",
|
|
171
|
+
settingsTitle: "DeepSeek 聊天设置",
|
|
172
|
+
settingsEnableTab: "显示顶部标签栏",
|
|
173
|
+
settingsEnableHint: "在会话顶栏「对话 / 轨迹 / 上下文 …」标签行加入网页聊天入口;关闭后只保留左侧栏面板。",
|
|
174
|
+
settingsTabLabel: "顶部标签栏名称",
|
|
175
|
+
settingsTabLabelHint: "默认:DeepSeek 聊天;留空即恢复默认。",
|
|
176
|
+
settingsPlaceholder: "DeepSeek 聊天",
|
|
177
|
+
aboutTitle: "关于本插件",
|
|
178
|
+
infoVersion: "插件版本号",
|
|
179
|
+
infoAuthor: "作者",
|
|
180
|
+
infoLicense: "开源许可证",
|
|
181
|
+
infoDescription: "功能简介",
|
|
182
|
+
infoSummary: "在左侧栏与会话顶栏直接打开 chat.deepseek.com 网页聊天;顶栏标签可开关、可改名,网页铺满会话区并记住上次会话。",
|
|
183
|
+
infoLinks: "相关链接",
|
|
184
|
+
linkNpm: "npm 插件页面",
|
|
185
|
+
linkGithub: "GitHub 仓库",
|
|
186
|
+
updateTitle: "检查更新",
|
|
187
|
+
checkUpdate: "检查更新",
|
|
188
|
+
updateIdle: "点击按钮,从 npm 或 GitHub 获取最新发布版本号。",
|
|
189
|
+
updateChecking: "正在检查更新…",
|
|
190
|
+
updateUpToDate: "已是最新版本",
|
|
191
|
+
updateFound: "发现新版本",
|
|
192
|
+
updateFailed: "检查更新失败",
|
|
193
|
+
updateNoFetch: "当前环境无法联网(没有可用的 fetch)。",
|
|
194
|
+
updateSource: "来源",
|
|
195
|
+
updateLatest: "最新版本",
|
|
196
|
+
updateCurrent: "当前版本"
|
|
110
197
|
};
|
|
111
198
|
|
|
112
|
-
/**
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
const
|
|
199
|
+
/** localStorage key for the two user preferences (separate from last-url memory). */
|
|
200
|
+
const CONFIG_KEY = "dsh-plugin-deepseek-chat:config";
|
|
201
|
+
const DEFAULT_TAB_LABEL = "DeepSeek 聊天";
|
|
202
|
+
const DEFAULTS = { showTab: true, tabLabel: DEFAULT_TAB_LABEL };
|
|
203
|
+
const configListeners = new Set();
|
|
204
|
+
let config = readConfig();
|
|
205
|
+
|
|
206
|
+
/** Read preferences, validating each field's type against DEFAULTS. */
|
|
207
|
+
function readConfig() {
|
|
208
|
+
const result = { ...DEFAULTS };
|
|
209
|
+
try {
|
|
210
|
+
const raw = window.localStorage.getItem(CONFIG_KEY);
|
|
211
|
+
if (typeof raw !== "string" || raw.length === 0) return result;
|
|
212
|
+
const parsed = JSON.parse(raw);
|
|
213
|
+
if (parsed === null || typeof parsed !== "object") return result;
|
|
214
|
+
if (typeof parsed.showTab === "boolean") result.showTab = parsed.showTab;
|
|
215
|
+
if (typeof parsed.tabLabel === "string") result.tabLabel = normalizeTabLabel(parsed.tabLabel);
|
|
216
|
+
} catch {
|
|
217
|
+
/* storage unavailable or corrupt: keep defaults */
|
|
218
|
+
}
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Trim, bound length, and fall back to the default when empty. */
|
|
223
|
+
function normalizeTabLabel(value) {
|
|
224
|
+
const trimmed = value.trim();
|
|
225
|
+
if (trimmed.length === 0) return DEFAULT_TAB_LABEL;
|
|
226
|
+
return trimmed.slice(0, 40);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Snapshot of the in-memory preferences (authoritative after load). */
|
|
230
|
+
function getConfig() {
|
|
231
|
+
return { ...config };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Current tab label (never empty). */
|
|
235
|
+
function tabLabel() {
|
|
236
|
+
return config.tabLabel.length > 0 ? config.tabLabel : DEFAULT_TAB_LABEL;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Merge a patch, persist it, and notify subscribers (Settings → tab re-injection). */
|
|
240
|
+
function writeConfig(patch) {
|
|
241
|
+
const next = { ...config };
|
|
242
|
+
if (typeof patch.showTab === "boolean") next.showTab = patch.showTab;
|
|
243
|
+
if (typeof patch.tabLabel === "string") next.tabLabel = normalizeTabLabel(patch.tabLabel);
|
|
244
|
+
config = next;
|
|
245
|
+
try {
|
|
246
|
+
window.localStorage.setItem(CONFIG_KEY, JSON.stringify(config));
|
|
247
|
+
} catch {
|
|
248
|
+
/* storage unavailable: the in-memory value still applies this session */
|
|
249
|
+
}
|
|
250
|
+
for (const listener of [...configListeners]) {
|
|
251
|
+
try {
|
|
252
|
+
listener(config);
|
|
253
|
+
} catch {
|
|
254
|
+
/* one bad subscriber must not stop the others */
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Subscribe to preference changes; returns a disposer. */
|
|
260
|
+
function subscribeConfig(listener) {
|
|
261
|
+
configListeners.add(listener);
|
|
262
|
+
return () => {
|
|
263
|
+
configListeners.delete(listener);
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── 关于 & 检查更新(v1.1.5)────────────────────────────────────────────
|
|
268
|
+
/** status: idle | checking | up-to-date | found | failed. */
|
|
269
|
+
let updateState = { status: "idle" };
|
|
270
|
+
function getUpdateState() {
|
|
271
|
+
return { ...updateState };
|
|
272
|
+
}
|
|
273
|
+
function setUpdateState(next) {
|
|
274
|
+
updateState = next;
|
|
275
|
+
}
|
|
276
|
+
function resetUpdateState() {
|
|
277
|
+
updateState = { status: "idle" };
|
|
278
|
+
}
|
|
116
279
|
|
|
117
280
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
281
|
+
* Numeric comparison of dotted versions, so 1.1.10 > 1.1.9. Missing
|
|
282
|
+
* parts count as zero and a non-numeric part parses as zero. A local
|
|
283
|
+
* unreleased build can legitimately be ahead of the published "latest".
|
|
284
|
+
* @param a - first version.
|
|
285
|
+
* @param b - second version.
|
|
286
|
+
* @returns -1 when a < b, 0 when equal, 1 when a > b.
|
|
287
|
+
*/
|
|
288
|
+
function compareVersion(a, b) {
|
|
289
|
+
const partsA = String(a ?? "").split(".").map((part) => parseInt(part, 10) || 0);
|
|
290
|
+
const partsB = String(b ?? "").split(".").map((part) => parseInt(part, 10) || 0);
|
|
291
|
+
const length = Math.max(partsA.length, partsB.length);
|
|
292
|
+
for (let index = 0; index < length; index += 1) {
|
|
293
|
+
const left = partsA[index] ?? 0;
|
|
294
|
+
const right = partsB[index] ?? 0;
|
|
295
|
+
if (left > right) return 1;
|
|
296
|
+
if (left < right) return -1;
|
|
297
|
+
}
|
|
298
|
+
return 0;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Extract a dotted version from an npm dist-tags or package.json payload. */
|
|
302
|
+
function versionFromPayload(text) {
|
|
303
|
+
let parsed;
|
|
304
|
+
try {
|
|
305
|
+
parsed = JSON.parse(text);
|
|
306
|
+
} catch {
|
|
307
|
+
return void 0;
|
|
308
|
+
}
|
|
309
|
+
if (parsed === null || typeof parsed !== "object") return void 0;
|
|
310
|
+
const candidate = typeof parsed.latest === "string" && parsed.latest.length > 0
|
|
311
|
+
? parsed.latest
|
|
312
|
+
: parsed.version;
|
|
313
|
+
if (typeof candidate !== "string") return void 0;
|
|
314
|
+
const trimmed = candidate.trim();
|
|
315
|
+
return /^\d+(?:\.\d+)*$/u.test(trimmed) ? trimmed : void 0;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** The renderer's fetch, if available (no Host route is added). */
|
|
319
|
+
function getFetch() {
|
|
320
|
+
try {
|
|
321
|
+
if (typeof fetch === "function") return fetch;
|
|
322
|
+
if (typeof globalThis !== "undefined" && typeof globalThis.fetch === "function") return globalThis.fetch;
|
|
323
|
+
} catch {
|
|
324
|
+
/* fetch unavailable */
|
|
325
|
+
}
|
|
326
|
+
return void 0;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** GET one JSON endpoint with an abort timeout; return its version. */
|
|
330
|
+
async function readVersionFrom(fetchImpl, url, timeoutMs) {
|
|
331
|
+
const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
|
|
332
|
+
const timer = controller !== void 0 ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
|
|
333
|
+
try {
|
|
334
|
+
const options = controller !== void 0 ? { signal: controller.signal } : {};
|
|
335
|
+
const response = await fetchImpl(url, options);
|
|
336
|
+
if (response === null || response === void 0 || response.ok === false) {
|
|
337
|
+
throw new Error(`HTTP ${response?.status ?? "no response"}`);
|
|
338
|
+
}
|
|
339
|
+
const text = await response.text();
|
|
340
|
+
const version = versionFromPayload(text);
|
|
341
|
+
if (version === void 0) throw new Error("no version in payload");
|
|
342
|
+
return version;
|
|
343
|
+
} finally {
|
|
344
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* npm first, GitHub raw package.json as a fallback. Both endpoints are
|
|
350
|
+
* public, need no authentication, and answer with permissive CORS.
|
|
351
|
+
* @param fetchImpl - fetch implementation.
|
|
352
|
+
* @param timeoutMs - per-request abort timeout.
|
|
353
|
+
* @returns {Promise<{version: string, source: string}>}
|
|
354
|
+
* @throws when both sources fail.
|
|
355
|
+
*/
|
|
356
|
+
async function fetchLatestVersion(fetchImpl, timeoutMs = 8000) {
|
|
357
|
+
let npmError;
|
|
358
|
+
try {
|
|
359
|
+
const version = await readVersionFrom(fetchImpl, NPM_VERSION_URL, timeoutMs);
|
|
360
|
+
return { version, source: "npm" };
|
|
361
|
+
} catch (error) {
|
|
362
|
+
npmError = error;
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
const version = await readVersionFrom(fetchImpl, GITHUB_VERSION_URL, timeoutMs);
|
|
366
|
+
return { version, source: "GitHub" };
|
|
367
|
+
} catch {
|
|
368
|
+
/* fall through to the npm error */
|
|
369
|
+
}
|
|
370
|
+
throw npmError ?? new Error("npm and GitHub both failed");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Run the update check. State transitions are published through
|
|
375
|
+
* setUpdateState() and onChange() lets the Settings view repaint. Overlap
|
|
376
|
+
* is ignored while a check is in flight.
|
|
377
|
+
* @param onChange - called after each published state.
|
|
378
|
+
*/
|
|
379
|
+
async function checkForUpdates(onChange) {
|
|
380
|
+
if (updateState.status === "checking") return;
|
|
381
|
+
const publish = (next) => {
|
|
382
|
+
setUpdateState(next);
|
|
383
|
+
if (typeof onChange === "function") {
|
|
384
|
+
try {
|
|
385
|
+
onChange();
|
|
386
|
+
} catch {
|
|
387
|
+
/* a dead view must not break the check */
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
const fetchImpl = getFetch();
|
|
392
|
+
if (fetchImpl === void 0) {
|
|
393
|
+
publish({ status: "failed", reason: COPY.updateNoFetch });
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
publish({ status: "checking" });
|
|
397
|
+
try {
|
|
398
|
+
const { version, source } = await fetchLatestVersion(fetchImpl);
|
|
399
|
+
publish(compareVersion(version, PLUGIN_VERSION) > 0
|
|
400
|
+
? { status: "found", latest: version, source }
|
|
401
|
+
: { status: "up-to-date", latest: version, source });
|
|
402
|
+
} catch (error) {
|
|
403
|
+
publish({ status: "failed", reason: String((error && error.message) || error) });
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* ONE stable guest partition for the whole process. release() destroys
|
|
409
|
+
* only the guest, never the partition's cookies, so this value is an
|
|
410
|
+
* absolute constant.
|
|
128
411
|
* @returns the fixed workspace identity for guest leases.
|
|
129
412
|
*/
|
|
130
413
|
function partitionKeyFor() {
|
|
@@ -132,9 +415,7 @@ window.__ModuleLoader__.load({
|
|
|
132
415
|
}
|
|
133
416
|
|
|
134
417
|
/**
|
|
135
|
-
* ONE stable conversation-memory key.
|
|
136
|
-
* move it either, or a transient probe failure would silently lose the
|
|
137
|
-
* remembered conversation.
|
|
418
|
+
* ONE stable conversation-memory key.
|
|
138
419
|
* @returns the fixed localStorage key.
|
|
139
420
|
*/
|
|
140
421
|
function storageKeyFor() {
|
|
@@ -190,8 +471,8 @@ window.__ModuleLoader__.load({
|
|
|
190
471
|
}
|
|
191
472
|
|
|
192
473
|
/**
|
|
193
|
-
* Read the conversation remembered from the last visit
|
|
194
|
-
* @param storageKey -
|
|
474
|
+
* Read the conversation remembered from the last visit.
|
|
475
|
+
* @param storageKey - storage key.
|
|
195
476
|
* @returns the URL to open; the site root when nothing usable is remembered.
|
|
196
477
|
*/
|
|
197
478
|
function rememberedTarget(storageKey) {
|
|
@@ -209,7 +490,7 @@ window.__ModuleLoader__.load({
|
|
|
209
490
|
* A brand-new chat clears the memory so the next visit starts clean. An auth
|
|
210
491
|
* page never overwrites it, so a login detour does not lose the conversation.
|
|
211
492
|
* @param raw - current page URL reported by the guest.
|
|
212
|
-
* @param storageKey -
|
|
493
|
+
* @param storageKey - storage key.
|
|
213
494
|
*/
|
|
214
495
|
function rememberTarget(raw, storageKey) {
|
|
215
496
|
const url = parseChatUrl(raw);
|
|
@@ -223,215 +504,6 @@ window.__ModuleLoader__.load({
|
|
|
223
504
|
}
|
|
224
505
|
}
|
|
225
506
|
|
|
226
|
-
// ── account sync store ────────────────────────────────────────────────
|
|
227
|
-
/**
|
|
228
|
-
* One module-level snapshot shared by every mounted panel. The account
|
|
229
|
-
* Remote is display/awareness only; it never feeds the lease identity or
|
|
230
|
-
* the guest lifecycle, so a flaky probe can never recreate the webview.
|
|
231
|
-
*/
|
|
232
|
-
let accountSnapshot = { status: ACCOUNT_UNKNOWN, label: "", probed: false };
|
|
233
|
-
const accountListeners = new Set();
|
|
234
|
-
|
|
235
|
-
function notifyAccount() {
|
|
236
|
-
for (const listener of [...accountListeners]) {
|
|
237
|
-
try {
|
|
238
|
-
listener(accountSnapshot);
|
|
239
|
-
} catch {
|
|
240
|
-
/* a dead panel must not break another one */
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/** Subscribe a mounted panel; returns its disposer. */
|
|
246
|
-
function subscribeAccount(listener) {
|
|
247
|
-
accountListeners.add(listener);
|
|
248
|
-
return () => {
|
|
249
|
-
accountListeners.delete(listener);
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Publish a probed account state. This ONLY updates the account strip:
|
|
255
|
-
* it never bumps an epoch and never rebuilds the guest. A transient
|
|
256
|
-
* "unknown" during reconnect is deliberately ignored so the displayed
|
|
257
|
-
* state does not flicker; a confirmed sign-out is shown immediately.
|
|
258
|
-
* @param status - normalized account state.
|
|
259
|
-
*/
|
|
260
|
-
function publishAccount(status) {
|
|
261
|
-
const previous = accountSnapshot;
|
|
262
|
-
// Ignore a transient unknown once we know a real state: it is usually
|
|
263
|
-
// a reconnect, not a sign-out, and must not disturb the panel.
|
|
264
|
-
const settled = status === ACCOUNT_UNKNOWN && previous.probed && previous.status !== ACCOUNT_UNKNOWN
|
|
265
|
-
? previous.status
|
|
266
|
-
: status;
|
|
267
|
-
accountSnapshot = {
|
|
268
|
-
status: settled,
|
|
269
|
-
// Preserve the name through a transient unknown; clear it only on a
|
|
270
|
-
// confirmed sign-out when the display explicitly publishes "".
|
|
271
|
-
label: settled === ACCOUNT_SIGNED_OUT ? "" : previous.label,
|
|
272
|
-
probed: true
|
|
273
|
-
};
|
|
274
|
-
notifyAccount();
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/** Publish a display name; a stale result from an older probe is dropped. */
|
|
278
|
-
function publishAccountLabel(label) {
|
|
279
|
-
if (accountSnapshot.label === label) return;
|
|
280
|
-
accountSnapshot = { ...accountSnapshot, label };
|
|
281
|
-
notifyAccount();
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Read one generated `ctx.remote.account` method. Generated Remotes answer
|
|
286
|
-
* with an `{ ok, value, error }` envelope, so a bare payload is never
|
|
287
|
-
* assumed; an absent namespace or a refused call degrades to undefined.
|
|
288
|
-
* @param ctx - client plugin context.
|
|
289
|
-
* @param method - generated account method name.
|
|
290
|
-
* @param args - method arguments.
|
|
291
|
-
* @returns the envelope's `value`, or undefined when unavailable.
|
|
292
|
-
*/
|
|
293
|
-
async function callAccount(ctx, method, args = []) {
|
|
294
|
-
let remote;
|
|
295
|
-
try {
|
|
296
|
-
remote = typeof ctx.get === "function" ? ctx.get("remote") : ctx.remote;
|
|
297
|
-
} catch {
|
|
298
|
-
remote = void 0;
|
|
299
|
-
}
|
|
300
|
-
const account = remote?.account;
|
|
301
|
-
const call = account?.[method];
|
|
302
|
-
if (typeof call !== "function") return void 0;
|
|
303
|
-
const result = await call.apply(account, args);
|
|
304
|
-
if (result === null || typeof result !== "object" || result.ok !== true) return void 0;
|
|
305
|
-
return result.value;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/** Platform client identity headers, read at call time as the UI does. */
|
|
309
|
-
function accountClient(ctx) {
|
|
310
|
-
return {
|
|
311
|
-
version: globalThis.__DSH_BOOT__?.release?.version || HOST_VERSION_FALLBACK,
|
|
312
|
-
locale: typeof navigator !== "undefined" ? navigator.language || "en-US" : "en-US",
|
|
313
|
-
timezoneOffsetSeconds: -new Date().getTimezoneOffset() * 60
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Read the live DSH account state and display name. This is awareness
|
|
319
|
-
* ONLY: no value here may ever feed the guest lease or rebuild the
|
|
320
|
-
* webview, or a transient Remote failure would drop the chat session.
|
|
321
|
-
* @param ctx - client plugin context.
|
|
322
|
-
* @returns `{ status, label }`.
|
|
323
|
-
*/
|
|
324
|
-
async function probeAccount(ctx) {
|
|
325
|
-
let state;
|
|
326
|
-
try {
|
|
327
|
-
state = await callAccount(ctx, "getState");
|
|
328
|
-
} catch {
|
|
329
|
-
state = void 0;
|
|
330
|
-
}
|
|
331
|
-
if (state?.status !== "credential-stored") {
|
|
332
|
-
return {
|
|
333
|
-
status: state?.status === "signed-out" ? ACCOUNT_SIGNED_OUT : ACCOUNT_UNKNOWN,
|
|
334
|
-
label: ""
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
const probe = { status: ACCOUNT_SIGNED_IN, label: "" };
|
|
338
|
-
try {
|
|
339
|
-
const outcome = await callAccount(ctx, "getProfile", [accountClient(ctx)]);
|
|
340
|
-
const value = outcome?.status === "ready" ? outcome.value : void 0;
|
|
341
|
-
probe.label = [value?.name, value?.contact, value?.id]
|
|
342
|
-
.find((item) => typeof item === "string" && item.length > 0) ?? "";
|
|
343
|
-
} catch {
|
|
344
|
-
/* profile is display-only: failure keeps the bare state */
|
|
345
|
-
}
|
|
346
|
-
return probe;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* Start the account watcher once per plugin activation. A 5-second poll
|
|
351
|
-
* plus `connection/reset` gives self-heal without depending on the exact
|
|
352
|
-
* Client shape of the account `watch` stream, and every resource is
|
|
353
|
-
* released through the plugin's own ctx effect.
|
|
354
|
-
* @param ctx - client plugin context.
|
|
355
|
-
*/
|
|
356
|
-
let accountSyncStarted = false;
|
|
357
|
-
function startAccountSync(ctx) {
|
|
358
|
-
if (accountSyncStarted) return;
|
|
359
|
-
accountSyncStarted = true;
|
|
360
|
-
let stopped = false;
|
|
361
|
-
let timer;
|
|
362
|
-
|
|
363
|
-
const refresh = async () => {
|
|
364
|
-
if (stopped) return;
|
|
365
|
-
let probe = { status: ACCOUNT_UNKNOWN, label: "" };
|
|
366
|
-
try {
|
|
367
|
-
probe = await probeAccount(ctx);
|
|
368
|
-
} catch {
|
|
369
|
-
/* Remote unavailable: publishAccount ignores a transient unknown */
|
|
370
|
-
probe = { status: ACCOUNT_UNKNOWN, label: "" };
|
|
371
|
-
}
|
|
372
|
-
if (stopped) return;
|
|
373
|
-
publishAccount(probe.status);
|
|
374
|
-
if (probe.label !== "") publishAccountLabel(probe.label);
|
|
375
|
-
};
|
|
376
|
-
|
|
377
|
-
const schedule = () => {
|
|
378
|
-
if (stopped) return;
|
|
379
|
-
timer = setTimeout(() => {
|
|
380
|
-
void refresh().finally(schedule);
|
|
381
|
-
}, 5000);
|
|
382
|
-
// A long-lived page keeps the poll; a minimal context (or a Node
|
|
383
|
-
// self-check) must not be held open by its timer alone.
|
|
384
|
-
try {
|
|
385
|
-
if (typeof timer.unref === "function") timer.unref();
|
|
386
|
-
} catch {
|
|
387
|
-
/* not a Node timer */
|
|
388
|
-
}
|
|
389
|
-
};
|
|
390
|
-
|
|
391
|
-
void refresh().finally(schedule);
|
|
392
|
-
|
|
393
|
-
let offReset;
|
|
394
|
-
try {
|
|
395
|
-
if (typeof ctx.on === "function") offReset = ctx.on("connection/reset", () => {
|
|
396
|
-
void refresh();
|
|
397
|
-
});
|
|
398
|
-
} catch {
|
|
399
|
-
/* optional: the poll still self-heals */
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
let hasDisposal = false;
|
|
403
|
-
try {
|
|
404
|
-
if (typeof ctx.effect === "function") {
|
|
405
|
-
ctx.effect(() => () => {
|
|
406
|
-
stopped = true;
|
|
407
|
-
if (timer !== void 0) clearTimeout(timer);
|
|
408
|
-
try {
|
|
409
|
-
if (typeof offReset === "function") offReset();
|
|
410
|
-
} catch {
|
|
411
|
-
/* already detached */
|
|
412
|
-
}
|
|
413
|
-
accountListeners.clear();
|
|
414
|
-
accountSyncStarted = false;
|
|
415
|
-
}, "deepseek-chat: account sync");
|
|
416
|
-
hasDisposal = true;
|
|
417
|
-
}
|
|
418
|
-
} catch {
|
|
419
|
-
/* fall through to the no-disposal path */
|
|
420
|
-
}
|
|
421
|
-
if (!hasDisposal) {
|
|
422
|
-
stopped = true;
|
|
423
|
-
if (timer !== void 0) clearTimeout(timer);
|
|
424
|
-
try {
|
|
425
|
-
if (typeof offReset === "function") offReset();
|
|
426
|
-
} catch {
|
|
427
|
-
/* already detached */
|
|
428
|
-
}
|
|
429
|
-
// Without a real ctx effect there is no teardown to stop the poll;
|
|
430
|
-
// keep the first probe but never leave a repeating timer behind.
|
|
431
|
-
accountSyncStarted = false;
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
507
|
// ── guest helpers ─────────────────────────────────────────────────────
|
|
436
508
|
/**
|
|
437
509
|
* Whether the guest element is still in the document. Electron's webview
|
|
@@ -502,29 +574,12 @@ window.__ModuleLoader__.load({
|
|
|
502
574
|
root: {
|
|
503
575
|
display: "flex",
|
|
504
576
|
flexDirection: "column",
|
|
577
|
+
flex: "1 1 auto",
|
|
505
578
|
height: "100%",
|
|
506
579
|
minHeight: "0",
|
|
507
580
|
background: "var(--dsw-alias-bg-base)",
|
|
508
581
|
color: "var(--dsw-alias-label-primary)"
|
|
509
582
|
},
|
|
510
|
-
accountBar: {
|
|
511
|
-
flex: "none",
|
|
512
|
-
display: "flex",
|
|
513
|
-
alignItems: "center",
|
|
514
|
-
gap: "6px",
|
|
515
|
-
minHeight: "24px",
|
|
516
|
-
padding: "3px 10px",
|
|
517
|
-
borderBottom: ".5px solid var(--dsw-alias-border-l3)",
|
|
518
|
-
color: "var(--dsw-alias-label-tertiary)",
|
|
519
|
-
font: "var(--dsw-font-xxxs-11)"
|
|
520
|
-
},
|
|
521
|
-
accountDot: {
|
|
522
|
-
width: "6px",
|
|
523
|
-
height: "6px",
|
|
524
|
-
borderRadius: "50%",
|
|
525
|
-
background: "currentColor",
|
|
526
|
-
flex: "none"
|
|
527
|
-
},
|
|
528
583
|
host: {
|
|
529
584
|
position: "relative",
|
|
530
585
|
flex: "1",
|
|
@@ -560,20 +615,121 @@ window.__ModuleLoader__.load({
|
|
|
560
615
|
padding: "4px 12px",
|
|
561
616
|
cursor: "pointer",
|
|
562
617
|
font: "var(--dsw-font-xxs-12)"
|
|
618
|
+
},
|
|
619
|
+
/**
|
|
620
|
+
* Tab entry wrapper. The conversation renderer's `viewArea` is a
|
|
621
|
+
* sibling of the sticky composerSeat; in the active phase it becomes
|
|
622
|
+
* `flex:1 0 auto` so `height:100%` on the panel has no definite
|
|
623
|
+
* parent height and the Session's own messages/composer stay visible
|
|
624
|
+
* below. An absolutely positioned overlay whose containing block is
|
|
625
|
+
* the conversation body (position:relative) covers exactly that body,
|
|
626
|
+
* hides the composer with an opaque background and a z-index above
|
|
627
|
+
* its 9, and leaves the header/tab row above the body clickable.
|
|
628
|
+
*/
|
|
629
|
+
overlay: {
|
|
630
|
+
position: "absolute",
|
|
631
|
+
inset: "0",
|
|
632
|
+
zIndex: "50",
|
|
633
|
+
display: "flex",
|
|
634
|
+
flexDirection: "column",
|
|
635
|
+
minHeight: "0",
|
|
636
|
+
overflow: "hidden",
|
|
637
|
+
background: "var(--dsw-alias-bg-base)"
|
|
638
|
+
},
|
|
639
|
+
settingsPage: {
|
|
640
|
+
boxSizing: "border-box",
|
|
641
|
+
maxWidth: "620px",
|
|
642
|
+
padding: "20px 24px",
|
|
643
|
+
color: "var(--dsw-alias-label-primary)"
|
|
644
|
+
},
|
|
645
|
+
settingsTitle: {
|
|
646
|
+
margin: "0 0 12px",
|
|
647
|
+
fontSize: "16px",
|
|
648
|
+
fontWeight: 600
|
|
649
|
+
},
|
|
650
|
+
settingsRow: {
|
|
651
|
+
display: "flex",
|
|
652
|
+
flexDirection: "column",
|
|
653
|
+
gap: "6px",
|
|
654
|
+
padding: "14px 0",
|
|
655
|
+
borderBottom: ".5px solid var(--dsw-alias-border-l3)"
|
|
656
|
+
},
|
|
657
|
+
settingsLabelRow: {
|
|
658
|
+
display: "flex",
|
|
659
|
+
alignItems: "center",
|
|
660
|
+
gap: "10px",
|
|
661
|
+
fontSize: "13px"
|
|
662
|
+
},
|
|
663
|
+
settingsCheckbox: {
|
|
664
|
+
width: "16px",
|
|
665
|
+
height: "16px",
|
|
666
|
+
margin: "0",
|
|
667
|
+
cursor: "pointer"
|
|
668
|
+
},
|
|
669
|
+
settingsHint: {
|
|
670
|
+
margin: "0",
|
|
671
|
+
fontSize: "12px",
|
|
672
|
+
lineHeight: "1.6",
|
|
673
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
674
|
+
},
|
|
675
|
+
settingsInput: {
|
|
676
|
+
boxSizing: "border-box",
|
|
677
|
+
width: "100%",
|
|
678
|
+
marginTop: "2px",
|
|
679
|
+
padding: "6px 10px",
|
|
680
|
+
border: ".5px solid var(--dsw-alias-border-l3)",
|
|
681
|
+
borderRadius: "var(--dsw-radius-sm)",
|
|
682
|
+
background: "var(--dsw-alias-bg-layer-1)",
|
|
683
|
+
color: "var(--dsw-alias-label-primary)",
|
|
684
|
+
font: "var(--dsw-font-xxs-12)"
|
|
685
|
+
},
|
|
686
|
+
aboutSectionTitle: {
|
|
687
|
+
margin: "18px 0 8px",
|
|
688
|
+
fontSize: "13px",
|
|
689
|
+
fontWeight: 600
|
|
690
|
+
},
|
|
691
|
+
aboutRow: {
|
|
692
|
+
display: "flex",
|
|
693
|
+
gap: "8px",
|
|
694
|
+
padding: "3px 0",
|
|
695
|
+
fontSize: "12px",
|
|
696
|
+
lineHeight: "1.6"
|
|
697
|
+
},
|
|
698
|
+
aboutLabel: {
|
|
699
|
+
flex: "0 0 88px",
|
|
700
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
701
|
+
},
|
|
702
|
+
aboutValue: {
|
|
703
|
+
flex: "1 1 auto",
|
|
704
|
+
minWidth: "0",
|
|
705
|
+
wordBreak: "break-word"
|
|
706
|
+
},
|
|
707
|
+
aboutLink: {
|
|
708
|
+
color: "var(--dsw-alias-label-link, var(--dsw-alias-label-primary))",
|
|
709
|
+
textDecoration: "underline",
|
|
710
|
+
cursor: "pointer",
|
|
711
|
+
marginRight: "14px"
|
|
712
|
+
},
|
|
713
|
+
updateButton: {
|
|
714
|
+
color: "var(--dsw-alias-label-primary)",
|
|
715
|
+
background: "var(--dsw-alias-bg-layer-1)",
|
|
716
|
+
border: ".5px solid var(--dsw-alias-border-l3)",
|
|
717
|
+
borderRadius: "var(--dsw-radius-sm)",
|
|
718
|
+
padding: "6px 14px",
|
|
719
|
+
cursor: "pointer",
|
|
720
|
+
font: "var(--dsw-font-xxs-12)"
|
|
721
|
+
},
|
|
722
|
+
updateResult: {
|
|
723
|
+
margin: "8px 0 0",
|
|
724
|
+
padding: "8px 10px",
|
|
725
|
+
borderRadius: "var(--dsw-radius-sm)",
|
|
726
|
+
background: "var(--dsw-alias-bg-layer-1)",
|
|
727
|
+
fontSize: "12px",
|
|
728
|
+
lineHeight: "1.7",
|
|
729
|
+
color: "var(--dsw-alias-label-primary)"
|
|
563
730
|
}
|
|
564
731
|
};
|
|
565
732
|
|
|
566
|
-
/** Copy for the account strip; the dot's opacity encodes the state. */
|
|
567
|
-
function accountCopy(account) {
|
|
568
|
-
if (account.status === ACCOUNT_SIGNED_IN) return {
|
|
569
|
-
text: account.label ? COPY.syncedAs(account.label) : COPY.synced,
|
|
570
|
-
opacity: "1"
|
|
571
|
-
};
|
|
572
|
-
if (account.status === ACCOUNT_SIGNED_OUT) return { text: COPY.unsigned, opacity: ".45" };
|
|
573
|
-
if (!account.probed) return { text: COPY.probing, opacity: ".3" };
|
|
574
|
-
return { text: COPY.unknown, opacity: ".3" };
|
|
575
|
-
}
|
|
576
|
-
|
|
577
733
|
/**
|
|
578
734
|
* Keeps a rendering failure inside this panel. DSH has no boundary of its own
|
|
579
735
|
* around slot entries: an escaping error unmounts the root slot, which is the
|
|
@@ -608,32 +764,22 @@ window.__ModuleLoader__.load({
|
|
|
608
764
|
}
|
|
609
765
|
|
|
610
766
|
/**
|
|
611
|
-
* The
|
|
612
|
-
*
|
|
767
|
+
* The web chat panel: a guest webview filling the available column, with
|
|
768
|
+
* no account strip above it.
|
|
613
769
|
*
|
|
614
770
|
* The guest is created only after the lease resolves, and it is mounted with
|
|
615
771
|
* `name`/`partition`/`src` already set, because the main process inspects
|
|
616
772
|
* exactly that bootstrap src when it approves the attachment. The identity
|
|
617
|
-
* and the conversation memory are both
|
|
618
|
-
*
|
|
619
|
-
* re-acquires a lease for the new identity.
|
|
773
|
+
* and the conversation memory are both absolute constants, so switching
|
|
774
|
+
* between the sidebar panel and the conversation tab preserves the login.
|
|
620
775
|
*/
|
|
621
776
|
function ChatPanel() {
|
|
622
777
|
const hostRef = react.useRef(null);
|
|
623
778
|
const lastUrlRef = react.useRef(void 0);
|
|
624
|
-
const [account, setAccount] = react.useState(accountSnapshot);
|
|
625
779
|
const [failure, setFailure] = react.useState(void 0);
|
|
626
780
|
const [loading, setLoading] = react.useState(true);
|
|
627
781
|
const [attempt, setAttempt] = react.useState(0);
|
|
628
782
|
|
|
629
|
-
react.useEffect(() => subscribeAccount((next) => {
|
|
630
|
-
try {
|
|
631
|
-
setAccount(next);
|
|
632
|
-
} catch {
|
|
633
|
-
/* the panel is gone */
|
|
634
|
-
}
|
|
635
|
-
}), []);
|
|
636
|
-
|
|
637
783
|
const retry = react.useCallback(() => {
|
|
638
784
|
try {
|
|
639
785
|
setFailure(void 0);
|
|
@@ -852,12 +998,7 @@ window.__ModuleLoader__.load({
|
|
|
852
998
|
};
|
|
853
999
|
}, [attempt]);
|
|
854
1000
|
|
|
855
|
-
const copy = accountCopy(account);
|
|
856
|
-
|
|
857
1001
|
return react.createElement("div", { style: styles.root },
|
|
858
|
-
react.createElement("div", { style: styles.accountBar },
|
|
859
|
-
react.createElement("span", { style: { ...styles.accountDot, opacity: copy.opacity }, "aria-hidden": "true" }),
|
|
860
|
-
react.createElement("span", null, copy.text)),
|
|
861
1002
|
react.createElement("div", { style: styles.host, ref: hostRef },
|
|
862
1003
|
failure === void 0 && loading
|
|
863
1004
|
? react.createElement("div", { style: styles.notice }, COPY.loading)
|
|
@@ -874,49 +1015,234 @@ window.__ModuleLoader__.load({
|
|
|
874
1015
|
return react.createElement(PanelBoundary, null, react.createElement(ChatPanel));
|
|
875
1016
|
}
|
|
876
1017
|
|
|
1018
|
+
/**
|
|
1019
|
+
* The conversation top-bar tab. Unlike the left-sidebar panel, this entry
|
|
1020
|
+
* must fill the whole conversation body and cover the Session's own
|
|
1021
|
+
* messages/composer (the "页面没有铺满下面" bug): an opaque, absolutely
|
|
1022
|
+
* positioned overlay on the conversation body's containing block.
|
|
1023
|
+
* The component is created ONCE (not per render) so React never remounts
|
|
1024
|
+
* the guest webview on an unrelated re-render.
|
|
1025
|
+
*/
|
|
1026
|
+
function ViewTabShell() {
|
|
1027
|
+
return react.createElement("div", {
|
|
1028
|
+
"data-dsh-plugin-deepseek-chat": "view",
|
|
1029
|
+
style: styles.overlay
|
|
1030
|
+
}, react.createElement(PanelBoundary, null, react.createElement(ChatPanel)));
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/** One metadata row: a fixed-width label plus a value. */
|
|
1034
|
+
function infoRow(label, value) {
|
|
1035
|
+
return react.createElement("div", { style: styles.aboutRow },
|
|
1036
|
+
react.createElement("span", { style: styles.aboutLabel }, label),
|
|
1037
|
+
react.createElement("span", { style: styles.aboutValue }, value));
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/** External https link; the desktop shell opens it in the system browser. */
|
|
1041
|
+
function externalLink(href, text) {
|
|
1042
|
+
return react.createElement("a", {
|
|
1043
|
+
href,
|
|
1044
|
+
target: "_blank",
|
|
1045
|
+
rel: "noopener noreferrer",
|
|
1046
|
+
style: styles.aboutLink
|
|
1047
|
+
}, text);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/** The two jump links shown in the About block and after an update check. */
|
|
1051
|
+
function linkRow() {
|
|
1052
|
+
return react.createElement("div", { style: styles.aboutRow },
|
|
1053
|
+
react.createElement("span", { style: styles.aboutLabel }, COPY.infoLinks),
|
|
1054
|
+
react.createElement("span", { style: styles.aboutValue },
|
|
1055
|
+
externalLink(NPM_PAGE_URL, COPY.linkNpm),
|
|
1056
|
+
externalLink(GITHUB_PAGE_URL, COPY.linkGithub)));
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/** Human-readable one-line status for the current update-check state. */
|
|
1060
|
+
function updateStatusLine(state) {
|
|
1061
|
+
if (state.status === "idle") return COPY.updateIdle;
|
|
1062
|
+
if (state.status === "checking") return COPY.updateChecking;
|
|
1063
|
+
let line;
|
|
1064
|
+
if (state.status === "found") {
|
|
1065
|
+
line = `${COPY.updateFound} ${state.latest}(${COPY.updateCurrent} ${PLUGIN_VERSION})`;
|
|
1066
|
+
} else if (state.status === "up-to-date") {
|
|
1067
|
+
line = `${COPY.updateUpToDate}(${COPY.updateLatest} ${state.latest})`;
|
|
1068
|
+
} else {
|
|
1069
|
+
line = `${COPY.updateFailed}:${state.reason ?? ""}`;
|
|
1070
|
+
}
|
|
1071
|
+
if (state.source !== void 0) line += ` · ${COPY.updateSource} ${state.source}`;
|
|
1072
|
+
return line;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* Settings page. Built once and reused: writing a checkbox or typing in the
|
|
1077
|
+
* text box calls writeConfig(), which persists and notifies apply()'s
|
|
1078
|
+
* subscriber; the local counter only refreshes this form's own state. The
|
|
1079
|
+
* About block and the "检查更新" button are also part of this one component.
|
|
1080
|
+
*/
|
|
1081
|
+
function makeSettingsView() {
|
|
1082
|
+
return function SettingsView() {
|
|
1083
|
+
const state = react.useState(0);
|
|
1084
|
+
const rerender = () => state[1]((value) => value + 1);
|
|
1085
|
+
const current = getConfig();
|
|
1086
|
+
const update = getUpdateState();
|
|
1087
|
+
// The default tab label is shown as a grey placeholder, never as a
|
|
1088
|
+
// solid value the user must delete first. `draft` is component-local:
|
|
1089
|
+
// it stays empty while the config holds the default, and follows what
|
|
1090
|
+
// the user types. Clearing it makes writeConfig() fall back to the
|
|
1091
|
+
// default again, so the box returns to its empty placeholder state.
|
|
1092
|
+
const draftState = react.useState(current.tabLabel === DEFAULT_TAB_LABEL ? "" : current.tabLabel);
|
|
1093
|
+
const draft = draftState[0];
|
|
1094
|
+
const setDraft = draftState[1];
|
|
1095
|
+
|
|
1096
|
+
const onToggle = (event) => {
|
|
1097
|
+
writeConfig({ showTab: !!event?.target?.checked });
|
|
1098
|
+
rerender();
|
|
1099
|
+
};
|
|
1100
|
+
const onLabelChange = (event) => {
|
|
1101
|
+
const value = String(event?.target?.value ?? "");
|
|
1102
|
+
// Whitespace-only input resolves back to the default name, so keep
|
|
1103
|
+
// the box empty to let the grey placeholder show even without a
|
|
1104
|
+
// remount (real React keeps this component mounted).
|
|
1105
|
+
setDraft(value.trim().length === 0 ? "" : value);
|
|
1106
|
+
writeConfig({ tabLabel: value });
|
|
1107
|
+
rerender();
|
|
1108
|
+
};
|
|
1109
|
+
const onCheckUpdate = () => {
|
|
1110
|
+
void checkForUpdates(rerender);
|
|
1111
|
+
};
|
|
1112
|
+
|
|
1113
|
+
const showLinks = update.status === "found" || update.status === "up-to-date" || update.status === "failed";
|
|
1114
|
+
|
|
1115
|
+
return react.createElement("div", { style: styles.settingsPage },
|
|
1116
|
+
react.createElement("h2", { style: styles.settingsTitle }, COPY.settingsTitle),
|
|
1117
|
+
react.createElement("div", { style: styles.settingsRow },
|
|
1118
|
+
react.createElement("label", { style: styles.settingsLabelRow },
|
|
1119
|
+
react.createElement("input", {
|
|
1120
|
+
type: "checkbox",
|
|
1121
|
+
style: styles.settingsCheckbox,
|
|
1122
|
+
checked: current.showTab !== false,
|
|
1123
|
+
onChange: onToggle
|
|
1124
|
+
}),
|
|
1125
|
+
COPY.settingsEnableTab),
|
|
1126
|
+
react.createElement("p", { style: styles.settingsHint }, COPY.settingsEnableHint)),
|
|
1127
|
+
react.createElement("div", { style: styles.settingsRow },
|
|
1128
|
+
react.createElement("label", { style: styles.settingsLabelRow }, COPY.settingsTabLabel),
|
|
1129
|
+
react.createElement("input", {
|
|
1130
|
+
type: "text",
|
|
1131
|
+
style: styles.settingsInput,
|
|
1132
|
+
value: draft,
|
|
1133
|
+
placeholder: COPY.settingsPlaceholder,
|
|
1134
|
+
maxLength: 40,
|
|
1135
|
+
onChange: onLabelChange
|
|
1136
|
+
}),
|
|
1137
|
+
react.createElement("p", { style: styles.settingsHint }, COPY.settingsTabLabelHint)),
|
|
1138
|
+
|
|
1139
|
+
// 关于本插件:版本号 / 作者 / 功能简介 / 许可证 / 相关链接。
|
|
1140
|
+
react.createElement("h3", { style: styles.aboutSectionTitle }, COPY.aboutTitle),
|
|
1141
|
+
infoRow(COPY.infoVersion, PLUGIN_VERSION),
|
|
1142
|
+
infoRow(COPY.infoAuthor, PLUGIN_AUTHOR),
|
|
1143
|
+
infoRow(COPY.infoDescription, COPY.infoSummary),
|
|
1144
|
+
infoRow(COPY.infoLicense, PLUGIN_LICENSE),
|
|
1145
|
+
linkRow(),
|
|
1146
|
+
|
|
1147
|
+
// 检查更新:按钮 + 文字状态 + 跳转链接。
|
|
1148
|
+
react.createElement("h3", { style: styles.aboutSectionTitle }, COPY.updateTitle),
|
|
1149
|
+
react.createElement("button", {
|
|
1150
|
+
type: "button",
|
|
1151
|
+
"data-dsh-plugin-deepseek-chat": "check-update",
|
|
1152
|
+
style: styles.updateButton,
|
|
1153
|
+
disabled: update.status === "checking",
|
|
1154
|
+
onClick: onCheckUpdate
|
|
1155
|
+
}, COPY.checkUpdate),
|
|
1156
|
+
react.createElement("div", {
|
|
1157
|
+
"data-dsh-plugin-deepseek-chat": "update-status",
|
|
1158
|
+
style: styles.updateResult
|
|
1159
|
+
},
|
|
1160
|
+
react.createElement("div", null, updateStatusLine(update)),
|
|
1161
|
+
showLinks ? linkRow() : null));
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
|
|
877
1165
|
/** Client service this plugin reads; the loader waits for the slot registry. */
|
|
878
1166
|
const inject = ["slots"];
|
|
879
1167
|
|
|
880
1168
|
/**
|
|
881
|
-
* Register the
|
|
1169
|
+
* Register the contributions. On the Web surface there is no desktop guest
|
|
1170
|
+
* bridge and an iframe cannot carry the site, so the plugin stays inert
|
|
1171
|
+
* there. Each registration is isolated: one bad slot must not undo the
|
|
1172
|
+
* others.
|
|
1173
|
+
*
|
|
1174
|
+
* The conversation.view tab is dynamic: its label comes from the Settings
|
|
1175
|
+
* name, and its visibility from the showTab switch. The tab row only
|
|
1176
|
+
* recomputes when the slot registry changes, so on any preference change we
|
|
1177
|
+
* dispose that single injection and re-inject it (label thunk + showTab
|
|
1178
|
+
* check). The main/sidebar/settings entries are registered once.
|
|
882
1179
|
* @param ctx - client plugin context.
|
|
883
1180
|
*/
|
|
884
1181
|
function apply(ctx) {
|
|
1182
|
+
let bridge;
|
|
885
1183
|
try {
|
|
886
|
-
|
|
887
|
-
// cannot carry the site, so the plugin stays inert there.
|
|
888
|
-
if (globalThis.dshDesktop?.browser === void 0) return;
|
|
1184
|
+
bridge = globalThis.dshDesktop?.browser;
|
|
889
1185
|
} catch {
|
|
890
1186
|
return;
|
|
891
1187
|
}
|
|
1188
|
+
if (bridge === void 0 || bridge === null) return;
|
|
892
1189
|
|
|
893
|
-
|
|
894
|
-
startAccountSync(ctx);
|
|
895
|
-
} catch (error) {
|
|
1190
|
+
const register = (slot, options, component) => {
|
|
896
1191
|
try {
|
|
897
|
-
|
|
898
|
-
} catch {
|
|
899
|
-
|
|
1192
|
+
return ctx.slots.inject(slot, () => ctx.slots.register(options, component));
|
|
1193
|
+
} catch (error) {
|
|
1194
|
+
try {
|
|
1195
|
+
console.error(`dsh-plugin-deepseek-chat: ${slot} registration failed`, error);
|
|
1196
|
+
} catch {
|
|
1197
|
+
/* console unavailable */
|
|
1198
|
+
}
|
|
1199
|
+
return void 0;
|
|
900
1200
|
}
|
|
901
|
-
}
|
|
1201
|
+
};
|
|
902
1202
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1203
|
+
// Static entries: left-sidebar panel + full main panel + Settings page.
|
|
1204
|
+
register("main", { name: "main", key: PANEL_ID }, ChatPanelShell);
|
|
1205
|
+
register("sidebar.panellist", {
|
|
1206
|
+
name: "sidebar.panellist",
|
|
1207
|
+
id: PANEL_ID,
|
|
1208
|
+
order: 5,
|
|
1209
|
+
label: () => COPY.label
|
|
1210
|
+
}, ChatGlyph);
|
|
1211
|
+
register("settings.section", {
|
|
1212
|
+
name: "settings.section",
|
|
1213
|
+
id: SETTINGS_ID,
|
|
1214
|
+
order: SETTINGS_ORDER,
|
|
1215
|
+
label: () => COPY.settingsNav
|
|
1216
|
+
}, makeSettingsView());
|
|
1217
|
+
|
|
1218
|
+
// Dynamic entry: the conversation top-bar tab. Re-injected whenever
|
|
1219
|
+
// a preference changes so the tab row recomputes id/label/visibility.
|
|
1220
|
+
let disposeView = void 0;
|
|
1221
|
+
const syncView = () => {
|
|
1222
|
+
if (typeof disposeView === "function") {
|
|
1223
|
+
try {
|
|
1224
|
+
disposeView();
|
|
1225
|
+
} catch {
|
|
1226
|
+
/* already gone */
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
disposeView = void 0;
|
|
1230
|
+
if (config.showTab === false) return;
|
|
1231
|
+
disposeView = register("conversation.view", {
|
|
1232
|
+
name: "conversation.view",
|
|
1233
|
+
id: VIEW_ID,
|
|
1234
|
+
order: VIEW_ORDER,
|
|
1235
|
+
label: () => tabLabel()
|
|
1236
|
+
}, ViewTabShell);
|
|
1237
|
+
};
|
|
1238
|
+
syncView();
|
|
1239
|
+
|
|
1240
|
+
const unsubscribe = subscribeConfig(syncView);
|
|
1241
|
+
if (typeof ctx.effect === "function") {
|
|
916
1242
|
try {
|
|
917
|
-
|
|
1243
|
+
ctx.effect(() => unsubscribe, "dsh-plugin-deepseek-chat: settings sync");
|
|
918
1244
|
} catch {
|
|
919
|
-
/*
|
|
1245
|
+
/* effect registration unavailable: the subscriber still lives */
|
|
920
1246
|
}
|
|
921
1247
|
}
|
|
922
1248
|
}
|
|
@@ -924,24 +1250,41 @@ window.__ModuleLoader__.load({
|
|
|
924
1250
|
exports.apply = apply;
|
|
925
1251
|
exports.inject = inject;
|
|
926
1252
|
exports.__test__ = {
|
|
927
|
-
ACCOUNT_UNKNOWN,
|
|
928
|
-
ACCOUNT_SIGNED_IN,
|
|
929
|
-
ACCOUNT_SIGNED_OUT,
|
|
930
1253
|
CHAT_URL,
|
|
931
1254
|
STORAGE_BASE,
|
|
932
|
-
|
|
933
|
-
|
|
1255
|
+
PANEL_ID,
|
|
1256
|
+
VIEW_ID,
|
|
1257
|
+
VIEW_ORDER,
|
|
1258
|
+
SETTINGS_ID,
|
|
1259
|
+
SETTINGS_ORDER,
|
|
1260
|
+
CONFIG_KEY,
|
|
1261
|
+
DEFAULTS,
|
|
1262
|
+
PLUGIN_NAME,
|
|
1263
|
+
PLUGIN_VERSION,
|
|
1264
|
+
PLUGIN_AUTHOR,
|
|
1265
|
+
PLUGIN_LICENSE,
|
|
1266
|
+
NPM_PAGE_URL,
|
|
1267
|
+
GITHUB_PAGE_URL,
|
|
1268
|
+
NPM_VERSION_URL,
|
|
1269
|
+
GITHUB_VERSION_URL,
|
|
1270
|
+
parseChatUrl,
|
|
1271
|
+
parsePopupUrl,
|
|
934
1272
|
partitionKeyFor,
|
|
935
1273
|
storageKeyFor,
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1274
|
+
readConfig,
|
|
1275
|
+
getConfig,
|
|
1276
|
+
normalizeTabLabel,
|
|
1277
|
+
tabLabel,
|
|
1278
|
+
writeConfig,
|
|
1279
|
+
subscribeConfig,
|
|
1280
|
+
compareVersion,
|
|
1281
|
+
versionFromPayload,
|
|
1282
|
+
readVersionFrom,
|
|
1283
|
+
fetchLatestVersion,
|
|
1284
|
+
getUpdateState,
|
|
1285
|
+
setUpdateState,
|
|
1286
|
+
resetUpdateState,
|
|
1287
|
+
checkForUpdates
|
|
945
1288
|
};
|
|
946
1289
|
return module.exports;
|
|
947
1290
|
}
|