evidence-upload
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chineseevidence-upload
证据上传
Upload a local file to the pylot assets backend (org-fenced S3) and get a stable, camo-safe public URL. and are already in every operator/worker env — no static AWS keys needed.
$PYLOT_GATEWAY_URL$PYLOT_DISPATCH_TOKENOptionally, pass (and ) to also attach the published asset to a chat conversation — it renders inline in the thread and Claude sees it on the conversation's next turn. See Attaching to a conversation below; without the flag, the flow is exactly the 4 steps that follow.
--conversation <conversation-id>--alt "<text>"将本地文件上传至pylot资源后端(组织隔离的S3存储),获取稳定且经camo安全处理的公共URL。和已配置在所有操作员/工作环境中——无需静态AWS密钥。
$PYLOT_GATEWAY_URL$PYLOT_DISPATCH_TOKEN你可选择传入(以及),将已发布的资源附加到聊天对话中——它会在线程中内联显示,且Claude会在对话的下一轮看到该图片。请查看下方的【附加到对话(--conversation)】部分;若不使用该标志,流程将严格遵循以下4个步骤。
--conversation <conversation-id>--alt "<text>"Allowlist
允许列表
| Type | MIME types | Max size |
|---|---|---|
| Images | | 25 MB |
| Video | | 25 MB |
Anything outside this list will get a 400 from . Capture screenshots as PNG; convert large GIFs to MP4 if they exceed 25 MB.
/assets/presign| 类型 | MIME 类型 | 最大大小 |
|---|---|---|
| 图片 | | 25 MB |
| 视频 | | 25 MB |
不在此列表中的文件会被接口返回400错误。请将截图保存为PNG格式;若大型GIF超过25 MB,请转换为MP4格式。
/assets/presignThe 4-step flow
四步流程
bash
undefinedbash
undefinedVariables you must set before running:
Variables you must set before running:
FILE=/path/to/screenshot.png
FILE=/path/to/screenshot.png
CONTENT_TYPE=image/png # must match allowlist
CONTENT_TYPE=image/png # must match allowlist
REPO=org/repo-name # scopes the org fence
REPO=org/repo-name # scopes the org fence
BYTES=$(wc -c < "$FILE")
BYTES=$(wc -c < "$FILE")
1. Presign — returns asset_id + a short-lived S3 upload URL
1. Presign — returns asset_id + a short-lived S3 upload URL
PRESIGN=$(curl -sS -X POST "$PYLOT_GATEWAY_URL/assets/presign"
-H "Authorization: Bearer $PYLOT_DISPATCH_TOKEN"
-H "Content-Type: application/json"
-d "{"repo":"$REPO","content_type":"$CONTENT_TYPE","size":$BYTES}") ASSET_ID=$(echo "$PRESIGN" | python3 -c "import sys,json; print(json.load(sys.stdin)['asset_id'])") UPLOAD_URL=$(echo "$PRESIGN" | python3 -c "import sys,json; print(json.load(sys.stdin)['upload_url'])")
-H "Authorization: Bearer $PYLOT_DISPATCH_TOKEN"
-H "Content-Type: application/json"
-d "{"repo":"$REPO","content_type":"$CONTENT_TYPE","size":$BYTES}") ASSET_ID=$(echo "$PRESIGN" | python3 -c "import sys,json; print(json.load(sys.stdin)['asset_id'])") UPLOAD_URL=$(echo "$PRESIGN" | python3 -c "import sys,json; print(json.load(sys.stdin)['upload_url'])")
PRESIGN=$(curl -sS -X POST "$PYLOT_GATEWAY_URL/assets/presign"
-H "Authorization: Bearer $PYLOT_DISPATCH_TOKEN"
-H "Content-Type: application/json"
-d "{"repo":"$REPO","content_type":"$CONTENT_TYPE","size":$BYTES}") ASSET_ID=$(echo "$PRESIGN" | python3 -c "import sys,json; print(json.load(sys.stdin)['asset_id'])") UPLOAD_URL=$(echo "$PRESIGN" | python3 -c "import sys,json; print(json.load(sys.stdin)['upload_url'])")
-H "Authorization: Bearer $PYLOT_DISPATCH_TOKEN"
-H "Content-Type: application/json"
-d "{"repo":"$REPO","content_type":"$CONTENT_TYPE","size":$BYTES}") ASSET_ID=$(echo "$PRESIGN" | python3 -c "import sys,json; print(json.load(sys.stdin)['asset_id'])") UPLOAD_URL=$(echo "$PRESIGN" | python3 -c "import sys,json; print(json.load(sys.stdin)['upload_url'])")
2. Direct PUT to S3 (no auth header — the presigned URL carries credentials)
2. Direct PUT to S3 (no auth header — the presigned URL carries credentials)
curl -sS -X PUT "$UPLOAD_URL"
--data-binary @"$FILE"
-H "Content-Type: $CONTENT_TYPE"
--data-binary @"$FILE"
-H "Content-Type: $CONTENT_TYPE"
curl -sS -X PUT "$UPLOAD_URL"
--data-binary @"$FILE"
-H "Content-Type: $CONTENT_TYPE"
--data-binary @"$FILE"
-H "Content-Type: $CONTENT_TYPE"
3. Publish → stable public URL (camo-proxied, safe to embed in GitHub)
3. Publish → stable public URL (camo-proxied, safe to embed in GitHub)
PUBLIC_URL=$(curl -sS -X PATCH "$PYLOT_GATEWAY_URL/assets/$ASSET_ID"
-H "Authorization: Bearer $PYLOT_DISPATCH_TOKEN"
-H "Content-Type: application/json"
-d '{"visibility":"public"}'
| python3 -c "import sys,json; print(json.load(sys.stdin)['public_url'])")
-H "Authorization: Bearer $PYLOT_DISPATCH_TOKEN"
-H "Content-Type: application/json"
-d '{"visibility":"public"}'
| python3 -c "import sys,json; print(json.load(sys.stdin)['public_url'])")
PUBLIC_URL=$(curl -sS -X PATCH "$PYLOT_GATEWAY_URL/assets/$ASSET_ID"
-H "Authorization: Bearer $PYLOT_DISPATCH_TOKEN"
-H "Content-Type: application/json"
-d '{"visibility":"public"}'
| python3 -c "import sys,json; print(json.load(sys.stdin)['public_url'])")
-H "Authorization: Bearer $PYLOT_DISPATCH_TOKEN"
-H "Content-Type: application/json"
-d '{"visibility":"public"}'
| python3 -c "import sys,json; print(json.load(sys.stdin)['public_url'])")
4. Embed in PR / issue / report body:
4. Embed in PR / issue / report body:
echo "
"
echo "
"
Optional — revoke public access when no longer needed:
Optional — revoke public access when no longer needed:
curl -sS -X PATCH "$PYLOT_GATEWAY_URL/assets/$ASSET_ID" \
curl -sS -X PATCH "$PYLOT_GATEWAY_URL/assets/$ASSET_ID" \
-H "Authorization: Bearer $PYLOT_DISPATCH_TOKEN" \
-H "Authorization: Bearer $PYLOT_DISPATCH_TOKEN" \
-H "Content-Type: application/json" \
-H "Content-Type: application/json" \
-d '{"visibility":"org"}'
-d '{"visibility":"org"}'
undefinedundefinedAttaching to a conversation (--conversation
)
--conversation附加到对话(--conversation)
Requires a gateway with fellowship-dev/pylot#1950 deployed — older gateways 400 on the extra fields (see error handling for the fallback). The plain 4-step flow above is unaffected either way.
When invoked with (and optionally ), run steps 1–2 as above, then replace step 3 with:
--conversation <conversation-id>--alt "<text>"bash
undefined需要部署了fellowship-dev/pylot#1950的网关——旧版网关会因额外字段返回400错误(请查看错误处理中的回退方案)。上述的基础四步流程不受任何影响。
当调用时传入(可选搭配),请按上述步骤1-2执行,然后替换步骤3为:
--conversation <conversation-id>--alt "<text>"bash
undefinedExtra variables from the invocation args:
Extra variables from the invocation args:
CONVERSATION_ID=<conversation-id> # from --conversation
CONVERSATION_ID=<conversation-id> # from --conversation
ALT="short image description" # from --alt (optional; omitted → server stores null)
ALT="short image description" # from --alt (optional; omitted → server stores null)
BODY=$(CONVERSATION_ID="$CONVERSATION_ID" ALT="${ALT:-}" python3 -c "
import json, os
b = {'visibility': 'public', 'conversation_id': os.environ['CONVERSATION_ID']}
if os.environ.get('ALT'):
b['alt'] = os.environ['ALT']
print(json.dumps(b))")
RESPONSE=$(curl -sS -X PATCH "$PYLOT_GATEWAY_URL/assets/$ASSET_ID"
-H "Authorization: Bearer $PYLOT_DISPATCH_TOKEN"
-H "Content-Type: application/json"
-d "$BODY") PUBLIC_URL=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['public_url'])") MESSAGE_ID=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('conversation_message_id',''))") ATTACHED=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('attached',''))")
-H "Authorization: Bearer $PYLOT_DISPATCH_TOKEN"
-H "Content-Type: application/json"
-d "$BODY") PUBLIC_URL=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['public_url'])") MESSAGE_ID=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('conversation_message_id',''))") ATTACHED=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('attached',''))")
The gateway idempotently appends a `{"type":"image","asset_id","alt","mime_type"}` user-message block to the conversation — it renders inline in the chat UI, and Claude sees the image on the conversation's next turn. No assistant turn is triggered by the append.
**Report both attach fields alongside the URL**, e.g. `attached to conversation <id> — message <MESSAGE_ID> (attached: <ATTACHED>)`:
- `conversation_message_id` — id of the appended (or pre-existing) conversation message
- `attached` — `True` on a fresh append; `False` means the asset was already attached to this conversation (idempotent re-publish — not an error, don't retry)BODY=$(CONVERSATION_ID="$CONVERSATION_ID" ALT="${ALT:-}" python3 -c "
import json, os
b = {'visibility': 'public', 'conversation_id': os.environ['CONVERSATION_ID']}
if os.environ.get('ALT'):
b['alt'] = os.environ['ALT']
print(json.dumps(b))")
RESPONSE=$(curl -sS -X PATCH "$PYLOT_GATEWAY_URL/assets/$ASSET_ID"
-H "Authorization: Bearer $PYLOT_DISPATCH_TOKEN"
-H "Content-Type: application/json"
-d "$BODY") PUBLIC_URL=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['public_url'])") MESSAGE_ID=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('conversation_message_id',''))") ATTACHED=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('attached',''))")
-H "Authorization: Bearer $PYLOT_DISPATCH_TOKEN"
-H "Content-Type: application/json"
-d "$BODY") PUBLIC_URL=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['public_url'])") MESSAGE_ID=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('conversation_message_id',''))") ATTACHED=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('attached',''))")
网关会幂等性地向对话中追加一个`{"type":"image","asset_id","alt","mime_type"}`格式的用户消息块——它会在聊天UI中内联显示,且Claude会在对话的下一轮看到该图片。追加操作不会触发助手回复。
**请将两个附加字段与URL一同报告**,例如:`已附加到对话<id> — 消息<MESSAGE_ID>(已附加:<ATTACHED>)`:
- `conversation_message_id` — 已追加(或已存在)的对话消息ID
- `attached` — 首次追加时为`True`;`False`表示该资源已附加到此对话(幂等性重新发布——并非错误,无需重试)Error handling
错误处理
- 400 from presign → file type or size outside allowlist. Convert or compress before retrying.
- 403 from presign → token doesn't have access to this repo's org. Check matches the org the token belongs to.
$REPO - Non-200 from PUT → S3 presigned URL expired (valid 15 min). Re-run presign and try again.
- Non-200 from publish → retry once; if it persists, skip evidence (never block the PR on upload failure).
Attach-specific (only when was given — check the response code):
--conversationerror- 400 from publish with → the target gateway predates pylot#1950. Re-run step 3 in its plain form (
conversation_id) so the public URL is still produced, and report that conversation attach is unsupported on this gateway.{"visibility":"public"} - 404 → no such conversation on this gateway. Check the id for typos and that
conversation_not_foundpoints at the environment the conversation lives in (staging vs prod). Fall back to plain publish so the URL isn't lost.$PYLOT_GATEWAY_URL - 403 → the conversation belongs to a different org than the asset. The
conversation_org_mismatchused in step 1 must be in the same org as the conversation — re-run the flow from step 1 with the right repo.$REPO - 409 → the S3 PUT (step 2) never completed, so there is nothing to attach. Re-run step 2, then retry the attach publish.
asset_not_uploaded
A failed attach must never lose the evidence: always finish with a successful publish (plain if necessary) and report the .
public_url- 预签名接口返回400 → 文件类型或大小超出允许列表范围。请转换或压缩后重试。
- 预签名接口返回403 → 令牌无此仓库所属组织的访问权限。请检查是否与令牌所属组织匹配。
$REPO - PUT请求返回非200状态 → S3预签名URL已过期(有效期15分钟)。请重新执行预签名步骤后重试。
- 发布请求返回非200状态 → 重试一次;若仍失败,请跳过证据上传(切勿因上传失败阻塞PR流程)。
仅当使用时的附加相关错误(请检查响应中的代码):
--conversationerror- 带的发布请求返回400 → 目标网关版本早于pylot#1950。请改用基础形式的步骤3(
conversation_id)以生成公共URL,并报告此网关不支持对话附加功能。{"visibility":"public"} - 404 → 此网关不存在该对话。请检查ID是否有误,以及
conversation_not_found是否指向对话所在的环境( staging或生产环境)。请回退到基础发布流程,避免丢失URL。$PYLOT_GATEWAY_URL - 403 → 对话所属组织与资源所属组织不一致。步骤1中使用的
conversation_org_mismatch必须与对话所属组织相同——请使用正确的仓库重新执行整个流程。$REPO - 409 → S3 PUT请求(步骤2)未完成,因此无内容可附加。请重新执行步骤2,然后重试附加发布操作。
asset_not_uploaded
附加失败时绝不能丢失证据:务必完成成功的发布(必要时使用基础发布流程)并报告。
public_urlWhen to skip
何时跳过
Skip evidence upload (and note "N/A" in the PR body) if:
- The change is backend-only, CLI-only, config/infra, or test-only with no visible output.
- Capture would take more than 120 s.
- The file exceeds 25 MB after compression.
Evidence is a bonus, never a gate.
若出现以下情况,请跳过证据上传(并在PR正文中注明“N/A”):
- 变更仅涉及后端、CLI、配置/基础设施,或仅涉及测试且无可见输出。
- 捕获证据耗时超过120秒。
- 文件压缩后仍超过25 MB。
证据是额外补充,绝非强制要求。