junsik/python-daouoffice-bot
GitHub: junsik/python-daouoffice-bot
这是一个通过逆向工程实现的非官方Python SDK,用于与无开放API的「多维Office」消息系统进行机器人交互。
Stars: 0 | Forks: 0
# python-daouoffice-bot
다우오피스(DaouOffice) 메신저용 비공식 봇 SDK.
공식 봇 API가 없는 다우오피스 메신저를, **PC 메신저가 쓰는 REST API를 역분석**하여 파이썬에서 다룰 수 있게 합니다. 방 목록 조회·메시지 송수신·폴링 기반 응답을 지원하며, 메시지 핸들러(`on_message`) 안에서 원하는 로직(LLM 포함)을 자유롭게 붙입니다 — SDK 자체는 LLM을 번들하지 않습니다.
[](https://github.com/junsik/python-daouoffice-bot/actions/workflows/ci.yml)  
## 安装
아직 PyPI에 게시 전입니다. 봇 프로젝트의 의존성으로 Git에서 직접 추가하세요:
```
uv add "git+https://github.com/junsik/python-daouoffice-bot"
# 或:pip install "git+https://github.com/junsik/python-daouoffice-bot"
```
특정 릴리스에 고정하려면 태그를 붙입니다:
```
uv add "git+https://github.com/junsik/python-daouoffice-bot@v0.1.0"
```
이 저장소 자체를 개발하려면 소스에서:
```
git clone https://github.com/junsik/python-daouoffice-bot
cd python-daouoffice-bot
uv sync # dev 의존성 포함
```
## 入门:`daoubot login` → 配置文件
봇은 백그라운드 데몬입니다. 처음 한 번 로그인하면 회사·사용자 정보, 세션 토큰, **비밀번호**가 `~/.daoubot/profile.yaml`(홈 디렉터리, 실행 위치 무관 — `~/.aws`/`~/.docker` 와 같은 방식)에 저장되고, 이후 코드/명령은 어느 디렉터리에서 실행하든 그 프로필을 자동으로 씁니다. 데몬이 무인 자동 재로그인하려면 비밀번호가 필요하므로 저장하는 것이며 — 파일은 `chmod 600`·`.daoubot/` gitignore, 화면 출력 시에는 항상 `****` 로 마스킹합니다. `company_id` 를 안 주면 공개 엔드포인트로 자동 탐색합니다.
```
# 若省略 --login-id 则提示输入登录 ID,若省略 --password 则提示输入密码
# (按此顺序)通过提示符输入 — 密码为隐藏输入,不会出现在 argv 或 shell
# 历史记录中,也无 ! 等特殊字符的引号问题:
daoubot login --base-url https://yourcompany.daouoffice.com
# → 保存至 ~/.daoubot/profile.yaml(令牌与密码在屏幕上仅显示为 ****)
```
한 호스트에서 여러 봇/테넌트를 쓰려면 `--config <경로>` 로 프로필 파일을 분리합니다 — 옵션은 **서브커맨드 뒤**에 옵니다(`daoubot login --config X ...`, `daoubot rooms --config X`).
### 无人(后台)运行
AccessToken 은 약 30분 뒤 만료됩니다. `daoubot login` 한 번이면 30일짜리 RefreshToken 과 비밀번호가 프로필에 함께 저장돼, 봇/CLI 는 401 을 받으면 **(1) RefreshToken 으로 새 AccessToken 만 빠르게 발급**받고, 그게 실패할 때만 **(2) 비밀번호로 풀 재로그인**합니다 — 새 토큰은 프로필에 다시 씁니다. 추가 설정 없이 무인 운영됩니다. (`DAOU_PASSWORD` 환경 변수로 오버라이드 가능, 예: systemd `EnvironmentFile`.) RefreshToken 도 비밀번호도 전혀 없을 때만 만료 시 명확한 에러로 멈춥니다(사용자에게 재로그인을 강제하지 않음).
모든 연결값(비밀번호 포함)은 **명시 인자 > `DAOU_*` 환경 변수 > 앱 설정(`DAOU_APP_CONFIG`) > 프로필** 순으로 해석됩니다. SDK는 `.env` 파일을 자동으로 읽지 않으니, 환경 변수로 오버라이드하려면 셸에 직접 export 하거나 systemd EnvironmentFile 을 쓰세요. 다운스트림 앱(예: dt-agent)이 자기 `agent.yaml` 의 `daouoffice:` 섹션에 연결값을 선언하면 SDK 가 그걸 **읽기 전용**으로 사용합니다 — 토큰·identity 는 여전히 프로필이 관리(파일 분리).
| 환경 변수 | 설명 |
|---|---|
| `DAOU_BASE_URL` | 테넌트 URL (`https://회사.daouoffice.com`) — 로그인 필수 |
| `DAOU_COMPANY_ID` | 숫자 회사 id — 생략 시 `daoubot login` 이 공개 엔드포인트로 자동 탐색 |
| `DAOU_LOGIN_ID` | 봇 계정 로그인 id — 로그인 필수 |
| `DAOU_PASSWORD` | 봇 계정 비밀번호 — 무인 자동 재로그인용. 프로필에 저장되므로 한 번 `daoubot login` 했으면 다시 설정할 필요 없음 |
| `DAOU_APP_CONFIG` | 다운스트림 앱의 YAML 경로(예: `agent.yaml`). 그 파일의 top-level `daouoffice:` 섹션에서 `base_url`/`company_id`/`login_id`/`password` 를 **읽기만** 함. SDK 가 그 파일에 절대 쓰지 않음 — 토큰·identity 는 프로필이 따로 관리. 우선순위는 위 4개 사이 (env 보다 아래, 프로필 보다 위). CLI 는 `--app-config ` 로도 가능 |
| `DAOU_LOG_LEVEL` | `daouoffice` 패키지 로거 레벨(`DEBUG`/`INFO`/`WARNING`/…). 연결값 아님. 미설정 시 앱 로깅 설정을 따름(라이브러리는 root/basicConfig 를 건드리지 않음). 메시지 본문·발신자 로그는 기본적으로 `DEBUG` 라 기본 `INFO` 에서는 **대화 내용이 로깅되지 않음** — 진단 시 `DEBUG` 로 켜고, 더 조용히 하려면 `WARNING` |
위 6개(`DAOU_` 접두사)가 **SDK가 읽는 환경 변수의 전부**입니다. 개별 예제가 자체적으로 쓰는 변수(LLM 키, 대상 방 id 등)는 각 예제의 docstring 에 적혀 있습니다 — SDK 코어와 무관하므로 여기서 다루지 않습니다.
**다운스트림 앱과의 통합 (예: dt-agent의 `agent.yaml`)**: 앱이 이미 자기 설정 YAML 을 들고 있으면, 거기에 `daouoffice:` 섹션 하나 더 붙이고 SDK 에 그 파일을 가리키면 됩니다. SDK 는 읽기만 하므로 앱이 자기 파일 포맷·주석을 그대로 유지합니다. 토큰/identity 는 여전히 `~/.daoubot/profile.yaml` (또는 `--config` 지정 경로) 에 SDK 가 자동 관리.
```
# agent.yaml(应用程序内置的现有文件)
daouoffice:
base_url: https://yourcompany.daouoffice.com
login_id: yourbot
password: <비밀번호> # 또는 DAOU_PASSWORD env 로(env 가 더 우선)
# company_id 는 생략 가능 (자동 탐색)
```
```
bot = DaouBot(on_message=on_message, app_config="agent.yaml")
# 或环境变量:DAOU_APP_CONFIG=/etc/myapp/agent.yaml python bot.py
```
## 快速开始
`daoubot login` 후, 봇 코드는 연결 설정이 필요 없습니다 — 프로필에서 자동 해석됩니다:
```
import asyncio
from daouoffice import DaouBot, NewMessage
async def on_message(msg: NewMessage) -> str | None:
if "안녕" in msg.message_text:
return f"안녕하세요, {msg.sender_name}님!"
return None # 응답 안 함
async def main():
bot = DaouBot(on_message=on_message) # 프로필/환경에서 자동 해석
await bot.run_forever() # Ctrl-C / SIGTERM 시 graceful 종료
asyncio.run(main())
```
`on_message` 가 문자열을 반환하면 답장, `None` 이면 무응답입니다. 답장은 그 핸들러를 유발한 메시지에 대한 **인용 회신(threaded reply)** 으로 자동 연결되므로(다우오피스 답장 UI 와 동일), 바쁜 방에서도 무엇에 대한 답인지 분명합니다. 핸들러를 주지 않으면 봇은 메시지를 읽기만 합니다(답장 안 함). 30분 만료 시 RefreshToken/자격증명이 있으면 자동 갱신·재로그인합니다.
**멘션:** 다우오피스 멘션은 본문 인라인 토큰입니다(전체 공개, 비공개 아님 — [docs/api/03-messages.md](docs/api/03-messages.md) §3.6). SDK가 파싱해 `msg.mentions` / `msg.mentions_me` / `msg.mention_all` 와 사람이 읽는 `message_text`(토큰 → `@이름`), 원본 `raw_text` 를 제공합니다. 바쁜 그룹에서 멘션 시에만 응답하려면 `only_when_mentioned(handler)` 로 감싸세요(글로벌 노브 아님 — 정책은 선언으로). 봇이 별명(예: `@디티`/`@DT`) 으로도 불릴 수 있어야 하면 `only_when_addressed(handler, aliases=("디티","DT"))` — 진짜 멘션 + 평문 `@별명` 모두 통과(별명은 인증 안 됨, 권한 게이트로 쓰지 말 것).
```
bot = DaouBot(..., on_message=only_when_mentioned(handle))
```
**마크다운 스타일 (`markdown=True`):** 채팅은 작은 HTML 부분집합만 렌더합니다 — 볼드·이탤릭·링크·번호목록·블릿목록(라이브 캡처로 확인). `DaouBot(..., markdown=True)` 면 핸들러가 반환한 마크다운을 엔진이 전송 전 자동 변환합니다(`**굵게**`/`*기울임*`/`[텍스트](url)`/`1.`/`-`). 기본은 비활성(그대로 전송). 부분집합 밖 문법(헤딩·코드)은 태그 대신 원문 그대로 degrade 하고, 텍스트는 HTML escape, 링크 href 는 `"` 까지 escape 되어 깨짐/주입이 없습니다. 직접 변환은 `to_chat_html(text)`. 계약·태그표는 [docs/api/03-messages.md](docs/api/03-messages.md) §3.1.
**파일 첨부 (예: LLM 뉴스레터):** 긴 MD/HTML *문서*는 인라인 렌더되지 않습니다(위 부분집합 밖). `bot.send_file(room_id, "news.md", "이번 주 뉴스레터")` 로 업로드 → 첨부로 전송(수신자 다운로드). `BotClient.upload_attachment()` + `send_message(..., attachments=[...])` 분해도 가능. 첨부 계약은 SAZ 기반이며 **라이브 미검증**입니다([docs/api/03-messages.md](docs/api/03-messages.md) §3.7).
**재시작 복구:** "어디까지 처리했는지"(방별 마지막 메시지 id)는 기본적으로 `~/.daoubot/cursors.json` 에 저장됩니다 — 봇이 재시작해도(어느 디렉터리에서 실행하든) 백로그를 다시 처리하거나 다운타임 메시지를 건너뛰지 않고 이어받습니다. 비영속을 원하면 `DaouBot(..., cursor_store=MemoryCursorStore())`. 단, 폴링 특성상 따라잡기는 방당 최근 ~100개 히스토리 창 안으로 제한됩니다(그보다 오래 다운되면 창 밖 메시지는 복구 불가 — "since id" 엔드포인트가 없음).
**전달 보장:** 엔진은 **at-least-once** 를 보장합니다 — 메시지 전달의 업계 표준(Kafka/SQS/Slack/Telegram)이라 노브로 노출하지 않고 SDK가 책임집니다. 핸들러가 예외 없이 끝날 때까지 방 내 순서대로 재전달하며, 같은 메시지가 `max_attempts`(기본 5)회 실패하면 poison으로 건너뜁니다.
- 중복이 의미를 가지면 **핸들러를 멱등하게** 작성하세요 (Kafka/SQS 교리). 트랜스포트 dedup은 엔진, 비즈니스 멱등성은 핸들러 책임.
- fire-and-forget(재시도 원치 않음)은 별도 모드가 아니라 **핸들러가 자기 예외를 삼키면**(실패로 안 침) 자연히 표현됩니다.
## 命令行界面
```
daoubot login ... # 인증 + 프로필 저장 (위 참고)
daoubot whoami # 저장된 봇 신원 출력
daoubot config # 저장된 프로필 보기(시크릿 마스킹)
daoubot config set base_url # 연결 항목 수정 (password 는 값 생략 시 숨김 입력)
daoubot config path # 프로필 파일 경로 출력
daoubot rooms # 채팅방 목록 (room id 포함)
daoubot room create --users a,b --name "Bot Test" [--type GROUP]
daoubot room open # 방 상세 + 구성원
daoubot send "" # 메시지 전송
daoubot login --config bots/a.json ... # 프로필 파일 위치 분리(멀티 봇/테넌트)
```
CLI는 온보딩·조회·단발 전송용입니다. **봇 실행은 CLI가 아니라** `DaouBot(on_message=...)` 를 담은 파이썬 스크립트(`python my_bot.py`)입니다 — CLI는 핸들러를 실을 수 없어 별도 `start` 명령을 두지 않습니다(아래 [빠른 시작](#빠른-시작)·`examples/`).
`--login-id`/`--password` 를 생략하면 (이 순서로) 프롬프트로 입력받습니다(비밀번호는 숨김 — argv·히스토리 노출 방지).
개발자는 `login` → `rooms`/`room create` 로 필요한 `company_id`·`user_id`·`room_id` 를 손에 넣은 뒤, 그 값들로 SDK 봇을 작성하면 됩니다. (설치 없이: `uv run python -m daouoffice.cli rooms`)
## 示例
`examples/` 의 각 봇은 `DAOU_*` 환경 변수만 설정하면 그대로 실행됩니다:
| 예제 | 설명 |
|---|---|
| `bot-echobot` | 받은 메시지를 그대로 반복 |
| `bot-command` | `/cmd args` 명령 디스패처 (help/echo/whoami; 접두사는 `BOT_CMD_PREFIX` 로 변경) |
| `bot-attachment` | `!report` → 그 자리에서 .md 생성해 파일 첨부로 답장 (`send_file`) |
| `bot-conversation` | 방별 상태 머신 대화 |
| `bot-assistant` | 핸들러에서 OpenAI 호환 LLM 호출 (LLM_* env 필요) |
| `bot-router` | 방별 핸들러 분기 (등록한 방만 처리하는 allowlist) |
| `bot-error-handler` | 핸들러 예외를 잡아 개발자 방에 알림 |
| `bot-room-saver` | `RoomRouter` 로 지정한 방만 JSONL로 저장(응답 안 함; 방 id는 `daoubot rooms`) |
```
uv run --with python-daouoffice-bot examples/bot-echobot/bot.py
```
## 使用 AI 创建机器人(智能体技能)
표준 **에이전트 스킬**(`SKILL.md` + frontmatter + 번들 파일)이 저장소에 포함돼 있어, AI에게 "다우오피스 봇 만들어줘"라고 시켜 스캐폴딩·확장할 수 있습니다. 특정 도구 전용이 아니라 이 스킬 포맷을 지원하는 어떤 에이전트 런타임(Claude.ai·Claude Code·Claude API/Agent SDK 등)에도 그대로 이식됩니다.
```
skills/daouoffice-bot/ # SKILL.md + reference.md + scaffold.py
```
설치 — 에이전트의 스킬 디렉터리에 폴더를 놓으면 됩니다. Claude 계열 런타임은 `~/.claude/skills/`(전역), 다른 런타임은 각자의 스킬 로더 규약을 따르세요:
```
cp -r skills/daouoffice-bot ~/.claude/skills/ # 수동 복사
# 或
npx skills add junsik/python-daouoffice-bot --skill daouoffice-bot
```
### 应用方法(安装后)
스킬은 별도 명령이 아니라 **요청 내용으로 자동 발동**합니다. 봇을 만들 작업 폴더에서 스킬 포맷을 지원하는 에이전트(Claude Code·Claude.ai·Claude Desktop·Agent SDK 등)를 열고, 그냥 평소처럼 말하면 됩니다:
```
다우오피스 봇 만들어줘. 우리 회사는 acme.daouoffice.com 이고,
#개발-알림 방에서 !배포 명령에만 응답하면 돼.
```
그러면 스킬이 잡혀서 — (1) 부족한 정보(테넌트·전용 계정·대상 방·트리거·상태·부작용)를 **되묻고**, (2) 결정 매트릭스로 설계를 정한 뒤, (3) `scaffold.py` 로 보일러플레이트를 깔고 핸들러를 구현하고, (4) SDK 불변규칙을 지키며, (5) `daoubot login`/`send` 로 라이브 스모크까지 안내합니다. ("스킬 써"라고 명시할 필요 없음 — `다우오피스 봇`/`DaouBot`/`daoubot` 같은 표현이면 발동합니다.)
설치 위치별 적용 범위:
- `~/.claude/skills/daouoffice-bot/` → **모든 프로젝트**에서 발동(권장).
- 특정 봇 프로젝트의 `<그_프로젝트>/.claude/skills/daouoffice-bot/` → 그 프로젝트에서만. *이 SDK 저장소 자체의 `.claude/` 에는 넣지 마세요* — 거긴 SDK 개발용입니다.
- 스킬은 템플릿 메뉴가 아니라 **설계 가이드**입니다 — AI가 요구사항을 인터뷰하고(테넌트·대상 방·트리거·상태·부작용), 결정 매트릭스로 프리미티브(`on_message`/`RoomRouter`/`only_when_mentioned`/상태/LLM)를 조합해 사용자가 원하는 봇을 만들도록, SDK 불변규칙(계정 전역 read·allowlist·멱등성·없는 API 날조 금지)과 함께 가르칩니다.
- `scaffold.py` 는 유스케이스를 추측하지 않고 **올바른 보일러플레이트만** 출력합니다(env/프로필 연결 + graceful run + 빈 핸들러). 설계는 AI가 요구사항에서 결정: `python skills/daouoffice-bot/scaffold.py > bot.py`
## SDK 概述
| 심볼 | 설명 |
|---|---|
| `BotClient` | REST API 래퍼 (로그인·방·메시지·첨부·`whoami`·`discover_company`) |
| `BotEngine` | 폴링 엔진 (단일 구현, async) |
| `DaouBot` | 고수준 봇 (`on_message` + 폴링 + 401 자동 재로그인) |
| `RoomRouter` | 방별 핸들러 분기 (등록한 방만 처리, 나머지 무시) |
| `only_when_mentioned` | 봇 멘션(`@봇`/`@전체`) 시에만 핸들러 실행 |
| `only_when_addressed` | 위 + 별명도 인식 (`@별명` 평문, `aliases=(...)`). 별명은 평문이라 권한 게이트로는 부적합 |
| `load_settings` / `Settings` | 연결설정 해석(인자>env>프로필); `DaouBot()` 이 내부 사용 |
| `FileCursorStore` / `MemoryCursorStore` | 처리 위치 영속/비영속 저장 |
| `NewMessage` | 정규화된 수신 메시지 |
| `BotIdentity` | 로그인 시 해석된 봇 자신의 신원 |
| `Profile` | `daoubot login` 이 저장하는 프로필 (`load_profile`) |
| `DaouAuthError` / `DaouConfigError` | 예외 |
전달은 **폴링만** 사용합니다. WebSocket(`GET /ws/pc`, STOMP)은 캡처에서 엔드포인트만 관측됐을 뿐 흐름을 검증하지 못해 **구현하지 않았습니다** (미검증 RE 메모: [docs/api/04-websocket.md](docs/api/04-websocket.md)).
## 项目结构
```
src/daouoffice/ SDK 패키지 (import daouoffice)
examples/ 실행 가능한 예제 봇 (echobot/command/attachment/
conversation/assistant/router/error-handler/room-saver)
skills/daouoffice-bot/ 배포용 에이전트 스킬 (SKILL.md + reference.md + scaffold.py)
docs/ ARCHITECTURE.md (설계 근거) + api/ (역분석 엔드포인트 레퍼런스)
tools/ SAZ 캡처 분석 스크립트 (개발용)
tests/ pytest (네트워크는 respx로 목)
```
## 开发
```
uv sync --extra dev
uv run ruff check .
uv run ruff format --check .
uv run pytest -q
```
설계 배경과 다이어그램은 [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md), 기여 가이드는 [CONTRIBUTING.md](CONTRIBUTING.md), 변경 이력은 [CHANGELOG.md](CHANGELOG.md)를 참고하세요.
## 许可证
[MIT](LICENSE) © junsik
标签:API逆向分析, Python, REST API, SDK开发, 云资产清单, 企业软件, 办公自动化, 即时通讯, 后台服务, 多办公消息器, 安全存储, 操作系统检测, 无后门, 机器人开发, 消息处理, 消息监听, 自动化响应, 轮询引擎, 逆向工具, 逆向工程, 非官方SDK, 韩国软件