11<template >
2- <el-dialog :model-value =" visible" @update:model-value =" onVisibleChange" title =" 🚀 分享到官方市场(GitHub)" width =" 580px " append-to-body >
2+ <el-dialog :model-value =" visible" @update:model-value =" onVisibleChange" title =" 🚀 分享到官方市场(GitHub)" width =" 600px " append-to-body >
33 <div style =" margin-bottom :10px ;color :#606266 ;font-size :13px ;line-height :1.7 " >
4- 分享流程:① 填写作者 → ② 在本机生成 <b >market/{type}/{id}.json</ b > 并更新 < b >market/index.json</ b >
5- → ③ 登录 GitHub 授权(Token)→ ④ 自动生成 <b >Pull Request </b > 到官方市场仓库
6- < b >pythonHuang/APIWorkflowCanvasDesignNet</ b >, 管理员定期审核合并后进入官方市场(其他用户可通过「发现」拉取)。
4+ 将当前条目以 <b >JSON 文件</ b > 形式提交到官方市场仓库
5+ < b >pythonHuang/APIWorkflowCanvasDesignNet</ b > 的 <b >market/{type}/{id}.json </b >,
6+ 管理员定期审核合并后进入官方市场(其他用户可通过「发现」拉取)。
77 </div >
88 <el-form label-width =" 90px" size =" small" >
9+ <el-form-item label =" 提交方式" >
10+ <el-radio-group v-model =" submitMode" >
11+ <el-radio value =" token" >Token 自动提交</el-radio >
12+ <el-radio value =" manual" >网页手动提交(无需 Token)</el-radio >
13+ </el-radio-group >
14+ </el-form-item >
915 <el-form-item label =" 作者" ><el-input v-model =" form.author" placeholder =" 您的署名(如 pythonHuang)" /></el-form-item >
1016 <el-form-item label =" 版本号" ><el-input v-model =" form.version" placeholder =" 如 1.0.0" /></el-form-item >
11- <el-form-item label =" GitHub Token" >
12- <el-input v-model =" form.token" type =" password" show-password placeholder =" ghp_...(仅保存在本机浏览器)" />
13- <div style =" font-size :11px ;color :#909399 ;line-height :1.6 " >
14- 需要 Personal Access Token(classic)并勾选 <b >repo</b > 权限:GitHub → Settings → Developer settings → Tokens。仅用于本次分享操作。
15- </div >
16- </el-form-item >
1717 <el-form-item label =" 条目" >
1818 <div style =" font-size :13px ;color :#303133 " >
1919 <span style =" margin-right :4px " >{{ props.icon || '📦' }}</span >{{ props.itemName }}
2020 <el-tag size =" small" style =" margin-left :6px " >{{ typeLabel }}</el-tag >
2121 </div >
2222 </el-form-item >
23+ <el-form-item v-if =" submitMode === 'token'" label =" GitHub Token" >
24+ <el-input v-model =" form.token" type =" password" show-password placeholder =" ghp_...(仅保存在本机浏览器)" />
25+ <div style =" font-size :11px ;color :#909399 ;line-height :1.6 " >
26+ 需要 Personal Access Token(classic)并勾选 <b >repo</b > 权限:GitHub → Settings → Developer settings → Tokens。仅用于本次分享操作。
27+ </div >
28+ </el-form-item >
2329 </el-form >
30+
31+ <!-- Token 模式:分步进度 -->
2432 <div v-if =" shareSteps.length > 0" style =" margin-bottom :8px ;font-size :12px ;color :#606266 ;line-height :1.8 " >
2533 <div v-for =" (s, i) in shareSteps" :key =" i" :style =" `color:${s.ok === undefined ? '#909399' : s.ok ? '#67c23a' : '#f56c6c'}`" >
2634 {{ s.ok === undefined ? '⏳' : s.ok ? '✅' : '❌' }} {{ s.text }}
2735 </div >
2836 </div >
37+
38+ <!-- 网页手动模式:操作指引 -->
39+ <div v-if =" submitMode === 'manual'" style =" background :#f5f7fa ;border-radius :6px ;padding :10px 12px ;margin-bottom :8px ;font-size :12px ;color :#606266 ;line-height :1.8 " >
40+ <b >操作步骤:</b ><br />
41+ ① 点击「打开 GitHub 创建文件页」— 文件内容与提交信息已自动填好(无写权限时 GitHub 会自动 fork 到你的账号并创建分支)<br />
42+ ② 在页面上点击 <b >Commit changes / Propose changes</b > 提交(PR 标题会自动带入提交信息)<br />
43+ ③ 点击「打开 PR 提交页」— 或复制下方 PR 标题与描述粘贴到 PR 表单中,确认提交
44+ <div style =" margin-top :4px ;color :#909399 " >提示:手动模式仅提交条目文件;market/index.json 的更新由管理员审核合并时处理,或使用 Token 模式自动同步。</div >
45+ </div >
46+
2947 <el-alert v-if =" shareResult" :type =" shareResult.ok ? 'success' : 'error'" :closable =" false" style =" margin-bottom :8px " >
3048 <template #title >{{ shareResult.msg }}</template >
3149 </el-alert >
50+
3251 <template #footer >
33- <el-button size =" small" @click =" emit('update:visible', false)" >关闭</el-button >
34- <el-button size =" small" type =" primary" :loading =" sharing" @click =" doShare" >🚀 生成 Pull Request</el-button >
52+ <template v-if =" submitMode === ' manual' " >
53+ <el-button size =" small" @click =" emit('update:visible', false)" >关闭</el-button >
54+ <el-button size =" small" @click =" copyPrInfo" >📋 复制 PR 标题与描述</el-button >
55+ <el-button size =" small" @click =" openPrPage" >② 打开 PR 提交页</el-button >
56+ <el-button size =" small" type =" primary" :loading =" sharing" @click =" prepareManual" >① 打开 GitHub 创建文件页</el-button >
57+ </template >
58+ <template v-else >
59+ <el-button size =" small" @click =" emit('update:visible', false)" >关闭</el-button >
60+ <el-button size =" small" type =" primary" :loading =" sharing" @click =" doShare" >🚀 生成 Pull Request</el-button >
61+ </template >
3562 </template >
3663 </el-dialog >
3764</template >
@@ -61,11 +88,15 @@ const TOKEN_KEY = 'market_github_token'
6188const typeLabels: Record <string , string > = { api: ' 接口' , flow: ' 流程' , assistant: ' 模型助手' , skill: ' Skill' , report: ' 报表' }
6289const typeLabel = computed (() => typeLabels [props .itemType ] || props .itemType )
6390
91+ const submitMode = ref <' token' | ' manual' >(' token' )
6492const form = ref ({ token: localStorage .getItem (TOKEN_KEY ) || ' ' , author: ' ' , version: ' 1.0.0' })
6593const sharing = ref (false )
6694const shareResult = ref <{ ok: boolean ; msg: string } | null >(null )
6795const shareSteps = ref <{ text: string ; ok? : boolean }[]>([])
6896
97+ // 已生成的分享信息(两种模式共用)
98+ const shareInfo = ref <{ id: number ; itemJson: string ; prTitle: string ; prBody: string ; filePath: string } | null >(null )
99+
69100async function gh(path : string , token : string , opts ? : { method? : string ; body? : any }) {
70101 const res = await fetch (` ${GITHUB_API }${path } ` , {
71102 headers: {
@@ -86,6 +117,96 @@ function sleep(ms: number) { return new Promise(r => setTimeout(r, ms)) }
86117/** base64 编码(支持中文) */
87118function b64(s : string ) { return btoa (unescape (encodeURIComponent (s ))) }
88119
120+ /** 组装 PR 标题与描述(两种模式共用) */
121+ function buildPrInfo(id : number , author : string , version : string ) {
122+ const filePath = ` market/${props .itemType }/${id }.json `
123+ const prTitle = ` [市场] ${typeLabel .value }:${props .itemName }(by ${author }) `
124+ const prBody = [
125+ ` 分享类型:${typeLabel .value } ` ,
126+ ` 名称:${props .itemName } ` ,
127+ ` 作者:${author } ` ,
128+ ` 版本:${version } ` ,
129+ ` 描述:${props .description || ' ' } ` ,
130+ ' ' ,
131+ ` 文件:${filePath } ` ,
132+ ' ' ,
133+ ' 管理员审核合并后进入官方市场(market 目录),其他用户可通过「市场 → 发现」拉取。'
134+ ].join (' \n ' )
135+ return { filePath , prTitle , prBody }
136+ }
137+
138+ /** ① 生成本地分享文件(后端落盘 market/{type}/{id}.json + 更新 index.json) */
139+ async function generateLocalFile() {
140+ const res: any = await request .post (' /market/generate-share-file' , {
141+ itemType: props .itemType ,
142+ name: props .itemName ,
143+ description: props .description || ' ' ,
144+ icon: props .icon || ' 📦' ,
145+ author: form .value .author .trim (),
146+ version: form .value .version .trim () || ' 1.0.0' ,
147+ contentJson: props .contentJson
148+ })
149+ const { id, itemJson } = res .data || {}
150+ if (! id || ! itemJson ) throw new Error (' 生成分享文件失败' )
151+ const { filePath, prTitle, prBody } = buildPrInfo (id , form .value .author .trim (), form .value .version .trim () || ' 1.0.0' )
152+ shareInfo .value = { id , itemJson , prTitle , prBody , filePath }
153+ return shareInfo .value
154+ }
155+
156+ /** 网页手动提交:① 打开 GitHub 创建文件页(内容/提交信息自动填好) */
157+ async function prepareManual() {
158+ if (! form .value .author .trim ()) { ElMessage .warning (' 请填写作者' ); return }
159+ sharing .value = true
160+ shareResult .value = null
161+ try {
162+ const info = await generateLocalFile ()
163+ const commitMsg = ` market: add ${typeLabel .value }「${props .itemName }」 by ${form .value .author .trim ()} `
164+ const url = ` https://github.com/${REPO_OWNER }/${REPO_NAME }/new/main `
165+ + ` ?filename=${encodeURIComponent (info .filePath )} `
166+ + ` &value=${encodeURIComponent (info .itemJson )} `
167+ + ` &message=${encodeURIComponent (commitMsg )} `
168+ window .open (url , ' _blank' )
169+ shareResult .value = { ok: true , msg: ' 已打开 GitHub 创建文件页(内容与提交信息已自动填好),提交后请继续第 ② 步打开 PR 提交页' }
170+ } catch (e : any ) {
171+ shareResult .value = { ok: false , msg: ` 生成分享文件失败:${e ?.message || ' 未知错误' } ` }
172+ } finally { sharing .value = false }
173+ }
174+
175+ /** 网页手动提交:② 打开 PR 提交页(标题/描述自动填写) */
176+ function openPrPage() {
177+ const info = shareInfo .value
178+ if (! info ) { ElMessage .warning (' 请先执行第 ① 步生成分享文件' ); return }
179+ const url = ` https://github.com/${REPO_OWNER }/${REPO_NAME }/compare `
180+ + ` ?expand=1&title=${encodeURIComponent (info .prTitle )}&body=${encodeURIComponent (info .prBody )} `
181+ window .open (url , ' _blank' )
182+ }
183+
184+ /** 复制 PR 标题与描述(供用户粘贴到 PR 表单) */
185+ async function copyPrInfo() {
186+ const info = shareInfo .value
187+ if (! info ) { ElMessage .warning (' 请先执行第 ① 步生成分享文件' ); return }
188+ const text = ` ${info .prTitle }\n\n ${info .prBody } `
189+ try {
190+ await navigator .clipboard .writeText (text )
191+ ElMessage .success (' 已复制 PR 标题与描述' )
192+ } catch {
193+ // 剪贴板不可用(非 HTTPS)时降级为提示框
194+ ElMessageBoxAlert (text )
195+ }
196+ }
197+
198+ function ElMessageBoxAlert(text : string ) {
199+ // 降级:弹窗展示可复制内容
200+ const w = window .open (' ' , ' _blank' )
201+ if (w ) {
202+ w .document .write (` <pre style="white-space:pre-wrap;font-family:monospace">${text .replace (/ </ g , ' <' )}</pre> ` )
203+ w .document .close ()
204+ } else {
205+ ElMessage .info (' 请手动复制:' + text )
206+ }
207+ }
208+
209+ /** Token 自动提交:fork → 分支 → 写条目文件 → 合并官方最新 index.json → 创建 PR */
89210async function doShare() {
90211 if (! form .value .author .trim ()) { ElMessage .warning (' 请填写作者' ); return }
91212 if (! form .value .token .trim ()) { ElMessage .warning (' 请填写 GitHub 访问令牌' ); return }
@@ -98,28 +219,15 @@ async function doShare() {
98219 const token = form .value .token .trim ()
99220
100221 try {
101- // ① 本地生成 market/{type}/{id}.json 并更新 market/index.json(后端落盘)
102222 step (' 生成本地 market/{type}/{id}.json 并更新 index.json...' )
103- const genRes: any = await request .post (' /market/generate-share-file' , {
104- itemType: props .itemType ,
105- name: props .itemName ,
106- description: props .description || ' ' ,
107- icon: props .icon || ' 📦' ,
108- author: form .value .author .trim (),
109- version: form .value .version .trim () || ' 1.0.0' ,
110- contentJson: props .contentJson
111- })
112- const { id, itemJson, indexEntry } = genRes .data || {}
113- if (! id || ! itemJson ) throw new Error (' 生成分享文件失败' )
114- step (` 已生成本地文件 market/${props .itemType }/${id }.json 并更新 index.json ` , true )
223+ const info = await generateLocalFile ()
224+ step (` 已生成本地文件 ${info .filePath } 并更新 index.json ` , true )
115225
116- // ② 校验令牌并获取登录用户
117226 step (' 校验 GitHub Token...' )
118227 const user = await gh (' /user' , token )
119228 const username = user .login
120229 step (` GitHub 登录成功(${username }) ` , true )
121230
122- // ③ 确保 fork 存在(不存在则创建并轮询等待)
123231 step (' 确保仓库 fork 存在...' )
124232 let fork = await gh (` /repos/${username }/${REPO_NAME } ` , token ).catch (() => null )
125233 if (! fork ) {
@@ -133,37 +241,36 @@ async function doShare() {
133241 }
134242 step (' fork 就绪' , true )
135243
136- // ④ 基于 fork 默认分支创建分享分支
137244 const baseBranch = fork .default_branch || ' main'
138245 const baseRef = await gh (` /repos/${username }/${REPO_NAME }/git/ref/heads/${baseBranch } ` , token )
139- const branch = ` market-share-${props .itemType }-${id } `
246+ const branch = ` market-share-${props .itemType }-${info . id } `
140247 try {
141248 await gh (` /repos/${username }/${REPO_NAME }/git/refs ` , token , {
142249 method: ' POST' ,
143250 body: { ref: ` refs/heads/${branch } ` , sha: baseRef .object .sha }
144251 })
145252 } catch { /* 分支已存在则直接复用 */ }
146253
147- // ⑤ 写入条目文件 market/{type}/{id}.json
148- step (` 写入 market/${props .itemType }/${id }.json... ` )
149- const filePath = ` market/${props .itemType }/${id }.json `
150- await gh (` /repos/${username }/${REPO_NAME }/contents/${encodeURIComponent (filePath )} ` , token , {
254+ step (` 写入 ${info .filePath }... ` )
255+ await gh (` /repos/${username }/${REPO_NAME }/contents/${encodeURIComponent (info .filePath )} ` , token , {
151256 method: ' PUT' ,
152257 body: {
153258 message: ` market: add ${typeLabel .value }「${props .itemName }」 by ${form .value .author .trim ()} ` ,
154- content: b64 (itemJson ),
259+ content: b64 (info . itemJson ),
155260 branch
156261 }
157262 })
158- step (` 已写入 ${filePath } ` , true )
263+ step (` 已写入 ${info . filePath } ` , true )
159264
160- // ⑥ 基于官方仓库最新 index.json 合并本条索引并写入
161265 step (' 更新 market/index.json...' )
162266 const upstreamIndex = await fetchUpstreamIndex ()
163267 const idx = upstreamIndex .findIndex ((e : any ) =>
164- Number (e ?.id ) === Number (indexEntry .id ) && e ?.type === indexEntry .type )
165- if (idx >= 0 ) upstreamIndex [idx ] = indexEntry
166- else upstreamIndex .push (indexEntry )
268+ Number (e ?.id ) === Number (info .id ) && e ?.type === props .itemType )
269+ const indexEntry = JSON .parse (info .itemJson )
270+ const { content, ... indexMeta } = indexEntry
271+ const indexEntryForIndex = { ... indexMeta , file: info .filePath }
272+ if (idx >= 0 ) upstreamIndex [idx ] = indexEntryForIndex
273+ else upstreamIndex .push (indexEntryForIndex )
167274 const indexJson = JSON .stringify ({ items: upstreamIndex }, null , 2 )
168275 const idxFile = await gh (` /repos/${username }/${REPO_NAME }/contents/market/index.json?ref=${encodeURIComponent (baseBranch )} ` , token ).catch (() => null )
169276 await gh (` /repos/${username }/${REPO_NAME }/contents/market/index.json ` , token , {
@@ -177,26 +284,14 @@ async function doShare() {
177284 })
178285 step (' 已更新 market/index.json' , true )
179286
180- // ⑦ 创建 Pull Request
181287 step (' 创建 Pull Request...' )
182288 const pr = await gh (` /repos/${REPO_OWNER }/${REPO_NAME }/pulls ` , token , {
183289 method: ' POST' ,
184290 body: {
185- title: ` [市场] ${ typeLabel . value }:${ props . itemName }(by ${ form . value . author . trim ()}) ` ,
291+ title: info . prTitle ,
186292 head: ` ${username }:${branch } ` ,
187293 base: baseBranch ,
188- body: [
189- ` 分享类型:${typeLabel .value } ` ,
190- ` 名称:${props .itemName } ` ,
191- ` 作者:${form .value .author .trim ()} ` ,
192- ` 版本:${form .value .version .trim () || ' 1.0.0' } ` ,
193- ` 描述:${props .description || ' ' } ` ,
194- ' ' ,
195- ` 文件:${filePath } ` ,
196- ` 索引:market/index.json(已同步本条目) ` ,
197- ' ' ,
198- ' 管理员审核合并后进入官方市场(market 目录),其他用户可通过「市场 → 发现」拉取。'
199- ].join (' \n ' )
294+ body: info .prBody
200295 }
201296 })
202297 step (` PR 创建成功:${pr .html_url } ` , true )
0 commit comments