Install
openclaw skills install sync-doc-to-lab将 arc-reactor 或 openclaw 生成的 Markdown 报告同步至 personal_lab 知识库。触发词:同步、sync、入库。流程:获取 userId → 写入正确目录 → 调用 /api/sync → 可选 /api/wiki/compile。
openclaw skills install sync-doc-to-lab将 arc-reactor / openclaw 生成的报告同步至 personal_lab 知识库。
data/workspaces/{userId}/reports/{yyyy}/{mm}/{report_id}.mdPOST /api/syncPOST /api/reports/{report_id}/share 获取 preview_tokenPOST /api/wiki/compile| 场景 | 触发方式 |
|---|---|
| arc-reactor 完成后询问用户 | 用户确认后才同步 |
| 用户说"同步知识库" | 手动触发 |
| 用户说"同步 recent" | 同步最近一份报告 |
| 用户说"同步 all" | 同步所有待同步报告 |
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
必须包含以下 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"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"
}
预览链接含义:
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(人工确认)。
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/uploadsGET /api/auth/mereports/ 全局目录knowledge/ 全局目录data/workspaces/{userId}/reports/{yyyy}/{mm}/{report_id}.mdPOST /api/sync 成功http://127.0.0.1:8002/app/#/report-only/{report_id}http://127.0.0.1:8002/app/#/report-only/{report_id}?share_token=...