Poco 使用手册
外部集成

WebHook

本文档说明 Poco 中 WebHook 的触发时机、签名机制与 Python 接收端实现示例。

1. 触发时机与范围

  • 事件类型:
  • session.completed:对话执行完成(对应前端 run.done
  • session.failed:对话执行失败
  • session.stopped:对话被用户停止
  • scheduled_task.run_status_changed:定时任务运行状态变化
  • user_input.requested:执行过程请求用户输入
  • user_input.answered:用户完成输入回答
  • workbench.insight_status_changed:工作台洞察状态变化
  • publish_page.published:发布中心成功生成一个新的发布页版本
  • publish_page.failed:发布页自动发布失败
  • workspace_export.completed:工作区文件导出完成,可读取文件清单与归档信息
  • 生效范围:
  • global:所有事件
  • project:仅指定项目下的事件(优先匹配事件关联的 session.project_id

2. 配置项

在设置页 WebHook 中可配置:

  • 名称
  • 推送地址(HTTP/HTTPS)
  • 订阅事件(见上方事件类型)
  • 生效范围(全局/指定项目)
  • 会话类型过滤(仅对话类事件生效,包括 session.*workspace_export.completed,可选 chat / api / scheduled
  • 签名密钥(可选)
  • 超时(毫秒)
  • 重试次数
  • 重试退避(毫秒)

管理员可在 WebHook 页面切换到“系统 WebHook”,或通过 GET /api/v1/webhooks?owner_scope=system 管理系统级 WebHook。系统级 WebHook 接收所有用户的匹配事件,仅支持全局生效范围;请求体中的 user 字段表示事件归属用户,webhook.owner_scope 表示本次投递来自用户级还是系统级 WebHook。

3. HTTP 请求格式

3.1 请求头

  • Content-Type: application/json
  • User-Agent: Poco-Webhook/1.0
  • X-Poco-Event: <event_type>
  • X-Poco-Event-Id: <event_id>
  • X-Poco-Timestamp: <ISO8601 时间>
  • X-Poco-Signature: sha256=<hex>(仅配置了签名密钥时发送)

3.2 对话完成请求体(示例)

{
  "schema_version": "v1",
  "event_type": "session.completed",
  "event_id": "2d3134c5-...",
  "occurred_at": "2026-02-15T14:30:00+00:00",
  "is_test": false,
  "user": {
    "user_id": "1234567890",
    "username": "alice",
    "full_name": "Alice",
    "department": "研发",
    "role": "user"
  },
  "webhook": {
    "id": 12,
    "owner_scope": "system"
  },
  "session": {
    "session_id": "5f6a...",
    "title": "日报总结",
    "kind": "chat",
    "status": "completed",
    "project_id": "97d2...",
    "created_at": "2026-02-15T14:20:00+00:00",
    "updated_at": "2026-02-15T14:30:00+00:00"
  },
  "run": {
    "run_id": "6dc9...",
    "turn_id": "20cf...",
    "status": "completed",
    "schedule_mode": "immediate",
    "scheduled_task_id": null,
    "started_at": "2026-02-15T14:20:10+00:00",
    "finished_at": "2026-02-15T14:29:58+00:00"
  },
  "summary": {
    "has_artifacts": true,
    "has_assistant_response": true,
    "message_count": 9,
    "assistant_message_count": 4,
    "tool_execution_count": 3,
    "total_cost_usd": 0.012345,
    "input_tokens": 2345,
    "output_tokens": 1234,
    "duration_ms": 588000
  },
  "meta": {
    "trigger": "run.done",
    "source": "poco.backend.callback.completed",
    "idempotency_key": "2d3134c5-...",
    "capability_profile_version": 3,
    "model_stop_reason": "end_turn"
  }
}

3.3 发布页新增版本请求体(示例)

{
  "schema_version": "v1",
  "event_type": "publish_page.published",
  "event_id": "7ec2d1a1-...",
  "occurred_at": "2026-02-15T14:35:00+00:00",
  "is_test": false,
  "user": {
    "user_id": "1234567890",
    "username": "alice",
    "full_name": "Alice",
    "department": "研发",
    "role": "user"
  },
  "webhook": {
    "id": 13,
    "owner_scope": "user"
  },
  "publish_page": {
    "publish_center_id": "8c01...",
    "name": "日报发布",
    "publish_key": "daily-report",
    "source_type": "scheduled_task",
    "category": "日报",
    "public_to_board_enabled": true
  },
  "version": {
    "version_no": 3,
    "session_id": "5f6a...",
    "entry_path": "/report.html",
    "share_code": "AbC123xYz0",
    "published_url": "/share/p/daily-report",
    "trigger_type": "auto",
    "reason": "AI selected the first publishable report"
  },
  "session": {
    "session_id": "5f6a...",
    "title": "日报总结",
    "kind": "scheduled",
    "project_id": "97d2..."
  },
  "meta": {
    "trigger": "publish_page.published",
    "source": "poco.backend.publish_center",
    "idempotency_key": "7ec2d1a1-..."
  }
}

3.4 其他事件请求体字段

以下事件均保持统一外层结构:schema_versionevent_typeevent_idoccurred_atis_testuserwebhookmeta。接收端建议优先用 event_idmeta.idempotency_key 做幂等处理。user 表示事件归属用户,系统级 WebHook 可用该字段区分不同用户的事件;webhook.id 表示本次投递使用的 WebHook 配置 ID,webhook.owner_scopeusersystem

事件主要字段说明
session.failedsessionrunmeta.error_message对话失败时推送会话、运行、错误信息
session.stoppedsessionrunmeta.error_message用户停止对话时推送会话、运行、停止原因
scheduled_task.run_status_changedscheduled_taskrunsession定时任务运行状态从 run.previous_status 变为 run.status 时推送
user_input.requesteduser_inputsession请求用户输入时推送 tool_nametool_input、过期时间
user_input.answereduser_inputsession用户回答后推送 answersanswered_at
workbench.insight_status_changedinsightmeta.trigger_source工作台洞察状态变化时推送标题、摘要、优先级、前后状态
publish_page.failedpublish_pagejoberrorsession发布页自动发布失败时推送发布页、任务、错误信息
workspace_export.completedworkspace_exportsessionrun工作区文件导出完成时推送清单、归档和关联会话信息

session.failed / session.stoppedsessionrun 字段与 session.completed 基本一致,但 status 分别为 failed / stopped,并会在 run.last_errormeta.error_message 中携带错误或停止原因。

scheduled_task.run_status_changed 的核心字段:

字段说明
scheduled_task.scheduled_task_id定时任务 ID
scheduled_task.name定时任务名称
scheduled_task.cronCron 表达式
scheduled_task.timezone定时任务时区
run.run_id本次运行 ID
run.session_id本次运行关联会话 ID
run.status当前运行状态
run.previous_status上一个运行状态,可能为 null
run.last_error失败原因,可能为 null
session.project_id关联项目 ID,可能为 null

user_input.requested / user_input.answered 的核心字段:

字段说明
user_input.request_id用户输入请求 ID
user_input.session_id关联会话 ID
user_input.tool_name发起请求的工具名
user_input.tool_input请求输入时的工具参数
user_input.status请求状态,通常为 pendinganswered
user_input.answers用户回答内容,未回答时为 null
user_input.expires_at请求过期时间
user_input.answered_at回答时间,未回答时为 null

publish_page.failed 的核心字段:

字段说明
publish_page.publish_center_id发布中心 ID
publish_page.name发布页名称
publish_page.publish_key发布页公开访问 key
job.job_id自动发布任务 ID,可能为 null
job.run_id关联运行 ID,可能为 null
job.scheduled_task_id关联定时任务 ID,可能为 null
job.status发布任务状态
job.attempt_count已尝试次数
job.max_attempts最大尝试次数
job.last_error发布任务最后一次错误
error.message本次失败错误信息

workspace_export.completed 的请求体示例:

{
  "schema_version": "v1",
  "event_type": "workspace_export.completed",
  "event_id": "f1b5a6d4-...",
  "occurred_at": "2026-02-15T14:32:00+00:00",
  "is_test": false,
  "user": {
    "user_id": "1234567890",
    "username": "alice",
    "full_name": "Alice",
    "department": "研发",
    "role": "user"
  },
  "webhook": {
    "id": 14,
    "owner_scope": "user"
  },
  "workspace_export": {
    "status": "ready",
    "files_prefix": "workspaces/user-1/session/files",
    "manifest_key": "workspaces/user-1/session/manifest.json",
    "archive_key": "workspaces/user-1/session/archive.zip",
    "archive_available": true
  },
  "session": {
    "session_id": "5f6a...",
    "title": "日报总结",
    "kind": "chat",
    "status": "completed",
    "project_id": "97d2...",
    "created_at": "2026-02-15T14:20:00+00:00",
    "updated_at": "2026-02-15T14:32:00+00:00"
  },
  "run": {
    "run_id": "6dc9...",
    "turn_id": "20cf...",
    "status": "completed",
    "schedule_mode": "immediate",
    "scheduled_task_id": null,
    "started_at": "2026-02-15T14:20:10+00:00",
    "finished_at": "2026-02-15T14:29:58+00:00"
  },
  "meta": {
    "trigger": "workspace_export.completed",
    "source": "poco.backend.callback.workspace_export",
    "idempotency_key": "f1b5a6d4-...",
    "capability_profile_version": 3
  }
}

workspace_export.completed 的核心字段:

字段说明
workspace_export.status导出状态,当前完成事件固定为 ready
workspace_export.files_prefix工作区文件对象前缀,可能为 null
workspace_export.manifest_key工作区文件清单对象 key,可能为 null
workspace_export.archive_key工作区归档包对象 key,可能为 null
workspace_export.archive_available是否存在归档包
session.session_id关联会话 ID
session.kind关联会话类型,chat / api / scheduled
session.project_id关联项目 ID,可能为 null
run.run_id关联运行 ID,未解析到运行时为 null
run.turn_id关联轮次 ID,未解析到运行时为 null
run.schedule_mode运行调度模式,可能为 null
run.scheduled_task_id关联定时任务 ID,可能为 null
meta.idempotency_key幂等 key,当前与 event_id 一致

该事件只暴露后端资源引用,不直接生成公开访问链接;接收端如果需要下载或读取工作区文件,应结合 API 访问能力使用这些 key 换取文件内容。

3.5 发布页事件可用数据

WebHook 接收端可直接从 publish_page.published 请求体拿到以下数据:

字段说明
event_id事件 ID,可用于幂等处理
occurred_at事件发生时间,对应本次版本发布时间
publish_page.publish_center_id发布中心 ID
publish_page.name发布页名称
publish_page.publish_key发布页公开访问 key
publish_page.source_type发布来源类型,scheduled_taskexisting_session
publish_page.category发布页分类,可能为 null
publish_page.public_to_board_enabled是否公开到发布广场
version.version_no本次发布版本号
version.session_id本次发布关联的会话 ID
version.entry_path本次发布入口文件路径
version.share_code本次发布版本的分享码
version.published_url发布页访问路径
version.trigger_type发布触发方式,manualauto
version.reason发布原因,可能为 null
session.session_id关联会话 ID
session.title关联会话标题
session.kind关联会话类型
session.project_id关联项目 ID,可能为 null
meta.idempotency_key幂等 key,当前与 event_id 一致

publish_page.published 事件本身只携带发布页标识、版本标识和关联会话信息;如果接收端需要展示或读取发布文件内容,可以使用事件中的 publish_page.publish_keyversion.version_noversion.share_code 调用公开接口。

公开接口响应均使用统一响应结构,以下字段位于 data

GET /api/v1/public/publishes/{publish_key} 可拿到当前发布页数据:

字段说明
publish_key发布页公开访问 key,对应 /share/p/{publish_key}
name发布页名称
description发布页描述,可能为 null
category发布页分类,可能为 null
origin发布来源标识
tags发布页标签数组
public_to_board_enabled是否公开到发布广场
latest_share_code最新版本的分享码
latest_entry_path最新版本入口文件路径
latest_published_at最新发布时间
distraction_free_mode_enabled是否开启沉浸阅读模式
share_type分享资源类型
workspace_file最新版本入口文件,可能为 null

workspace_file 字段结构:

字段说明
name文件名
path文件路径
mimeType文件 MIME 类型,可能为 null
url文件预览地址
downloadUrl文件下载地址

GET /api/v1/public/publishes/{publish_key}/versions 可拿到发布页版本列表,支持 limitoffsetversion_nodatedate_fromdate_totz 查询参数:

字段说明
version_no版本号
share_code当前版本分享码
entry_path当前版本入口文件路径
published_at当前版本发布时间,可能为 null
is_latest是否为最新版本

GET /api/v1/public/publishes/{publish_key}/versions/{version_no}/files 可拿到指定版本的文件列表,返回 FileNode[]

字段说明
id文件节点 ID
name文件或文件夹名称
type节点类型,filefolder
path节点路径
children子节点列表,文件节点通常为 null
url预览地址,文件节点可能存在
downloadUrl下载地址,文件节点可能存在
mimeTypeMIME 类型,文件节点可能存在
oss_status对象存储状态,可能为 null
oss_meta对象存储元数据,可能为 null

如果需要按 version.share_code 读取事件对应版本的入口文件,可调用 GET /api/v1/public/shares/{share_code},其中 workspace_file 与上方结构一致。

3.6 测试推送排查技巧(推荐带 session_id

为避免出现“测试推送成功,但真实任务完成后收不到”的误判,建议测试时带上真实会话 session_id,让后端按真实过滤条件做匹配校验(enabled / event_types / session_kinds / scope / project_id)。

推荐操作:

  1. 进入目标会话页面(URL 形如 /chat/{session_id})。
  2. 打开设置 -> WebHook -> 点击“测试推送”。
  3. 系统会自动携带当前 session_id 发起测试。

也可以直接调用接口:

curl -X POST 'http://<backend>/api/v1/webhooks/<webhook_id>/test' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <token>' \
  -d '{"event_type":"session.completed","session_id":"<session_uuid>"}'

测试发布页事件:

curl -X POST 'http://<backend>/api/v1/webhooks/<webhook_id>/test' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <token>' \
  -d '{"event_type":"publish_page.published","session_id":"<session_uuid>"}'

测试工作区文件导出完成事件:

curl -X POST 'http://<backend>/api/v1/webhooks/<webhook_id>/test' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <token>' \
  -d '{"event_type":"workspace_export.completed","session_id":"<session_uuid>"}'

返回字段说明(data):

  • delivered: 是否成功发到目标 URL(HTTP 2xx)
  • matched: 是否通过了 WebHook 过滤规则
  • filtered_reason: 未匹配时的原因(匹配成功时为 null
  • tested_session_id: 本次用于匹配的会话 ID
  • tested_session_kind: 本次会话类型(chat / api / scheduled
  • tested_project_id: 本次会话所属项目 ID(可能为 null

常见 filtered_reason

  • event_type_filtered:WebHook 没有订阅本次测试事件
  • session_kind_filtered:会话类型不在 WebHook 勾选范围内
  • session_project_missing:WebHook 是项目级,但该会话没有项目
  • project_mismatch:会话项目与 WebHook 指定项目不一致
  • disabled:该 WebHook 已禁用

说明:

  • 不传 session_id 时,测试会使用默认上下文(kind=chatproject_id=null),仅用于连通性验证,不代表真实任务完成场景一定可达。

4. 签名机制与 Python 示例

签名算法:

  1. 取请求头 X-Poco-Timestamp,记为 timestamp
  2. 取 HTTP 原始请求体字节,记为 body_bytes
  3. 拼接:timestamp + "." + body_bytes
  4. 使用 HMAC-SHA256(密钥为你的 WebHook 密钥)
  5. 十六进制输出后加前缀:sha256=<hexdigest>

4.1 Python 生成签名(演示)

import hashlib
import hmac


def build_signature(secret: str, timestamp: str, body_bytes: bytes) -> str:
    signing_payload = timestamp.encode("utf-8") + b"." + body_bytes
    digest = hmac.new(
        secret.encode("utf-8"),
        signing_payload,
        hashlib.sha256,
    ).hexdigest()
    return f"sha256={digest}"

4.2 Python 验签(推荐)

import hashlib
import hmac


def verify_signature(
    secret: str,
    timestamp: str,
    body_bytes: bytes,
    signature_from_header: str,
) -> bool:
    expected = build_signature(secret, timestamp, body_bytes)
    return hmac.compare_digest(expected, signature_from_header or "")

5. Python 完整接收端示例(FastAPI)

示例包含:验签、时间窗口校验、幂等去重(内存版)、事件分发。

from __future__ import annotations

import hashlib
import hmac
import json
from datetime import datetime, timedelta, timezone
from typing import Any

from fastapi import FastAPI, Header, HTTPException, Request

app = FastAPI()

# 请改成你自己的密钥;如果你在 Poco 中没配置密钥,可关闭验签逻辑。
WEBHOOK_SECRET = "replace-with-your-secret"

# 简单幂等缓存(生产建议换 Redis/DB,并设置过期时间)
PROCESSED_EVENT_IDS: set[str] = set()


def build_signature(secret: str, timestamp: str, body_bytes: bytes) -> str:
    signing_payload = timestamp.encode("utf-8") + b"." + body_bytes
    digest = hmac.new(
        secret.encode("utf-8"),
        signing_payload,
        hashlib.sha256,
    ).hexdigest()
    return f"sha256={digest}"


def verify_signature(secret: str, timestamp: str, body_bytes: bytes, signature: str) -> bool:
    expected = build_signature(secret, timestamp, body_bytes)
    return hmac.compare_digest(expected, signature or "")


def verify_timestamp(timestamp: str, max_skew_minutes: int = 5) -> None:
    try:
        ts = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
    except ValueError as exc:
        raise HTTPException(status_code=400, detail="Invalid X-Poco-Timestamp") from exc

    now = datetime.now(timezone.utc)
    if abs(now - ts.astimezone(timezone.utc)) > timedelta(minutes=max_skew_minutes):
        raise HTTPException(status_code=400, detail="Timestamp skew too large")


@app.post("/webhook/poco")
async def receive_poco_webhook(
    request: Request,
    x_poco_event: str | None = Header(default=None, alias="X-Poco-Event"),
    x_poco_event_id: str | None = Header(default=None, alias="X-Poco-Event-Id"),
    x_poco_timestamp: str | None = Header(default=None, alias="X-Poco-Timestamp"),
    x_poco_signature: str | None = Header(default=None, alias="X-Poco-Signature"),
) -> dict[str, Any]:
    if not x_poco_event or not x_poco_event_id or not x_poco_timestamp:
        raise HTTPException(status_code=400, detail="Missing webhook headers")

    body_bytes = await request.body()

    # 如果设置了密钥,执行验签
    if WEBHOOK_SECRET:
        if not x_poco_signature:
            raise HTTPException(status_code=401, detail="Missing signature")
        verify_timestamp(x_poco_timestamp, max_skew_minutes=5)
        if not verify_signature(WEBHOOK_SECRET, x_poco_timestamp, body_bytes, x_poco_signature):
            raise HTTPException(status_code=401, detail="Invalid signature")

    # 解析 JSON
    try:
        payload = json.loads(body_bytes.decode("utf-8"))
    except json.JSONDecodeError as exc:
        raise HTTPException(status_code=400, detail="Invalid JSON payload") from exc

    # 幂等处理
    if x_poco_event_id in PROCESSED_EVENT_IDS:
        return {"ok": True, "duplicate": True}
    PROCESSED_EVENT_IDS.add(x_poco_event_id)

    # 事件路由
    if x_poco_event == "session.completed":
        session_id = payload.get("session", {}).get("session_id")
        title = payload.get("session", {}).get("title")
        kind = payload.get("session", {}).get("kind")
        has_artifacts = payload.get("summary", {}).get("has_artifacts")
        # TODO: 在这里写你的业务逻辑(入库、推送消息、触发任务等)
        print(
            "[Webhook] session.completed",
            f"session_id={session_id}",
            f"title={title}",
            f"kind={kind}",
            f"has_artifacts={has_artifacts}",
        )
    elif x_poco_event == "publish_page.published":
        publish_page = payload.get("publish_page", {})
        version = payload.get("version", {})
        print(
            "[Webhook] publish_page.published",
            f"publish_key={publish_page.get('publish_key')}",
            f"version_no={version.get('version_no')}",
            f"entry_path={version.get('entry_path')}",
        )
    else:
        print("[Webhook] ignored event:", x_poco_event)

    return {"ok": True}

运行:

pip install fastapi uvicorn
uvicorn webhook_receiver:app --host 0.0.0.0 --port 8000 --reload

On this page