diff --git a/ui/src/api/bioflow.js b/ui/src/api/bioflow.js index b7ca47a..942b3e6 100644 --- a/ui/src/api/bioflow.js +++ b/ui/src/api/bioflow.js @@ -127,3 +127,75 @@ export function runBioflowArchiveByName(body, opts = {}) { export function registerBioflowReport(body, opts = {}) { return post('/api/bioflow/reports/register', body, opts); } + +// --------------------------------------------------------------- +// 用户自定义扩展分析(EXTENSION_CONTRACT.md §6 端点清单) +// 生命周期:draft → build → trial_run → validate/confirm → repair → publish → run。 +// 与任务端点同风格:全部经 BFF 反代(浏览器同源 /api/bioflow/extensions/*), +// 底层走 http.js 统一错误信封;SSE 流 raw 透传由调用方消费。 +// --------------------------------------------------------------- + +/** POST /api/bioflow/extensions —— 上传 zip 创建扩展。 + * body 为 FormData(multipart):zip 文件 + display_name + command + + * runtime_image?(8 base 镜像) + golden?(可选金标输出文件)。 */ +export function createExtension(body, opts = {}) { + return post('/api/bioflow/extensions', body, { + timeout: 0, // 上传不设前端超时(与 uploadBioflowFile 同约定) + ...opts, + }); +} + +/** GET /api/bioflow/extensions —— 列表(published 优先) */ +export function listExtensions(params = {}, opts = {}) { + return get('/api/bioflow/extensions', params, opts); +} + +/** GET /api/bioflow/extensions/{id} —— 详情(extension.json) */ +export function getExtension(extId, opts = {}) { + return get(`/api/bioflow/extensions/${extId}`, {}, opts); +} + +/** DELETE /api/bioflow/extensions/{id} */ +export function deleteExtension(extId, opts = {}) { + return del(`/api/bioflow/extensions/${extId}`, opts); +} + +/** POST /api/bioflow/extensions/{id}/build —— AI 转换(命令 → command_template) */ +export function buildExtension(extId, body = {}, opts = {}) { + return post(`/api/bioflow/extensions/${extId}/build`, body, opts); +} + +/** POST /api/bioflow/extensions/{id}/trial-run —— 容器试运行(test_inputs 文件) */ +export function trialRunExtension(extId, body = {}, opts = {}) { + return post(`/api/bioflow/extensions/${extId}/trial-run`, body, opts); +} + +/** POST /api/bioflow/extensions/{id}/repair —— LLM 修复循环(上限 20 轮可配) */ +export function repairExtension(extId, body = {}, opts = {}) { + return post(`/api/bioflow/extensions/${extId}/repair`, body, opts); +} + +/** POST /api/bioflow/extensions/{id}/publish —— 发布(nl_triggers 可编辑) */ +export function publishExtension(extId, body = {}, opts = {}) { + return post(`/api/bioflow/extensions/${extId}/publish`, body, opts); +} + +/** POST /api/bioflow/extensions/{id}/run —— 正式运行(params + files → run_id) */ +export function runExtension(extId, body = {}, opts = {}) { + return post(`/api/bioflow/extensions/${extId}/run`, body, opts); +} + +/** GET /api/bioflow/extensions/{id}/runs —— 运行历史 */ +export function listExtensionRuns(extId, params = {}, opts = {}) { + return get(`/api/bioflow/extensions/${extId}/runs`, params, opts); +} + +/** GET /api/bioflow/extensions/{id}/runs/{run_id}/events —— SSE(首帧快照)。 + * 返回原始 fetch Response(SSE 由调用方消费)。 */ +export function extensionRunEvents(extId, runId, params = {}, opts = {}) { + return get(`/api/bioflow/extensions/${extId}/runs/${runId}/events`, params, { + raw: true, + timeout: 0, + ...opts, + }); +} diff --git a/ui/src/components/chat/AssistantMessage.vue b/ui/src/components/chat/AssistantMessage.vue index e8960b5..73d308d 100644 --- a/ui/src/components/chat/AssistantMessage.vue +++ b/ui/src/components/chat/AssistantMessage.vue @@ -17,6 +17,15 @@ import FlowFormCard from './FlowFormCard.vue'; import FlowFileCard from './FlowFileCard.vue'; import FlowProgressCard from './FlowProgressCard.vue'; import FlowArchiveCard from './FlowArchiveCard.vue'; +// 用户自定义扩展分析:生命周期引导卡 + 运行卡(EXTENSION_CONTRACT.md §4 块) +import ExtensionUploadCard from './ExtensionUploadCard.vue'; +import ExtensionBuildCard from './ExtensionBuildCard.vue'; +import ExtensionTrialCard from './ExtensionTrialCard.vue'; +import ExtensionValidateCard from './ExtensionValidateCard.vue'; +import ExtensionPublishCard from './ExtensionPublishCard.vue'; +import ExtensionCandidatesCard from './ExtensionCandidatesCard.vue'; +import ExtensionConfirmCard from './ExtensionConfirmCard.vue'; +import ExtensionRunCard from './ExtensionRunCard.vue'; // FR-PRIV-005:私有资料参考块(独立 i18n 文件,不碰 labels.js 红线) import { PRIV } from '../../i18n/private.js'; @@ -224,6 +233,59 @@ const degradedNote = computed(() => props.message.degradedNote || null);
{{ BF.flowQaEmpty }}
+ + + + + + + + + + + + + + + + + + +
+
{{ BF.extPublishedTitle }}
+
+ {{ BF.extPublishedHint.replace('{name}', b.displayName || '') }} + + {{ BF.extPublishTriggersLabel }}:{{ b.nlTriggers.join('、') }} + +
+
+ props.message.degradedNote || null); flex-direction: column; gap: 8px; } + +/* ---------- 用户自定义扩展分析:发布确认块(extension_published) ---------- */ +.ext-published-block { + margin: 14px 0; + padding: 14px 16px; + background: var(--pa-surface-1); + border: 1px solid var(--pa-hairline); + border-radius: var(--pa-r-lg); +} +.ext-published-title { + font-size: 12.5px; + font-weight: 600; + letter-spacing: 0.3px; + color: var(--pa-success); + margin-bottom: 4px; +} +.ext-published-hint { + font-size: 12px; + color: var(--pa-ink-muted); + line-height: 1.6; +} +.ext-published-triggers { + display: block; + margin-top: 4px; + font-size: 11px; + color: var(--pa-ink-subtle); + word-break: break-all; +} .bf-qa-cite { display: flex; align-items: flex-start; diff --git a/ui/src/components/chat/Composer.vue b/ui/src/components/chat/Composer.vue index cccf06c..47422d2 100644 --- a/ui/src/components/chat/Composer.vue +++ b/ui/src/components/chat/Composer.vue @@ -17,7 +17,7 @@ import { MODES as MODE_LABELS, T } from '../../i18n/labels.js'; import { DG, BUILTIN_TEMPLATES } from '../../i18n/docgen.js'; import { BF } from '../../i18n/bioflow.js'; import { route } from '../../intent.js'; -import { route as routeBioflow } from '../../utils/intent.js'; +import { route as routeBioflow, isExtensionCreateIntent } from '../../utils/intent.js'; import { uploadLibrary, listLibrary } from '../../api/docgen.js'; import { uploadBioflowFile } from '../../api/bioflow.js'; import { useDocgenStore } from '../../stores/docgen.js'; @@ -244,7 +244,25 @@ const submit = () => { bioflowRoute = { intent: 'qa', reuseName: null }; } } - if (bioflowRoute.intent === 'bioflow_qa') { + // 用户自定义扩展分析:A 层创建意图(EXTENSION_CONTRACT.md §5 词典同表双跑; + // 「创建/上传 一个扩展」→ sendExtension 上传引导卡。歧义消解与运行触发由 + // 服务端 intent.py 在 chat/stream 内判定——前端只做确定性创建识别,漏判安全。) + let extIntent = false; + if (bioflowRoute.intent !== 'bioflow_qa' && bioflowRoute.intent !== 'bioflow' && bioflowEnabled.value) { + try { + extIntent = isExtensionCreateIntent(text); + } catch { + extIntent = false; // 路由异常按漏判安全处理 + } + } + if (extIntent) { + // 扩展创建引导流:chat.sendExtension(upload 引导卡 → extension_* 事件帧) + try { + useChatStore().sendExtension(text, { mode: 'ask' }).catch(() => { /* 内部已标注错误 */ }); + } catch { + emit('send', text); // chat store 未就绪(测试/降级):漏判安全退化为 QA 路径 + } + } else if (bioflowRoute.intent === 'bioflow_qa') { // 生信流内流程咨询(FR-BIO-026):也走 sendBioflow(后端流内返引用块,不新建任务) try { useChatStore().sendBioflow(text, { mode: 'ask' }).catch(() => { /* 内部已标注错误 */ }); diff --git a/ui/src/components/chat/ExtensionBuildCard.vue b/ui/src/components/chat/ExtensionBuildCard.vue new file mode 100644 index 0000000..33ed0e1 --- /dev/null +++ b/ui/src/components/chat/ExtensionBuildCard.vue @@ -0,0 +1,340 @@ + + + + + diff --git a/ui/src/components/chat/ExtensionCandidatesCard.vue b/ui/src/components/chat/ExtensionCandidatesCard.vue new file mode 100644 index 0000000..fed24de --- /dev/null +++ b/ui/src/components/chat/ExtensionCandidatesCard.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/ui/src/components/chat/ExtensionConfirmCard.vue b/ui/src/components/chat/ExtensionConfirmCard.vue new file mode 100644 index 0000000..ebd5cdd --- /dev/null +++ b/ui/src/components/chat/ExtensionConfirmCard.vue @@ -0,0 +1,208 @@ + + + + + diff --git a/ui/src/components/chat/ExtensionPublishCard.vue b/ui/src/components/chat/ExtensionPublishCard.vue new file mode 100644 index 0000000..1d4ff72 --- /dev/null +++ b/ui/src/components/chat/ExtensionPublishCard.vue @@ -0,0 +1,181 @@ + + + + + diff --git a/ui/src/components/chat/ExtensionRunCard.vue b/ui/src/components/chat/ExtensionRunCard.vue new file mode 100644 index 0000000..0a4b649 --- /dev/null +++ b/ui/src/components/chat/ExtensionRunCard.vue @@ -0,0 +1,177 @@ + + + + + diff --git a/ui/src/components/chat/ExtensionTrialCard.vue b/ui/src/components/chat/ExtensionTrialCard.vue new file mode 100644 index 0000000..21eab72 --- /dev/null +++ b/ui/src/components/chat/ExtensionTrialCard.vue @@ -0,0 +1,260 @@ + + + + + diff --git a/ui/src/components/chat/ExtensionUploadCard.vue b/ui/src/components/chat/ExtensionUploadCard.vue new file mode 100644 index 0000000..06776d3 --- /dev/null +++ b/ui/src/components/chat/ExtensionUploadCard.vue @@ -0,0 +1,388 @@ + + + + + diff --git a/ui/src/components/chat/ExtensionValidateCard.vue b/ui/src/components/chat/ExtensionValidateCard.vue new file mode 100644 index 0000000..67e539b --- /dev/null +++ b/ui/src/components/chat/ExtensionValidateCard.vue @@ -0,0 +1,200 @@ + + + + + diff --git a/ui/src/components/session/ExtensionsPane.vue b/ui/src/components/session/ExtensionsPane.vue new file mode 100644 index 0000000..4b5e51c --- /dev/null +++ b/ui/src/components/session/ExtensionsPane.vue @@ -0,0 +1,338 @@ + + + + + diff --git a/ui/src/components/session/SessionPanel.vue b/ui/src/components/session/SessionPanel.vue index 44e011f..d326dfe 100644 --- a/ui/src/components/session/SessionPanel.vue +++ b/ui/src/components/session/SessionPanel.vue @@ -3,18 +3,23 @@ // 新建按钮 / 搜索框 / 分组列表(今天/昨天/更早)/ 状态点四态 / 活动渐变条 / // 重命名双击 / 删除二次确认 / 空搜索提示(FR-SESSION-001~009)。 // 注意:左抽屉形态由 B1 布局层负责;本组件只渲染面板内容(session-panel 根元素)。 -import { computed, onMounted } from 'vue'; +import { computed, onMounted, ref } from 'vue'; import { useSessionsStore } from '../../stores/sessions.js'; import { useChatStore } from '../../stores/chat.js'; import { T, SESSION_GROUPS } from '../../i18n/labels.js'; import SearchBox from './SearchBox.vue'; import SessionItem from './SessionItem.vue'; import SessionRail from './SessionRail.vue'; +// 用户自定义扩展分析:侧栏「扩展」入口(仅列表/历史/删除;创建在对话引导卡) +import ExtensionsPane from './ExtensionsPane.vue'; + +const emit = defineEmits(['open-rail', 'goto-kg', 'goto-settings']); const sessions = useSessionsStore(); const chat = useChatStore(); const confirmDelete = computed(() => sessions._confirmDelete || null); +const extensionsOpen = ref(false); onMounted(() => { sessions.load().catch(() => { @@ -145,10 +150,14 @@ const doDelete = async () => { + + +