将 arc-reactor 或 openclaw 生成的 Markdown 报告同步至 personal_lab 知识库。触发词:同步、sync、入库。流程:获取 userId → 写入正确目录 → 调用 /api/sync → 可选 /api/wiki/compile。
---
name: sync-doc-to-lab
description: "将 arc-reactor 或 openclaw 生成的 Markdown 报告同步至 personal_lab 知识库。触发词:同步、sync、入库。流程:获取 userId → 写入正确目录 → 调用 /api/sync → 可选 /api/wiki/compile。"
metadata: {"openclaw": {"requires": {"bins": ["python3"]}}}
---
# sync-doc-to-lab
将 arc-reactor / openclaw 生成的报告同步至 personal_lab 知识库。
## 核心职责
1. **获取用户身份**:用 APPKEY 调外部登录 API 获取 userId
2. **写入标准目录**:`data/workspaces/{userId}/reports/{yyyy}/{mm}/{report_id}.md`
3. **触发入库**:调用 `POST /api/sync`
4. **生成预览链接**:调用 `POST /api/reports/{report_id}/share` 获取 preview_token
5. **可选编译**:调用 `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`
返回示例:
```json
{
"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` 保持单行
**推荐模板**:
```md
---
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 入库
```http
POST http://127.0.0.1:8002/api/sync
Header:
X-Appkey: A2d5J8fCDNHT3Vbkv3dndsEzoQ3zMNsv
Content-Type: application/json
Body:
{
"mode": "incremental"
}
```
---
## 第五步:获取预览链接
入库成功后,调用预览接口生成带 token 的链接:
```http
POST http://127.0.0.1:8002/api/reports/{report_id}/share?expires_in_hours=168
Header:
X-Appkey: A2d5J8fCDNHT3Vbkv3dndsEzoQ3zMNsv
Content-Type: application/json
```
返回示例:
```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
```http
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 调用封装
```python
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/` 全局目录
---
## 成功标准
1. ✅ 通过外部登录 API 获取 userId
2. ✅ 生成标准 Markdown 报告(含 frontmatter)
3. ✅ 写入 `data/workspaces/{userId}/reports/{yyyy}/{mm}/{report_id}.md`
4. ✅ 调用 `POST /api/sync` 成功
5. ✅ 报告可在本地报告中心被检索
6. ✅ 返回详情链接:`http://127.0.0.1:8002/app/#/report-only/{report_id}`
7. ✅ 生成预览链接(含 preview_token):`http://127.0.0.1:8002/app/#/report-only/{report_id}?share_token=...`
don't have the plugin yet? install it then click "run inline in claude" again.