Skip to content

Commit 59a5ea6

Browse files
committed
feat: 菜单调整+Skill AI优化+分享双模式-大模型设置/Skill管理移入模型助手菜单前两位(从系统设置移出),Skill编辑弹窗描述与技能内容新增AI优化生成按钮(optimize-prompt接口增加purpose=description一句话描述模式),分享弹窗新增提交方式选择:Token自动提交(原流程)或网页手动提交(无需Token,自动填好文件内容与提交信息打开GitHub创建文件页,提供PR标题描述复制与PR提交页跳转)
1 parent dfb09be commit 59a5ea6

4 files changed

Lines changed: 196 additions & 58 deletions

File tree

‎Juggle.Api/Controllers/Api/AiController.cs‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,9 @@ public async Task<ApiResult> OptimizePrompt([FromBody] AiOptimizePromptRequest r
247247
{
248248
try
249249
{
250-
var system = "你是提示词优化专家。请把用户提供的提示词草稿优化为结构清晰、约束明确、可稳定执行的高质量系统提示词(保持原语言,直接输出优化后的提示词文本,不要解释、不要 markdown 围栏)。";
250+
var system = (req.Purpose ?? "") == "description"
251+
? "你是文案专家。请把用户提供的内容优化为一句简洁、准确、易检索的中文描述(30字以内,直接输出描述文本,不要解释、不要 markdown 围栏、不要引号)。"
252+
: "你是提示词优化专家。请把用户提供的提示词草稿优化为结构清晰、约束明确、可稳定执行的高质量系统提示词(保持原语言,直接输出优化后的提示词文本,不要解释、不要 markdown 围栏)。";
251253
var result = await _aiService.ChatAsync(system, req.Prompt ?? "", req.ProviderId, modelOverride: req.Model);
252254
return ApiResult.Success(new { prompt = result });
253255
}
@@ -482,6 +484,8 @@ public class AiOptimizePromptRequest
482484
public string? Prompt { get; set; }
483485
public long ProviderId { get; set; }
484486
public string? Model { get; set; }
487+
/// <summary>优化目标:空=系统提示词优化;description=一句话描述优化</summary>
488+
public string? Purpose { get; set; }
485489
}
486490

487491
public class AiGenerateIconRequest

‎JuggleNet6.Frontend/src/components/MarketShareDialog.vue‎

Lines changed: 148 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,64 @@
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'
6188
const typeLabels: Record<string, string> = { api: '接口', flow: '流程', assistant: '模型助手', skill: 'Skill', report: '报表' }
6289
const typeLabel = computed(() => typeLabels[props.itemType] || props.itemType)
6390
91+
const submitMode = ref<'token' | 'manual'>('token')
6492
const form = ref({ token: localStorage.getItem(TOKEN_KEY) || '', author: '', version: '1.0.0' })
6593
const sharing = ref(false)
6694
const shareResult = ref<{ ok: boolean; msg: string } | null>(null)
6795
const 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+
69100
async 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 编码(支持中文) */
87118
function 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, '&lt;')}</pre>`)
203+
w.document.close()
204+
} else {
205+
ElMessage.info('请手动复制:' + text)
206+
}
207+
}
208+
209+
/** Token 自动提交:fork → 分支 → 写条目文件 → 合并官方最新 index.json → 创建 PR */
89210
async 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)

‎JuggleNet6.Frontend/src/views/Layout.vue‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@
6767
<el-icon><MagicStick /></el-icon>
6868
<span>模型助手</span>
6969
</template>
70+
<el-menu-item index="/system/ai-provider">大模型设置</el-menu-item>
71+
<el-menu-item index="/system/skills">Skill 管理</el-menu-item>
7072
<el-menu-item index="/ai/assistants">模型助手管理</el-menu-item>
7173
<el-menu-item index="/ai/flow-assistant">流程智能编排助手</el-menu-item>
7274
<el-menu-item index="/ai/api-assistant">接口智能接入助手</el-menu-item>
@@ -95,9 +97,7 @@
9597
<el-menu-item index="/system/role" v-if="hasMenu('/system/role')">角色管理</el-menu-item>
9698
<el-menu-item index="/system/tenant" v-if="hasMenu('/system/tenant')">租户管理</el-menu-item>
9799
<el-menu-item index="/system/config" v-if="hasMenu('/system/config')">系统配置</el-menu-item>
98-
<el-menu-item index="/system/ai-provider" v-if="hasMenu('/system/config')">大模型设置</el-menu-item>
99100
<el-menu-item index="/system/redis" v-if="hasMenu('/system/config')">Redis 配置</el-menu-item>
100-
<el-menu-item index="/system/skills" v-if="hasMenu('/system/config')">Skill 管理</el-menu-item>
101101
<el-menu-item index="/system/login-log" v-if="hasMenu('/system/login-log')">登录日志</el-menu-item>
102102
<el-menu-item index="/system/audit-log" v-if="hasMenu('/system/audit-log')">审计日志</el-menu-item>
103103
</el-sub-menu>

0 commit comments

Comments
 (0)