mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
feat: sync latest workspace changes
This commit is contained in:
@@ -1,534 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ProxyCast Connect 测试页面</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
.container { max-width: 900px; margin: 0 auto; }
|
||||
h1 { color: white; text-align: center; margin-bottom: 10px; font-size: 2rem; }
|
||||
.subtitle { color: rgba(255,255,255,0.8); text-align: center; margin-bottom: 40px; }
|
||||
.tabs {
|
||||
display: flex; gap: 8px; margin-bottom: 24px;
|
||||
background: rgba(255,255,255,0.1); padding: 8px; border-radius: 12px;
|
||||
}
|
||||
.tab {
|
||||
flex: 1; padding: 12px 20px; border: none; border-radius: 8px;
|
||||
font-size: 0.95rem; font-weight: 600; cursor: pointer;
|
||||
background: transparent; color: rgba(255,255,255,0.7); transition: all 0.2s;
|
||||
}
|
||||
.tab.active { background: white; color: #667eea; }
|
||||
.tab:hover:not(.active) { background: rgba(255,255,255,0.1); color: white; }
|
||||
.tab-content { display: none; }
|
||||
.tab-content.active { display: block; }
|
||||
.card {
|
||||
background: white; border-radius: 16px; padding: 30px;
|
||||
margin-bottom: 24px; box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
||||
}
|
||||
.card-title {
|
||||
font-size: 1.25rem; font-weight: 600; margin-bottom: 20px; color: #1a1a2e;
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
}
|
||||
.card-title::before {
|
||||
content: ''; width: 4px; height: 24px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 2px;
|
||||
}
|
||||
.form-group { margin-bottom: 20px; }
|
||||
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
||||
@media (max-width: 600px) { .form-row { grid-template-columns: 1fr; } }
|
||||
label { display: block; font-weight: 500; margin-bottom: 8px; color: #374151; }
|
||||
.label-hint { font-weight: normal; color: #9ca3af; font-size: 0.85rem; }
|
||||
input, select {
|
||||
width: 100%; padding: 12px 16px; border: 2px solid #e5e7eb;
|
||||
border-radius: 10px; font-size: 1rem; transition: border-color 0.2s;
|
||||
}
|
||||
input:focus, select:focus { outline: none; border-color: #667eea; }
|
||||
.btn {
|
||||
display: inline-flex; align-items: center; gap: 8px;
|
||||
padding: 14px 28px; border: none; border-radius: 10px;
|
||||
font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.2s;
|
||||
}
|
||||
.btn-primary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; }
|
||||
.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); }
|
||||
.btn-secondary { background: #f3f4f6; color: #374151; }
|
||||
.btn-secondary:hover { background: #e5e7eb; }
|
||||
.btn-group { display: flex; gap: 12px; flex-wrap: wrap; }
|
||||
|
||||
.result-box {
|
||||
background: #f8fafc; border: 2px dashed #e2e8f0;
|
||||
border-radius: 10px; padding: 20px; margin-top: 20px;
|
||||
}
|
||||
.result-box.success { background: #ecfdf5; border-color: #10b981; border-style: solid; }
|
||||
.result-label { font-size: 0.875rem; color: #64748b; margin-bottom: 8px; }
|
||||
.result-url {
|
||||
font-family: 'Monaco', 'Menlo', monospace; font-size: 0.875rem;
|
||||
word-break: break-all; color: #1e293b; background: white;
|
||||
padding: 12px; border-radius: 8px; border: 1px solid #e2e8f0;
|
||||
}
|
||||
.quick-test {
|
||||
display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 12px; margin-top: 20px;
|
||||
}
|
||||
.quick-test-btn {
|
||||
padding: 16px; background: #f8fafc; border: 2px solid #e2e8f0;
|
||||
border-radius: 12px; cursor: pointer; transition: all 0.2s; text-align: left;
|
||||
}
|
||||
.quick-test-btn:hover { border-color: #667eea; background: #f0f4ff; }
|
||||
.quick-test-btn .name { font-weight: 600; color: #1a1a2e; margin-bottom: 4px; }
|
||||
.quick-test-btn .desc { font-size: 0.875rem; color: #64748b; }
|
||||
.tips {
|
||||
background: #fffbeb; border: 1px solid #fcd34d;
|
||||
border-radius: 10px; padding: 16px; margin-top: 20px;
|
||||
}
|
||||
.tips.info { background: #eff6ff; border-color: #3b82f6; }
|
||||
.tips-title { font-weight: 600; color: #92400e; margin-bottom: 8px; }
|
||||
.tips.info .tips-title { color: #1d4ed8; }
|
||||
.tips-content { font-size: 0.875rem; color: #a16207; line-height: 1.6; }
|
||||
.tips.info .tips-content { color: #1e40af; }
|
||||
.log-container {
|
||||
max-height: 300px; overflow-y: auto; background: #1a1a2e;
|
||||
border-radius: 10px; padding: 16px; margin-top: 16px;
|
||||
}
|
||||
.log-entry {
|
||||
font-family: 'Monaco', 'Menlo', monospace; font-size: 0.8rem;
|
||||
color: #a0aec0; margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid #2d3748;
|
||||
}
|
||||
.log-entry:last-child { border-bottom: none; margin-bottom: 0; padding-bottom: 0; }
|
||||
.log-entry .time { color: #718096; }
|
||||
.log-entry .type { padding: 2px 6px; border-radius: 4px; font-size: 0.7rem; margin: 0 8px; }
|
||||
.log-entry .type.info { background: #3182ce; color: white; }
|
||||
.log-entry .type.success { background: #38a169; color: white; }
|
||||
.log-entry .type.error { background: #e53e3e; color: white; }
|
||||
.log-entry .message { color: #e2e8f0; }
|
||||
.toast {
|
||||
position: fixed; bottom: 20px; right: 20px; background: #1a1a2e;
|
||||
color: white; padding: 12px 20px; border-radius: 8px;
|
||||
opacity: 0; transform: translateY(20px); transition: all 0.3s; z-index: 1000;
|
||||
}
|
||||
.toast.show { opacity: 1; transform: translateY(0); }
|
||||
.toast.success { background: #10b981; }
|
||||
.toast.error { background: #ef4444; }
|
||||
.status-badge {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
padding: 4px 12px; border-radius: 20px; font-size: 0.8rem; font-weight: 500;
|
||||
}
|
||||
.status-badge.success { background: #dcfce7; color: #166534; }
|
||||
.status-badge.cancelled { background: #fef3c7; color: #92400e; }
|
||||
.status-badge.error { background: #fee2e2; color: #991b1b; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🔗 ProxyCast Connect 测试</h1>
|
||||
<p class="subtitle">模拟中转商后台的一键配置功能 & Webhook 回调测试</p>
|
||||
|
||||
<div class="tabs">
|
||||
<button class="tab active" onclick="switchTab('deeplink')">🚀 Deep Link 测试</button>
|
||||
<button class="tab" onclick="switchTab('webhook')">📤 Webhook 回调</button>
|
||||
<button class="tab" onclick="switchTab('docs')">📖 使用说明</button>
|
||||
</div>
|
||||
|
||||
<!-- Deep Link 测试标签页 -->
|
||||
<div id="tab-deeplink" class="tab-content active">
|
||||
<div class="card">
|
||||
<h2 class="card-title">自定义测试</h2>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="relay">中转商 ID (relay) <span class="label-hint">必填</span></label>
|
||||
<input type="text" id="relay" placeholder="例如: openrouter" value="openrouter">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="key">API Key <span class="label-hint">必填</span></label>
|
||||
<input type="text" id="key" placeholder="例如: sk-or-v1-xxxx" value="sk-test-1234567890">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="name">Key 名称 <span class="label-hint">可选</span></label>
|
||||
<input type="text" id="name" placeholder="例如: 我的主账号" value="测试Key">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="ref">推广码 <span class="label-hint">可选,用于统计</span></label>
|
||||
<input type="text" id="ref" placeholder="例如: promo2024">
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" onclick="openDeepLink()">🚀 一键配置 ProxyCast</button>
|
||||
<button class="btn btn-secondary" onclick="generateLink()">📋 生成链接</button>
|
||||
</div>
|
||||
<div class="result-box" id="resultBox" style="display: none;">
|
||||
<div class="result-label">生成的 Deep Link:</div>
|
||||
<div class="result-url" id="resultUrl"></div>
|
||||
<button class="btn btn-secondary" style="margin-top: 12px; padding: 8px 16px; font-size: 0.875rem;" onclick="copyLink()">复制链接</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-title">快速测试场景</h2>
|
||||
<div class="quick-test">
|
||||
<button class="quick-test-btn" onclick="quickTest('openrouter', 'sk-or-v1-test123', 'OpenRouter测试', '')">
|
||||
<div class="name">✅ OpenRouter</div>
|
||||
<div class="desc">正常配置流程</div>
|
||||
</button>
|
||||
<button class="quick-test-btn" onclick="quickTest('siliconflow', 'sk-sf-test456', '硅基流动测试', 'promo2024')">
|
||||
<div class="name">✅ 硅基流动 + 推广码</div>
|
||||
<div class="desc">带推广码的配置</div>
|
||||
</button>
|
||||
<button class="quick-test-btn" onclick="quickTest('unknown-relay', 'sk-unknown', '未知中转', '')">
|
||||
<div class="name">❌ 未注册中转商</div>
|
||||
<div class="desc">测试错误处理</div>
|
||||
</button>
|
||||
<button class="quick-test-btn" onclick="quickTest('openrouter', '', '', '')">
|
||||
<div class="name">❌ 缺少 Key</div>
|
||||
<div class="desc">测试参数校验</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Webhook 回调测试标签页 -->
|
||||
<div id="tab-webhook" class="tab-content">
|
||||
<div class="card">
|
||||
<h2 class="card-title">Webhook 回调测试</h2>
|
||||
<p style="color: #64748b; margin-bottom: 20px;">模拟 ProxyCast 发送的统计回调请求</p>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="callbackUrl">回调地址 (Callback URL) <span class="label-hint">必填</span></label>
|
||||
<input type="text" id="callbackUrl" placeholder="https://api.myrelay.com/proxycast/callback">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="callbackRelay">中转商 ID</label>
|
||||
<input type="text" id="callbackRelay" placeholder="openrouter" value="openrouter">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="callbackStatus">回调状态</label>
|
||||
<select id="callbackStatus">
|
||||
<option value="success">✅ success - 配置成功</option>
|
||||
<option value="cancelled">⚠️ cancelled - 用户取消</option>
|
||||
<option value="error">❌ error - 配置失败</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="callbackRef">推广码 (ref) <span class="label-hint">可选</span></label>
|
||||
<input type="text" id="callbackRef" placeholder="promo2024" value="promo2024">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="callbackKeyPrefix">Key 前缀 <span class="label-hint">脱敏后的 Key(前 7 位)</span></label>
|
||||
<input type="text" id="callbackKeyPrefix" placeholder="sk-or-v" value="sk-or-v" maxlength="7">
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" onclick="sendCallback()">📤 发送回调</button>
|
||||
<button class="btn btn-secondary" onclick="generateCallbackPayload()">📋 生成 Payload</button>
|
||||
<button class="btn btn-secondary" onclick="clearLogs()">🗑️ 清空日志</button>
|
||||
</div>
|
||||
|
||||
<div class="result-box" id="callbackResultBox" style="display: none;">
|
||||
<div class="result-label">回调 Payload:</div>
|
||||
<pre class="result-url" id="callbackPayload" style="white-space: pre-wrap; font-size: 0.8rem;"></pre>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 24px;">
|
||||
<div class="result-label">请求日志:</div>
|
||||
<div class="log-container" id="logContainer">
|
||||
<div class="log-entry">
|
||||
<span class="time">[--:--:--]</span>
|
||||
<span class="type info">INFO</span>
|
||||
<span class="message">等待发送回调请求...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tips info" style="margin-top: 20px;">
|
||||
<div class="tips-title">💡 验证说明</div>
|
||||
<div class="tips-content">
|
||||
由于 ProxyCast 是开源软件,不使用签名验证。<br>
|
||||
中转商应通过检查 <code>key_prefix</code> 是否为自己下发的 Key 来验证请求。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-title">回调字段说明</h2>
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr style="border-bottom: 2px solid #e5e7eb;">
|
||||
<th style="padding: 12px 0; text-align: left; color: #374151;">字段</th>
|
||||
<th style="padding: 12px 0; text-align: left; color: #374151;">说明</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="border-bottom: 1px solid #e5e7eb;">
|
||||
<td style="padding: 10px 0; font-family: monospace;">status</td>
|
||||
<td style="padding: 10px 0;">
|
||||
<span class="status-badge success">success</span>
|
||||
<span class="status-badge cancelled">cancelled</span>
|
||||
<span class="status-badge error">error</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="border-bottom: 1px solid #e5e7eb;">
|
||||
<td style="padding: 10px 0; font-family: monospace;">relay_id</td>
|
||||
<td style="padding: 10px 0; color: #64748b;">中转商 ID</td>
|
||||
</tr>
|
||||
<tr style="border-bottom: 1px solid #e5e7eb;">
|
||||
<td style="padding: 10px 0; font-family: monospace;">key_prefix</td>
|
||||
<td style="padding: 10px 0; color: #64748b;">Key 前缀(脱敏,仅前 7 位)- 用于验证</td>
|
||||
</tr>
|
||||
<tr style="border-bottom: 1px solid #e5e7eb;">
|
||||
<td style="padding: 10px 0; font-family: monospace;">ref</td>
|
||||
<td style="padding: 10px 0; color: #64748b;">推广码(可选)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 10px 0; font-family: monospace;">client</td>
|
||||
<td style="padding: 10px 0; color: #64748b;">客户端信息 {version, platform}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 使用说明标签页 -->
|
||||
<div id="tab-docs" class="tab-content">
|
||||
<div class="card">
|
||||
<h2 class="card-title">Deep Link 协议</h2>
|
||||
<div style="margin-bottom: 20px;">
|
||||
<h3 style="font-size: 1rem; margin-bottom: 12px; color: #374151;">协议格式</h3>
|
||||
<div class="result-url">proxycast://connect?relay={relay_id}&key={api_key}&name={key_name}&ref={ref_code}</div>
|
||||
</div>
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<tr style="border-bottom: 1px solid #e5e7eb;">
|
||||
<td style="padding: 10px 0; font-weight: 500; font-family: monospace;">relay</td>
|
||||
<td style="padding: 10px 0; color: #ef4444; font-weight: 500;">必填</td>
|
||||
<td style="padding: 10px 0; color: #64748b;">中转商 ID</td>
|
||||
</tr>
|
||||
<tr style="border-bottom: 1px solid #e5e7eb;">
|
||||
<td style="padding: 10px 0; font-weight: 500; font-family: monospace;">key</td>
|
||||
<td style="padding: 10px 0; color: #ef4444; font-weight: 500;">必填</td>
|
||||
<td style="padding: 10px 0; color: #64748b;">API Key</td>
|
||||
</tr>
|
||||
<tr style="border-bottom: 1px solid #e5e7eb;">
|
||||
<td style="padding: 10px 0; font-weight: 500; font-family: monospace;">name</td>
|
||||
<td style="padding: 10px 0; color: #64748b;">可选</td>
|
||||
<td style="padding: 10px 0; color: #64748b;">Key 显示名称</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 10px 0; font-weight: 500; font-family: monospace;">ref</td>
|
||||
<td style="padding: 10px 0; color: #64748b;">可选</td>
|
||||
<td style="padding: 10px 0; color: #64748b;">推广码</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="tips">
|
||||
<div class="tips-title">💡 测试前提</div>
|
||||
<div class="tips-content">
|
||||
1. 确保 ProxyCast 应用已安装并运行<br>
|
||||
2. 确保 Deep Link 协议已正确注册<br>
|
||||
3. 如果点击无反应,检查浏览器是否阻止了协议跳转
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-title">相关链接</h2>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px;">
|
||||
<a href="https://github.com/aiclientproxy/connect" target="_blank" class="quick-test-btn" style="text-decoration: none;">
|
||||
<div class="name">📦 中转商注册仓库</div>
|
||||
<div class="desc">提交 PR 注册中转商</div>
|
||||
</a>
|
||||
<a href="https://proxycast.dev/docs/open-platform/connect" target="_blank" class="quick-test-btn" style="text-decoration: none;">
|
||||
<div class="name">📖 Connect 文档</div>
|
||||
<div class="desc">详细的接入指南</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
|
||||
<script>
|
||||
// Tab 切换
|
||||
function switchTab(tabId) {
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||||
document.querySelector(`[onclick="switchTab('${tabId}')"]`).classList.add('active');
|
||||
document.getElementById(`tab-${tabId}`).classList.add('active');
|
||||
}
|
||||
|
||||
// 生成 Deep Link
|
||||
function generateDeepLink() {
|
||||
const relay = document.getElementById('relay').value.trim();
|
||||
const key = document.getElementById('key').value.trim();
|
||||
const name = document.getElementById('name').value.trim();
|
||||
const ref = document.getElementById('ref').value.trim();
|
||||
|
||||
if (!relay) { showToast('请输入中转商 ID', 'error'); return null; }
|
||||
if (!key) { showToast('请输入 API Key', 'error'); return null; }
|
||||
|
||||
let url = `proxycast://connect?relay=${encodeURIComponent(relay)}&key=${encodeURIComponent(key)}`;
|
||||
if (name) url += `&name=${encodeURIComponent(name)}`;
|
||||
if (ref) url += `&ref=${encodeURIComponent(ref)}`;
|
||||
return url;
|
||||
}
|
||||
|
||||
// 打开 Deep Link
|
||||
function openDeepLink() {
|
||||
const url = generateDeepLink();
|
||||
if (url) {
|
||||
window.location.href = url;
|
||||
showToast('正在打开 ProxyCast...', 'success');
|
||||
}
|
||||
}
|
||||
|
||||
// 生成并显示链接
|
||||
function generateLink() {
|
||||
const url = generateDeepLink();
|
||||
if (url) {
|
||||
document.getElementById('resultUrl').textContent = url;
|
||||
document.getElementById('resultBox').style.display = 'block';
|
||||
document.getElementById('resultBox').classList.add('success');
|
||||
}
|
||||
}
|
||||
|
||||
// 复制链接
|
||||
function copyLink() {
|
||||
const url = document.getElementById('resultUrl').textContent;
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
showToast('链接已复制', 'success');
|
||||
});
|
||||
}
|
||||
|
||||
// 快速测试
|
||||
function quickTest(relay, key, name, ref) {
|
||||
document.getElementById('relay').value = relay;
|
||||
document.getElementById('key').value = key;
|
||||
document.getElementById('name').value = name;
|
||||
document.getElementById('ref').value = ref;
|
||||
openDeepLink();
|
||||
}
|
||||
|
||||
// Toast 提示
|
||||
function showToast(message, type = 'info') {
|
||||
const toast = document.getElementById('toast');
|
||||
toast.textContent = message;
|
||||
toast.className = `toast show ${type}`;
|
||||
setTimeout(() => toast.classList.remove('show'), 3000);
|
||||
}
|
||||
|
||||
// 添加日志
|
||||
function addLog(type, message) {
|
||||
const container = document.getElementById('logContainer');
|
||||
const time = new Date().toLocaleTimeString();
|
||||
const entry = document.createElement('div');
|
||||
entry.className = 'log-entry';
|
||||
entry.innerHTML = `
|
||||
<span class="time">[${time}]</span>
|
||||
<span class="type ${type}">${type.toUpperCase()}</span>
|
||||
<span class="message">${message}</span>
|
||||
`;
|
||||
container.appendChild(entry);
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
// 清空日志
|
||||
function clearLogs() {
|
||||
const container = document.getElementById('logContainer');
|
||||
container.innerHTML = `
|
||||
<div class="log-entry">
|
||||
<span class="time">[--:--:--]</span>
|
||||
<span class="type info">INFO</span>
|
||||
<span class="message">日志已清空</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 生成回调 Payload
|
||||
function generateCallbackPayload() {
|
||||
const payload = buildCallbackPayload();
|
||||
if (payload) {
|
||||
document.getElementById('callbackPayload').textContent = JSON.stringify(payload, null, 2);
|
||||
document.getElementById('callbackResultBox').style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// 构建回调 Payload
|
||||
function buildCallbackPayload() {
|
||||
const relay = document.getElementById('callbackRelay').value.trim();
|
||||
const status = document.getElementById('callbackStatus').value;
|
||||
const ref = document.getElementById('callbackRef').value.trim();
|
||||
const keyPrefix = document.getElementById('callbackKeyPrefix').value.trim();
|
||||
|
||||
if (!relay) { showToast('请输入中转商 ID', 'error'); return null; }
|
||||
if (!keyPrefix) { showToast('请输入 Key 前缀', 'error'); return null; }
|
||||
|
||||
const payload = {
|
||||
event: 'connect',
|
||||
status: status,
|
||||
relay_id: relay,
|
||||
key_prefix: keyPrefix.substring(0, 7),
|
||||
timestamp: new Date().toISOString(),
|
||||
client: {
|
||||
version: '0.29.0',
|
||||
platform: 'web-test'
|
||||
}
|
||||
};
|
||||
|
||||
if (ref) payload.ref_code = ref;
|
||||
if (status === 'error') {
|
||||
payload.error_code = 'TEST_ERROR';
|
||||
payload.error_message = '测试错误消息';
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
// 发送回调
|
||||
async function sendCallback() {
|
||||
const url = document.getElementById('callbackUrl').value.trim();
|
||||
if (!url) { showToast('请输入回调地址', 'error'); return; }
|
||||
if (!url.startsWith('https://')) { showToast('回调地址必须使用 HTTPS', 'error'); return; }
|
||||
|
||||
const payload = buildCallbackPayload();
|
||||
if (!payload) return;
|
||||
|
||||
addLog('info', `发送回调到: ${url}`);
|
||||
addLog('info', `Payload: ${JSON.stringify(payload)}`);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'ProxyCast/0.29.0 (Test)'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
addLog('success', `回调成功! HTTP ${response.status}`);
|
||||
showToast('回调发送成功', 'success');
|
||||
} else {
|
||||
addLog('error', `回调失败: HTTP ${response.status}`);
|
||||
showToast(`回调失败: HTTP ${response.status}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
addLog('error', `网络错误: ${error.message}`);
|
||||
showToast('网络错误,请检查回调地址', 'error');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@
|
||||
> 文档版本:v1.0
|
||||
> 状态:Draft
|
||||
> 更新时间:2026-04-06
|
||||
> 适用范围:`image_generate` skill、`lime-cli`、媒体任务协议、Claw 对话框、图片工作台
|
||||
> 适用范围:`image_generate` skill、`lime-cli`、媒体任务协议、聊天区、图片工作台
|
||||
> 目标读者:产品、前端、Rust/CLI、测试
|
||||
|
||||
---
|
||||
@@ -19,12 +19,12 @@
|
||||
1. `@配图` 前端快路径
|
||||
已具备占位反馈、图片卡、图片工作台展开等体验,但真相源偏前端本地运行时。
|
||||
2. `image_generate` skill 任务路径
|
||||
已具备稳定的任务创建能力,能输出 `task_id / task_type / path / status`,但当前只做到“提交任务”,没有把任务状态与结果动态回流到 Claw 对话框。
|
||||
已具备稳定的任务创建能力,能输出 `task_id / task_type / path / status`,但当前只做到“提交任务”,没有把任务状态与结果动态回流到聊天区。
|
||||
|
||||
这导致三个核心问题:
|
||||
|
||||
1. **能力割裂**:前端快路径与 skill 任务路径体验不一致。
|
||||
2. **黑盒感强**:用户在 Claw 中看不到真实生成进度与真实结果替换。
|
||||
2. **黑盒感强**:用户在聊天区中看不到真实生成进度与真实结果替换。
|
||||
3. **难以组合与测试**:如果图片生成过程继续停留在某个前端页面或某个 skill 内部,就很难在自动化、批处理、队列、重试、回放中复用。
|
||||
|
||||
本 PRD 的目标是把图片生成正式收敛为一条异步、解耦、标准化的主链:
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
### 2.1 产品目标
|
||||
|
||||
1. 让用户在 Claw 对话框中发起图片 skill 后,立即看到可感知的动态反馈。
|
||||
1. 让用户在聊天区中发起图片 skill 后,立即看到可感知的动态反馈。
|
||||
2. 让图片 skill 与正文配图、封面、自动化、未来 worker 共用同一套任务协议。
|
||||
3. 让图片任务具备标准生命周期:创建、排队、执行、成功、失败、重试、取消、恢复。
|
||||
4. 让前端显示的是“真实任务状态”,而不是 assistant 文案猜测。
|
||||
@@ -63,7 +63,7 @@
|
||||
|
||||
### 3.2 Task File 是唯一真相源
|
||||
|
||||
图片任务的状态、结果、错误、来源关系都必须落到标准 task file 或其等价协议存储中。Claw、工作台、CLI、测试统一读取这一事实源。
|
||||
图片任务的状态、结果、错误、来源关系都必须落到标准 task file 或其等价协议存储中。聊天区、工作台、CLI、测试统一读取这一事实源。
|
||||
|
||||
### 3.3 前端是观察者,不是执行器
|
||||
|
||||
@@ -95,9 +95,9 @@
|
||||
|
||||
## 四、用户体验目标
|
||||
|
||||
## 4.1 Claw 对话框中的图片任务体验
|
||||
## 4.1 聊天区中的图片任务体验
|
||||
|
||||
用户在 Claw 中触发 `image_generate` 后,系统行为应如下:
|
||||
用户在聊天区中触发 `image_generate` 后,系统行为应如下:
|
||||
|
||||
1. 立即创建图片任务。
|
||||
2. 聊天区立即出现一张图片任务卡。
|
||||
@@ -147,7 +147,7 @@
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
USER[用户 / Claw 对话框] --> SKILL[image_generate skill]
|
||||
USER[用户 / 聊天区] --> SKILL[image_generate skill]
|
||||
SKILL --> CLI[CLI / Task API]
|
||||
CLI --> TASKFILE[Task File / 标准任务协议]
|
||||
WORKER[图片执行器 / Worker] --> TASKFILE
|
||||
@@ -512,7 +512,7 @@ skill 输出的语义是:
|
||||
|
||||
### 11.1 用户体验验收
|
||||
|
||||
1. 用户在 Claw 中通过图片 skill 发起任务后,聊天区立刻出现图片占位卡。
|
||||
1. 用户在聊天区中通过图片 skill 发起任务后,聊天区立刻出现图片占位卡。
|
||||
2. 任务执行中,占位卡保持动态渲染,不出现“无反馈空窗”。
|
||||
3. 任务成功后,占位卡自动替换为真实图片。
|
||||
4. 点击成功图片可以打开图片工作台或图片画布。
|
||||
@@ -551,7 +551,7 @@ skill 输出的语义是:
|
||||
- 暴露前端任务读取接口
|
||||
- 保证执行器会写回结果与错误
|
||||
|
||||
### Phase 2:Claw 聊天区动态卡
|
||||
### Phase 2:聊天区动态卡
|
||||
|
||||
- 监听任务创建
|
||||
- 插入占位图片卡
|
||||
|
||||
@@ -14,7 +14,7 @@ Lime 当前已经具备一套轻量 task file 底座,能够覆盖:
|
||||
- `typesetting`
|
||||
- `modal_resource_search`
|
||||
|
||||
现有协议已经解决了“任务创建、状态读取、简单重试”的基础问题,但当 Lime 开始把图片生成、图片编辑、视频生成、视频编辑、自动化工作流、Claw 动态渲染统一收口到同一条主线时,现有 task file 还存在四类缺口:
|
||||
现有协议已经解决了“任务创建、状态读取、简单重试”的基础问题,但当 Lime 开始把图片生成、图片编辑、视频生成、视频编辑、自动化工作流、聊天区动态渲染统一收口到同一条主线时,现有 task file 还存在四类缺口:
|
||||
|
||||
1. **顶层字段不够稳定**
|
||||
- 现在更像“任务记录壳”,还不是“统一任务协议信封”
|
||||
@@ -27,7 +27,7 @@ Lime 当前已经具备一套轻量 task file 底座,能够覆盖:
|
||||
|
||||
这会直接影响后续几条主线:
|
||||
|
||||
- Claw 对话框中的动态占位图与结果替换
|
||||
- 聊天区中的动态占位图与结果替换
|
||||
- 图片生成 / 图片编辑 / 视频生成 / 视频编辑的统一观察面
|
||||
- 队列、重试、幂等、恢复、诊断能力
|
||||
- 类似竞品“统一任务面板”的可交付体验
|
||||
@@ -297,7 +297,7 @@ Lime 当前已经具备一套轻量 task file 底座,能够覆盖:
|
||||
如果每次重试都新建任务文件,会带来这些问题:
|
||||
|
||||
- 前端需要在旧卡片和新卡片之间重新绑定
|
||||
- Claw 动态替换更复杂
|
||||
- 聊天区动态替换更复杂
|
||||
- 统一任务列表会出现大量碎片任务
|
||||
- “这其实还是同一个任务”的语义丢失
|
||||
|
||||
@@ -561,7 +561,7 @@ Lime 当前已经具备一套轻量 task file 底座,能够覆盖:
|
||||
|
||||
## 10. 进度、错误与 UI 提示
|
||||
|
||||
为了支撑 Claw 动态占位与结果替换,task file 不能只提供最终状态,还必须提供“可渲染的运行时信息”。
|
||||
为了支撑聊天区动态占位与结果替换,task file 不能只提供最终状态,还必须提供“可渲染的运行时信息”。
|
||||
|
||||
## 10.1 `progress`
|
||||
|
||||
@@ -716,7 +716,7 @@ Lime 当前已经具备一套轻量 task file 底座,能够覆盖:
|
||||
|
||||
流程:
|
||||
|
||||
1. 用户在 Claw 中触发 `image_generate`
|
||||
1. 用户在聊天区中触发 `image_generate`
|
||||
2. 创建 `task_family=image / task_type=image_generate`
|
||||
3. `relationships.slot_id` 绑定正文占位块
|
||||
4. 前端先显示占位图
|
||||
@@ -779,7 +779,7 @@ Lime 当前已经具备一套轻量 task file 底座,能够覆盖:
|
||||
|
||||
并且保证:
|
||||
|
||||
- Claw、CLI、worker 读到的是同一份状态
|
||||
- 聊天区、CLI、worker 读到的是同一份状态
|
||||
- 可以支撑统一任务面板,而不需要第二套状态系统
|
||||
|
||||
---
|
||||
@@ -831,5 +831,4 @@ task file 应该被设计成 Lime 的统一任务协议,而不是某个业务
|
||||
- **父子任务与依赖关系**
|
||||
- **结构化进度、错误、UI 提示**
|
||||
|
||||
这样 Lime 才能把图片生成、图片编辑、视频生成、视频编辑、Claw 动态渲染、CLI、worker、统一任务面板全部收敛到同一条主链。
|
||||
|
||||
这样 Lime 才能把图片生成、图片编辑、视频生成、视频编辑、聊天区动态渲染、CLI、worker、统一任务面板全部收敛到同一条主链。
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user