cramt/m365-copilot-proxy

GitHub: cramt/m365-copilot-proxy

将 Microsoft 365 Copilot 的私有协议封装为兼容 OpenAI 的代理,使现有 M365 许可可直接作为编程 Agent 的后端。

Stars: 40 | Forks: 8

# m365-copilot-proxy 将 Microsoft 365 Copilot 用作兼容 OpenAI 的编程 agent(如 [pi](https://pi.dev/) 和 [OpenClaw](https://docs.openclaw.ai/))的 LLM 后端。将 M365 Copilot 的 WebSocket/SignalR API 封装为兼容 OpenAI 的接口,并支持工具调用(tool calling)。 ## 工作原理 M365 Copilot 使用的是 SignalR WebSocket 协议,而不是 OpenAI API。本项目在两者之间进行转换: 1. **独立代理** — 带有 `/v1/chat/completions` 和 `/v1/models` 端点的 HTTP 服务器。适用于任何兼容 OpenAI 的客户端(pi、OpenClaw 等)。 2. **OpenClaw 插件** — 为 OpenClaw 的 provider 系统提供配置生成器 + 设置 CLI。 ### 工具调用 M365 Copilot 原生不支持 OpenAI 风格的 `tool_calls`。相反,工具是通过 **Markdown 围栏格式** 进行模拟的(之前的 JSON `{"tool":...}` 格式已被移除——它在实际的 agentic 任务中得分为 0/5;参见[假设 §9](docs/hypotheses.md)): - 工具定义作为围栏模板注入到 prompt 中的 `` 块内 - 模型输出一个围栏工具调用 —— 即一个 info-string 为工具名称的代码块(标量参数作为 `key: value` 标题行,一个自由格式的 body 参数作为围栏主体,`old`/`new` 编辑采用 aider 风格的 `SEARCH/REPLACE` diff) - 代理/handler 会解析该内容并将其转换为 OpenAI 的 `tool_calls` 格式 - **Shell 路由(关键手段):** M365 经过 chat 调优的模型不会按需“充当 agent”,但*会*下意识地编写 ```` ```bash ```` 块。当工具集包含 shell 工具(`bash`/`shell`/`run`/`run_command`/… — 任何名称)时,代理会注入“通过编写一个 ```` ```bash ```` 块来完成整个步骤”的框架,并将该块路由到 shell 工具。这利用了 Microsoft 系统提示词允许的唯一 agentic 行为,也是将 0/5 转化为真正多轮循环(已验证可进行 9 次工具调用的 bug 修复)的关键。 - **可靠性来自 Copilot Studio agent(见下文)+ 围栏/shell 框架** —— 如果没有该 agent,M365 会忽略工具指令并以纯文本回复 ### Agent 模式 首次使用时,系统会创建一个 **Copilot Studio agent**,并在其服务端系统提示词中内置工具调用指令。这是通过 PowerPlatform API 完成的: 1. 通过 BAP API (`api.bap.microsoft.com`) 发现环境 URL 2. 使用 Copilot Studio 的 `minimalBots` API 创建一个带有指令的 bot 3. 发布 bot 以获取 `TitleId` 4. 在 WebSocket 聊天请求中使用该 agent ID(`T_{titleId}.{botId}.gpt.default`) 5. 将 agent ID 缓存在 `~/.config/opencode-m365/agent-id.json` 中 ### 会话复用 每个 agent 会话都会复用同一个 M365 会话(相同的 `sessionId` + `conversationId`)。WebSocket 每轮都会重新连接,但 M365 会在服务端维护上下文。这能节省额度 —— 600 条消息的限制适用于每个会话。 ## 安装包 ``` @m365-copilot/core — Shared: auth, WebSocket client, tool formatting, proxy server, agent management, session ├── @m365-copilot/proxy — Standalone HTTP proxy binary └── @m365-copilot/openclaw-plugin — OpenClaw config generator + setup CLI + skill ``` ## 设置 ### 前置条件 - Node.js 24+ - pnpm 10+ - 一个拥有 Copilot 访问权限的 M365 账户 - 基于 TOTP 的 MFA,并手握 base32 密钥 —— 自动登录会自行输入 6 位验证码,因此它需要的是种子密钥,而不是你手机上的应用。请参阅[获取 TOTP 密钥](#getting-the-totp-secret)(如果你的租户根本不提供 TOTP,请参阅其后的说明)。 ### 1. 安装 ``` git clone https://github.com/cramt/m365-copilot-proxy cd m365-copilot-proxy pnpm install pnpm build ``` ### 2. 配置凭据 创建 `~/.config/opencode-m365/secrets.json`: ``` { "email": "you@company.com", "password": "your-password", "mfaSecret": "YOUR_TOTP_BASE32_SECRET" } ``` `mfaSecret` 是你的身份验证器应用生成 6 位验证码所用的 **base32 种子** —— 而不是验证码本身。它的格式类似于 `JBSWY3DPEHPK3PXP`:由 16-32 个字符组成,且只能包含 `A`–`Z` 和 `2`–`7`,没有空格。 #### 获取 TOTP 密钥 **已经在使用密码管理器管理 TOTP 了?直接读取出来即可。** 1Password、Bitwarden、KeePassXC、Aegis、Ente Auth 等同类软件都会保存该种子,并可根据需要显示出来 —— 打开该项目的一次性密码字段并查看即可。你会得到一串纯 base32 字符串或一个 `otpauth://totp/...?secret=JBSWY3DPEHPK3PXP&...` URI;在第二种情况下,`secret=` 参数就是你需要的内容。无需重新注册。 **如果种子被困在 Microsoft Authenticator 中,请注册第二种方法。** 该应用特意不暴露种子,而且 Microsoft 的安全信息页面在注册后也不会重新显示密钥 —— 在这两者之间,这是种子真正无法恢复的唯一情况。注册一个新条目并在过程中复制它: 1. 前往 (**我的账户 → 安全信息**)。 2. **添加登录方法 → Authenticator 应用**。 3. 点击 **“我想使用其他身份验证器应用”**。这一步至关重要 —— 默认路径会假定使用 Microsoft Authenticator,并注册一种仅限推送的方法,没有你可以提取的种子。 4. 在二维码界面,点击 **“无法扫描图像?”**。它会显示一个 **密钥** —— 那就是你的 base32 字符串。 5. 使用它生成验证码以完成注册 —— `oathtool --totp -b `,或者将种子粘贴到你的密码管理器中 —— 然后输入该验证码。 将其存储在可以取回的地方(见上文),这样这就是一项一次性的任务。新条目将与现有的登录方法并存;你无需移除 Microsoft Authenticator。 #### 如果你的租户没有 TOTP 选项 许多租户无法执行上述操作,因此无法提取种子: - 租户策略禁用了 authenticator 应用 / 软件 OATH 方法; - MFA 仅限推送 / 数字匹配、FIDO2、Windows Hello 或基于证书; - 登录联合到负责 MFA 步骤的第三方 IdP(Okta、Ping、Duo)。 在这些情况下,上述存储凭据的流程无法工作 —— 自动登录没有可输入的验证码,而且任何配置都无法解决此问题。用户驱动的备用方案(可见浏览器,你手动完成一次 MFA,之后 token 会静默刷新)已在 [#4](https://github.com/cramt/m365-copilot-proxy/issues/4) 中跟踪,尚未发布。 #### 首次运行 首次运行时,系统会执行自动浏览器登录(通过 Playwright/Chromium)以获取 OAuth token。此后,token 将从 MSAL 缓存中静默刷新。 ### 3. 配合 pi(或任何兼容 OpenAI 的 agent)使用 启动代理: ``` m365-proxy 4143 # or: pnpm run proxy 4143 ``` 通过 `~/.pi/agent/models.json` 将 [pi](https://pi.dev/) 指向它: ``` { "providers": { "m365": { "baseUrl": "http://localhost:4143/v1", "api": "openai-completions", "apiKey": "m365", "compat": { "supportsDeveloperRole": false, "supportsReasoningEffort": false, "supportsUsageInStreaming": false }, "models": [ { "id": "gpt-5.5-think-deeper", "name": "M365 Copilot (GPT-5.5, recommended)" }, { "id": "m365-copilot", "name": "M365 Copilot (Auto)" } ] } } } ``` 然后运行 pi(使用 `gpt-5.5-think-deeper` —— 这是可靠的工具调用模型 —— 并保持工具集精简;M365 在遇到非常庞大的 tool payload 时会“脱离”,请参阅 [docs/m365-copilot-api.md](docs/m365-copilot-api.md#the-disengaged-filter)): ``` pi --models "gpt-5.5-think-deeper" -p --tools read,list,edit,write "your task" ``` 这已经过端到端验证,可以正常工作,包括多工具调用和实际的文件编辑。 ### 4. 配合 OpenClaw 使用 ``` # 一条命令配置并启动 m365-openclaw-setup --start # 或者仅配置,然后单独启动 m365-openclaw-setup m365-proxy 4141 ``` 代理使用会话复用和增量消息 —— 后续轮次仅发送新消息,从而节省 M365 额度。当消息数组缩小或第一条用户消息发生变化时,会自动检测到新的对话。 ### 5. 作为独立代理使用 ``` npx m365-proxy 4141 # 或者 pnpm run dev ``` 然后将任何兼容 OpenAI 的客户端指向 `http://localhost:4141/v1`。 ### 6. 在 NixOS 上运行(systemd 服务) 该代理是一个 [Nitro](https://nitro.build/) 服务。该 flake 暴露了一个 package(通过 [pnpm2nix](https://github.com/cramt/pnpm2nix) 从工作区构建)和一个 NixOS 模块: ``` # flake.nix { inputs.m365.url = "github:cramt/m365-copilot-proxy"; outputs = { nixpkgs, m365, ... }: { nixosConfigurations.myhost = nixpkgs.lib.nixosSystem { modules = [ m365.nixosModules.default { services.m365-copilot-proxy = { enable = true; # JSON with { email, password, mfaSecret } — kept out of the Nix store, # delivered via systemd LoadCredential. Manage with sops-nix/agenix. secretsFile = "/run/secrets/m365-copilot.json"; # port = 4141; # default # host = "127.0.0.1"; # default — do not expose; unauthenticated, paid account # openFirewall = false; }; } ]; }; }; } ``` 该服务作为受严格限制的 `DynamicUser` 单元运行。身份验证状态(`msal-cache.json`、`agent-id.json`)持久化存储在 `/var/lib/m365-copilot-proxy` 中;全新的部署会通过使用 `secretsFile` + 内置 Chromium 的无头登录进行自我引导。要在不使用 NixOS 的情况下直接运行该 package:`nix run github:cramt/m365-copilot-proxy -- 4141`。 ## 可用模型 | 模型 ID | M365 语气 | 描述 | |---|---|---| | `gpt-5.5-think-deeper` | Gpt_5_5_Reasoning | **推荐的 agent/工具调用默认模型** — 工具依从性强 | | `gpt-5.5` / `gpt-5.5-quick` | Gpt_5_5_Chat | GPT-5.5 快速版 | | `m365-copilot` / `auto` | magic | 自动路由 — 在工具调用方面表现极不稳定(会产生虚构;见下文) | | `quick` | Gpt_Quick | 快速响应 | | `think-deeper` | Gpt_Reasoning | 速度较慢,更详尽 | | `claude` / `claude-sonnet` | Claude_Sonnet | 真正的 Anthropic Claude(无 agent 路径) | | `claude-sonnet-think-deeper` | Claude_Sonnet_Reasoning | Claude 推理 | | `gpt-5.4` / `gpt-5.4-quick` | Gpt_5_4_* | GPT-5.4 | | `gpt-5.3` / `gpt-5.3-think-deeper` | Gpt_5_3_* | GPT-5.3 | | `gpt-5.2` / `gpt-5.2-think-deeper` | Gpt_5_2_* | GPT-5.2 | ## 身份验证 身份验证流程使用带有 PKCE 的 Azure MSAL: 1. **静默刷新** — 使用来自 `~/.config/opencode-m365/msal-cache.json` 的缓存 token 2. **自动登录** — 通过 Playwright 驱动浏览器,使用存储的凭据 + TOTP 进行登录 3. **交互式登录** — 打开浏览器进行手动 OAuth 流程(备用方案) 会获取三个 token 范围: - `substrate.office.com/sydney/*` — 用于 M365 Copilot 聊天 - `api.powerplatform.com/.default` — 用于 Copilot Studio agent 管理 - `api.bap.microsoft.com/.default` — 用于环境发现 ## 环境变量 | 变量 | 描述 | |---|---| | `M365_DEBUG` | 设置为 `1` 可启用调试日志记录到 `~/.config/opencode-m365/debug.log`(已截断的 payload) | | `M365_TRACE` | 设置为 `1` 以获取完整的、未截断的调试日志(每个 WS 帧/prompt/响应)— 隐含包含 `M365_DEBUG`。用于逆向工程。 | | `M365_LOG_STDOUT` | 设置为 `1` 可将调试信息同时镜像到代理的 stdout 和日志文件中,这样你就可以在不通过第二个终端执行 tail 命令的情况下监视运行过程。需要设置 `M365_DEBUG` 或 `M365_TRACE` — 单独设置此项不会记录任何日志。 | | `M365_DUMP_FRAMES` | 设置为 `1` 可将每个 WebSocket 帧(双向)写入 `~/.config/opencode-m365/frames/.ndjson`。用于离线对比新增的 M365 字段。 | | `M365_ALLOW_MULTI_TOOL` | 允许模型每轮发出多个工具调用(默认:仅保留第一个) | | `M365_INJECT_REPLY_TOOL` | 设置为 `1` 可注入一个合成的 `reply(text)` 工具。强制每一轮都成为工具调用,包括纯文本回答。为模型提供了更清晰的契约,但 prompt 中会增加 1 个工具(注意 Disengaged 阈值)。在 2026 年 6 月 9 日确认达到 5/5 的依从性([假设 §1.1]( 2 × 10⁻³ 的阈值处触发。客户端可以监控此项指标,以便在触发过滤器之前选择退避。 忽略未知扩展字段的客户端将照常工作;好奇的用户可以读取它们。有关完整的调查结果转储,请参阅 [docs/hypotheses.md §0](docs/hypotheses.md),有关我们尝试过但未果的内容,请参阅 [§2](docs/hypotheses.md)。 ## 配置文件 全部存储在 `~/.config/opencode-m365/` 中: | 文件 | 描述 | |---|---| | `secrets.json` | 登录凭据(电子邮件、密码、mfaSecret) | | `msal-cache.json` | MSAL token 缓存(自动管理) | | `agent-id.json` | 缓存的 Copilot Studio agent ID | | `debug.log` | 调试日志(当设置 `M365_DEBUG=1` 时) | ## 开发 ``` pnpm install pnpm build # Build all packages pnpm run dev # Start standalone proxy on :4141 pnpm run test:unit # Run vitest unit tests (no auth/network) pnpm run test:live # Run live integration tests against M365 ``` ## 已知限制 - **M365 在遇到庞大的 tool payload 时会“脱离”** —— 复杂的 agent 框架(例如 opencode 约 15 个工具的 prompt)会收到空的 `Disengaged` 响应。请保持工具集精简(这就是 [pi](https://pi.dev/) 能良好运行的原因)。请参阅 [docs/m365-copilot-api.md](docs/m365-copilot-api.md#the-disengaged-filter)。 - 工具调用是模拟出来的(prompt 注入 + Copilot Studio agent),而不是原生的函数调用 —— 配合 agent 使用时很稳健,离开 agent 则不可靠 - `think-deeper` / `*_Reasoning` 模型每次响应需要 10-30 秒 - 每个**对话**有约 600 条消息的硬性额度(通过会话复用 + 增量发送来缓解) - 流式传输:**无工具**响应支持增量流式传输(增量在到达时即被转发)。**工具调用**轮次仍会在服务端进行缓冲 —— 必须先解析原始文本以提取工具调用围栏,然后才能发出 —— 因此这些内容会在结束时作为一个单独的数据块到达(并带有立即的 HTTP 200 + 心跳,以确保客户端在等待时永远不会超时) ## 许可证 [MIT](LICENSE)。使用风险自负 —— 本项目使用你自己的凭据,在你自己的账户上与 Microsoft 的 API 进行通信,这完全是你与租户的可接受使用政策之间的事情。
标签:API网关, DLL 劫持, Microsoft Copilot, MITM代理, OpenAI兼容, SignalR, 代理服务, 大语言模型, 开发辅助工具, 特征检测, 自定义脚本