dsh-dock 0.20.0 → 0.21.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/README.md +15 -10
- package/client.js +1403 -654
- package/features/balance/view.js +8 -6
- package/features/mobile/host.js +22 -0
- package/features/mobile/view.js +345 -0
- package/features/notify/host.js +50 -15
- package/features/notify/view.jsx +713 -367
- package/features/tokenlog/view.jsx +8 -3
- package/index.js +3 -0
- package/package.json +2 -2
- package/screenshots/dock-notify.png +0 -0
- package/src/client.jsx +38 -7
- package/src/host-core.js +6 -0
package/features/balance/view.js
CHANGED
|
@@ -260,10 +260,10 @@ function chipBalanceText(p) {
|
|
|
260
260
|
const b = p && p.balance;
|
|
261
261
|
if (!b) return "余额 …";
|
|
262
262
|
if (b.status !== "ok") {
|
|
263
|
-
if (b.status === "login-required") return "
|
|
264
|
-
if (b.status === "no-credential") return "
|
|
265
|
-
if (b.status === "unsupported") return "
|
|
266
|
-
return "
|
|
263
|
+
if (b.status === "login-required") return "余额 需登录";
|
|
264
|
+
if (b.status === "no-credential") return "余额 无密钥";
|
|
265
|
+
if (b.status === "unsupported") return "余额 不支持";
|
|
266
|
+
return "余额 查询失败";
|
|
267
267
|
}
|
|
268
268
|
if (b.kind === "quota") {
|
|
269
269
|
const v = Number(b.remaining);
|
|
@@ -289,11 +289,12 @@ function BalanceChip(props) {
|
|
|
289
289
|
const def = selProvider || (data && data.default ? data.default.provider : null);
|
|
290
290
|
const cur = def ? providers.find((p) => p.id === def) : null;
|
|
291
291
|
const accent = cur ? accentOf(cur.id) : "#64748b";
|
|
292
|
-
const text = snap.error ? "
|
|
292
|
+
const text = snap.error ? "余额 失败" : (!data ? (snap.loading ? "余额 …" : "余额 —") : (cur ? chipBalanceText(cur) : "余额 —"));
|
|
293
293
|
const selModel = sel && (sel.model || (sel.selected && sel.selected.model)) || null;
|
|
294
294
|
const title = cur
|
|
295
295
|
? (cur.displayName || cur.id) + (selModel ? " · 当前模型 " + selModel : "") + " · 点击在功能坞查看余额详情"
|
|
296
296
|
: "点击打开功能坞 · 模型余额";
|
|
297
|
+
const mm = /^(余额 )([\s\S]*)$/.exec(text);
|
|
297
298
|
return react.createElement("button", {
|
|
298
299
|
type: "button",
|
|
299
300
|
className: "dockchip" + (snap.error || (cur && cur.balance && cur.balance.status === "error") ? " err" : ""),
|
|
@@ -302,7 +303,8 @@ function BalanceChip(props) {
|
|
|
302
303
|
onClick: () => openPanel("balance", def ? { provider: def } : null),
|
|
303
304
|
},
|
|
304
305
|
react.createElement("span", { className: "dockchip-dot", style: { background: accent } }),
|
|
305
|
-
react.createElement("span",
|
|
306
|
+
mm ? react.createElement("span", { className: "dockchip-pre" }, mm[1]) : null,
|
|
307
|
+
react.createElement("span", null, mm ? mm[2] : text));
|
|
306
308
|
}
|
|
307
309
|
|
|
308
310
|
export const feature = {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// dsh-dock · 功能模块【手机适配】· 宿主半部(开关占位)
|
|
2
|
+
//
|
|
3
|
+
// 手机适配的实现全部在客户端半部(features/mobile/view.js):样式与行为在 Overlay
|
|
4
|
+
// 挂载时注入、卸载时移除,所以这个宿主半部**什么都不注入**,只保留一个 feature 描述符,
|
|
5
|
+
// 让功能开关能照常持久化(settings 的 features 表按 id 记开关,路由 /dsh-dock/features
|
|
6
|
+
// 用 FEATURES 列表校验 id 是否存在)。
|
|
7
|
+
//
|
|
8
|
+
// 为什么不再经 webServer.tapIndex 内联注入 index.html:
|
|
9
|
+
// · 旧版这样做必须重启 dsh web 才能彻底摘除注入的 <style>/<script>;用户要求
|
|
10
|
+
// 「关闭就要关闭得彻底、不能干扰原来的」。改成客户端运行时注入 + 卸载清理后,
|
|
11
|
+
// 关闭开关的瞬间样式、body 属性、遮罩/按钮、监听器、观察器、定时器全部消失,
|
|
12
|
+
// index.html 里一个标记都不残留,也不动 <meta viewport>。
|
|
13
|
+
export const feature = {
|
|
14
|
+
id: 'mobile',
|
|
15
|
+
name: '手机适配',
|
|
16
|
+
description: '手机/窄屏体验优化(客户端运行时注入,关闭后零残留):16px 输入防缩放、触控热区、代码块滚动、会话 chip 收起、设置弹层单列与旧版抽屉兜底',
|
|
17
|
+
defaultEnabled: false,
|
|
18
|
+
setup() {
|
|
19
|
+
// 无宿主副作用;返回空清理函数以匹配生命周期约定。
|
|
20
|
+
return () => {}
|
|
21
|
+
},
|
|
22
|
+
}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
// dsh-dock · 功能模块【手机适配】· 客户端视图(唯一实现)
|
|
2
|
+
//
|
|
3
|
+
// 这一版刻意「做减法」:手机适配的全部逻辑都放在客户端这一半,宿主半部
|
|
4
|
+
// (features/mobile/host.js)只剩一个开关占位。原因有三:
|
|
5
|
+
//
|
|
6
|
+
// 1) 关闭要彻底。旧版把排版样式与行为脚本经 webServer.tapIndex 内联注入 index.html,
|
|
7
|
+
// 必须重启 dsh web 才能彻底摘除;现在改成「Overlay 挂载时才注入、卸载时立刻移除」,
|
|
8
|
+
// 关闭开关 = Overlay 卸载 = <style> 元素、body 属性、遮罩/按钮、监听器、观察器、
|
|
9
|
+
// 定时器全部一并消失,页面 <head> 不残留任何标记,也不动 <meta viewport>。
|
|
10
|
+
// 2) 不与 dsh 原生布局打架。dsh 自身(<768px)已是抽屉模式、设置弹层已是单列、
|
|
11
|
+
// 账号卡片排版正常。旧版强行 position:fixed + 100vw + flex-direction 覆盖,
|
|
12
|
+
// 反而把好好的原生排版改坏(账号名一字一行、查询用量/充值被裁)。
|
|
13
|
+
// 这一版只补原生没做的事,且用「行为检测」决定是否兜底旧版:
|
|
14
|
+
// · 设置弹层:仅当实测仍是「左导航 + 右内容」两栏时才加 data-dsh-mobile-stack
|
|
15
|
+
// 改为纵向堆叠(现代 dsh 已单列 → 检测为已堆叠 → 不加,原生排版不动);
|
|
16
|
+
// · 侧栏:仅当 <768 却仍是静态轨道(旧版 dsh 无抽屉模式)时才浮层化 + 注入
|
|
17
|
+
// 汉堡/遮罩(现代 dsh 有 data-sidebar-drawer → 不动,用原生抽屉)。
|
|
18
|
+
// 3) 单一来源。样式只此一份(MOBILE_CSS),不存在「宿主一份、客户端一份」对不齐。
|
|
19
|
+
//
|
|
20
|
+
// 只保留真正常见的手机痛点优化:
|
|
21
|
+
// · 输入类控件字号 16px —— iOS 聚焦 <16px 输入框会强制整页放大;
|
|
22
|
+
// · 触控基础:去点按高亮、touch-action:manipulation(消双击缩放判定延迟);
|
|
23
|
+
// · 原生侧栏开关点击热区扩到 44×44(图标位置视觉不变);
|
|
24
|
+
// · 代码块横向滚动、行内代码换行;
|
|
25
|
+
// · 会话输入行上 dsh-dock 的随身小控件(余额/用量 chip)在手机上整组收起——
|
|
26
|
+
// 窄屏下它们被压成「余额…」既读不出数值又白占输入行;
|
|
27
|
+
// · 设置弹层内容区补安全区内边距、滚动体补底部安全区。
|
|
28
|
+
//
|
|
29
|
+
// 命中:窗口宽 ≤767px,或主指针为粗触且无悬停(真机/平板)。
|
|
30
|
+
import react from "react";
|
|
31
|
+
import { useEffect, useState } from "react";
|
|
32
|
+
|
|
33
|
+
/** 与 dsh 原生窄屏断点对齐(SIDEBAR_DRAWER_BREAKPOINT=768):<768 才做抽屉/单列兜底。 */
|
|
34
|
+
const MOBILE_QUERY = "(max-width: 767px)";
|
|
35
|
+
/** 粗指针且无悬停:真机/平板(不含桌面鼠标);用于「不分宽度的触控优化」。 */
|
|
36
|
+
const COARSE_QUERY = "(pointer: coarse) and (hover: none)";
|
|
37
|
+
|
|
38
|
+
/** 当前视口是否命中手机形态(SSR/无 window 时恒 false)。 */
|
|
39
|
+
export function mobileActive() {
|
|
40
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
|
|
41
|
+
try {
|
|
42
|
+
return window.matchMedia(MOBILE_QUERY).matches || window.matchMedia(COARSE_QUERY).matches;
|
|
43
|
+
} catch { return false; }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// —— 运行时样式(挂载时注入、卸载时移除;全部选择器再以 body[data-dk-mobile="1"] 兜底,
|
|
47
|
+
// 即便某帧 body 属性先被摘掉也不会误伤原生排版) ——
|
|
48
|
+
const MOBILE_CSS = [
|
|
49
|
+
'/* 触控与输入:粗指针真机(不分宽度) */',
|
|
50
|
+
'@media (pointer: coarse) and (hover: none){',
|
|
51
|
+
' body[data-dk-mobile="1"] :is(textarea,input:not([type]),input[type="text"],input[type="search"],input[type="url"],input[type="password"],input[type="number"],input[type="email"],[contenteditable="true"]){font-size:16px !important;}',
|
|
52
|
+
' body[data-dk-mobile="1"] :is(button,[role="button"]){-webkit-tap-highlight-color:transparent;touch-action:manipulation;}',
|
|
53
|
+
'}',
|
|
54
|
+
'@media (max-width:767px){',
|
|
55
|
+
' body[data-dk-mobile="1"] :is(textarea,input:not([type]),input[type="text"],input[type="search"],input[type="url"],input[type="password"],input[type="number"],input[type="email"],[contenteditable="true"]){font-size:16px !important;}',
|
|
56
|
+
' body[data-dk-mobile="1"] :is(button,[role="button"]){-webkit-tap-highlight-color:transparent;touch-action:manipulation;}',
|
|
57
|
+
' body[data-dk-mobile="1"] pre{max-width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;}',
|
|
58
|
+
' body[data-dk-mobile="1"] code{overflow-wrap:anywhere;}',
|
|
59
|
+
' body[data-dk-mobile="1"] pre code{overflow-wrap:normal;}',
|
|
60
|
+
' body[data-dk-mobile="1"] .dockchip-row{display:none;}',
|
|
61
|
+
' body[data-dk-mobile="1"] [data-sidebar-opener]{position:relative;}',
|
|
62
|
+
' body[data-dk-mobile="1"] [data-sidebar-opener]::after{content:"";position:absolute;left:50%;top:50%;width:44px;height:44px;transform:translate(-50%,-50%);}',
|
|
63
|
+
' body[data-dk-mobile="1"] [role="dialog"][class*="panel"] [class*="options"]{padding-left:max(16px,env(safe-area-inset-left,0px));padding-right:max(16px,env(safe-area-inset-right,0px));}',
|
|
64
|
+
' body[data-dk-mobile="1"] [class*="scrollBody"]{padding-bottom:env(safe-area-inset-bottom,0px) !important;}',
|
|
65
|
+
' /* 功能坞面板(.dockm-*)挂在 .overlayLayer(z20) 内,而手机抽屉 .sidebarCol 是 z71——面板虽是全屏却被抽屉盖住。把 overlayLayer 抬到抽屉之上,功能坞/全局浮层才能全屏盖住抽屉。 */',
|
|
66
|
+
' body[data-dk-mobile="1"] [class*="overlayLayer"]{z-index:100 !important;}',
|
|
67
|
+
' body[data-dk-mobile="1"] [role="dialog"][class*="panel"][data-dsh-mobile-stack]{display:flex !important;flex-direction:column !important;width:100vw !important;max-width:100vw !important;height:100dvh !important;max-height:none !important;border-radius:0 !important;overflow:hidden !important;}',
|
|
68
|
+
' body[data-dk-mobile="1"] [role="dialog"][class*="panel"][data-dsh-mobile-stack] > [class*="header"]{order:0;flex:none;width:auto !important;height:auto !important;padding:8px 12px !important;justify-content:flex-end !important;}',
|
|
69
|
+
' body[data-dk-mobile="1"] [role="dialog"][class*="panel"][data-dsh-mobile-stack] > [class*="navTitle"]{order:1;flex:none;width:auto !important;padding:2px 16px 8px !important;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}',
|
|
70
|
+
' body[data-dk-mobile="1"] [role="dialog"][class*="panel"][data-dsh-mobile-stack] > nav{order:2;flex:none;width:auto !important;height:auto !important;padding:0 8px 8px !important;overflow-x:auto;overflow-y:hidden;}',
|
|
71
|
+
' body[data-dk-mobile="1"] [role="dialog"][class*="panel"][data-dsh-mobile-stack] > nav [class*="navList"]{flex-direction:row !important;}',
|
|
72
|
+
' body[data-dk-mobile="1"] [role="dialog"][class*="panel"][data-dsh-mobile-stack] > [class*="options"]{order:3;flex:1 1 auto !important;min-height:0 !important;width:auto !important;overflow-y:auto !important;padding-bottom:max(16px,env(safe-area-inset-bottom,0px)) !important;}',
|
|
73
|
+
' body[data-dk-mobile="1"][data-dsh-mobile-legacy] [class$="frame"]{grid-template-columns:0px minmax(0,1fr) 0px !important;}',
|
|
74
|
+
' body[data-dk-mobile="1"][data-dsh-mobile-legacy] [class$="frame"] [class*="sidebarCol"]{position:fixed !important;top:0 !important;bottom:0 !important;left:0 !important;width:min(86vw,300px) !important;z-index:80 !important;box-shadow:0 12px 48px rgba(0,0,0,.45);transform:none !important;visibility:visible !important;transition:transform .22s ease !important;}',
|
|
75
|
+
' body[data-dk-mobile="1"][data-dsh-mobile-legacy] [class$="frame"][data-sidebar-collapsed] [class*="sidebarCol"]{transform:translateX(-105%) !important;visibility:hidden !important;transition:transform .22s ease, visibility 0s linear .22s !important;}',
|
|
76
|
+
' body[data-dk-mobile="1"] .dsh-mobile-scrim{position:fixed;inset:0;z-index:75;background:rgba(8,10,14,.45);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);touch-action:none;}',
|
|
77
|
+
' body[data-dk-mobile="1"] .dsh-mobile-burger{position:fixed;left:calc(4px + env(safe-area-inset-left,0px));top:calc(4px + env(safe-area-inset-top,0px));z-index:66;box-sizing:border-box;width:44px;height:44px;display:flex;align-items:center;justify-content:center;padding:0;border:0;background:transparent;color:var(--dsw-alias-label-primary,#e6eaf2);-webkit-tap-highlight-color:transparent;touch-action:manipulation;}',
|
|
78
|
+
' body[data-dk-mobile="1"] .dsh-mobile-burger svg{width:22px;height:22px;fill:none;stroke:currentColor;stroke-width:1.9;stroke-linecap:round;}',
|
|
79
|
+
' body[data-dk-mobile="1"] .dsh-mobile-burger:active{background:color-mix(in srgb,currentColor 12%,transparent);}',
|
|
80
|
+
' body[data-dk-mobile="1"][data-dsh-mobile-legacy] [class$="frame"] header:has([class*="titleRow"]){padding-left:calc(48px + env(safe-area-inset-left,0px));}',
|
|
81
|
+
' html.dsh-dock-kbd body[data-dk-mobile="1"] .dsh-mobile-burger{display:none !important;}',
|
|
82
|
+
'}',
|
|
83
|
+
].join('\n');
|
|
84
|
+
|
|
85
|
+
const STYLE_ATTR = 'data-dsh-mobile-runtime';
|
|
86
|
+
const BODY_ATTR = 'data-dk-mobile';
|
|
87
|
+
const LEGACY_ATTR = 'data-dsh-mobile-legacy';
|
|
88
|
+
|
|
89
|
+
// 运行时装配:把样式注入 <head>、给 body 打属性、按需兜底旧版 dsh 的弹层/抽屉,
|
|
90
|
+
// 并返回一个「连一点痕迹都不留」的清理函数。挂载即生效、卸载即还原,无需重启 dsh web。
|
|
91
|
+
// 单例:重复挂载(React StrictMode 双调 effect、或多半部误触)时先清理上一份,保证全程只有一套。
|
|
92
|
+
let currentDispose = null;
|
|
93
|
+
function applyRuntime() {
|
|
94
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') return function () {};
|
|
95
|
+
const doc = document;
|
|
96
|
+
const body = doc.body;
|
|
97
|
+
if (!body) return function () {};
|
|
98
|
+
if (currentDispose) { try { currentDispose(); } catch (e) {} currentDispose = null; }
|
|
99
|
+
|
|
100
|
+
body.setAttribute(BODY_ATTR, '1');
|
|
101
|
+
let styleEl = doc.querySelector('style[' + STYLE_ATTR + ']');
|
|
102
|
+
if (!styleEl) {
|
|
103
|
+
styleEl = doc.createElement('style');
|
|
104
|
+
styleEl.setAttribute(STYLE_ATTR, '');
|
|
105
|
+
styleEl.textContent = MOBILE_CSS;
|
|
106
|
+
doc.head.appendChild(styleEl);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const mql = window.matchMedia ? window.matchMedia(MOBILE_QUERY) : null;
|
|
110
|
+
const vv = window.visualViewport || null;
|
|
111
|
+
const phone = function () { return !!(mql && mql.matches); };
|
|
112
|
+
const frame = function () { return doc.querySelector('[class$="frame"]'); };
|
|
113
|
+
const isCollapsed = function () { const f = frame(); return !!(f && f.hasAttribute('data-sidebar-collapsed')); };
|
|
114
|
+
let scrim = null;
|
|
115
|
+
let burger = null;
|
|
116
|
+
let observer = null;
|
|
117
|
+
let timer = 0;
|
|
118
|
+
|
|
119
|
+
function sidebarOpenToggle() {
|
|
120
|
+
const b = doc.querySelector('[data-sidebar-opener]') || doc.querySelector('[data-sidebar-toggle]');
|
|
121
|
+
if (b && typeof b.click === 'function') b.click();
|
|
122
|
+
}
|
|
123
|
+
function clearStack() {
|
|
124
|
+
const panels = doc.querySelectorAll('[role="dialog"][class*="panel"][data-dsh-mobile-stack]');
|
|
125
|
+
for (let i = 0; i < panels.length; i++) panels[i].removeAttribute('data-dsh-mobile-stack');
|
|
126
|
+
}
|
|
127
|
+
// 设置弹层:只在「左导航与右内容并排」时兜底成纵向堆叠;已是单列(现代 dsh)则什么都不做。
|
|
128
|
+
function dialogStackSync() {
|
|
129
|
+
const panel = doc.querySelector('[role="dialog"][class*="panel"]');
|
|
130
|
+
if (!panel) return;
|
|
131
|
+
let nav = null;
|
|
132
|
+
let opt = null;
|
|
133
|
+
const kids = panel.children;
|
|
134
|
+
for (let i = 0; i < kids.length; i++) {
|
|
135
|
+
const k = kids[i];
|
|
136
|
+
if (!nav && k.tagName === 'NAV') nav = k;
|
|
137
|
+
if (!opt && k.className && String(k.className).indexOf('options') >= 0) opt = k;
|
|
138
|
+
}
|
|
139
|
+
if (!nav || !opt) { panel.removeAttribute('data-dsh-mobile-stack'); return; }
|
|
140
|
+
const a = nav.getBoundingClientRect();
|
|
141
|
+
const b = opt.getBoundingClientRect();
|
|
142
|
+
if (!a.width || !b.width) return;
|
|
143
|
+
const sideBySide = b.left >= a.right - 8 && b.top < a.bottom - 4 && b.bottom > a.top + 4;
|
|
144
|
+
if (sideBySide) panel.setAttribute('data-dsh-mobile-stack', '1');
|
|
145
|
+
else panel.removeAttribute('data-dsh-mobile-stack');
|
|
146
|
+
}
|
|
147
|
+
function removeChrome() {
|
|
148
|
+
if (scrim) { scrim.remove(); scrim = null; }
|
|
149
|
+
if (burger) { burger.remove(); burger = null; }
|
|
150
|
+
body.removeAttribute(LEGACY_ATTR);
|
|
151
|
+
}
|
|
152
|
+
function ensureChrome() {
|
|
153
|
+
if (!scrim) {
|
|
154
|
+
scrim = doc.createElement('div');
|
|
155
|
+
scrim.className = 'dsh-mobile-scrim';
|
|
156
|
+
scrim.style.display = 'none';
|
|
157
|
+
scrim.addEventListener('click', function () { if (!isCollapsed()) sidebarOpenToggle(); });
|
|
158
|
+
body.appendChild(scrim);
|
|
159
|
+
}
|
|
160
|
+
if (!burger) {
|
|
161
|
+
burger = doc.createElement('button');
|
|
162
|
+
burger.type = 'button';
|
|
163
|
+
burger.className = 'dsh-mobile-burger';
|
|
164
|
+
burger.setAttribute('aria-label', '打开会话列表');
|
|
165
|
+
burger.setAttribute('aria-expanded', 'false');
|
|
166
|
+
burger.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16"/><path d="M4 12h16"/><path d="M4 17h16"/></svg>';
|
|
167
|
+
burger.addEventListener('click', function () { sidebarOpenToggle(); });
|
|
168
|
+
body.appendChild(burger);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// 旧版 dsh(<768 无 data-sidebar-drawer,侧栏仍是静态轨道)才浮层化 + 注入汉堡/遮罩。
|
|
172
|
+
function legacySync() {
|
|
173
|
+
const f = frame();
|
|
174
|
+
const legacy = !!f && phone() && !f.hasAttribute('data-sidebar-drawer');
|
|
175
|
+
if (!legacy) { removeChrome(); return; }
|
|
176
|
+
body.setAttribute(LEGACY_ATTR, '1');
|
|
177
|
+
ensureChrome();
|
|
178
|
+
const collapsed = isCollapsed();
|
|
179
|
+
if (scrim) scrim.style.display = collapsed ? 'none' : '';
|
|
180
|
+
if (burger) {
|
|
181
|
+
burger.style.display = '';
|
|
182
|
+
burger.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function kbdSync() {
|
|
186
|
+
if (!vv) return;
|
|
187
|
+
const kbd = vv.height > 0 && (window.innerHeight - vv.height) > 120;
|
|
188
|
+
const de = doc.documentElement;
|
|
189
|
+
if (kbd) de.classList.add('dsh-dock-kbd');
|
|
190
|
+
else de.classList.remove('dsh-dock-kbd');
|
|
191
|
+
}
|
|
192
|
+
let dockAutoClosed = false;
|
|
193
|
+
function dockOpen() { return !!doc.querySelector('.dockm-dialog'); }
|
|
194
|
+
function tick() {
|
|
195
|
+
try {
|
|
196
|
+
kbdSync();
|
|
197
|
+
if (phone()) {
|
|
198
|
+
// 功能坞面板打开时自动收起抽屉,否则抽屉会盖在面板左侧、且中列处于 inert。
|
|
199
|
+
const dopen = dockOpen();
|
|
200
|
+
if (dopen && !isCollapsed() && !dockAutoClosed) { dockAutoClosed = true; sidebarOpenToggle(); }
|
|
201
|
+
if (!dopen) dockAutoClosed = false;
|
|
202
|
+
dialogStackSync();
|
|
203
|
+
legacySync();
|
|
204
|
+
}
|
|
205
|
+
else { clearStack(); removeChrome(); dockAutoClosed = false; }
|
|
206
|
+
} catch (e) { /* 任何一步失败都不影响页面 */ }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
tick();
|
|
210
|
+
if (typeof MutationObserver !== 'undefined') {
|
|
211
|
+
observer = new MutationObserver(function () { tick(); });
|
|
212
|
+
try {
|
|
213
|
+
observer.observe(doc.documentElement, { attributes: true, attributeFilter: ['data-sidebar-collapsed', 'data-sidebar-drawer'], subtree: true });
|
|
214
|
+
observer.observe(body, { childList: true, subtree: true });
|
|
215
|
+
} catch (e) { /* observe 失败就只靠定时器 */ }
|
|
216
|
+
}
|
|
217
|
+
timer = window.setInterval(tick, 1200);
|
|
218
|
+
const onResize = function () { tick(); };
|
|
219
|
+
window.addEventListener('resize', onResize);
|
|
220
|
+
if (vv && vv.addEventListener) vv.addEventListener('resize', kbdSync);
|
|
221
|
+
if (mql && mql.addEventListener) mql.addEventListener('change', tick);
|
|
222
|
+
else if (mql && mql.addListener) mql.addListener(tick);
|
|
223
|
+
|
|
224
|
+
const dispose = function () {
|
|
225
|
+
if (currentDispose === dispose) currentDispose = null;
|
|
226
|
+
try { if (observer) observer.disconnect(); } catch (e) {}
|
|
227
|
+
if (timer) window.clearInterval(timer);
|
|
228
|
+
window.removeEventListener('resize', onResize);
|
|
229
|
+
if (vv && vv.removeEventListener) vv.removeEventListener('resize', kbdSync);
|
|
230
|
+
if (mql && mql.removeEventListener) mql.removeEventListener('change', tick);
|
|
231
|
+
else if (mql && mql.removeListener) mql.removeListener(tick);
|
|
232
|
+
try { doc.documentElement.classList.remove('dsh-dock-kbd'); } catch (e) {}
|
|
233
|
+
try { clearStack(); } catch (e) {}
|
|
234
|
+
removeChrome();
|
|
235
|
+
try { body.removeAttribute(BODY_ATTR); } catch (e) {}
|
|
236
|
+
try { if (styleEl && styleEl.parentNode) styleEl.parentNode.removeChild(styleEl); } catch (e) {}
|
|
237
|
+
};
|
|
238
|
+
currentDispose = dispose;
|
|
239
|
+
return dispose;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// —— Overlay:功能启用期间常驻;active 时挂载运行时、非 active 时什么也不做 ——
|
|
243
|
+
export function MobileOverlay() {
|
|
244
|
+
const [active, setActive] = useState(function () { return mobileActive(); });
|
|
245
|
+
useEffect(function () {
|
|
246
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return undefined;
|
|
247
|
+
let mqs = [];
|
|
248
|
+
try { mqs = [window.matchMedia(MOBILE_QUERY), window.matchMedia(COARSE_QUERY)]; } catch (e) { return undefined; }
|
|
249
|
+
const update = function () { setActive(mqs.some(function (m) { return m.matches; })); };
|
|
250
|
+
update();
|
|
251
|
+
for (let i = 0; i < mqs.length; i++) {
|
|
252
|
+
const m = mqs[i];
|
|
253
|
+
if (m.addEventListener) m.addEventListener('change', update);
|
|
254
|
+
else if (m.addListener) m.addListener(update);
|
|
255
|
+
}
|
|
256
|
+
return function () {
|
|
257
|
+
for (let i = 0; i < mqs.length; i++) {
|
|
258
|
+
const m = mqs[i];
|
|
259
|
+
if (m.removeEventListener) m.removeEventListener('change', update);
|
|
260
|
+
else if (m.removeListener) m.removeListener(update);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
}, []);
|
|
264
|
+
useEffect(function () {
|
|
265
|
+
if (!active) return undefined;
|
|
266
|
+
return applyRuntime();
|
|
267
|
+
}, [active]);
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// —— 功能页:一行说清当前命中状态与兜底对象 ——
|
|
272
|
+
function useViewport() {
|
|
273
|
+
const [size, setSize] = useState(function () {
|
|
274
|
+
return typeof window === 'undefined' ? null : { w: window.innerWidth, h: window.innerHeight };
|
|
275
|
+
});
|
|
276
|
+
useEffect(function () {
|
|
277
|
+
if (typeof window === 'undefined') return undefined;
|
|
278
|
+
const update = function () { setSize({ w: window.innerWidth, h: window.innerHeight }); };
|
|
279
|
+
window.addEventListener('resize', update);
|
|
280
|
+
return function () { window.removeEventListener('resize', update); };
|
|
281
|
+
}, []);
|
|
282
|
+
return size;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function MobileView() {
|
|
286
|
+
const size = useViewport();
|
|
287
|
+
const [active, setActive] = useState(function () { return mobileActive(); });
|
|
288
|
+
const [drawer, setDrawer] = useState(false);
|
|
289
|
+
useEffect(function () {
|
|
290
|
+
if (typeof window === 'undefined') return undefined;
|
|
291
|
+
const tick = function () {
|
|
292
|
+
setActive(mobileActive());
|
|
293
|
+
const f = document.querySelector('[class$="frame"]');
|
|
294
|
+
setDrawer(!!(f && f.hasAttribute('data-sidebar-drawer')));
|
|
295
|
+
};
|
|
296
|
+
tick();
|
|
297
|
+
const t = setInterval(tick, 1000);
|
|
298
|
+
return function () { clearInterval(t); };
|
|
299
|
+
}, []);
|
|
300
|
+
const mode = !active ? '未命中' : (window.matchMedia && window.matchMedia(MOBILE_QUERY).matches ? (drawer ? '抽屉模式(原生)' : '旧版兜底(浮层抽屉)') : '仅触控优化');
|
|
301
|
+
return react.createElement('div', { className: 'dkmob-root' },
|
|
302
|
+
react.createElement('div', { className: 'dkmob-row' },
|
|
303
|
+
react.createElement('span', { className: 'dkmob-label' }, '手机模式'),
|
|
304
|
+
react.createElement('span', { className: 'dkmob-state' + (active ? ' on' : '') }, active ? '已命中' : '未命中'),
|
|
305
|
+
react.createElement('span', { className: 'dkmob-dim' }, size ? size.w + '×' + size.h + 'px' : '')),
|
|
306
|
+
react.createElement('div', { className: 'dkmob-row' },
|
|
307
|
+
react.createElement('span', { className: 'dkmob-label' }, '当前形态'),
|
|
308
|
+
react.createElement('span', { className: 'dkmob-state' + (active ? ' on' : '') }, mode)),
|
|
309
|
+
react.createElement('div', { className: 'dkmob-note' },
|
|
310
|
+
'生效中的优化:输入类控件字号 16px(防 iOS 聚焦整页缩放)· 原生侧栏开关热区扩到 44×44 · ',
|
|
311
|
+
'去点按高亮与双击缩放判定 · 代码块横向滚动 / 行内代码换行 · 会话输入行的余额/用量 chip 收起 · ',
|
|
312
|
+
'设置弹层内容安全区内边距。旧版 dsh(<768 仍是两栏弹层或静态图标轨道)会自动兜底为单列 / 浮层抽屉。',
|
|
313
|
+
'关闭本功能即全部还原——运行时样式、body 属性、遮罩/按钮与所有监听器一并移除,页面不残留任何注入。'));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function MobileStat() {
|
|
317
|
+
const active = mobileActive();
|
|
318
|
+
if (!active) return react.createElement('span', null, '当前视口未命中手机形态,未启用任何优化');
|
|
319
|
+
return react.createElement('span', null, '手机模式生效中(16px 输入 / 触控热区 / 代码滚动 / 弹层兜底)');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// —— 样式:模块自身仅样式为空(运行时样式由 Overlay 注入/卸载,关闭后零残留);
|
|
323
|
+
// View 自身的小样式随全局 <style> 注入,命名空间 dkmob- 避免与其他模块冲突。 ——
|
|
324
|
+
const css = [
|
|
325
|
+
'.dkmob-root{display:flex;flex-direction:column;gap:10px;color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.6;}',
|
|
326
|
+
'.dkmob-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}',
|
|
327
|
+
'.dkmob-label{font-weight:600;flex:none;}',
|
|
328
|
+
'.dkmob-state{flex:none;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);border-radius:999px;padding:1px 10px;font-size:11px;}',
|
|
329
|
+
'.dkmob-state.on{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary);}',
|
|
330
|
+
'.dkmob-dim{color:var(--dsw-alias-label-secondary);font-size:12px;}',
|
|
331
|
+
'.dkmob-note{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.7;border-top:1px solid var(--dsw-alias-border-l1);padding-top:10px;}',
|
|
332
|
+
].join('\n');
|
|
333
|
+
|
|
334
|
+
export const feature = {
|
|
335
|
+
id: 'mobile',
|
|
336
|
+
name: '手机适配',
|
|
337
|
+
order: 170,
|
|
338
|
+
accent: '#34d399',
|
|
339
|
+
description: '手机/窄屏体验优化:16px 输入防缩放、触控热区、代码块滚动、会话 chip 收起、设置弹层单列与旧版抽屉兜底;关闭后零残留',
|
|
340
|
+
defaultEnabled: false,
|
|
341
|
+
css,
|
|
342
|
+
Overlay: MobileOverlay,
|
|
343
|
+
View: MobileView,
|
|
344
|
+
HomeStat: MobileStat,
|
|
345
|
+
};
|
package/features/notify/host.js
CHANGED
|
@@ -50,6 +50,11 @@ function defaultConfig() {
|
|
|
50
50
|
voiceLang: '',
|
|
51
51
|
// 指定音色(speechSynthesis voiceURI,设备相关;空 = 按语言自动挑,本机没有时客户端回落)
|
|
52
52
|
voiceURI: '',
|
|
53
|
+
// 事件 × 渠道矩阵(扁平键 card:complete / sound:error / system:confirm / ...;
|
|
54
|
+
// 未设置的键回退旧字段,升级不改变既有行为)
|
|
55
|
+
eventMatrix: {},
|
|
56
|
+
// 群机器人消息口径三挡:brief / medium / detailed
|
|
57
|
+
pushMode: 'detailed',
|
|
53
58
|
// 迁移标记(schema 默认 false):settings 不可用时的兜底取 true——迁移由
|
|
54
59
|
// src/host-core.js migrateNotifyConfig 统一负责,这里只是原样带回,不影响通知行为。
|
|
55
60
|
migratedFromAnimation: true,
|
|
@@ -69,11 +74,21 @@ function readConfig(ctx) {
|
|
|
69
74
|
if (!SOUND_EFFECTS.includes(cfg.soundEffect)) cfg.soundEffect = 'chime'
|
|
70
75
|
if (cfg.voiceMode !== 'brief' && cfg.voiceMode !== 'medium' && cfg.voiceMode !== 'detailed') cfg.voiceMode = 'brief'
|
|
71
76
|
if (cfg.cardMode !== 'brief' && cfg.cardMode !== 'medium' && cfg.cardMode !== 'detailed') cfg.cardMode = 'detailed'
|
|
77
|
+
if (cfg.pushMode !== 'brief' && cfg.pushMode !== 'medium' && cfg.pushMode !== 'detailed') cfg.pushMode = 'detailed'
|
|
78
|
+
if (!cfg.eventMatrix || typeof cfg.eventMatrix !== 'object' || Array.isArray(cfg.eventMatrix)) cfg.eventMatrix = {}
|
|
72
79
|
}
|
|
73
80
|
} catch { /* settings 未挂载,用默认 */ }
|
|
74
81
|
return cfg
|
|
75
82
|
}
|
|
76
83
|
|
|
84
|
+
/** 事件 × 渠道开关:矩阵里显式设置过就用它,否则回退到旧字段(fallback)。 */
|
|
85
|
+
function matrixEnabled(cfg, channel, event, fallback) {
|
|
86
|
+
const key = channel + ':' + event
|
|
87
|
+
const matrix = cfg && cfg.eventMatrix
|
|
88
|
+
if (matrix && typeof matrix === 'object' && typeof matrix[key] === 'boolean') return matrix[key]
|
|
89
|
+
return fallback !== false
|
|
90
|
+
}
|
|
91
|
+
|
|
77
92
|
function truncate(str, max = 160) {
|
|
78
93
|
if (!str) return ''
|
|
79
94
|
return String(str).length > max ? String(str).slice(0, max) + '…' : str
|
|
@@ -106,19 +121,23 @@ const END_REASON_LABELS = {
|
|
|
106
121
|
}
|
|
107
122
|
|
|
108
123
|
// 钉钉与飞书共用的消息正文(各端再按各自 markdown 方言拼接)
|
|
109
|
-
|
|
124
|
+
// mode: brief = 任务与结果;medium = 加模型/耗时;detailed = 再加回合、Token 与摘要。
|
|
125
|
+
function buildMessageLines(t, mode) {
|
|
110
126
|
const success = !t.endReason || t.endReason === 'completed'
|
|
111
127
|
const reasonLabel = END_REASON_LABELS[t.endReason] || '✅ 完成'
|
|
112
128
|
const models = t.models.length > 0 ? t.models.join(', ') : '未知'
|
|
129
|
+
const tone = mode === 'brief' || mode === 'medium' ? mode : 'detailed'
|
|
113
130
|
const lines = []
|
|
114
131
|
lines.push(`### ${success ? '✅' : '📢'} dsh 任务${success ? '完成' : '结束'}`)
|
|
115
132
|
lines.push(`**任务**: ${t.title}`)
|
|
116
|
-
lines.push(`**模型**: ${models}${t.provider ? `(${t.provider})` : ''}`)
|
|
117
|
-
lines.push(`**耗时**: ${fmtDurCn(t.duration)}
|
|
118
|
-
|
|
119
|
-
|
|
133
|
+
if (tone !== 'brief') lines.push(`**模型**: ${models}${t.provider ? `(${t.provider})` : ''}`)
|
|
134
|
+
lines.push(`**耗时**: ${fmtDurCn(t.duration)}${tone === 'brief' ? '' : `(${fmtClockOf(t.startTime)} → ${fmtClockOf(t.endTime)})`}`)
|
|
135
|
+
if (tone === 'detailed') {
|
|
136
|
+
lines.push(`**回合**: ${t.turns} · 步骤 ${t.steps}${t.toolCalls ? ` · 工具 ${t.toolCalls} 次` : ''}`)
|
|
137
|
+
lines.push(`**Token**: 输入 ${t.inputTokens.toLocaleString()} / 输出 ${t.outputTokens.toLocaleString()} / 总计 ${t.totalTokens.toLocaleString()}`)
|
|
138
|
+
}
|
|
120
139
|
lines.push(`**结果**: ${reasonLabel}${t.errorMessage ? `:${truncate(t.errorMessage, 120)}` : ''}`)
|
|
121
|
-
if (t.lastText) {
|
|
140
|
+
if (tone !== 'brief' && t.lastText) {
|
|
122
141
|
lines.push(`> ${truncate(t.lastText, 200)}`)
|
|
123
142
|
}
|
|
124
143
|
lines.push('---')
|
|
@@ -130,8 +149,8 @@ function buildMessageLines(t) {
|
|
|
130
149
|
}
|
|
131
150
|
}
|
|
132
151
|
|
|
133
|
-
function buildDingtalkMessage(t) {
|
|
134
|
-
const { title, lines } = buildMessageLines(t)
|
|
152
|
+
function buildDingtalkMessage(t, mode) {
|
|
153
|
+
const { title, lines } = buildMessageLines(t, mode)
|
|
135
154
|
return {
|
|
136
155
|
title,
|
|
137
156
|
// 钉钉 markdown 不渲染单个 \n,行之间必须用空行分隔才能换行
|
|
@@ -140,8 +159,8 @@ function buildDingtalkMessage(t) {
|
|
|
140
159
|
}
|
|
141
160
|
|
|
142
161
|
// 飞书 interactive 卡片:标题进 header,正文走 lark_md(支持 **加粗** 与 \n 换行,不支持 ### 标题)
|
|
143
|
-
function buildFeishuMessage(t) {
|
|
144
|
-
const { title, success, lines } = buildMessageLines(t)
|
|
162
|
+
function buildFeishuMessage(t, mode) {
|
|
163
|
+
const { title, success, lines } = buildMessageLines(t, mode)
|
|
145
164
|
const body = lines.filter((l) => !l.startsWith('### ')).join('\n')
|
|
146
165
|
return {
|
|
147
166
|
title,
|
|
@@ -223,14 +242,15 @@ export const feature = {
|
|
|
223
242
|
disposers.push(() => lease.release())
|
|
224
243
|
const tracker = lease.tracker
|
|
225
244
|
|
|
226
|
-
// =====
|
|
245
|
+
// ===== 群机器人推送(宿主侧直发,异步不阻塞;事件筛选走 eventMatrix,缺省回退 notifyOn*) =====
|
|
227
246
|
async function pushDingtalkIfNeeded(record) {
|
|
228
247
|
try {
|
|
229
248
|
const cfg = readConfig(ctx)
|
|
230
249
|
if (!cfg.dingtalkEnabled || !cfg.dingtalkWebhook) return
|
|
231
250
|
const success = !record.endReason || record.endReason === 'completed'
|
|
232
|
-
|
|
233
|
-
|
|
251
|
+
const event = success ? 'complete' : 'error'
|
|
252
|
+
if (!matrixEnabled(cfg, 'dingtalk', event, success ? cfg.notifyOnComplete : cfg.notifyOnError)) return
|
|
253
|
+
const { title, text } = buildDingtalkMessage(record, cfg.pushMode)
|
|
234
254
|
const r = await sendDingtalk(cfg.dingtalkWebhook, title, text)
|
|
235
255
|
if (r.ok) console.log('[dsh-dock] notify dingtalk pushed:', title)
|
|
236
256
|
else console.error('[dsh-dock] notify dingtalk push failed:', r.error)
|
|
@@ -244,8 +264,9 @@ export const feature = {
|
|
|
244
264
|
const cfg = readConfig(ctx)
|
|
245
265
|
if (!cfg.feishuEnabled || !cfg.feishuWebhook) return
|
|
246
266
|
const success = !record.endReason || record.endReason === 'completed'
|
|
247
|
-
|
|
248
|
-
|
|
267
|
+
const event = success ? 'complete' : 'error'
|
|
268
|
+
if (!matrixEnabled(cfg, 'feishu', event, success ? cfg.notifyOnComplete : cfg.notifyOnError)) return
|
|
269
|
+
const { title, card } = buildFeishuMessage(record, cfg.pushMode)
|
|
249
270
|
const r = await sendFeishu(cfg.feishuWebhook, title, card)
|
|
250
271
|
if (r.ok) console.log('[dsh-dock] notify feishu pushed:', title)
|
|
251
272
|
else console.error('[dsh-dock] notify feishu push failed:', r.error)
|
|
@@ -298,6 +319,20 @@ export const feature = {
|
|
|
298
319
|
if (typeof p.systemNotify === 'boolean') cfg.systemNotify = p.systemNotify
|
|
299
320
|
if (typeof p.soundNotify === 'boolean') cfg.soundNotify = p.soundNotify
|
|
300
321
|
if (typeof p.soundEffect === 'string' && SOUND_EFFECTS.includes(p.soundEffect)) cfg.soundEffect = p.soundEffect
|
|
322
|
+
// 群机器人消息口径三挡
|
|
323
|
+
if (typeof p.pushMode === 'string' && (p.pushMode === 'brief' || p.pushMode === 'medium' || p.pushMode === 'detailed')) {
|
|
324
|
+
cfg.pushMode = p.pushMode
|
|
325
|
+
}
|
|
326
|
+
// 事件 × 渠道矩阵:只接受白名单键 + 布尔值,合并写回
|
|
327
|
+
if (p.eventMatrix && typeof p.eventMatrix === 'object' && !Array.isArray(p.eventMatrix)) {
|
|
328
|
+
const matrix = Object.assign({}, cfg.eventMatrix || {})
|
|
329
|
+
for (const [key, value] of Object.entries(p.eventMatrix)) {
|
|
330
|
+
if (typeof value === 'boolean' && /^(card|sound|system|voice|dingtalk|feishu):(complete|error|confirm|stuck)$/.test(key)) {
|
|
331
|
+
matrix[key] = value
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
cfg.eventMatrix = matrix
|
|
335
|
+
}
|
|
301
336
|
if (typeof p.dingtalkEnabled === 'boolean') cfg.dingtalkEnabled = p.dingtalkEnabled
|
|
302
337
|
if (typeof p.dingtalkWebhook === 'string') {
|
|
303
338
|
const hook = p.dingtalkWebhook.trim()
|