@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.
- package/designer/README.md +110 -0
- package/main/default/objects/flows/flows.object.yml +18 -0
- package/main/default/pages/flow_selector.page.amis.json +1 -1
- package/main/default/routes/am.router.js +28 -0
- package/main/default/routes/api_workflow_ai_design.router.js +184 -0
- package/main/default/routes/designer-v2.router.js +62 -0
- package/package.json +13 -1
- package/AI_PLUGIN_GUIDE.md +0 -939
- package/APPROVAL_COMMENTS_OPERATIONS.md +0 -673
- package/APPROVAL_COMMENTS_UPGRADE.md +0 -854
- package/README_TEMPLATE.md +0 -222
- package/convert-templates.js +0 -51
- package/run.js +0 -388
|
@@ -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.
|
|
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",
|