Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 35
refactor: split web ui modules and harden provider switching#68
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
26d3eeba5c327f3156c6ba7eb34d974fe04c404349bf27523b35987557d76d929c7da9e1e8c35a73ad9e02c2d5580d5f806358e7ccb05e7d9851d4917d2424d8dd96235a8be2d1b924b2347988File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -62,6 +62,12 @@ const { | ||
| validateWorkflowDefinition, | ||
| executeWorkflowDefinition | ||
| } = require('./lib/workflow-engine'); | ||
| const { | ||
| readBundledWebUiCss, | ||
| readBundledWebUiHtml, | ||
| readExecutableBundledJavaScriptModule, | ||
| readExecutableBundledWebUiScript | ||
| } = require('./web-ui/source-bundle.cjs'); | ||
| const DEFAULT_WEB_PORT = 3737; | ||
| const DEFAULT_WEB_HOST = '0.0.0.0'; | ||
| @@ -9945,10 +9951,11 @@ function formatHostForUrl(host) { | ||
| return value; | ||
| } | ||
| // #region watchPathsForRestart | ||
| function watchPathsForRestart(targets, onChange) { | ||
| const disposers = []; | ||
| const debounceMs = 300; | ||
| let timer = null; | ||
| const watcherEntries = new Map(); | ||
| const trigger = (info) => { | ||
| if (timer) clearTimeout(timer); | ||
| @@ -9958,35 +9965,201 @@ function watchPathsForRestart(targets, onChange) { | ||
| }, debounceMs); | ||
| }; | ||
| const addWatcher = (target, recursive) => { | ||
| const closeWatcher = (watchKey) => { | ||
| const entry = watcherEntries.get(watchKey); | ||
| if (!entry) return; | ||
| watcherEntries.delete(watchKey); | ||
| try { | ||
| entry.watcher.close(); | ||
| } catch (_) {} | ||
| }; | ||
| const listDirectoryTree = (rootDir) => { | ||
| const queue = [rootDir]; | ||
| const directories = []; | ||
| const seen = new Set(); | ||
| while (queue.length) { | ||
| const current = queue.shift(); | ||
| if (!current || seen.has(current) || !fs.existsSync(current)) { | ||
| continue; | ||
| } | ||
| seen.add(current); | ||
| let stat = null; | ||
| try { | ||
| stat = fs.statSync(current); | ||
| } catch (_) { | ||
| continue; | ||
| } | ||
| if (!stat || !stat.isDirectory()) { | ||
| continue; | ||
| } | ||
| directories.push(current); | ||
| let entries = []; | ||
| try { | ||
| entries = fs.readdirSync(current, { withFileTypes: true }); | ||
| } catch (_) { | ||
| continue; | ||
| } | ||
| for (const entry of entries) { | ||
| if (entry && typeof entry.isDirectory === 'function' && entry.isDirectory()) { | ||
| queue.push(path.join(current, entry.name)); | ||
| } | ||
| } | ||
| } | ||
| return directories; | ||
| }; | ||
| const isSameOrNestedPath = (candidate, rootDir) => { | ||
| return candidate === rootDir || candidate.startsWith(`${rootDir}${path.sep}`); | ||
| }; | ||
| const addWatcher = (target, recursive, isDirectory = false) => { | ||
| if (!fs.existsSync(target)) return; | ||
| const watchKey = `${recursive ? 'recursive' : 'plain'}:${target}`; | ||
| if (watcherEntries.has(watchKey)) { | ||
| return true; | ||
| } | ||
| try { | ||
| const watcher = fs.watch(target, { recursive }, (eventType, filename) => { | ||
| const basename = isDirectory ? '' : path.basename(target); | ||
| const watchTarget = isDirectory ? target : path.dirname(target); | ||
| const watcher = fs.watch(watchTarget, { recursive }, (eventType, filename) => { | ||
| if (isDirectory && !recursive && eventType === 'rename') { | ||
| syncDirectoryTree(target); | ||
| } | ||
| if (!filename) return; | ||
| const lower = filename.toLowerCase(); | ||
| if (!(/\.(html|js|mjs|css)$/.test(lower))) return; | ||
| trigger({ target, eventType, filename }); | ||
| let normalizedFilename = String(filename).replace(/\\/g, '/'); | ||
| if (!isDirectory) { | ||
| const fileNameOnly = normalizedFilename.split('/').pop(); | ||
| if (fileNameOnly !== basename) { | ||
| return; | ||
| } | ||
| normalizedFilename = basename; | ||
| } | ||
| const lower = normalizedFilename.toLowerCase(); | ||
| if (!(/\.(html|js|mjs|cjs|css)$/.test(lower))) return; | ||
| trigger({ target, eventType, filename: normalizedFilename }); | ||
| }); | ||
| watcher.on('error', () => { | ||
| closeWatcher(watchKey); | ||
| if (isDirectory && recursive && !fs.existsSync(target)) { | ||
| syncDirectoryTree(target); | ||
| addMissingDirectoryWatcher(target); | ||
| return; | ||
| } | ||
| if (isDirectory && !recursive) { | ||
| syncDirectoryTree(target); | ||
| } else if (fs.existsSync(target)) { | ||
| addWatcher(target, recursive, isDirectory); | ||
| } | ||
| }); | ||
coderabbitai[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| watcherEntries.set(watchKey, { | ||
| watcher, | ||
| target, | ||
| recursive, | ||
| isDirectory | ||
| }); | ||
| disposers.push(() => watcher.close()); | ||
| return true; | ||
| } catch (e) { | ||
| return false; | ||
| } | ||
| }; | ||
coderabbitai[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| const addMissingDirectoryWatcher = (target) => { | ||
| const parentDir = path.dirname(target); | ||
| if (!parentDir || parentDir === target || !fs.existsSync(parentDir)) { | ||
| return false; | ||
| } | ||
| const watchKey = `missing-dir:${target}`; | ||
| if (watcherEntries.has(watchKey)) { | ||
| return true; | ||
| } | ||
| const basename = path.basename(target); | ||
| try { | ||
| const watcher = fs.watch(parentDir, { recursive: false }, (_eventType, filename) => { | ||
| if (!filename) return; | ||
| const fileNameOnly = String(filename).replace(/\\/g, '/').split('/').pop(); | ||
| if (fileNameOnly !== basename) { | ||
| return; | ||
| } | ||
| if (!fs.existsSync(target)) { | ||
| syncDirectoryTree(target); | ||
| return; | ||
| } | ||
| closeWatcher(watchKey); | ||
| const ok = addWatcher(target, true, true); | ||
| if (!ok) { | ||
| syncDirectoryTree(target); | ||
| } | ||
| }); | ||
| watcher.on('error', () => { | ||
| closeWatcher(watchKey); | ||
| if (fs.existsSync(parentDir) && !fs.existsSync(target)) { | ||
| addMissingDirectoryWatcher(target); | ||
| } | ||
| }); | ||
| watcherEntries.set(watchKey, { | ||
| watcher, | ||
| target: parentDir, | ||
| recursive: false, | ||
| isDirectory: false | ||
| }); | ||
| return true; | ||
| } catch (_) { | ||
| return false; | ||
| } | ||
| }; | ||
| const syncDirectoryTree = (rootDir) => { | ||
| const directories = listDirectoryTree(rootDir); | ||
| const existingDirectorySet = new Set(directories); | ||
| for (const [watchKey, entry] of Array.from(watcherEntries.entries())) { | ||
| if (!entry.isDirectory || entry.recursive) { | ||
| continue; | ||
| } | ||
| if (!isSameOrNestedPath(entry.target, rootDir)) { | ||
| continue; | ||
| } | ||
| if (!existingDirectorySet.has(entry.target)) { | ||
| closeWatcher(watchKey); | ||
| } | ||
| } | ||
| for (const directory of directories) { | ||
| addWatcher(directory, false, true); | ||
| } | ||
| }; | ||
| for (const target of targets) { | ||
| const ok = addWatcher(target, true); | ||
| if (!fs.existsSync(target)) continue; | ||
| let stat = null; | ||
| try { | ||
| stat = fs.statSync(target); | ||
| } catch (_) { | ||
| continue; | ||
| } | ||
| if (stat && stat.isDirectory()) { | ||
| const ok = addWatcher(target, true, true); | ||
| if (!ok) { | ||
| syncDirectoryTree(target); | ||
| } | ||
| continue; | ||
| } | ||
| const ok = addWatcher(target, true, false); | ||
| if (!ok) { | ||
| addWatcher(target, false); | ||
| addWatcher(target, false, false); | ||
| } | ||
| } | ||
| return () => { | ||
| for (const dispose of disposers) { | ||
| try { dispose(); } catch (_) {} | ||
| if (timer) { | ||
| clearTimeout(timer); | ||
| timer = null; | ||
| } | ||
| for (const watchKey of Array.from(watcherEntries.keys())) { | ||
| closeWatcher(watchKey); | ||
| } | ||
| }; | ||
| } | ||
| // #endregion watchPathsForRestart | ||
| function writeJsonResponse(res, statusCode, payload) { | ||
| const body = JSON.stringify(payload, null, 2); | ||
| @@ -10131,8 +10304,46 @@ async function handleImportSkillsZipUpload(req, res, options = {}) { | ||
| } | ||
| } | ||
| const PUBLIC_WEB_UI_DYNAMIC_ASSETS = new Map([ | ||
| ['app.js', { | ||
| mime: 'application/javascript; charset=utf-8', | ||
| reader: readExecutableBundledWebUiScript | ||
| }], | ||
| ['index.html', { | ||
| mime: 'text/html; charset=utf-8', | ||
| reader: readBundledWebUiHtml | ||
| }], | ||
| ['logic.mjs', { | ||
| mime: 'application/javascript; charset=utf-8', | ||
| reader: readExecutableBundledJavaScriptModule | ||
| }], | ||
| ['styles.css', { | ||
| mime: 'text/css; charset=utf-8', | ||
| reader: readBundledWebUiCss | ||
| }] | ||
| ]); | ||
| const PUBLIC_WEB_UI_STATIC_ASSETS = new Set([ | ||
| 'modules/config-mode.computed.mjs', | ||
| 'modules/skills.computed.mjs', | ||
| 'modules/skills.methods.mjs', | ||
| 'session-helpers.mjs' | ||
| ]); | ||
| function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser }) { | ||
| const connections = new Set(); | ||
| const writeWebUiAssetError = (res, requestPath, error) => { | ||
| const message = error && error.message ? error.message : String(error); | ||
| console.error(`! Web UI 资源读取失败 [${requestPath}]:`, message); | ||
| if (res.headersSent) { | ||
| try { | ||
| res.destroy(error); | ||
| } catch (_) {} | ||
| return; | ||
| } | ||
| res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' }); | ||
| res.end('Internal Server Error'); | ||
| }; | ||
| const server = http.createServer((req, res) => { | ||
| const requestPath = (req.url || '/').split('?')[0]; | ||
| @@ -10562,6 +10773,14 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser | ||
| res.end(errorBody, 'utf-8'); | ||
| } | ||
| }); | ||
| } else if (requestPath === '/web-ui') { | ||
| try { | ||
| const html = readBundledWebUiHtml(htmlPath); | ||
| res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); | ||
| res.end(html); | ||
| } catch (error) { | ||
| writeWebUiAssetError(res, requestPath, error); | ||
| } | ||
| } else if (requestPath.startsWith('/web-ui/')) { | ||
coderabbitai[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| const normalized = path.normalize(requestPath).replace(/^([\\.\\/])+/, ''); | ||
| const filePath = path.join(__dirname, normalized); | ||
| @@ -10570,6 +10789,23 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser | ||
| res.end('Forbidden'); | ||
| return; | ||
| } | ||
| const relativePath = path.relative(webDir, filePath).replace(/\\/g, '/'); | ||
| const dynamicAsset = PUBLIC_WEB_UI_DYNAMIC_ASSETS.get(relativePath); | ||
| if (dynamicAsset) { | ||
| try { | ||
| const assetBody = dynamicAsset.reader(filePath); | ||
| res.writeHead(200, { 'Content-Type': dynamicAsset.mime }); | ||
| res.end(assetBody, 'utf-8'); | ||
| } catch (error) { | ||
| writeWebUiAssetError(res, requestPath, error); | ||
| } | ||
| return; | ||
| } | ||
| if (!PUBLIC_WEB_UI_STATIC_ASSETS.has(relativePath)) { | ||
| res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }); | ||
| res.end('Not Found'); | ||
| return; | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) { | ||
| res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }); | ||
| res.end('Not Found'); | ||
| @@ -10642,9 +10878,13 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser | ||
| res.writeHead(200, { 'Content-Type': mime }); | ||
| fs.createReadStream(filePath).pipe(res); | ||
| } else { | ||
| const html = fs.readFileSync(htmlPath, 'utf-8'); | ||
| res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); | ||
| res.end(html); | ||
| try { | ||
| const html = readBundledWebUiHtml(htmlPath); | ||
| res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); | ||
| res.end(html); | ||
| } catch (error) { | ||
| writeWebUiAssetError(res, requestPath, error); | ||
| } | ||
| } | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.