Issue 01중국 AI
AC POST
중국 AI 목록
掘金2026년 11월 1일 21:16중국어 → 한국어

DeepSeek Harness 플러그인으로 확장자별 윈도우 우클릭 열기 제어

DSH 데스크톱판 문서 이용자가 겪는 기본 앱 지정 문제를 해결하기 위해 확장자별로 윈도우 우클릭 열기 방식을 제어하는 플러그인 작성 방법을 소개했다.

중국어 원문을 AI로 번역했습니다. 고유명사와 수치는 원문 표기를 우선하며, 중요한 판단에는 아래 출처 원문을 함께 확인하세요.

이 글은 DSH 0.2.1-rc.5를 기준으로 하며, 플러그인 인터페이스는 이후 변경될 수 있습니다.

DSH 데스크톱 버전으로 문서를 보는 사람이라면 대개 이런 일을 겪었을 것입니다:

.md 파일을 우클릭해서 Typora로 열고 싶은데 시스템이 메모장을 띄운다. Windows 설정에서 기본 앱을 바꿔 놓아도 며칠 지나면 다른 소프트웨어가 다시 가져간다.

Windows의 파일 연결은 전역적이지만, 나는 DSH 안의 우클릭 동작만 바꾸고 싶었다. 그래서 DSH 플러그인 dsh-open-with를 작성했다: 플러그인 안에서 「확장자 → 프로그램」 매핑을 유지하며, 시스템 레지스트리를 수정하지 않고 Windows 기본 연결도 빼앗지 않는다.

이 글은 플러그인의 구조, Host 측 구현, Windows 연결 조회의 함정, 그리고 설정 페이지를 두 번 다시 만든 과정을 기록한다.

一、먼저 DSH 플러그인의 최소 구조부터

DSH 플러그인은 본질적으로 dsh 필드가 있는 npm 패키지이며, bundle patch를 통해 profile에挂(연결)된다:

{ "name" : "dsh-open-with" , "version" : "1.0.0" , "type" : "module" , "main" : "lib/index.js" , "dsh" : { "client" : { "platform" : "web" } , "bundle" : { "patch" : "./cordis.patch.yml" } } }

cordis.patch.yml은 플러그인을 DSH의 bundle 목록에 삽입하는 역할을 한다:

- insert: - id: open-with name: dsh-open-with

DSH 데스크톱 버전 profile에 설치한 뒤에는 완전히 종료했다가 다시 시작해야 하며, 페이지를 새로고침해도 bundle이 다시 로드되지 않는다.

플러그인은 두 부분으로 나뉜다:

- lib/index.js: Host 측으로, 설정 읽기·쓰기, 파일 열기, Windows 연결 조회, 파일 선택 대화상자 표시를 담당한다;

- lib/client.js: 클라이언트 측으로, 우클릭 메뉴와 설정 페이지를 담당한다.

클라이언트는 두 개의 slot을 통해 마운트된다:

ctx. slots . inject ( "shell.overlay" , () => ctx. slots . register ({ name : "shell.overlay" , id : "open-with" , order : 70 , locale : NS }, Overlay ) ); ctx. slots . inject ( "plugins.bundle.config" , () => ctx. slots . register ({ name : "plugins.bundle.config" , key : "dsh-open-with" , order : 0 , locale : NS }, ConfigPage ) );

shell.overlay는 우클릭 메뉴를 그리는 데 쓰이고, plugins.bundle.config는 플러그인 상세 페이지 안의 설정 영역이다.

二、Host 측: 설정, 라우트, 파일 열기

설정은 $DSH_HOME/open-with.json에 저장되며 구조는 매우 단순하다:

{ "version" : 1 , "apps" : { "md" : { "name" : "Typora" , "path" : "C:\\Users\\you\\AppData\\Local\\Programs\\Typora\\Typora.exe" } } }

쓰기는 임시 파일 + rename 방식을 사용해, 쓰는 도중 전원이 꺼져 반쯤 잘린 JSON이 남는 일을 피한다:

const tmp = ` ${configFile} .tmp` ; await writeFile (tmp, JSON . stringify (state, null , 2 ), "utf8" ); await rename (tmp, configFile);

라우트는 ctx.webServer.register로 등록하며, 모두 먼저 DSH의 연결 인증을 거친다:

export const inject = [ "webServer" , "connection" ]; export function apply ( ctx ) { ctx. webServer . register ({ kind : "exact" , path : "/open-with/handlers" , handler : async (req, res) => { if (ctx. connection . requestRejection (req)) return ; const ext = new URL (req. url , "http://localhost" ) . searchParams . get ( "ext" ) ?? "" ; const apps = await handlersFor (ext); res. writeHead ( 200 , { "content-type" : "application/json" }); res. end ( JSON . stringify ({ apps })); } }); }

파일을 열 때 Host는 설정에 있는 exe를 직접 spawn하고 파일 경로를 첫 번째 인자로 넘기며, Windows 기본 연결 확인을 거치지 않는다. 실행 전에 두 가지를 한다:

- 파일 경로가 DSH 파일 시스템 매핑 범위 안에 있는지 검증한다;

- 자식 프로세스 환경의 민감한 변수를 정리한다.

/open-with/open은 설정에 존재하는 exe만 받아들이고, 클라이언트가 임의의 명령을 전달하는 것은 받아들이지 않는다.

三、핵심 함정: Windows의 "범용 폴백 목록"

앱 추천은 Windows Shell의 SHAssocEnumHandlers에서 오며, @deepseek-ai/dsh-native-command의 nativeFileApplications를 통해 호출한다:

import { nativeFileApplications } from "@deepseek-ai/dsh-native-command" ; const apps = await nativeFileApplications ( "md" );

문제는 이것이다. 확장자에 아무 처리기도 등록되어 있지 않으면 Windows는 빈 배열을 반환하지 않고, 약 18개 애플리케이션이 담긴 범용 목록을 반환한다. 그 안에는 클라우드 스토리지, 압축 프로그램, 플레이어가 뒤섞여 있다. 사용자가 "너트스토어(Nutstore)로 .zzz 열기"를 보면 플러그인이 대충 찍고 있다고 느낄 것이다.

나는 먼저 레지스트리를 조회해서 이 확장자에 연관 클래스가 실제로 있는지 확인했다.

// 의사 코드 async function hasRegisteredClass ( ext ) { const { code } = await run ( "reg.exe" , [ "query" , `HKCR\\. ${ext} ` ]); return code === 0 ; } async function handlersFor ( ext ) { if (!( await hasRegisteredClass (ext))) return []; return nativeFileApplications (ext); }

효과 비교:

확장자 수정 전 수정 후 md 20개 애플리케이션 20개 애플리케이션 py 21개 애플리케이션 21개 애플리케이션 zzz 18개 무관한 애플리케이션 0개 odp 18개 무관한 애플리케이션 0개

등록되어 있지 않을 때 플러그인은 " .xxx 를 열 수 있는 애플리케이션을 찾지 못했습니다"라고 표시하고, 사용자가 exe 경로를 직접 입력하도록 하며, 무관한 프로그램을 잔뜩 추천하지 않는다.

조회 결과는 확장자별로 5분간 캐시해서, 드롭다운을 열 때마다 Shell API를 호출하지 않도록 했다.

四. 클라이언트: 우클릭 메뉴와 설정 페이지

우클릭 메뉴는 파일 카드에 붙어 있으며, 주로 다음 동작을 제공한다.

- X로 열기: 플러그인 설정을 읽고, 설정이 없으면 시스템 기본값으로 폴백한다.

- 연결 프로그램…: 플러그인 애플리케이션 + 시스템 연결 애플리케이션

- 폴더에서 표시

- 경로 복사

- 사이드바에서 미리보기

- 연결 프로그램 설정…

설정 페이지는 이번 재작업이 가장 많았던 곳이다.

첫 번째 버전: 표를 플러그인 상세 페이지에 밀어 넣기

첫 번째 버전은 「확장자, 애플리케이션, 경로」를 표로 만들어 플러그인 상세 페이지에 그대로 넣었다. 기능에는 문제가 없었지만 31.5인치 4K 화면에서는 보기 좋지 않았다.

- 플러그인 상세 페이지는 읽기 폭이고, 본문 컬럼은 약 650px이다.

- 표의 네 컬럼이 650px 안에 눌려 들어가 경로가 잘린다.

- 화면 양쪽에는 넓은 여백이 남는데 표는 오히려 공간이 부족하다.

- 확장자 드롭다운은 300px뿐이라 유형 필터 버튼이 줄바꿈된다.

두 번째 버전: 요약 + 넓은 드로어

이후 다음과 같이 바꿨다.

- 플러그인 페이지에는 요약만 표시한다. 설정된 개수, 앞쪽 몇 개의 매핑, 설정 열기

- 「설정 열기」를 누르거나 우클릭 「연결 프로그램 설정…」을 누르면 넓은 드로어가 열린다.

- 드로어 너비는 min(1320px, 100vw - 280px) 이며, 더 이상 플러그인 상세 페이지의 max-width 제약을 받지 않는다.

- 드로어 안에는 전체 표가 있고, Esc, ✕, 마스크 클릭 모두로 닫을 수 있다.

이렇게 하면 DSH 상위 컨테이너의 너비를 덮어쓸 필요가 없고, DSH 업그레이드로 레이아웃이 바뀌어도 어긋나지 않는다.

그림 1: 우클릭 메뉴. 그림 2: 넓은 드로어 설정 페이지.

글자 크기 확대와 가림

드로어에는 Aa 확대를 넣었다. 100% / 110% / 120% / 130%, 기본값 120%.

처음에는 글자만 확대했더니 컬럼 너비, 버튼, 드롭다운 패널이 따라 커지지 않아 오히려 더 답답해졌다. 이후 전체 zoom 을 사용해 글자, 컬럼 너비, 행 높이, 버튼, 플로팅 레이어를 같은 비율로 확대해서야 일관성이 유지되었다.

또 다른 문제는 일괄 추가였다. 원래는 표 오른쪽 위에 떠 있는 레이어였는데, 열리면 경로 컬럼을 가렸다. 이를 제목 아래의 전체 폭 인라인 패널 로 바꿔 표 전체가 아래로 내려가고, 패널 내부에서 스크롤되며, 하단의 "취소/추가"가 고정되어 보이도록 했다.

그림 3: 3D 포맷 일괄 추가 인라인 패널.

자동 저장

설정 페이지에는 「저장」 버튼이 없다.

- 드롭다운 선택, 삭제는 즉시 저장된다.

- 경로 입력은 600ms 멈추면 저장된다.

- "확장자 + 절대 경로"가 완전한 행만 저장한다.

- 애플리케이션/경로가 빠진 행은 초안이며 설정에 기록되지 않고, 제목 행에 「N행 완료 대기」가 표시된다.

이렇게 하면 디스크에 자주 쓰지도 않고, 미완성 상태를 설정에 기록하지도 않는다.

五. 두 가지 Windows 함정

1. 중국어 파일명과 PowerShell 5.1

설치 스크립트에서는 安装与使用说明.pdf 같은 중국어 파일명을 복사해야 한다. 스크립트를 UTF-8 BOM 없이 저장하면 Windows PowerShell 5.1이 ANSI로 읽어서 중국어 파일명이 바로 깨지고, Test-Path 가 파일을 찾지 못한다.

해결: install.ps1 을 UTF-8 with BOM 으로 저장한다. PowerShell 7 과도 호환된다.

2. 자식 프로세스와 테스트 샌드박스

테스트 중 spawn EPERM 을 만났는데, 살펴보니 플러그인 로직 문제가 아니라 테스트용 DSH 서비스가 제한된 샌드박스에서 돌아 자식 프로세스 파이프가 막힌 것이었다. 정상적인 데스크톱 환경에서 실행하니 정상 동작했다.

앞으로 비슷한 플러그인을 만들 때는 테스트 환경을 실제 데스크톱 환경과 일치시켜야 하며, 그렇지 않으면 이런 종류의 오류를 쉽게 오판하게 된다.

六. 설치와 배포

플러그인은 Windows 10/11 + DSH 데스크톱 버전 만 지원하며, 관리자 권한이 필요 없다.

설치:

powershell -ExecutionPolicy Bypass -File .\install.ps1

DSH를 완전히 종료했다가 다시 열면 플러그인 목록에 「파일 열기 방식 v1.0.0」이 나타난다.

현재 DSH에는 공식 플러그인 마켓이 없으므로 주로 로컬 bundle + 설치 스크립트로 배포한다. DSH에는 실험적인 사용자 정의 플러그인 마켓 인덱스도 있어서, 나중에 market-index.json 을 하나 더 연결할 수 있다.

제거:

powershell -ExecutionPolicy Bypass -File .\install.ps1 -Uninstall

설정 파일 open-with.json 은 삭제되지 않으며, 필요하면 직접 정리해야 한다.

七. 프로젝트 주소

- Gitee: https://gitee.com/leecmd786567/dsh-open-with

- 라이선스: MIT

- 버전: V1.0.0

코드는 많지 않으니, Issue를 올리거나 구현을 직접 확인하셔도 좋습니다. DSH를 사용 중이면서 Windows 기본 연결이 변경되는 문제를 겪었다면, 이 플러그인이 수고를 조금 덜어줄 것입니다.