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, 代理服务, 大语言模型, 开发辅助工具, 特征检测, 自定义脚本