@dsh-plus/notify-email 0.1.8 → 0.1.10

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.
@@ -9,12 +9,22 @@ import type { Context } from '@deepseek-ai/cordis'
9
9
  import type { NotifyEmailService } from '../service.ts'
10
10
 
11
11
  export function installDecisionWatcher(ctx: Context, service: NotifyEmailService): void {
12
- const seen = new Set<string>()
12
+ // 按 sessionId 分桶去重:callId 粒度的全局集合会随每个工具调用无限增长;
13
+ // 会话销毁即整桶释放。无 agent 归属的调用落到 unknown 桶(无生命周期可挂),
14
+ // 以有界环形覆盖兜底,保证任何路径都不产生无界内存。
15
+ const seen = new Map<string, Set<string>>()
16
+ const UNKNOWN_BUCKET_CAP = 1000
13
17
  const logger = ctx.logger('notify-email')
14
18
  ctx.on('tools/pre-execute', (exec, next) => {
15
- if (seen.has(exec.callId)) return next()
16
- seen.add(exec.callId)
17
19
  const sessionId = exec.agent?.id ?? 'unknown'
20
+ let bucket = seen.get(sessionId)
21
+ if (bucket === undefined) {
22
+ bucket = new Set()
23
+ seen.set(sessionId, bucket)
24
+ }
25
+ if (bucket.has(exec.callId)) return next()
26
+ if (sessionId === 'unknown' && bucket.size >= UNKNOWN_BUCKET_CAP) bucket.clear()
27
+ bucket.add(exec.callId)
18
28
  service
19
29
  .dispatchDecision({
20
30
  callId: exec.callId,
@@ -30,4 +40,7 @@ export function installDecisionWatcher(ctx: Context, service: NotifyEmailService
30
40
  })
31
41
  return next()
32
42
  })
43
+ ctx.on('session/disposed', (session) => {
44
+ seen.delete(session.id)
45
+ })
33
46
  }
@@ -1,75 +0,0 @@
1
- /**
2
- * 配置卡片的基础控件:文本/数字/密码字段与勾选行。
3
- * 视觉对齐官方卡片字段(label + hint + input 纵列)。
4
- * @module notify-email/client/fields
5
- */
6
- import type { ReactElement } from 'react'
7
-
8
- export interface TextFieldProps {
9
- id: string
10
- label: string
11
- hint?: string
12
- value: string
13
- disabled?: boolean
14
- invalid?: boolean
15
- invalidLabel?: string
16
- numeric?: boolean
17
- password?: boolean
18
- badge?: { text: string; set: boolean }
19
- onEdit(text: string): void
20
- }
21
-
22
- export function TextField(props: TextFieldProps): ReactElement {
23
- const invalid = props.invalid === true
24
- return (
25
- <div className="dne-field">
26
- <div className="dne-head">
27
- <label className="dne-label" htmlFor={props.id}>
28
- {props.label}
29
- </label>
30
- {props.badge !== undefined ? (
31
- <span className={`dne-badge ${props.badge.set ? 'dne-badgeSet' : 'dne-badgeUnset'}`}>
32
- {props.badge.text}
33
- </span>
34
- ) : null}
35
- </div>
36
- <input
37
- id={props.id}
38
- className={`dne-input${invalid ? ' dne-inputInvalid' : ''}`}
39
- type={props.password === true ? 'password' : 'text'}
40
- inputMode={props.numeric === true ? 'numeric' : undefined}
41
- autoComplete={props.password === true ? 'off' : undefined}
42
- aria-invalid={invalid || undefined}
43
- value={props.value}
44
- disabled={props.disabled === true}
45
- onChange={(event) => props.onEdit(event.target.value)}
46
- />
47
- <p className={invalid ? 'dne-invalid' : 'dne-hint'}>
48
- {invalid ? (props.invalidLabel ?? '') : (props.hint ?? '')}
49
- </p>
50
- </div>
51
- )
52
- }
53
-
54
- export interface CheckRowProps {
55
- id: string
56
- label: string
57
- checked: boolean
58
- disabled?: boolean
59
- onEdit(checked: boolean): void
60
- }
61
-
62
- export function CheckRow(props: CheckRowProps): ReactElement {
63
- return (
64
- <div className="dne-checkRow">
65
- <input
66
- id={props.id}
67
- type="checkbox"
68
- checked={props.checked}
69
- disabled={props.disabled === true}
70
- onChange={(event) => props.onEdit(event.target.checked)}
71
- />
72
- <label htmlFor={props.id}>{props.label}</label>
73
- </div>
74
- )
75
- }
@@ -1,59 +0,0 @@
1
- /**
2
- * 浏览器半 settingsScope/connection 的最小本地接口声明。
3
- * 只声明本插件用到的窄面,避免为构建期类型引入整条官方 client 依赖树;
4
- * 运行时契约以官方 dsh-client-connection 的 settings RPC 面为准
5
- * (rc7:白名单已移除,任何已注册命名空间均可 describe/update/replace)。
6
- *
7
- * 实现说明:本插件不复用 settingsScope 服务的 bind()——它在非 loopback
8
- * 页面(如经 tailnet 域名访问)下走 memory 模式、永远无数据;这里按官方
9
- * 配置页 tab 的同款模式用 `api.settings` 直连实现 Scope(describe 读 +
10
- * remote `settings/document-updated` / `connection/reset` 刷新,见 client.ts)。
11
- * @module notify-email/client/scope
12
- */
13
-
14
- /** settings 命名空间 scope 的快照(官方 SettingsScopeSnapshot 的最小投影)。 */
15
- export interface ScopeSnapshot {
16
- status: 'loading' | 'ready' | 'unavailable'
17
- /** schema 解析后的配置值(secret 字段已脱敏剥除)。 */
18
- value: unknown
19
- /** 命名空间 revision,写操作 fencing 用;首次 Host 应答前为 undefined。 */
20
- revision: number | undefined
21
- /** Host 文档是否可写。 */
22
- writable: boolean
23
- }
24
-
25
- /** settingsScope.bind({namespace}) 返回的控制器面(本插件由 api 直连实现)。 */
26
- export interface Scope {
27
- getSnapshot(): ScopeSnapshot
28
- subscribe(listener: () => void): () => void
29
- load(): Promise<void>
30
- }
31
-
32
- /** connection api.settings 的 RPC 面(本插件用到的三个方法)。 */
33
- export interface SettingsApi {
34
- describe(payload?: Record<string, never>): Promise<{
35
- result: {
36
- ok: boolean
37
- value?: {
38
- writable: boolean
39
- namespaces: Array<{
40
- ns: string
41
- value: unknown
42
- revision: number
43
- secrets: Array<{ path: string[]; set: boolean }>
44
- }>
45
- }
46
- error?: { message?: string }
47
- }
48
- }>
49
- update(request: {
50
- ns: string
51
- patch: Record<string, unknown>
52
- expectedRevision?: number
53
- }): Promise<{ result: { ok: boolean; error?: { message?: string } } }>
54
- replace(request: {
55
- ns: string
56
- section: Record<string, unknown>
57
- expectedRevision?: number
58
- }): Promise<{ result: { ok: boolean; error?: { message?: string } } }>
59
- }
@@ -1,64 +0,0 @@
1
- /**
2
- * 配置卡片样式:沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载),
3
- * 视觉对齐官方卡片(--dsw-alias-* 变量),不覆盖上游任何选择器。
4
- * @module notify-email/client/styles
5
- */
6
-
7
- export const PLUGIN_ID = '@dsh-plus/notify-email'
8
- export const STYLE_TAG_ID = `${PLUGIN_ID}/card.css`
9
-
10
- export const cardCss = `
11
- .dne-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}
12
- .dne-card:hover{border-color:var(--dsw-alias-label-dimmed)}
13
- .dne-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}
14
- .dne-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}
15
- .dne-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
16
- .dne-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
17
- .dne-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}
18
- .dne-description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
19
- .dne-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s;font-size:12px}
20
- .dne-chevronOpen{transform:rotate(180deg)}
21
- .dne-pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
22
- .dne-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}
23
- .dne-field{flex-direction:column;gap:6px;padding:12px 0;display:flex}
24
- .dne-field+.dne-field{border-top:1px solid var(--dsw-alias-border-l2)}
25
- .dne-head{align-items:center;gap:8px;display:flex}
26
- .dne-label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}
27
- .dne-badge{white-space:nowrap;border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
28
- .dne-badgeSet{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
29
- .dne-badgeUnset{color:var(--dsw-alias-label-tertiary)}
30
- .dne-input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px}
31
- .dne-input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}
32
- .dne-input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
33
- .dne-inputInvalid{border-color:var(--dsw-alias-label-error)}
34
- .dne-hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}
35
- .dne-invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}
36
- .dne-checkRow{align-items:center;gap:8px;display:flex;padding:6px 0}
37
- .dne-checkRow input{accent-color:var(--dsw-alias-brand-primary)}
38
- .dne-checkRow label{color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5;cursor:pointer}
39
- .dne-groupLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;padding:12px 0 2px;margin:0}
40
- .dne-readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}
41
- .dne-footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex;flex-wrap:wrap}
42
- .dne-status{min-width:0;color:var(--dsw-alias-label-secondary);flex:1;margin:0;font-size:12px;line-height:1.5}
43
- .dne-statusError{color:var(--dsw-alias-label-error)}
44
- .dne-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}
45
- .dne-btnGhost{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}
46
- .dne-btnGhost:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}
47
- .dne-btnPrimary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}
48
- .dne-btn:disabled{opacity:.4;cursor:default}
49
- .dne-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
50
- `
51
-
52
- /** 幂等注入样式标签;返回标签(已存在或环境无 document 时为 null)。 */
53
- export function injectStyle(): HTMLStyleElement | null {
54
- if (typeof document === 'undefined') return null
55
- if (document.querySelector(`style[data-plugin-css=${JSON.stringify(STYLE_TAG_ID)}]`) !== null) {
56
- return null
57
- }
58
- const tag = document.createElement('style')
59
- tag.dataset.plugin = PLUGIN_ID
60
- tag.dataset.pluginCss = STYLE_TAG_ID
61
- tag.textContent = cardCss
62
- document.head.appendChild(tag)
63
- return tag
64
- }