@nocobase/portal-template-default 1.0.1 → 1.0.3
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/README.MD +22 -3
- package/package.json +4 -2
- package/registry/nocobase-acl/components/role-options.ts +7 -0
- package/registry/nocobase-acl/components/role-switcher.tsx +9 -4
- package/registry/nocobase-acl/demo/boundary-api.tsx +87 -16
- package/registry/nocobase-acl/demo/components.tsx +32 -11
- package/registry/nocobase-acl/demo/index.tsx +45 -14
- package/registry/nocobase-acl/demo/lazy-route.tsx +2 -7
- package/registry/nocobase-acl/demo/prompt-generator.tsx +52 -33
- package/registry/nocobase-acl/demo/role-switcher-prompt-generator.tsx +28 -9
- package/registry/nocobase-acl/extension.tsx +1 -1
- package/registry/nocobase-acl/tests/acl-regression.mjs +7 -6
- package/registry/nocobase-ai/components/chat/chat-messages.tsx +3 -4
- package/registry/nocobase-ai/components/chat/conversation-list.tsx +2 -4
- package/registry/nocobase-ai/components/tools/business-report-dialog.tsx +16 -19
- package/registry/nocobase-ai/components/tools/chart-renderer.tsx +3 -14
- package/registry/nocobase-ai/components/tools/echarts-preview.tsx +2 -5
- package/registry/nocobase-ai/demo/configuration-gate.tsx +9 -12
- package/registry/nocobase-ai/extension.tsx +2 -7
- package/registry/nocobase-i18n/README.md +53 -0
- package/registry/nocobase-i18n/components/index.ts +1 -0
- package/registry/nocobase-i18n/components/language-switcher.tsx +138 -0
- package/registry/nocobase-i18n/demo/index.tsx +209 -0
- package/registry/nocobase-i18n/demo/prompt-generator.tsx +155 -0
- package/registry/nocobase-i18n/extension.tsx +46 -0
- package/registry/nocobase-i18n/i18n-provider.ts +9 -0
- package/registry/nocobase-i18n/index.ts +6 -0
- package/registry/nocobase-i18n/locale-store.ts +134 -0
- package/registry/nocobase-i18n/locales/en-US.ts +89 -0
- package/registry/nocobase-i18n/locales/zh-CN.ts +85 -0
- package/registry/nocobase-i18n/provider.tsx +32 -0
- package/registry/nocobase-i18n/runtime.ts +118 -0
- package/registry/nocobase-i18n/server-resources.ts +184 -0
- package/registry/nocobase-i18n/tests/i18n-regression.mjs +42 -0
- package/registry.config.json +34 -14
- package/scripts/registry.mjs +5 -0
- package/src/App.tsx +27 -86
- package/src/app/extension.ts +3 -1
- package/src/app/extensions.tsx +21 -0
- package/src/components/access-control/acl-gate.tsx +4 -10
- package/src/components/app-shell/breadcrumb.tsx +39 -3
- package/src/components/app-shell/document-title-handler.tsx +11 -20
- package/src/components/app-shell/header.tsx +18 -3
- package/src/components/app-shell/loading-overlay.tsx +2 -4
- package/src/components/app-shell/loading-state.tsx +15 -0
- package/src/components/app-shell/sidebar.tsx +75 -47
- package/src/components/data-table/data-table-pagination.tsx +27 -7
- package/src/components/data-table/data-table-styles.ts +57 -0
- package/src/components/data-table/data-table.tsx +14 -59
- package/src/components/resources/resource-label.ts +101 -0
- package/src/components/resources/views/create-view.tsx +17 -14
- package/src/components/resources/views/edit-view.tsx +17 -15
- package/src/components/resources/views/list-view.tsx +20 -10
- package/src/components/resources/views/show-view.tsx +20 -18
- package/src/extensions/nocobase-acl/README.md +19 -0
- package/src/extensions/nocobase-acl/components/acl-boundary.tsx +106 -0
- package/src/extensions/nocobase-acl/components/index.ts +3 -0
- package/src/extensions/nocobase-acl/components/role-options.ts +36 -0
- package/src/extensions/nocobase-acl/components/role-switcher.tsx +135 -0
- package/src/extensions/nocobase-acl/demo/boundary-api.tsx +186 -0
- package/src/extensions/nocobase-acl/demo/components.tsx +231 -0
- package/src/extensions/nocobase-acl/demo/index.tsx +505 -0
- package/src/extensions/nocobase-acl/demo/lazy-route.tsx +10 -0
- package/src/extensions/nocobase-acl/demo/prompt-generator.tsx +134 -0
- package/src/extensions/nocobase-acl/demo/role-switcher-prompt-generator.tsx +198 -0
- package/src/extensions/nocobase-acl/demo/scenario-section.tsx +37 -0
- package/src/extensions/nocobase-acl/extension.tsx +71 -0
- package/src/extensions/nocobase-acl/index.ts +1 -0
- package/src/extensions/nocobase-ai/README.md +70 -0
- package/src/extensions/nocobase-ai/adapters/react-hook-form.ts +20 -0
- package/src/extensions/nocobase-ai/assets/nocobase-ai-chat.svg +50 -0
- package/src/extensions/nocobase-ai/components/ai-root-provider.tsx +32 -0
- package/src/extensions/nocobase-ai/components/chat/ai-employee-avatar.tsx +31 -0
- package/src/extensions/nocobase-ai/components/chat/chat-attachment.tsx +92 -0
- package/src/extensions/nocobase-ai/components/chat/chat-compact.tsx +90 -0
- package/src/extensions/nocobase-ai/components/chat/chat-composer.tsx +429 -0
- package/src/extensions/nocobase-ai/components/chat/chat-empty-state.tsx +51 -0
- package/src/extensions/nocobase-ai/components/chat/chat-header.tsx +88 -0
- package/src/extensions/nocobase-ai/components/chat/chat-history-dialog.tsx +77 -0
- package/src/extensions/nocobase-ai/components/chat/chat-message.tsx +252 -0
- package/src/extensions/nocobase-ai/components/chat/chat-messages.tsx +173 -0
- package/src/extensions/nocobase-ai/components/chat/chat-window.tsx +118 -0
- package/src/extensions/nocobase-ai/components/chat/conversation-list.tsx +343 -0
- package/src/extensions/nocobase-ai/components/chat/markdown-message.tsx +165 -0
- package/src/extensions/nocobase-ai/components/chat/model-select-options.tsx +30 -0
- package/src/extensions/nocobase-ai/components/chat/reasoning-panel.tsx +29 -0
- package/src/extensions/nocobase-ai/components/chat/sub-agent-conversation.tsx +142 -0
- package/src/extensions/nocobase-ai/components/chat/tool-call-card.tsx +316 -0
- package/src/extensions/nocobase-ai/components/chat/user-prompt-editor.tsx +99 -0
- package/src/extensions/nocobase-ai/components/chat/work-context-chip.tsx +114 -0
- package/src/extensions/nocobase-ai/components/index.ts +59 -0
- package/src/extensions/nocobase-ai/components/page-elements/ai-form.tsx +44 -0
- package/src/extensions/nocobase-ai/components/page-elements/page-element-provider.tsx +469 -0
- package/src/extensions/nocobase-ai/components/surfaces/chat-dialog.tsx +31 -0
- package/src/extensions/nocobase-ai/components/surfaces/chat-inline.tsx +18 -0
- package/src/extensions/nocobase-ai/components/surfaces/chat-page.tsx +18 -0
- package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +54 -0
- package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel.tsx +74 -0
- package/src/extensions/nocobase-ai/components/surfaces/chat-surface-actions.tsx +54 -0
- package/src/extensions/nocobase-ai/components/tools/builtin-tool-renderers.tsx +31 -0
- package/src/extensions/nocobase-ai/components/tools/business-report-dialog.tsx +364 -0
- package/src/extensions/nocobase-ai/components/tools/business-report-renderer.tsx +122 -0
- package/src/extensions/nocobase-ai/components/tools/business-report-utils.tsx +282 -0
- package/src/extensions/nocobase-ai/components/tools/chart-renderer.tsx +99 -0
- package/src/extensions/nocobase-ai/components/tools/echarts-preview.tsx +59 -0
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime-advanced.ts +18 -0
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime-common.ts +20 -0
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime-components.ts +34 -0
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +24 -0
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime.ts +138 -0
- package/src/extensions/nocobase-ai/components/tools/sub-agent-renderer.tsx +67 -0
- package/src/extensions/nocobase-ai/components/tools/suggestions-renderer.tsx +59 -0
- package/src/extensions/nocobase-ai/components/tools/tool-renderer-provider.tsx +55 -0
- package/src/extensions/nocobase-ai/components/tools/tool-renderer-utils.ts +18 -0
- package/src/extensions/nocobase-ai/components/tools/workflow-renderer.tsx +108 -0
- package/src/extensions/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +73 -0
- package/src/extensions/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +149 -0
- package/src/extensions/nocobase-ai/demo/configuration-gate.tsx +49 -0
- package/src/extensions/nocobase-ai/demo/container-showcase.tsx +210 -0
- package/src/extensions/nocobase-ai/demo/floating.tsx +134 -0
- package/src/extensions/nocobase-ai/demo/index.tsx +346 -0
- package/src/extensions/nocobase-ai/demo/interaction-showcase.tsx +151 -0
- package/src/extensions/nocobase-ai/demo/page-context-prompt-generator.tsx +355 -0
- package/src/extensions/nocobase-ai/demo/page-context.tsx +884 -0
- package/src/extensions/nocobase-ai/demo/page-element-showcase.tsx +197 -0
- package/src/extensions/nocobase-ai/demo/prompt-card.tsx +41 -0
- package/src/extensions/nocobase-ai/demo/prompt-generator.tsx +452 -0
- package/src/extensions/nocobase-ai/demo/shortcut.tsx +1207 -0
- package/src/extensions/nocobase-ai/demo/tool-cards.tsx +530 -0
- package/src/extensions/nocobase-ai/extension.tsx +158 -0
- package/src/extensions/nocobase-ai/global-ai-chat.tsx +213 -0
- package/src/extensions/nocobase-ai/index.ts +3 -0
- package/src/extensions/nocobase-ai/providers/ai-provider.tsx +323 -0
- package/src/extensions/nocobase-ai/providers/avatars.ts +817 -0
- package/src/extensions/nocobase-ai/providers/chat-context.tsx +109 -0
- package/src/extensions/nocobase-ai/providers/chat-controller.ts +84 -0
- package/src/extensions/nocobase-ai/providers/chat-message-utils.ts +198 -0
- package/src/extensions/nocobase-ai/providers/chat-provider.tsx +871 -0
- package/src/extensions/nocobase-ai/providers/chat-reducer.ts +147 -0
- package/src/extensions/nocobase-ai/providers/chat-task-utils.ts +105 -0
- package/src/extensions/nocobase-ai/providers/chat-transport.ts +184 -0
- package/src/extensions/nocobase-ai/providers/form-registry.tsx +271 -0
- package/src/extensions/nocobase-ai/providers/frontend-tool-registry.tsx +231 -0
- package/src/extensions/nocobase-ai/providers/index.ts +61 -0
- package/src/extensions/nocobase-ai/providers/model.ts +32 -0
- package/src/extensions/nocobase-ai/providers/page-context.tsx +136 -0
- package/src/extensions/nocobase-ai/providers/stream-coalescer.ts +60 -0
- package/src/extensions/nocobase-ai/providers/stream-event-utils.ts +82 -0
- package/src/extensions/nocobase-ai/providers/stream-parser.ts +61 -0
- package/src/extensions/nocobase-ai/providers/sub-agent-stream.ts +316 -0
- package/src/extensions/nocobase-ai/providers/types.ts +183 -0
- package/src/extensions/nocobase-ai/providers/ui-message-stream.ts +463 -0
- package/src/extensions/nocobase-ai/providers/use-automatic-tool-approval.ts +94 -0
- package/src/extensions/nocobase-ai/providers/use-chat-attachments.ts +206 -0
- package/src/extensions/nocobase-ai/providers/use-chat-message-actions.ts +554 -0
- package/src/extensions/nocobase-ai/providers/use-chat-runtime.ts +210 -0
- package/src/extensions/nocobase-ai/providers/use-chat-work-context.ts +93 -0
- package/src/extensions/nocobase-ai/providers/use-conversation-catalog.ts +108 -0
- package/src/extensions/nocobase-ai/providers/use-conversation-history.ts +178 -0
- package/src/extensions/nocobase-ai/services/index.ts +7 -0
- package/src/extensions/nocobase-ai/services/nocobase-ai-service.ts +454 -0
- package/src/extensions/nocobase-ai/services/types.ts +82 -0
- package/src/index.tsx +2 -3
- package/src/lib/i18n.ts +161 -0
- package/src/lib/nocobase/client.ts +19 -2
- package/src/locales/en-US.ts +82 -0
- package/src/locales/index.ts +13 -0
- package/src/locales/zh-CN.ts +79 -0
- package/src/pages/users/create.tsx +98 -0
- package/src/pages/users/edit.tsx +92 -0
- package/src/pages/users/form-context.ts +57 -0
- package/src/pages/users/form-fields.tsx +211 -0
- package/src/pages/users/list.tsx +205 -0
- package/src/pages/users/show.tsx +187 -0
- package/src/pages/users/types.ts +20 -0
- package/src/providers/constants.ts +1 -0
- package/src/pages/blog-posts/create.tsx +0 -177
- package/src/pages/blog-posts/edit.tsx +0 -197
- package/src/pages/blog-posts/list.tsx +0 -131
- package/src/pages/blog-posts/show.tsx +0 -81
- package/src/pages/categories/create.tsx +0 -73
- package/src/pages/categories/edit.tsx +0 -73
- package/src/pages/categories/index.ts +0 -4
- package/src/pages/categories/list.tsx +0 -86
- package/src/pages/categories/show.tsx +0 -37
- /package/src/pages/{blog-posts → users}/index.ts +0 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useMemo,
|
|
5
|
+
type PropsWithChildren,
|
|
6
|
+
} from "react";
|
|
7
|
+
import type { AIToolCallInvocationContext, AIToolInvokerMap } from "./types";
|
|
8
|
+
|
|
9
|
+
export type AIFrontendToolPermission = "ASK" | "ALLOW";
|
|
10
|
+
|
|
11
|
+
export type AIFrontendToolRegistration<
|
|
12
|
+
TArgs = unknown,
|
|
13
|
+
TResult = unknown,
|
|
14
|
+
> = {
|
|
15
|
+
name: string;
|
|
16
|
+
title?: string;
|
|
17
|
+
description: string;
|
|
18
|
+
permission?: AIFrontendToolPermission;
|
|
19
|
+
inputSchema?: Record<string, unknown>;
|
|
20
|
+
execute: (args: TArgs) => TResult | Promise<TResult>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type AIFrontendToolManifest = {
|
|
24
|
+
id: string;
|
|
25
|
+
blockUid: string;
|
|
26
|
+
name: string;
|
|
27
|
+
title?: string;
|
|
28
|
+
description: string;
|
|
29
|
+
permission: AIFrontendToolPermission;
|
|
30
|
+
inputSchema: Record<string, unknown>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type AIFrontendToolEntry = {
|
|
34
|
+
token: symbol;
|
|
35
|
+
manifest: AIFrontendToolManifest;
|
|
36
|
+
execute: (args: unknown) => unknown | Promise<unknown>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const TOOL_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
|
|
40
|
+
|
|
41
|
+
const cloneManifest = (
|
|
42
|
+
manifest: AIFrontendToolManifest
|
|
43
|
+
): AIFrontendToolManifest => structuredClone(manifest);
|
|
44
|
+
|
|
45
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
46
|
+
!!value && typeof value === "object" && !Array.isArray(value);
|
|
47
|
+
|
|
48
|
+
export function defineAIFrontendTool<TArgs, TResult>(
|
|
49
|
+
registration: AIFrontendToolRegistration<TArgs, TResult>
|
|
50
|
+
) {
|
|
51
|
+
return registration;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class AIFrontendToolRegistry {
|
|
55
|
+
private readonly tools = new Map<string, AIFrontendToolEntry>();
|
|
56
|
+
|
|
57
|
+
register(contextId: string, registration: AIFrontendToolRegistration) {
|
|
58
|
+
if (!contextId.trim()) {
|
|
59
|
+
throw new Error("Frontend Tool context id is required");
|
|
60
|
+
}
|
|
61
|
+
if (!TOOL_NAME_PATTERN.test(registration.name)) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
"Frontend Tool name must start with a letter and contain only letters, numbers, underscores, or hyphens"
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (!registration.description.trim()) {
|
|
67
|
+
throw new Error("Frontend Tool description is required");
|
|
68
|
+
}
|
|
69
|
+
if (typeof registration.execute !== "function") {
|
|
70
|
+
throw new Error("Frontend Tool execute must be a function");
|
|
71
|
+
}
|
|
72
|
+
if (
|
|
73
|
+
registration.inputSchema !== undefined &&
|
|
74
|
+
!isRecord(registration.inputSchema)
|
|
75
|
+
) {
|
|
76
|
+
throw new Error("Frontend Tool inputSchema must be an object");
|
|
77
|
+
}
|
|
78
|
+
if (
|
|
79
|
+
registration.permission !== undefined &&
|
|
80
|
+
registration.permission !== "ASK" &&
|
|
81
|
+
registration.permission !== "ALLOW"
|
|
82
|
+
) {
|
|
83
|
+
throw new Error("Frontend Tool permission must be ASK or ALLOW");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const id = `${contextId}:${registration.name}`;
|
|
87
|
+
if (this.tools.has(id)) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Frontend Tool "${registration.name}" is already registered for context "${contextId}"`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
const token = Symbol(id);
|
|
93
|
+
let inputSchema: Record<string, unknown>;
|
|
94
|
+
try {
|
|
95
|
+
inputSchema = structuredClone(
|
|
96
|
+
registration.inputSchema ?? { type: "object", properties: {} }
|
|
97
|
+
);
|
|
98
|
+
} catch {
|
|
99
|
+
throw new Error("Frontend Tool inputSchema must be serializable");
|
|
100
|
+
}
|
|
101
|
+
const manifest: AIFrontendToolManifest = {
|
|
102
|
+
id,
|
|
103
|
+
blockUid: contextId,
|
|
104
|
+
name: registration.name,
|
|
105
|
+
title: registration.title,
|
|
106
|
+
description: registration.description,
|
|
107
|
+
permission: registration.permission ?? "ASK",
|
|
108
|
+
inputSchema,
|
|
109
|
+
};
|
|
110
|
+
this.tools.set(id, {
|
|
111
|
+
token,
|
|
112
|
+
manifest,
|
|
113
|
+
execute: registration.execute as (
|
|
114
|
+
args: unknown
|
|
115
|
+
) => unknown | Promise<unknown>,
|
|
116
|
+
});
|
|
117
|
+
return () => {
|
|
118
|
+
if (this.tools.get(id)?.token === token) this.tools.delete(id);
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
list(contextId: string) {
|
|
123
|
+
return [...this.tools.values()]
|
|
124
|
+
.filter((entry) => entry.manifest.blockUid === contextId)
|
|
125
|
+
.map((entry) => cloneManifest(entry.manifest));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
getManifest(toolId: string) {
|
|
129
|
+
const manifest = this.tools.get(toolId)?.manifest;
|
|
130
|
+
return manifest ? cloneManifest(manifest) : undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async execute(toolId: string, args: unknown) {
|
|
134
|
+
const entry = this.tools.get(toolId);
|
|
135
|
+
if (!entry) throw new Error(`Frontend Tool "${toolId}" is unavailable`);
|
|
136
|
+
const result = await entry.execute(args);
|
|
137
|
+
try {
|
|
138
|
+
structuredClone(result);
|
|
139
|
+
JSON.stringify(result);
|
|
140
|
+
} catch {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Frontend Tool "${toolId}" returned a non-serializable result`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const AIFrontendToolRegistryContext =
|
|
150
|
+
createContext<AIFrontendToolRegistry | null>(null);
|
|
151
|
+
|
|
152
|
+
export function AIFrontendToolRegistryProvider({
|
|
153
|
+
children,
|
|
154
|
+
}: PropsWithChildren) {
|
|
155
|
+
const registry = useMemo(() => new AIFrontendToolRegistry(), []);
|
|
156
|
+
return (
|
|
157
|
+
<AIFrontendToolRegistryContext.Provider value={registry}>
|
|
158
|
+
{children}
|
|
159
|
+
</AIFrontendToolRegistryContext.Provider>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function useAIFrontendToolRegistry() {
|
|
164
|
+
const registry = useContext(AIFrontendToolRegistryContext);
|
|
165
|
+
if (!registry) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
"useAIFrontendToolRegistry must be used inside AIFrontendToolRegistryProvider"
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
return registry;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function useOptionalAIFrontendToolRegistry() {
|
|
174
|
+
return useContext(AIFrontendToolRegistryContext);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const errorResult = (error: unknown) => ({
|
|
178
|
+
status: "error" as const,
|
|
179
|
+
content: error instanceof Error ? error.message : String(error),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const getToolId = (input: unknown) =>
|
|
183
|
+
input && typeof input === "object" && !Array.isArray(input)
|
|
184
|
+
? (input as { toolId?: unknown }).toolId
|
|
185
|
+
: undefined;
|
|
186
|
+
|
|
187
|
+
const isToolAllowed = (
|
|
188
|
+
toolId: string,
|
|
189
|
+
context: AIToolCallInvocationContext
|
|
190
|
+
) => context.allowedFrontendToolIds?.includes(toolId) === true;
|
|
191
|
+
|
|
192
|
+
export function createFrontendToolInvokers(
|
|
193
|
+
registry: AIFrontendToolRegistry
|
|
194
|
+
): AIToolInvokerMap {
|
|
195
|
+
return {
|
|
196
|
+
loadFrontendTool: async (input, context) => {
|
|
197
|
+
const toolId = getToolId(input);
|
|
198
|
+
if (typeof toolId !== "string" || !toolId) {
|
|
199
|
+
return errorResult("Frontend Tool id is required");
|
|
200
|
+
}
|
|
201
|
+
if (!isToolAllowed(toolId, context)) {
|
|
202
|
+
return errorResult(
|
|
203
|
+
`Frontend Tool "${toolId}" is not available in this conversation context`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
return (
|
|
207
|
+
registry.getManifest(toolId) ??
|
|
208
|
+
errorResult(`Frontend Tool "${toolId}" is unavailable`)
|
|
209
|
+
);
|
|
210
|
+
},
|
|
211
|
+
executeFrontendTool: async (input, context) => {
|
|
212
|
+
const params =
|
|
213
|
+
input && typeof input === "object" && !Array.isArray(input)
|
|
214
|
+
? (input as { toolId?: unknown; args?: unknown })
|
|
215
|
+
: {};
|
|
216
|
+
if (typeof params.toolId !== "string" || !params.toolId) {
|
|
217
|
+
return errorResult("Frontend Tool id is required");
|
|
218
|
+
}
|
|
219
|
+
if (!isToolAllowed(params.toolId, context)) {
|
|
220
|
+
return errorResult(
|
|
221
|
+
`Frontend Tool "${params.toolId}" is not available in this conversation context`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
return await registry.execute(params.toolId, params.args ?? {});
|
|
226
|
+
} catch (error) {
|
|
227
|
+
return errorResult(error);
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export {
|
|
2
|
+
AIProvider,
|
|
3
|
+
useAI,
|
|
4
|
+
useGlobalAIChatController,
|
|
5
|
+
type AIProviderProps,
|
|
6
|
+
} from "./ai-provider";
|
|
7
|
+
export { AIChatProvider, type AIChatProviderProps } from "./chat-provider";
|
|
8
|
+
export {
|
|
9
|
+
useAIChat,
|
|
10
|
+
useAIChatBase,
|
|
11
|
+
useAIChatMessages,
|
|
12
|
+
useAIChatStatus,
|
|
13
|
+
type AIChatBaseContextValue,
|
|
14
|
+
type AIChatContextValue,
|
|
15
|
+
type AIChatMessagesContextValue,
|
|
16
|
+
type AIChatStatusContextValue,
|
|
17
|
+
} from "./chat-context";
|
|
18
|
+
export {
|
|
19
|
+
createAIChatController,
|
|
20
|
+
useAIChatController,
|
|
21
|
+
useAIChatControllerState,
|
|
22
|
+
type AIChatController,
|
|
23
|
+
type AIChatControllerSnapshot,
|
|
24
|
+
} from "./chat-controller";
|
|
25
|
+
export { NocoBaseChatTransport } from "./chat-transport";
|
|
26
|
+
export { getAIEmployeeAvatar } from "./avatars";
|
|
27
|
+
export { findAIModel, getAIModelKey, groupAIModels } from "./model";
|
|
28
|
+
export type { AIModelGroup } from "./model";
|
|
29
|
+
export {
|
|
30
|
+
AIFormRegistry,
|
|
31
|
+
AIFormRegistryProvider,
|
|
32
|
+
createFormFillerInvoker,
|
|
33
|
+
useAIFormRegistry,
|
|
34
|
+
type AIFormField,
|
|
35
|
+
type AIFormFillResult,
|
|
36
|
+
type AIFormFillSkippedField,
|
|
37
|
+
type AIFormTarget,
|
|
38
|
+
} from "./form-registry";
|
|
39
|
+
export {
|
|
40
|
+
AIFrontendToolRegistry,
|
|
41
|
+
AIFrontendToolRegistryProvider,
|
|
42
|
+
createFrontendToolInvokers,
|
|
43
|
+
defineAIFrontendTool,
|
|
44
|
+
useAIFrontendToolRegistry,
|
|
45
|
+
useOptionalAIFrontendToolRegistry,
|
|
46
|
+
type AIFrontendToolManifest,
|
|
47
|
+
type AIFrontendToolPermission,
|
|
48
|
+
type AIFrontendToolRegistration,
|
|
49
|
+
} from "./frontend-tool-registry";
|
|
50
|
+
export {
|
|
51
|
+
AIPageContextResolverProvider,
|
|
52
|
+
AIPageContextScope,
|
|
53
|
+
createAIPageContextReference,
|
|
54
|
+
getAIWorkContextRequiredTools,
|
|
55
|
+
getAIWorkContextToolScope,
|
|
56
|
+
mergeAIRequiredTools,
|
|
57
|
+
useAIPageContextScope,
|
|
58
|
+
useAIPageContextResolver,
|
|
59
|
+
type AIPageContextResolver,
|
|
60
|
+
} from "./page-context";
|
|
61
|
+
export type * from "./types";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { AIModel } from "./types";
|
|
2
|
+
|
|
3
|
+
export type AIModelGroup = {
|
|
4
|
+
key: string;
|
|
5
|
+
label: string;
|
|
6
|
+
models: AIModel[];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function getAIModelKey(model: AIModel) {
|
|
10
|
+
return model.llmService ? `${model.llmService}:${model.value}` : model.value;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function findAIModel(models: AIModel[], key: string) {
|
|
14
|
+
return models.find((model) => getAIModelKey(model) === key);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function groupAIModels(models: AIModel[]): AIModelGroup[] {
|
|
18
|
+
const groups = new Map<string, AIModelGroup>();
|
|
19
|
+
|
|
20
|
+
for (const model of models) {
|
|
21
|
+
const key = model.llmService ?? "__models__";
|
|
22
|
+
const group = groups.get(key) ?? {
|
|
23
|
+
key,
|
|
24
|
+
label: model.llmServiceTitle ?? model.llmService ?? "Models",
|
|
25
|
+
models: [],
|
|
26
|
+
};
|
|
27
|
+
group.models.push(model);
|
|
28
|
+
groups.set(key, group);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return Array.from(groups.values());
|
|
32
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useMemo,
|
|
5
|
+
type PropsWithChildren,
|
|
6
|
+
} from "react";
|
|
7
|
+
import type {
|
|
8
|
+
AIEmployeeTask,
|
|
9
|
+
AIWorkContextItem,
|
|
10
|
+
} from "./types";
|
|
11
|
+
|
|
12
|
+
export type AIPageContextResolver = (
|
|
13
|
+
items: AIWorkContextItem[]
|
|
14
|
+
) => Promise<AIWorkContextItem[]>;
|
|
15
|
+
|
|
16
|
+
const AIPageContextResolverContext = createContext<
|
|
17
|
+
AIPageContextResolver | undefined
|
|
18
|
+
>(undefined);
|
|
19
|
+
|
|
20
|
+
export function AIPageContextResolverProvider({
|
|
21
|
+
resolve,
|
|
22
|
+
children,
|
|
23
|
+
}: PropsWithChildren<{ resolve: AIPageContextResolver }>) {
|
|
24
|
+
return (
|
|
25
|
+
<AIPageContextResolverContext.Provider value={resolve}>
|
|
26
|
+
{children}
|
|
27
|
+
</AIPageContextResolverContext.Provider>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const useAIPageContextResolver = () =>
|
|
32
|
+
useContext(AIPageContextResolverContext);
|
|
33
|
+
|
|
34
|
+
const EMPTY_PAGE_CONTEXT: AIWorkContextItem[] = [];
|
|
35
|
+
const AIPageContextScopeContext = createContext(EMPTY_PAGE_CONTEXT);
|
|
36
|
+
|
|
37
|
+
export function AIPageContextScope({
|
|
38
|
+
context,
|
|
39
|
+
mode = "replace",
|
|
40
|
+
children,
|
|
41
|
+
}: PropsWithChildren<{
|
|
42
|
+
context: AIWorkContextItem | AIWorkContextItem[];
|
|
43
|
+
mode?: "replace" | "append";
|
|
44
|
+
}>) {
|
|
45
|
+
const inheritedContext = useContext(AIPageContextScopeContext);
|
|
46
|
+
const value = useMemo(
|
|
47
|
+
() => {
|
|
48
|
+
const ownContext = Array.isArray(context) ? context : [context];
|
|
49
|
+
if (mode !== "append") return ownContext;
|
|
50
|
+
const combined = [...inheritedContext, ...ownContext];
|
|
51
|
+
return combined.filter((item, index) => {
|
|
52
|
+
if (!item.id) return true;
|
|
53
|
+
return (
|
|
54
|
+
combined.findLastIndex(
|
|
55
|
+
(candidate) =>
|
|
56
|
+
candidate.type === item.type && candidate.id === item.id
|
|
57
|
+
) === index
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
[context, inheritedContext, mode]
|
|
62
|
+
);
|
|
63
|
+
return (
|
|
64
|
+
<AIPageContextScopeContext.Provider value={value}>
|
|
65
|
+
{children}
|
|
66
|
+
</AIPageContextScopeContext.Provider>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const useAIPageContextScope = () =>
|
|
71
|
+
useContext(AIPageContextScopeContext);
|
|
72
|
+
|
|
73
|
+
export function createAIPageContextReference({
|
|
74
|
+
id,
|
|
75
|
+
title,
|
|
76
|
+
kind,
|
|
77
|
+
}: {
|
|
78
|
+
id: string;
|
|
79
|
+
title: string;
|
|
80
|
+
kind?: string;
|
|
81
|
+
}): AIWorkContextItem {
|
|
82
|
+
return {
|
|
83
|
+
type: "page-element",
|
|
84
|
+
id,
|
|
85
|
+
title,
|
|
86
|
+
...(kind ? { kind } : {}),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const isFormContext = (item: AIWorkContextItem) => {
|
|
91
|
+
if (item.kind === "form") return true;
|
|
92
|
+
if (!item.content || typeof item.content !== "object") return false;
|
|
93
|
+
const content = item.content as Record<string, unknown>;
|
|
94
|
+
return typeof content.form === "string" && Array.isArray(content.fields);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export function getAIWorkContextRequiredTools(items: AIWorkContextItem[]) {
|
|
98
|
+
return items.some(isFormContext) ? ["formFiller"] : [];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function getAIWorkContextToolScope(items: AIWorkContextItem[]) {
|
|
102
|
+
const frontendToolIds = new Set<string>();
|
|
103
|
+
const formIds = new Set<string>();
|
|
104
|
+
for (const item of items) {
|
|
105
|
+
if (Array.isArray(item.frontendTools)) {
|
|
106
|
+
for (const tool of item.frontendTools) {
|
|
107
|
+
if (
|
|
108
|
+
tool &&
|
|
109
|
+
typeof tool === "object" &&
|
|
110
|
+
typeof (tool as { id?: unknown }).id === "string"
|
|
111
|
+
) {
|
|
112
|
+
frontendToolIds.add((tool as { id: string }).id);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (isFormContext(item)) {
|
|
117
|
+
const form = (item.content as { form?: unknown } | undefined)?.form;
|
|
118
|
+
if (typeof form === "string" && form) formIds.add(form);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
allowedFrontendToolIds: [...frontendToolIds],
|
|
123
|
+
allowedFormIds: [...formIds],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function mergeAIRequiredTools(
|
|
128
|
+
skillSettings: AIEmployeeTask["skillSettings"],
|
|
129
|
+
requiredTools: string[]
|
|
130
|
+
): AIEmployeeTask["skillSettings"] {
|
|
131
|
+
if (!requiredTools.length) return skillSettings;
|
|
132
|
+
return {
|
|
133
|
+
...skillSettings,
|
|
134
|
+
tools: [...new Set([...(skillSettings?.tools ?? []), ...requiredTools])],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export type StreamCoalescerOptions<Key, Value> = {
|
|
2
|
+
interval: number;
|
|
3
|
+
maxSize: number;
|
|
4
|
+
getSize: (value: Value) => number;
|
|
5
|
+
merge: (current: Value, incoming: Value) => Value;
|
|
6
|
+
onFlush: (key: Key, value: Value) => void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type PendingValue<Value> = {
|
|
10
|
+
value: Value;
|
|
11
|
+
timer: ReturnType<typeof setTimeout>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export class StreamCoalescer<Key, Value> {
|
|
15
|
+
private readonly pending = new Map<Key, PendingValue<Value>>();
|
|
16
|
+
|
|
17
|
+
constructor(private readonly options: StreamCoalescerOptions<Key, Value>) {}
|
|
18
|
+
|
|
19
|
+
push(key: Key, value: Value) {
|
|
20
|
+
const existing = this.pending.get(key);
|
|
21
|
+
if (existing) {
|
|
22
|
+
existing.value = this.options.merge(existing.value, value);
|
|
23
|
+
if (this.options.getSize(existing.value) >= this.options.maxSize) {
|
|
24
|
+
this.flush(key);
|
|
25
|
+
}
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const timer = setTimeout(() => this.flush(key), this.options.interval);
|
|
30
|
+
this.pending.set(key, { value, timer });
|
|
31
|
+
if (this.options.getSize(value) >= this.options.maxSize) {
|
|
32
|
+
this.flush(key);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
has(key: Key) {
|
|
37
|
+
return this.pending.has(key);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
flush(key: Key) {
|
|
41
|
+
const pending = this.pending.get(key);
|
|
42
|
+
if (!pending) return;
|
|
43
|
+
clearTimeout(pending.timer);
|
|
44
|
+
this.pending.delete(key);
|
|
45
|
+
this.options.onFlush(key, pending.value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
flushAll() {
|
|
49
|
+
for (const key of [...this.pending.keys()]) {
|
|
50
|
+
this.flush(key);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
clear() {
|
|
55
|
+
for (const pending of this.pending.values()) {
|
|
56
|
+
clearTimeout(pending.timer);
|
|
57
|
+
}
|
|
58
|
+
this.pending.clear();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { NocoBaseStreamEvent } from "./stream-parser";
|
|
2
|
+
|
|
3
|
+
export type NocoBaseToolCall = {
|
|
4
|
+
id?: string;
|
|
5
|
+
index?: number;
|
|
6
|
+
name?: string;
|
|
7
|
+
args?: unknown;
|
|
8
|
+
input?: unknown;
|
|
9
|
+
output?: unknown;
|
|
10
|
+
content?: unknown;
|
|
11
|
+
status?: string;
|
|
12
|
+
invokeStatus?: string;
|
|
13
|
+
invokeStartTime?: unknown;
|
|
14
|
+
invokeEndTime?: unknown;
|
|
15
|
+
messageId?: string;
|
|
16
|
+
willInterrupt?: boolean;
|
|
17
|
+
auto?: boolean;
|
|
18
|
+
selectedSuggestion?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
22
|
+
!!value && typeof value === "object" && !Array.isArray(value);
|
|
23
|
+
|
|
24
|
+
export const toolCallsFromEvent = (event: NocoBaseStreamEvent) => {
|
|
25
|
+
if (Array.isArray(event.body)) return event.body as NocoBaseToolCall[];
|
|
26
|
+
if (!isRecord(event.body) || !Array.isArray(event.body.toolCalls)) return [];
|
|
27
|
+
return event.body.toolCalls as NocoBaseToolCall[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const parseToolInput = (input: unknown) => {
|
|
31
|
+
if (typeof input !== "string") return input;
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(input) as unknown;
|
|
34
|
+
} catch {
|
|
35
|
+
return input;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const completedToolStatus = new Set(["success", "done", "confirmed"]);
|
|
40
|
+
const failedToolStatus = new Set(["error", "failed", "rejected"]);
|
|
41
|
+
const pendingInvokeStatus = new Set([
|
|
42
|
+
"interrupted",
|
|
43
|
+
"waiting",
|
|
44
|
+
"pending",
|
|
45
|
+
"running",
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
export const getToolCallState = (toolCall: NocoBaseToolCall) => {
|
|
49
|
+
const invokeStatus = String(toolCall.invokeStatus ?? "").toLowerCase();
|
|
50
|
+
const resultStatus = String(toolCall.status ?? "").toLowerCase();
|
|
51
|
+
const failed =
|
|
52
|
+
failedToolStatus.has(invokeStatus) || failedToolStatus.has(resultStatus);
|
|
53
|
+
const completed =
|
|
54
|
+
!failed &&
|
|
55
|
+
(completedToolStatus.has(invokeStatus) ||
|
|
56
|
+
(!invokeStatus && completedToolStatus.has(resultStatus)));
|
|
57
|
+
return {
|
|
58
|
+
invokeStatus,
|
|
59
|
+
resultStatus,
|
|
60
|
+
failed,
|
|
61
|
+
completed,
|
|
62
|
+
pending: !failed && !completed && pendingInvokeStatus.has(invokeStatus),
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const getToolProviderMetadata = (toolCall: NocoBaseToolCall) => {
|
|
67
|
+
const { invokeStatus, resultStatus } = getToolCallState(toolCall);
|
|
68
|
+
return {
|
|
69
|
+
nocobase: {
|
|
70
|
+
requiresApproval:
|
|
71
|
+
(invokeStatus === "interrupted" ||
|
|
72
|
+
(!invokeStatus && toolCall.willInterrupt === true)) &&
|
|
73
|
+
toolCall.auto !== true,
|
|
74
|
+
autoApprove: invokeStatus === "interrupted" && toolCall.auto === true,
|
|
75
|
+
invokeStatus,
|
|
76
|
+
...(typeof toolCall.selectedSuggestion === "string"
|
|
77
|
+
? { selectedSuggestion: toolCall.selectedSuggestion }
|
|
78
|
+
: {}),
|
|
79
|
+
status: resultStatus,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createParser,
|
|
3
|
+
type EventSourceMessage,
|
|
4
|
+
type ParseError,
|
|
5
|
+
} from "eventsource-parser";
|
|
6
|
+
|
|
7
|
+
export type NocoBaseStreamEvent = {
|
|
8
|
+
type: string;
|
|
9
|
+
body?: unknown;
|
|
10
|
+
sessionId?: string;
|
|
11
|
+
from?: string;
|
|
12
|
+
username?: string;
|
|
13
|
+
errorName?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const MAX_SSE_BUFFER_SIZE = 16 * 1024 * 1024;
|
|
17
|
+
|
|
18
|
+
export async function* parseNocoBaseSSE(
|
|
19
|
+
stream: ReadableStream<Uint8Array>
|
|
20
|
+
): AsyncGenerator<NocoBaseStreamEvent> {
|
|
21
|
+
const reader = stream.getReader();
|
|
22
|
+
const decoder = new TextDecoder();
|
|
23
|
+
const events: EventSourceMessage[] = [];
|
|
24
|
+
let parseError: ParseError | undefined;
|
|
25
|
+
const parser = createParser({
|
|
26
|
+
maxBufferSize: MAX_SSE_BUFFER_SIZE,
|
|
27
|
+
onEvent: (event) => events.push(event),
|
|
28
|
+
onError: (error) => {
|
|
29
|
+
parseError = error;
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const drainEvents = function* () {
|
|
34
|
+
while (events.length) {
|
|
35
|
+
const event = events.shift();
|
|
36
|
+
if (!event) continue;
|
|
37
|
+
const payload = event.data.trim();
|
|
38
|
+
if (!payload || payload === "[DONE]") continue;
|
|
39
|
+
yield JSON.parse(payload) as NocoBaseStreamEvent;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
while (true) {
|
|
45
|
+
const { done, value } = await reader.read();
|
|
46
|
+
if (value) parser.feed(decoder.decode(value, { stream: true }));
|
|
47
|
+
if (parseError) throw parseError;
|
|
48
|
+
yield* drainEvents();
|
|
49
|
+
if (!done) continue;
|
|
50
|
+
|
|
51
|
+
const remainder = decoder.decode();
|
|
52
|
+
if (remainder) parser.feed(remainder);
|
|
53
|
+
parser.reset({ consume: true });
|
|
54
|
+
if (parseError) throw parseError;
|
|
55
|
+
yield* drainEvents();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
} finally {
|
|
59
|
+
reader.releaseLock();
|
|
60
|
+
}
|
|
61
|
+
}
|