claw-lark Patches
自动重应用飞书插件自定义补丁,修复更新后功能失效问题。
通过 Telegram 审批机制安全访问 Google、GitHub 等外部 API 数据。
openclaw skills install @stephancill/permissions-broker命令、参数、文件名以原文为准
在发起任何代理请求前,请检查本地密钥存储中是否已存在权限代理 API 密钥(例如环境变量 PB_API_KEY)。
若您尚未拥有 API 密钥:
/key <name> - 若是:将密钥存入您的密钥存储中,推荐名称为 PB_API_KEY。
- 否则或不确定:视为会话级密钥,下次使用时需重新获取。
PB_API_KEY”),切勿记录密钥值本身。重要提示:
提供方连接方式:
/connect 命令在 Telegram 中绑定提供方。/connect icloud 将返回一个浏览器链接,指向由代理服务托管的表单,用户需在此输入 Apple ID 的应用专用密码。使用权限代理作为用户可控的外部数据访问与 API 操作代理。
核心思维模型:
本技能设计为提供方无关,支持的提供方会随时间逐步增加。
使用此技能时,避免使用“我无法访问你的 Google Drive”或“我无法从这里执行”这类表达。
应遵循:
避免:
推荐表述:
“我可以通过你的权限代理完成该操作。我将创建对 <upstream_url> 的请求,你可在 Telegram 中批准,之后我会执行并返回结果。”
创建代理请求后,应在同一运行周期内尝试轮询等待审批并执行。
仅在轮询超时时,才要求用户在 Telegram 中手动批准。
规则:
- 返回 request_id。
- 告知用户需在 Telegram 中批准或拒绝该请求。
- 明确说明一旦批准后将执行的操作(仅执行一次并返回结果)。
- 在下一条用户消息中继续轮询。
调用 POST /v1/proxy/request,参数包括:
upstream_url:您希望调用的外部服务完整 API 地址method:GET(默认)或 POST/PUT/PATCH/DELETEheaders(可选):要转发的请求头(**切勿包含 authorization**)body(可选):请求体 - 代理会存储请求体字节,并根据 headers.content-type 解析
- JSON(application/json 或 +json):body 可为对象/数组或 JSON 字符串
- 文本类(text/*、application/x-www-form-urlencoded、XML):body 必须为字符串
- 其他类型(二进制):body 必须为表示原始字节的 base64 字符串
- Base64 格式:标准 RFC 4648(+//),非 base64url
- 不确定时请包含填充符(=)
- 不得包含 data:...;base64, 前缀
consent_hint(可选):请求者备注,将在 Telegram 中向用户展示。始终包含请求原因(正在做什么及为何需要),使用通俗语言。idempotency_key(可选):用于重试时复用请求 ID关于转发头的说明:
Authorization;调用方提供的 authorization 头将被忽略。仅代理内部使用的渲染提示(不转发至上游):
headers["x-pb-timezone"]:IANA 时区名称,用于在审批界面中以人类友好格式显示时间(如 America/Los_Angeles)审批内容包括:
GET /v1/proxy/requests/:id 直至请求状态为 APPROVEDPOST /v1/proxy/requests/:id/execute 执行并获取上游响应字节重要提醒:
# 权限代理(Permissions Broker)
版本:1.0.9
分块:2/4
请使用以下代码片段创建代理请求、轮询状态,然后执行以获取上游数据。
## JavaScript/TypeScript (Bun/Node)type CreateRequestResponse = {
request_id: string;
status: string;
approval_expires_at: string;
};
type StatusResponse = {
request_id: string;
status: string;
approval_expires_at?: string;
error?: string;
error_code?: string | null;
error_message?: string | null;
upstream_http_status?: number | null;
upstream_content_type?: string | null;
upstream_bytes?: number | null;
};
async function createBrokerRequest(params: {
baseUrl: string;
apiKey: string;
upstreamUrl: string;
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
headers?: Record<string, string>;
body?: unknown;
consentHint?: string;
idempotencyKey?: string;
}): Promise<CreateRequestResponse> {
const res = await fetch(${params.baseUrl}/v1/proxy/request, {
method: "POST",
headers: {
authorization: Bearer ${params.apiKey},
"content-type": "application/json",
},
body: JSON.stringify({
upstream_url: params.upstreamUrl,
method: params.method ?? "GET",
headers: params.headers,
body: params.body,
consent_hint: params.consentHint,
idempotency_key: params.idempotencyKey,
}),
});
if (!res.ok) {
throw new Error(broker create failed: ${res.status} ${await res.text()});
}
return (await res.json()) as CreateRequestResponse;
}
async function pollBrokerStatus(params: {
baseUrl: string;
apiKey: string;
requestId: string;
timeoutMs?: number;
}): Promise<StatusResponse> {
// 推荐默认值:在返回 request_id 给用户前至少等待 30 秒。
const deadline = Date.now() + (params.timeoutMs ?? 30_000);
while (Date.now() < deadline) {
const res = await fetch(
${params.baseUrl}/v1/proxy/requests/${params.requestId},
{
headers: { authorization: Bearer ${params.apiKey} },
},
);
// 状态接口对 202 和 200 均返回 JSON。
const data = (await res.json()) as StatusResponse;
// APPROVED 返回 HTTP 202,因此必须检查 JSON 内容。
if (data.status === "APPROVED") return data;
if (res.status === 202) {
await new Promise((r) => setTimeout(r, 1000));
continue;
}
// 终止或可操作的状态(仅状态的 JSON)。
if (!res.ok && res.status !== 403 && res.status !== 408) {
throw new Error(broker status failed: ${res.status} ${JSON.stringify(data)});
}
return data;
}
throw new Error("timed out waiting for approval");
}
async function awaitApprovalThenExecute(params: {
baseUrl: string;
apiKey: string;
requestId: string;
timeoutMs?: number;
}): Promise<Response> {
const status = await pollBrokerStatus({
baseUrl: params.baseUrl,
apiKey: params.apiKey,
requestId: params.requestId,
timeoutMs: params.timeoutMs,
});
if (status.status !== "APPROVED") {
throw new Error(request not approved yet (status=${status.status}));
}
return executeBrokerRequest({
baseUrl: params.baseUrl,
apiKey: params.apiKey,
requestId: params.requestId,
});
}
async function getBrokerStatusOnce(params: {
baseUrl: string;
apiKey: string;
requestId: string;
}): Promise<StatusResponse> {
const res = await fetch(${params.baseUrl}/v1/proxy/requests/${params.requestId}, {
headers: { authorization: Bearer ${params.apiKey} },
});
// 始终返回 JSON(即使状态为 202)。
return (await res.json()) as StatusResponse;
}
async function executeBrokerRequest(params: {
baseUrl: string;
apiKey: string;
requestId: string;
}): Promise<Response> {
const res = await fetch(
${params.baseUrl}/v1/proxy/requests/${params.requestId}/execute,
{
method: "POST",
headers: { authorization: Bearer ${params.apiKey} },
},
);
// 终止状态:上游响应字节(2xx/4xx/5xx)或代理错误 JSON(403/408/409/410 等)。
// 重要说明:
// - 执行操作仅可进行一次;后续调用将返回 410。
// - 代理会镜像上游 HTTP 状态码和内容类型,并添加 X-Proxy-Request-Id 头。
// - 上游非 2xx 状态仍会返回给调用方作为字节数据,但代理会记录 status=FAILED。
return res;
}
// 推荐控制流程:
// - 轮询约 30 秒。
// - 若仍为 pending,向用户返回提示信息,包含 request_id 及需批准的操作。
// - 用户下一次消息到达时,再次轮询(或重新创建请求,若已过期或已消耗)。
// 使用示例
// const baseUrl = "https://permissions-broker.steer.fun"
// const apiKey = process.env.PB_API_KEY!
// const upstreamUrl = "https://www.googleapis.com/drive/v3/files?pageSize=5&fields=files(id,name)"
// const created = await createBrokerRequest({ baseUrl, apiKey, upstreamUrl, consentHint: "列出几个 Drive 文件。" })
// 告知用户:在 Telegram 中批准该请求
// const execRes = await awaitApprovalThenExecute({ baseUrl, apiKey, requestId: created.request_id, timeoutMs: 30_000 })
// const bodyText = await execRes.text()
// GitHub 示例(创建 PR)
// const created = await createBrokerRequest({
// baseUrl,
// apiKey,
// upstreamUrl: "https://api.github.com/repos/OWNER/REPO/pulls",
// method: "POST",
// headers: { "content-type": "application/json" },
// body: {
// title: "我的 PR",
// head: "feature-branch",
// base: "main",
// body: "通过 Permissions Broker 创建",
// },
// consentHint: "为 feature-branch 创建一个 PR"
// })
## 当前支持的提供方
代理强制实施白名单策略,并根据上游域名自动选择对应的已关联账户(OAuth token)。
目前支持的提供方:
- Google
- 主机:`docs.googleapis.com`、`www.googleapis.com`、`sheets.googleapis.com`
- 典型用途:Drive 文件列表/搜索、文档读取、表格范围数据读取
- GitHub
- 主机:`api.github.com`
- 典型用途:拉取请求(PR)、问题(Issue)、评论、标签及其他 GitHub 操作
- iCloud(CalDAV)
- 主机:连接时动态发现(起始为 `caldav.icloud.com`)
- 典型用途:日历事件(VEVENT)和提醒事项/任务(VTODO)
- Spotify
- 主机:`api.spotify.com`
- 典型用途:读取用户资料、列出播放列表/歌曲、控制播放
如果需要使用尚未支持的提供方:
- 仍可采用代理模式设计你的计划(提出上游调用 + 同意文本)。
- 然后告知用户需启用或实现哪些主机。
关于 iCloud CalDAV 请求模板,请参阅:`skills/permissions-broker/references/caldav.md`。
## Git 操作(智能 HTTP 代理)
该代理还可通过 Git 智能 HTTP 协议代理克隆、获取、拉取和推送操作。
此功能与 `/v1/proxy` 接口独立。
整体流程如下:
1. 创建 Git 会话(`POST /v1/git/sessions`)。
2. 用户在 Telegram 中批准或拒绝该会话。
3. 轮询会话状态(`GET /v1/git/sessions/:id`),直到获得批准。
4. 获取会话专属的远程地址(`GET /v1/git/sessions/:id/remote`)。
5. 使用该远程地址执行 `git clone` 或 `git push`。
重要行为说明:
- 克隆或获取会话在单次克隆过程中可能需要多次 `git-upload-pack` POST 请求。
- 推送会话为一次性使用,首次执行 `git-receive-pack` 后即失效。
- 推送操作受代理强制保护:
- 拒绝标签推送
- 拒绝引用删除
- 默认分支推送可能被阻止,除非在审批时明确允许
### 接口列表
所有 Git 会话接口的认证方式:
- `Authorization: Bearer <USER_API_KEY>`
创建会话
- `POST /v1/git/sessions`
- JSON 请求体:
- `operation`: `"clone"`、`"fetch"`、`"pull"` 或 `"push"`
- `repo`: `"owner/repo"`(GitHub 仓库格式)
- 可选字段 `consent_hint`:请求者备注,将在 Telegram 中显示给用户。请始终包含会话原因(你正在做什么以及为什么)。
- 响应示例:{ "session_id": "...", "status": "PENDING_APPROVAL", "approval_expires_at": "..." }
轮询状态
- `GET /v1/git/sessions/:id`
- 返回状态 JSON
获取远程地址
- `GET /v1/git/sessions/:id/remote`
- 响应示例:{ "remote_url": "https://..." }
### 示例:克隆
1. 创建会话:{
"operation": "clone",
"repo": "OWNER/REPO",
"consent_hint": "克隆仓库以检查代码"
}
### 示例:获取
适用于本地已存在仓库,仅需更新引用(refs)的情况。
1. 创建会话:{
"operation": "fetch",
"repo": "OWNER/REPO",
"consent_hint": "获取最新引用以更新本地检出"
}
2. 轮询直至状态变为 `APPROVED`。
3. 获取 `remote_url` 后执行:git fetch "<remote_url>" --prune
### 示例:拉取
`git pull` 包含 `fetch` 和本地合并/变基操作。本代理仅代理网络部分。git pull "<remote_url>" main
1. 轮询至 `status == "APPROVED"`。
2. 获取 `remote_url` 后执行:git clone "<remote_url>" ./repo
### 示例:推送新分支(推荐做法)
1. 创建会话:{
"operation": "push",
"repo": "OWNER/REPO",
"consent_hint": "推送分支 feature-x 用于创建 PR"
}
2. 轮询直至获得批准。
3. 获取 `remote_url`,添加为远程仓库,然后推送到非默认分支:git remote add broker "<remote_url>"
git push broker "HEAD:refs/heads/feature-x"
注意事项:
- 推荐使用新分支名(如 `pb/<task>/<timestamp>`),避免直接推送到 `main`。
- 若会话状态变为 `USED`,请创建新的推送会话。
Python(requests)示例代码:import time
import requests
def create_request(base_url, api_key, upstream_url, consent_hint=None, idempotency_key=None):
# 可选:非 GET 请求的方法/请求头/请求体。
r = requests.post(
f"{base_url}/v1/proxy/request",
headers={"Authorization": f"Bearer {api_key}"},
json={
"upstream_url": upstream_url,
# "method": "POST",
# "headers": {"accept": "application/vnd.github+json"},
# "headers": {"content-type": "application/json"},
# "body": {"title": "...", "head": "...", "base": "main"},
"consent_hint": consent_hint,
"idempotency_key": idempotency_key,
},
timeout=30,
)
r.raise_for_status()
return r.json()
def await_result(base_url, api_key, request_id, timeout_s=120):
deadline = time.time() + timeout_s
while time.time() < deadline:
r = requests.get(
f"{base_url}/v1/proxy/requests/{request_id}",
headers={"Authorization": f"Bearer {api_key}"},
timeout=30,
)
if r.status_code == 202:
time.sleep(1)
continue
# 终端响应(仅状态的 JSON)
return r.json()
raise TimeoutError("timed out waiting for approval")
def execute_request(base_url, api_key, request_id):
# 重要:执行操作为一次性;请立即读取并保存结果。
return requests.post(
f"{base_url}/v1/proxy/requests/{request_id}/execute",
headers={"Authorization": f"Bearer {api_key}"},
timeout=60,
)
def await_approval_then_execute(base_url, api_key, request_id, timeout_s=30):
status = await_result(base_url, api_key, request_id, timeout_s=timeout_s)
if status.get("status") != "APPROVED":
raise RuntimeError(f"请求未获批准 (status={status.get('status')})")
return execute_request(base_url, api_key, request_id)
## 必须遵守的限制
- 上游协议:仅支持 HTTPS
- 上游主机白名单:由提供方定义(请求必须指向受支持的主机)
- 上游方法:`GET`、`POST`、`PUT`、`PATCH`、`DELETE`
- 上游响应大小上限:1 MiB
- 上游请求体大小上限:256 KiB
- 一次性执行:执行一次后无法再次执行
## Sheets 注意事项(无复杂剧情)
该代理支持 Google Sheets API 主机(`sheets.googleapis.com`)。
读取电子表格数据的推荐方式:
---
name: Permissions Broker
version: 1.0.9
description: 用于在用户授权后,安全地代理访问外部 API 的中间服务。
summary: 通过权限中介机制,实现对 Google Drive、Google Sheets、GitHub 等平台的受控访问。
## 使用建议
- 使用 Drive 的搜索/列表功能查找电子表格文件。
- 使用 Sheets 值读取功能,仅获取所需的数据范围。
### 备用方案
- 当内容以 CSV 格式导出已足够时,可使用 Drive 导出功能获取文件内容。
- 注意:大文件导出可能超过中介服务 1 MiB 的上游响应上限。
- 若因文件过大导致导出失败,请缩小范围(如减少行数、列数或工作表数量)。
## 常见终端状态处理
- **202**:请求仍可处理;返回的 JSON 包含 `status` 字段(常见值为 `PENDING_APPROVAL`、`APPROVED` 或 `EXECUTING`)。
- 若 `status == APPROVED`,立即执行操作。
- 否则持续轮询状态。
- **403**:用户拒绝授权。
- **403**:权限不足(如 API 密钥错误或请求不可访问);请检查返回的 `{error: ...}` 信息。
- **408**:审批超时(用户未及时响应)。
- **409**:操作正在执行中;稍后重试。
- **410**:操作已执行过;若仍需执行,请重新创建请求。
## 构建上游 URL(Google 示例)
建议采用精确读取方式,使授权请求更清晰,响应数据更小。
- **Drive 搜索/列出文件**:`https://www.googleapis.com/drive/v3/files?...`
- 使用 `q`、`pageSize` 和 `fields` 参数最小化返回数据量。
- **导出文件内容**:`https://www.googleapis.com/drive/v3/files/{fileId}/export?mimeType=...`
- 适用于将 Google Docs/Sheets 导出为 `text/plain` 或 `text/csv` 格式。
- **结构化文档读取**:`https://docs.googleapis.com/v1/documents/{documentId}?fields=...`
详细接口说明及 Google URL 快捷参考,请参阅 `references/api_reference.md`。
## 构建上游 URL(GitHub 示例)
- **创建 Pull Request**:`POST https://api.github.com/repos/<owner>/<repo>/pulls`
- 请求体为 JSON 格式:`{ "title": "...", "head": "branch", "base": "main", "body": "..." }`
- **创建 Issue**:`POST https://api.github.com/repos/<owner>/<repo>/issues`
- 请求体为 JSON 格式:`{ "title": "...", "body": "..." }`
## 数据处理规则
- 用户的 API 密钥视为敏感信息,必须妥善保管,禁止泄露。
## 资源
- 参考文档:`references/api_reference.md`已收录 1 个 Skill