通过 Graph 连接器把外部知识喂给 Microsoft 365 Copilot 时,条目形如
externalItem:必有id、properties、acl,体积有上限(文档约 30MB)。本篇只做 JSON 形状校验。
先看清成品
- sample_ok → OK;缺 id/acl 的 bad → INVALID。
- 3 项 pytest 通过。
合规提醒:个人 Copilot 与 Microsoft 365 Copilot 是不同产品;企业场景用公司账号并遵守组织数据政策。不要把密钥、客户名单、未公开财报等粘贴进个人版聊天。Graph 连接器写入的外部内容受 ACL 约束;错误开放等于数据泄露。本文不编造价格。
运行说明:无 Microsoft 365 / Graph 租户密钥。三篇均为本地离线助手:A 从会议记录抽待办;B 粘贴前 DLP 启发式扫描;C 校验 externalItem 形状。未调用官方聊天 API(消费级 Copilot 无通用「随便调」的公开 chat API)。2026-10-23(Asia/Shanghai)pytest 通过。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
external_item_check.py |
形状校验 |
| 样例 JSON | ok/bad |
test_102c.py |
测试 |
需要准备哪些环境?
| 项目 | 说明 |
|---|---|
| Python | 3.10+ |
关键事实
| 事实 | 来源 |
|---|---|
| externalItem 含 acl,决定谁能在 Copilot 里看见 | 官方 Graph 文档 |
| 单项大小上限约 30 MB(以 Graph 文档为准) | 官方 |
| 本校验不调用 Graph,不能代替连接器注册与权限配置 | 重要 |
文件结构
102-microsoft-copilot-C-src/
├── external_item_check.py
├── sample_ok.json
├── sample_bad.json
└── test_102c.py
第 1 步:校验脚本
#!/usr/bin/env python3
"""Validate a Microsoft 365 Copilot connector externalItem-shaped JSON (offline)."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
MAX_BYTES = 30 * 1024 * 1024 # Graph docs: max externalItem size 30 MB
def validate(item: dict) -> list[str]:
errs = []
if "id" not in item or not str(item.get("id") or "").strip():
errs.append("id 必填(外部系统主键)")
props = item.get("properties")
if props is None:
errs.append("properties 必填")
elif not isinstance(props, dict):
errs.append("properties 须为对象")
acl = item.get("acl") or item.get("accessControlList")
if acl is None:
errs.append("acl 必填(谁能看见这条外部内容)")
elif isinstance(acl, list):
for i, ace in enumerate(acl):
if not isinstance(ace, dict):
errs.append(f"acl[{i}] 须为对象")
continue
if not ace.get("type") or not ace.get("value"):
errs.append(f"acl[{i}] 需要 type 与 value")
if ace.get("accessType") not in (None, "grant", "deny"):
errs.append(f"acl[{i}].accessType 非法")
else:
errs.append("acl 须为数组")
raw = json.dumps(item, ensure_ascii=False).encode()
if len(raw) > MAX_BYTES:
errs.append(f"序列化后超过 30MB 限制({len(raw)} bytes)")
# content
content = item.get("content")
if content is not None:
if not isinstance(content, dict) or "value" not in content:
errs.append("content 应含 value(及可选 type)")
return errs
def main(argv=None) -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("item_json")
args = ap.parse_args(argv)
item = json.loads(Path(args.item_json).read_text(encoding="utf-8"))
errs = validate(item)
if errs:
print("INVALID")
for e in errs:
print("-", e)
return 1
print("OK")
print(json.dumps({"id": item.get("id"), "acl_entries": len(item.get("acl") or [])}, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())
第 2 步:测试
from pathlib import Path
import json, subprocess, sys
from external_item_check import validate
HERE = Path(__file__).resolve().parent
def test_ok():
assert validate(json.loads((HERE/"sample_ok.json").read_text())) == []
def test_bad():
assert validate(json.loads((HERE/"sample_bad.json").read_text()))
def test_cli():
r = subprocess.run([sys.executable, str(HERE/"external_item_check.py"), str(HERE/"sample_ok.json")], capture_output=True, text=True)
assert r.returncode == 0
第 3 步:运行
$ python external_item_check.py sample_ok.json
OK
{"id": "kb-42", "acl_entries": 1}
$ python external_item_check.py sample_bad.json
INVALID
- id 必填(外部系统主键)
- acl[0] 需要 type 与 value
$ pytest -q
... [100%]
3 passed in 0.03s
怎么检查结果?
- OK 样例通过。
- bad 指出 id 或 acl 问题。
调试示例
acl 写成全公司 Everyone 图省事。 校验只查字段存在;过宽 ACL 是安全事故。按部门/用户最小权限建模。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 没有 Graph 权限 | 本篇仍可练形状 | 上线需管理员同意 |
| content 怎么放 | properties / content 字段 | 以当前 Graph 文档为准 |
| 30MB | 教程软检查 | 超限请切片 |
模板规则
- ACL 默认拒绝,显式授权。
- 勿索引无授权数据源。
- 变更连接器要审计。
项目资源
形状对了,还要 ACL 对——否则 Copilot 会「帮你泄密」。