sync-doc-to-lab
将 arc-reactor / openclaw 生成的报告同步至 personal_lab 知识库。
核心职责
- 获取用户身份:用 APPKEY 调外部登录 API 获取 userId
- 写入标准目录:
data/workspaces/{userId}/reports/{yyyy}/{mm}/{report_id}.md - 触发入库:调用
POST /api/sync - 生成预览链接:调用
POST /api/reports/{report_id}/share获取 preview_token - 可选编译:调用
POST /api/wiki/compile
触发场景
| 场景 | 触发方式 |
|---|---|
| arc-reactor 完成后询问用户 | 用户确认后才同步 |
| 用户说"同步知识库" | 手动触发 |
| 用户说"同步 recent" | 同步最近一份报告 |
| 用户说"同步 all" | 同步所有待同步报告 |
第一步:用 APPKEY 调外部登录 API
GET https://sg-al-cwork-web.mediportal.com.cn/user/login/appkey?appKey=<APPKEY>&appCode=personal_lab
APPKEY:A2d5J8fCDNHT3Vbkv3dndsEzoQ3zMNsv
返回示例:
{
"userId": "0210023418672077",
"userName": "刘健"
}
字段映射:
workspace_id = userId
workspace_name = userName
第二步:生成标准 Markdown 报告
必须包含以下 frontmatter 字段:
| 字段 | 说明 |
|---|---|
report_id |
格式:rpt_{YYYYMMDD}_{HHMMSS}_{hash} |
title |
报告标题 |
source_ref |
原始链接 |
skill_name |
固定写 openclaw |
generated_at |
ISO 格式时间 |
status |
固定写 published |
summary |
单行摘要 |
格式限制:
- 只用简单
key: value - 列表用
- item格式 - 不写嵌套对象
- 不写多行块文本
summary保持单行
推荐模板:
---
report_id: rpt_20260415_110000_a1b2c3d4
title: 报告标题
source_ref: https://example.com/article/123
source_url: https://example.com/article/123
source_domain: example.com
source_type: url
skill_name: openclaw
generated_at: 2026-04-15T11:00:00+08:00
status: published
language: zh-CN
summary: 这是报告的单行摘要。
tags:
- tag1
- tag2
related_urls:
- https://example.com/article/123
author: openclaw
---
# 报告标题
## 摘要
正文内容...
## 关键信息
核心内容整理...
## 原始来源
- 来源地址:https://example.com/article/123
第三步:写入工作区目录
目标路径:
<project_root>/data/workspaces/{userId}/reports/{yyyy}/{mm}/{report_id}.md
示例:
C:/WorkSpace/personal_lab/data/workspaces/0210023418672077/reports/2026/04/rpt_20260415_110000_a1b2c3d4.md
要求:
- 目录不存在则创建
- 文件名 =
report_id + ".md"
第四步:调用本地 sync 入库
POST http://127.0.0.1:8002/api/sync
Header:
X-Appkey: A2d5J8fCDNHT3Vbkv3dndsEzoQ3zMNsv
Content-Type: application/json
Body:
{
"mode": "incremental"
}
第五步:获取预览链接
入库成功后,调用预览接口生成带 token 的链接:
POST http://127.0.0.1:8002/api/reports/{report_id}/share?expires_in_hours=168
Header:
X-Appkey: A2d5J8fCDNHT3Vbkv3dndsEzoQ3zMNsv
Content-Type: application/json
返回示例:
{
"report_id": "rpt_20260415_142500_claude_code_china",
"share_token": "eyJ2IjoxLCJyZXBvcnRfaWQiOiJycHRfMjAyNjA0MTVfMTQyNTAwX2NsYXVkZV9jb2RlX2NoaW5hIiwid29ya3NwYWNlX2lkIjoiMDIxMDAyMzQxODY3MjA3NyIsImV4cCI6MTc3Njg0OTAyNX0...",
"share_url": "http://127.0.0.1:8002/app/#/report-only/rpt_20260415_142500_claude_code_china?share_token=eyJ2...",
"expires_at": "2026-05-22T17:20:25Z"
}
预览链接含义:
- 只允许匿名读取这一篇 report
- 不开放列表、搜索、上传、删除
- token 默认 168 小时(7天)后过期
- 过期后链接失效
第六步:可选调用 wiki compile
POST http://127.0.0.1:8002/api/wiki/compile
Header:
X-Appkey: A2d5J8fCDNHT3Vbkv3dndsEzoQ3zMNsv
Content-Type: application/json
Body:
{
"mode": "propose",
"report_id": "<report_id>"
}
默认使用 mode = propose(人工确认)。
API 调用封装
import urllib.request
import json
from pathlib import Path
from datetime import datetime
APPKEY = "A2d5J8fCDNHT3Vbkv3dndsEzoQ3zMNsv"
API_BASE = "http://127.0.0.1:8002"
PROJECT_ROOT = Path("C:/WorkSpace/personal_lab")
def get_user_info():
"""调外部登录 API 获取 userId"""
url = f"https://sg-al-cwork-web.mediportal.com.cn/user/login/appkey?appKey={APPKEY}&appCode=personal_lab"
req = urllib.request.Request(url, method="GET")
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.loads(resp.read().decode("utf-8"))
return data["data"]["userId"], data["data"]["userName"]
def write_report(report_path: Path, content: str):
"""写入报告文件"""
report_path.parent.mkdir(parents=True, exist_ok=True)
report_path.write_text(content, encoding="utf-8")
def call_sync():
"""调 /api/sync 入库"""
req = urllib.request.Request(
f"{API_BASE}/api/sync",
data=json.dumps({"mode": "incremental"}).encode("utf-8"),
headers={
"X-Appkey": APPKEY,
"Content-Type": "application/json"
},
method="POST"
)
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read().decode("utf-8"))
def call_compile(report_id: str, mode: str = "propose"):
"""调 /api/wiki/compile"""
req = urllib.request.Request(
f"{API_BASE}/api/wiki/compile",
data=json.dumps({"mode": mode, "report_id": report_id}).encode("utf-8"),
headers={
"X-Appkey": APPKEY,
"Content-Type": "application/json"
},
method="POST"
)
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read().decode("utf-8"))
def create_share_link(report_id: str, expires_in_hours: int = 168) -> dict:
"""生成预览链接"""
req = urllib.request.Request(
f"{API_BASE}/api/reports/{report_id}/share?expires_in_hours={expires_in_hours}",
data=b"",
headers={
"X-Appkey": APPKEY,
"Content-Type": "application/json"
},
method="POST"
)
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read().decode("utf-8"))
def sync_report(report_content: str) -> dict:
"""完整同步流程"""
# 1. 获取 userId
user_id, user_name = get_user_info()
# 2. 解析 report_id
import re
match = re.search(r'report_id:\s*(\S+)', report_content)
if not match:
raise ValueError("报告中未找到 report_id")
report_id = match.group(1)
# 3. 计算路径
now = datetime.now()
year = now.strftime("%Y")
month = now.strftime("%m")
report_path = PROJECT_ROOT / "data" / "workspaces" / user_id / "reports" / year / month / f"{report_id}.md"
# 4. 写入文件
write_report(report_path, report_content)
# 5. 调 sync
sync_result = call_sync()
# 6. 生成预览链接
share_result = create_share_link(report_id)
return {
"user_id": user_id,
"user_name": user_name,
"report_id": report_id,
"report_path": str(report_path),
"report_url": f"http://127.0.0.1:8002/app/#/report-only/{report_id}",
"share_url": share_result.get("share_url"),
"expires_at": share_result.get("expires_at"),
"sync_result": sync_result
}
禁止事项
- ❌ 不要调用
/api/uploads - ❌ 不要调用
GET /api/auth/me - ❌ 不要直接写数据库
- ❌ 不要写
reports/全局目录 - ❌ 不要写
knowledge/全局目录
成功标准
- ✅ 通过外部登录 API 获取 userId
- ✅ 生成标准 Markdown 报告(含 frontmatter)
- ✅ 写入
data/workspaces/{userId}/reports/{yyyy}/{mm}/{report_id}.md - ✅ 调用
POST /api/sync成功 - ✅ 报告可在本地报告中心被检索
- ✅ 返回详情链接:
http://127.0.0.1:8002/app/#/report-only/{report_id} - ✅ 生成预览链接(含 preview_token):
http://127.0.0.1:8002/app/#/report-only/{report_id}?share_token=...
