内容
# ansvar-m365-copilot-agent
Ansvar 官方的 Microsoft 365 Copilot 应用。它是一个声明式代理,连接 Copilot 到 Ansvar MCP 网关,网址为 `https://gateway.ansvar.eu/mcp`,使用户可以询问 Copilot 关于欧洲和美国的法律、法规和标准,并获得带有段落级引用的答案,引用源自官方出版商。
该仓库包含通过合作伙伴中心提交给 Microsoft 的应用包。它不包含任何应用程序代码。代理是一组清单,所有检索都在网关中进行。
## 包布局
```
appPackage/
manifest.json Microsoft 365 应用清单(模式 1.25)
declarativeAgent.json 名称、描述、说明、对话启动器(模式 v1.8)
ai-plugin.json 指向 MCP 网关的 API 插件清单(模式 v2.4)
color.png 192x192
outline.png 32x32
assets/
ansvar-mark.svg 品牌标志,用于生成两个图标
scripts/
render-icons.py 从标志重新生成两个图标
validate.sh 模式、复制和图标检查(也在 CI 中运行)
package.sh 构建 dist/ansvar-m365-copilot.zip
```
zip 文件存储这五个文件在根目录下。Microsoft 拒绝将成员放在文件夹中的包。
## 工具发现是动态的,按设计
`ai-plugin.json` 声明了一个 `RemoteMCPServer` 运行时,带有 `spec.url`,没有 `mcp_tool_description`。在 v2.4 插件模式下,省略工具描述告诉运行时在运行时调用网关的 `tools/list`,而不是使用静态定义。
这是应用程序需要的形状,而不是快捷方式。网关将其工具表面范围限定为已登录用户的订阅层,因此工具列表因用户而异。将工具固定在清单中会宣传一个固定的表面,这与给定用户可以调用的表面不匹配。
## 身份验证
每个用户都以自己的身份进行身份验证,包中不携带共享或嵌入的凭证。
- 网关是一个 OAuth 2.1 资源服务器。Copilot 与 Ansvar 身份服务在 `https://auth.ansvar.eu` 进行授权代码流,领域为 `ansvar`。
- Keycloak 客户端是 `m365-copilot`,重定向 URI 为 `https://teams.microsoft.com/api/platform/v1.0/oAuthRedirect`。
- 访问令牌携带该用户的自身层级声明。网关根据令牌决定调用哪些语料库、工具和工作流,因此权限在服务器端强制执行,不在清单中。
- `ai-plugin.json` 中的 `reference_id` 保留在 Teams 开发者门户中注册的 OAuth 客户端的 ID。客户端密钥在此仓库或包中均不出现。
## 构建包
`reference_id` 作为占位符 `${AUTH_CONFIG_ID}` 提交。 在 Teams 开发者门户中注册一次 OAuth 客户端,然后传递返回的配置 ID:
```bash
AUTH_CONFIG_ID=<oauth-config-id> ./scripts/package.sh
```
脚本写入 `dist/ansvar-m365-copilot.zip`。 在错误的 ID 和构建包之间有三个防护措施:
- `AUTH_CONFIG_ID` 必须匹配 `^[A-Za-z0-9+/=._-]{6,128}$` (门户发布 base64 注册 ID,末尾带有 `=`),这会拒绝未设置或空白的值、空格、引号和尖括号。
- 如果它包含 `AUTH_CONFIG`、`YOUR`、`TODO`、`PLACEHOLDER` 或 `CHANGEME`(任意大小写),以及全零 GUID(`validate.sh` 使用的虚拟 ID),则会被拒绝。
- 在替换后,脚本将结果解析为 JSON,并断言 `runtimes[0].auth.reference_id` 等于给定的 ID,因此文本替换产生无效 JSON 或留下身份验证块未动会失败构建。
这些防护措施可以捕获未解析或格式错误的 ID,而不是错误的 ID。 一个格式良好的 ID 指向错误的注册仍然会构建,只有在侧载包时才会显示。
zip 文件在临时目录中组装和验证,只有在通过验证后才移动到 `dist/` 目录。
## 验证
```bash
./scripts/validate.sh
```
验证门户需要 `python3` 带有 `jsonschema` 和 `pillow`,以及网络访问来获取三个模式。`AUTH_CONFIG_ID` 不是必需的:门户使用虚拟 ID 替换,因此提交的模板会得到验证。`.github/workflows/validate.yml` 在每次推送和拉取请求上运行相同的脚本。
它检查的内容:
1. `manifest.json` 和 `declarativeAgent.json` 与未修改的发布模式相对照。`ai-plugin.json` 与发布的 v2.4 模式相对照,但有一个文档化的放松,下面会描述。
2. 应用名称在 `manifest.json`、`declarativeAgent.json` 和 `ai-plugin.json` 中完全相同。商店验证如果三个文件不一致,则拒绝该包。
3. 每个长度限制字段的字符限制。
4. 所有三个文件在商店可见文本和代理可见文本的复制规则:不允许 URL、非 ASCII 字符、营销超级词或排名声明、响应格式命令,以及 ADR-009 禁止的单词。
5. 图标尺寸。
6. 打包路径,在 CI 中:在工作流中,使用一次性身份验证 ID 构建 zip,然后断言五个成员位于 zip 根目录下,JSON 成员可以解析,并且 `reference_id` 携带给定的 ID。 该工件被丢弃且从不上传。
然后,门户打印一行 `[MANUAL]`,表示它不检查的每个商店规则,因此绿色运行不被视为商店准备就绪。 语法、两个图标是否相似以及它们与品牌是否相似,以及说明是否产生良好的代理行为,这些是人工检查。 Microsoft 方面的接受度通过侧载构建的包和合作伙伴中心审查建立,而不是通过此处模式的一致性。
### 一个放松
发布的 v2.4 插件模式定义 `runtime.spec` 为 `oneOf`,其 OpenAPI 分支接受一个裸露的 `{"url": ...}`,与 MCP 分支完全相同。省略 `mcp_tool_description` 的 `RemoteMCPServer` 因此匹配两个分支并失败 `oneOf`,尽管省略它是 Microsoft 文档中动态工具发现的方式。 冲突仅特定于该形状:添加 `mcp_tool_description` 以固定工具使规范仅与 MCP 分支匹配并干净地验证,因此固定工具的插件永远不会遇到此问题。
`ai-plugin.json` 保留文档化的动态发现表单。`scripts/validate.sh` 将单个 `oneOf` 转换为 `anyOf`,然后根据声明的运行时类型选择的子模式验证规范。 模式的其他部分不受影响。
## 图标
两个图标都从 `assets/ansvar-mark.svg` 渲染,这是随附的品牌标志:
```bash
python3 scripts/render-icons.py
```
`color.png` 将完整的标志放在 192x192 紫色板的 120x120 安全区域内。`outline.png` 删除文字标记并重新居中“A”,因为在 32 个像素下,文字标记会变成无法阅读的污点。
这使得轮廓图标成为标志的简化,而不是其缩放副本,这是一种可接受的风险。 如果商店审查标志两个图标不匹配,请从完整标志重新生成 `outline.png` 并损失可读性。
## 许可证
MIT。 见 `LICENSE`。