@gadmin2n/schematics 0.0.104 → 0.0.106

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.
Files changed (21) hide show
  1. package/dist/lib/application/application.factory.js +0 -16
  2. package/dist/lib/application/files/gadmin2-game-angle-demo/readme.md +52 -2
  3. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/auth.guard.ts +1 -1
  4. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-dsl-validate.ts +7 -4
  5. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/App.tsx +4 -5
  6. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/agentPanel/AgentContext.tsx +4 -1
  7. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/env.ts +4 -0
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/dev-shell/DevShell.tsx +7 -8
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/helpers/login.ts +10 -0
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/plugins/devShellPlugin.ts +11 -3
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agenda/index.tsx +14 -11
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasCell.tsx +3 -2
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasListPage.tsx +39 -28
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasPage.tsx +5 -4
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasToolbar.tsx +208 -199
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/editor.tsx +1 -1
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/components/NodeInstanceForm.tsx +1 -1
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/show.tsx +1 -1
  19. package/dist/lib/application/files/gadmin2-game-angle-demo/web/vite.config.ts +1 -0
  20. package/package.json +1 -1
  21. package/dist/lib/application/files/gadmin2-game-angle-demo/gitignore +0 -52
@@ -16,7 +16,6 @@ function main(options) {
16
16
  if (options["template"] !== "customize") {
17
17
  return (0, schematics_1.branchAndMerge)((0, schematics_1.chain)([
18
18
  (0, schematics_1.mergeWith)(generateByTemplate(options, path)),
19
- renameDotfiles(path),
20
19
  ]))(tree, context);
21
20
  }
22
21
  if (options["with-web"]) {
@@ -61,21 +60,6 @@ function resolvePackageName(path) {
61
60
  }
62
61
  return baseFilename;
63
62
  }
64
- function renameDotfiles(targetPath) {
65
- return (tree) => {
66
- tree.visit((filePath) => {
67
- if (!filePath.startsWith('/' + targetPath + '/') && !filePath.startsWith(targetPath + '/'))
68
- return;
69
- if ((0, path_1.basename)(filePath) === 'gitignore') {
70
- const content = tree.read(filePath);
71
- if (content) {
72
- tree.create(filePath.replace(/\/gitignore$/, '/.gitignore'), content);
73
- tree.delete(filePath);
74
- }
75
- }
76
- });
77
- };
78
- }
79
63
  function generateByTemplate(options, path) {
80
64
  return (0, schematics_1.apply)((0, schematics_1.url)((0, core_1.join)('./files', options['template'])), [
81
65
  (0, schematics_1.template)(Object.assign(Object.assign({}, core_1.strings), options)),
@@ -215,6 +215,8 @@ $ ./sync-to-template.sh
215
215
 
216
216
  ## 登录流程
217
217
 
218
+ ### 非 woa.com 域名(本地开发 / 外部部署)
219
+
218
220
  ```
219
221
  ┌─────────────────────────────────────────────────────────────────┐
220
222
  │ 用户访问页面 │
@@ -273,13 +275,61 @@ $ ./sync-to-template.sh
273
275
  跳转后端 /login 重新认证
274
276
  ```
275
277
 
278
+ ### woa.com 域名(SmartGate 拦截)
279
+
280
+ woa.com 域名下,SmartGate 会在每个请求到达前注入 `staffid`/`staffname` 等认证 header,保证进入页面时用户已有登录态,无需跳转 Taihu 登录页。
281
+
282
+ ```
283
+ ┌─────────────────────────────────────────────────────────────────┐
284
+ │ 用户访问 *.woa.com 页面 │
285
+ │ SmartGate 已注入 staffid/staffname │
286
+ └──────────────────────────────┬──────────────────────────────────┘
287
+
288
+
289
+ authProvider.check()
290
+ checkLogin()
291
+
292
+ ┌───────────────┼─────────────────────┐
293
+ ▼ ▼ ▼
294
+ URL 有 Cookie 有 woa.com 域名
295
+ gadmin-token gadmin-token (isWoaDomain())
296
+ │ │ │
297
+ ▼ │ ▼
298
+ 写入 Cookie │ 直接返回 true
299
+ 返回 true │ (SmartGate 保证登录态)
300
+ │ │ │
301
+ └───────────────┴─────────────────────┘
302
+
303
+ ▼ authenticated: true
304
+ ┌──────────────────────┐
305
+ │ 进入页面 │
306
+ └──────────┬───────────┘
307
+
308
+
309
+ 发起 API 请求
310
+ 后端 AuthGuard 优先走 WOA 认证
311
+ (staffid/staffname header)
312
+
313
+ ┌──────────┴──────────┐
314
+ ▼ ▼
315
+ 200 OK 401 响应
316
+ │ (SmartGate 签名验证失败等异常情况)
317
+ ▼ │
318
+ 正常使用 ▼
319
+ login() 函数
320
+ 跳转后端 /login → Taihu 重新认证
321
+ (避免死循环,通过拿 JWT token 解决)
322
+ ```
323
+
276
324
  **关键节点说明:**
277
325
 
278
326
  | 位置 | 文件 | 作用 |
279
327
  |------|------|------|
280
- | `checkLogin()` | `web/src/helpers/login.ts` | 检查 URL 参数 / Cookie,将 URL token 存入 Cookie |
328
+ | `isWoaDomain()` | `web/src/helpers/login.ts` | 判断当前是否在 woa.com 域名下 |
329
+ | `checkLogin()` | `web/src/helpers/login.ts` | 检查 URL 参数 / Cookie / woa.com 域名,将 URL token 存入 Cookie |
281
330
  | `authProvider.check()` | `web/src/authProvider.ts` | Refine 框架调用,决定是否已认证 |
282
- | `login()` | `web/src/helpers/login.ts` | 跳转后端登录页,携带 `redirect_url` |
331
+ | `login()` | `web/src/helpers/login.ts` | 跳转后端登录页,携带 `redirect_url`(woa.com 下也走此流程) |
283
332
  | 后端 `/login` | NestJS | 走 WOA 或 Taihu OAuth,认证后带 token 跳回前端 |
333
+ | 后端 `AuthGuard` | `server/src/lib/auth.guard.ts` | 优先 WOA 认证,其次 Taihu JWT |
284
334
  | axios 拦截器 | `web/src/helpers/http.ts` | 捕获 API 401,触发重新登录 |
285
335
  | `requestHeaders()` | `web/src/helpers/login.ts` | 每次请求自动从 Cookie 读取 token 加入 header |
@@ -43,7 +43,7 @@ async function getIdentity(headers, cookies): Promise<UserType> {
43
43
  logger.info(
44
44
  `woa_auth_success - 用户ID: ${staffid}, 用户名: ${staffname}`,
45
45
  );
46
- return { userid: staffid, username: staffname };
46
+ return { userid: staffname, username: staffname };
47
47
  }
48
48
  } catch (e) {
49
49
  logger.warn(`woa_auth_failed - WOA认证失败: ${e.message}`);
@@ -68,8 +68,7 @@ export function validateDslHandles(
68
68
  if (!(src.type in outputsMap)) continue; // unknown node type — out of scope
69
69
 
70
70
  const declared = outputsMap[src.type];
71
- const handles =
72
- declared === null ? deriveDynamicOutputs(src) : declared;
71
+ const handles = declared === null ? deriveDynamicOutputs(src) : declared;
73
72
 
74
73
  // Empty named-handle list AND declared-non-null => no sourceHandle allowed
75
74
  if (declared !== null && handles.length === 0) {
@@ -94,7 +93,9 @@ export function validateDslHandles(
94
93
  source: edge.source,
95
94
  target: edge.target,
96
95
  code: 'MISSING_HANDLE',
97
- reason: `Node "${src.type}" requires a sourceHandle; choose one of: ${handles.join(', ')}.`,
96
+ reason: `Node "${
97
+ src.type
98
+ }" requires a sourceHandle; choose one of: ${handles.join(', ')}.`,
98
99
  });
99
100
  continue;
100
101
  }
@@ -105,7 +106,9 @@ export function validateDslHandles(
105
106
  source: edge.source,
106
107
  target: edge.target,
107
108
  code: 'UNKNOWN_HANDLE',
108
- reason: `Node "${src.type}" sourceHandle "${edge.sourceHandle}" is not in declared outputs: ${handles.join(', ')}.`,
109
+ reason: `Node "${src.type}" sourceHandle "${
110
+ edge.sourceHandle
111
+ }" is not in declared outputs: ${handles.join(', ')}.`,
109
112
  });
110
113
  }
111
114
  }
@@ -24,14 +24,13 @@ import { auditLogProvider } from 'auditLogProvider';
24
24
  import { gadminCrudProvider as dataProvider } from './helpers/http';
25
25
  import { useDynamicResources } from 'hooks/useDynamicResources';
26
26
  import { renderRoutes } from 'config/routeRegistry';
27
+ import { isAgentEnabled } from 'config/env';
27
28
 
28
- const isDev = import.meta.env.DEV;
29
-
30
- // Wrapper component that only renders AgentProvider in development
29
+ // Wrapper component that only renders AgentProvider when agent is enabled
31
30
  const DevAgentWrapper: React.FC<{ children: React.ReactNode }> = ({
32
31
  children,
33
32
  }) => {
34
- if (!isDev) return <>{children}</>;
33
+ if (!isAgentEnabled) return <>{children}</>;
35
34
  return (
36
35
  <AgentProvider>
37
36
  {children}
@@ -58,7 +57,7 @@ function App() {
58
57
  const resources = useDynamicResources();
59
58
 
60
59
  return (
61
- <BrowserRouter basename={isDev ? '/app' : '/'}>
60
+ <BrowserRouter basename={isAgentEnabled ? '/app' : '/'}>
62
61
  <RefineKbarProvider>
63
62
  <BusinessContextProvider>
64
63
  <ColorModeContextProvider>
@@ -31,7 +31,10 @@ export interface PageInfo {
31
31
  * In that case, open/close/sendPrompt are forwarded to the parent shell
32
32
  * via postMessage instead of managing local state.
33
33
  */
34
- const isInsideDevShell = window.parent !== window && import.meta.env.DEV;
34
+ const isInsideDevShell =
35
+ window.parent !== window &&
36
+ import.meta.env.DEV &&
37
+ import.meta.env.VITE_ENABLE_AGENT !== 'false';
35
38
 
36
39
  interface AgentContextValue {
37
40
  /** Whether the agent panel is visible */
@@ -0,0 +1,4 @@
1
+ // Agent panel is enabled in dev mode unless explicitly disabled via VITE_ENABLE_AGENT=false.
2
+ // To disable: set VITE_ENABLE_AGENT=false in web/.env.local or pass it at startup.
3
+ export const isAgentEnabled =
4
+ import.meta.env.DEV && import.meta.env.VITE_ENABLE_AGENT !== 'false';
@@ -12,6 +12,7 @@ import DeleteDataConfirm from './DeleteDataConfirm';
12
12
  import EditDataModal from './EditDataModal';
13
13
  import { resolvePagePaths } from '../components/agentPanel/pagePathUtils';
14
14
  import SkillMenu from './SkillMenu';
15
+ import { isAgentEnabled } from '../config/env';
15
16
  import './style.css';
16
17
  import UndoConfirm from './UndoConfirm';
17
18
 
@@ -60,13 +61,16 @@ export type ModalType =
60
61
  | null;
61
62
 
62
63
  export default function DevShell() {
64
+ if (!isAgentEnabled) return null;
65
+ return <DevShellInner />;
66
+ }
67
+
68
+ function DevShellInner() {
63
69
  const iframeRef = useRef<HTMLIFrameElement>(null);
64
70
  const agentPanelRef = useRef<HTMLDivElement>(null);
65
71
  const sdkRef = useRef<any>(null);
66
72
 
67
- const [agentVisible, setAgentVisible] = useState(
68
- () => sessionStorage.getItem('gadmin-agent-visible') === 'true',
69
- );
73
+ const [agentVisible, setAgentVisible] = useState(true);
70
74
  const [isInspecting, setIsInspecting] = useState(false);
71
75
  const [skillMenuOpen, setSkillMenuOpen] = useState(false);
72
76
  const [modal, setModal] = useState<ModalType>(null);
@@ -128,11 +132,6 @@ export default function DevShell() {
128
132
  [startNewConversationAndSend],
129
133
  );
130
134
 
131
- // ── persist agent visibility ─────────────────────────────────────────────
132
- useEffect(() => {
133
- sessionStorage.setItem('gadmin-agent-visible', String(agentVisible));
134
- }, [agentVisible]);
135
-
136
135
  // ── SDK bootstrap ────────────────────────────────────────────────────────
137
136
  useEffect(() => {
138
137
  function tryInit(remaining: number) {
@@ -37,6 +37,11 @@ export function logout() {
37
37
  )}`;
38
38
  }
39
39
 
40
+ export function isWoaDomain(): boolean {
41
+ const hostname = window.location.hostname;
42
+ return hostname === 'woa.com' || hostname.endsWith('.woa.com');
43
+ }
44
+
40
45
  /**
41
46
  * 通过cookie检测是否登录及参数转cookie(for dev)
42
47
  */
@@ -53,6 +58,11 @@ export function checkLogin(): boolean {
53
58
  return true;
54
59
  }
55
60
 
61
+ // woa.com 域名下 SmartGate 保证进入页面时已有登录态,无需跳转 Taihu
62
+ if (isWoaDomain()) {
63
+ return true;
64
+ }
65
+
56
66
  return false;
57
67
  }
58
68
 
@@ -49,10 +49,14 @@ export function devShellPlugin(): Plugin {
49
49
  return;
50
50
  }
51
51
 
52
- // "/" → dev-shell-entry.html(Agent 外壳)
52
+ // "/" → dev-shell-entry.html(Agent 外壳),除非 agent 被禁用
53
53
  if (url === '/') {
54
+ const agentDisabled = process.env.VITE_ENABLE_AGENT === 'false';
55
+ const entryFile = agentDisabled
56
+ ? 'index.html'
57
+ : 'dev-shell-entry.html';
54
58
  const rawHtml = fs.readFileSync(
55
- path.resolve(__dirname, '..', '..', 'dev-shell-entry.html'),
59
+ path.resolve(__dirname, '..', '..', entryFile),
56
60
  'utf-8',
57
61
  );
58
62
  const html = await server.transformIndexHtml(url, rawHtml);
@@ -62,10 +66,14 @@ export function devShellPlugin(): Plugin {
62
66
  }
63
67
 
64
68
  // 其他 HTML 导航请求 → index.html(SPA fallback)
69
+ // /api/* 必须放行给后续中间件(vite proxy),否则浏览器的 Accept: text/html 会让登录跳转被 SPA fallback 吞掉
65
70
  const lastSegment = url.split('/').pop() ?? '';
66
71
  const isAsset = lastSegment.includes('.');
72
+ const isApi = url === '/api' || url.startsWith('/api/');
67
73
  const isHtmlNav =
68
- !isAsset && (req.headers.accept ?? '').includes('text/html');
74
+ !isAsset &&
75
+ !isApi &&
76
+ (req.headers.accept ?? '').includes('text/html');
69
77
  if (isHtmlNav) {
70
78
  const rawHtml = fs.readFileSync(
71
79
  path.resolve(__dirname, '..', '..', 'index.html'),
@@ -1,4 +1,5 @@
1
1
  import React, { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { isAgentEnabled } from 'config/env';
2
3
  import {
3
4
  Alert,
4
5
  Badge,
@@ -482,17 +483,19 @@ export default function AgendaJobsPage() {
482
483
  任务原始表
483
484
  </Button>
484
485
  </Tooltip>
485
- <Button
486
- type="primary"
487
- icon={<PlusOutlined />}
488
- onClick={() => {
489
- setAiModalOpen(true);
490
- setAiSent(false);
491
- setAiPrompt('');
492
- }}
493
- >
494
- AI创建任务
495
- </Button>
486
+ {isAgentEnabled && (
487
+ <Button
488
+ type="primary"
489
+ icon={<PlusOutlined />}
490
+ onClick={() => {
491
+ setAiModalOpen(true);
492
+ setAiSent(false);
493
+ setAiPrompt('');
494
+ }}
495
+ >
496
+ AI创建任务
497
+ </Button>
498
+ )}
496
499
  <Button icon={<ReloadOutlined />} onClick={fetchJobs}></Button>
497
500
  </Space>
498
501
  </div>
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import IsolatedLivePreview from './IsolatedLivePreview';
3
+ import { isAgentEnabled } from 'config/env';
3
4
 
4
5
  interface CanvasCellProps extends React.HTMLAttributes<HTMLDivElement> {
5
6
  code: string;
@@ -53,12 +54,12 @@ const CanvasCell = React.forwardRef<HTMLDivElement, CanvasCellProps>(
53
54
  onClick(e);
54
55
  }}
55
56
  onDoubleClick={(e) => {
56
- if (isPreview) return;
57
+ if (isPreview || !isAgentEnabled) return;
57
58
  e.stopPropagation();
58
59
  onDoubleClick(e);
59
60
  }}
60
61
  onContextMenu={(e) => {
61
- if (isPreview) return;
62
+ if (isPreview || !isAgentEnabled) return;
62
63
  e.preventDefault();
63
64
  e.stopPropagation();
64
65
  onContextMenu(e);
@@ -24,6 +24,7 @@ import IsolatedLivePreview from './IsolatedLivePreview';
24
24
  import { useTranslation } from 'react-i18next';
25
25
  import { useUserPageAccess } from 'hooks/useUserPageAccess';
26
26
  import { customRequest } from 'helpers/http';
27
+ import { isAgentEnabled } from 'config/env';
27
28
 
28
29
  const COLS = 48;
29
30
  const ROW_HEIGHT = 10;
@@ -231,9 +232,15 @@ const CanvasListPage: React.FC = () => {
231
232
  <Typography.Title level={4} style={{ margin: 0 }}>
232
233
  {t('canvas.list.title')}
233
234
  </Typography.Title>
234
- <Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>
235
- {t('canvas.list.create')}
236
- </Button>
235
+ {isAgentEnabled && (
236
+ <Button
237
+ type="primary"
238
+ icon={<PlusOutlined />}
239
+ onClick={handleCreate}
240
+ >
241
+ {t('canvas.list.create')}
242
+ </Button>
243
+ )}
237
244
  </div>
238
245
  </Card>
239
246
 
@@ -352,31 +359,35 @@ const CanvasListPage: React.FC = () => {
352
359
  {new Date(canvas.updatedAt).toLocaleString('zh-CN')}
353
360
  </span>
354
361
  <div style={{ display: 'flex', gap: 4 }}>
355
- <Button
356
- type="text"
357
- size="small"
358
- icon={<EditOutlined />}
359
- onClick={(e) => {
360
- e.stopPropagation();
361
- openRename(canvas);
362
- }}
363
- style={{ fontSize: 12, color: '#666' }}
364
- >
365
- {t('canvas.list.rename')}
366
- </Button>
367
- <Button
368
- type="text"
369
- size="small"
370
- danger
371
- icon={<DeleteOutlined />}
372
- onClick={(e) => {
373
- e.stopPropagation();
374
- setDeleteConfirm(canvas);
375
- }}
376
- style={{ fontSize: 12 }}
377
- >
378
- {t('canvas.list.deleteBtn')}
379
- </Button>
362
+ {isAgentEnabled && (
363
+ <>
364
+ <Button
365
+ type="text"
366
+ size="small"
367
+ icon={<EditOutlined />}
368
+ onClick={(e) => {
369
+ e.stopPropagation();
370
+ openRename(canvas);
371
+ }}
372
+ style={{ fontSize: 12, color: '#666' }}
373
+ >
374
+ {t('canvas.list.rename')}
375
+ </Button>
376
+ <Button
377
+ type="text"
378
+ size="small"
379
+ danger
380
+ icon={<DeleteOutlined />}
381
+ onClick={(e) => {
382
+ e.stopPropagation();
383
+ setDeleteConfirm(canvas);
384
+ }}
385
+ style={{ fontSize: 12 }}
386
+ >
387
+ {t('canvas.list.deleteBtn')}
388
+ </Button>
389
+ </>
390
+ )}
380
391
  </div>
381
392
  </div>
382
393
  </div>
@@ -29,6 +29,7 @@ import { generatePrompt } from '@/components/agentPanel/promptGenerator';
29
29
  import { CANVAS_CONTEXT_MENU_REGISTRY } from './canvasContextMenuRegistry';
30
30
  import type { MenuActionContext } from './canvasContextMenuRegistry';
31
31
  import type { CanvasConfigModalProps } from './canvasConfigRegistry';
32
+ import { isAgentEnabled } from 'config/env';
32
33
  import NumCardDataSourceModal from './components/NumCardDataSourceModal';
33
34
  import TableDataSourceModal from './components/TableDataSourceModal';
34
35
  import BarChartDataSourceModal from './components/BarChartDataSourceModal';
@@ -778,14 +779,14 @@ const CanvasPage: React.FC<CanvasPageProps> = ({
778
779
  margin: [MARGIN_Y, MARGIN_Y] as const,
779
780
  }}
780
781
  dragConfig={{
781
- enabled: !isPreview,
782
+ enabled: !isPreview && isAgentEnabled,
782
783
  handle: '.canvas-item-drag',
783
784
  }}
784
785
  resizeConfig={{
785
- enabled: !isPreview,
786
+ enabled: !isPreview && isAgentEnabled,
786
787
  }}
787
- dropConfig={isPreview ? undefined : dropConfig}
788
- onDrop={isPreview ? undefined : handleDrop}
788
+ dropConfig={isPreview || !isAgentEnabled ? undefined : dropConfig}
789
+ onDrop={isPreview || !isAgentEnabled ? undefined : handleDrop}
789
790
  compactor={sectionCompactor}
790
791
  onResizeStop={handleResizeStop}
791
792
  onDragStart={() => setIsDragging(true)}
@@ -1,5 +1,6 @@
1
1
  import React, { useCallback } from 'react';
2
2
  import { Button, Tooltip, Popover } from 'antd';
3
+ import { isAgentEnabled } from 'config/env';
3
4
  import {
4
5
  SaveOutlined,
5
6
  ArrowLeftOutlined,
@@ -137,29 +138,31 @@ const CanvasToolbar: React.FC<CanvasToolbarProps> = ({
137
138
  gap: 10,
138
139
  }}
139
140
  >
140
- <Button
141
- size="small"
142
- icon={<SaveOutlined />}
143
- onClick={onSave}
144
- style={{
145
- borderRadius: 8,
146
- border: 'none',
147
- fontWeight: 500,
148
- ...(isDirty
149
- ? {
150
- background: '#4361ee',
151
- color: '#fff',
152
- boxShadow: '0 2px 8px rgba(67,97,238,0.3)',
153
- }
154
- : {
155
- background: '#f0f1f5',
156
- color: '#999',
157
- }),
158
- transition: 'all 200ms ease',
159
- }}
160
- >
161
- {isDirty ? t('canvas.saveDirty') : t('canvas.save')}
162
- </Button>
141
+ {isAgentEnabled && (
142
+ <Button
143
+ size="small"
144
+ icon={<SaveOutlined />}
145
+ onClick={onSave}
146
+ style={{
147
+ borderRadius: 8,
148
+ border: 'none',
149
+ fontWeight: 500,
150
+ ...(isDirty
151
+ ? {
152
+ background: '#4361ee',
153
+ color: '#fff',
154
+ boxShadow: '0 2px 8px rgba(67,97,238,0.3)',
155
+ }
156
+ : {
157
+ background: '#f0f1f5',
158
+ color: '#999',
159
+ }),
160
+ transition: 'all 200ms ease',
161
+ }}
162
+ >
163
+ {isDirty ? t('canvas.saveDirty') : t('canvas.save')}
164
+ </Button>
165
+ )}
163
166
  <Button
164
167
  size="small"
165
168
  icon={<EyeOutlined />}
@@ -183,234 +186,240 @@ const CanvasToolbar: React.FC<CanvasToolbarProps> = ({
183
186
  >
184
187
  {t('canvas.preview')}
185
188
  </Button>
186
- <Button
187
- size="small"
188
- icon={<RocketOutlined />}
189
- onClick={isPublished ? onUnpublish : onPublish}
190
- danger={isPublished}
191
- style={{
192
- borderRadius: 8,
193
- border: isPublished ? '1px solid #ff4d4f' : '1px solid #e0e0e0',
194
- background: isPublished ? '#fff2f0' : 'transparent',
195
- color: isPublished ? '#ff4d4f' : '#555',
196
- fontWeight: 500,
197
- transition: 'all 200ms ease',
198
- }}
199
- >
200
- {isPublished ? t('canvas.unpublish') : t('canvas.publish')}
201
- </Button>
189
+ {isAgentEnabled && (
190
+ <Button
191
+ size="small"
192
+ icon={<RocketOutlined />}
193
+ onClick={isPublished ? onUnpublish : onPublish}
194
+ danger={isPublished}
195
+ style={{
196
+ borderRadius: 8,
197
+ border: isPublished ? '1px solid #ff4d4f' : '1px solid #e0e0e0',
198
+ background: isPublished ? '#fff2f0' : 'transparent',
199
+ color: isPublished ? '#ff4d4f' : '#555',
200
+ fontWeight: 500,
201
+ transition: 'all 200ms ease',
202
+ }}
203
+ >
204
+ {isPublished ? t('canvas.unpublish') : t('canvas.publish')}
205
+ </Button>
206
+ )}
202
207
  </div>
203
208
  </div>
204
209
 
205
- {/* ── Thumbnail row (always visible) ── */}
206
- <div
207
- style={{
208
- display: 'flex',
209
- alignItems: 'center',
210
- gap: 24,
211
- padding: '16px 24px 20px',
212
- overflowX: 'auto',
213
- overflowY: 'hidden',
214
- borderTop: '1px solid #f0f0f0',
215
- }}
216
- >
217
- {CANVAS_COMPONENTS.map((type) => {
218
- const def = CANVAS_DEFAULTS[type];
219
- const variants = def?.variants ?? [];
220
-
221
- // ── 单 variant:thumbnail 直接可拖拽,跳过 Popover ──
222
- if (variants.length === 1) {
223
- return (
224
- <div
225
- key={type}
226
- draggable
227
- onDragStart={(e) =>
228
- handleVariantDragStart(e, type, variants[0].code)
229
- }
230
- style={{
231
- display: 'flex',
232
- flexDirection: 'column',
233
- alignItems: 'center',
234
- gap: 8,
235
- cursor: 'grab',
236
- userSelect: 'none',
237
- flexShrink: 0,
238
- width: 140,
239
- }}
240
- >
241
- <div
242
- style={{
243
- border: '1px solid #eaeaea',
244
- borderRadius: 10,
245
- overflow: 'hidden',
246
- background: '#fff',
247
- transition: 'all 200ms cubic-bezier(0.4, 0, 0.2, 1)',
248
- }}
249
- onMouseEnter={(e) => {
250
- const el = e.currentTarget;
251
- el.style.borderColor = '#4361ee';
252
- el.style.boxShadow = '0 4px 16px rgba(67,97,238,0.12)';
253
- el.style.transform = 'translateY(-3px) scale(1.03)';
254
- }}
255
- onMouseLeave={(e) => {
256
- const el = e.currentTarget;
257
- el.style.borderColor = '#eaeaea';
258
- el.style.boxShadow = 'none';
259
- el.style.transform = 'translateY(0) scale(1)';
260
- }}
261
- >
262
- <ComponentThumbnail componentType={type} width={140} />
263
- </div>
264
- <span
265
- style={{
266
- fontSize: 11,
267
- color: '#888',
268
- fontWeight: 500,
269
- letterSpacing: '0.3px',
270
- textAlign: 'center',
271
- pointerEvents: 'none',
272
- }}
273
- >
274
- {getComponentLabel(type, t)}
275
- </span>
276
- </div>
277
- );
278
- }
210
+ {/* ── Thumbnail row (only in dev) ── */}
211
+ {isAgentEnabled && (
212
+ <div
213
+ style={{
214
+ display: 'flex',
215
+ alignItems: 'center',
216
+ gap: 24,
217
+ padding: '16px 24px 20px',
218
+ overflowX: 'auto',
219
+ overflowY: 'hidden',
220
+ borderTop: '1px solid #f0f0f0',
221
+ }}
222
+ >
223
+ {CANVAS_COMPONENTS.map((type) => {
224
+ const def = CANVAS_DEFAULTS[type];
225
+ const variants = def?.variants ?? [];
279
226
 
280
- // ── variant:保持 Popover hover 展开 ──
281
- const popoverContent = (
282
- <div
283
- style={{
284
- display: 'flex',
285
- alignItems: 'center',
286
- gap: 16,
287
- padding: 4,
288
- }}
289
- >
290
- {variants.map((v) => (
227
+ // ── variant:thumbnail 直接可拖拽,跳过 Popover ──
228
+ if (variants.length === 1) {
229
+ return (
291
230
  <div
292
- key={v.name}
231
+ key={type}
293
232
  draggable
294
- onDragStart={(e) => handleVariantDragStart(e, type, v.code)}
233
+ onDragStart={(e) =>
234
+ handleVariantDragStart(e, type, variants[0].code)
235
+ }
295
236
  style={{
296
237
  display: 'flex',
297
238
  flexDirection: 'column',
298
239
  alignItems: 'center',
299
- gap: 6,
240
+ gap: 8,
300
241
  cursor: 'grab',
301
242
  userSelect: 'none',
302
243
  flexShrink: 0,
244
+ width: 140,
303
245
  }}
304
246
  >
305
247
  <div
306
248
  style={{
307
- border: '1px solid #e8e8e8',
308
- borderRadius: 8,
249
+ border: '1px solid #eaeaea',
250
+ borderRadius: 10,
309
251
  overflow: 'hidden',
310
252
  background: '#fff',
311
- transition: 'all 180ms ease',
253
+ transition: 'all 200ms cubic-bezier(0.4, 0, 0.2, 1)',
312
254
  }}
313
255
  onMouseEnter={(e) => {
314
256
  const el = e.currentTarget;
315
257
  el.style.borderColor = '#4361ee';
316
- el.style.boxShadow = '0 4px 12px rgba(67,97,238,0.15)';
317
- el.style.transform = 'translateY(-3px) scale(1.02)';
258
+ el.style.boxShadow = '0 4px 16px rgba(67,97,238,0.12)';
259
+ el.style.transform = 'translateY(-3px) scale(1.03)';
318
260
  }}
319
261
  onMouseLeave={(e) => {
320
262
  const el = e.currentTarget;
321
- el.style.borderColor = '#e8e8e8';
263
+ el.style.borderColor = '#eaeaea';
322
264
  el.style.boxShadow = 'none';
323
265
  el.style.transform = 'translateY(0) scale(1)';
324
266
  }}
325
267
  >
326
- <ComponentThumbnail
327
- componentType={type}
328
- code={v.code}
329
- width={124}
330
- />
268
+ <ComponentThumbnail componentType={type} width={140} />
331
269
  </div>
332
270
  <span
333
271
  style={{
334
- fontSize: 10,
335
- color: '#666',
272
+ fontSize: 11,
273
+ color: '#888',
336
274
  fontWeight: 500,
275
+ letterSpacing: '0.3px',
337
276
  textAlign: 'center',
338
277
  pointerEvents: 'none',
339
278
  }}
340
279
  >
341
- {getVariantLabel(v.label, t)}
280
+ {getComponentLabel(type, t)}
342
281
  </span>
343
282
  </div>
344
- ))}
345
- </div>
346
- );
283
+ );
284
+ }
347
285
 
348
- return (
349
- <Popover
350
- key={type}
351
- content={popoverContent}
352
- title={
353
- <span style={{ fontSize: 12, fontWeight: 600, color: '#333' }}>
354
- {getComponentLabel(type, t)}
355
- </span>
356
- }
357
- trigger="hover"
358
- placement="bottomLeft"
359
- mouseEnterDelay={0.2}
360
- mouseLeaveDelay={0.3}
361
- >
286
+ // ── 多 variant:保持 Popover hover 展开 ──
287
+ const popoverContent = (
362
288
  <div
363
289
  style={{
364
290
  display: 'flex',
365
- flexDirection: 'column',
366
291
  alignItems: 'center',
367
- gap: 8,
368
- cursor: 'grab',
369
- userSelect: 'none',
370
- flexShrink: 0,
371
- width: 140,
292
+ gap: 16,
293
+ padding: 4,
372
294
  }}
295
+ >
296
+ {variants.map((v) => (
297
+ <div
298
+ key={v.name}
299
+ draggable
300
+ onDragStart={(e) => handleVariantDragStart(e, type, v.code)}
301
+ style={{
302
+ display: 'flex',
303
+ flexDirection: 'column',
304
+ alignItems: 'center',
305
+ gap: 6,
306
+ cursor: 'grab',
307
+ userSelect: 'none',
308
+ flexShrink: 0,
309
+ }}
310
+ >
311
+ <div
312
+ style={{
313
+ border: '1px solid #e8e8e8',
314
+ borderRadius: 8,
315
+ overflow: 'hidden',
316
+ background: '#fff',
317
+ transition: 'all 180ms ease',
318
+ }}
319
+ onMouseEnter={(e) => {
320
+ const el = e.currentTarget;
321
+ el.style.borderColor = '#4361ee';
322
+ el.style.boxShadow = '0 4px 12px rgba(67,97,238,0.15)';
323
+ el.style.transform = 'translateY(-3px) scale(1.02)';
324
+ }}
325
+ onMouseLeave={(e) => {
326
+ const el = e.currentTarget;
327
+ el.style.borderColor = '#e8e8e8';
328
+ el.style.boxShadow = 'none';
329
+ el.style.transform = 'translateY(0) scale(1)';
330
+ }}
331
+ >
332
+ <ComponentThumbnail
333
+ componentType={type}
334
+ code={v.code}
335
+ width={124}
336
+ />
337
+ </div>
338
+ <span
339
+ style={{
340
+ fontSize: 10,
341
+ color: '#666',
342
+ fontWeight: 500,
343
+ textAlign: 'center',
344
+ pointerEvents: 'none',
345
+ }}
346
+ >
347
+ {getVariantLabel(v.label, t)}
348
+ </span>
349
+ </div>
350
+ ))}
351
+ </div>
352
+ );
353
+
354
+ return (
355
+ <Popover
356
+ key={type}
357
+ content={popoverContent}
358
+ title={
359
+ <span
360
+ style={{ fontSize: 12, fontWeight: 600, color: '#333' }}
361
+ >
362
+ {getComponentLabel(type, t)}
363
+ </span>
364
+ }
365
+ trigger="hover"
366
+ placement="bottomLeft"
367
+ mouseEnterDelay={0.2}
368
+ mouseLeaveDelay={0.3}
373
369
  >
374
370
  <div
375
371
  style={{
376
- border: '1px solid #eaeaea',
377
- borderRadius: 10,
378
- overflow: 'hidden',
379
- background: '#fff',
380
- transition: 'all 200ms cubic-bezier(0.4, 0, 0.2, 1)',
381
- }}
382
- onMouseEnter={(e) => {
383
- const el = e.currentTarget;
384
- el.style.borderColor = '#4361ee';
385
- el.style.boxShadow = '0 4px 16px rgba(67,97,238,0.12)';
386
- el.style.transform = 'translateY(-3px) scale(1.03)';
387
- }}
388
- onMouseLeave={(e) => {
389
- const el = e.currentTarget;
390
- el.style.borderColor = '#eaeaea';
391
- el.style.boxShadow = 'none';
392
- el.style.transform = 'translateY(0) scale(1)';
372
+ display: 'flex',
373
+ flexDirection: 'column',
374
+ alignItems: 'center',
375
+ gap: 8,
376
+ cursor: 'grab',
377
+ userSelect: 'none',
378
+ flexShrink: 0,
379
+ width: 140,
393
380
  }}
394
381
  >
395
- <ComponentThumbnail componentType={type} width={140} />
382
+ <div
383
+ style={{
384
+ border: '1px solid #eaeaea',
385
+ borderRadius: 10,
386
+ overflow: 'hidden',
387
+ background: '#fff',
388
+ transition: 'all 200ms cubic-bezier(0.4, 0, 0.2, 1)',
389
+ }}
390
+ onMouseEnter={(e) => {
391
+ const el = e.currentTarget;
392
+ el.style.borderColor = '#4361ee';
393
+ el.style.boxShadow = '0 4px 16px rgba(67,97,238,0.12)';
394
+ el.style.transform = 'translateY(-3px) scale(1.03)';
395
+ }}
396
+ onMouseLeave={(e) => {
397
+ const el = e.currentTarget;
398
+ el.style.borderColor = '#eaeaea';
399
+ el.style.boxShadow = 'none';
400
+ el.style.transform = 'translateY(0) scale(1)';
401
+ }}
402
+ >
403
+ <ComponentThumbnail componentType={type} width={140} />
404
+ </div>
405
+ <span
406
+ style={{
407
+ fontSize: 11,
408
+ color: '#888',
409
+ fontWeight: 500,
410
+ letterSpacing: '0.3px',
411
+ textAlign: 'center',
412
+ pointerEvents: 'none',
413
+ }}
414
+ >
415
+ {getComponentLabel(type, t)}
416
+ </span>
396
417
  </div>
397
- <span
398
- style={{
399
- fontSize: 11,
400
- color: '#888',
401
- fontWeight: 500,
402
- letterSpacing: '0.3px',
403
- textAlign: 'center',
404
- pointerEvents: 'none',
405
- }}
406
- >
407
- {getComponentLabel(type, t)}
408
- </span>
409
- </div>
410
- </Popover>
411
- );
412
- })}
413
- </div>
418
+ </Popover>
419
+ );
420
+ })}
421
+ </div>
422
+ )}
414
423
  </div>
415
424
  );
416
425
  };
@@ -31,6 +31,7 @@ import { FlowRenderer } from './components/FlowRenderer';
31
31
  import { NodePropertyPanel } from './components/NodePropertyPanel';
32
32
  import { DslView, type DslViewHandle } from './components/DslView';
33
33
  import { useWorkflowAgent } from './hooks/useWorkflowAgent';
34
+ import { isAgentEnabled as isDev } from 'config/env';
34
35
  import type {
35
36
  Workflow,
36
37
  WorkflowDSL,
@@ -44,7 +45,6 @@ const { TextArea } = Input;
44
45
 
45
46
  const NEW_DRAFT_KEY = 'workflow_new_draft';
46
47
  const EDIT_DRAFT_KEY = (id: string) => `workflow_draft_${id}`;
47
- const isDev = import.meta.env.DEV;
48
48
 
49
49
  export default function WorkflowEditorPage() {
50
50
  const { id } = useParams<{ id: string }>();
@@ -19,7 +19,7 @@ import {
19
19
  import type { WorkflowNodeType } from '../../types';
20
20
 
21
21
  const { Title } = Typography;
22
- const isDev = import.meta.env.DEV;
22
+ import { isAgentEnabled as isDev } from 'config/env';
23
23
 
24
24
  interface NodeInstanceFormProps {
25
25
  form: FormInstance;
@@ -48,7 +48,7 @@ const STATUS_COLOR: Record<string, string> = {
48
48
  PUBLISHED: 'success',
49
49
  };
50
50
 
51
- const isDev = import.meta.env.DEV;
51
+ import { isAgentEnabled as isDev } from 'config/env';
52
52
 
53
53
  export default function WorkflowShowPage() {
54
54
  const { id } = useParams<{ id: string }>();
@@ -34,6 +34,7 @@ export default defineConfig(({ mode }) => {
34
34
  },
35
35
  server: {
36
36
  host: true,
37
+ allowedHosts: true,
37
38
  proxy: proxyConfig,
38
39
  },
39
40
  build: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gadmin2n/schematics",
3
- "version": "0.0.104",
3
+ "version": "0.0.106",
4
4
  "description": "Gadmin - modern, fast, powerful node.js web framework (@schematics)",
5
5
  "main": "dist/index.js",
6
6
  "files": [
@@ -1,52 +0,0 @@
1
- # compiled output
2
- bin/
3
- dist/
4
- node_modules/
5
- generated/
6
- build/
7
- server/prisma/dbml
8
- server/prisma/schema.prisma
9
- upload/
10
- # Logs
11
- logs/
12
- *.log
13
- npm-debug.log*
14
- pnpm-debug.log*
15
- yarn-debug.log*
16
- yarn-error.log*
17
- lerna-debug.log*
18
- .env.local
19
-
20
- # OS
21
- .DS_Store
22
-
23
- # Tests
24
- coverage
25
- .nyc_output
26
-
27
- # IDEs and editors
28
- .idea
29
- .project
30
- .classpath
31
- .c9/
32
- *.launch
33
- .settings/
34
- *.sublime-workspace
35
-
36
- # IDE - VSCode
37
- .vscode/*
38
- !.vscode/settings.json
39
- !.vscode/tasks.json
40
- !.vscode/launch.json
41
- !.vscode/extensions.json
42
-
43
- .yalc/
44
-
45
- nginx-external-page/
46
- settings.local.json
47
-
48
- .agent/
49
- .claude-internal/
50
- .claude
51
- docs/
52
- .frontend-slides/