sync-doc-to-lab

Data & APIs

将 arc-reactor 或 openclaw 生成的 Markdown 报告同步至 personal_lab 知识库。触发词:同步、sync、入库。流程:获取 userId → 写入正确目录 → 调用 /api/sync → 可选 /api/wiki/compile。

Install

openclaw skills install sync-doc-to-lab

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

APPKEYA2d5J8fCDNHT3Vbkv3dndsEzoQ3zMNsv

返回示例:

{
  "userId": "0210023418672077",
  "userName": "刘健"
}

字段映射:

workspace_id = userId
workspace_name = userName

第二步:生成标准 Markdown 报告

必须包含以下 frontmatter 字段:

字段说明
report_id格式:rpt_{YYYYMMDD}_{HHMMSS}_{hash}
title报告标题
source_ref原始链接
skill_name固定写 openclaw
generated_atISO 格式时间
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/ 全局目录

成功标准

  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=...