@nolit/dsh-notify 0.0.0-stage → 0.0.1-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +388 -0
- package/LICENSE +21 -0
- package/PROVENANCE.md +96 -0
- package/README.md +298 -2
- package/cordis.patch.yml +19 -0
- package/docs/2026-10-20-dsh-notify-design.md +335 -0
- package/docs/2026-10-20-final-status.md +125 -0
- package/docs/2026-10-23-asset-policy.md +170 -0
- package/docs/2026-10-23-deployment-strategy-review.md +530 -0
- package/docs/2026-10-23-headless-nonintrusive-ui-architecture.md +579 -0
- package/docs/2026-10-23-hybrid-dual-mode-design.md +955 -0
- package/docs/2026-10-23-timezone-global-report.md +204 -0
- package/docs/2026-10-23-web-notify-plugin-spec.md +454 -0
- package/docs/2026-10-23-web-plugin-mount.md +111 -0
- package/docs/2026-10-23-windows-widget-poc-report.md +312 -0
- package/docs/2026-10-24-linux-desktop-verification.md +134 -0
- package/docs/BOOT-CARD.md +176 -0
- package/docs/RECOVERY-NOTES.md +111 -0
- package/docs/WORK-PROTOCOL.md +155 -0
- package/docs/deepseek-peak-spec.md +370 -0
- package/docs/dsh-harness-notify-spec.md +149 -0
- package/icon.svg +6 -0
- package/lib/client.js +2172 -0
- package/lib/client.src.js +1963 -0
- package/lib/index.js +13 -0
- package/lib/locale-table.js +215 -0
- package/locale/en.json +104 -0
- package/locale/ko.json +104 -0
- package/market/icons/dsh-notify.svg +6 -0
- package/market/index.json +77 -0
- package/package.json +80 -4
- package/scripts/build-client.mjs +41 -0
- package/scripts/gen-locale.mjs +36 -0
- package/test/client.test.mjs +601 -0
- package/test/locale.test.mjs +114 -0
- package/test/manifest.test.mjs +115 -0
- package/test/peak.test.mjs +333 -0
- package/test/typography.test.mjs +85 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# 웹 알림 플러그인 실운영 마운트 보고서
|
|
2
|
+
|
|
3
|
+
<!-- skeleton:
|
|
4
|
+
version: 1.0
|
|
5
|
+
kind: report
|
|
6
|
+
status: historical
|
|
7
|
+
updated: 2026-10-23
|
|
8
|
+
summary: Real-mount report for the web notification plugin: how it was mounted and what the live boot proved.
|
|
9
|
+
-->
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
> 상태: active · 최종 갱신: 2026-10-23 · git: main · 상위: [dsh-notify README](../README.md)
|
|
13
|
+
|
|
14
|
+
- 실제 프로필: `C:\Users\nolit\.dsh\profiles\web` (`DSH_PROFILE=web`)
|
|
15
|
+
- 플러그인: `@nolit/dsh-notify` (`plugin/`, 로컬 링크)
|
|
16
|
+
- 마운트 완료: 2026-10-23 06:0x KST
|
|
17
|
+
|
|
18
|
+
## 인덱스
|
|
19
|
+
|
|
20
|
+
| § | 섹션 | 한 줄 요약 |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| 1 | 등록 | web 프로필 dependency + bundle + loader entry |
|
|
23
|
+
| 2 | 런타임 | Plugin Manager에서 활성화, Settings → 알림 표시 |
|
|
24
|
+
| 3 | 비침습 | 사이드바 DOM 추가 없음, Settings/Overlay만 사용 |
|
|
25
|
+
| 4 | 실측 | 권한 granted, 테스트 알림 전송 |
|
|
26
|
+
| 5 | 재현·복구 | 설치/활성화/확인 명령 |
|
|
27
|
+
|
|
28
|
+
## 1. 프로필 등록
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
C:\Users\nolit\.dsh\profiles\web\package.json
|
|
32
|
+
dependencies["@nolit/dsh-notify"] = "link:C:/Users/nolit/Workspace/projects/DSH Notify/plugin"
|
|
33
|
+
dsh.profile.bundles += "@nolit/dsh-notify"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`node_modules\@nolit\dsh-notify-web`은 체크아웃 `plugin\`을 가리키는 Junction이다.
|
|
37
|
+
`dsh --dump-config --profile web`에 다음 entry가 포함된다.
|
|
38
|
+
|
|
39
|
+
```yaml
|
|
40
|
+
- id: dsh-notify-web
|
|
41
|
+
name: '@nolit/dsh-notify'
|
|
42
|
+
config:
|
|
43
|
+
notifyOnComplete: true
|
|
44
|
+
notifyOnApproval: true
|
|
45
|
+
notifyOnQuestion: true
|
|
46
|
+
notifyOnError: true
|
|
47
|
+
completeCooldownMs: 5000
|
|
48
|
+
soundEnabled: true
|
|
49
|
+
soundSource: synth
|
|
50
|
+
volume: 0.3
|
|
51
|
+
suppressWhenFocused: false
|
|
52
|
+
requireInteractionForApproval: true
|
|
53
|
+
notifyOnSubagents: false
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
> 요청 표기의 `@local/dsh-notify-web`은 개념적 이름이고, 실제 패키지 ID는 `@nolit/dsh-notify`이다.
|
|
57
|
+
> DSH 프로필에는 실제 package.json name으로 등록해야 한다.
|
|
58
|
+
|
|
59
|
+
## 2. 런타임 마운트
|
|
60
|
+
|
|
61
|
+
DSH Plugins 페이지에서 `DSH 알림 (웹)` 항목의 토글을 **On**으로 전환했다.
|
|
62
|
+
이후 페이지를 새로고침하자 Settings 모달에 `알림` 탭이 나타났다.
|
|
63
|
+
|
|
64
|
+
- Settings → 알림
|
|
65
|
+
- `DSH 알림`
|
|
66
|
+
- `알림 켜짐`
|
|
67
|
+
- `브라우저 알림 켜기`
|
|
68
|
+
- `소리 켜기`
|
|
69
|
+
- `테스트 알림`
|
|
70
|
+
- 알림 종류: 턴 완료 / 도구 승인 필요 / 질문 대기 / 오류 / 서브에이전트 / 포커스 숨김
|
|
71
|
+
- 소리: 알림 소리 사용 / 소리 소스
|
|
72
|
+
- `권한: granted`
|
|
73
|
+
|
|
74
|
+
## 3. 비침습 확인
|
|
75
|
+
|
|
76
|
+
- 플러그인은 `settings.section`과 `shell.overlay`만 등록한다.
|
|
77
|
+
- 사이드바 footer/header에 새 DOM을 추가하지 않는다.
|
|
78
|
+
- 실제 화면에서도 기존 사이드바 항목(새 세션, 플러그인, 자동화 작업, 워크스페이스 관리자, 세션 검색, 계정 메뉴)은 그대로였다.
|
|
79
|
+
- `sidebar.footer.action`/`conversation.header`를 사용하지 않는 구현 계약을 유지한다.
|
|
80
|
+
|
|
81
|
+
## 4. 실측
|
|
82
|
+
|
|
83
|
+
| 항목 | 결과 |
|
|
84
|
+
|---|---|
|
|
85
|
+
| 프로필 dependency | `link:` Junction 확인 |
|
|
86
|
+
| 번들 목록 | `@nolit/dsh-notify` 포함 |
|
|
87
|
+
| `dsh --dump-config` | `dsh-notify-web` entry + config 확인 |
|
|
88
|
+
| Plugins 페이지 | `DSH 알림 (웹)` toggle **On** |
|
|
89
|
+
| Settings | `알림` 탭 표시 |
|
|
90
|
+
| 브라우저 권한 | `granted` |
|
|
91
|
+
| 테스트 알림 | `테스트 알림을 보냈습니다.` |
|
|
92
|
+
| 사이드바 변경 | 없음 |
|
|
93
|
+
|
|
94
|
+
## 5. 재현·복구
|
|
95
|
+
|
|
96
|
+
```powershell
|
|
97
|
+
dsh plugin --profile web add "link:C:/Users/nolit/Workspace/projects/DSH Notify/plugin"
|
|
98
|
+
dsh --dump-config --profile web
|
|
99
|
+
# DSH Plugins 페이지에서 DSH 알림 (웹) 토글 On
|
|
100
|
+
# Settings → 알림에서 권한/소리/테스트 확인
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
롤백:
|
|
104
|
+
|
|
105
|
+
```powershell
|
|
106
|
+
dsh plugin --profile web remove "@nolit/dsh-notify"
|
|
107
|
+
# 또는 Plugins 페이지에서 토글 Off
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
백업은 `C:\Users\nolit\.dsh\profiles\web\package.json.bak-notify-*` 및
|
|
111
|
+
`cordis.patch.yml.bak-notify-*`에 남겨 두었다.
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# 윈도우 오프피크 위젯 — Phase 2 PoC 실측 보고서
|
|
2
|
+
|
|
3
|
+
<!-- skeleton:
|
|
4
|
+
version: 1.0
|
|
5
|
+
kind: report
|
|
6
|
+
status: historical
|
|
7
|
+
updated: 2026-10-23
|
|
8
|
+
summary: Windows off-peak widget Phase-2 proof of concept: what was measured and why the track was not carried forward.
|
|
9
|
+
-->
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
> 상태: proposed · 최종 갱신: 2026-10-23 · git: main @ 8fb5b58 · 상위: [README.md](../README.md)
|
|
13
|
+
|
|
14
|
+
- 범위: 모듈 A(윈도우 위젯)의 PowerShell 7 기준선, WPF Drag & Drop 폰트 주입, UIPI, `start.cmd` 포터블 실행.
|
|
15
|
+
- 이번 단계에서는 **제품 코드를 수정하지 않았다.** PoC는 `%TEMP%`에서 실행 후 삭제했다.
|
|
16
|
+
- 5.1 다운그레이드는 기각한다. 기준선은 **PowerShell 7+ (`pwsh.exe`)** 이다.
|
|
17
|
+
|
|
18
|
+
## 인덱스
|
|
19
|
+
|
|
20
|
+
| § | 섹션 | 한 줄 요약 |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| 1 | 결론 | WPF 드롭/폰트 리바인딩은 PoC 통과. 실제 Explorer 드래그 1회 수동 확인만 남음 |
|
|
23
|
+
| 2 | 기준선 | pwsh 7.6.7, 5.1 포팅 폐기 |
|
|
24
|
+
| 3 | WPF D&D | AllowDrop·FileDrop·저장·Unblock·FontFamily 리바인딩 검증 |
|
|
25
|
+
| 4 | UIPI | High → Explorer 경유 Medium 재실행으로 회피 |
|
|
26
|
+
| 5 | 저장·MOTW | Program Files 거부, LocalAppData/앱 폴더 성공, Zone.Identifier 제거 |
|
|
27
|
+
| 6 | start.cmd | pwsh 가드 + Bypass + Hidden WPF 구동 확인 |
|
|
28
|
+
| 7 | 구현 계획 | font-loader, D&D 이벤트, 데이터 루트, 패키징 |
|
|
29
|
+
| 8 | 리스크·다음 | 실제 OLE 드래그, 폰트 파서, 콘솔 깜빡임 |
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 1. 결론
|
|
34
|
+
|
|
35
|
+
**PoC 판정: 조건부 Go.** 핵심 기능은 구현 가능하다.
|
|
36
|
+
|
|
37
|
+
| 항목 | 판정 | 근거 |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| WPF `AllowDrop`/`Drop` 수신 | **통과** | 합성 `DragEventArgs`로 `FileDrop` 1개 수신, 핸들러 실행 |
|
|
40
|
+
| 드롭 파일 저장 + MOTW 해제 | **통과** | `fonts/` 복사, `Zone.Identifier` 제거 확인 |
|
|
41
|
+
| 런타임 폰트 전환 | **통과** | 재시작 없이 `FontFamily` 교체, typefaces 4개 로드 |
|
|
42
|
+
| 기본 시스템 폰트 | **통과** | Consolas/Segoe UI로 초기 렌더링 |
|
|
43
|
+
| 임의 파일명 폰트 패밀리 탐색 | **통과** | `Fonts.GetFontFamilies`/GDI+로 `Digital-7` 식별 |
|
|
44
|
+
| 저장 경로 폴백 | **통과** | Medium 권한에서 Program Files 쓰기 거부, LocalAppData/앱 폴더 성공 |
|
|
45
|
+
| UIPI 회피 | **통과** | High 셸 → Explorer 경유 Medium 자식 실행 확인 |
|
|
46
|
+
| `start.cmd` WPF 구동 | **통과** | `where pwsh`, Bypass, Hidden, WPF Loaded/Tick 확인 |
|
|
47
|
+
| 실제 Explorer 마우스 드래그 | **미확인** | 자동화 드래그가 drop을 만들지 못함. 수동 1회 확인 필요 |
|
|
48
|
+
|
|
49
|
+
**남은 조건:** 실제 Explorer에서 폰트 파일을 위젯 위로 끌어다 놓는 **수동 1회 확인**. WPF 표준 OLE 드롭 경로이고 합성 이벤트는 통과했으므로 구현 리스크는 낮다. 다만 자동화로 재현하지 못한 것은 사실이다.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 2. 기준선: PowerShell 7
|
|
54
|
+
|
|
55
|
+
| 항목 | 값 |
|
|
56
|
+
|---|---|
|
|
57
|
+
| OS | Windows 11 10.0.26300 |
|
|
58
|
+
| PowerShell | **7.6.7** (`pwsh`) |
|
|
59
|
+
| 현재 셸 무결성 | **High (관리자)** |
|
|
60
|
+
| Explorer 무결성 | Medium |
|
|
61
|
+
| `Start-ThreadJob` | 사용 가능 |
|
|
62
|
+
| `ConvertFrom-Json -AsHashtable` | 사용 가능 |
|
|
63
|
+
| WPF/DispatcherTimer | 정상 |
|
|
64
|
+
|
|
65
|
+
### 5.1을 기각한 이유(실측)
|
|
66
|
+
|
|
67
|
+
- 5.1에서는 `Start-ThreadJob` 없음, `-AsHashtable` 없음.
|
|
68
|
+
- 36/41 스크립트가 UTF-8 BOM 없이 저장되어 5.1에서 한글 파싱이 깨지는 사례가 다수 확인됨.
|
|
69
|
+
- 5.1/.NET Framework는 `America/Los_Angeles` 같은 IANA 시간대 ID를 찾지 못함(`TimeZoneNotFoundException`). 현재 provider는 기기 시간대로 폴백하지만 지역 줄 기능이 손실된다.
|
|
70
|
+
- `Start-Job`/해시테이블 변환 자체는 5.1에서 가능했지만, 위 두 문제를 제품 코드에 우겨넣는 것은 아키텍처 오염이다.
|
|
71
|
+
- **결론: 5.1 포팅 중단. `pwsh` 7 필수. 기존 `Start-ThreadJob`/`-AsHashtable` 유지.**
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 3. WPF Drag & Drop 폰트 주입
|
|
76
|
+
|
|
77
|
+
### 3.1 검증한 경로
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
Window.AllowDrop = true
|
|
81
|
+
→ Drop 이벤트
|
|
82
|
+
→ e.Data.GetData([DataFormats]::FileDrop)
|
|
83
|
+
→ .ttf/.otf 검사
|
|
84
|
+
→ Unblock-File
|
|
85
|
+
→ fonts/ (또는 LocalAppData 폴백) 복사
|
|
86
|
+
→ FontFamily 재생성
|
|
87
|
+
→ TextBlock.FontFamily 교체
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 3.2 실측 결과
|
|
91
|
+
|
|
92
|
+
| 항목 | 결과 |
|
|
93
|
+
|---|---|
|
|
94
|
+
| `AllowDrop` 설정 | `True` 확인 |
|
|
95
|
+
| Drop 이벤트 등록 | `Add_Drop` 등록, 합성 이벤트에서 실행 |
|
|
96
|
+
| `DataFormats.FileDrop` | 파일 1개 수신 |
|
|
97
|
+
| 파일 복사 | `fonts/digital-7.ttf` 생성 |
|
|
98
|
+
| MOTW 제거 | `Zone.Identifier` 제거 확인 |
|
|
99
|
+
| 폰트 로드 | `FontFamily` typefaces 4개 |
|
|
100
|
+
| 런타임 전환 | `Consolas` → `./#Digital-7`, 재시작 없음 |
|
|
101
|
+
| 경로 공백/한글 | `DSH Font Test 한글` 경로에서 typefaces 4개 |
|
|
102
|
+
| 기본 폰트 | `Consolas`/`Segoe UI` 정상 |
|
|
103
|
+
|
|
104
|
+
### 3.3 폰트 패밀리 이름 탐색
|
|
105
|
+
|
|
106
|
+
BYO 폰트는 파일명과 패밀리명이 다를 수 있다. 두 방법 모두 동작했다.
|
|
107
|
+
|
|
108
|
+
```powershell
|
|
109
|
+
# 방법 A: 폴더의 폰트 패밀리 열거
|
|
110
|
+
[System.Windows.Media.Fonts]::GetFontFamilies([Uri]$folder)
|
|
111
|
+
# → ./#Digital-7
|
|
112
|
+
|
|
113
|
+
# 방법 B: 파일에서 직접 패밀리명 추출
|
|
114
|
+
$pfc = New-Object System.Drawing.Text.PrivateFontCollection
|
|
115
|
+
$pfc.AddFontFile($fontPath)
|
|
116
|
+
$pfc.Families[0].Name
|
|
117
|
+
# → Digital-7
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
권장: 파일 단위로 격리 폴더(`fonts/custom/<id>/`)에 복사한 뒤 방법 A로 열거하고, 실패하면 방법 B로 폰트 패밀리를 얻는다. 방법 B는 GDI+ 로드이므로 예외 처리가 필요하다.
|
|
121
|
+
|
|
122
|
+
### 3.4 실제 Explorer 드래그 상태
|
|
123
|
+
|
|
124
|
+
- 합성 `DragEventArgs`(`DragDrop.DropEvent`)로 Drop 핸들러/저장/폰트 리바인딩은 검증했다.
|
|
125
|
+
- 실제 Explorer 파일 아이콘에서 WPF 창으로 마우스 드래그하는 자동화는 여러 번 시도했으나 drop이 발생하지 않았다. DPI 200% 좌표 보정 후에도 재현되지 않았다.
|
|
126
|
+
- 이는 WPF 드롭 구현 문제인지, 자동화 마우스 주입/Explorer OLE 드래그 문제인지 분리하지 못했다.
|
|
127
|
+
- **따라서 실제 Explorer 드래그 1회는 수동 검증 항목으로 남긴다.** WPF의 표준 `AllowDrop`/`FileDrop` 경로이고 합성 이벤트는 통과했으므로 구현 자체의 리스크는 낮다.
|
|
128
|
+
|
|
129
|
+
**후속 수정(2026-10-23):** 실제 마우스 드롭 무반응을 막기 위해 `Drop` 대신 `PreviewDrop`/`PreviewDragOver`를
|
|
130
|
+
Window와 Border 양쪽에 등록하고, `AllowDrop`을 둘 다 보장했다. 또한 `ChangeWindowMessageFilterEx`로
|
|
131
|
+
`WM_DROPFILES`/`WM_COPYDATA`/`0x0049`를 허용하고, `cache/core.log`에 `font-drop:` 진입/예외 로그를 추가했다.
|
|
132
|
+
합성 `PreviewDrop`으로 FontFamily 전환과 로그를 확인했으며, 실제 Explorer 드래그는 수동 확인 항목이다.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 4. UIPI 권한 간섭
|
|
137
|
+
|
|
138
|
+
### 4.1 실측
|
|
139
|
+
|
|
140
|
+
- 현재 DSH 셸: **High Mandatory Level** (`S-1-16-12288`). 여기서 앱을 직접 띄우면 High가 된다.
|
|
141
|
+
- Explorer: **Medium** (`S-1-16-8192`).
|
|
142
|
+
- High 앱은 Explorer(Medium)에서 오는 OLE 드롭을 UIPI로 차단한다.
|
|
143
|
+
- `runas /trustlevel:0x20000`은 이 환경에서 **High를 유지**했다. 신뢰할 수 있는 de-elevation 수단이 아니다.
|
|
144
|
+
- **Explorer 경유 실행은 Medium을 만들었다.** `explorer.exe "start.cmd"`로 재실행하면 Medium 자식이 뜨고, 그 자식에서 `pwsh -v`도 정상 동작했다.
|
|
145
|
+
|
|
146
|
+
### 4.2 권장 가드
|
|
147
|
+
|
|
148
|
+
`start.cmd`:
|
|
149
|
+
|
|
150
|
+
```bat
|
|
151
|
+
@echo off
|
|
152
|
+
whoami /groups | findstr /C:"S-1-16-12288" >nul
|
|
153
|
+
if not errorlevel 1 (
|
|
154
|
+
explorer.exe "%~f0"
|
|
155
|
+
exit /b
|
|
156
|
+
)
|
|
157
|
+
where pwsh >nul 2>&1
|
|
158
|
+
if errorlevel 1 (
|
|
159
|
+
echo PowerShell 7이 필요합니다.
|
|
160
|
+
echo Windows Terminal에서: winget install Microsoft.PowerShell
|
|
161
|
+
exit /b 1
|
|
162
|
+
)
|
|
163
|
+
pwsh -NoProfile -ExecutionPolicy Bypass -Sta -WindowStyle Hidden -File "%~dp0app\core.ps1"
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
- High에서 실행되면 Explorer를 통해 자기 자신을 다시 띄워 **Medium**으로 전환한다.
|
|
167
|
+
- `core.ps1`을 관리자 셸에서 직접 실행하는 경로도 막아야 한다. 시작 시 High를 감지하면 “D&D가 차단될 수 있으니 `start.cmd`로 실행하세요” 경고를 표시하거나, D&D를 비활성화한다.
|
|
168
|
+
- 자동 재실행은 `start.cmd`에서만 수행하고, `core.ps1`은 무한 루프를 피하기 위해 경고만 한다.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## 5. 저장 경로·MOTW
|
|
173
|
+
|
|
174
|
+
### 5.1 Medium 권한 쓰기 실측
|
|
175
|
+
|
|
176
|
+
| 경로 | 결과 |
|
|
177
|
+
|---|---|
|
|
178
|
+
| `C:\Program Files\<test>` | **Access denied** |
|
|
179
|
+
| `%LOCALAPPDATA%\DSH-Notify\...` | 성공 |
|
|
180
|
+
| 앱 폴더 `fonts\` | 성공 |
|
|
181
|
+
|
|
182
|
+
**폴백 규칙:**
|
|
183
|
+
|
|
184
|
+
1. 앱 루트 `fonts\`가 쓰기 가능하면 그곳 사용.
|
|
185
|
+
2. 쓰기 불가(Program Files 등)면 `%LOCALAPPDATA%\DSH-Notify\fonts\` 사용.
|
|
186
|
+
3. 둘 다 실패하면 드롭을 거부하고 사용자에게 경로를 안내.
|
|
187
|
+
|
|
188
|
+
> 폰트뿐 아니라 `config.json`/`cache`도 같은 문제를 가진다. 공개 패키지는 **코드 루트와 데이터 루트를 분리**하고, 데이터는 앱 폴더가 쓰기 가능하면 그곳, 아니면 `%LOCALAPPDATA%\DSH-Notify`를 사용해야 한다. 이번 모듈 A 구현에서 함께 정리한다.
|
|
189
|
+
|
|
190
|
+
### 5.2 MOTW
|
|
191
|
+
|
|
192
|
+
- `Zone.Identifier`가 붙은 `.ttf`를 `Unblock-File`로 처리한 뒤 WPF에서 로드했다.
|
|
193
|
+
- `Zone.Identifier` 스트림이 제거된 것을 확인했다.
|
|
194
|
+
- 드롭 핸들러에서 복사 전/후 `Unblock-File -LiteralPath`를 호출한다. 실패해도 폰트 로드는 시도하고, 실패 시 조용히 기본 폰트로 폴백한다.
|
|
195
|
+
|
|
196
|
+
### 5.3 폰트 파일 안전성
|
|
197
|
+
|
|
198
|
+
- 허용 확장자: `.ttf`, `.otf`.
|
|
199
|
+
- 크기 제한: 5MB 이하 권장.
|
|
200
|
+
- 파일명은 `[IO.Path]::GetFileName`으로 정규화하고, 격리 폴더에 저장.
|
|
201
|
+
- WPF/GDI+ 로드 예외를 모두 잡고 기본 폰트로 폴백.
|
|
202
|
+
- 폰트 파일은 시스템에 설치하지 않는다. `PrivateFontCollection`/URI 로드만 사용한다.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## 6. start.cmd 포터블 실행
|
|
207
|
+
|
|
208
|
+
### 6.1 검증
|
|
209
|
+
|
|
210
|
+
| 항목 | 결과 |
|
|
211
|
+
|---|---|
|
|
212
|
+
| `where pwsh` | 7.6.7 발견 |
|
|
213
|
+
| High → Explorer 재실행 | Medium 자식 확인 |
|
|
214
|
+
| `pwsh -ExecutionPolicy Bypass -Sta -WindowStyle Hidden -File` | WPF 창 Loaded/Tick/종료 확인 |
|
|
215
|
+
| 경로 공백 | 인용 처리로 동작 |
|
|
216
|
+
| 관리자 권한 요구 | 없음 |
|
|
217
|
+
|
|
218
|
+
### 6.2 콘솔 깜빡임
|
|
219
|
+
|
|
220
|
+
- `start.cmd` 자체는 콘솔 앱이므로 **최초 더블클릭 시 cmd 창이 짧게 보인다.**
|
|
221
|
+
- `-WindowStyle Hidden`은 그 자식 `pwsh` 콘솔을 숨긴다. WPF 창은 정상 표시된다.
|
|
222
|
+
- 일상 실행은 `start.cmd`가 생성한 `.lnk`(WindowStyle 7, 대상 `start.cmd` 또는 `pwsh`)를 쓰는 것이 좋다.
|
|
223
|
+
- “완전 무콘솔”이 필요하면 `.lnk`가 정답이며, `.cmd` 단일 파일만으로 0ms 플래시를 보장하기는 어렵다.
|
|
224
|
+
|
|
225
|
+
### 6.3 pwsh 미설치 메시지
|
|
226
|
+
|
|
227
|
+
```text
|
|
228
|
+
PowerShell 7이 필요합니다.
|
|
229
|
+
Windows Terminal에서 다음 명령어로 설치할 수 있습니다:
|
|
230
|
+
winget install Microsoft.PowerShell
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
- `where pwsh`가 실패하면 위 메시지를 출력하고 `exit /b 1`.
|
|
234
|
+
- Store 앱 별칭 경로가 아닌 `where` 결과를 사용한다.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## 7. 구현 계획
|
|
239
|
+
|
|
240
|
+
### 7.1 파일 추가
|
|
241
|
+
|
|
242
|
+
```
|
|
243
|
+
app/
|
|
244
|
+
core.ps1
|
|
245
|
+
config.ps1
|
|
246
|
+
providers/
|
|
247
|
+
ui/
|
|
248
|
+
font-loader.ps1 # 신규
|
|
249
|
+
indicator.ps1
|
|
250
|
+
...
|
|
251
|
+
start.cmd
|
|
252
|
+
install-startup.cmd
|
|
253
|
+
uninstall-startup.cmd
|
|
254
|
+
README.txt
|
|
255
|
+
LICENSE
|
|
256
|
+
THIRD-PARTY-NOTICES.md
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### 7.2 font-loader.ps1 책임
|
|
260
|
+
|
|
261
|
+
- `Get-FontStorePath` — 앱 `fonts` 쓰기 테스트 → LocalAppData 폴백.
|
|
262
|
+
- `Test-FontCandidate` — `.ttf/.otf`, 크기, 존재.
|
|
263
|
+
- `Import-FontFile` — Unblock, 격리 폴더 복사, 패밀리 탐색, `FontFamily` 반환.
|
|
264
|
+
- `Get-DefaultFontFamily` — `Consolas` → `Segoe UI` → `Segoe UI Variable` → 시스템 기본.
|
|
265
|
+
- `Set-WidgetFont` — `$script:TimerFontFamily` 갱신 + 기존 TextBlock 재바인딩.
|
|
266
|
+
- `Initialize-CustomFont` — 시작 시 저장된 폰트 로드, 실패 시 기본.
|
|
267
|
+
|
|
268
|
+
### 7.3 D&D 이벤트
|
|
269
|
+
|
|
270
|
+
- `core.ps1`의 메인 Window에 `AllowDrop = $true`.
|
|
271
|
+
- `DragEnter`/`DragOver`: `FileDrop`에 `.ttf/.otf`가 있으면 `e.Effects = Copy`, 아니면 `None`.
|
|
272
|
+
- `Drop`: 첫 유효 파일만 처리, 성공 시 짧은 토스트/카드 테두리 피드백.
|
|
273
|
+
- 실패 시 기존 폰트 유지, 콘솔/트레이 로그.
|
|
274
|
+
- 드롭 후 `$script:config.CustomFont` 저장.
|
|
275
|
+
|
|
276
|
+
### 7.4 UIPI·데이터 루트
|
|
277
|
+
|
|
278
|
+
- `start.cmd` High → Explorer 재실행.
|
|
279
|
+
- `core.ps1` 시작 시 High 감지 → 경고 + D&D 비활성(또는 start.cmd 안내).
|
|
280
|
+
- config/cache/fonts 모두 쓰기 가능한 데이터 루트 결정. 앱 루트가 읽기 전용이면 LocalAppData로 폴백.
|
|
281
|
+
|
|
282
|
+
### 7.5 테스트
|
|
283
|
+
|
|
284
|
+
| 테스트 | 방법 |
|
|
285
|
+
|---|---|
|
|
286
|
+
| Drop 핸들러 | 합성 `DragEventArgs` + FileDrop |
|
|
287
|
+
| 실제 Explorer 드래그 | 수동 1회 |
|
|
288
|
+
| MOTW | Zone.Identifier 생성 → Drop → 스트림 제거 확인 |
|
|
289
|
+
| 폰트 리바인딩 | Consolas → custom, typefaces 수 확인 |
|
|
290
|
+
| 임의 파일명 | `random-name.ttf` + 패밀리 탐색 |
|
|
291
|
+
| 저장 폴백 | Program Files 거부 / LocalAppData 성공 |
|
|
292
|
+
| UIPI | High 셸 → start.cmd → Medium 확인 + 수동 드래그 |
|
|
293
|
+
| pwsh 가드 | PATH에서 pwsh 제거 후 메시지 확인 |
|
|
294
|
+
| 기본 폰트 | 폰트 없이 Consolas/Segoe UI 렌더링 |
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## 8. 리스크·다음 단계
|
|
299
|
+
|
|
300
|
+
| 리스크 | 영향 | 완화 |
|
|
301
|
+
|---|---|---|
|
|
302
|
+
| 실제 Explorer OLE 드래그 미확인 | UX 핵심 | 수동 1회 확인 후 구현 확정 |
|
|
303
|
+
| High 앱에서 D&D 차단 | 기능 불가 | start.cmd 자동 Medium 재실행 + core.ps1 경고 |
|
|
304
|
+
| `runas /trustlevel` 불안정 | — | 사용하지 않음. Explorer 경유만 사용 |
|
|
305
|
+
| 폰트 파서 예외/악성 파일 | 앱 크래시 | 확장자/크기 제한, try/catch, 기본 폰트 폴백 |
|
|
306
|
+
| 패밀리명 탐색 실패 | 폰트 미적용 | GDI+ → Fonts.GetFontFamilies 순차 시도 |
|
|
307
|
+
| 앱 루트 읽기 전용 | 설정/폰트 저장 실패 | 데이터 루트 LocalAppData 폴백 |
|
|
308
|
+
| cmd 콘솔 깜빡임 | 첫 실행 UX | 최초 1회 허용, 일상은 생성된 `.lnk` |
|
|
309
|
+
| pwsh 미설치 | 실행 불가 | `where pwsh` 가드 + winget 안내 |
|
|
310
|
+
| 5.1 회귀 유혹 | 아키텍처 오염 | 기준선 pwsh 7 고정, 5.1 분기 금지 |
|
|
311
|
+
|
|
312
|
+
**다음 단계:** 위 PoC 결과를 승인하면 모듈 A를 `pwsh 7` 기준으로 구현한다. 순서는 ① `font-loader.ps1` + 단위 PoC, ② `core.ps1` D&D 연결 및 동적 리바인딩, ③ `start.cmd`/UIPI 가드, ④ 포터블 패키징·테스트.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# 2026-10-24 — Linux/Pop!_OS desktop verification of `@nolit/dsh-notify`
|
|
2
|
+
|
|
3
|
+
<!-- skeleton:
|
|
4
|
+
version: 1.0
|
|
5
|
+
kind: report
|
|
6
|
+
status: measured
|
|
7
|
+
updated: 2026-10-24
|
|
8
|
+
summary: Linux/Pop!_OS desktop verification of @nolit/dsh-notify: what was checked and the result of each check.
|
|
9
|
+
-->
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
Target: DSH **0.2.1-rc.3** on Pop!_OS (Ubuntu-based), Node v24.2.0.
|
|
13
|
+
Method: static audit against the real rc.3 install + isolated `DSH_HOME=/tmp/dsh-home-B`
|
|
14
|
+
served on a spare port + headless Chromium (CDP) driving the real page.
|
|
15
|
+
Nothing under `~/.dsh` was modified; the live 3080 server was never restarted.
|
|
16
|
+
|
|
17
|
+
## 1. Verdict (short)
|
|
18
|
+
|
|
19
|
+
The plugin **boots and works** on this Linux box, but the advertised
|
|
20
|
+
"hybrid/dual mode" is **misleading for remote browsers**:
|
|
21
|
+
|
|
22
|
+
| Capability | Served on `localhost:3080` (secure) | Served on `http://<lan-ip>:3080` (insecure) |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| OS/desktop notification | ✅ works (permission grantable, `show` fires) | ❌ **impossible** — Chrome pins `permission` to `denied`, no prompt exists |
|
|
25
|
+
| In-page toast fallback | ✅ | ✅ (this is the only channel that works) |
|
|
26
|
+
| Synth chime (WebAudio) | ✅ | ✅ |
|
|
27
|
+
| Settings/consent UI | ✅ | ✅ |
|
|
28
|
+
|
|
29
|
+
So "dual mode" (OS notification **and** in-page) degrades to **toast-only** for any
|
|
30
|
+
browser that is not on the same machine. That is a real capability gap, not a bug
|
|
31
|
+
in the plugin — it is a platform constraint that the plugin previously hid.
|
|
32
|
+
|
|
33
|
+
## 2. Environment findings (measured, not assumed)
|
|
34
|
+
|
|
35
|
+
| Item | Measured value | Note |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| Session type | **No graphical user session.** `XDG_SESSION_TYPE`, `WAYLAND_DISPLAY`, `DISPLAY` all unset in this context | DSH runs as a **user** systemd unit |
|
|
38
|
+
| `loginctl` sessions | only `cosmic-greeter` on `seat0/tty1` (uid 987) | The user (uid 1000) has no seated graphical session |
|
|
39
|
+
| `dsh-web.service` | `systemctl --user` → `active`; listening `0.0.0.0:3080` | User scope, **not** system scope |
|
|
40
|
+
| D-Bus session bus | `unix:path=/run/user/1000/bus` present | bus reachable |
|
|
41
|
+
| `org.freedesktop.Notifications` | **`ServiceUnknown: The name is not activatable`** | No notification daemon is running **or** activatable |
|
|
42
|
+
| Daemon on the bus | none (only `com.system76.CosmicStatusNotifierWatcher`, an SNI tray watcher) | Not a notification server |
|
|
43
|
+
| `notify-send` | **not installed** (`libnotify-bin` absent) | `libnotify4` is installed (library only) |
|
|
44
|
+
| `notification-daemon` | installed (`/usr/lib/notification-daemon/notification-daemon`, 3.20.0) but has **no D-Bus service file** | Never auto-activated |
|
|
45
|
+
| dunst / mako / notify-osd / xfce4-notifyd | all absent | — |
|
|
46
|
+
|
|
47
|
+
**Consequence:** no process on this machine could display a freedesktop
|
|
48
|
+
notification today. A browser notification would be handed to the OS and
|
|
49
|
+
**silently dropped**. The in-page toast is the only reliable channel here.
|
|
50
|
+
|
|
51
|
+
## 3. Defects found
|
|
52
|
+
|
|
53
|
+
| # | Severity | Defect | Status |
|
|
54
|
+
|---|---|---|---|
|
|
55
|
+
| D1 | **High** | `dsh.client.inject` listed `@deepseek-ai/dsh-client-ui-slots`, which **does not exist** in rc.3. The `slots` service is owned by `@deepseek-ai/dsh-client-ui-renderer` (`super(ctx,"slots")`), injected by 31 core plugins. | **FIXED** |
|
|
56
|
+
| D2 | **Medium** | No secure-context awareness. On `http://<lan-ip>:3080` the API reports `denied`, so the UI claimed "알림 차단됨" (user blocked) when the truth is "never possible". | **FIXED** |
|
|
57
|
+
| D3 | Low | No desktop notification daemon on this host (environment, not plugin). | Reported — see §2 |
|
|
58
|
+
| D4 | Low (advisory) | `approval/request` + `user-questions/request` listeners are bound as `(scope, request, next)`; rc.3 core consumers use `(request, next)`. Verified harmless today (rc.3 always supplies the `next` bail closure) but the leading `scope` arg is a dead parameter. | Documented, **not changed** |
|
|
59
|
+
|
|
60
|
+
### D1 detail — why it still booted
|
|
61
|
+
|
|
62
|
+
`client-modules` resolves each `inject` entry against the boot graph and
|
|
63
|
+
**silently skips** unknown ids:
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
for (const packageName of row.inject) {
|
|
67
|
+
const dependency = this.graphRows.get(packageName);
|
|
68
|
+
if (dependency !== void 0) await this.arriveDependency(row.id, dependency, [], visited);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
So the package booted anyway (verified: `window.__dshNotifyWeb` exists, both slots
|
|
73
|
+
rendered, zero console errors) but it lost its **ordering guarantee** after the
|
|
74
|
+
renderer that declares the slot map. It worked by luck.
|
|
75
|
+
|
|
76
|
+
### D2 detail — measured evidence
|
|
77
|
+
|
|
78
|
+
Same CDP grant (`Browser.grantPermissions`, `notifications`) applied to both
|
|
79
|
+
origins, full Chromium in new-headless mode:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
http://192.168.50.203:3098 isSecureContext=false permission=denied requestPermission→denied (no prompt)
|
|
83
|
+
http://localhost:3098 isSecureContext=true permission=granted requestPermission→granted events=["show"]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The insecure origin is forced to `denied` **even with a pre-granted permission**.
|
|
87
|
+
|
|
88
|
+
## 4. What was verified working
|
|
89
|
+
|
|
90
|
+
- `node --test test/client.test.mjs` → **19/19 pass** (15 original + 4 new).
|
|
91
|
+
- Boot on isolated server: no exceptions, no console errors, both slots render
|
|
92
|
+
(`settings.section` nav + `shell.overlay` consent card "DSH 알림을 켜시겠습니까?").
|
|
93
|
+
- Event wiring uses live rc.3 APIs: `ctx.remote.$on('api-session/status'|'api-session/error'|'approval/request'|'user-questions/request')`
|
|
94
|
+
(all four are declared in `dsh-api-remotes`' forwarded-event allowlist) and
|
|
95
|
+
`ctx.sessions.list.getSnapshot().byId` + `ctx.sessions.scopeOf(ctx)` (both real).
|
|
96
|
+
- `exports.inject = ['remote','sessions','slots','locale']` — all four are real
|
|
97
|
+
rc.3 client services.
|
|
98
|
+
- Audio: WebAudio reaches `running`, oscillators schedule, `<audio>` data-URI
|
|
99
|
+
fallback plays; no throw in headless, no gesture requirement violated.
|
|
100
|
+
- Degradation: on insecure origin the plugin now reports `insecure` and falls
|
|
101
|
+
back to a toast (verified on a real LAN origin through a TCP proxy).
|
|
102
|
+
|
|
103
|
+
## 5. Verification commands
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
cd /home/minami/Workspace/projects/dsh-workspace/dsh-plugins/dsh-notify
|
|
107
|
+
node --test test/client.test.mjs
|
|
108
|
+
|
|
109
|
+
# injected package names
|
|
110
|
+
ls /home/minami/.local/nodejs/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/ | grep -E 'ui-slots|ui-renderer'
|
|
111
|
+
|
|
112
|
+
# desktop notification pipeline
|
|
113
|
+
export XDG_RUNTIME_DIR=/run/user/1000 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
|
|
114
|
+
gdbus call --session --dest org.freedesktop.Notifications \
|
|
115
|
+
--object-path /org/freedesktop/Notifications \
|
|
116
|
+
--method org.freedesktop.Notifications.GetServerInformation
|
|
117
|
+
|
|
118
|
+
# isolated boot (spare port, killed afterwards)
|
|
119
|
+
rsync -a ~/.dsh/ /tmp/dsh-home-B/
|
|
120
|
+
ln -sfn "$PWD" /tmp/dsh-home-B/profiles/web/node_modules/@nolit/dsh-notify
|
|
121
|
+
DSH_HOME=/tmp/dsh-home-B ~/.local/bin/dsh --profile web \
|
|
122
|
+
--patch /tmp/dsh-home-B/profiles/web/cordis.patch.yml.append.yml \
|
|
123
|
+
--host 127.0.0.1 --port 3098 --no-open
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## 6. Recommendation
|
|
127
|
+
|
|
128
|
+
1. Enable the plugin. In-page toast + chime is genuinely useful on this host.
|
|
129
|
+
2. Do **not** market it as desktop notifications unless DSH is reached via
|
|
130
|
+
`https://` or `localhost`. For a real desktop channel, install a notification
|
|
131
|
+
daemon (`sudo apt install libnotify-bin` for `notify-send`; and enable a
|
|
132
|
+
daemon such as `dunst` or run a COSMIC session) — otherwise the OS drop is silent.
|
|
133
|
+
3. Long term: an HTTPS reverse proxy (or SSH port-forward to `localhost:3080`)
|
|
134
|
+
restores full OS notifications for remote browsers.
|