# 플러그인 기여 가이드 Voyager의 플러그인 시스템은 선언형 플러그인을 우선합니다. `plugin.json`에는 플러그인 정보와 DOM 작업을 적고, CSS에는 시각적 변경을 적습니다. 플러그인은 원격 JavaScript를 실행하지 않으며, Voyager에 내장된 플러그인 엔진이 manifest와 스타일을 해석합니다. 이 방식은 리뷰와 유지보수를 쉽게 만듭니다. 플러그인을 기여하고 싶다면 여기서 시작하는 것을 권장합니다. ## 나만 쓸 플러그인: 로컬 플러그인 내 브라우저에서만 필요한 플러그인이라면 이 과정을 거칠 필요가 없습니다. 로컬 플러그인으로 만들어 팝업에서 가져오면 됩니다. Issue도, PR도, 저장소 체크아웃도 필요 없습니다. [직접 플러그인 만들기](./write-a-plugin.md)에서 세 가지 제작 방법을 설명합니다. `create-voyager-plugin` 검사 도구와 코딩 에이전트용 skill도 포함됩니다. ## 권장 흐름 1. 먼저 플러그인에 적합한지 확인하세요. 읽기 폭, 레이아웃 수정, 테마 조정, 페이지 요소 숨김 또는 표시, 간단한 사이트 대응은 보통 좋은 후보입니다. 2. Voyager 메인 저장소에 먼저 Issue를 열어 해결하려는 문제, 대상 웹사이트, 기존 플러그인과의 차이를 설명하세요. 유지관리자의 명시적 승인을 받은 뒤 구현과 PR 작성을 시작하세요. 3. `plugin.json`에 메타데이터, 사이트 매칭, 설정, 기여 내용을 작성하세요. 4. 스타일은 같은 디렉터리의 `style.css`에 넣고 `contributes.styles`에서 참조하세요. 5. 로컬에서 테스트한 뒤 PR에 테스트 페이지, 스크린샷 또는 짧은 녹화를 첨부하세요. 유지관리자는 완성도를 보고 공식 catalog 포함 여부를 결정합니다. ## 디렉터리 구조 공식 번들 플러그인은 `src/features/plugins/catalog/` 아래에 있고, 플러그인 플랫폼마다 디렉터리가 하나씩 있습니다. ``` src/features/plugins/catalog/ marketplace.json 문서 플러그인 스토어가 읽는 색인 sites//site.json 데이터로 표현한 사이트 어댑터 sites//plugins//plugin.json 선언형 플러그인 sites//plugins//style.css 해당 스타일 sites//plugins//README.md 무엇을 왜 고치는지에 대한 설명 ``` 탐색은 자동입니다. `catalog/sites/index.ts`가 `import.meta.glob`으로 모든 `site.json`과 `plugin.json`을 찾으므로, 사이트나 플러그인을 추가하는 일은 파일을 추가하는 일입니다. 손으로 관리하는 매핑 표는 없습니다. `marketplace.json`은 그 매핑 표가 아니라 문서 플러그인 스토어가 읽는 색인입니다. 테스트가 탐색 결과와의 동기화를 강제하므로 새 플러그인은 여기에도 항목을 추가해야 합니다. `source`는 catalog 기준 상대 경로이며, 예를 들어 `sites/deepseek/plugins/reading-width/plugin.json`입니다. 단, `sites/gemini/`와 `sites/aistudio/` 아래 플러그인은 예외입니다. 이들은 Voyager에 함께 포함되어 배포되므로 `marketplace.json`에 넣지 않습니다. 플러그인의 `matches`는 그 플러그인이 속한 사이트의 `matches` 안에 있어야 합니다. 범위를 벗어나면 빌드가 실패합니다. ## 의미 선택자 키 `site.json`은 고정된 의미 키 어휘를 사이트별 CSS 선택자에 연결합니다. 어휘는 `src/features/plugins/sites/semanticKeys.ts`에 정의되어 있고, `site.json`은 이 목록의 키만 사용할 수 있으며 모르는 키는 거부됩니다. - `userTurn`: 사용자 메시지 컨테이너. - `assistantTurn`: 어시스턴트 메시지 컨테이너. - `thinkingBlock`: 어시스턴트 응답 안의 추론 영역. - `codeBlock`: 렌더링된 코드 블록. - `composer`: 사용자가 입력하는 프롬프트 입력창. - `sidebar`: 대화 목록 또는 내비게이션 레일. - `sidePanel`: artifacts나 canvas 같은 보조 패널. - `headerActions`: 대화 화면 오른쪽 위의 동작 영역. - `scrollContainer`: 대화를 스크롤하는 요소. - `composerRoot`: 프롬프트 입력창과 그 동작 버튼을 감싸는 컨테이너. - `sendButton`: 프롬프트를 보내는 버튼. 입력창에 내용이 있을 때만 나타나는 사이트도 있습니다. 사이트는 실제로 제공할 수 있는 키만 선언합니다. 플러그인은 선택자를 직접 쓰지 않고 DOM 작업의 `target`에 `{ "kind": "semantic", "key": "userTurn" }`을 적어 키를 참조하므로, 사이트가 개편되어도 `site.json` 한 파일만 고치면 됩니다. `conversationIdPattern`은 선택자가 아니라 `site.json`의 별도 필드입니다. URL 경로에 대한 정규식이며 첫 번째 캡처 그룹이 대화 id입니다. 예를 들면 `^/chat/([^/?#]+)`입니다. ## 새 플랫폼 추가하기 새 채팅 웹사이트를 추가하는 작업은 두 개의 층으로 나뉩니다. 1. **사이트 어댑터** `catalog/sites//site.json`: Voyager에게 웹사이트를 인식하는 방법과 사용자 턴, 어시스턴트 턴, 입력창, 사이드바, 테마 표식을 찾는 방법을 알려 줍니다. 2. **플러그인** `catalog/sites//plugins//`: 그 웹사이트에서 사용자가 겪는 문제 하나를 해결합니다. 읽기 너비나 렌더링 수정 같은 것입니다. `site.json`에는 `id`(디렉터리 이름과 같아야 합니다), `label`, `matches`, `selectors`, `theme`, `brandColor`, `capabilities`가 들어가며, 선택 항목으로 `conversationIdPattern`과 `turnKeyAttributes`를 둘 수 있습니다. `turnKeyAttributes`는 페이지가 사용자 메시지를 다시 렌더링할 때도 그 메시지의 정체성을 유지하는 속성 이름입니다. 메시지 전송 추적(폴더 Activity와 타임라인 타임스탬프에서 사용)은 이 속성이 설정된 사이트에서만, 그리고 `composer`, `userTurn`, `conversationIdPattern`과 함께 실행됩니다. `src/features/plugins/sites/siteAdapterData.ts`의 `validateSiteAdapterData`가 이를 검증하며, `bun run catalog:build`는 게시 전에 모든 사이트와 플러그인을 검증합니다. `site.json`은 사이트 어댑터 자체를 데이터로 적은 것입니다. 현재 플러그인 플랫폼은 ChatGPT, Claude, DeepSeek입니다. Gemini와 AI Studio는 Voyager의 네이티브 화면이며 각자 `site.json`(`catalog/sites/gemini/`, `catalog/sites/aistudio/`)을 갖지만, 이 파일은 확장 프로그램에 포함되어 배포될 뿐 원격 카탈로그에는 게시되지 않습니다. `sites/adapters/gemini.ts`, `aistudio.ts`, `claude.ts`, `chatgpt.ts`, `deepseek.ts`는 모두 `site.json`을 감싼 한 줄짜리 껍데기입니다. TypeScript가 아니라 JSON을 수정하세요. 게시되는 호스트별 카탈로그가 사이트 데이터도 함께 싣기 때문에, 선택자 수정은 확장 프로그램을 새로 배포하지 않아도 사용자에게 전달됩니다. 다만 완전히 새로운 사이트는 여전히 확장 프로그램 배포가 필요합니다. 호스트 권한과 콘텐츠 스크립트 등록도 패키지에 포함되어 배포되어야 하기 때문입니다. DeepSeek 같은 새 플랫폼을 추가할 때는 어댑터 작업을 플랫폼 계약에 집중하세요. `site.json`, 필요한 선택자 테스트, 지원 내용을 설명하는 최소한의 문서를 하나의 완결된 변경으로 묶으세요. 사이트 레지스트리는 파일 자체에서 새 어댑터를 찾으므로 등록 목록을 수정할 필요가 없습니다. 플러그인마다 새 사이트를 추가하지는 마세요. 어댑터가 승인된 뒤에는 후속 플러그인을 별도의 집중된 변경으로 제출할 수 있습니다. 여러 변경이 서로 의존한다면 스택 PR 체인이 유용합니다. 각 후속 브랜치는 이전 브랜치를 기반으로 하고, 각 PR은 자신의 점진적 기능만 보여 줍니다. 이 체인도 같은 범위 원칙을 따라야 합니다. PR마다 명확한 사용자 목표 하나와 필요한 테스트 및 정리 작업을 담으세요. 사이트 지원에는 모델 API 연동이 필요하지 않습니다. 새 플랫폼을 위해 정적 콘텐츠 스크립트나 필수 호스트 권한을 추가하지 마세요. 플러그인만 제공하는 사이트는 기존의 선택적 권한과 동적 등록 흐름을 사용하면 됩니다. 선택적 권한과 동적 등록 경로는 브라우저 버전에 따라 다릅니다. Chrome과 Edge는 확장 프로그램이 사용하는 흐름을 지원합니다. Firefox는 선택적 호스트 권한에 128 이상이 필요하고, Safari는 동적 콘텐츠 스크립트 등록에 16.4 이상이 필요합니다. 더 오래된 지원 브라우저에서는 필요한 API가 지속적인 허용을 제공할 수 없으므로, 팝업이 사용자 지정 사이트의 플러그인 활성화를 올바르게 거부할 수 있습니다. 이를 피하려고 관리자 승인 없이 넓은 정적 호스트 권한을 추가하지 마세요. ## 플러그인 범위 플러그인은 플랫폼별로 기계적으로 나누기보다, 사용자가 해결하려는 문제를 기준으로 나누는 것이 좋습니다. 카탈로그 플러그인은 하나의 사이트 폴더에 속하며, 모든 `matches`는 그 폴더의 `site.json`이 포함하는 범위 안에 있어야 합니다(그렇지 않으면 `bun run plugin:check`가 실패합니다). 따라서 하나의 플러그인이 Claude와 ChatGPT를 함께 다룰 수 없습니다. 읽기 폭, 페이지 넘김, 코드 블록 레이아웃처럼 같은 기능이 여러 플랫폼에 맞는다면 사이트 폴더마다 플러그인을 하나씩 만들고(예: `sites/claude/plugins/reading-width`와 `sites/chatgpt/plugins/reading-width`) 이름, 설정, 설명을 일관되게 유지하세요. 반대로 플랫폼마다 설정, DOM 로직, 사용자 문구가 완전히 다르다면 여러 플러그인으로 나누는 편이 명확합니다. "하나로 모든 것을 처리"하기 위해 관련 없는 기능을 억지로 넣지 마세요. 하나의 플러그인은 하나의 분명한 문제를 해결하는 것이 가장 좋습니다. 간단한 기준: - 사용자 목표와 설정이 같고 셀렉터만 다름: 사이트 폴더마다 플러그인 하나씩, 이름과 설정은 일관되게. - 같은 주제지만 플랫폼별 경험 차이가 큼: 나눌 수 있지만 이름과 설명의 관련성을 유지합니다. - 기능 목표가 다름: 합치지 않습니다. ## 중복 플러그인 피하기 제출 전 플러그인 마켓플레이스와 기존 공식 플러그인을 확인하세요. 이미 좋은 플러그인이 있다면 비슷한 것을 새로 만들기보다 기존 플러그인을 개선하는 PR을 우선하세요. 중복 플러그인은 다음처럼 명확한 개선이 있을 때만 받을 가치가 있습니다. - 기존 플러그인이 지원하지 않는 중요한 플랫폼을 지원합니다. - 기존 플러그인이 오랫동안 해결하지 못한 호환성 문제를 고칩니다. - 성능, 접근성, 유지보수성이 명확히 더 좋습니다. - 이름이나 스타일을 조금 바꾼 수준이 아니라 충분히 다른 사용자 경험을 제공합니다. 이렇게 해야 마켓플레이스가 깔끔해지고 사용자가 선택하기 쉬워집니다. ## 최소 예시 빈 디렉터리부터 시작하지 마세요. `bun run plugin:new --site `를 실행하면 `catalog/sites//plugins//`가 만들어져 `plugin.json`, `style.css`, `README.md`가 들어가고 `marketplace.json`에도 해당 항목이 추가됩니다. 생성된 결과는 출발점일 뿐 승인이 아닙니다. 채워 넣을 매니페스트는 다음과 같은 모양입니다. ```json { "$schema": "https://voyager.nagi.fun/plugin.schema.json", "id": "your-name.example-plugin", "name": "Example Plugin", "version": "1.0.0", "description": "A short description of what this plugin improves.", "author": "your-name", "category": "readability", "license": "MIT", "engine": ">=1.0.0", "tier": "declarative", "matches": ["https://claude.ai/*"], "contributes": { "styles": [{ "file": "style.css" }], "domOps": [ { "op": "addClass", "target": "body", "className": "gv-plugin-example" } ] } } ``` `$schema` 줄은 선택 사항이며 편집기를 위한 것입니다. 매니페스트의 JSON Schema를 가리키므로 작성하는 동안 검증과 자동 완성이 됩니다. `site.json`용은 `https://voyager.nagi.fun/site.schema.json`에 있습니다. `style.css`는 일반 CSS처럼 작성할 수 있지만, 플러그인 스타일은 자신의 `gv-plugin-*` 클래스 아래에 두는 것을 권장합니다. ```css .gv-plugin-example .some-target { max-width: 880px; } ``` ## Manifest 주의사항 - `id`는 `your-name.reading-width`처럼 작성자 접두사나 역도메인 스타일을 사용해 충돌을 피하세요. - `matches`는 좁게 유지하고, 플러그인이 실제로 필요한 사이트만 포함하세요. - `matches`에 여러 호스트를 나열할 수 있는 것은 해당 사이트의 `site.json`이 모두 포함할 때뿐입니다(예: chatgpt.com과 chat.openai.com). 다른 사이트를 다루려면 그 사이트 폴더에 플러그인을 추가하세요. - `category`는 `render-fix`, `theme`, `layout`, `readability`, `productivity`, `integration`, `other`를 권장합니다. - 필요한 플러그인 엔진 버전을 `engine`에 명확히 적으세요. 공식 플러그인을 예시로 참고할 수 있습니다. - `i18n`에는 가능하면 중국어, 영어, 기타 자주 쓰이는 언어의 이름, 설명, 설정 문구를 추가하세요. ## CSS와 리소스 제한 선언형 플러그인은 신뢰할 수 없는 입력으로 검증되므로 리소스를 자체 포함 형태로 유지하세요. - `@import`를 사용하지 마세요. - 원격 이미지, 외부 폰트, 원격 CSS를 참조하지 마세요. - 일반 CSS, 사용자 정의 속성, Voyager가 제공하는 설정값 치환은 사용할 수 있습니다. - 클래스 이름은 `gv-plugin-` 접두사를 사용해 호스트 사이트나 Voyager 자체 스타일을 오염시키지 않도록 하세요. 설정이 필요하다면 먼저 숫자 설정을 사용하는 것을 권장합니다. 예를 들어 읽기 폭 플러그인은 설정값을 CSS 변수로 쓰고 CSS에서 그 값을 사용할 수 있습니다. ## DOM 작업 범위 현재 선언형 플러그인은 다음 작업을 지원합니다. - `addClass`: 대상 요소에 클래스를 추가합니다. - `setAttribute`: 속성을 설정합니다. - `setStyle`: 인라인 스타일 또는 CSS 변수를 설정합니다. 값 끝에 `!important`를 붙이면 그 우선순위로 적용됩니다. 이전 Voyager 버전은 이런 값을 버리므로, 플러그인이 이 기능에 의존한다면 `engine`을 `>=1.9.0`로 설정하세요. `margin` 같은 축약 속성은 개별 longhand 단위로 추적되므로, 한 플러그인을 꺼도 다른 플러그인의 `margin-top`이 지워지지 않습니다. - `hide`: 대상 요소를 숨깁니다. 대상은 CSS 선택자이거나 위에 나열한 의미 키를 `{ "kind": "semantic", "key": "userTurn" }` 형태로 쓴 것일 수 있습니다. 의미 키는 보통 더 안정적이지만, 현재 사이트 어댑터가 그 키를 선언해야 합니다. 선언형 작업은 되돌릴 수 있고 반복 실행해도 안전해야 합니다. 한 번뿐인 페이지 상태에 의존하지 말고, DOM이 항상 그대로라고 가정하지 마세요. ### 프리미티브 CSS와 되돌릴 수 있는 DOM 수정만으로는 표현할 수 없는 동작도 있습니다. 프리미티브는 Voyager 안에 함께 배포되는 자체 코드이며, manifest는 `native` 작업으로 이름을 적어 호출할 수 있습니다. ```json { "engine": ">=1.3.0", "requires": { "handlers": ["formulaCopy"] }, "contributes": { "domOps": [{ "op": "native", "handler": "formulaCopy", "params": {} }] } } ``` manifest는 프리미티브를 고르고 설정할 뿐 로직을 담지 않습니다. `params`는 실행 전에 해당 프리미티브가 직접 검증합니다. 프리미티브를 쓰는 플러그인에는 두 가지 규칙이 있습니다. - 플러그인이 매칭하는 모든 사이트가 그 프리미티브가 읽는 시맨틱 키를 선언해야 합니다(예: `mermaid`는 `assistantTurn`과 `codeBlock`을 읽습니다). `requires.handlers`는 선택 사항입니다. `native` 작업이 이미 그 프리미티브를 가리키기 때문입니다. - `engine`은 그 프리미티브가 처음 들어간 엔진 버전 이상이어야 합니다. `formulaCopy`는 엔진 1.3.0에 들어갔으므로 이를 쓰는 플러그인은 `">=1.3.0"`으로 선언합니다. 범위가 더 낮으면 `bun run catalog:build`가 실패합니다. 예전 Voyager에서는 업데이트를 안내하는 대신 handler가 없다고 보고하기 때문입니다. 프리미티브는 늘어나기만 합니다. 새 매개변수는 항상 선택이고, 호환성을 깨는 변경은 새 이름으로 나갑니다. 동작이 바뀐 플러그인은 한 줄짜리 `changelog`도 넣을 수 있고, popup이 버전 옆에 보여줍니다. 번역은 `name`, `description`과 나란히 `i18n..changelog`에 씁니다. ## 일반 플러그인에 적합하지 않은 경우 JavaScript 실행, 요청 가로채기, Voyager 내부 데이터 읽기/쓰기, 복잡한 런타임 로직이 필요한 기능은 일반 선언형 플러그인에 적합하지 않습니다. 이런 기능은 먼저 Issue를 열어 요구사항을 설명하세요. 내장 기능이 꼭 필요하다면 Formula Copy처럼 Voyager 메인 저장소의 builtin/native 플러그인으로 구현하는 것을 검토할 수 있습니다. ## PR 전 확인 `bun run plugin:check <플러그인 디렉터리>`를 실행하고 그 출력을 PR에 붙여 넣으세요. 매니페스트와 CSS, `matches`가 사이트 범위를 벗어나는지, 프리미티브, 의미 선택자 키, 10개 언어 번역, README까지 모든 문제를 한 번에 알려 줍니다. CI도 모든 내장 플러그인에 같은 검사를 실행합니다. - 플러그인은 기본적으로 꺼져 있고 사용자가 직접 켭니다. - 거의 동일한 기존 플러그인이 없는지 확인했습니다. 있다면 기존 플러그인 개선을 우선했습니다. - 대상 사이트의 라이트/다크 테마에서 테스트했습니다. - `matches`가 관련 없는 사이트를 포함하지 않습니다. - 원격 리소스 참조가 없습니다. - 플러그인 디렉터리에 `plugin.json`, 필요한 CSS 파일, 짧은 README가 있습니다. - 공식 플러그인이라면 디렉터리가 `catalog/sites//plugins//`에 있고, `matches`가 사이트의 `matches` 안에 있으며, `catalog/marketplace.json`에 등록되어 있습니다(Voyager와 함께 배포되는 Gemini와 AI Studio 플러그인은 제외). - PR 설명에 테스트 페이지, 스크린샷 또는 녹화, 영향을 받을 페이지 영역을 적었습니다. 단순하고 절제되며 되돌릴 수 있게 유지하세요. 하나의 분명한 문제를 해결하는 플러그인이 보통 더 쉽게 병합되고 유지보수됩니다.