이 튜토리얼은 처음부터 첫 DeepSeek Harness 플러그인을 개발합니다. 소스 저장소 안에 플러그인 모듈을 만들고, patch 설정으로 등록하고, Web 인터페이스를 시작해 로드를 확인한 뒤, 라이프사이클 정리, 의존성 주입, 세 가지 플러그인 형태를 익힙니다. 완료하면 당신의 기능을 Harness에 연결할 수 있습니다.
사전 준비
시작하기 전에 다음을 확인하세요:
- 공식 README의 run-from-source 절차에 따라
deepseek-ai/deepseek-harness저장소를 checkout하고 의존성을 설치했습니다; pnpm사용 가능 — Harness가 의존성과 시작 명령을 관리하는 데 사용합니다;- TypeScript를 작성할 수 있는 에디터.
플러그인 프로젝트 생성
저장소 루트에 scratch-plugin 디렉터리를 만들고, 플러그인 코드는 src 아래에 둡니다:
cd /path/to/deepseek-harness
mkdir -p scratch-plugin/src
최종 구조는 다음과 같으며, 이 튜토리얼의 모든 파일이 여기에 들어갑니다:
scratch-plugin/
├── cordis.yml
└── src/
└── my-plugin.ts
첫 플러그인 작성
scratch-plugin/src/my-plugin.ts를 만듭니다:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello-plugin'
export function apply(ctx: Context) {
console.log('[hello-plugin] plugin loaded!')
}
플러그인은 본질적으로 apply 함수를 내보내는 TypeScript 모듈입니다. 프레임워크는 플러그인을 로드할 때 apply를 호출하고 ctx 컨텍스트를 전달합니다. 플러그인과 Harness의 모든 상호작용이 이것을 통해 이루어집니다. name은 플러그인의 고유 식별자로, 로그와 의존성 관리에 사용됩니다.
name과 apply 규칙
플러그인을 작성할 때 세 가지 규칙을 지키면 대부분의 로드 문제를 피할 수 있습니다:
name은 고유해야 하고 kebab-case여야 합니다: 모두 소문자, 단어는 하이픈으로 연결합니다(예:hello-plugin). 전역 식별자이므로 이름이 겹치면 로드 충돌이 발생하고, 로그와 의존성 관리가 이것으로 위치를 찾습니다.apply는 플러그인 범위의 Context를 받습니다: 전달되는ctx는 이 플러그인 전용 컨텍스트이며, 이것으로 등록한 리스너와 타이머는 플러그인 언로드 시 자동으로 정리되어 전역을 오염시키지 않습니다.- 플러그인 모듈은 시작할 때 한 번만 로드됩니다: 프레임워크는 시작할 때 모듈을 읽고 최상위 코드를 실행한 뒤 다시 로드하지 않습니다. 따라서 플러그인 파일을 고친 뒤에는 프로세스를 재시작해야 적용됩니다(마지막 문제 해결 절 참고).
플러그인 등록
Harness는 아직 이 파일의 존재를 모릅니다. patch 설정으로 등록해야 합니다. 먼저 저장소 루트에서 pwd를 실행해 절대 경로를 얻은 뒤, scratch-plugin/cordis.yml을 만듭니다:
- insert:
- id: hello
name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'
로드하고 확인하기
--patch로 방금 작성한 설정을 가리키고 Harness Web 인터페이스를 시작합니다:
pnpm dsh web --patch ./scratch-plugin/cordis.yml
# http://127.0.0.1:3080 열기
터미널 시작 시 플러그인 로드 로그가 출력됩니다. [hello-plugin] plugin loaded!가 보이면 플러그인이 성공적으로 로드된 것입니다. 브라우저에서 http://127.0.0.1:3080에 접속하면 인터페이스가 정상적으로 시작되었는지 확인할 수 있습니다.
라이프사이클과 자동 정리
플러그인이 자원(타이머, 리스너, 연결)을 가지고 있다면 언로드 시 해제해야 합니다. ctx.effect는 함수를 받고, 그 반환값이 정리 함수가 되어 플러그인 언로드 시 실행됩니다:
export const name = 'hello-plugin'
export function apply(ctx: Context) {
const timer = setInterval(() => {
console.log('[hello-plugin] heartbeat')
}, 5000)
// 반환된 정리 함수는 플러그인 언로드 시 실행됩니다
ctx.effect(() => {
return () => clearInterval(timer)
})
}
더 나아가, ctx로 등록한 리스너, 타이머 등은 프레임워크가 언로드 시 자동으로 정리해 주므로 직접 해제 코드를 작성할 필요가 없습니다. ctx.effect는 ctx를 거치지 않고 직접 만든 자원을 관리할 때 사용합니다.
의존성 주입
플러그인은 Harness의 내장 서비스(예: 도구 서비스 tools)가 필요할 때가 많습니다. inject로 의존성을 선언하면, 프레임워크는 이 서비스들이 준비될 때까지 기다렸다가 플러그인을 로드합니다:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello-plugin'
export const inject = ['tools']
export function apply(ctx: Context) {
// 여기서는 tools 서비스가 준비되어 안전하게 호출할 수 있습니다
ctx.tools.register(/* ... */)
}
inject가 없으면 플러그인이 즉시 로드되며, 이때 ctx.tools에 접근하면 아직 준비되지 않은 서비스를 받을 수 있습니다. 의존성을 선언하는 것이 이런 타이밍 문제를 없애는 표준적인 방법입니다.
세 가지 플러그인 형태
지금까지는 함수 형태만 사용했습니다. Harness는 객체 형태와 클래스 형태도 지원합니다:
// 객체 형태: 메타데이터와 로직을 하나의 기본 내보내기로 모음
export default {
name: 'hello-plugin',
inject: ['tools'],
apply(ctx: Context) {
console.log('[hello-plugin] loaded!')
},
}
// 클래스 형태: 다른 플러그인에 서비스를 제공할 때 사용
import { Service } from '@deepseek-ai/cordis'
export default class MyService extends Service {
static inject = ['tools']
constructor(ctx: Context) {
super(ctx, 'myService')
}
}
문제 해결
첫 플러그인 실행은 보통 네 가지 문제 중 하나에서 막힙니다. 아래 순서대로 하나씩 확인하세요.
플러그인이 로드되지 않음
시작 로그에 [hello-plugin] plugin loaded!가 보이지 않는다면, 십중팔구 cordis.yml의 경로가 잘못된 것입니다.
cd /path/to/deepseek-harness
pwd
# 출력 예: /Users/you/code/deepseek-harness
# 그 뒤에 scratch-plugin/src/my-plugin.ts를 붙입니다
Cannot find module 오류
터미널에 Cannot find module이 나타나면 원인은 둘 중 하나입니다. cordis.yml의 .ts 파일 경로를 잘못 적었거나(디렉터리 이름, 파일 이름, 대소문자를 글자 단위로 대조하세요), 파일에 문법 오류가 있어 모듈을 해석할 수 없거나. 먼저 경로를 대조하고, 그다음 에디터의 TypeScript 검사로 파일에 빨간 줄 오류가 없는지 확인하세요.
3080 포트가 이미 사용 중
시작할 때 포트 사용 중 오류가 나면, 다른 프로세스가 아직 3080을 듣고 있다는 뜻입니다. 보통은 깨끗하게 종료되지 않은 이전 Harness입니다. 먼저 점유 프로세스를 찾으세요:
lsof -i :3080
그 프로세스를 중지한 뒤 다시 시작하세요. 중지하고 싶지 않다면 다른 포트로 시작할 수도 있습니다.
코드를 고쳐도 효과가 없음
플러그인 모듈은 시작할 때 한 번만 로드됩니다. my-plugin.ts나 cordis.yml을 편집한 뒤에는 시작 명령을 재시작해야 변경이 보입니다:
pnpm dsh web --patch ./scratch-plugin/cordis.yml
개발 중 patch는 설정만 제공할 뿐 전역 profile에 기록하지 않으므로, 안심하고 반복 재시작하세요.
플러그인 개발 자주 묻는 질문
플러그인 경로는 왜 절대 경로여야 하나요?
patch 설정은 파일 시스템 경로로 모듈을 직접 로드하며, Harness는 설정 파일이나 저장소 루트를 기준으로 상대 경로를 해석하지 않습니다. pwd로 얻은 절대 경로를 name 필드에 넣어야 매번 시작할 때마다 플러그인 파일을 찾을 수 있습니다.
함수 형태, 객체 형태, 클래스 형태 중 어떻게 고르나요?
기본은 함수 형태입니다. 대부분의 플러그인은 name, inject, apply 함수만 내보내면 됩니다. 객체 형태는 메타데이터와 로직을 하나의 기본 내보내기로 모으고 싶을 때 적합합니다. 클래스 형태는 다른 플러그인에 서비스를 제공할 때(다른 사람이 당신의 기능을 inject할 때)만 Service를 상속해 사용합니다.
플러그인 코드를 고치면 재시작해야 하나요?
네. 플러그인 모듈은 시작할 때 한 번만 로드됩니다. my-plugin.ts나 cordis.yml을 수정한 뒤 pnpm dsh web --patch ./scratch-plugin/cordis.yml을 다시 실행하면 결과를 볼 수 있습니다. 개발 중 patch는 설정만 제공할 뿐 전역 profile에 기록하지 않으므로, 반복 재시작해도 부작용이 없습니다.
시작할 때 오류가 나고 플러그인이 로드되지 않으면 어떡하죠?
터미널 오류의 첫 줄을 먼저 보세요. 실패의 대부분은 두 가지입니다. cordis.yml의 경로가 절대 경로가 아니거나, my-plugin.ts에 문법 오류가 있거나. 이 튜토리얼 마지막의 「문제 해결」 절을 하나씩 대조해 고치고 다시 시작하세요.
Context의 TypeScript 타입 힌트를 받으려면 어떻게 하나요?
@deepseek-ai/cordis에서 타입을 가져와 apply 매개변수에 표시하세요. import type { Context }를 한 뒤 apply(ctx: Context)로 작성합니다. inject로 선언한 서비스(예: tools)는 ctx의 속성으로 타입 힌트가 제공되어, 에디터에서 바로 자동 완성할 수 있습니다.