dsh-notify-yimit 0.1.2 → 0.1.4

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/lib/client.js CHANGED
@@ -1,790 +1,1369 @@
1
- /**
2
- * dsh-notify-yimit 浏览器端 bundle(单文件,经 __ModuleLoader__ 加载,零构建)。
3
- *
4
- * 提供两块界面:
5
- * - settings.section「通知」:插件总开关、通知方式三选一(system 系统通知 /
6
- * custom 自定义样式 / off 关闭,默认关闭)、自定义通知的最大同时显示数、显示
7
- * 时长、背景色与文字色,以及系统通知权限申请;
8
- * - shell.overlay:自定义样式通知浮层(右上角堆叠,含标题/内容/忽略/跳转会话
9
- * 按钮);系统通知经 Web Notification API 弹出,点击通知跳转到对应会话。
10
- *
11
- * 数据通道:
12
- * - remote.notify.getState / updateConfig / ackEvents(Typert RPC,250ms 轮询);
13
- * - ctx.sessions.open(id) 跳转会话。
14
- * 样式全部使用 --dsw-* 主题变量(自定义通知的背景/文字色为用户配置)。
15
- */
16
-
17
- window.__ModuleLoader__.load({
18
- id: 'dsh-notify-yimit',
19
- factory: (require) => {
20
- var module = { exports: {} }
21
- var exports = module.exports
22
- Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
23
-
24
- const React = require('react')
25
-
26
- // ── 样式 ────────────────────────────────────────────────────────────────
27
-
28
- const css = [
29
- '/* dsh-notify-yimit: 设置页与自定义通知浮层 */',
30
- // 设置页
31
- '.dn-root{display:flex;flex-direction:column;gap:20px;padding:4px 2px 24px;font-size:13px;color:var(--dsw-alias-label-primary)}',
32
- '.dn-block{display:flex;flex-direction:column;gap:10px}',
33
- '.dn-title{font-size:13px;font-weight:600;margin:0}',
34
- '.dn-note{font-size:12px;line-height:20px;color:var(--dsw-alias-label-tertiary);margin:0}',
35
- '.dn-row{display:flex;align-items:center;gap:10px}',
36
- '.dn-label{font-size:13px;color:var(--dsw-alias-label-secondary);flex:none}',
37
- // 总开关:关闭态把手用次要文字色,深浅主题下都与轨道保持清晰对比
38
- '.dn-switch{position:relative;width:38px;height:22px;border-radius:999px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;padding:0;flex:none;transition:background .15s ease,border-color .15s ease}',
39
- '.dn-switch[data-on="true"]{background:var(--dsw-alias-state-business-primary);border-color:transparent}',
40
- '.dn-switch::after{content:"";position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:var(--dsw-alias-label-secondary);transition:transform .15s ease,background .15s ease}',
41
- '.dn-switch[data-on="true"]::after{transform:translateX(16px);background:var(--dsw-alias-label-primary-inverted)}',
42
- // 分段控制器(通知方式三选一)
43
- '.dn-seg{display:flex;gap:2px;padding:2px;border-radius:10px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);max-width:520px}',
44
- '.dn-seg-btn{flex:1;font:inherit;font-size:13px;color:var(--dsw-alias-label-secondary);border:0;border-radius:8px;padding:6px 10px;cursor:pointer;background:transparent;transition:background .15s ease,color .15s ease;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
45
- '.dn-seg-btn:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}',
46
- '.dn-seg-btn[data-on="true"]{background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);box-shadow:var(--dsw-shadow-lv1)}',
47
- '.dn-seg-btn:disabled{opacity:.45;cursor:not-allowed}',
48
- '.dn-seg-btn:disabled:hover{background:transparent;color:var(--dsw-alias-label-secondary)}',
49
- // 折叠区块(自定义通知设置显隐过渡动画)
50
- '.dn-collapse{display:grid;grid-template-rows:1fr;opacity:1;transition:grid-template-rows .24s ease,opacity .2s ease}',
51
- '.dn-collapse[data-open="false"]{grid-template-rows:0fr;opacity:0}',
52
- '.dn-collapse>.dn-collapse-body{overflow:hidden;min-height:0}',
53
- '.dn-collapse>.dn-collapse-body>.dn-block{min-width:0}',
54
- // 每种通知类型的颜色配置列表
55
- '.dn-kind-list{display:flex;flex-direction:column;gap:8px}',
56
- '.dn-kind-row{display:flex;align-items:center;gap:12px;padding:8px 12px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-1)}',
57
- '.dn-kind-swatch{flex:none;width:20px;height:20px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2);box-shadow:inset 0 0 0 1px rgba(0,0,0,.12)}',
58
- '.dn-kind-name{flex:1;font-size:13px;font-weight:600;min-width:0}',
59
- '.dn-kind-field{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--dsw-alias-label-tertiary);flex:none}',
60
- '.dn-input{font:inherit;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:5px 10px;outline:none;max-width:120px}',
61
- '.dn-input:focus{border-color:var(--dsw-alias-state-business-primary)}',
62
- '.dn-input[type=color]{padding:2px 4px;width:48px;height:28px;cursor:pointer;max-width:none}',
63
- '.dn-input:disabled{opacity:.45;cursor:not-allowed}',
64
- '.dn-select{max-width:260px;cursor:pointer}',
65
- '.dn-color-preview{font-size:12px;color:var(--dsw-alias-label-tertiary);font-family:var(--dsw-font-mono)}',
66
- '.dn-btn{font:inherit;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-button-elevated-fill);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:6px 14px;cursor:pointer}',
67
- '.dn-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}',
68
- '.dn-btn.primary{background:var(--dsw-alias-state-business-primary);border-color:transparent;color:var(--dsw-alias-label-primary-inverted)}',
69
- '.dn-btn.primary:hover{opacity:0.88}',
70
- '.dn-btn:disabled{opacity:.5;cursor:not-allowed}',
71
- '.dn-msg{font-size:12px;line-height:18px;padding:8px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l1)}',
72
- '.dn-msg.ok{color:var(--dsw-alias-state-success-primary)}',
73
- '.dn-msg.err{color:var(--dsw-alias-state-error-primary)}',
74
- '.dn-divider{height:1px;background:var(--dsw-alias-border-l1)}',
75
- // 自定义通知浮层(背景/文字色来自配置,经 --dn-bg / --dn-fg 注入)
76
- '.dn-layer{position:fixed;top:16px;right:16px;z-index:9999;display:flex;flex-direction:column;align-items:flex-end;gap:10px;pointer-events:none}',
77
- '.dn-card{pointer-events:auto;width:320px;max-width:calc(100vw - 32px);border-radius:12px;box-shadow:var(--dsw-shadow-lv3);border:1px solid var(--dsw-alias-border-l2);background:var(--dn-bg,#20272f);color:var(--dn-fg,#e6edf3);overflow:hidden;animation:dn-in .18s ease}',
78
- '@keyframes dn-in{from{opacity:0;transform:translateX(24px)}to{opacity:1;transform:translateX(0)}}',
79
- '.dn-card-head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 12px 6px}',
80
- '.dn-card-title{font-size:13px;font-weight:600;line-height:20px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
81
- '.dn-card-kind{flex:none;font-size:11px;line-height:18px;border-radius:999px;padding:0 8px;border:1px solid currentColor;opacity:.85}',
82
- '.dn-card-body{padding:0 12px 8px;font-size:13px;line-height:20px;word-break:break-word;max-height:120px;overflow:hidden;display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical}',
83
- '.dn-card-actions{display:flex;align-items:center;gap:6px;padding:6px 12px 10px}',
84
- '.dn-card-btn{font:inherit;font-size:12px;line-height:18px;border-radius:6px;padding:4px 10px;cursor:pointer;border:1px solid color-mix(in srgb,currentColor 35%,transparent);background:transparent;color:inherit}',
85
- '.dn-card-btn:hover{background:color-mix(in srgb,currentColor 12%,transparent)}',
86
- '.dn-card-btn.primary{background:color-mix(in srgb,currentColor 18%,transparent)}',
87
- '.dn-card-bar{height:2px;background:color-mix(in srgb,currentColor 25%,transparent);transform-origin:left}',
88
- ].join('\n')
89
- const cssTagId = 'dsh-notify-yimit/client.css'
90
- if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css=' + JSON.stringify(cssTagId) + ']') === null) {
91
- const tag = document.createElement('style')
92
- tag.dataset.plugin = 'dsh-notify-yimit'
93
- tag.dataset.pluginCss = cssTagId
94
- tag.textContent = css
95
- document.head.appendChild(tag)
96
- }
97
-
98
- // ── 多语言(中/英) ──────────────────────────────────────────────────────
99
-
100
- const MESSAGES = {
101
- zh: {
102
- sectionLabel: '通知',
103
- enabledLabel: '启用通知插件',
104
- enabledNote: '关闭后不产生任何通知(设置仍会保存)。',
105
- modeLabel: '通知方式',
106
- modeSystem: 'Windows 系统通知',
107
- modeCustom: '插件自定义样式通知',
108
- modeOff: '关闭',
109
- modeNote: '系统通知由浏览器弹出、可在系统通知中心查看;自定义通知为桌面右下角浮窗(不依赖浏览器页面)。',
110
- browserLabel: '跳转会话浏览器',
111
- browserDefault: '默认浏览器(系统)',
112
- browserNote: '「跳转会话」始终打开浏览器并定位到该会话;可选指定浏览器打开(默认=系统默认浏览器)。',
113
- customTitle: '自定义通知设置',
114
- colorsTitle: '每种通知的样式',
115
- colorBg: '背景',
116
- colorFg: '文字',
117
- maxVisibleLabel: '同时最多显示数量',
118
- durationLabel: '通知显示时长(秒)',
119
- bgColorLabel: '背景颜色',
120
- textColorLabel: '文字颜色',
121
- systemTitle: '系统通知',
122
- permissionGranted: '已获得系统通知权限',
123
- permissionDenied: '系统通知权限被拒绝,请在浏览器设置中允许本页面通知',
124
- permissionDefault: '系统通知需要浏览器通知权限',
125
- requestPermission: '申请通知权限',
126
- triggerNote: '触发场景:任务完成、任务出错、运行中(实时内容)、等待审批、等待回答。通知标题为对话标题;点击通知或跳转按钮可打开对应会话。',
127
- saveHint: '所有设置修改后自动保存。',
128
- saved: '已保存',
129
- saveFailed: '保存失败:{message}',
130
- loading: '正在读取配置…',
131
- rpcFailed: '读取配置失败:{message}',
132
- // 通知卡片
133
- kindCompleted: '已完成',
134
- kindError: '出错',
135
- kindRunning: '运行中',
136
- kindApproval: '待审批',
137
- kindQuestion: '待回答',
138
- ignore: '忽略',
139
- jump: '跳转会话',
140
- },
141
- en: {
142
- sectionLabel: 'Notifications',
143
- enabledLabel: 'Enable notification plugin',
144
- enabledNote: 'When disabled, no notifications are produced (settings are still saved).',
145
- modeLabel: 'Notification style',
146
- modeSystem: 'Windows system notifications',
147
- modeCustom: 'Custom in-app notifications',
148
- modeOff: 'Off',
149
- modeNote: 'System notifications are shown by the browser and live in the system notification center; custom notifications are desktop toasts at the bottom-right (independent of the browser page).',
150
- browserLabel: 'Browser for jumping to a session',
151
- browserDefault: 'Default browser (system)',
152
- browserNote: '"Jump to session" always opens the browser and navigates to the session; optionally pick a specific browser (default = system default).',
153
- customTitle: 'Custom notification settings',
154
- colorsTitle: 'Style per notification type',
155
- colorBg: 'BG',
156
- colorFg: 'FG',
157
- maxVisibleLabel: 'Max simultaneously visible',
158
- durationLabel: 'Display duration (seconds)',
159
- bgColorLabel: 'Background color',
160
- textColorLabel: 'Text color',
161
- systemTitle: 'System notifications',
162
- permissionGranted: 'System notification permission granted',
163
- permissionDenied: 'System notification permission denied — allow notifications for this site in your browser settings',
164
- permissionDefault: 'System notifications need browser notification permission',
165
- requestPermission: 'Request permission',
166
- triggerNote: 'Triggers: task completed, task failed, running (live activity), awaiting approval, awaiting answer. The title is the conversation title; clicking the notification or the jump button opens the session.',
167
- saveHint: 'All settings changes are auto-saved.',
168
- saved: 'Saved',
169
- saveFailed: 'Save failed: {message}',
170
- loading: 'Loading config…',
171
- rpcFailed: 'Failed to load config: {message}',
172
- kindCompleted: 'Done',
173
- kindError: 'Failed',
174
- kindRunning: 'Running',
175
- kindApproval: 'Approval',
176
- kindQuestion: 'Question',
177
- ignore: 'Ignore',
178
- jump: 'Open',
179
- },
180
- }
181
-
182
- function detectBrowserLocale() {
183
- const lang = typeof navigator !== 'undefined' && typeof navigator.language === 'string' ? navigator.language : ''
184
- return lang.toLowerCase().startsWith('zh') ? 'zh' : 'en'
185
- }
186
- function makeT(locale) {
187
- const dict = locale === 'zh' ? MESSAGES.zh : MESSAGES.en
188
- return (key, vars) => {
189
- let text = dict[key] ?? MESSAGES.en[key] ?? key
190
- if (vars) for (const name of Object.keys(vars)) text = text.split('{' + name + '}').join(String(vars[name]))
191
- return text
192
- }
193
- }
194
-
195
- // ── 线路数据校验(与服务端 zod 清单对应,宽松校验) ──────────────────────
196
-
197
- function fail(path) {
198
- throw new Error('dsh-notify-yimit: 服务端数据非法 (' + path + ')')
199
- }
200
- function needStr(v, path) {
201
- if (typeof v !== 'string') fail(path)
202
- return v
203
- }
204
- function needNum(v, path) {
205
- if (typeof v !== 'number' || !Number.isFinite(v)) fail(path)
206
- return v
207
- }
208
- function needBool(v, path) {
209
- if (typeof v !== 'boolean') fail(path)
210
- return v
211
- }
212
- function parseConfig(v, path) {
213
- if (v === null || typeof v !== 'object' || Array.isArray(v)) fail(path)
214
- const kindColors = (key, fallbackBg, fallbackFg) => {
215
- const entry = v.colors !== null && typeof v.colors === 'object' && !Array.isArray(v.colors) ? v.colors[key] : undefined
216
- return {
217
- bg: typeof entry?.bg === 'string' ? entry.bg : fallbackBg,
218
- fg: typeof entry?.fg === 'string' ? entry.fg : fallbackFg,
219
- }
220
- }
221
- const topBg = typeof v.bgColor === 'string' ? v.bgColor : '#20272f'
222
- const topFg = typeof v.textColor === 'string' ? v.textColor : '#e6edf3'
223
- return {
224
- enabled: v.enabled === true,
225
- mode: v.mode === 'system' || v.mode === 'custom' || v.mode === 'off' ? v.mode : 'off',
226
- maxVisible: typeof v.maxVisible === 'number' ? v.maxVisible : 3,
227
- durationMs: typeof v.durationMs === 'number' ? v.durationMs : 8000,
228
- browser: typeof v.browser === 'string' ? v.browser : '',
229
- bgColor: topBg,
230
- textColor: topFg,
231
- colors: {
232
- completed: kindColors('completed', topBg, topFg),
233
- error: kindColors('error', topBg, topFg),
234
- running: kindColors('running', topBg, topFg),
235
- approval: kindColors('approval', topBg, topFg),
236
- question: kindColors('question', topBg, topFg),
237
- },
238
- }
239
- }
240
- function parseEvent(v, path) {
241
- if (v === null || typeof v !== 'object' || Array.isArray(v)) fail(path)
242
- return {
243
- id: needStr(v.id, path + '.id'),
244
- sessionId: needStr(v.sessionId, path + '.sessionId'),
245
- title: typeof v.title === 'string' ? v.title : '',
246
- kind: v.kind === 'completed' || v.kind === 'error' || v.kind === 'running' || v.kind === 'approval' || v.kind === 'question' ? v.kind : 'running',
247
- text: needStr(v.text, path + '.text'),
248
- activity: typeof v.activity === 'string' ? v.activity : '',
249
- at: needNum(v.at, path + '.at'),
250
- }
251
- }
252
- function parseState(v, path) {
253
- if (v === null || typeof v !== 'object' || Array.isArray(v)) fail(path)
254
- return {
255
- config: parseConfig(v.config, path + '.config'),
256
- events: Array.isArray(v.events) ? v.events.map((e, i) => parseEvent(e, path + '.events[' + i + ']')) : [],
257
- }
258
- }
259
- const codecOf = (parse) => ({ parse })
260
- const stateCodec = codecOf((v) => parseState(v, 'state'))
261
- const configCodec = codecOf((v) => parseConfig(v, 'config'))
262
- const ackCodec = codecOf((v) => {
263
- if (v === null || typeof v !== 'object') fail('ack')
264
- return { ok: v.ok === true }
265
- })
266
- const patchCodec = codecOf((v) => {
267
- if (v === null || typeof v !== 'object' || Array.isArray(v)) fail('patch')
268
- return v
269
- })
270
- const browsersCodec = codecOf((v) => {
271
- if (v === null || typeof v !== 'object' || !Array.isArray(v.browsers)) fail('browsers')
272
- return {
273
- browsers: v.browsers.map((b, i) => ({
274
- id: typeof b?.id === 'string' ? b.id : String(i),
275
- name: typeof b?.name === 'string' ? b.name : '',
276
- path: typeof b?.path === 'string' ? b.path : '',
277
- })),
278
- }
279
- })
280
-
281
- // ── RPC 贡献(与服务端 ./typert 清单一一对应) ───────────────────────────
282
-
283
- const CONTRIBUTION = {
284
- package: 'dsh-notify-yimit',
285
- descriptors: [
286
- {
287
- id: 'dsh-notify-yimit#notify/getState', service: 'notify', namespace: 'notify', method: 'getState',
288
- invocation: { kind: 'direct' }, parameters: [],
289
- result: { mode: 'strict', typeSymbol: 'dsh-notify-yimit#NotifyState', schema: stateCodec },
290
- },
291
- {
292
- id: 'dsh-notify-yimit#notify/updateConfig', service: 'notify', namespace: 'notify', method: 'updateConfig',
293
- invocation: { kind: 'direct' },
294
- parameters: [{ name: 'patch', wire: 'patch', source: 'json', codec: { mode: 'strict', typeSymbol: 'dsh-notify-yimit#NotifyConfigPatch', schema: patchCodec } }],
295
- result: { mode: 'strict', typeSymbol: 'dsh-notify-yimit#NotifyConfig', schema: configCodec },
296
- },
297
- {
298
- id: 'dsh-notify-yimit#notify/ackEvents', service: 'notify', namespace: 'notify', method: 'ackEvents',
299
- invocation: { kind: 'direct' },
300
- parameters: [{ name: 'ids', wire: 'ids', source: 'json', codec: { mode: 'strict', typeSymbol: 'dsh-notify-yimit#EventIds', schema: codecOf((v) => Array.isArray(v) ? v.map((x) => String(x)) : []) } }],
301
- result: { mode: 'strict', typeSymbol: 'dsh-notify-yimit#AckResult', schema: ackCodec },
302
- },
303
- {
304
- id: 'dsh-notify-yimit#notify/listBrowsers', service: 'notify', namespace: 'notify', method: 'listBrowsers',
305
- invocation: { kind: 'direct' }, parameters: [],
306
- result: { mode: 'strict', typeSymbol: 'dsh-notify-yimit#BrowserList', schema: browsersCodec },
307
- },
308
- ],
309
- }
310
-
311
- // ── 客户端状态存储 ──────────────────────────────────────────────────────
312
-
313
- function makeStore(initial) {
314
- let snapshot = initial
315
- const listeners = new Set()
316
- return {
317
- getSnapshot: () => snapshot,
318
- subscribe: (fn) => {
319
- listeners.add(fn)
320
- return () => { listeners.delete(fn) }
321
- },
322
- set: (next) => {
323
- if (next === snapshot) return
324
- snapshot = next
325
- for (const fn of [...listeners]) fn()
326
- },
327
- }
328
- }
329
-
330
- const { createElement: el, Fragment, useState, useEffect, useRef, useCallback } = React
331
-
332
- // ── 通知卡片(kind 徽标文案) ────────────────────────────────────────────
333
-
334
- function kindLabel(kind, t) {
335
- switch (kind) {
336
- case 'completed': return t('kindCompleted')
337
- case 'error': return t('kindError')
338
- case 'approval': return t('kindApproval')
339
- case 'question': return t('kindQuestion')
340
- default: return t('kindRunning')
341
- }
342
- }
343
-
344
- // ── 通知调度器(shell.overlay) ────────────────────────────────────────
345
-
346
- /**
347
- * 事件调度组件:消费 store 中的 events 并回执。
348
- * - system 模式:Web Notification(系统通知中心,点击跳转会话);
349
- * - custom 模式:通知由宿主插件以桌面浮窗显示(不依赖浏览器页面),
350
- * 此处仅消费回执,不渲染任何页面内浮层;
351
- * - off / 未启用:直接消费。
352
- * 组件始终返回 null(无页面内 UI)。
353
- */
354
- function NotifyOverlay(props) {
355
- const notifyStore = props.useNotify ? props.useNotify((s) => s) : undefined
356
- const api = props.api
357
- const sessions = props.sessions
358
- const seenRef = useRef(new Set())
359
-
360
- const config = notifyStore?.config
361
- const events = notifyStore?.events
362
-
363
- const openSession = useCallback((sessionId) => {
364
- try {
365
- if (typeof sessions?.open === 'function') sessions.open(sessionId)
366
- } catch { /* 跳转失败静默 */ }
367
- }, [sessions])
368
-
369
- // 事件分发:新事件 → 系统通知 / 直接消费
370
- useEffect(() => {
371
- if (!config || !Array.isArray(events)) return
372
- const fresh = events.filter((e) => !seenRef.current.has(e.id))
373
- if (fresh.length === 0) return
374
- for (const e of fresh) seenRef.current.add(e.id)
375
- // 消费即回执(任何模式下都避免积压)。
376
- const ack = () => {
377
- try { api?.ackEvents(fresh.map((e) => e.id)) } catch { /* 静默 */ }
378
- }
379
- if (config.enabled !== true || config.mode === 'off' || config.mode === 'custom') {
380
- // custom:宿主桌面浮窗已显示(host 直接消费,队列通常为空);此处防御性回执。
381
- ack()
382
- return
383
- }
384
- // system 模式:浏览器原生通知。
385
- if (typeof Notification === 'undefined' || Notification.permission === 'denied') { ack(); return }
386
- const request = Notification.permission === 'granted'
387
- ? Promise.resolve('granted')
388
- : Notification.requestPermission()
389
- request.then((permission) => {
390
- if (permission !== 'granted') { ack(); return }
391
- for (const e of fresh) {
392
- let n
393
- try {
394
- n = new Notification(e.title && e.title.length > 0 ? e.title : e.sessionId, {
395
- body: e.text,
396
- tag: `${e.sessionId}:${e.kind}`,
397
- silent: false,
398
- })
399
- } catch {
400
- continue
401
- }
402
- n.onclick = () => {
403
- try {
404
- window.focus()
405
- openSession(e.sessionId)
406
- n.close()
407
- } catch { /* 静默 */ }
408
- }
409
- }
410
- ack()
411
- }).catch(() => ack())
412
- }, [events, config, api, openSession])
413
-
414
- return null
415
- }
416
-
417
- // ── 设置页「通知」 ─────────────────────────────────────────────────────
418
-
419
- /** 分段控制器:通知方式三选一(符合 DSH 主题的分段样式)。 */
420
- function Segmented(props) {
421
- const { value, options, onChange, disabled } = props
422
- return el('div', { className: 'dn-seg', role: 'tablist' },
423
- options.map((entry) => el('button', {
424
- key: entry[0],
425
- type: 'button',
426
- role: 'tab',
427
- 'aria-selected': String(value === entry[0]),
428
- className: 'dn-seg-btn',
429
- 'data-on': String(value === entry[0]),
430
- disabled: disabled === true,
431
- onClick: () => { if (value !== entry[0]) onChange(entry[0]) },
432
- }, entry[1])))
433
- }
434
-
435
- /** 通知类型顺序(颜色列表行序)。 */
436
- const KIND_ORDER = ['completed', 'error', 'running', 'approval', 'question']
437
-
438
- function NotifySection(props) {
439
- const notifyStore = props.useNotify ? props.useNotify((s) => s) : undefined
440
- const api = props.api
441
- const t = makeT(detectBrowserLocale())
442
- const state = notifyStore
443
- const config = state?.config
444
- const [msg, setMsg] = useState(null)
445
- const [saving, setSaving] = useState(false)
446
- const [permission, setPermission] = useState(
447
- typeof Notification !== 'undefined' ? Notification.permission : 'unsupported',
448
- )
449
-
450
- const save = useCallback(async (patch) => {
451
- if (!api?.updateConfig) return
452
- setSaving(true)
453
- setMsg(null)
454
- try {
455
- await api.updateConfig(patch)
456
- setMsg({ kind: 'ok', text: t('saved') })
457
- } catch (error) {
458
- setMsg({ kind: 'err', text: t('saveFailed', { message: error?.message ?? String(error) }) })
459
- } finally {
460
- setSaving(false)
461
- }
462
- }, [api, t])
463
-
464
- const requestPerm = useCallback(async () => {
465
- if (typeof Notification === 'undefined' || Notification.permission === 'granted') {
466
- setPermission(typeof Notification !== 'undefined' ? Notification.permission : 'unsupported')
467
- return
468
- }
469
- try {
470
- const result = await Notification.requestPermission()
471
- setPermission(result)
472
- } catch {
473
- setPermission('denied')
474
- }
475
- }, [])
476
-
477
- if (!config) {
478
- return el('div', { className: 'dn-root' }, el('div', { className: 'dn-note' }, t('loading')))
479
- }
480
-
481
- const locale = detectBrowserLocale()
482
- const tt = makeT(locale)
483
- // 关闭状态下,除总开关外的所有控件禁用。
484
- const controlsDisabled = !config.enabled
485
-
486
- // 每种通知类型一行:色块 + 名称 + 背景/文字取色器(colors 深合并保存)。
487
- const kindRows = KIND_ORDER.map((kind) => {
488
- const entry = config.colors?.[kind] ?? {}
489
- const bg = entry.bg || config.bgColor || '#20272f'
490
- const fg = entry.fg || config.textColor || '#e6edf3'
491
- return el('div', { key: kind, className: 'dn-kind-row' },
492
- el('span', { className: 'dn-kind-swatch', style: { background: bg } }),
493
- el('span', { className: 'dn-kind-name' }, kindLabel(kind, t)),
494
- el('label', { className: 'dn-kind-field' },
495
- el('input', {
496
- type: 'color',
497
- className: 'dn-input',
498
- title: tt('colorBg'),
499
- value: /^#[0-9a-fA-F]{6}$/.test(bg) ? bg : '#20272f',
500
- disabled: controlsDisabled,
501
- onChange: (e) => save({ colors: { [kind]: { ...entry, bg: e.target.value } } }),
502
- }),
503
- tt('colorBg')),
504
- el('label', { className: 'dn-kind-field' },
505
- el('input', {
506
- type: 'color',
507
- className: 'dn-input',
508
- title: tt('colorFg'),
509
- value: /^#[0-9a-fA-F]{6}$/.test(fg) ? fg : '#e6edf3',
510
- disabled: controlsDisabled,
511
- onChange: (e) => save({ colors: { [kind]: { ...entry, fg: e.target.value } } }),
512
- }),
513
- tt('colorFg')))
514
- })
515
-
516
- // 自定义通知设置区块:始终渲染以支持显隐过渡动画(折叠/淡出)。
517
- const customOpen = config.mode === 'custom'
518
-
519
- return el('div', { className: 'dn-root' },
520
- // 总开关
521
- el('div', { className: 'dn-block' },
522
- el('div', { className: 'dn-row' },
523
- el('button', {
524
- type: 'button',
525
- role: 'switch',
526
- 'aria-checked': config.enabled,
527
- className: 'dn-switch',
528
- 'data-on': String(config.enabled),
529
- onClick: () => save({ enabled: !config.enabled }),
530
- }),
531
- el('span', { className: 'dn-label' }, t('enabledLabel'))),
532
- el('p', { className: 'dn-note' }, t('enabledNote'))),
533
-
534
- el('div', { className: 'dn-divider' }),
535
-
536
- // 通知方式(分段控制器)
537
- el('div', { className: 'dn-block' },
538
- el('p', { className: 'dn-title' }, t('modeLabel')),
539
- el(Segmented, {
540
- value: config.mode,
541
- disabled: controlsDisabled,
542
- onChange: (mode) => save({ mode }),
543
- options: [
544
- ['system', t('modeSystem')],
545
- ['custom', t('modeCustom')],
546
- ['off', t('modeOff')],
547
- ],
548
- }),
549
- el('p', { className: 'dn-note' }, t('modeNote'))),
550
-
551
- // 跳转会话浏览器选择器(检测本机已安装浏览器;空 = 系统默认)。
552
- // 「跳转会话」始终打开浏览器(功能恒开启),故不再提供开关;选择器在通知方式
553
- // 开启时(system / custom)显示,插件总开关关闭时禁用。
554
- config.mode === 'system' || config.mode === 'custom'
555
- ? el('div', { className: 'dn-block' },
556
- el('div', { className: 'dn-row' },
557
- el('span', { className: 'dn-label' }, t('browserLabel')),
558
- el('select', {
559
- className: 'dn-input dn-select',
560
- value: config.browser || '',
561
- disabled: controlsDisabled,
562
- onChange: (e) => save({ browser: e.target.value }),
563
- },
564
- el('option', { value: '' }, t('browserDefault')),
565
- (state?.browsers ?? []).map((b) => el('option', { key: b.id, value: b.path }, b.name)))),
566
- el('p', { className: 'dn-note' }, t('browserNote')))
567
- : null,
568
-
569
- el('div', { className: 'dn-divider' }),
570
-
571
- // 系统通知:权限
572
- config.mode === 'system'
573
- ? el('div', { className: 'dn-block' },
574
- el('p', { className: 'dn-title' }, t('systemTitle')),
575
- permission === 'granted'
576
- ? el('div', { className: 'dn-msg ok' }, t('permissionGranted'))
577
- : permission === 'denied'
578
- ? el('div', { className: 'dn-msg err' }, t('permissionDenied'))
579
- : el(Fragment, null,
580
- el('p', { className: 'dn-note' }, t('permissionDefault')),
581
- el('button', {
582
- className: 'dn-btn primary',
583
- disabled: controlsDisabled,
584
- onClick: requestPerm,
585
- }, t('requestPermission'))))
586
- : null,
587
-
588
- // 自定义通知设置(折叠 + 淡出过渡;关闭时可见但不可操作)
589
- el('div', {
590
- className: 'dn-collapse',
591
- 'data-open': String(customOpen),
592
- },
593
- el('div', { className: 'dn-collapse-body' },
594
- el('div', { className: 'dn-block' },
595
- el('p', { className: 'dn-title' }, t('customTitle')),
596
- el('div', { className: 'dn-row' },
597
- el('span', { className: 'dn-label' }, t('maxVisibleLabel')),
598
- el('input', {
599
- type: 'number',
600
- min: 1,
601
- max: 20,
602
- className: 'dn-input',
603
- value: config.maxVisible,
604
- disabled: controlsDisabled,
605
- onChange: (e) => {
606
- const n = Math.max(1, Math.min(20, Math.floor(Number(e.target.value) || 1)))
607
- save({ maxVisible: n })
608
- },
609
- })),
610
- el('div', { className: 'dn-row' },
611
- el('span', { className: 'dn-label' }, t('durationLabel')),
612
- el('input', {
613
- type: 'number',
614
- min: 1,
615
- max: 300,
616
- className: 'dn-input',
617
- value: Math.round(config.durationMs / 1000),
618
- disabled: controlsDisabled,
619
- onChange: (e) => {
620
- const s = Math.max(1, Math.min(300, Math.floor(Number(e.target.value) || 1)))
621
- save({ durationMs: s * 1000 })
622
- },
623
- })),
624
- el('p', { className: 'dn-title' }, t('colorsTitle')),
625
- el('div', { className: 'dn-kind-list' }, kindRows)))),
626
-
627
- el('div', { className: 'dn-divider' }),
628
- el('p', { className: 'dn-note' }, t('triggerNote')),
629
- el('p', { className: 'dn-note' }, t('saveHint')),
630
- msg !== null ? el('div', { className: 'dn-msg ' + msg.kind }, msg.text) : null,
631
- saving ? el('div', { className: 'dn-note' }, '…') : null,
632
- )
633
- }
634
-
635
- // ── 插件主体 ────────────────────────────────────────────────────────────
636
-
637
- const inject = ['remote']
638
-
639
- async function apply(ctx) {
640
- const remote = ctx.remote
641
- if (remote === undefined || typeof remote.$mount !== 'function') return
642
- const unmount = await remote.$mount(CONTRIBUTION)
643
- ctx.effect(() => () => { unmount() }, 'dsh-notify-yimit: remote contribution')
644
- const notify = ctx.get('remote.notify')
645
- if (notify === undefined) return
646
-
647
- const store = makeStore({ config: null, events: [], browsers: [] })
648
-
649
- // 探测本机浏览器(设置页"跳转会话浏览器"选择器数据源)。
650
- const loadBrowsers = async () => {
651
- try {
652
- const result = await notify.listBrowsers()
653
- if (result !== null && typeof result === 'object' && result.ok === true) {
654
- const parsed = browsersCodec.parse(result.value)
655
- const state = store.getSnapshot()
656
- store.set({ config: state.config, events: state.events, browsers: parsed.browsers })
657
- }
658
- } catch {
659
- // 探测失败:选择器仅剩"默认浏览器"。
660
- }
661
- }
662
-
663
- let reloading = false
664
- const reload = async () => {
665
- if (reloading) return
666
- reloading = true
667
- try {
668
- const result = await notify.getState()
669
- if (result !== null && typeof result === 'object' && result.ok === true) {
670
- // 保留 browsers(浏览器列表来自独立 listBrowsers 调用,轮询不得覆盖)。
671
- const prev = store.getSnapshot()
672
- store.set({ ...parseState(result.value, 'state'), browsers: prev.browsers })
673
- }
674
- } catch {
675
- // 传输失败保持旧快照;下次轮询重试。
676
- } finally {
677
- reloading = false
678
- }
679
- }
680
-
681
- const api = {
682
- reload,
683
- updateConfig: async (patch) => {
684
- const result = await notify.updateConfig(patch)
685
- if (result !== null && typeof result === 'object' && result.ok === true) {
686
- // 保留 browsers(浏览器列表来自独立 listBrowsers 调用,配置更新不得覆盖),
687
- // 否则选择器选项会塌缩成只剩"默认浏览器"(改一次选择后消失的 bug)。
688
- const state = store.getSnapshot()
689
- store.set({ config: parseConfig(result.value, 'config'), events: state.events, browsers: state.browsers })
690
- }
691
- return result?.value
692
- },
693
- ackEvents: async (ids) => {
694
- try {
695
- await notify.ackEvents(ids)
696
- } catch {
697
- // 回执失败:事件保留,下次轮询会重新看到(seen 去重避免重复展示)。
698
- }
699
- },
700
- }
701
-
702
- void reload()
703
- void loadBrowsers()
704
- ctx.effect(() => ctx.on('connection/reset', () => { void reload(); void loadBrowsers() }), 'dsh-notify-yimit: reconnect reload')
705
- // system 模式走浏览器通知:250ms 轮询把平均延迟从 ~500ms 压到 ~125ms,感知为"即时"。
706
- const pollTimer = setInterval(() => { void reload() }, 250)
707
- ctx.effect(() => () => { clearInterval(pollTimer) }, 'dsh-notify-yimit: poll timer')
708
- const onVisible = () => { if (document.visibilityState === 'visible') void reload() }
709
- document.addEventListener('visibilitychange', onVisible)
710
- ctx.effect(() => () => { document.removeEventListener('visibilitychange', onVisible) }, 'dsh-notify-yimit: visibility reload')
711
-
712
- const sessions = ctx.get('sessions')
713
- const slots = ctx.get('slots')
714
- if (slots === undefined) return
715
-
716
- // 会话深链接(桌面浮窗"跳转会话"通道):监听 #dsh-notify-yimit/session=<id>。
717
- const openFromHash = () => {
718
- try {
719
- const m = /#dsh-notify-yimit\/session=([^&]+)/.exec(window.location.hash)
720
- if (m !== null && typeof sessions?.open === 'function') {
721
- sessions.open(decodeURIComponent(m[1]))
722
- const clean = window.location.href.split('#')[0]
723
- window.history.replaceState(null, '', clean)
724
- }
725
- } catch { /* 静默 */ }
726
- }
727
- window.addEventListener('hashchange', openFromHash)
728
- openFromHash()
729
- ctx.effect(() => () => window.removeEventListener('hashchange', openFromHash), 'dsh-notify-yimit: hash listener')
730
-
731
- const injected = () => ({ hooks: { notify: store }, api, sessions })
732
- const sectionInjected = () => ({ hooks: { notify: store }, api })
733
-
734
- // 设置页「通知」分节
735
- const sectionActive = { gen: 0, dispose: null }
736
- const registerSection = () => {
737
- if (sectionActive.dispose !== null) { sectionActive.dispose(); sectionActive.dispose = null }
738
- sectionActive.gen += 1
739
- const gen = sectionActive.gen
740
- slots.inject('settings.section', () => {
741
- if (sectionActive.gen !== gen) return
742
- const dispose = slots.register({
743
- name: 'settings.section',
744
- id: 'dsh-notify-yimit',
745
- order: 60,
746
- label: () => makeT(detectBrowserLocale())('sectionLabel'),
747
- inject: sectionInjected,
748
- }, NotifySection)
749
- if (sectionActive.gen !== gen) { dispose(); return }
750
- sectionActive.dispose = dispose
751
- return () => {
752
- if (sectionActive.dispose === dispose) sectionActive.dispose = null
753
- dispose()
754
- }
755
- })
756
- }
757
- registerSection()
758
-
759
- // 全局浮层(自定义通知)
760
- const overlayActive = { gen: 0, dispose: null }
761
- const registerOverlay = () => {
762
- if (overlayActive.dispose !== null) { overlayActive.dispose(); overlayActive.dispose = null }
763
- overlayActive.gen += 1
764
- const gen = overlayActive.gen
765
- slots.inject('shell.overlay', () => {
766
- if (overlayActive.gen !== gen) return
767
- const dispose = slots.register({
768
- name: 'shell.overlay',
769
- id: 'dsh-notify-yimit',
770
- order: 10,
771
- inject: injected,
772
- }, NotifyOverlay)
773
- if (overlayActive.gen !== gen) { dispose(); return }
774
- overlayActive.dispose = dispose
775
- return () => {
776
- if (overlayActive.dispose === dispose) overlayActive.dispose = null
777
- dispose()
778
- }
779
- })
780
- }
781
- registerOverlay()
782
-
783
- return () => {}
784
- }
785
-
786
- exports.apply = apply
787
- exports.inject = inject
788
- return module.exports
789
- },
790
- })
1
+ /**
2
+ * dsh-notify-yimit 浏览器端 bundle。
3
+ * 语言跟随:接入 `locale` 服务(由 dsh 框架注入),完全对齐 dsh-market 的标准用法;
4
+ * 旧宿主无该服务时降级到 navigator/localStorage 轮询,绝不阻塞启动。
5
+ */
6
+ window.__ModuleLoader__.load({
7
+ id: 'dsh-notify-yimit',
8
+ factory: (require) => {
9
+ var module = {
10
+ exports: {}
11
+ }
12
+ var exports = module.exports
13
+ Object.defineProperty(exports, Symbol.toStringTag, {
14
+ value: 'Module'
15
+ })
16
+
17
+ const React = require('react')
18
+ const {
19
+ createElement: el,
20
+ Fragment,
21
+ useState,
22
+ useEffect,
23
+ useRef,
24
+ useCallback,
25
+ useSyncExternalStore
26
+ } = React
27
+
28
+ const css = [
29
+ '/* dsh-notify-yimit: 设置页与自定义通知浮层 */',
30
+ '.dn-root{display:flex;flex-direction:column;gap:20px;padding:4px 2px 24px;font-size:13px;color:var(--dsw-alias-label-primary)}',
31
+ '.dn-block{display:flex;flex-direction:column;gap:10px}',
32
+ '.dn-title{font-size:13px;font-weight:600;margin:0}',
33
+ '.dn-note{font-size:12px;line-height:20px;color:var(--dsw-alias-label-tertiary);margin:0}',
34
+ '.dn-row{display:flex;align-items:center;gap:10px}',
35
+ '.dn-label{font-size:13px;color:var(--dsw-alias-label-secondary);flex:none}',
36
+ '.dn-switch{position:relative;width:38px;height:22px;border-radius:999px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;padding:0;flex:none;transition:background .15s ease,border-color .15s ease}',
37
+ '.dn-switch[data-on="true"]{background:var(--dsw-alias-state-business-primary);border-color:transparent}',
38
+ '.dn-switch::after{content:"";position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:var(--dsw-alias-label-secondary);transition:transform .15s ease,background .15s ease}',
39
+ '.dn-switch[data-on="true"]::after{transform:translateX(16px);background:var(--dsw-alias-label-primary-inverted)}',
40
+ '.dn-seg{display:flex;gap:2px;padding:2px;border-radius:10px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);max-width:520px}',
41
+ '.dn-seg-btn{flex:1;font:inherit;font-size:13px;color:var(--dsw-alias-label-secondary);border:0;border-radius:8px;padding:6px 10px;cursor:pointer;background:transparent;transition:background .15s ease,color .15s ease;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
42
+ '.dn-seg-btn:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}',
43
+ // 选中态用主题主色 + 反色文字:浅色模式下与背景对比也足够清晰(此前浅色下
44
+ // 白底+深灰与容器太接近,深色正常)。
45
+ '.dn-seg-btn[data-on="true"]{background:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-primary-inverted);box-shadow:var(--dsw-shadow-lv1)}',
46
+ '.dn-seg-btn[data-on="true"]:hover{background:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-primary-inverted)}',
47
+ '.dn-seg-btn:disabled{opacity:.45;cursor:not-allowed}',
48
+ '.dn-seg-btn:disabled:hover{background:transparent;color:var(--dsw-alias-label-secondary)}',
49
+ '.dn-collapse{display:grid;grid-template-rows:1fr;opacity:1;transition:grid-template-rows .24s ease,opacity .2s ease}',
50
+ '.dn-collapse[data-open="false"]{grid-template-rows:0fr;opacity:0}',
51
+ '.dn-collapse>.dn-collapse-body{overflow:hidden;min-height:0}',
52
+ '.dn-collapse>.dn-collapse-body>.dn-block{min-width:0}',
53
+ '.dn-kind-list{display:flex;flex-direction:column;gap:8px}',
54
+ '.dn-kind-row{display:flex;align-items:center;gap:12px;padding:8px 12px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-1)}',
55
+ // 每类型的背景/文字/开关项右对齐(预览块留在左侧)
56
+ '.dn-kind-row>.dn-kind-field:first-of-type{margin-left:auto}',
57
+ // 样式预览:纯色块内嵌类型文字,始终用配置的背景/文字色(内联样式,不受主题影响)
58
+ '.dn-kind-preview{flex:none;min-width:72px;text-align:center;font-size:12px;font-weight:600;line-height:22px;padding:2px 10px;border-radius:6px;white-space:nowrap;box-shadow:inset 0 0 0 1px rgba(0,0,0,.08)}',
59
+ '.dn-kind-field{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--dsw-alias-label-tertiary);flex:none}',
60
+ '.dn-input{font:inherit;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:5px 10px;outline:none;max-width:120px}',
61
+ '.dn-input:focus{border-color:var(--dsw-alias-state-business-primary)}',
62
+ '.dn-input[type=color]{padding:2px 4px;width:48px;height:28px;cursor:pointer;max-width:none}',
63
+ '.dn-input:disabled{opacity:.45;cursor:not-allowed}',
64
+ // 浏览器选择器:原生 select 的展开列表为系统绘制、无法贴合主题,改为自绘下拉,
65
+ // 触发器与弹层都走 --dsw-* 变量,深浅色模式下与设置页风格一致。
66
+ '.dn-bselect{position:relative;width:280px;max-width:100%;flex:none}',
67
+ '.dn-bselect-trigger{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;font:inherit;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:5px 10px;cursor:pointer;outline:none}',
68
+ '.dn-bselect-trigger:hover:not(:disabled){border-color:var(--dsw-alias-border-l2)}',
69
+ '.dn-bselect-trigger.open{border-color:var(--dsw-alias-state-business-primary)}',
70
+ '.dn-bselect-trigger:disabled{opacity:.45;cursor:not-allowed}',
71
+ '.dn-bselect-value{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
72
+ '.dn-bselect-arrow{flex:none;font-size:10px;color:var(--dsw-alias-label-tertiary)}',
73
+ '.dn-bselect-pop{position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:100;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;box-shadow:var(--dsw-shadow-lv3);padding:4px;max-height:240px;overflow:auto}',
74
+ '.dn-bselect-opt{display:block;width:100%;text-align:left;font:inherit;font-size:13px;color:var(--dsw-alias-label-primary);background:transparent;border:0;border-radius:7px;padding:6px 10px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
75
+ '.dn-bselect-opt:hover{background:var(--dsw-alias-interactive-bg-hover)}',
76
+ '.dn-bselect-opt.sel{color:var(--dsw-alias-state-business-primary);font-weight:600}',
77
+ '.dn-color-preview{font-size:12px;color:var(--dsw-alias-label-tertiary);font-family:var(--dsw-font-mono)}',
78
+ '.dn-btn{font:inherit;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-button-elevated-fill);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:6px 14px;cursor:pointer}',
79
+ '.dn-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}',
80
+ '.dn-btn.primary{background:var(--dsw-alias-state-business-primary);border-color:transparent;color:var(--dsw-alias-label-primary-inverted)}',
81
+ '.dn-btn.primary:hover{opacity:0.88}',
82
+ '.dn-btn:disabled{opacity:.5;cursor:not-allowed}',
83
+ '.dn-msg{font-size:12px;line-height:18px;padding:8px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l1)}',
84
+ '.dn-msg.ok{color:var(--dsw-alias-state-success-primary)}',
85
+ '.dn-msg.err{color:var(--dsw-alias-state-error-primary)}',
86
+ '.dn-divider{height:1px;background:var(--dsw-alias-border-l1)}',
87
+ '.dn-layer{position:fixed;top:16px;right:16px;z-index:9999;display:flex;flex-direction:column;align-items:flex-end;gap:10px;pointer-events:none}',
88
+ '.dn-card{pointer-events:auto;width:320px;max-width:calc(100vw - 32px);border-radius:12px;box-shadow:var(--dsw-shadow-lv3);border:1px solid var(--dsw-alias-border-l2);background:var(--dn-bg,#20272f);color:var(--dn-fg,#e6edf3);overflow:hidden;animation:dn-in .18s ease}',
89
+ '@keyframes dn-in{from{opacity:0;transform:translateX(24px)}to{opacity:1;transform:translateX(0)}}',
90
+ '.dn-card-head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 12px 6px}',
91
+ '.dn-card-title{font-size:13px;font-weight:600;line-height:20px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
92
+ '.dn-card-kind{flex:none;font-size:11px;line-height:18px;border-radius:999px;padding:0 8px;border:1px solid currentColor;opacity:.85}',
93
+ '.dn-card-body{padding:0 12px 8px;font-size:13px;line-height:20px;word-break:break-word;max-height:120px;overflow:hidden;display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical}',
94
+ '.dn-card-actions{display:flex;align-items:center;gap:6px;padding:6px 12px 10px}',
95
+ '.dn-card-btn{font:inherit;font-size:12px;line-height:18px;border-radius:6px;padding:4px 10px;cursor:pointer;border:1px solid rgba(128,128,128,0.35);border:1px solid color-mix(in srgb,currentColor 35%,transparent);background:transparent;background:color-mix(in srgb,currentColor 12%,transparent);color:inherit}',
96
+ '.dn-card-btn:hover{background:rgba(128,128,128,0.12);background:color-mix(in srgb,currentColor 12%,transparent)}',
97
+ '.dn-card-btn.primary{background:rgba(128,128,128,0.18);background:color-mix(in srgb,currentColor 18%,transparent)}',
98
+ '.dn-card-bar{height:2px;background:rgba(128,128,128,0.25);background:color-mix(in srgb,currentColor 25%,transparent);transform-origin:left}',
99
+ '@media (prefers-reduced-motion: reduce){.dn-card{animation:none}}',
100
+ ].join('\n')
101
+
102
+ const cssTagId = 'dsh-notify-yimit/client.css'
103
+ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css=' + JSON
104
+ .stringify(cssTagId) + ']') === null) {
105
+ const tag = document.createElement('style')
106
+ tag.dataset.plugin = 'dsh-notify-yimit'
107
+ tag.dataset.pluginCss = cssTagId
108
+ tag.textContent = css
109
+ document.head.appendChild(tag)
110
+ }
111
+
112
+ const NS = 'dsh-notify-yimit'
113
+
114
+ const zh = {
115
+ sectionLabel: '通知',
116
+ enabledLabel: '启用通知插件',
117
+ enabledNote: '关闭后不产生任何通知(设置仍会保存)。',
118
+ modeLabel: '通知方式',
119
+ modeSystem: 'Windows 系统通知',
120
+ modeCustom: '插件自定义样式通知',
121
+ modeOff: '关闭',
122
+ modeNote: '系统通知由浏览器弹出、可在系统通知中心查看;自定义通知为桌面右下角浮窗(不依赖浏览器页面)。',
123
+ browserLabel: '跳转会话浏览器',
124
+ browserDefault: '默认浏览器(系统)',
125
+ browserNote: '「跳转会话」始终打开浏览器并定位到该会话;可选指定浏览器打开(默认=系统默认浏览器)。',
126
+ customTitle: '自定义通知设置',
127
+ colorsTitle: '每种通知的样式',
128
+ colorBg: '背景',
129
+ colorFg: '文字',
130
+ maxVisibleLabel: '同时最多显示数量',
131
+ durationLabel: '通知显示时长(秒)',
132
+ bgColorLabel: '背景颜色',
133
+ textColorLabel: '文字颜色',
134
+ systemTitle: '系统通知',
135
+ permissionGranted: '已获得系统通知权限',
136
+ permissionDenied: '系统通知权限被拒绝,请在浏览器设置中允许本页面通知',
137
+ permissionDefault: '系统通知需要浏览器通知权限',
138
+ requestPermission: '申请通知权限',
139
+ triggerNote: '触发场景:任务完成、任务出错、运行中(实时内容)、等待审批、等待回答。通知标题为对话标题;点击通知或跳转按钮可打开对应会话。',
140
+ toastStyleTitle: '通知窗口样式',
141
+ toastGapLabel: '通知间距(px)',
142
+ enterEdit: '进入编辑模式',
143
+ endEdit: '完成编辑',
144
+ editHint: '拖动样板窗调整位置;拖拽窗口边缘调整宽度;完成后点击「完成编辑」保存。',
145
+ resetPosition: '恢复默认位置',
146
+ saveHint: '所有设置修改后自动保存。',
147
+ saved: '已保存',
148
+ saveFailed: '保存失败:{message}',
149
+ loading: '正在读取配置…',
150
+ rpcFailed: '读取配置失败:{message}',
151
+ kindCompleted: '已完成',
152
+ kindError: '出错',
153
+ kindRunning: '运行中',
154
+ kindApproval: '待审批',
155
+ kindQuestion: '待回答',
156
+ ignore: '忽略',
157
+ jump: '跳转会话',
158
+ }
159
+
160
+ const en = {
161
+ sectionLabel: 'Notifications',
162
+ enabledLabel: 'Enable notification plugin',
163
+ enabledNote: 'When disabled, no notifications are produced (settings are still saved).',
164
+ modeLabel: 'Notification style',
165
+ modeSystem: 'Windows system notifications',
166
+ modeCustom: 'Custom in-app notifications',
167
+ modeOff: 'Off',
168
+ modeNote: 'System notifications are shown by the browser and live in the system notification center; custom notifications are desktop toasts at the bottom-right (independent of the browser page).',
169
+ browserLabel: 'Browser for jumping to a session',
170
+ browserDefault: 'Default browser (system)',
171
+ browserNote: '"Jump to session" always opens the browser and navigates to the session; optionally pick a specific browser (default = system default).',
172
+ customTitle: 'Custom notification settings',
173
+ colorsTitle: 'Style per notification type',
174
+ colorBg: 'BG',
175
+ colorFg: 'FG',
176
+ maxVisibleLabel: 'Max simultaneously visible',
177
+ durationLabel: 'Display duration (seconds)',
178
+ bgColorLabel: 'Background color',
179
+ textColorLabel: 'Text color',
180
+ systemTitle: 'System notifications',
181
+ permissionGranted: 'System notification permission granted',
182
+ permissionDenied: 'System notification permission denied — allow notifications for this site in your browser settings',
183
+ permissionDefault: 'System notifications need browser notification permission',
184
+ requestPermission: 'Request permission',
185
+ triggerNote: 'Triggers: task completed, task failed, running (live activity), awaiting approval, awaiting answer. The title is the conversation title; clicking the notification or the jump button opens the session.',
186
+ toastStyleTitle: 'Toast window style',
187
+ toastGapLabel: 'Toast gap (px)',
188
+ enterEdit: 'Enter edit mode',
189
+ endEdit: 'Finish editing',
190
+ editHint: 'Drag the sample window to reposition; drag its edges to resize the width; click "Finish editing" to save.',
191
+ resetPosition: 'Reset to default position',
192
+ saveHint: 'All settings changes are auto-saved.',
193
+ saved: 'Saved',
194
+ saveFailed: 'Save failed: {message}',
195
+ loading: 'Loading config…',
196
+ rpcFailed: 'Failed to load config: {message}',
197
+ kindCompleted: 'Done',
198
+ kindError: 'Failed',
199
+ kindRunning: 'Running',
200
+ kindApproval: 'Approval',
201
+ kindQuestion: 'Question',
202
+ ignore: 'Ignore',
203
+ jump: 'Open',
204
+ }
205
+
206
+ // ===== 兼容层:ctx.locale 可用时直接用,不可用时降级为外部 store =====
207
+ let __localeService = null
208
+ let __fallbackUnsub = null
209
+ const __fallbackListeners = new Set()
210
+ let __fallbackLocale = 'en'
211
+
212
+ function detectLocaleId() {
213
+ try {
214
+ if (typeof localStorage !== 'undefined') {
215
+ const sl = localStorage.getItem('dsh-language') || localStorage.getItem('language') ||
216
+ localStorage.getItem('i18nextLng') || localStorage.getItem('locale')
217
+ if (sl) return String(sl).toLowerCase().startsWith('zh') ? 'zh' : 'en'
218
+ }
219
+ } catch {}
220
+ if (typeof document !== 'undefined' && document.documentElement && typeof document
221
+ .documentElement.lang === 'string' && document.documentElement.lang) {
222
+ return document.documentElement.lang.toLowerCase().startsWith('zh') ? 'zh' : 'en'
223
+ }
224
+ const lang = typeof navigator !== 'undefined' && typeof navigator.language === 'string' ?
225
+ navigator.language : ''
226
+ return lang.toLowerCase().startsWith('zh') ? 'zh' : 'en'
227
+ }
228
+
229
+ // 外部 store:供 useSyncExternalStore 使用,统一走 ctx.locale 或降级源
230
+ const localeStore = {
231
+ subscribe(cb) {
232
+ if (__localeService && typeof __localeService.subscribe === 'function') {
233
+ return __localeService.subscribe(cb)
234
+ }
235
+ __fallbackListeners.add(cb)
236
+ if (__fallbackUnsub === null) {
237
+ __fallbackLocale = detectLocaleId()
238
+ __fallbackUnsub = setInterval(() => {
239
+ const next = detectLocaleId()
240
+ if (next !== __fallbackLocale) {
241
+ __fallbackLocale = next
242
+ for (const fn of [...__fallbackListeners]) fn()
243
+ }
244
+ }, 500)
245
+ }
246
+ return () => {
247
+ __fallbackListeners.delete(cb)
248
+ if (__fallbackListeners.size === 0 && __fallbackUnsub !== null) {
249
+ clearInterval(__fallbackUnsub);
250
+ __fallbackUnsub = null
251
+ }
252
+ }
253
+ },
254
+ getSnapshot() {
255
+ if (__localeService && typeof __localeService.getSnapshot === 'function') {
256
+ return __localeService.getSnapshot()
257
+ }
258
+ return {
259
+ active: __fallbackLocale,
260
+ locales: ['zh', 'en'],
261
+ revision: 0
262
+ }
263
+ }
264
+ }
265
+
266
+ function currentLangId() {
267
+ try {
268
+ const snap = localeStore.getSnapshot()
269
+ const active = String(snap?.active ?? snap?.locale ?? snap?.language ?? __fallbackLocale ??
270
+ 'en')
271
+ return active.toLowerCase().startsWith('zh') ? 'zh' : 'en'
272
+ } catch {
273
+ return 'en'
274
+ }
275
+ }
276
+
277
+ function makeT() {
278
+ const dict = () => currentLangId() === 'zh' ? zh : en
279
+ return (key, vars) => {
280
+ const d = dict()
281
+ let text = d[key] ?? en[key] ?? key
282
+ if (vars)
283
+ for (const name of Object.keys(vars)) text = text.split('{' + name + '}').join(
284
+ String(vars[name]))
285
+ return text
286
+ }
287
+ }
288
+
289
+ function fail(path) {
290
+ throw new Error('dsh-notify-yimit: Invalid server data (' + path + ')')
291
+ }
292
+
293
+ function needStr(v, path) {
294
+ if (typeof v !== 'string') fail(path);
295
+ return v
296
+ }
297
+
298
+ function needNum(v, path) {
299
+ if (typeof v !== 'number' || !Number.isFinite(v)) fail(path);
300
+ return v
301
+ }
302
+
303
+ function needBool(v, path) {
304
+ if (typeof v !== 'boolean') fail(path);
305
+ return v
306
+ }
307
+
308
+ function parseConfig(v, path) {
309
+ if (v === null || typeof v !== 'object' || Array.isArray(v)) fail(path)
310
+ const kindColors = (key, fallbackBg, fallbackFg) => {
311
+ const entry = v.colors !== null && typeof v.colors === 'object' && !Array.isArray(v
312
+ .colors) ? v.colors[key] : undefined
313
+ return {
314
+ bg: typeof entry?.bg === 'string' ? entry.bg : fallbackBg,
315
+ fg: typeof entry?.fg === 'string' ? entry.fg : fallbackFg,
316
+ enabled: entry?.enabled !== false
317
+ }
318
+ }
319
+ const topBg = typeof v.bgColor === 'string' ? v.bgColor : '#20272f'
320
+ const topFg = typeof v.textColor === 'string' ? v.textColor : '#e6edf3'
321
+ return {
322
+ enabled: v.enabled === true,
323
+ mode: v.mode === 'system' || v.mode === 'custom' || v.mode === 'off' ? v.mode : 'off',
324
+ maxVisible: typeof v.maxVisible === 'number' ? v.maxVisible : 3,
325
+ durationMs: typeof v.durationMs === 'number' ? v.durationMs : 8000,
326
+ browser: typeof v.browser === 'string' ? v.browser : '',
327
+ bgColor: topBg,
328
+ textColor: topFg,
329
+ locale: v.locale === 'zh' || v.locale === 'en' ? v.locale : 'en',
330
+ toast: {
331
+ width: typeof v.toast?.width === 'number' ? v.toast.width : 340,
332
+ gap: typeof v.toast?.gap === 'number' ? v.toast.gap : 12,
333
+ position: v.toast !== null && typeof v.toast === 'object' && v.toast.position !== null &&
334
+ typeof v.toast.position === 'object' && !Array.isArray(v.toast.position) &&
335
+ typeof v.toast.position.left === 'number' && typeof v.toast.position.top === 'number'
336
+ ? { left: v.toast.position.left, top: v.toast.position.top }
337
+ : null,
338
+ },
339
+ colors: {
340
+ completed: kindColors('completed', topBg, topFg),
341
+ error: kindColors('error', topBg, topFg),
342
+ running: kindColors('running', topBg, topFg),
343
+ approval: kindColors('approval', topBg, topFg),
344
+ question: kindColors('question', topBg, topFg),
345
+ },
346
+ }
347
+ }
348
+
349
+ function parseEvent(v, path) {
350
+ if (v === null || typeof v !== 'object' || Array.isArray(v)) fail(path)
351
+ return {
352
+ id: needStr(v.id, path + '.id'),
353
+ sessionId: needStr(v.sessionId, path + '.sessionId'),
354
+ title: typeof v.title === 'string' ? v.title : '',
355
+ kind: v.kind === 'completed' || v.kind === 'error' || v.kind === 'running' || v.kind ===
356
+ 'approval' || v.kind === 'question' ? v.kind : 'running',
357
+ text: needStr(v.text, path + '.text'),
358
+ activity: typeof v.activity === 'string' ? v.activity : '',
359
+ at: needNum(v.at, path + '.at'),
360
+ }
361
+ }
362
+
363
+ function parseState(v, path) {
364
+ if (v === null || typeof v !== 'object' || Array.isArray(v)) fail(path)
365
+ return {
366
+ config: parseConfig(v.config, path + '.config'),
367
+ events: Array.isArray(v.events) ? v.events.map((e, i) => parseEvent(e, path + '.events[' +
368
+ i + ']')) : []
369
+ }
370
+ }
371
+
372
+ const codecOf = (parse) => ({
373
+ parse
374
+ })
375
+ const stateCodec = codecOf((v) => parseState(v, 'state'))
376
+ const configCodec = codecOf((v) => parseConfig(v, 'config'))
377
+ const ackCodec = codecOf((v) => {
378
+ if (v === null || typeof v !== 'object') fail('ack');
379
+ return {
380
+ ok: v.ok === true
381
+ }
382
+ })
383
+ const patchCodec = codecOf((v) => {
384
+ if (v === null || typeof v !== 'object' || Array.isArray(v)) fail('patch');
385
+ return v
386
+ })
387
+ const browsersCodec = codecOf((v) => {
388
+ if (v === null || typeof v !== 'object' || !Array.isArray(v.browsers)) fail('browsers')
389
+ return {
390
+ browsers: v.browsers.map((b, i) => ({
391
+ id: typeof b?.id === 'string' ? b.id : String(i),
392
+ name: typeof b?.name === 'string' ? b.name : '',
393
+ path: typeof b?.path === 'string' ? b.path : ''
394
+ }))
395
+ }
396
+ })
397
+
398
+ const CONTRIBUTION = {
399
+ package: 'dsh-notify-yimit',
400
+ descriptors: [{
401
+ id: 'dsh-notify-yimit#notify/getState',
402
+ service: 'notify',
403
+ namespace: 'notify',
404
+ method: 'getState',
405
+ invocation: {
406
+ kind: 'direct'
407
+ },
408
+ parameters: [],
409
+ result: {
410
+ mode: 'strict',
411
+ typeSymbol: 'dsh-notify-yimit#NotifyState',
412
+ schema: stateCodec
413
+ }
414
+ }, {
415
+ id: 'dsh-notify-yimit#notify/updateConfig',
416
+ service: 'notify',
417
+ namespace: 'notify',
418
+ method: 'updateConfig',
419
+ invocation: {
420
+ kind: 'direct'
421
+ },
422
+ parameters: [{
423
+ name: 'patch',
424
+ wire: 'patch',
425
+ source: 'json',
426
+ codec: {
427
+ mode: 'strict',
428
+ typeSymbol: 'dsh-notify-yimit#NotifyConfigPatch',
429
+ schema: patchCodec
430
+ }
431
+ }],
432
+ result: {
433
+ mode: 'strict',
434
+ typeSymbol: 'dsh-notify-yimit#NotifyConfig',
435
+ schema: configCodec
436
+ }
437
+ }, {
438
+ id: 'dsh-notify-yimit#notify/ackEvents',
439
+ service: 'notify',
440
+ namespace: 'notify',
441
+ method: 'ackEvents',
442
+ invocation: {
443
+ kind: 'direct'
444
+ },
445
+ parameters: [{
446
+ name: 'ids',
447
+ wire: 'ids',
448
+ source: 'json',
449
+ codec: {
450
+ mode: 'strict',
451
+ typeSymbol: 'dsh-notify-yimit#EventIds',
452
+ schema: codecOf((v) => Array.isArray(v) ? v.map((x) => String(
453
+ x)) : [])
454
+ }
455
+ }],
456
+ result: {
457
+ mode: 'strict',
458
+ typeSymbol: 'dsh-notify-yimit#AckResult',
459
+ schema: ackCodec
460
+ }
461
+ }, {
462
+ id: 'dsh-notify-yimit#notify/listBrowsers',
463
+ service: 'notify',
464
+ namespace: 'notify',
465
+ method: 'listBrowsers',
466
+ invocation: {
467
+ kind: 'direct'
468
+ },
469
+ parameters: [],
470
+ result: {
471
+ mode: 'strict',
472
+ typeSymbol: 'dsh-notify-yimit#BrowserList',
473
+ schema: browsersCodec
474
+ }
475
+ }, {
476
+ id: 'dsh-notify-yimit#notify/startEditMode',
477
+ service: 'notify',
478
+ namespace: 'notify',
479
+ method: 'startEditMode',
480
+ invocation: {
481
+ kind: 'direct'
482
+ },
483
+ parameters: [],
484
+ result: {
485
+ mode: 'strict',
486
+ typeSymbol: 'dsh-notify-yimit#AckResult',
487
+ schema: ackCodec
488
+ }
489
+ }, {
490
+ id: 'dsh-notify-yimit#notify/endEditMode',
491
+ service: 'notify',
492
+ namespace: 'notify',
493
+ method: 'endEditMode',
494
+ invocation: {
495
+ kind: 'direct'
496
+ },
497
+ parameters: [],
498
+ result: {
499
+ mode: 'strict',
500
+ typeSymbol: 'dsh-notify-yimit#AckResult',
501
+ schema: ackCodec
502
+ }
503
+ }]
504
+ }
505
+
506
+ function makeStore(initial) {
507
+ let snapshot = initial
508
+ const listeners = new Set()
509
+ return {
510
+ getSnapshot: () => snapshot,
511
+ subscribe: (fn) => {
512
+ listeners.add(fn);
513
+ return () => {
514
+ listeners.delete(fn)
515
+ }
516
+ },
517
+ set: (next) => {
518
+ if (next === snapshot) return;
519
+ snapshot = next;
520
+ for (const fn of [...listeners]) fn()
521
+ },
522
+ }
523
+ }
524
+
525
+ function kindLabel(kind, t) {
526
+ switch (kind) {
527
+ case 'completed':
528
+ return t('kindCompleted')
529
+ case 'error':
530
+ return t('kindError')
531
+ case 'approval':
532
+ return t('kindApproval')
533
+ case 'question':
534
+ return t('kindQuestion')
535
+ default:
536
+ return t('kindRunning')
537
+ }
538
+ }
539
+
540
+ function NotifyOverlay(props) {
541
+ const notifyStore = props.useNotify ? props.useNotify((s) => s) : undefined
542
+ const api = props.api
543
+ const sessions = props.sessions
544
+ const seenRef = useRef(new Set())
545
+ const config = notifyStore?.config
546
+ const events = notifyStore?.events
547
+ const openSession = useCallback((sessionId) => {
548
+ try {
549
+ if (typeof sessions?.open === 'function') sessions.open(sessionId)
550
+ } catch {
551
+ /* 静默 */ }
552
+ }, [sessions])
553
+
554
+ useEffect(() => {
555
+ const cleanup = setInterval(() => {
556
+ if (seenRef.current.size > 1000) seenRef.current.clear()
557
+ }, 60000)
558
+ return () => clearInterval(cleanup)
559
+ }, [])
560
+
561
+ useEffect(() => {
562
+ if (!config || !Array.isArray(events)) return
563
+ const fresh = events.filter((e) => !seenRef.current.has(e.id))
564
+ if (fresh.length === 0) return
565
+ for (const e of fresh) seenRef.current.add(e.id)
566
+
567
+ const ack = () => {
568
+ try {
569
+ api?.ackEvents(fresh.map((e) => e.id))
570
+ } catch {
571
+ /* 静默 */ }
572
+ }
573
+
574
+ if (config.enabled !== true || config.mode === 'off') {
575
+ ack();
576
+ return
577
+ }
578
+
579
+ if (config.mode === 'custom') {
580
+ if (typeof Notification === 'undefined' || Notification.permission !==
581
+ 'granted') {
582
+ ack();
583
+ return
584
+ }
585
+ } else if (config.mode === 'system') {
586
+ if (typeof Notification === 'undefined' || Notification.permission ===
587
+ 'denied') {
588
+ ack();
589
+ return
590
+ }
591
+ }
592
+
593
+ const request = Notification.permission === 'granted' ? Promise.resolve('granted') :
594
+ Notification.requestPermission()
595
+ request.then((permission) => {
596
+ if (permission !== 'granted') {
597
+ ack();
598
+ return
599
+ }
600
+ for (const e of fresh) {
601
+ let n
602
+ try {
603
+ n = new Notification(e.title && e.title.length > 0 ? e.title : e
604
+ .sessionId, {
605
+ body: e.text,
606
+ tag: e.kind === 'running' ?
607
+ `${e.sessionId}:${e.kind}` :
608
+ `${e.sessionId}:${e.kind}:${e.at}`,
609
+ silent: false,
610
+ })
611
+ } catch {
612
+ continue
613
+ }
614
+ n.onclick = () => {
615
+ try {
616
+ window.focus();
617
+ openSession(e.sessionId);
618
+ n.close()
619
+ } catch {
620
+ /* 静默 */ }
621
+ }
622
+ }
623
+ ack()
624
+ }).catch(() => ack())
625
+ }, [events, config, api, openSession])
626
+
627
+ return null
628
+ }
629
+
630
+ function Segmented(props) {
631
+ const {
632
+ value,
633
+ options,
634
+ onChange,
635
+ disabled
636
+ } = props
637
+ return el('div', {
638
+ className: 'dn-seg',
639
+ role: 'tablist'
640
+ },
641
+ options.map((entry) => el('button', {
642
+ key: entry[0],
643
+ type: 'button',
644
+ role: 'tab',
645
+ 'aria-selected': String(value === entry[0]),
646
+ className: 'dn-seg-btn',
647
+ 'data-on': String(value === entry[0]),
648
+ disabled: disabled === true,
649
+ onClick: () => {
650
+ if (value !== entry[0]) onChange(entry[0])
651
+ },
652
+ }, entry[1])))
653
+ }
654
+
655
+ /** 跳转浏览器选择器:原生 select 的展开列表无法贴合主题,自绘下拉(触发器 + 弹层)。 */
656
+ function BrowserSelect(props) {
657
+ const {
658
+ value,
659
+ options,
660
+ disabled,
661
+ onChange,
662
+ t
663
+ } = props
664
+ const [open, setOpen] = useState(false)
665
+ const wrapRef = useRef(null)
666
+ // 点击外部 / Esc 关闭弹层。
667
+ useEffect(() => {
668
+ if (!open) return
669
+ const onDown = (e) => {
670
+ if (wrapRef.current !== null && !wrapRef.current.contains(e.target)) setOpen(false)
671
+ }
672
+ const onKey = (e) => {
673
+ if (e.key === 'Escape') setOpen(false)
674
+ }
675
+ document.addEventListener('mousedown', onDown)
676
+ document.addEventListener('keydown', onKey)
677
+ return () => {
678
+ document.removeEventListener('mousedown', onDown)
679
+ document.removeEventListener('keydown', onKey)
680
+ }
681
+ }, [open])
682
+
683
+ const current = options.find((b) => b.path === value)
684
+ const pick = (path) => {
685
+ onChange(path)
686
+ setOpen(false)
687
+ }
688
+ return el('div', {
689
+ className: 'dn-bselect',
690
+ ref: wrapRef
691
+ },
692
+ el('button', {
693
+ type: 'button',
694
+ className: 'dn-bselect-trigger' + (open ? ' open' : ''),
695
+ disabled: disabled === true,
696
+ 'aria-haspopup': 'listbox',
697
+ 'aria-expanded': String(open),
698
+ onClick: () => {
699
+ if (disabled !== true) setOpen(!open)
700
+ },
701
+ },
702
+ el('span', {
703
+ className: 'dn-bselect-value'
704
+ }, current !== undefined ? current.name : t('browserDefault')),
705
+ el('span', {
706
+ className: 'dn-bselect-arrow'
707
+ }, open ? '▴' : '▾')),
708
+ open ? el('div', {
709
+ className: 'dn-bselect-pop',
710
+ role: 'listbox'
711
+ },
712
+ el('button', {
713
+ type: 'button',
714
+ role: 'option',
715
+ 'aria-selected': String(value === ''),
716
+ className: 'dn-bselect-opt' + (value === '' ? ' sel' : ''),
717
+ onClick: () => pick(''),
718
+ }, t('browserDefault')),
719
+ options.map((b) => el('button', {
720
+ type: 'button',
721
+ role: 'option',
722
+ key: b.id,
723
+ 'aria-selected': String(value === b.path),
724
+ className: 'dn-bselect-opt' + (value === b.path ? ' sel' : ''),
725
+ onClick: () => pick(b.path),
726
+ }, b.name)))
727
+ : null)
728
+ }
729
+
730
+ const KIND_ORDER = ['completed', 'error', 'running', 'approval', 'question']
731
+
732
+ function NotifySection(props) {
733
+ const notifyStore = props.useNotify ? props.useNotify((s) => s) : undefined
734
+ const api = props.api
735
+ // 关键:订阅 locale 变化,语言切换时本组件立即重渲染
736
+ useSyncExternalStore(localeStore.subscribe, localeStore.getSnapshot)
737
+ const t = makeT()
738
+ const state = notifyStore
739
+ const config = state?.config
740
+ const [msg, setMsg] = useState(null)
741
+ const [saving, setSaving] = useState(false)
742
+ const [permission, setPermission] = useState(typeof Notification !== 'undefined' ? Notification
743
+ .permission : 'unsupported')
744
+ // 通知窗口编辑模式(样板窗拖拽/缩放)
745
+ const [editing, setEditing] = useState(false)
746
+ // 最新值镜像:卸载清理时读取(避免空依赖闭包拿到陈旧值)。
747
+ const editingRef = useRef(false)
748
+ editingRef.current = editing
749
+ useEffect(() => () => {
750
+ // 离开通知设置页(组件卸载)时若仍在编辑模式,自动关闭样板窗。
751
+ if (editingRef.current) void api.endEditMode()
752
+ }, [])
753
+
754
+ const saveTimerRef = useRef(null)
755
+ const save = useCallback((patch) => {
756
+ if (saveTimerRef.current) clearTimeout(saveTimerRef.current)
757
+ saveTimerRef.current = setTimeout(async () => {
758
+ if (!api?.updateConfig) return
759
+ setSaving(true);
760
+ setMsg(null)
761
+ try {
762
+ await api.updateConfig(patch)
763
+ setMsg({
764
+ kind: 'ok',
765
+ text: t('saved')
766
+ })
767
+ } catch (error) {
768
+ setMsg({
769
+ kind: 'err',
770
+ text: t('saveFailed', {
771
+ message: error?.message ?? String(error)
772
+ })
773
+ })
774
+ } finally {
775
+ setSaving(false)
776
+ }
777
+ }, 300)
778
+ }, [api, t])
779
+
780
+ const requestPerm = useCallback(async () => {
781
+ if (typeof Notification === 'undefined' || Notification.permission ===
782
+ 'granted') {
783
+ setPermission(typeof Notification !== 'undefined' ? Notification
784
+ .permission : 'unsupported')
785
+ return
786
+ }
787
+ try {
788
+ const result = await Notification.requestPermission();
789
+ setPermission(result)
790
+ } catch {
791
+ setPermission('denied')
792
+ }
793
+ }, [])
794
+
795
+ if (!config) return el('div', {
796
+ className: 'dn-root'
797
+ }, el('div', {
798
+ className: 'dn-note'
799
+ }, t('loading')))
800
+
801
+ const controlsDisabled = !config.enabled
802
+
803
+ const kindRows = KIND_ORDER.map((kind) => {
804
+ const entry = config.colors?. [kind] ?? {}
805
+ const bg = entry.bg || config.bgColor || '#20272f'
806
+ const fg = entry.fg || config.textColor || '#e6edf3'
807
+ const enabled = entry.enabled !== false
808
+ return el('div', {
809
+ key: kind,
810
+ className: 'dn-kind-row'
811
+ },
812
+ // 样式预览:类型文字显示在配置色的纯色块内(内联样式,不受深色模式影响)
813
+ el('span', {
814
+ className: 'dn-kind-preview',
815
+ style: {
816
+ background: bg,
817
+ color: fg
818
+ }
819
+ }, kindLabel(kind, t)),
820
+ el('label', {
821
+ className: 'dn-kind-field'
822
+ },
823
+ el('input', {
824
+ type: 'color',
825
+ className: 'dn-input',
826
+ title: t('colorBg'),
827
+ value: /^#[0-9a-fA-F]{6}$/.test(bg) ? bg : '#20272f',
828
+ disabled: controlsDisabled,
829
+ onChange: (e) => save({
830
+ colors: {
831
+ [kind]: {
832
+ ...entry,
833
+ bg: e.target.value
834
+ }
835
+ }
836
+ })
837
+ }), t('colorBg')),
838
+ el('label', {
839
+ className: 'dn-kind-field'
840
+ },
841
+ el('input', {
842
+ type: 'color',
843
+ className: 'dn-input',
844
+ title: t('colorFg'),
845
+ value: /^#[0-9a-fA-F]{6}$/.test(fg) ? fg : '#e6edf3',
846
+ disabled: controlsDisabled,
847
+ onChange: (e) => save({
848
+ colors: {
849
+ [kind]: {
850
+ ...entry,
851
+ fg: e.target.value
852
+ }
853
+ }
854
+ })
855
+ }), t('colorFg')),
856
+ // 每类型开关:控制该类型通知是否产生(默认全开)
857
+ el('label', {
858
+ className: 'dn-kind-field'
859
+ },
860
+ el('button', {
861
+ type: 'button',
862
+ role: 'switch',
863
+ 'aria-checked': enabled,
864
+ 'aria-label': kindLabel(kind, t),
865
+ className: 'dn-switch',
866
+ 'data-on': String(enabled),
867
+ disabled: controlsDisabled,
868
+ onClick: () => save({
869
+ colors: {
870
+ [kind]: {
871
+ ...entry,
872
+ enabled: !enabled
873
+ }
874
+ }
875
+ })
876
+ })))
877
+ })
878
+
879
+ const customOpen = config.mode === 'custom'
880
+
881
+ return el('div', {
882
+ className: 'dn-root'
883
+ },
884
+ el('div', {
885
+ className: 'dn-block'
886
+ },
887
+ el('div', {
888
+ className: 'dn-row'
889
+ },
890
+ el('button', {
891
+ type: 'button',
892
+ role: 'switch',
893
+ 'aria-checked': config.enabled,
894
+ className: 'dn-switch',
895
+ 'data-on': String(config.enabled),
896
+ onClick: () => save({
897
+ enabled: !config.enabled
898
+ })
899
+ }),
900
+ el('span', {
901
+ className: 'dn-label'
902
+ }, t('enabledLabel'))),
903
+ el('p', {
904
+ className: 'dn-note'
905
+ }, t('enabledNote'))),
906
+ el('div', {
907
+ className: 'dn-divider'
908
+ }),
909
+ el('div', {
910
+ className: 'dn-block'
911
+ },
912
+ el('p', {
913
+ className: 'dn-title'
914
+ }, t('modeLabel')),
915
+ el(Segmented, {
916
+ value: config.mode,
917
+ disabled: controlsDisabled,
918
+ onChange: (mode) => save({
919
+ mode
920
+ }),
921
+ options: [
922
+ ['system', t('modeSystem')],
923
+ ['custom', t('modeCustom')],
924
+ ['off', t('modeOff')]
925
+ ]
926
+ }),
927
+ el('p', {
928
+ className: 'dn-note'
929
+ }, t('modeNote'))),
930
+ (config.mode === 'system' || config.mode === 'custom') ? el('div', {
931
+ className: 'dn-block'
932
+ },
933
+ el('div', {
934
+ className: 'dn-row'
935
+ },
936
+ el('span', {
937
+ className: 'dn-label'
938
+ }, t('browserLabel')),
939
+ el(BrowserSelect, {
940
+ value: config.browser || '',
941
+ options: state?.browsers ?? [],
942
+ disabled: controlsDisabled,
943
+ onChange: (path) => save({
944
+ browser: path
945
+ }),
946
+ t
947
+ })),
948
+ el('p', {
949
+ className: 'dn-note'
950
+ }, t('browserNote'))
951
+ ) : null,
952
+ el('div', {
953
+ className: 'dn-divider'
954
+ }),
955
+ config.mode === 'system' ? el('div', {
956
+ className: 'dn-block'
957
+ },
958
+ el('p', {
959
+ className: 'dn-title'
960
+ }, t('systemTitle')),
961
+ permission === 'granted' ? el('div', {
962
+ className: 'dn-msg ok'
963
+ }, t('permissionGranted')) :
964
+ permission === 'denied' ? el('div', {
965
+ className: 'dn-msg err'
966
+ }, t('permissionDenied')) :
967
+ el(Fragment, null,
968
+ el('p', {
969
+ className: 'dn-note'
970
+ }, t('permissionDefault')),
971
+ el('button', {
972
+ className: 'dn-btn primary',
973
+ disabled: controlsDisabled,
974
+ onClick: requestPerm
975
+ }, t('requestPermission')))
976
+ ) : null,
977
+ el('div', {
978
+ className: 'dn-collapse',
979
+ 'data-open': String(customOpen)
980
+ },
981
+ el('div', {
982
+ className: 'dn-collapse-body'
983
+ },
984
+ el('div', {
985
+ className: 'dn-block'
986
+ },
987
+ el('p', {
988
+ className: 'dn-title'
989
+ }, t('customTitle')),
990
+ el('div', {
991
+ className: 'dn-row'
992
+ },
993
+ el('span', {
994
+ className: 'dn-label'
995
+ }, t('maxVisibleLabel')),
996
+ el('input', {
997
+ type: 'number',
998
+ min: 1,
999
+ max: 20,
1000
+ className: 'dn-input',
1001
+ value: config.maxVisible,
1002
+ disabled: controlsDisabled,
1003
+ onChange: (e) => {
1004
+ const n = Math.max(1, Math.min(20, Math.floor(Number(e
1005
+ .target.value) || 1)))
1006
+ save({
1007
+ maxVisible: n
1008
+ })
1009
+ }
1010
+ })),
1011
+ el('div', {
1012
+ className: 'dn-row'
1013
+ },
1014
+ el('span', {
1015
+ className: 'dn-label'
1016
+ }, t('durationLabel')),
1017
+ el('input', {
1018
+ type: 'number',
1019
+ min: 1,
1020
+ max: 300,
1021
+ className: 'dn-input',
1022
+ value: Math.round(config.durationMs / 1000),
1023
+ disabled: controlsDisabled,
1024
+ onChange: (e) => {
1025
+ const s = Math.max(1, Math.min(300, Math.floor(Number(e
1026
+ .target.value) || 1)))
1027
+ save({
1028
+ durationMs: s * 1000
1029
+ })
1030
+ }
1031
+ })),
1032
+ el('p', {
1033
+ className: 'dn-title'
1034
+ }, t('colorsTitle')),
1035
+ el('div', {
1036
+ className: 'dn-kind-list'
1037
+ }, kindRows),
1038
+ el('div', {
1039
+ className: 'dn-divider'
1040
+ }),
1041
+ el('p', {
1042
+ className: 'dn-title'
1043
+ }, t('toastStyleTitle')),
1044
+ el('div', {
1045
+ className: 'dn-row'
1046
+ },
1047
+ el('span', {
1048
+ className: 'dn-label'
1049
+ }, t('toastGapLabel')),
1050
+ el('input', {
1051
+ type: 'number',
1052
+ min: 0,
1053
+ max: 60,
1054
+ className: 'dn-input',
1055
+ value: config.toast?.gap ?? 12,
1056
+ disabled: controlsDisabled,
1057
+ onChange: (e) => {
1058
+ const g = Math.max(0, Math.min(60, Math.floor(Number(e
1059
+ .target.value) || 0)))
1060
+ save({
1061
+ toast: {
1062
+ ...(config.toast ?? {}),
1063
+ gap: g
1064
+ }
1065
+ })
1066
+ }
1067
+ })),
1068
+ el('div', {
1069
+ className: 'dn-row'
1070
+ },
1071
+ el('button', {
1072
+ className: 'dn-btn primary',
1073
+ disabled: controlsDisabled,
1074
+ onClick: () => {
1075
+ if (editing) {
1076
+ void api.endEditMode()
1077
+ setEditing(false)
1078
+ } else {
1079
+ setEditing(true)
1080
+ void api.startEditMode()
1081
+ }
1082
+ }
1083
+ }, editing ? t('endEdit') : t('enterEdit')),
1084
+ el('button', {
1085
+ className: 'dn-btn',
1086
+ disabled: controlsDisabled || !config.toast?.position,
1087
+ onClick: () => {
1088
+ save({
1089
+ toast: {
1090
+ ...(config.toast ?? {}),
1091
+ position: null
1092
+ }
1093
+ })
1094
+ // 编辑模式下:重开样板窗,让其在默认位置(右下)刷新显示。
1095
+ if (editing) void api.startEditMode()
1096
+ }
1097
+ }, t('resetPosition'))),
1098
+ el('p', {
1099
+ className: 'dn-note'
1100
+ }, t('editHint')))
1101
+ )),
1102
+ el('div', {
1103
+ className: 'dn-divider'
1104
+ }),
1105
+ el('p', {
1106
+ className: 'dn-note'
1107
+ }, t('triggerNote')),
1108
+ el('p', {
1109
+ className: 'dn-note'
1110
+ }, t('saveHint')),
1111
+ msg !== null ? el('div', {
1112
+ className: 'dn-msg ' + msg.kind
1113
+ }, msg.text) : null,
1114
+ saving ? el('div', {
1115
+ className: 'dn-note'
1116
+ }, '…') : null,
1117
+ )
1118
+ }
1119
+
1120
+ // ===== 修改点:inject 数组改为 ['remote', 'locale'] =====
1121
+ const inject = ['remote', 'locale']
1122
+
1123
+ async function apply(ctx) {
1124
+ const remote = ctx.remote
1125
+ if (remote === undefined || typeof remote.$mount !== 'function') return
1126
+ const unmount = await remote.$mount(CONTRIBUTION)
1127
+ ctx.effect(() => () => {
1128
+ unmount()
1129
+ }, 'dsh-notify-yimit: remote contribution')
1130
+
1131
+ const notify = ctx.get('remote.notify')
1132
+ if (notify === undefined) return
1133
+
1134
+ const store = makeStore({
1135
+ config: null,
1136
+ events: [],
1137
+ browsers: []
1138
+ })
1139
+
1140
+ // 定义 loadBrowsers 和 reload(不依赖 api)
1141
+ const loadBrowsers = async () => {
1142
+ try {
1143
+ const result = await notify.listBrowsers()
1144
+ if (result !== null && typeof result === 'object' && result.ok === true) {
1145
+ const parsed = browsersCodec.parse(result.value)
1146
+ const state = store.getSnapshot()
1147
+ store.set({
1148
+ config: state.config,
1149
+ events: state.events,
1150
+ browsers: parsed.browsers
1151
+ })
1152
+ }
1153
+ } catch {
1154
+ /* 探测失败 */ }
1155
+ }
1156
+
1157
+ let reloading = false
1158
+ const reload = async () => {
1159
+ if (reloading) return
1160
+ reloading = true
1161
+ try {
1162
+ const result = await notify.getState()
1163
+ if (result !== null && typeof result === 'object' && result.ok === true) {
1164
+ const prev = store.getSnapshot()
1165
+ store.set({
1166
+ ...parseState(result.value, 'state'),
1167
+ browsers: prev.browsers
1168
+ })
1169
+ }
1170
+ } catch {
1171
+ /* 传输失败 */ } finally {
1172
+ reloading = false
1173
+ }
1174
+ }
1175
+
1176
+ // ===== 先定义 api(包含 updateConfig),供后续语言同步使用 =====
1177
+ const api = {
1178
+ reload,
1179
+ updateConfig: async (patch) => {
1180
+ const result = await notify.updateConfig(patch)
1181
+ if (result !== null && typeof result === 'object' && result.ok === true) {
1182
+ const state = store.getSnapshot()
1183
+ store.set({
1184
+ config: parseConfig(result.value, 'config'),
1185
+ events: state.events,
1186
+ browsers: state.browsers
1187
+ })
1188
+ }
1189
+ return result?.value
1190
+ },
1191
+ ackEvents: async (ids) => {
1192
+ try {
1193
+ await notify.ackEvents(ids)
1194
+ } catch {
1195
+ /* 静默 */ }
1196
+ },
1197
+ startEditMode: async () => {
1198
+ try {
1199
+ await notify.startEditMode()
1200
+ } catch {
1201
+ /* 静默 */ }
1202
+ },
1203
+ endEditMode: async () => {
1204
+ try {
1205
+ await notify.endEditMode()
1206
+ } catch {
1207
+ /* 静默 */ }
1208
+ },
1209
+ }
1210
+
1211
+ // ===== 现在设置 __localeService 并使用 api =====
1212
+ __localeService = ctx.locale
1213
+
1214
+ if (__localeService && typeof __localeService.register === 'function') {
1215
+ ctx.effect(() => __localeService.register(NS, {
1216
+ zh,
1217
+ en
1218
+ }), 'dsh-notify-yimit: dictionaries')
1219
+ }
1220
+
1221
+ if (__localeService && typeof __localeService.subscribe === 'function') {
1222
+ const syncToHost = () => {
1223
+ const newLang = currentLangId()
1224
+ if (api && typeof api.updateConfig === 'function') {
1225
+ void api.updateConfig({
1226
+ locale: newLang
1227
+ })
1228
+ }
1229
+ }
1230
+ const unsub = __localeService.subscribe(syncToHost)
1231
+ ctx.effect(() => unsub, 'dsh-notify-yimit: locale sync')
1232
+ void syncToHost()
1233
+ }
1234
+
1235
+ // 初始加载配置和浏览器列表
1236
+ void reload()
1237
+ void loadBrowsers()
1238
+
1239
+ ctx.effect(() => ctx.on('connection/reset', () => {
1240
+ void reload();
1241
+ void loadBrowsers()
1242
+ }), 'dsh-notify-yimit: reconnect reload')
1243
+
1244
+ const pollTimer = setInterval(() => {
1245
+ if (document.visibilityState === 'hidden') return
1246
+ void reload()
1247
+ }, 250)
1248
+ ctx.effect(() => () => {
1249
+ clearInterval(pollTimer)
1250
+ }, 'dsh-notify-yimit: poll timer')
1251
+
1252
+ const onVisible = () => {
1253
+ if (document.visibilityState === 'visible') void reload()
1254
+ }
1255
+ document.addEventListener('visibilitychange', onVisible)
1256
+ ctx.effect(() => () => {
1257
+ document.removeEventListener('visibilitychange', onVisible)
1258
+ }, 'dsh-notify-yimit: visibility reload')
1259
+
1260
+ const sessions = ctx.get('sessions')
1261
+ const slots = ctx.get('slots')
1262
+ if (slots === undefined) return
1263
+
1264
+ const openFromHash = () => {
1265
+ try {
1266
+ const m = /#dsh-notify-yimit\/session=([^&]+)/.exec(window.location.hash)
1267
+ if (m !== null && typeof sessions?.open === 'function') {
1268
+ sessions.open(decodeURIComponent(m[1]))
1269
+ const clean = window.location.href.split('#')[0]
1270
+ window.history.replaceState(null, '', clean)
1271
+ }
1272
+ } catch {
1273
+ /* 静默 */ }
1274
+ }
1275
+ window.addEventListener('hashchange', openFromHash)
1276
+ openFromHash()
1277
+ ctx.effect(() => () => window.removeEventListener('hashchange', openFromHash),
1278
+ 'dsh-notify-yimit: hash listener')
1279
+
1280
+ const injected = () => ({
1281
+ hooks: {
1282
+ notify: store
1283
+ },
1284
+ api,
1285
+ sessions
1286
+ })
1287
+ const sectionInjected = () => ({
1288
+ hooks: {
1289
+ notify: store
1290
+ },
1291
+ api
1292
+ })
1293
+
1294
+ const sectionActive = {
1295
+ gen: 0,
1296
+ dispose: null
1297
+ }
1298
+ const registerSection = () => {
1299
+ if (sectionActive.dispose !== null) {
1300
+ sectionActive.dispose();
1301
+ sectionActive.dispose = null
1302
+ }
1303
+ sectionActive.gen += 1
1304
+ const gen = sectionActive.gen
1305
+ slots.inject('settings.section', () => {
1306
+ if (sectionActive.gen !== gen) return
1307
+ const dispose = slots.register({
1308
+ name: 'settings.section',
1309
+ id: 'dsh-notify-yimit',
1310
+ order: 60,
1311
+ label: () => makeT()('sectionLabel'),
1312
+ locale: NS,
1313
+ inject: sectionInjected
1314
+ }, NotifySection)
1315
+ if (sectionActive.gen !== gen) {
1316
+ dispose();
1317
+ return
1318
+ }
1319
+ sectionActive.dispose = dispose
1320
+ return () => {
1321
+ if (sectionActive.dispose === dispose) sectionActive.dispose =
1322
+ null
1323
+ dispose()
1324
+ }
1325
+ })
1326
+ }
1327
+ registerSection()
1328
+
1329
+ const overlayActive = {
1330
+ gen: 0,
1331
+ dispose: null
1332
+ }
1333
+ const registerOverlay = () => {
1334
+ if (overlayActive.dispose !== null) {
1335
+ overlayActive.dispose();
1336
+ overlayActive.dispose = null
1337
+ }
1338
+ overlayActive.gen += 1
1339
+ const gen = overlayActive.gen
1340
+ slots.inject('shell.overlay', () => {
1341
+ if (overlayActive.gen !== gen) return
1342
+ const dispose = slots.register({
1343
+ name: 'shell.overlay',
1344
+ id: 'dsh-notify-yimit',
1345
+ order: 10,
1346
+ inject: injected
1347
+ }, NotifyOverlay)
1348
+ if (overlayActive.gen !== gen) {
1349
+ dispose();
1350
+ return
1351
+ }
1352
+ overlayActive.dispose = dispose
1353
+ return () => {
1354
+ if (overlayActive.dispose === dispose) overlayActive.dispose =
1355
+ null
1356
+ dispose()
1357
+ }
1358
+ })
1359
+ }
1360
+ registerOverlay()
1361
+
1362
+ return () => {}
1363
+ }
1364
+
1365
+ exports.apply = apply
1366
+ exports.inject = inject
1367
+ return module.exports
1368
+ },
1369
+ })