junsik/python-daouoffice-bot

GitHub: junsik/python-daouoffice-bot

这是一个通过逆向工程实现的非官方Python SDK,用于与无开放API的「多维Office」消息系统进行机器人交互。

Stars: 0 | Forks: 0

# python-daouoffice-bot 非官方的 DaouOffice Messenger Bot SDK。 DaouOffice Messenger 没有官方的 Bot API,本项目通过**逆向分析 PC 客户端使用的 REST API**,使其能够在 Python 中进行操作。支持房间列表查询、消息收发以及基于轮询的响应,你可以在消息处理程序(`on_message`)中自由添加所需的逻辑(包括 LLM)——SDK 本身并不打包 LLM。 [![CI](https://static.pigsec.cn/wp-content/uploads/repos/cas/39/39faa54be350a1dab8afd3b2fb8c1c83e4d9cff84abfef2374d19a18053687c4.svg)](https://github.com/junsik/python-daouoffice-bot/actions/workflows/ci.yml) ![Python](https://img.shields.io/badge/python-3.12%2B-blue) ![License](https://img.shields.io/badge/license-MIT-green) ## 与普通 Messenger Bot 有何不同(本项目的存在理由) Telegram、Slack 和 Discord 都拥有**官方的 Bot 平台**——注册 Bot、获取 token/OAuth,然后通过 webhook 或事件推送接收消息。而 DaouOffice **没有这些**。它不存在官方的 Bot API。但是,使用 DaouOffice 的组织同样对 ChatOps、通知和助手 Bot 有强烈需求。唯一的途径就是**逆向分析 PC 客户端使用的私有 REST API**。这个过程充满了非显性的运营陷阱(见下文),本项目将其封装在一处,以免各个团队重复踩坑。 核心在于,**创建 Bot 的流程本身就截然不同**: | | Telegram/Slack/Discord | DaouOffice (本 SDK) | |---|---|---| | Bot 注册 | BotFather / 应用注册 + OAuth / 开发者门户 | **无。** 管理员签发**普通用户账号** | | 认证 | Bot token / OAuth scope | 使用账号 `loginId`/`password` 登录(会话约 30 分钟,自动重新登录) | | “连接”到房间 | 邀请 Bot + 权限/Scope,按频道安装 | 将该账号作为成员加入房间**即完成**——成员关系即为连接 | | 消息接收 | Webhook / Events / Gateway 推送 | **无推送**——像 PC 客户端一样轮询 REST API | | 权限模型 | 受限的 Bot token | 完全继承该账号的权限(因此*必须使用专用*账号) | | 官方支持与稳定性 | 有文档的稳定 API | 私有逆向分析——服务器更改可能会导致失效 | | 租户模型 | 单一平台 | 公司专属的 SaaS 子域名——不对任何值进行硬编码 | 流程对比: - **Telegram** — 通过 BotFather 创建 Bot → 签发 token → 邀请至房间 → Webhook/`getUpdates` - **DaouOffice** — 向管理员申请自动化专用账号 → 像桌面客户端一样登录 → 作为成员添加至房间 → 轮询 也就是说,既不需要“签发 token”,也不需要“注册应用”或“设置 webhook”。 在这种模型下衍生出了普通 Bot SDK 所没有的运营限制,而本 SDK 已经吸收了这些限制:基于账号的已读状态(必须使用专用账号)、基于轮询的 at-least-once 与重启 cursor、群组防刷白名单、内联 mention token 解析、30 分钟 token 自动重新登录。设计理由请参考 [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)。 准备工作: 1. DaouOffice 租户 URL — `https://<公司>.daouoffice.com` 2. 自动化专用账号(由管理员签发) (租户的数字 `companyId` 会由 `daoubot login` 通过公开端点自动获取,因此无需提前准备——必要时可使用 `--company-id` 直接指定。) ## 安装 尚未发布至 PyPI。请通过 Git 直接将其作为依赖项添加到你的 Bot 项目中: ``` 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` → profile Bot 是一个后台 daemon。只需登录一次,公司/用户信息、session token 以及**密码**就会被保存到 `~/.daoubot/profile.yaml`(位于主目录,与执行位置无关——类似于 `~/.aws`/`~/.docker`)。此后,无论在哪个目录下执行代码或命令,都会自动读取该 profile。之所以保存密码,是因为 daemon 进行无人值守自动重新登录时需要它——该文件会设置 `chmod 600` 权限并加入 `.daoubot/` gitignore,在屏幕输出时也始终以 `****` 掩码显示。如果不提供 `company_id`,会通过公开端点自动探测。 ``` # 如果省略 --login-id 则输入登录 id,如果省略 --password 则输入密码 # (按此顺序)通过 prompt 输入 — 密码为隐藏输入,因此不会留在 argv·shell # 历史记录中,也没有 ! 等特殊字符的引用问题: daoubot login --base-url https://yourcompany.daouoffice.com # → 保存到 ~/.daoubot/profile.yaml(token·密码在屏幕上仅以 **** 显示) ``` 如果要在同一台主机上使用多个 Bot/租户,可以使用 `--config ` 分离 profile 文件——该选项需放在**子命令之后**(`daoubot login --config X ...`, `daoubot rooms --config X`)。 ### 无人值守(后台)运营 AccessToken 约 30 分钟后过期。只要执行过一次 `daoubot login`,有效期为 30 天的 RefreshToken 和密码就会一同保存在 profile 中。当 Bot/CLI 收到 401 错误时,**(1) 首先使用 RefreshToken 快速签发新的 AccessToken**,仅当其失败时,才会 **(2) 使用密码进行完整的重新登录**——新 token 会被重新写回 profile。无需额外配置即可实现无人值守运营。(可通过 `DAOU_PASSWORD` 环境变量覆盖,例如用于 systemd `EnvironmentFile`。)仅当 RefreshToken 和密码都不存在时,才会在过期时抛出明确的错误并停止(不会强制要求用户重新登录)。 所有连接配置(包括密码)的解析优先级依次为:**显式参数 > `DAOU_*` 环境变量 > 应用配置 (`DAOU_APP_CONFIG`) > profile**。SDK 不会自动读取 `.env` 文件,若要使用环境变量进行覆盖,请直接在 shell 中 export 或使用 systemd EnvironmentFile。如果下游应用(例如 dt-agent)在其 `agent.yaml` 的 `daouoffice:` 配置段中声明了连接信息,SDK 会以**只读**方式使用它——token 和 identity 依然由 profile 管理(实现文件分离)。 | 环境变量 | 说明 | |---|---| | `DAOU_BASE_URL` | 租户 URL (`https://公司.daouoffice.com`) — 登录必填 | | `DAOU_COMPANY_ID` | 数字公司 ID — 如省略,`daoubot login` 会通过公开端点自动探测 | | `DAOU_LOGIN_ID` | Bot 账号登录 ID — 登录必填 | | `DAOU_PASSWORD` | Bot 账号密码 — 用于无人值守自动重新登录。因为会保存在 profile 中,只要执行过一次 `daoubot login` 就无需再次设置 | | `DAOU_APP_CONFIG` | 下游应用的 YAML 路径(例如 `agent.yaml`)。**只读取**该文件 top-level 的 `daouoffice:` 配置段中的 `base_url`/`company_id`/`login_id`/`password`。SDK 绝不会写入该文件——token 和 identity 另外由 profile 管理。优先级位于上述 4 项之间(低于 env,高于 profile)。CLI 也可通过 `--app-config ` 指定 | | `DAOU_LOG_LEVEL` | `daouoffice` 包 logger 的级别(`DEBUG`/`INFO`/`WARNING`/…)。非连接配置。未设置时遵循应用的日志设置(作为库不会触碰 root/basicConfig)。由于消息正文和发送者的日志默认级别为 `DEBUG`,因此在默认的 `INFO` 级别下**不会记录对话内容**——诊断时可开启 `DEBUG`,若想更安静则设置为 `WARNING` | 上述 6 个(带有 `DAOU_` 前缀的变量)即是 **SDK 读取的所有环境变量**。各个示例自行使用的变量(LLM 密钥、目标房间 ID 等)记录在对应示例的 docstring 中——由于与 SDK 核心无关,此处不作赘述。 **与下游应用的集成(例如:dt-agent 的 `agent.yaml`)**:如果应用已经拥有自己的配置 YAML,只需在其中添加一个 `daouoffice:` 配置段并向 SDK 指定该文件即可。SDK 仅作读取,因此应用可以原样保留其文件格式和注释。token/identity 依然由 SDK 自动管理在 `~/.daoubot/profile.yaml`(或通过 `--config` 指定的路径)中。 ``` # agent.yaml(App 中包含的现有文件) 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` 后,Bot 代码无需配置连接信息——会自动从 profile 中解析: ``` 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` 则不回复。回复会自动作为对触发该处理程序的消息的**引用回复**(与 DaouOffice 的回复 UI 相同),因此即使在繁忙的房间里,也能清楚这是对哪条消息的回答。如果不提供处理程序,Bot 只会读取消息(不作回复)。如果遇到 30 分钟过期,只要存在 RefreshToken/凭据,就会自动进行刷新和重新登录。 **Mention:** DaouOffice 的 mention 是正文中的内联 token(完全公开,非私有——见 [docs/api/03-messages.md](docs/api/03-messages.md) §3.6)。SDK 会将其解析,并提供 `msg.mentions` / `msg.mentions_me` / `msg.mention_all`,人类可读的 `message_text`(token → `@名字`)以及原始的 `raw_text`。如果只想在繁忙群组中被 mention 时才响应,请使用 `only_when_mentioned(handler)` 进行包装(这不是一个全局开关——而是声明式的策略)。如果希望 Bot 也能响应别名(例如 `@迪蒂`/`@DT`),请使用 `only_when_addressed(handler, aliases=("迪蒂","DT"))`——真正的 mention 和纯文本的 `@别名` 均可生效(别名未经过身份验证,请勿用作权限校验)。 ``` bot = DaouBot(..., on_message=only_when_mentioned(handle)) ``` **Markdown 样式 (`markdown=True`):** 聊天仅渲染一部分 HTML 子集——加粗、斜体、链接、有序列表、无序列表(已通过实时抓取确认)。如果设置 `DaouBot(..., markdown=True)`,引擎会在发送前自动转换处理程序返回的 Markdown(`**加粗**`/`*斜体*`/`[文本](url)`/`1.`/`-`)。默认为禁用状态(原样发送)。子集之外的语法(标题、代码)会原样降级输出为文本而非标签,文本会被 HTML escape,链接的 href 也会被转义至包含 `"`,确保不会发生显示错误或注入。手动转换可使用 `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` 中——即使 Bot 重启(无论在哪个目录下执行),也会接着之前的进度处理积压消息,既不会重复处理,也不会遗漏停机期间的消息。如果不需要持久化,可以使用 `DaouBot(..., cursor_store=MemoryCursorStore())`。不过,受限于轮询的特性,追赶机制仅限于每个房间最近约 100 条消息的窗口内(如果停机时间超过此范围,窗口之外的消息将无法恢复——因为不存在类似于 "since id" 的端点)。 **传输保证:** 引擎确保实现 **at-least-once** 语义——这是消息传递的行业标准(Kafka/SQS/Slack/Telegram),因此不作为开关暴露,而是由 SDK 承担责任。它会按房间内的顺序持续重试投递,直到处理程序无异常结束为止;如果同一条消息连续失败达到 `max_attempts`(默认为 5)次,则会被判定为 poison 消息并跳过。 - 如果重复处理会产生影响,请**将处理程序编写为幂等的**(遵循 Kafka/SQS 的理念)。传输层面的去重由引擎负责,而业务层面的幂等性则是处理程序的责任。 - Fire-and-forget(不需要重试)并不是一种特定的模式,只要**处理程序吞掉了自身的异常**(不计为失败),自然就实现了这种效果。 ## CLI ``` daoubot login ... # 인증 + 프로필 저장 (위 참고) daoubot whoami # 저장된 봇 신원 출력 daoubot config # 저장된 프로필 보기(시크릿 마스킹) daoubot config set base_url # 연결 항목 수정 (password 는 값 생략 시 숨김 입력) daoubot config path # 프로필 파일 경로 출력 daoubot rooms # 모든 채팅방 목록 (room id 포함, 페이지 끝까지 조회) daoubot room list # rooms 와 동일한 별칭 daoubot room create --users a,b --name "Bot Test" [--type GROUP] daoubot room open # 방 상세 + 구성원 daoubot send "" # 메시지 전송 daoubot login --config bots/a.json ... # 프로필 파일 위치 분리(멀티 봇/테넌트) ``` CLI 主要用于引导、查询和单次发送。**Bot 的运行并不通过 CLI**,而是通过包含 `DaouBot(on_message=...) 的 Python 脚本(`python my_bot.py`)来执行——由于 CLI 无法承载处理程序,因此不提供单独的 `start` 命令(参见下方的 [快速开始](#빠른-시작) 和 `examples/`)。 如果省略 `--login-id`/`--password`,系统会提示你按此顺序进行输入(密码会隐藏输入——防止在 argv 或历史记录中暴露)。 开发者可以通过 `login` → `rooms`/`room list`/`room create` 获取所需的 `company_id`、`user_id` 和 `room_id`,然后利用这些参数编写基于 SDK 的 Bot。`rooms`/`room list` 会翻页直至末尾,显示完整的房间列表。(免安装体验:`uv run python -m daouoffice.cli rooms`) ## 示例 `examples/` 中的每一个 Bot,只需设置好 `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_* 环境变量) | | `bot-router` | 基于房间的处理程序分发 (仅处理已注册房间的白名单机制) | | `bot-error-handler` | 捕获处理程序异常并通知开发者房间 | | `bot-room-saver` | 使用 `RoomRouter` 仅将指定房间的消息保存为 JSONL(不作响应;房间 ID 可通过 `daoubot rooms` 获取) | ``` uv run --with python-daouoffice-bot examples/bot-echobot/bot.py ``` ## 利用 AI 创建 Bot(Agent Skill) 仓库内置了标准的 **Agent Skill**(`SKILL.md` + frontmatter + 打包文件),你可以让 AI “帮我写一个 DaouOffice Bot” 来进行脚手架搭建与扩展。它并不绑定于特定工具,只要支持该 Skill 格式的任何 Agent 运行时(Claude.ai、Claude Code、Claude API/Agent SDK 等)都可以直接无缝迁移。 ``` skills/daouoffice-bot/ # SKILL.md + reference.md + scaffold.py ``` 安装——只需将文件夹放置在 Agent 的 skills 目录下即可。Claude 系列运行时使用 `~/.claude/skills/`(全局),其他运行时请遵循各自的 skill 加载器约定: ``` cp -r skills/daouoffice-bot ~/.claude/skills/ # 수동 복사 # 或 npx skills add junsik/python-daouoffice-bot --skill daouoffice-bot ``` ### 应用方式(安装后) Skill 并非一个单独的指令,而是会**根据请求内容自动触发**。在你要创建 Bot 的工作文件夹中打开支持 Skill 格式的 Agent(Claude Code、Claude.ai、Claude Desktop、Agent SDK 等),像平常一样用自然语言表达即可: ``` 다우오피스 봇 만들어줘. 우리 회사는 acme.daouoffice.com 이고, #개발-알림 방에서 !배포 명령에만 응답하면 돼. ``` 这样 Skill 就会被激活——(1) **反问**缺失的信息(租户、专用账号、目标房间、触发器、状态、副作用),(2) 使用决策矩阵确定设计方案,(3) 通过 `scaffold.py` 生成样板代码并实现处理程序,(4) 确保遵守 SDK 的不变规则,(5) 指导你完成直至进行 `daoubot login`/`send` 的实时冒烟测试。(无需显式输入“使用 Skill”——只要包含 `DaouOffice Bot`/`DaouBot`/`daoubot` 等表述即可触发。) 根据安装位置的不同,触发范围如下: - `~/.claude/skills/daouoffice-bot/` → 在**所有项目**中均可触发(推荐)。 - 特定 Bot 项目的 `<该项目>/.claude/skills/daouoffice-bot/` → 仅在该项目中触发。*请不要将其放入本 SDK 仓库自身的 `.claude/` 目录中*——那里是用于 SDK 开发的。 - Skill 不是模板菜单,而是**设计指南**——它会教会 AI 结合 SDK 的不变规则(账号全局 read、白名单、幂等性、禁止虚构不存在的 API),通过访谈获取需求(租户、目标房间、触发器、状态、副作用),利用决策矩阵组合各种基础组件(`on_message`/`RoomRouter`/`only_when_mentioned`/状态机/LLM)来构建出符合用户期望的 Bot。 - `scaffold.py` 不会去猜测你的用例,而是仅输出**正确的样板代码**(绑定环境变量/Profile + 优雅运行 + 空的处理程序)。具体的业务设计交由 AI 根据需求决定:`python skills/daouoffice-bot/scaffold.py > bot.py` ## SDK 概览 | 符号 | 说明 | |---|---| | `BotClient` | REST API 封装(登录、房间、消息、附件、`whoami`、`discover_company`) | | `BotEngine` | 轮询引擎(单一实现,async) | | `DaouBot` | 高阶 Bot(`on_message` + 轮询 + 401 自动重新登录) | | `RoomRouter` | 按房间分发处理程序(仅处理已注册房间,忽略其余房间) | | `only_when_mentioned` | 仅在提及 Bot(`@Bot`/`@全体成员`)时执行处理程序 | | `only_when_addressed` | 包含上述功能,并额外识别别名(纯文本的 `@别名`,通过 `aliases=(...)` 设定)。由于别名是纯文本,不适合用作权限校验 | | `load_settings` / `Settings` | 解析连接配置(参数>环境变量>Profile);`DaouBot()` 内部会调用此功能 | | `FileCursorStore` / `MemoryCursorStore` | 持久化/非持久化存储处理进度 | | `NewMessage` | 标准化接收到的消息 | | `BotIdentity` | 登录时解析出的 Bot 自身身份 | | `Profile` | `daoubot login` 保存的 Profile (`load_profile`) | | `DaouAuthError` / `DaouConfigError` | 异常 | 传递机制**仅使用轮询**。虽然通过抓包观测到了 WebSocket (`GET /ws/pc`, STOMP) 端点,但由于未能验证其流程,因此**未予实现**(未经验证的逆向备忘录:[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
标签:Python, REST客户端, 云资产清单, 企业办公, 无后门, 聊天机器人, 逆向工程, 非官方API