evidence-upload

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

evidence-upload

证据上传

Upload a local file to the pylot assets backend (org-fenced S3) and get a stable, camo-safe public URL.
$PYLOT_GATEWAY_URL
and
$PYLOT_DISPATCH_TOKEN
are already in every operator/worker env — no static AWS keys needed.
Optionally, pass
--conversation <conversation-id>
(and
--alt "<text>"
) 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.
将本地文件上传至pylot资源后端(组织隔离的S3存储),获取稳定且经camo安全处理的公共URL。
$PYLOT_GATEWAY_URL
$PYLOT_DISPATCH_TOKEN
已配置在所有操作员/工作环境中——无需静态AWS密钥。
你可选择传入
--conversation <conversation-id>
(以及
--alt "<text>"
),将已发布的资源附加到聊天对话中——它会在线程中内联显示,且Claude会在对话的下一轮看到该图片。请查看下方的【附加到对话(--conversation)】部分;若不使用该标志,流程将严格遵循以下4个步骤。

Allowlist

允许列表

TypeMIME typesMax size
Images
image/png
,
image/jpeg
,
image/gif
,
image/webp
25 MB
Video
video/mp4
25 MB
Anything outside this list will get a 400 from
/assets/presign
. Capture screenshots as PNG; convert large GIFs to MP4 if they exceed 25 MB.
类型MIME 类型最大大小
图片
image/png
,
image/jpeg
,
image/gif
,
image/webp
25 MB
视频
video/mp4
25 MB
不在此列表中的文件会被
/assets/presign
接口返回400错误。请将截图保存为PNG格式;若大型GIF超过25 MB,请转换为MP4格式。

The 4-step flow

四步流程

bash
undefined
bash
undefined

Variables 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'])")
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'])")

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"
curl -sS -X PUT "$UPLOAD_URL"
--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'])")
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'])")

4. Embed in PR / issue / report body:

4. Embed in PR / issue / report body:

echo "evidence"
echo "evidence"

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"}'

undefined
undefined

Attaching to a 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
--conversation <conversation-id>
(and optionally
--alt "<text>"
), run steps 1–2 as above, then replace step 3 with:
bash
undefined
需要部署了fellowship-dev/pylot#1950的网关——旧版网关会因额外字段返回400错误(请查看错误处理中的回退方案)。上述的基础四步流程不受任何影响。
当调用时传入
--conversation <conversation-id>
(可选搭配
--alt "<text>"
),请按上述步骤1-2执行,然后替换步骤3为:
bash
undefined

Extra 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',''))")

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',''))")

网关会幂等性地向对话中追加一个`{"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
    $REPO
    matches the org the token belongs to.
  • 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
--conversation
was given — check the response
error
code):
  • 400 from publish with
    conversation_id
    → the target gateway predates pylot#1950. Re-run step 3 in its plain form (
    {"visibility":"public"}
    ) so the public URL is still produced, and report that conversation attach is unsupported on this gateway.
  • 404
    conversation_not_found
    → no such conversation on this gateway. Check the id for typos and that
    $PYLOT_GATEWAY_URL
    points at the environment the conversation lives in (staging vs prod). Fall back to plain publish so the URL isn't lost.
  • 403
    conversation_org_mismatch
    → the conversation belongs to a different org than the asset. The
    $REPO
    used in step 1 must be in the same org as the conversation — re-run the flow from step 1 with the right repo.
  • 409
    asset_not_uploaded
    → the S3 PUT (step 2) never completed, so there is nothing to attach. Re-run step 2, then retry the attach publish.
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流程)。
仅当使用
--conversation
时的附加相关错误(请检查响应中的
error
代码):
  • conversation_id
    的发布请求返回400
    → 目标网关版本早于pylot#1950。请改用基础形式的步骤3(
    {"visibility":"public"}
    )以生成公共URL,并报告此网关不支持对话附加功能。
  • 404
    conversation_not_found
    → 此网关不存在该对话。请检查ID是否有误,以及
    $PYLOT_GATEWAY_URL
    是否指向对话所在的环境( staging或生产环境)。请回退到基础发布流程,避免丢失URL。
  • 403
    conversation_org_mismatch
    → 对话所属组织与资源所属组织不一致。步骤1中使用的
    $REPO
    必须与对话所属组织相同——请使用正确的仓库重新执行整个流程。
  • 409
    asset_not_uploaded
    → S3 PUT请求(步骤2)未完成,因此无内容可附加。请重新执行步骤2,然后重试附加发布操作。
附加失败时绝不能丢失证据:务必完成成功的发布(必要时使用基础发布流程)并报告
public_url

When 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。
证据是额外补充,绝非强制要求。