mirror of
https://github.com/insistence/RuoYi-Vue3-FastAPI.git
synced 2026-10-15 21:35:31 +00:00
feat: 支持 v2 插件、独立前端和签名制品发布 (#130)
* feat: 支持 v2 插件、原生扩展与签名发布 * fix: 修复测试 * feat: 增强宿主 SDK、运行观测与制品维护能力 * feat: 增强文件传输、配置观测与本地开发能力 * feat: 支持 SSE 实时事件流并完善开发脚手架 * feat: 完善长连接鉴权复核与关闭回收 * feat: 增强生命周期与任务关闭可靠性 * feat: 收紧健康检查契约并补齐前端与真实网络验收 * feat: 增加SDK溯源与离线更新工具并验证跨版本兼容 * feat: 增加持久化业务示例与受控 Python 交付 * feat: 增加统一运行诊断与多worker状态观测 * feat: 增加固定目标发布等待与自动验收 * fix: 修复多环境插件部署前缀与代理配置不一致 * fix: 修复测试
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import { createPluginHostBridge, validatePluginSession } from '@ruoyi/plugin-bridge'
|
||||
import { createMockPluginRequest, createMockPluginStream } from './mockRequest.js'
|
||||
|
||||
const frame = document.getElementById('plugin-frame')
|
||||
let route = '/'
|
||||
const bridge = createPluginHostBridge({
|
||||
pluginId: 'bundle_demo',
|
||||
session: validatePluginSession(
|
||||
{
|
||||
pluginId: 'bundle_demo',
|
||||
bridgeVersion: 1,
|
||||
uiBase: '/apps/bundle_demo/ui/',
|
||||
apiBase: '/apps/bundle_demo/api/',
|
||||
csrfToken: 'local-mock-session-no-credentials',
|
||||
expiresIn: 3600,
|
||||
},
|
||||
'bundle_demo'
|
||||
),
|
||||
getTarget: () => frame.contentWindow,
|
||||
request: createMockPluginRequest({
|
||||
getDelay: () => document.getElementById('delay').value,
|
||||
shouldFail: () => document.getElementById('failure').checked,
|
||||
}),
|
||||
stream: createMockPluginStream({
|
||||
getDelay: () => document.getElementById('delay').value,
|
||||
shouldFail: () => document.getElementById('failure').checked,
|
||||
}),
|
||||
getContext: () => ({
|
||||
theme: { mode: document.getElementById('theme').value },
|
||||
language: 'zh-CN',
|
||||
timeZone: 'Asia/Shanghai',
|
||||
route,
|
||||
}),
|
||||
onRoute: (value) => {
|
||||
route = value
|
||||
bridge.updateRoute(route)
|
||||
},
|
||||
onReady: () => {
|
||||
document.getElementById('status').textContent = '模拟连接已建立'
|
||||
},
|
||||
})
|
||||
frame.src = '/index.html'
|
||||
document.getElementById('theme').addEventListener('change', () => bridge.updatePreferences())
|
||||
document
|
||||
.getElementById('reload')
|
||||
.addEventListener('click', () => frame.contentWindow.location.reload())
|
||||
document.getElementById('logout').addEventListener('click', () => {
|
||||
bridge.destroy({ logout: true })
|
||||
document.getElementById('status').textContent = '模拟会话已退出;刷新整个页面可重新连接'
|
||||
})
|
||||
window.addEventListener('pagehide', () => bridge.destroy(), { once: true })
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* 创建仅供本地开发的内存请求适配器,不发送网络请求或保存文件。
|
||||
*
|
||||
* @param {Object} options 延迟和故障注入选项
|
||||
* @returns {Function} 可传入宿主桥的异步请求函数
|
||||
*/
|
||||
export function createMockPluginRequest({ getDelay = () => 300, shouldFail = () => false } = {}) {
|
||||
const pause = (signal) =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (signal?.aborted) return reject(new Error('请求已取消'))
|
||||
const cancel = () => {
|
||||
clearTimeout(timer)
|
||||
reject(new Error('请求已取消'))
|
||||
}
|
||||
const timer = setTimeout(
|
||||
() => {
|
||||
signal?.removeEventListener('abort', cancel)
|
||||
resolve()
|
||||
},
|
||||
Math.max(0, Math.min(5000, Number(getDelay()) || 0))
|
||||
)
|
||||
signal?.addEventListener('abort', cancel, { once: true })
|
||||
})
|
||||
return async (config) => {
|
||||
const prefix = '/apps/bundle_demo/api/'
|
||||
if (!config.url.startsWith(prefix)) throw new Error('模拟宿主只处理当前插件 API')
|
||||
const path = config.url.slice(prefix.length)
|
||||
await pause(config.signal)
|
||||
if (shouldFail()) throw new Error('模拟接口失败')
|
||||
if (config.method === 'get' && path === 'summary') {
|
||||
return {
|
||||
pluginId: 'bundle_demo',
|
||||
userName: '本地模拟用户',
|
||||
serverTime: new Date().toISOString(),
|
||||
greeting: '本地模拟连接已就绪,文件仅在浏览器内处理。',
|
||||
}
|
||||
}
|
||||
if (config.method === 'post' && path === 'echo') {
|
||||
return {
|
||||
message: config.data.message,
|
||||
serverTime: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
if (config.method === 'post' && path === 'files/inspect') {
|
||||
const file = config.data.get('file')
|
||||
if (!(file instanceof Blob)) throw new Error('缺少文件')
|
||||
config.onUploadProgress?.({
|
||||
loaded: Math.floor(file.size / 2),
|
||||
total: file.size,
|
||||
})
|
||||
await pause(config.signal)
|
||||
config.onUploadProgress?.({ loaded: file.size, total: file.size })
|
||||
const digest = await crypto.subtle.digest('SHA-256', await file.arrayBuffer())
|
||||
return {
|
||||
filename: file.name,
|
||||
size: file.size,
|
||||
sha256: Array.from(new Uint8Array(digest), (byte) =>
|
||||
byte.toString(16).padStart(2, '0')
|
||||
).join(''),
|
||||
}
|
||||
}
|
||||
if (config.method === 'get' && path === 'files/report') {
|
||||
const report = new Blob(['\uFEFFname,value\nmode,local-mock\n'], {
|
||||
type: 'text/csv;charset=utf-8',
|
||||
})
|
||||
config.onDownloadProgress?.({ loaded: report.size, total: report.size })
|
||||
return report
|
||||
}
|
||||
throw new Error('模拟接口未实现')
|
||||
}
|
||||
}
|
||||
|
||||
/** 创建逐条产出事件的本地适配器,复用延迟和故障开关,支持取消与游标恢复。 */
|
||||
export function createMockPluginStream({ getDelay = () => 300, shouldFail = () => false } = {}) {
|
||||
return async function* (config) {
|
||||
if (config.url !== '/apps/bundle_demo/api/events') throw new Error('模拟实时接口未实现')
|
||||
const count = Number(config.params?.count ?? 10)
|
||||
const cursorText = config.lastEventId || '0'
|
||||
const cursor = Number(cursorText)
|
||||
if (
|
||||
!Number.isInteger(count) ||
|
||||
count < 1 ||
|
||||
count > 20 ||
|
||||
!/^[0-9]{1,2}$/.test(cursorText) ||
|
||||
!Number.isInteger(cursor) ||
|
||||
cursor < 0 ||
|
||||
cursor > count
|
||||
)
|
||||
throw new Error('模拟事件参数无效')
|
||||
for (let index = cursor + 1; index <= count; index++) {
|
||||
await new Promise((resolve, reject) => {
|
||||
if (config.signal.aborted) return reject(new Error('已取消'))
|
||||
const cancel = () => {
|
||||
clearTimeout(timer)
|
||||
reject(new Error('已取消'))
|
||||
}
|
||||
const timer = setTimeout(
|
||||
() => {
|
||||
config.signal.removeEventListener('abort', cancel)
|
||||
resolve()
|
||||
},
|
||||
Math.max(100, Math.min(5000, Number(getDelay()) || 0))
|
||||
)
|
||||
config.signal.addEventListener('abort', cancel, { once: true })
|
||||
})
|
||||
if (shouldFail()) throw new Error('模拟实时接口失败')
|
||||
yield {
|
||||
event: 'tick',
|
||||
id: String(index),
|
||||
data: JSON.stringify({ index, total: count, serverTime: new Date().toISOString() }),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user