|
| 1 | +<template> |
| 2 | + <el-dialog :model-value="visible" @update:model-value="onVisibleChange" title="🚀 分享到官方市场(GitHub)" width="560px" append-to-body> |
| 3 | + <div style="margin-bottom:10px;color:#606266;font-size:13px;line-height:1.7"> |
| 4 | + 将当前条目以 <b>JSON 文件</b> 形式生成 <b>Pull Request</b> 到官方市场仓库 |
| 5 | + <b>pythonHuang/APIWorkflowCanvasDesignNet</b> 的 <b>market/{type}/{id}.json</b>, |
| 6 | + 管理员定期审核合并后进入官方市场,其他用户可通过「发现」拉取。 |
| 7 | + </div> |
| 8 | + <el-form label-width="90px" size="small"> |
| 9 | + <el-form-item label="GitHub Token"> |
| 10 | + <el-input v-model="form.token" type="password" show-password placeholder="ghp_...(仅保存在本机浏览器)" /> |
| 11 | + <div style="font-size:11px;color:#909399;line-height:1.6"> |
| 12 | + 需要 Personal Access Token(classic)并勾选 <b>repo</b> 权限:GitHub → Settings → Developer settings → Tokens。仅用于本次分享操作,保存在 localStorage。 |
| 13 | + </div> |
| 14 | + </el-form-item> |
| 15 | + <el-form-item label="作者"><el-input v-model="form.author" placeholder="您的署名(如 pythonHuang)" /></el-form-item> |
| 16 | + <el-form-item label="版本号"><el-input v-model="form.version" placeholder="如 1.0.0" /></el-form-item> |
| 17 | + <el-form-item label="条目"> |
| 18 | + <div style="font-size:13px;color:#303133"> |
| 19 | + <span style="margin-right:4px">{{ props.icon || '📦' }}</span>{{ props.itemName }} |
| 20 | + <el-tag size="small" style="margin-left:6px">{{ typeLabel }}</el-tag> |
| 21 | + </div> |
| 22 | + </el-form-item> |
| 23 | + </el-form> |
| 24 | + <el-alert v-if="shareResult" :type="shareResult.ok ? 'success' : 'error'" :closable="false" style="margin-bottom:8px"> |
| 25 | + <template #title>{{ shareResult.msg }}</template> |
| 26 | + </el-alert> |
| 27 | + <template #footer> |
| 28 | + <el-button size="small" @click="emit('update:visible', false)">关闭</el-button> |
| 29 | + <el-button size="small" type="primary" :loading="sharing" @click="doShare">🚀 生成 Pull Request</el-button> |
| 30 | + </template> |
| 31 | + </el-dialog> |
| 32 | +</template> |
| 33 | + |
| 34 | +<script setup lang="ts"> |
| 35 | +import { ref, computed } from 'vue' |
| 36 | +import { ElMessage } from 'element-plus' |
| 37 | +
|
| 38 | +const props = defineProps<{ |
| 39 | + visible: boolean |
| 40 | + itemType: string |
| 41 | + itemName: string |
| 42 | + description?: string |
| 43 | + icon?: string |
| 44 | + contentJson: string |
| 45 | +}>() |
| 46 | +const emit = defineEmits<{ (e: 'update:visible', v: boolean): void; (e: 'shared', url: string): void }>() |
| 47 | +
|
| 48 | +function onVisibleChange(v: boolean) { emit('update:visible', v) } |
| 49 | +
|
| 50 | +const GITHUB_API = 'https://api.github.com' |
| 51 | +const REPO_OWNER = 'pythonHuang' |
| 52 | +const REPO_NAME = 'APIWorkflowCanvasDesignNet' |
| 53 | +const TOKEN_KEY = 'market_github_token' |
| 54 | +
|
| 55 | +const typeLabels: Record<string, string> = { api: '接口', flow: '流程', assistant: '模型助手', skill: 'Skill', report: '报表' } |
| 56 | +const typeLabel = computed(() => typeLabels[props.itemType] || props.itemType) |
| 57 | +
|
| 58 | +const form = ref({ token: localStorage.getItem(TOKEN_KEY) || '', author: '', version: '1.0.0' }) |
| 59 | +const sharing = ref(false) |
| 60 | +const shareResult = ref<{ ok: boolean; msg: string } | null>(null) |
| 61 | +
|
| 62 | +async function gh(path: string, token: string, opts?: { method?: string; body?: any }) { |
| 63 | + const res = await fetch(`${GITHUB_API}${path}`, { |
| 64 | + headers: { |
| 65 | + Authorization: `token ${token}`, |
| 66 | + Accept: 'application/vnd.github+json', |
| 67 | + 'Content-Type': 'application/json' |
| 68 | + }, |
| 69 | + method: opts?.method || 'GET', |
| 70 | + body: opts?.body ? JSON.stringify(opts.body) : undefined |
| 71 | + }) |
| 72 | + const data = await res.json().catch(() => ({})) |
| 73 | + if (!res.ok) throw new Error(data?.message || `GitHub API 返回 ${res.status}`) |
| 74 | + return data |
| 75 | +} |
| 76 | +
|
| 77 | +function sleep(ms: number) { return new Promise(r => setTimeout(r, ms)) } |
| 78 | +
|
| 79 | +async function doShare() { |
| 80 | + const token = form.value.token.trim() |
| 81 | + if (!token) { ElMessage.warning('请填写 GitHub 访问令牌'); return } |
| 82 | + if (!form.value.author.trim()) { ElMessage.warning('请填写作者'); return } |
| 83 | + localStorage.setItem(TOKEN_KEY, token) |
| 84 | +
|
| 85 | + sharing.value = true |
| 86 | + shareResult.value = null |
| 87 | + try { |
| 88 | + // 1. 校验令牌并获取登录用户 |
| 89 | + const user = await gh('/user', token) |
| 90 | + const username = user.login |
| 91 | +
|
| 92 | + // 2. 确保 fork 存在(不存在则创建并轮询等待) |
| 93 | + let fork = await gh(`/repos/${username}/${REPO_NAME}`, token).catch(() => null) |
| 94 | + if (!fork) { |
| 95 | + await gh(`/repos/${REPO_OWNER}/${REPO_NAME}/forks`, token, { method: 'POST' }) |
| 96 | + for (let i = 0; i < 20; i++) { |
| 97 | + await sleep(3000) |
| 98 | + fork = await gh(`/repos/${username}/${REPO_NAME}`, token).catch(() => null) |
| 99 | + if (fork) break |
| 100 | + } |
| 101 | + if (!fork) throw new Error('Fork 创建超时,请稍后在 GitHub 页面确认后重试') |
| 102 | + } |
| 103 | +
|
| 104 | + // 3. 基于 fork 默认分支创建分享分支 |
| 105 | + const baseBranch = fork.default_branch || 'main' |
| 106 | + const baseRef = await gh(`/repos/${username}/${REPO_NAME}/git/ref/heads/${baseBranch}`, token) |
| 107 | + const shareId = Math.floor(Date.now() / 1000) |
| 108 | + const branch = `market-share-${props.itemType}-${shareId}` |
| 109 | + try { |
| 110 | + await gh(`/repos/${username}/${REPO_NAME}/git/refs`, token, { |
| 111 | + method: 'POST', |
| 112 | + body: { ref: `refs/heads/${branch}`, sha: baseRef.object.sha } |
| 113 | + }) |
| 114 | + } catch { /* 分支已存在则直接复用 */ } |
| 115 | +
|
| 116 | + // 4. 组装条目 JSON 并写入 market/{type}/{id}.json |
| 117 | + let content: any = {} |
| 118 | + try { content = JSON.parse(props.contentJson || '{}') } catch { content = {} } |
| 119 | + const itemJson = JSON.stringify({ |
| 120 | + id: shareId, |
| 121 | + type: props.itemType, |
| 122 | + name: props.itemName, |
| 123 | + description: props.description || '', |
| 124 | + icon: props.icon || '📦', |
| 125 | + author: form.value.author.trim(), |
| 126 | + version: form.value.version.trim() || '1.0.0', |
| 127 | + updatedAt: new Date().toISOString().slice(0, 10), |
| 128 | + content |
| 129 | + }, null, 2) |
| 130 | + const filePath = `market/${props.itemType}/${shareId}.json` |
| 131 | + await gh(`/repos/${username}/${REPO_NAME}/contents/${encodeURIComponent(filePath)}`, token, { |
| 132 | + method: 'PUT', |
| 133 | + body: { |
| 134 | + message: `market: add ${typeLabel.value}「${props.itemName}」 by ${form.value.author.trim()}`, |
| 135 | + content: btoa(unescape(encodeURIComponent(itemJson))), |
| 136 | + branch |
| 137 | + } |
| 138 | + }) |
| 139 | +
|
| 140 | + // 5. 创建 Pull Request |
| 141 | + const pr = await gh(`/repos/${REPO_OWNER}/${REPO_NAME}/pulls`, token, { |
| 142 | + method: 'POST', |
| 143 | + body: { |
| 144 | + title: `[市场] ${typeLabel.value}:${props.itemName}(by ${form.value.author.trim()})`, |
| 145 | + head: `${username}:${branch}`, |
| 146 | + base: baseBranch, |
| 147 | + body: [ |
| 148 | + `分享类型:${typeLabel.value}`, |
| 149 | + `名称:${props.itemName}`, |
| 150 | + `作者:${form.value.author.trim()}`, |
| 151 | + `版本:${form.value.version.trim() || '1.0.0'}`, |
| 152 | + `描述:${props.description || ''}`, |
| 153 | + '', |
| 154 | + `文件:${filePath}`, |
| 155 | + '', |
| 156 | + '管理员审核合并后进入官方市场(market 目录),其他用户可通过「市场 → 发现」拉取。' |
| 157 | + ].join('\n') |
| 158 | + } |
| 159 | + }) |
| 160 | +
|
| 161 | + shareResult.value = { ok: true, msg: `PR 创建成功:${pr.html_url}` } |
| 162 | + emit('shared', pr.html_url) |
| 163 | + window.open(pr.html_url, '_blank') |
| 164 | + } catch (e: any) { |
| 165 | + shareResult.value = { ok: false, msg: `分享失败:${e?.message || '未知错误'}` } |
| 166 | + } finally { |
| 167 | + sharing.value = false |
| 168 | + } |
| 169 | +} |
| 170 | +</script> |
0 commit comments