@steedos-labs/plugin-workflow 3.0.26 → 3.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,110 @@
1
+ # Workflow Designer (React + shadcn/ui)
2
+
3
+ Modern workflow designer built with React 18, TypeScript, and shadcn/ui.
4
+
5
+ ## 🚀 Quick Start
6
+
7
+ ### Development
8
+
9
+ ```bash
10
+ npm install
11
+ npm run dev
12
+ ```
13
+
14
+ Open http://localhost:5173/
15
+
16
+ ### Production Build
17
+
18
+ ```bash
19
+ npm run build
20
+ ```
21
+
22
+ Built files will be in `dist/` directory.
23
+
24
+ ## 🛠ïļ Tech Stack
25
+
26
+ - **React 19** - UI framework
27
+ - **TypeScript** - Type safety
28
+ - **Vite** - Build tool
29
+ - **Tailwind CSS** - Styling
30
+ - **shadcn/ui** - UI components
31
+ - **Zustand** - State management
32
+ - **Axios** - HTTP client
33
+
34
+ ## 📁 Project Structure
35
+
36
+ ```
37
+ designer/
38
+ ├── src/
39
+ │ ├── components/
40
+ │ │ ├── ui/ # shadcn/ui components
41
+ │ │ ├── layout/ # Layout components
42
+ │ │ ├── workflow/ # Workflow-specific (TODO)
43
+ │ │ └── form/ # Form designer (TODO)
44
+ │ ├── lib/ # Utilities
45
+ │ ├── pages/ # Page components
46
+ │ │ └── WorkflowDesigner.tsx
47
+ │ ├── stores/ # Zustand stores (TODO)
48
+ │ ├── types/ # TypeScript types (TODO)
49
+ │ ├── App.tsx
50
+ │ └── main.tsx
51
+ ├── public/
52
+ ├── dist/ # Build output
53
+ └── package.json
54
+ ```
55
+
56
+ ## 🌐 Backend Integration
57
+
58
+ The new designer is served via:
59
+ - **Development**: http://localhost:5173/
60
+ - **Production**: /api/workflow/designer-v2
61
+
62
+ Backend route: `main/default/routes/designer-v2.router.js`
63
+
64
+ ## 📝 Features
65
+
66
+ ### Implemented ✅
67
+ - Modern UI with shadcn/ui
68
+ - Responsive layout
69
+ - TypeScript type safety
70
+ - Basic component structure
71
+ - Development server
72
+ - Production build
73
+
74
+ ### TODO 🚧
75
+ - Workflow canvas with React Flow
76
+ - Form designer components
77
+ - State management with Zustand
78
+ - API integration
79
+ - Internationalization (i18n)
80
+ - Unit tests
81
+ - More shadcn/ui components
82
+
83
+ ## ðŸŽĻ Customization
84
+
85
+ ### Tailwind Configuration
86
+
87
+ Edit `tailwind.config.js` to customize theme.
88
+
89
+ ### Path Aliases
90
+
91
+ TypeScript path aliases are configured in `tsconfig.app.json`:
92
+ - `@/` maps to `./src/`
93
+
94
+ ## ðŸ“Ķ Scripts
95
+
96
+ - `npm run dev` - Start development server
97
+ - `npm run build` - Build for production
98
+ - `npm run lint` - Run ESLint
99
+ - `npm run preview` - Preview production build
100
+
101
+ ## 🔗 Related Documentation
102
+
103
+ See parent directory for comprehensive planning docs:
104
+ - [Refactoring Plan](../DESIGNER_REFACTOR_PLAN.md)
105
+ - [URL Structure](../URL_STRUCTURE.md)
106
+ - [Quick Start Guide](../QUICK_START_GUIDE.md)
107
+
108
+ ## 📄 License
109
+
110
+ MIT
@@ -769,6 +769,24 @@ actions:
769
769
  const iframe_url = `/api/workflow/designer?url=${url}&title=${title}`;
770
770
  return Steedos.openWindow(Steedos.absoluteUrl(iframe_url));
771
771
  }
772
+ designFlowV2:
773
+ label: æĩįĻ‹čŪūčŪĄå™ĻV2(BETA)
774
+ visible: !<tag:yaml.org,2002:js/function> |-
775
+ function (object_name, record_id, record_permissions) {
776
+ return true;
777
+ }
778
+ 'on': record_more
779
+ todo: !<tag:yaml.org,2002:js/function> |-
780
+ function (object_name, record_id, record_permissions, data) {
781
+ const flow = data && data.record;
782
+ const space = Builder.settings.context.user.spaceId;
783
+ const companyId = Builder.settings.context.user.company_id;
784
+ let url = `/api/workflow/designer-v2?flowId=${flow._id}`;
785
+ if (companyId && !Builder.settings.context.user.is_space_admin) {
786
+ url = url + `&companyId=${companyId}`;
787
+ }
788
+ return Steedos.openWindow(url);
789
+ }
772
790
  enableFlow:
773
791
  label: Enable Flow
774
792
  visible: !<tag:yaml.org,2002:js/function> |-
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "type": "liquid",
3
- "template": "<style>\n @keyframes fadeUpSpring {\n 0% { opacity: 0; transform: translateY(10px); }\n 100% { opacity: 1; transform: translateY(0); }\n }\n \n /* Make scrollbars standardized and visible */\n ::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n }\n ::-webkit-scrollbar-track {\n background: transparent;\n }\n ::-webkit-scrollbar-thumb {\n background-color: rgba(0, 0, 0, 0.25); /* Darker for visibility on gray bg */\n border-radius: 4px;\n border: 2px solid transparent; /* Creates padding effect */\n background-clip: content-box;\n }\n ::-webkit-scrollbar-thumb:hover {\n background-color: rgba(0, 0, 0, 0.4);\n }\n\n /* \n Fix outer modal scrollbar - SAFER VERSION \n Only apply these aggressive overrides (no padding, hidden overflow)\n to the specific modal that contains our component (identified by #steedosFlowSelectorSidebarList).\n This prevents breaking other stacked modals like 'Confirm Dialogs'.\n */\n .antd-Modal-body:has(#steedosFlowSelectorSidebarList) {\n overflow: hidden !important;\n padding: 0 !important; /* Optional: maximize space */\n display: flex;\n flex-direction: column;\n }\n\n /* Ensure the AMIS container fills height if needed */\n .antd-Service, .liquid-amis-container {\n height: 100%;\n }\n</style>\n\n<!-- Main Container: Fixed Height 70vh. -->\n<div class=\"flex h-[70vh] max-h-[800px] w-full overflow-hidden font-sans text-gray-900 bg-white\" style=\"min-height: 0;\">\n\n <!-- Left Sidebar -->\n <!-- flex-col, h-full, overflow-hidden -->\n <div class=\"flex flex-col w-[260px] h-full border-r border-gray-200 bg-[#F2F2F7] shrink-0 overflow-hidden\">\n <!-- Header -->\n <div class=\"shrink-0 pt-4 pb-2 px-3\">\n <div class=\"px-2 mb-3 text-2xl font-bold tracking-tight text-black\">æĩįĻ‹</div>\n <div class=\"relative group\">\n <div class=\"pointer-events-none absolute inset-y-0 left-0 flex items-center pl-2.5 text-gray-500\">\n <svg class=\"h-4 w-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"></circle>\n <line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"></line>\n </svg>\n </div>\n <input type=\"text\" id=\"searchInput\" placeholder=\"搜įīĒ\" class=\"w-full rounded-[10px] border-none bg-[#767680]/10 py-1.5 pl-9 pr-3 text-[14px] text-gray-900 placeholder:text-gray-500 outline-none transition-all duration-200 focus:bg-white focus:shadow-sm focus:ring-2 focus:ring-blue-500/20\">\n </div>\n </div>\n \n <!-- List Container -->\n <!-- min-h-0 is CRITICAL for flex child scrolling -->\n <div class=\"flex-1 min-h-0 overflow-y-auto px-2 pb-4 space-y-0.5 scroll-smooth\" id=\"steedosFlowSelectorSidebarList\">\n </div>\n </div>\n\n <!-- Right Content -->\n <!-- flex-1 fills remaining width -->\n <div class=\"flex-1 h-full relative bg-white overflow-hidden\">\n <!-- Absolute inset-0 locks the scroll container size -->\n <div id=\"mainContentScroll\" class=\"absolute inset-0 overflow-y-auto scroll-smooth p-6\">\n <div id=\"contentContainer\" class=\"w-full h-auto min-h-full\">\n <div class=\"flex h-full w-full flex-col items-center justify-center pt-20\">\n <div class=\"inline-flex items-center gap-2 text-gray-400 text-sm animate-pulse\">\n <svg class=\"animate-spin h-4 w-4\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle>\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\"></path>\n </svg>\n <span>æ­ĢåœĻåŠ č――čĩ„暐...</span>\n </div>\n </div>\n </div>\n </div>\n </div>\n</div>\n\n<script>\n const WorkflowService = {\n apiBase: \"\", \n getHeaders: function() { return { 'Content-Type': 'application/json' }; },\n getData: async function() {\n try {\n const appId = (typeof data !== 'undefined' && data.context && data.context.app_id) ? data.context.app_id : \"\";\n const url = this.apiBase + \"/service/api/flows/getList?action=new&appId=\" + encodeURIComponent(appId);\n const res = await fetch(url, { headers: this.getHeaders() });\n const treeData = await res.json();\n const categories = [];\n const parsedFlows = [];\n if (Array.isArray(treeData)) {\n treeData.forEach(cat => {\n categories.push({ _id: cat._id, name: cat.name });\n if (Array.isArray(cat.flows)) {\n cat.flows.forEach(f => {\n parsedFlows.push({\n id: f._id, name: f.name, categoryId: cat._id, categoryName: cat.name || \"å…ķäŧ–æĩįĻ‹\" \n });\n });\n }\n });\n }\n return { categories: categories, flows: parsedFlows };\n } catch (e) { \n console.error(\"WorkflowService Error:\", e);\n return { categories: [], flows: [] }; \n }\n },\n getFavorites: function() {\n const saved = localStorage.getItem('steedos_fav_ids');\n return saved ? JSON.parse(saved) : [];\n },\n toggleFavorite: function(flowId, isFav) {\n let favs = this.getFavorites();\n if (isFav) { if (!favs.includes(flowId)) favs.push(flowId); } \n else { favs = favs.filter(id => id !== flowId); }\n localStorage.setItem('steedos_fav_ids', JSON.stringify(favs));\n return favs;\n }\n };\n\n const AppState = { allFlows: [], categories: [], favorites: [] };\n const sidebarEl = document.getElementById('steedosFlowSelectorSidebarList');\n const contentEl = document.getElementById('contentContainer');\n const searchInput = document.getElementById('searchInput');\n\n async function init() {\n try {\n const data = await WorkflowService.getData();\n AppState.allFlows = data.flows;\n AppState.categories = data.categories;\n AppState.favorites = WorkflowService.getFavorites();\n renderUI();\n } catch (e) {\n contentEl.innerHTML = `<div class=\"text-gray-400 text-sm\">åŠ č――åĪąčīĨčŊ·æĢ€æŸĨį―‘įŧœ</div>`;\n }\n }\n\n function renderUI(filterText = \"\") {\n sidebarEl.innerHTML = \"\";\n contentEl.innerHTML = \"\";\n\n const isSearching = filterText.length > 0;\n let groups = [];\n\n const favFlows = AppState.allFlows.filter(f => \n AppState.favorites.includes(f.id) && \n (isSearching ? f.name.includes(filterText) : true)\n );\n if (favFlows.length > 0) {\n groups.push({ id: 'fav', name: \"æˆ‘įš„æ”ķ藏\", items: favFlows, isFav: true });\n }\n\n AppState.categories.forEach(cat => {\n const items = AppState.allFlows.filter(f => \n f.categoryId === cat._id &&\n (isSearching ? f.name.includes(filterText) : true)\n );\n if (items.length > 0) {\n groups.push({ id: cat._id, name: cat.name, items: items, isFav: false });\n }\n });\n\n const otherItems = AppState.allFlows.filter(f => \n !AppState.categories.find(c => c._id === f.categoryId) &&\n (isSearching ? f.name.includes(filterText) : true)\n );\n if (otherItems.length > 0) {\n groups.push({ id: 'other', name: \"å…ķäŧ–æĩįĻ‹\", items: otherItems, isFav: false });\n }\n\n if (groups.length === 0) {\n contentEl.innerHTML = `<div class=\"animate-[fadeUpSpring_0.5s_ease-out] text-center pt-20\"><div class=\"text-gray-200 text-7xl mb-4\">∅</div><div class=\"text-gray-400 text-sm\">朊æ‰ū到åŒđ配æĩįĻ‹</div></div>`;\n return;\n }\n\n groups.forEach((group, index) => {\n const groupId = `group-\\${group.id}`;\n const navItem = document.createElement('div');\n let navBase = \"group flex cursor-pointer items-center justify-between rounded-md px-3 py-2 text-[14px] transition-all duration-200 ease-out select-none\";\n let activeClass = \"bg-[#007AFF] text-white shadow-sm font-medium\";\n let inactiveClass = \"text-gray-700 hover:bg-black/5 active:bg-black/10\";\n \n navItem.className = `\\${navBase} \\${index === 0 ? activeClass : inactiveClass}`;\n const badgeClass = index === 0 ? \"text-white/80\" : \"text-gray-400 group-hover:text-gray-500\";\n \n navItem.innerHTML = `<span class=\"truncate\">\\${group.isFav ? '★ ' : ''}\\${group.name}</span><span class=\"\\${badgeClass} text-[12px] font-medium transition-colors\">\\${group.items.length}</span>`;\n \n navItem.onclick = () => {\n Array.from(sidebarEl.children).forEach(el => {\n el.className = `\\${navBase} \\${inactiveClass}`;\n el.querySelector('span:last-child').className = \"text-gray-400 group-hover:text-gray-500 text-[12px] font-medium transition-colors\";\n });\n navItem.className = `\\${navBase} \\${activeClass}`;\n navItem.querySelector('span:last-child').className = \"text-white/80 text-[12px] font-medium transition-colors\";\n \n const target = document.getElementById(groupId);\n const container = document.getElementById('mainContentScroll');\n if(target && container) {\n const targetTop = target.getBoundingClientRect().top; \n const containerTop = container.getBoundingClientRect().top; \n container.scrollTo({ top: container.scrollTop + targetTop - containerTop - 16, behavior: 'smooth' });\n }\n };\n sidebarEl.appendChild(navItem);\n\n const section = document.createElement('div');\n section.id = groupId;\n section.className = \"mb-10\";\n const headerColor = group.isFav ? 'text-amber-500' : 'text-gray-900';\n section.innerHTML = `<div class=\"sticky top-0 z-20 mb-4 bg-white/95 pb-2 text-xl font-bold tracking-tight backdrop-blur-xl text-left border-b border-gray-100 \\${headerColor}\">\\${group.name}</div>`;\n\n const grid = document.createElement('div');\n grid.className = 'grid grid-cols-[repeat(auto-fill,minmax(260px,1fr))] gap-4';\n\n group.items.forEach((flow, i) => {\n const isFav = AppState.favorites.includes(flow.id);\n const colorMap = ['bg-blue-50 text-blue-600', 'bg-orange-50 text-orange-600', 'bg-emerald-50 text-emerald-600', 'bg-indigo-50 text-indigo-600'];\n const colorClass = colorMap[(flow.name.length + i) % 4];\n const firstChar = flow.name.replace(/【.*?】/g, '').charAt(0) || flow.name.charAt(0);\n const card = document.createElement('div');\n card.className = 'group relative flex h-auto min-h-[72px] cursor-pointer items-center rounded-2xl border border-gray-100 bg-white p-3 text-left shadow-[0_2px_8px_rgba(0,0,0,0.04)] ring-1 ring-black/[0.02] transition-all duration-300 ease-out animate-[fadeUpSpring_0.6s_cubic-bezier(0.16,1,0.3,1)_forwards] hover:-translate-y-1 hover:border-gray-200 hover:shadow-[0_12px_24px_rgba(0,0,0,0.08)] active:scale-[0.98] active:bg-gray-50';\n card.style.animationDelay = `\\${Math.min(i * 0.04, 0.6)}s`;\n card.style.opacity = '0';\n const iconClass = isFav ? 'text-yellow-400 fill-current' : 'text-gray-300 group-hover/btn:text-gray-400 fill-none stroke-current stroke-[1.5]';\n const btnBgClass = isFav ? 'opacity-100 hover:scale-110' : 'opacity-0 group-hover:opacity-100 hover:bg-gray-100 hover:scale-110';\n\n card.innerHTML = `\n <div class=\"star-btn group/btn absolute right-2 top-1/2 -translate-y-1/2 z-20 flex h-8 w-8 items-center justify-center rounded-full transition-all duration-200 \\${btnBgClass}\" title=\"\\${isFav ? '取æķˆæ”ķ藏' : '加å…Ĩæ”ķ藏'}\">\n <svg class=\"h-5 w-5 transition-colors duration-300 \\${iconClass}\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z\" />\n </svg>\n </div>\n <div class=\"mr-4 flex h-11 w-11 shrink-0 items-center justify-center rounded-xl text-[16px] font-bold \\${colorClass}\">\\${firstChar}</div>\n <div class=\"flex-1 pr-8 text-[15px] font-medium text-gray-900 line-clamp-3 leading-relaxed tracking-tight\" title=\"\\${flow.name}\">\\${flow.name}</div>\n `;\n card.onclick = () => {\n setTimeout(() => {\n data._scoped.doAction([\n { \"actionType\": \"broadcast\", \"args\": { \"eventName\": \"flows.selected\" }, \"data\": { \"value\": flow.id } }\n ])\n }, 50);\n };\n const starBtn = card.querySelector('.star-btn');\n const starIcon = starBtn.querySelector('svg');\n starBtn.onclick = (e) => {\n e.stopPropagation();\n const newFavState = !starBtn.classList.contains('active-fav');\n if (newFavState) {\n starBtn.classList.add('active-fav', 'opacity-100');\n starBtn.classList.add('animate-[starPop_0.4s_ease-out]');\n starIcon.setAttribute('class', 'h-5 w-5 transition-colors duration-300 text-yellow-400 fill-current');\n } else {\n starBtn.classList.remove('active-fav', 'opacity-100');\n starBtn.classList.remove('animate-[starPop_0.4s_ease-out]');\n starIcon.setAttribute('class', 'h-5 w-5 transition-colors duration-300 text-gray-300 group-hover/btn:text-gray-400 fill-none stroke-current stroke-[1.5]');\n }\n AppState.favorites = WorkflowService.toggleFavorite(flow.id, newFavState);\n setTimeout(() => renderUI(searchInput.value), 300);\n };\n if (isFav) starBtn.classList.add('active-fav');\n grid.appendChild(card);\n });\n section.appendChild(grid);\n contentEl.appendChild(section);\n });\n }\n\n searchInput.addEventListener('input', (e) => renderUI(e.target.value.trim()));\n init();\n</script>\n",
3
+ "template": "<style>\n @keyframes fadeUpSpring {\n 0% { opacity: 0; transform: translateY(10px); }\n 100% { opacity: 1; transform: translateY(0); }\n }\n \n /* Make scrollbars standardized and visible */\n ::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n }\n ::-webkit-scrollbar-track {\n background: transparent;\n }\n ::-webkit-scrollbar-thumb {\n background-color: rgba(0, 0, 0, 0.25); /* Darker for visibility on gray bg */\n border-radius: 4px;\n border: 2px solid transparent; /* Creates padding effect */\n background-clip: content-box;\n }\n ::-webkit-scrollbar-thumb:hover {\n background-color: rgba(0, 0, 0, 0.4);\n }\n\n /* \n Fix outer modal scrollbar - SAFER VERSION \n Only apply these aggressive overrides (no padding, hidden overflow)\n to the specific modal that contains our component (identified by #steedosFlowSelectorSidebarList).\n This prevents breaking other stacked modals like 'Confirm Dialogs'.\n */\n .antd-Modal-body:has(#steedosFlowSelectorSidebarList) {\n overflow: hidden !important;\n padding: 0 !important; /* Optional: maximize space */\n display: flex;\n flex-direction: column;\n }\n\n /* Ensure the AMIS container fills height if needed */\n .antd-Service, .liquid-amis-container {\n height: 100%;\n }\n</style>\n\n<!-- Main Container: Fixed Height 70vh. -->\n<div class=\"flex h-[70vh] max-h-[800px] w-full overflow-hidden font-sans text-gray-900 bg-white\" style=\"min-height: 0;\">\n\n <!-- Left Sidebar -->\n <!-- flex-col, h-full, overflow-hidden -->\n <div class=\"flex flex-col w-[260px] h-full border-r border-gray-200 bg-[#F2F2F7] shrink-0 overflow-hidden\">\n <!-- Header -->\n <div class=\"shrink-0 pt-4 pb-2 px-3\">\n <div class=\"relative group\">\n <div class=\"pointer-events-none absolute inset-y-0 left-0 flex items-center pl-2.5 text-gray-500\">\n <svg class=\"h-4 w-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"></circle>\n <line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"></line>\n </svg>\n </div>\n <input type=\"text\" id=\"searchInput\" placeholder=\"搜įīĒæĩįĻ‹åį§°...\" class=\"w-full rounded-[10px] border-none bg-[#767680]/10 py-1.5 pl-9 pr-3 text-[14px] text-gray-900 placeholder:text-gray-500 outline-none transition-all duration-200 focus:bg-white focus:shadow-sm focus:ring-2 focus:ring-blue-500/20\">\n </div>\n </div>\n \n <!-- List Container -->\n <!-- min-h-0 is CRITICAL for flex child scrolling -->\n <div class=\"flex-1 min-h-0 overflow-y-auto px-2 pb-4 space-y-0.5 scroll-smooth\" id=\"steedosFlowSelectorSidebarList\">\n </div>\n </div>\n\n <!-- Right Content -->\n <!-- flex-1 fills remaining width -->\n <div class=\"flex-1 h-full relative bg-white overflow-hidden\">\n <!-- Absolute inset-0 locks the scroll container size -->\n <div id=\"mainContentScroll\" class=\"absolute inset-0 overflow-y-auto scroll-smooth p-6\">\n <div id=\"contentContainer\" class=\"w-full h-auto min-h-full\">\n <div class=\"flex h-full w-full flex-col items-center justify-center pt-20\">\n <div class=\"inline-flex items-center gap-2 text-gray-400 text-sm animate-pulse\">\n <svg class=\"animate-spin h-4 w-4\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle>\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\"></path>\n </svg>\n <span>æ­ĢåœĻåŠ č――čĩ„暐...</span>\n </div>\n </div>\n </div>\n </div>\n </div>\n</div>\n\n<script>\n const WorkflowService = {\n apiBase: \"\", \n getHeaders: function() { return { 'Content-Type': 'application/json' }; },\n getData: async function() {\n try {\n const appId = (typeof data !== 'undefined' && data.context && data.context.app_id) ? data.context.app_id : \"\";\n const url = this.apiBase + \"/service/api/flows/getList?action=new&appId=\" + encodeURIComponent(appId);\n const res = await fetch(url, { headers: this.getHeaders() });\n const treeData = await res.json();\n const categories = [];\n const parsedFlows = [];\n if (Array.isArray(treeData)) {\n treeData.forEach(cat => {\n categories.push({ _id: cat._id, name: cat.name });\n if (Array.isArray(cat.flows)) {\n cat.flows.forEach(f => {\n parsedFlows.push({\n id: f._id, name: f.name, categoryId: cat._id, categoryName: cat.name || \"å…ķäŧ–æĩįĻ‹\" \n });\n });\n }\n });\n }\n return { categories: categories, flows: parsedFlows };\n } catch (e) { \n console.error(\"WorkflowService Error:\", e);\n return { categories: [], flows: [] }; \n }\n },\n getFavorites: function() {\n const saved = localStorage.getItem('steedos_fav_ids');\n return saved ? JSON.parse(saved) : [];\n },\n toggleFavorite: function(flowId, isFav) {\n let favs = this.getFavorites();\n if (isFav) { if (!favs.includes(flowId)) favs.push(flowId); } \n else { favs = favs.filter(id => id !== flowId); }\n localStorage.setItem('steedos_fav_ids', JSON.stringify(favs));\n return favs;\n }\n };\n\n const AppState = { allFlows: [], categories: [], favorites: [] };\n const sidebarEl = document.getElementById('steedosFlowSelectorSidebarList');\n const contentEl = document.getElementById('contentContainer');\n const searchInput = document.getElementById('searchInput');\n\n async function init() {\n try {\n const data = await WorkflowService.getData();\n AppState.allFlows = data.flows;\n AppState.categories = data.categories;\n AppState.favorites = WorkflowService.getFavorites();\n renderUI();\n } catch (e) {\n contentEl.innerHTML = `<div class=\"text-gray-400 text-sm\">åŠ č――åĪąčīĨčŊ·æĢ€æŸĨį―‘įŧœ</div>`;\n }\n }\n\n function renderUI(filterText = \"\") {\n sidebarEl.innerHTML = \"\";\n contentEl.innerHTML = \"\";\n\n const isSearching = filterText.length > 0;\n let groups = [];\n\n const favFlows = AppState.allFlows.filter(f => \n AppState.favorites.includes(f.id) && \n (isSearching ? f.name.includes(filterText) : true)\n );\n if (favFlows.length > 0) {\n groups.push({ id: 'fav', name: \"æˆ‘įš„æ”ķ藏\", items: favFlows, isFav: true });\n }\n\n AppState.categories.forEach(cat => {\n const items = AppState.allFlows.filter(f => \n f.categoryId === cat._id &&\n (isSearching ? f.name.includes(filterText) : true)\n );\n if (items.length > 0) {\n groups.push({ id: cat._id, name: cat.name, items: items, isFav: false });\n }\n });\n\n const otherItems = AppState.allFlows.filter(f => \n !AppState.categories.find(c => c._id === f.categoryId) &&\n (isSearching ? f.name.includes(filterText) : true)\n );\n if (otherItems.length > 0) {\n groups.push({ id: 'other', name: \"å…ķäŧ–æĩįĻ‹\", items: otherItems, isFav: false });\n }\n\n if (groups.length === 0) {\n contentEl.innerHTML = `<div class=\"animate-[fadeUpSpring_0.5s_ease-out] text-center pt-20\"><div class=\"text-gray-200 text-7xl mb-4\">∅</div><div class=\"text-gray-400 text-sm\">朊æ‰ū到åŒđ配æĩįĻ‹</div></div>`;\n return;\n }\n\n const contentFragment = document.createDocumentFragment();\n groups.forEach((group, index) => {\n const groupId = `group-\\${group.id}`;\n const navItem = document.createElement('div');\n let navBase = \"group flex cursor-pointer items-center justify-between rounded-md px-3 py-2 text-[14px] transition-all duration-200 ease-out select-none\";\n let activeClass = \"bg-[#007AFF] text-white shadow-sm font-medium\";\n let inactiveClass = \"text-gray-700 hover:bg-black/5 active:bg-black/10\";\n \n navItem.className = `\\${navBase} \\${index === 0 ? activeClass : inactiveClass}`;\n const badgeClass = index === 0 ? \"text-white/80\" : \"text-gray-400 group-hover:text-gray-500\";\n \n navItem.innerHTML = `<span class=\"truncate\">\\${group.isFav ? '★ ' : ''}\\${group.name}</span><span class=\"\\${badgeClass} text-[12px] font-medium transition-colors\">\\${group.items.length}</span>`;\n \n navItem.onclick = () => {\n Array.from(sidebarEl.children).forEach(el => {\n el.className = `\\${navBase} \\${inactiveClass}`;\n el.querySelector('span:last-child').className = \"text-gray-400 group-hover:text-gray-500 text-[12px] font-medium transition-colors\";\n });\n navItem.className = `\\${navBase} \\${activeClass}`;\n navItem.querySelector('span:last-child').className = \"text-white/80 text-[12px] font-medium transition-colors\";\n \n const target = document.getElementById(groupId);\n const container = document.getElementById('mainContentScroll');\n if(target && container) {\n const targetTop = target.getBoundingClientRect().top; \n const containerTop = container.getBoundingClientRect().top; \n container.scrollTo({ top: container.scrollTop + targetTop - containerTop - 16, behavior: 'smooth' });\n }\n };\n sidebarEl.appendChild(navItem);\n\n const section = document.createElement('div');\n section.id = groupId;\n section.className = \"mb-10\";\n const headerColor = group.isFav ? 'text-amber-500' : 'text-gray-900';\n section.innerHTML = `<div class=\"sticky top-0 z-20 mb-4 bg-white pb-2 text-xl font-bold tracking-tight text-left border-b border-gray-100 \\${headerColor}\">\\${group.name}</div>`;\n\n const grid = document.createElement('div');\n grid.className = 'grid grid-cols-[repeat(auto-fill,minmax(260px,1fr))] gap-4';\n\n const gridFragment = document.createDocumentFragment();\n group.items.forEach((flow, i) => {\n const isFav = AppState.favorites.includes(flow.id);\n const colorMap = ['bg-blue-50 text-blue-600', 'bg-orange-50 text-orange-600', 'bg-emerald-50 text-emerald-600', 'bg-indigo-50 text-indigo-600'];\n const colorClass = colorMap[(flow.name.length + i) % 4];\n const firstChar = flow.name.replace(/【.*?】/g, '').charAt(0) || flow.name.charAt(0);\n const card = document.createElement('div');\n card.className = 'group relative flex h-auto min-h-[72px] cursor-pointer items-center rounded-2xl border border-gray-100 bg-white p-3 text-left shadow-[0_2px_8px_rgba(0,0,0,0.04)] ring-1 ring-black/[0.02] transition-[transform,box-shadow] duration-200 ease-out hover:-translate-y-1 hover:border-gray-200 hover:shadow-[0_12px_24px_rgba(0,0,0,0.08)] active:scale-[0.98] active:bg-gray-50' + (i < 12 ? ' animate-[fadeUpSpring_0.4s_cubic-bezier(0.16,1,0.3,1)_forwards]' : '');\n if (i < 12) { card.style.animationDelay = (i * 0.03) + 's'; card.style.opacity = '0'; }\n const iconClass = isFav ? 'text-yellow-400 fill-current' : 'text-gray-300 group-hover/btn:text-gray-400 fill-none stroke-current stroke-[1.5]';\n const btnBgClass = isFav ? 'opacity-100 hover:scale-110' : 'opacity-0 group-hover:opacity-100 hover:bg-gray-100 hover:scale-110';\n\n card.innerHTML = `\n <div class=\"star-btn group/btn absolute right-2 top-1/2 -translate-y-1/2 z-20 flex h-8 w-8 items-center justify-center rounded-full transition-all duration-200 \\${btnBgClass}\" title=\"\\${isFav ? '取æķˆæ”ķ藏' : '加å…Ĩæ”ķ藏'}\">\n <svg class=\"h-5 w-5 transition-colors duration-300 \\${iconClass}\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z\" />\n </svg>\n </div>\n <div class=\"mr-4 flex h-11 w-11 shrink-0 items-center justify-center rounded-xl text-[16px] font-bold \\${colorClass}\">\\${firstChar}</div>\n <div class=\"flex-1 pr-8 text-[15px] font-medium text-gray-900 line-clamp-3 leading-relaxed tracking-tight\" title=\"\\${flow.name}\">\\${flow.name}</div>\n `;\n card.onclick = () => {\n if (card.dataset.loading === 'true') return;\n card.dataset.loading = 'true';\n card.style.pointerEvents = 'none';\n card.style.opacity = '0.6';\n card.innerHTML = '<div class=\"flex items-center justify-center w-full gap-2 text-gray-400 text-sm\"><svg class=\"animate-spin h-4 w-4\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle><path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\"></path></svg><span>æ­ĢåœĻ创åŧš...</span></div>';\n setTimeout(() => {\n data._scoped.doAction([\n { \"actionType\": \"broadcast\", \"args\": { \"eventName\": \"flows.selected\" }, \"data\": { \"value\": flow.id } }\n ])\n }, 50);\n };\n const starBtn = card.querySelector('.star-btn');\n const starIcon = starBtn.querySelector('svg');\n starBtn.onclick = (e) => {\n e.stopPropagation();\n const newFavState = !starBtn.classList.contains('active-fav');\n if (newFavState) {\n starBtn.classList.add('active-fav', 'opacity-100');\n starBtn.classList.add('animate-[starPop_0.4s_ease-out]');\n starIcon.setAttribute('class', 'h-5 w-5 transition-colors duration-300 text-yellow-400 fill-current');\n } else {\n starBtn.classList.remove('active-fav', 'opacity-100');\n starBtn.classList.remove('animate-[starPop_0.4s_ease-out]');\n starIcon.setAttribute('class', 'h-5 w-5 transition-colors duration-300 text-gray-300 group-hover/btn:text-gray-400 fill-none stroke-current stroke-[1.5]');\n }\n AppState.favorites = WorkflowService.toggleFavorite(flow.id, newFavState);\n setTimeout(() => renderUI(searchInput.value), 300);\n };\n if (isFav) starBtn.classList.add('active-fav');\n gridFragment.appendChild(card);\n });\n grid.appendChild(gridFragment);\n section.appendChild(grid);\n contentFragment.appendChild(section);\n });\n contentEl.appendChild(contentFragment);\n }\n\n let _searchTimer = null;\n searchInput.addEventListener('input', (e) => {\n clearTimeout(_searchTimer);\n _searchTimer = setTimeout(() => renderUI(e.target.value.trim()), 300);\n });\n init();\n</script>\n",
4
4
  "className": "h-full"
5
5
  }
@@ -81,6 +81,34 @@ router.get('/am/designer/startup', async function (req, res) {
81
81
  }
82
82
  })
83
83
 
84
+ // startup v2 - lightweight: only return the specified flow and its form
85
+ router.get('/am/designer/startup/v2', async function (req, res) {
86
+ try {
87
+ let flowId = req.query.flowId;
88
+ if (!flowId) {
89
+ return res.status(400).send({ status: 'error', message: 'flowId is required' });
90
+ }
91
+
92
+ let flowCollection = await getCollection('flows');
93
+ let formCollection = await getCollection('forms');
94
+
95
+ let flow = await flowCollection.findOne({ _id: flowId }, { projection: { historys: 0 } });
96
+ if (!flow) {
97
+ return res.status(404).send({ status: 'error', message: 'Flow not found' });
98
+ }
99
+
100
+ let form = await formCollection.findOne({ _id: flow.form }, { projection: { historys: 0 } });
101
+
102
+ res.send({
103
+ flow,
104
+ form,
105
+ sync_token: new Date().getTime() / 1000
106
+ });
107
+ } catch (error) {
108
+ res.status(500).send(error.message);
109
+ }
110
+ })
111
+
84
112
  // čĄĻ单
85
113
  router.post('/am/forms', async function (req, res) {
86
114
  try {
@@ -0,0 +1,184 @@
1
+ const express = require("express");
2
+ const router = express.Router();
3
+ const steedosAuth = require('@steedos/auth');
4
+ const axios = require('axios');
5
+ const bodyParser = require('body-parser');
6
+
7
+ router.use(bodyParser.json({ limit: '2mb' }));
8
+
9
+ /**
10
+ * AI Workflow Designer endpoint
11
+ *
12
+ * Accepts natural language instructions and current workflow steps,
13
+ * calls an OpenAI-compatible LLM API to generate modified steps.
14
+ *
15
+ * Environment variables:
16
+ * WORKFLOW_AI_API_KEY - API key for the LLM provider
17
+ * WORKFLOW_AI_BASE_URL - Base URL (default: https://api.openai.com/v1)
18
+ * WORKFLOW_AI_MODEL - Model name (default: gpt-4o)
19
+ */
20
+ router.post('/am/ai/design', async function auth(req, res, next) {
21
+ try {
22
+ const result = await steedosAuth.auth(req, res);
23
+ if (result && result.userId) {
24
+ req.user = result;
25
+ next();
26
+ } else {
27
+ res.status(401).json({ error: 'čŊ·å…ˆį™ŧå―•' });
28
+ }
29
+ } catch (e) {
30
+ res.status(401).json({ error: 'čŪĪčŊåĪąčīĨ' });
31
+ }
32
+ }, async function (req, res) {
33
+ try {
34
+ const apiKey = process.env.WORKFLOW_AI_API_KEY;
35
+ if (!apiKey) {
36
+ return res.status(500).json({ error: 'čŊ·å…ˆé…į―ŪįŽŊåĒƒå˜é‡ WORKFLOW_AI_API_KEY' });
37
+ }
38
+
39
+ const baseURL = process.env.WORKFLOW_AI_BASE_URL || 'https://api.openai.com/v1';
40
+ const model = process.env.WORKFLOW_AI_MODEL || 'gpt-4o';
41
+
42
+ const { steps, formFields, userRequest } = req.body;
43
+
44
+ if (!userRequest) {
45
+ return res.status(400).json({ error: 'čŊ·čū“å…Ĩéœ€æą‚æčŋ°' });
46
+ }
47
+
48
+ // Build form fields description
49
+ const fieldsDesc = (formFields || [])
50
+ .filter(f => f.code)
51
+ .map(f => ` - ${f.name || f.code} (įž–į : ${f.code}, įąŧ型: ${f.type || 'text'})`)
52
+ .join('\n');
53
+
54
+ const systemPrompt = `ä― æ˜Ŋäļ€äļŠäļ“äļšįš„åŪĄæ‰đæĩįĻ‹čŪūčŪĄåļˆã€‚ä― įš„äŧŧåŠĄæ˜Ŋæ đæŪį”Ļæˆ·įš„臊į„ķčŊ­čĻ€æčŋ°ïžŒäŋŪæ”đåŪĄæ‰đæĩįĻ‹įš„æ­ĨéŠĪåۚäđ‰ïžˆJSON 栞垏。
55
+
56
+ ## æ­ĨéŠĪ数æŪįŧ“æž„
57
+
58
+ æŊäļŠæ­ĨéŠĪ(step)æ˜Ŋäļ€äļŠ JSON åŊđčąĄïžŒåŋ…éĄŧ包åŦäŧĨäļ‹å­—æŪĩïžš
59
+ - _id: 字įŽĶäļēå”Ŋä ‡čŊ†ã€‚å·ē有æ­ĨéŠĪäŋį•™åŽŸ _id新æ­ĨéŠĪä―ŋį”Ļ "step-" + 小写字æŊæ•°å­—随朚äļēåĶ‚ "step-a1b2c3d4"
60
+ - name: 字įŽĶäļēæ­ĨéŠĪåį§°
61
+ - step_type: 字įŽĶäļēįąŧåž‹ïžŒåŠčƒ―æ˜ŊäŧĨäļ‹äđ‹äļ€ïžš
62
+ - "start" — åž€å§‹čŠ‚į‚đæĩįĻ‹åŋ…éĄŧ有äļ”äŧ…有äļ€äļŠïž‰
63
+ - "end" — įŧ“æŸčŠ‚į‚đæĩįĻ‹åŋ…éĄŧ有äļ”äŧ…有äļ€äļŠïž‰
64
+ - "submit" — åĄŦ写æ­ĨéŠĪ
65
+ - "sign" — åŪĄæ‰đæ­ĨéŠĪ
66
+ - "counterSign" — 侚į­ūæ­ĨéŠĪåĪšäššåŒæ—ķåŪĄæ‰đ
67
+ - "condition" — æĄäŧķ分æ”Ŋ节į‚đ
68
+ - deal_type: 字įŽĶäļē, åĪ„į†äšščšŦäŧ―submit/sign/counterSign åŋ…åĄŦåŊ选倞
69
+ - "pickupAtRuntime" — åŪĄæ‰đæ—ķ指åۚ
70
+ - "specifyUser" — 指åŪšäššå‘˜
71
+ - "applicantSuperior" — į”ģčŊ·äššįš„äļŠįš§
72
+ - "applicant" — į”ģčŊ·äšš
73
+ - "hrRole" — 指åŪšč§’č‰ē
74
+ - "applicantRole" — 指åۚåŪĄæ‰đåē—ä―
75
+ - posx, posy: æ•°å­—ïžŒčŠ‚į‚đä―į―Ū。įšĩ向äŧŽäļŠåˆ°äŽ’åˆ—ïžŒåž€å§‹čŠ‚į‚đ posx=200,posy=40äđ‹åŽæŊæ­Ĩ y åĒžåŠ įšĶ120
76
+ - timeout_hours: 数字čķ…æ—ķ小æ—ķ数éŧ˜čŪĪ 168
77
+ - lines: 数įŧ„äŧŽå―“前æ­ĨéŠĪå‡šå‘įš„čŋžįšŋæŊæĄčŋžįšŋ包åŦïžš
78
+ - _id: 字įŽĶäļēčŋžįšŋå”Ŋä ‡čŊ†ïžŒį”Ļ "line-" + 随朚äļē
79
+ - name: 字įŽĶäļēčŋžįšŋåį§°ïžˆåŊįĐšïž‰
80
+ - to_step: 字įŽĶäļēį›Ū标æ­ĨéŠĪįš„ _id
81
+ - state: 字įŽĶäļē"submitted"提äšĪ/ "approved"æ ļ准/ "rejected"éĐģ回
82
+ - condition: 字įŽĶäļēïžŒæĄäŧķčĄĻčūū垏äŧ…æĄäŧķ节į‚đįš„å‡šįšŋ需čĶïž‰ïžŒæ žåžåĶ‚ {字æŪĩįž–į } > 10000
83
+
84
+ ## č§„åˆ™
85
+ 1. æĩįĻ‹åŋ…éĄŧ有äļ”äŧ…有äļ€äļŠ start 和äļ€äļŠ end 节į‚đ
86
+ 2. 所有æ­ĨéŠĪåŋ…éĄŧ通čŋ‡ lines 乒į›ļčŋžé€šïžŒäļčƒ―有å­ĪįŦ‹čŠ‚į‚đ
87
+ 3. äŧŽ start 垀始åŋ…éĄŧčƒ―æœ€įŧˆåˆ°čūū end
88
+ 4. åŪĄæ‰đæ­ĨéŠĪ(sign)通åļᅵ‰äļĪæĄå‡šįšŋïžšapprovedæ ļ准到äļ‹ä­ĨrejectedéĐģ回回到åĄŦ写或äļŠä­Ĩ
89
+ 5. æĄäŧķ节į‚đ(condition)有åĪšæĄå‡šįšŋæŊæĄåļĶäļåŒįš„ condition čĄĻčūū垏
90
+ 6. äļč́æ·ŧ加 notify、concurrentStart、concurrentEnd įąŧåž‹įš„æ­ĨéŠĪ
91
+ 7. äŋŪæ”đæ—ķå°―åŊčƒ―äŋį•™į”Ļ户å·ē有æ­ĨéŠĪįš„ _id 和čŪūį―Ū及æ”đ需č́æ”đįš„éƒĻ分
92
+ 8. čŋ”回åیæ•īįš„ steps 数įŧ„
93
+
94
+ ## čū“凚栞垏
95
+ į›īæŽĨčŋ”回 JSON 数įŧ„äļčĶæœ‰äŧŧä―•å…ķäŧ–文字、č§Ģ释、markdown 标čŪ°ã€‚`;
96
+
97
+ const userContent = `## å―“å‰čĄĻ单字æŪĩ
98
+ ${fieldsDesc || '无字æŪĩäŋĄæŊ'}
99
+
100
+ ## å―“å‰æĩįĻ‹æ­ĨéŠĪ
101
+ ${JSON.stringify(steps || [], null, 2)}
102
+
103
+ ## į”Ļæˆ·éœ€æą‚
104
+ ${userRequest}
105
+
106
+ čŊ·čŋ”回äŋŪæ”đåŽįš„åیæ•ī steps JSON 数įŧ„。`;
107
+
108
+ const response = await axios.post(`${baseURL}/chat/completions`, {
109
+ model,
110
+ messages: [
111
+ { role: 'system', content: systemPrompt },
112
+ { role: 'user', content: userContent },
113
+ ],
114
+ temperature: 0.3,
115
+ max_tokens: 8000,
116
+ }, {
117
+ headers: {
118
+ 'Authorization': `Bearer ${apiKey}`,
119
+ 'Content-Type': 'application/json',
120
+ },
121
+ timeout: 120000,
122
+ });
123
+
124
+ const content = response.data?.choices?.[0]?.message?.content;
125
+ if (!content) {
126
+ return res.status(500).json({ error: 'AI 朊čŋ”回有效内åŪđ' });
127
+ }
128
+
129
+ // Try to parse the JSON from the response
130
+ let stepsResult;
131
+ try {
132
+ let text = content.trim();
133
+ // Strip markdown code block if present
134
+ if (text.startsWith('```json')) text = text.slice(7);
135
+ else if (text.startsWith('```')) text = text.slice(3);
136
+ if (text.endsWith('```')) text = text.slice(0, -3);
137
+ text = text.trim();
138
+
139
+ // Find JSON array
140
+ const arrStart = text.indexOf('[');
141
+ const arrEnd = text.lastIndexOf(']');
142
+ if (arrStart !== -1 && arrEnd > arrStart) {
143
+ stepsResult = JSON.parse(text.substring(arrStart, arrEnd + 1));
144
+ } else {
145
+ stepsResult = JSON.parse(text);
146
+ }
147
+ } catch (parseErr) {
148
+ return res.status(500).json({
149
+ error: 'AI čŋ”回内åŪđæ— æģ•č§Ģ析äļš JSON',
150
+ raw: content,
151
+ });
152
+ }
153
+
154
+ if (!Array.isArray(stepsResult)) {
155
+ return res.status(500).json({ error: 'AI čŋ”å›žįš„äļæ˜Ŋæ­ĨéŠĪ数įŧ„' });
156
+ }
157
+
158
+ // Basic validation
159
+ const hasStart = stepsResult.some(s => s.step_type === 'start');
160
+ const hasEnd = stepsResult.some(s => s.step_type === 'end');
161
+ if (!hasStart || !hasEnd) {
162
+ return res.status(500).json({ error: 'AI čŋ”å›žįš„æĩįĻ‹įžšå°‘åž€å§‹æˆ–įŧ“æŸčŠ‚į‚đ' });
163
+ }
164
+
165
+ return res.json({ steps: stepsResult });
166
+
167
+ } catch (error) {
168
+ console.error('AI workflow design error:', error?.response?.data || error?.message || error);
169
+ if (error.response) {
170
+ const status = error.response.status;
171
+ const data = error.response.data;
172
+ if (status === 401 || status === 403) {
173
+ return res.status(500).json({ error: 'AI API åŊ†é’Ĩ无效或权限äļčķģčŊ·æĢ€æŸĨ WORKFLOW_AI_API_KEY' });
174
+ }
175
+ if (status === 429) {
176
+ return res.status(500).json({ error: 'AI API čŊ·æą‚éĒ‘įŽ‡čķ…限čŊ·įĻåŽé‡čŊ•' });
177
+ }
178
+ return res.status(500).json({ error: data?.error?.message || 'AI API 调į”ĻåĪąčīĨ' });
179
+ }
180
+ return res.status(500).json({ error: error.message || 'AI æœåŠĄč°ƒį”ĻåĪąčīĨ' });
181
+ }
182
+ });
183
+
184
+ exports.default = router;
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+ const express = require('express');
3
+ const router = express.Router();
4
+ const { requireAuthentication } = require("@steedos/auth");
5
+ const path = require('path');
6
+
7
+ /**
8
+ * New React-based workflow designer
9
+ * URL: /api/workflow/designer-v2
10
+ */
11
+ router.get('/api/workflow/designer-v2', requireAuthentication, async (req, res) => {
12
+ try {
13
+ // In development, proxy to Vite dev server
14
+ if (process.env.NODE_ENV === 'development') {
15
+ return res.redirect('http://localhost:5173');
16
+ }
17
+
18
+ // In production, serve built React app
19
+ const designerPath = path.join(__dirname, '../../../designer/dist/index.html');
20
+ res.sendFile(designerPath);
21
+ } catch (e) {
22
+ res.status(500).send({
23
+ errors: [{ errorMessage: e.message }]
24
+ });
25
+ }
26
+ });
27
+
28
+ /**
29
+ * New designer startup API
30
+ * URL: /api/designer-v2/startup
31
+ */
32
+ router.get('/api/designer-v2/startup', requireAuthentication, async (req, res) => {
33
+ try {
34
+ const { getObject } = require('@steedos/objectql');
35
+
36
+ // Get workspace data
37
+ const space = await getObject("spaces").findOne(req.user.spaceId);
38
+
39
+ // Return initialization data
40
+ res.json({
41
+ user: {
42
+ userId: req.user.userId,
43
+ name: req.user.name,
44
+ spaceId: req.user.spaceId
45
+ },
46
+ space: space,
47
+ message: 'New designer startup API - ready for implementation'
48
+ });
49
+ } catch (e) {
50
+ res.status(500).send({
51
+ errors: [{ errorMessage: e.message }]
52
+ });
53
+ }
54
+ });
55
+
56
+ /**
57
+ * Serve static assets for production (js, css, images, etc.)
58
+ */
59
+ const designerDistPath = path.join(__dirname, '../../../designer/dist');
60
+ router.use('/api/workflow/designer-v2', express.static(designerDistPath));
61
+
62
+ exports.default = router;
package/package.json CHANGED
@@ -1,8 +1,20 @@
1
1
  {
2
2
  "name": "@steedos-labs/plugin-workflow",
3
- "version": "3.0.26",
3
+ "version": "3.0.27",
4
4
  "main": "package.service.js",
5
5
  "license": "MIT",
6
+ "files": [
7
+ "main",
8
+ "src",
9
+ "designer/dist",
10
+ "public/applications",
11
+ "public/office",
12
+ "public/workflow",
13
+ "package.service.js",
14
+ "package.service.yml",
15
+ "LICENSE.txt",
16
+ "README.md"
17
+ ],
6
18
  "scripts": {
7
19
  "build:watch": "tsc --watch",
8
20
  "release": "npm publish --registry https://registry.npmjs.org && npx cnpm sync @steedos-labs/plugin-workflow",