@nocobase/portal-template-default 1.0.0 → 1.0.1
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 +70 -0
- package/components.json +3 -1
- package/package.json +19 -5
- package/registry/nocobase-acl/README.md +19 -0
- package/registry/nocobase-acl/components/acl-boundary.tsx +106 -0
- package/registry/nocobase-acl/components/index.ts +3 -0
- package/registry/nocobase-acl/components/role-options.ts +29 -0
- package/registry/nocobase-acl/components/role-switcher.tsx +130 -0
- package/registry/nocobase-acl/demo/boundary-api.tsx +115 -0
- package/registry/nocobase-acl/demo/components.tsx +210 -0
- package/registry/nocobase-acl/demo/index.tsx +474 -0
- package/registry/nocobase-acl/demo/lazy-route.tsx +15 -0
- package/registry/nocobase-acl/demo/prompt-generator.tsx +115 -0
- package/registry/nocobase-acl/demo/role-switcher-prompt-generator.tsx +179 -0
- package/registry/nocobase-acl/demo/scenario-section.tsx +37 -0
- package/registry/nocobase-acl/extension.tsx +71 -0
- package/registry/nocobase-acl/index.ts +1 -0
- package/registry/nocobase-acl/tests/acl-regression.mjs +313 -0
- package/registry/nocobase-ai/README.md +70 -0
- package/registry/nocobase-ai/adapters/react-hook-form.ts +20 -0
- package/registry/nocobase-ai/assets/nocobase-ai-chat.svg +50 -0
- package/registry/nocobase-ai/components/ai-root-provider.tsx +32 -0
- package/registry/nocobase-ai/components/chat/ai-employee-avatar.tsx +31 -0
- package/registry/nocobase-ai/components/chat/chat-attachment.tsx +92 -0
- package/registry/nocobase-ai/components/chat/chat-compact.tsx +90 -0
- package/registry/nocobase-ai/components/chat/chat-composer.tsx +429 -0
- package/registry/nocobase-ai/components/chat/chat-empty-state.tsx +51 -0
- package/registry/nocobase-ai/components/chat/chat-header.tsx +88 -0
- package/registry/nocobase-ai/components/chat/chat-history-dialog.tsx +77 -0
- package/registry/nocobase-ai/components/chat/chat-message.tsx +252 -0
- package/registry/nocobase-ai/components/chat/chat-messages.tsx +174 -0
- package/registry/nocobase-ai/components/chat/chat-window.tsx +118 -0
- package/registry/nocobase-ai/components/chat/conversation-list.tsx +345 -0
- package/registry/nocobase-ai/components/chat/markdown-message.tsx +165 -0
- package/registry/nocobase-ai/components/chat/model-select-options.tsx +30 -0
- package/registry/nocobase-ai/components/chat/reasoning-panel.tsx +29 -0
- package/registry/nocobase-ai/components/chat/sub-agent-conversation.tsx +142 -0
- package/registry/nocobase-ai/components/chat/tool-call-card.tsx +316 -0
- package/registry/nocobase-ai/components/chat/user-prompt-editor.tsx +99 -0
- package/registry/nocobase-ai/components/chat/work-context-chip.tsx +114 -0
- package/registry/nocobase-ai/components/index.ts +59 -0
- package/registry/nocobase-ai/components/page-elements/ai-form.tsx +44 -0
- package/registry/nocobase-ai/components/page-elements/page-element-provider.tsx +469 -0
- package/registry/nocobase-ai/components/surfaces/chat-dialog.tsx +31 -0
- package/registry/nocobase-ai/components/surfaces/chat-inline.tsx +18 -0
- package/registry/nocobase-ai/components/surfaces/chat-page.tsx +18 -0
- package/registry/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +54 -0
- package/registry/nocobase-ai/components/surfaces/chat-side-panel.tsx +74 -0
- package/registry/nocobase-ai/components/surfaces/chat-surface-actions.tsx +54 -0
- package/registry/nocobase-ai/components/tools/builtin-tool-renderers.tsx +31 -0
- package/registry/nocobase-ai/components/tools/business-report-dialog.tsx +367 -0
- package/registry/nocobase-ai/components/tools/business-report-renderer.tsx +122 -0
- package/registry/nocobase-ai/components/tools/business-report-utils.tsx +282 -0
- package/registry/nocobase-ai/components/tools/chart-renderer.tsx +110 -0
- package/registry/nocobase-ai/components/tools/echarts-preview.tsx +62 -0
- package/registry/nocobase-ai/components/tools/echarts-runtime-advanced.ts +18 -0
- package/registry/nocobase-ai/components/tools/echarts-runtime-common.ts +20 -0
- package/registry/nocobase-ai/components/tools/echarts-runtime-components.ts +34 -0
- package/registry/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +24 -0
- package/registry/nocobase-ai/components/tools/echarts-runtime.ts +138 -0
- package/registry/nocobase-ai/components/tools/sub-agent-renderer.tsx +67 -0
- package/registry/nocobase-ai/components/tools/suggestions-renderer.tsx +59 -0
- package/registry/nocobase-ai/components/tools/tool-renderer-provider.tsx +55 -0
- package/registry/nocobase-ai/components/tools/tool-renderer-utils.ts +18 -0
- package/registry/nocobase-ai/components/tools/workflow-renderer.tsx +108 -0
- package/registry/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +73 -0
- package/registry/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +149 -0
- package/registry/nocobase-ai/demo/configuration-gate.tsx +52 -0
- package/registry/nocobase-ai/demo/container-showcase.tsx +210 -0
- package/registry/nocobase-ai/demo/floating.tsx +134 -0
- package/registry/nocobase-ai/demo/index.tsx +346 -0
- package/registry/nocobase-ai/demo/interaction-showcase.tsx +151 -0
- package/registry/nocobase-ai/demo/page-context-prompt-generator.tsx +355 -0
- package/registry/nocobase-ai/demo/page-context.tsx +884 -0
- package/registry/nocobase-ai/demo/page-element-showcase.tsx +197 -0
- package/registry/nocobase-ai/demo/prompt-card.tsx +41 -0
- package/registry/nocobase-ai/demo/prompt-generator.tsx +452 -0
- package/registry/nocobase-ai/demo/shortcut.tsx +1207 -0
- package/registry/nocobase-ai/demo/tool-cards.tsx +530 -0
- package/registry/nocobase-ai/extension.tsx +163 -0
- package/registry/nocobase-ai/global-ai-chat.tsx +213 -0
- package/registry/nocobase-ai/index.ts +3 -0
- package/registry/nocobase-ai/providers/ai-provider.tsx +323 -0
- package/registry/nocobase-ai/providers/avatars.ts +817 -0
- package/registry/nocobase-ai/providers/chat-context.tsx +109 -0
- package/registry/nocobase-ai/providers/chat-controller.ts +84 -0
- package/registry/nocobase-ai/providers/chat-message-utils.ts +198 -0
- package/registry/nocobase-ai/providers/chat-provider.tsx +871 -0
- package/registry/nocobase-ai/providers/chat-reducer.ts +147 -0
- package/registry/nocobase-ai/providers/chat-task-utils.ts +105 -0
- package/registry/nocobase-ai/providers/chat-transport.ts +184 -0
- package/registry/nocobase-ai/providers/form-registry.tsx +271 -0
- package/registry/nocobase-ai/providers/frontend-tool-registry.tsx +231 -0
- package/registry/nocobase-ai/providers/index.ts +61 -0
- package/registry/nocobase-ai/providers/model.ts +32 -0
- package/registry/nocobase-ai/providers/page-context.tsx +136 -0
- package/registry/nocobase-ai/providers/stream-coalescer.ts +60 -0
- package/registry/nocobase-ai/providers/stream-event-utils.ts +82 -0
- package/registry/nocobase-ai/providers/stream-parser.ts +61 -0
- package/registry/nocobase-ai/providers/sub-agent-stream.ts +316 -0
- package/registry/nocobase-ai/providers/types.ts +183 -0
- package/registry/nocobase-ai/providers/ui-message-stream.ts +463 -0
- package/registry/nocobase-ai/providers/use-automatic-tool-approval.ts +94 -0
- package/registry/nocobase-ai/providers/use-chat-attachments.ts +206 -0
- package/registry/nocobase-ai/providers/use-chat-message-actions.ts +554 -0
- package/registry/nocobase-ai/providers/use-chat-runtime.ts +210 -0
- package/registry/nocobase-ai/providers/use-chat-work-context.ts +93 -0
- package/registry/nocobase-ai/providers/use-conversation-catalog.ts +108 -0
- package/registry/nocobase-ai/providers/use-conversation-history.ts +178 -0
- package/registry/nocobase-ai/services/index.ts +7 -0
- package/registry/nocobase-ai/services/nocobase-ai-service.ts +454 -0
- package/registry/nocobase-ai/services/types.ts +82 -0
- package/registry/nocobase-ai/tests/business-report-regression.mjs +70 -0
- package/registry/nocobase-ai/tests/form-filler-regression.mjs +129 -0
- package/registry/nocobase-ai/tests/frontend-tool-regression.mjs +128 -0
- package/registry/nocobase-ai/tests/page-context-regression.mjs +48 -0
- package/registry/nocobase-ai/tests/stream-regression.mjs +207 -0
- package/registry.config.json +143 -0
- package/scripts/registry.mjs +161 -0
- package/src/App.css +95 -0
- package/src/App.tsx +127 -50
- package/src/app/extension.ts +9 -0
- package/src/app/extensions.tsx +28 -0
- package/src/components/access-control/access-denied.tsx +30 -0
- package/src/components/access-control/acl-bootstrap.tsx +9 -0
- package/src/components/access-control/acl-gate.tsx +42 -0
- package/src/components/access-control/navigate-to-accessible-resource.tsx +17 -0
- package/src/components/access-control/resource-access-guard.tsx +21 -0
- package/src/components/access-control/use-acl-runtime.ts +28 -0
- package/src/components/app-shell/header.tsx +4 -1
- package/src/components/app-shell/sidebar.tsx +34 -4
- package/src/lib/nocobase/acl/action.ts +147 -0
- package/src/lib/nocobase/acl/data-source.ts +24 -0
- package/src/lib/nocobase/acl/index.ts +6 -0
- package/src/lib/nocobase/acl/menu.ts +41 -0
- package/src/lib/nocobase/acl/record-permissions.ts +111 -0
- package/src/lib/nocobase/acl/store.ts +182 -0
- package/src/lib/nocobase/acl/types.ts +96 -0
- package/src/lib/nocobase/client.ts +262 -0
- package/src/lib/nocobase/error.ts +32 -0
- package/src/providers/access-control.ts +34 -0
- package/src/providers/auth.ts +74 -60
- package/src/providers/constants.ts +2 -37
- package/src/providers/data.ts +242 -85
- package/vite.config.ts +17 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { createServer } from "vite";
|
|
3
|
+
|
|
4
|
+
const server = await createServer({
|
|
5
|
+
appType: "custom",
|
|
6
|
+
logLevel: "silent",
|
|
7
|
+
server: { middlewareMode: true },
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const { AIFormRegistry, createFormFillerInvoker } =
|
|
12
|
+
await server.ssrLoadModule(
|
|
13
|
+
"/registry/nocobase-ai/providers/form-registry.tsx"
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const registry = new AIFormRegistry();
|
|
17
|
+
const applied = [];
|
|
18
|
+
const unregister = registry.register({
|
|
19
|
+
id: "lead-form",
|
|
20
|
+
title: "Lead form",
|
|
21
|
+
fields: [
|
|
22
|
+
{ name: "company", type: "string" },
|
|
23
|
+
{
|
|
24
|
+
name: "priority",
|
|
25
|
+
type: "string",
|
|
26
|
+
enum: ["normal", "high"],
|
|
27
|
+
},
|
|
28
|
+
{ name: "owner", type: "string", readonly: true },
|
|
29
|
+
],
|
|
30
|
+
getValues: () => ({}),
|
|
31
|
+
setValues: (values) => applied.push(values),
|
|
32
|
+
});
|
|
33
|
+
const invoke = createFormFillerInvoker(registry);
|
|
34
|
+
|
|
35
|
+
const allowedContext = { allowedFormIds: ["lead-form"] };
|
|
36
|
+
assert.deepEqual(
|
|
37
|
+
await invoke(
|
|
38
|
+
{
|
|
39
|
+
form: "lead-form",
|
|
40
|
+
data: { company: "Acme", priority: "high" },
|
|
41
|
+
},
|
|
42
|
+
allowedContext
|
|
43
|
+
),
|
|
44
|
+
{
|
|
45
|
+
status: "success",
|
|
46
|
+
content:
|
|
47
|
+
'Filled "Lead form". Please review the values and submit the form manually.',
|
|
48
|
+
appliedFields: ["company", "priority"],
|
|
49
|
+
skippedFields: [],
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
assert.deepEqual(applied, [{ company: "Acme", priority: "high" }]);
|
|
53
|
+
|
|
54
|
+
assert.deepEqual(
|
|
55
|
+
await invoke(
|
|
56
|
+
{
|
|
57
|
+
form: "lead-form",
|
|
58
|
+
data: { company: 42, owner: "Ada", unknown: true },
|
|
59
|
+
},
|
|
60
|
+
allowedContext
|
|
61
|
+
),
|
|
62
|
+
{
|
|
63
|
+
status: "error",
|
|
64
|
+
content: 'No valid editable fields were provided for "Lead form".',
|
|
65
|
+
appliedFields: [],
|
|
66
|
+
skippedFields: [
|
|
67
|
+
{
|
|
68
|
+
name: "company",
|
|
69
|
+
reason: "invalid",
|
|
70
|
+
message: "Expected a string.",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "owner",
|
|
74
|
+
reason: "readonly",
|
|
75
|
+
message: "This field is read-only.",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "unknown",
|
|
79
|
+
reason: "undeclared",
|
|
80
|
+
message: "This field is not declared by the target form.",
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
assert.throws(
|
|
87
|
+
() =>
|
|
88
|
+
registry.register({
|
|
89
|
+
id: "lead-form",
|
|
90
|
+
title: "Duplicate lead form",
|
|
91
|
+
fields: [],
|
|
92
|
+
getValues: () => ({}),
|
|
93
|
+
setValues: () => undefined,
|
|
94
|
+
}),
|
|
95
|
+
/already registered/
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
unregister();
|
|
99
|
+
assert.deepEqual(
|
|
100
|
+
await invoke(
|
|
101
|
+
{ form: "lead-form", data: { company: "Acme" } },
|
|
102
|
+
allowedContext
|
|
103
|
+
),
|
|
104
|
+
{
|
|
105
|
+
status: "error",
|
|
106
|
+
content: 'The target form "lead-form" is not available on this page.',
|
|
107
|
+
appliedFields: [],
|
|
108
|
+
skippedFields: [],
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
assert.deepEqual(
|
|
113
|
+
await invoke(
|
|
114
|
+
{ form: "lead-form", data: { company: "Acme" } },
|
|
115
|
+
{ allowedFormIds: [] }
|
|
116
|
+
),
|
|
117
|
+
{
|
|
118
|
+
status: "error",
|
|
119
|
+
content:
|
|
120
|
+
'The target form "lead-form" is not available in this conversation context.',
|
|
121
|
+
appliedFields: [],
|
|
122
|
+
skippedFields: [],
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
console.log("AI Form filler regression tests passed");
|
|
127
|
+
} finally {
|
|
128
|
+
await server.close();
|
|
129
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { createServer } from "vite";
|
|
3
|
+
|
|
4
|
+
const server = await createServer({
|
|
5
|
+
appType: "custom",
|
|
6
|
+
logLevel: "silent",
|
|
7
|
+
server: { middlewareMode: true },
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const { AIFrontendToolRegistry, createFrontendToolInvokers } =
|
|
12
|
+
await server.ssrLoadModule(
|
|
13
|
+
"/registry/nocobase-ai/providers/frontend-tool-registry.tsx"
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const registry = new AIFrontendToolRegistry();
|
|
17
|
+
const calls = [];
|
|
18
|
+
const unregister = registry.register("quote-card", {
|
|
19
|
+
name: "update_quote_discount",
|
|
20
|
+
description: "Update the quote discount preview.",
|
|
21
|
+
permission: "ASK",
|
|
22
|
+
inputSchema: {
|
|
23
|
+
type: "object",
|
|
24
|
+
properties: { discountPercent: { type: "number" } },
|
|
25
|
+
},
|
|
26
|
+
execute: (args) => {
|
|
27
|
+
calls.push(args);
|
|
28
|
+
return { updated: true };
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
const [manifest] = registry.list("quote-card");
|
|
32
|
+
assert.deepEqual(manifest, {
|
|
33
|
+
id: "quote-card:update_quote_discount",
|
|
34
|
+
blockUid: "quote-card",
|
|
35
|
+
name: "update_quote_discount",
|
|
36
|
+
title: undefined,
|
|
37
|
+
description: "Update the quote discount preview.",
|
|
38
|
+
permission: "ASK",
|
|
39
|
+
inputSchema: {
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: { discountPercent: { type: "number" } },
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const invokers = createFrontendToolInvokers(registry);
|
|
46
|
+
const allowedContext = { allowedFrontendToolIds: [manifest.id] };
|
|
47
|
+
assert.deepEqual(
|
|
48
|
+
await invokers.loadFrontendTool({ toolId: manifest.id }, allowedContext),
|
|
49
|
+
manifest
|
|
50
|
+
);
|
|
51
|
+
assert.deepEqual(
|
|
52
|
+
await invokers.executeFrontendTool(
|
|
53
|
+
{ toolId: manifest.id, args: { discountPercent: 12 } },
|
|
54
|
+
allowedContext
|
|
55
|
+
),
|
|
56
|
+
{ updated: true }
|
|
57
|
+
);
|
|
58
|
+
assert.deepEqual(calls, [{ discountPercent: 12 }]);
|
|
59
|
+
|
|
60
|
+
assert.throws(
|
|
61
|
+
() =>
|
|
62
|
+
registry.register("quote-card", {
|
|
63
|
+
name: "update_quote_discount",
|
|
64
|
+
description: "Duplicate Tool",
|
|
65
|
+
execute: () => undefined,
|
|
66
|
+
}),
|
|
67
|
+
/already registered/
|
|
68
|
+
);
|
|
69
|
+
assert.throws(
|
|
70
|
+
() =>
|
|
71
|
+
registry.register("quote-card", {
|
|
72
|
+
name: "invalid_schema",
|
|
73
|
+
description: "Invalid schema",
|
|
74
|
+
inputSchema: [],
|
|
75
|
+
execute: () => undefined,
|
|
76
|
+
}),
|
|
77
|
+
/inputSchema must be an object/
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const unregisterInvalidResult = registry.register("quote-card", {
|
|
81
|
+
name: "invalid_result",
|
|
82
|
+
description: "Return an invalid result.",
|
|
83
|
+
execute: () => ({ value: 1n }),
|
|
84
|
+
});
|
|
85
|
+
assert.deepEqual(
|
|
86
|
+
await invokers.executeFrontendTool(
|
|
87
|
+
{ toolId: "quote-card:invalid_result", args: {} },
|
|
88
|
+
{
|
|
89
|
+
allowedFrontendToolIds: ["quote-card:invalid_result"],
|
|
90
|
+
}
|
|
91
|
+
),
|
|
92
|
+
{
|
|
93
|
+
status: "error",
|
|
94
|
+
content:
|
|
95
|
+
'Frontend Tool "quote-card:invalid_result" returned a non-serializable result',
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
unregisterInvalidResult();
|
|
99
|
+
|
|
100
|
+
unregister();
|
|
101
|
+
assert.deepEqual(
|
|
102
|
+
await invokers.executeFrontendTool(
|
|
103
|
+
{ toolId: manifest.id, args: {} },
|
|
104
|
+
allowedContext
|
|
105
|
+
),
|
|
106
|
+
{
|
|
107
|
+
status: "error",
|
|
108
|
+
content:
|
|
109
|
+
'Frontend Tool "quote-card:update_quote_discount" is unavailable',
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
assert.deepEqual(
|
|
114
|
+
await invokers.executeFrontendTool(
|
|
115
|
+
{ toolId: manifest.id, args: {} },
|
|
116
|
+
{ allowedFrontendToolIds: [] }
|
|
117
|
+
),
|
|
118
|
+
{
|
|
119
|
+
status: "error",
|
|
120
|
+
content:
|
|
121
|
+
'Frontend Tool "quote-card:update_quote_discount" is not available in this conversation context',
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
console.log("AI frontend Tool regression tests passed");
|
|
126
|
+
} finally {
|
|
127
|
+
await server.close();
|
|
128
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { createServer } from "vite";
|
|
3
|
+
|
|
4
|
+
const server = await createServer({
|
|
5
|
+
appType: "custom",
|
|
6
|
+
logLevel: "silent",
|
|
7
|
+
server: { middlewareMode: true },
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const {
|
|
12
|
+
createAIPageContextReference,
|
|
13
|
+
getAIWorkContextRequiredTools,
|
|
14
|
+
mergeAIRequiredTools,
|
|
15
|
+
} = await server.ssrLoadModule(
|
|
16
|
+
"/registry/nocobase-ai/providers/page-context.tsx"
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const formContext = createAIPageContextReference({
|
|
20
|
+
id: "lead-form",
|
|
21
|
+
title: "Lead form",
|
|
22
|
+
kind: "form",
|
|
23
|
+
});
|
|
24
|
+
assert.deepEqual(formContext, {
|
|
25
|
+
type: "page-element",
|
|
26
|
+
id: "lead-form",
|
|
27
|
+
title: "Lead form",
|
|
28
|
+
kind: "form",
|
|
29
|
+
});
|
|
30
|
+
assert.deepEqual(getAIWorkContextRequiredTools([formContext]), [
|
|
31
|
+
"formFiller",
|
|
32
|
+
]);
|
|
33
|
+
assert.deepEqual(
|
|
34
|
+
mergeAIRequiredTools(
|
|
35
|
+
{ skills: ["lead-review"], tools: ["inspect-record"] },
|
|
36
|
+
["formFiller", "inspect-record"]
|
|
37
|
+
),
|
|
38
|
+
{
|
|
39
|
+
skills: ["lead-review"],
|
|
40
|
+
tools: ["inspect-record", "formFiller"],
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
assert.equal(mergeAIRequiredTools(undefined, []), undefined);
|
|
44
|
+
|
|
45
|
+
console.log("AI page context regression tests passed");
|
|
46
|
+
} finally {
|
|
47
|
+
await server.close();
|
|
48
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { createServer } from "vite";
|
|
3
|
+
|
|
4
|
+
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
5
|
+
|
|
6
|
+
const encodeSSE = (events) => {
|
|
7
|
+
const encoder = new TextEncoder();
|
|
8
|
+
return new ReadableStream({
|
|
9
|
+
start(controller) {
|
|
10
|
+
controller.enqueue(
|
|
11
|
+
encoder.encode(
|
|
12
|
+
events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join("")
|
|
13
|
+
)
|
|
14
|
+
);
|
|
15
|
+
controller.close();
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const collectStream = async (stream) => {
|
|
21
|
+
const result = [];
|
|
22
|
+
for await (const chunk of stream) result.push(chunk);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const server = await createServer({
|
|
27
|
+
appType: "custom",
|
|
28
|
+
logLevel: "silent",
|
|
29
|
+
server: { middlewareMode: true },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const { StreamCoalescer } = await server.ssrLoadModule(
|
|
34
|
+
"/registry/nocobase-ai/providers/stream-coalescer.ts"
|
|
35
|
+
);
|
|
36
|
+
const { NocoBaseChatTransport } = await server.ssrLoadModule(
|
|
37
|
+
"/registry/nocobase-ai/providers/chat-transport.ts"
|
|
38
|
+
);
|
|
39
|
+
const { SubAgentStreamAccumulator } = await server.ssrLoadModule(
|
|
40
|
+
"/registry/nocobase-ai/providers/sub-agent-stream.ts"
|
|
41
|
+
);
|
|
42
|
+
const { NocoBaseClient } = await server.ssrLoadModule(
|
|
43
|
+
"/src/lib/nocobase/client.ts"
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
{
|
|
47
|
+
const client = new NocoBaseClient("http://localhost:13001/api");
|
|
48
|
+
assert.equal(
|
|
49
|
+
client.resolveUrl("/files/main/main/aiFiles/5.png?preview=1"),
|
|
50
|
+
"http://localhost:13001/files/main/main/aiFiles/5.png?preview=1"
|
|
51
|
+
);
|
|
52
|
+
assert.equal(
|
|
53
|
+
client.resolveUrl("https://cdn.example.com/file.png"),
|
|
54
|
+
"https://cdn.example.com/file.png"
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
{
|
|
59
|
+
const flushed = [];
|
|
60
|
+
const coalescer = new StreamCoalescer({
|
|
61
|
+
interval: 20,
|
|
62
|
+
maxSize: 100,
|
|
63
|
+
getSize: (value) => value.length,
|
|
64
|
+
merge: (current, incoming) => current + incoming,
|
|
65
|
+
onFlush: (key, value) => flushed.push([key, value]),
|
|
66
|
+
});
|
|
67
|
+
coalescer.push("message", "hello ");
|
|
68
|
+
coalescer.push("message", "world");
|
|
69
|
+
await wait(35);
|
|
70
|
+
assert.deepEqual(flushed, [["message", "hello world"]]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
{
|
|
74
|
+
const seedMessage = {
|
|
75
|
+
id: "assistant-existing",
|
|
76
|
+
role: "assistant",
|
|
77
|
+
parts: [
|
|
78
|
+
{
|
|
79
|
+
type: "data-subAgent",
|
|
80
|
+
id: "sub-1",
|
|
81
|
+
data: {
|
|
82
|
+
sessionId: "sub-1",
|
|
83
|
+
username: "viz",
|
|
84
|
+
status: "pending",
|
|
85
|
+
messages: [
|
|
86
|
+
{
|
|
87
|
+
id: "sub-message",
|
|
88
|
+
role: "assistant",
|
|
89
|
+
parts: [{ type: "text", text: "saved ", state: "done" }],
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
const accumulator = new SubAgentStreamAccumulator(seedMessage);
|
|
97
|
+
const [chunk] = accumulator.process({
|
|
98
|
+
type: "content",
|
|
99
|
+
body: "tail",
|
|
100
|
+
from: "sub-agent",
|
|
101
|
+
sessionId: "sub-1",
|
|
102
|
+
username: "viz",
|
|
103
|
+
});
|
|
104
|
+
assert.equal(chunk.data.messages.length, 1);
|
|
105
|
+
assert.equal(chunk.data.messages[0].parts[0].text, "saved tail");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
{
|
|
109
|
+
const toolCallId = "report-tool";
|
|
110
|
+
const events = [
|
|
111
|
+
...Array.from({ length: 100 }, (_, index) => ({
|
|
112
|
+
type: "tool_call_chunks",
|
|
113
|
+
from: "main-agent",
|
|
114
|
+
sessionId: "conversation-1",
|
|
115
|
+
body: [
|
|
116
|
+
index === 0
|
|
117
|
+
? {
|
|
118
|
+
id: toolCallId,
|
|
119
|
+
name: "businessReportGenerator",
|
|
120
|
+
args: "{",
|
|
121
|
+
}
|
|
122
|
+
: { id: toolCallId, args: "x" },
|
|
123
|
+
],
|
|
124
|
+
})),
|
|
125
|
+
{ type: "stream_end", from: "main-agent", sessionId: "conversation-1" },
|
|
126
|
+
];
|
|
127
|
+
const service = {
|
|
128
|
+
resumeConversationStream: async () => encodeSSE(events),
|
|
129
|
+
};
|
|
130
|
+
const transport = new NocoBaseChatTransport({
|
|
131
|
+
service,
|
|
132
|
+
getContext: () => ({
|
|
133
|
+
sessionId: "conversation-1",
|
|
134
|
+
employee: { username: "atlas", nickname: "Atlas" },
|
|
135
|
+
model: { value: "test", label: "Test" },
|
|
136
|
+
}),
|
|
137
|
+
});
|
|
138
|
+
transport.prepareConversationResume([
|
|
139
|
+
{ id: "assistant-existing", role: "assistant", parts: [] },
|
|
140
|
+
]);
|
|
141
|
+
const chunks = await collectStream(await transport.reconnectToStream());
|
|
142
|
+
const start = chunks.find((chunk) => chunk.type === "start");
|
|
143
|
+
const toolDeltas = chunks.filter(
|
|
144
|
+
(chunk) => chunk.type === "tool-input-delta"
|
|
145
|
+
);
|
|
146
|
+
assert.equal(start.messageId, undefined);
|
|
147
|
+
assert.ok(toolDeltas.length < 10, `received ${toolDeltas.length} deltas`);
|
|
148
|
+
assert.equal(
|
|
149
|
+
toolDeltas.reduce((size, chunk) => size + chunk.inputTextDelta.length, 0),
|
|
150
|
+
100
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
{
|
|
155
|
+
let request;
|
|
156
|
+
const service = {
|
|
157
|
+
sendMessagesStream: async (input) => {
|
|
158
|
+
request = input;
|
|
159
|
+
return encodeSSE([
|
|
160
|
+
{ type: "content", body: "ok", from: "main-agent" },
|
|
161
|
+
{ type: "stream_end", from: "main-agent" },
|
|
162
|
+
]);
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
const transport = new NocoBaseChatTransport({
|
|
166
|
+
service,
|
|
167
|
+
getContext: () => ({
|
|
168
|
+
sessionId: "conversation-context",
|
|
169
|
+
employee: { username: "mira", nickname: "Mira" },
|
|
170
|
+
model: { value: "test", label: "Test" },
|
|
171
|
+
}),
|
|
172
|
+
});
|
|
173
|
+
await collectStream(
|
|
174
|
+
await transport.sendMessages({
|
|
175
|
+
messages: [
|
|
176
|
+
{
|
|
177
|
+
id: "user-context",
|
|
178
|
+
role: "user",
|
|
179
|
+
metadata: {
|
|
180
|
+
workContext: [
|
|
181
|
+
{
|
|
182
|
+
type: "page-element",
|
|
183
|
+
id: "customer-form",
|
|
184
|
+
title: "Customer form",
|
|
185
|
+
content: { values: { name: "Northwind" } },
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
},
|
|
189
|
+
parts: [{ type: "text", text: "Review this form" }],
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
})
|
|
193
|
+
);
|
|
194
|
+
assert.deepEqual(request.messages[0].workContext, [
|
|
195
|
+
{
|
|
196
|
+
type: "page-element",
|
|
197
|
+
id: "customer-form",
|
|
198
|
+
title: "Customer form",
|
|
199
|
+
content: { values: { name: "Northwind" } },
|
|
200
|
+
},
|
|
201
|
+
]);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
console.log("AI stream regression tests passed");
|
|
205
|
+
} finally {
|
|
206
|
+
await server.close();
|
|
207
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nocobase-ai",
|
|
3
|
+
"homepage": "https://www.nocobase.com",
|
|
4
|
+
"items": [
|
|
5
|
+
{
|
|
6
|
+
"name": "ai-runtime",
|
|
7
|
+
"type": "registry:lib",
|
|
8
|
+
"title": "NocoBase AI Runtime",
|
|
9
|
+
"description": "NocoBase AI services, providers, transports, controllers, and avatars.",
|
|
10
|
+
"dependencies": [
|
|
11
|
+
"@ai-sdk/react@2.0.0",
|
|
12
|
+
"ai@5.0.0",
|
|
13
|
+
"@dicebear/core@^9.4.3",
|
|
14
|
+
"@nocobase/ai-employee-avatars@^1.0.2",
|
|
15
|
+
"eventsource-parser@3.1.0"
|
|
16
|
+
],
|
|
17
|
+
"source": {
|
|
18
|
+
"root": "registry/nocobase-ai",
|
|
19
|
+
"target": "src/extensions/nocobase-ai",
|
|
20
|
+
"include": ["providers", "services"]
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"name": "ai-chat",
|
|
25
|
+
"type": "registry:block",
|
|
26
|
+
"title": "NocoBase AI Chat Components",
|
|
27
|
+
"description": "Reusable NocoBase AI chat window, containers, floating trigger, and employee shortcut.",
|
|
28
|
+
"dependencies": [
|
|
29
|
+
"echarts@^5.5.0",
|
|
30
|
+
"echarts-for-react@3.0.2",
|
|
31
|
+
"lucide-react@^0.487.0",
|
|
32
|
+
"next-themes@^0.4.6",
|
|
33
|
+
"react-markdown@^10.1.0",
|
|
34
|
+
"remark-gfm@^4.0.1"
|
|
35
|
+
],
|
|
36
|
+
"registryDependencies": [
|
|
37
|
+
"@nocobase/ai-runtime",
|
|
38
|
+
"badge",
|
|
39
|
+
"button",
|
|
40
|
+
"collapsible",
|
|
41
|
+
"dialog",
|
|
42
|
+
"dropdown-menu",
|
|
43
|
+
"hover-card",
|
|
44
|
+
"input",
|
|
45
|
+
"input-group",
|
|
46
|
+
"label",
|
|
47
|
+
"popover",
|
|
48
|
+
"select",
|
|
49
|
+
"tabs",
|
|
50
|
+
"textarea",
|
|
51
|
+
"tooltip"
|
|
52
|
+
],
|
|
53
|
+
"source": {
|
|
54
|
+
"root": "registry/nocobase-ai",
|
|
55
|
+
"target": "src/extensions/nocobase-ai",
|
|
56
|
+
"include": ["components", "assets", "index.ts", "README.md"]
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"name": "ai-react-hook-form",
|
|
61
|
+
"type": "registry:lib",
|
|
62
|
+
"title": "NocoBase AI React Hook Form Adapter",
|
|
63
|
+
"description": "Optional React Hook Form adapter for the built-in AI Form filler.",
|
|
64
|
+
"dependencies": ["react-hook-form@^7.57.0"],
|
|
65
|
+
"registryDependencies": ["@nocobase/ai-chat"],
|
|
66
|
+
"source": {
|
|
67
|
+
"root": "registry/nocobase-ai",
|
|
68
|
+
"target": "src/extensions/nocobase-ai",
|
|
69
|
+
"include": ["adapters/react-hook-form.ts"]
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"name": "ai",
|
|
74
|
+
"type": "registry:block",
|
|
75
|
+
"title": "NocoBase AI Components and Demo",
|
|
76
|
+
"description": "Complete NocoBase AI component runtime with Refine resources, routes, and component showcase pages.",
|
|
77
|
+
"dependencies": ["@refinedev/core@^5.0.8", "react-router@^7.0.2"],
|
|
78
|
+
"registryDependencies": [
|
|
79
|
+
"@nocobase/ai-chat",
|
|
80
|
+
"@nocobase/ai-react-hook-form",
|
|
81
|
+
"accordion",
|
|
82
|
+
"alert",
|
|
83
|
+
"badge",
|
|
84
|
+
"card",
|
|
85
|
+
"checkbox",
|
|
86
|
+
"input",
|
|
87
|
+
"switch",
|
|
88
|
+
"table",
|
|
89
|
+
"textarea"
|
|
90
|
+
],
|
|
91
|
+
"docs": "This item includes the AI extension integration and Demo pages. It is designed for a compatible starter with the AppExtension host enabled. After installation, src/extensions/nocobase-ai/extension.tsx is discovered automatically and contributes its Refine resources and routes without AI-specific App.tsx edits.",
|
|
92
|
+
"source": {
|
|
93
|
+
"root": "registry/nocobase-ai",
|
|
94
|
+
"target": "src/extensions/nocobase-ai",
|
|
95
|
+
"include": ["demo", "extension.tsx", "global-ai-chat.tsx"]
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"name": "acl-ui",
|
|
100
|
+
"type": "registry:block",
|
|
101
|
+
"title": "NocoBase ACL UI",
|
|
102
|
+
"description": "Page, region, field, and role-switching components backed by the Starter's NocoBase accessControlProvider.",
|
|
103
|
+
"dependencies": [
|
|
104
|
+
"@refinedev/core@^5.0.8",
|
|
105
|
+
"lucide-react@^0.487.0"
|
|
106
|
+
],
|
|
107
|
+
"registryDependencies": [
|
|
108
|
+
"badge",
|
|
109
|
+
"select"
|
|
110
|
+
],
|
|
111
|
+
"docs": "Requires a compatible NocoBase Starter with the built-in accessControlProvider and ACL store.",
|
|
112
|
+
"source": {
|
|
113
|
+
"root": "registry/nocobase-acl",
|
|
114
|
+
"target": "src/extensions/nocobase-acl",
|
|
115
|
+
"include": ["components", "index.ts", "README.md"]
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"name": "acl",
|
|
120
|
+
"type": "registry:block",
|
|
121
|
+
"title": "NocoBase ACL Components and Demo",
|
|
122
|
+
"description": "Complete ACL composition demo with live role switching, action matrix, multi-collection regions, and field permissions.",
|
|
123
|
+
"dependencies": [
|
|
124
|
+
"@refinedev/core@^5.0.8",
|
|
125
|
+
"react-router@^7.0.2"
|
|
126
|
+
],
|
|
127
|
+
"registryDependencies": [
|
|
128
|
+
"@nocobase/acl-ui",
|
|
129
|
+
"button",
|
|
130
|
+
"card",
|
|
131
|
+
"input",
|
|
132
|
+
"label",
|
|
133
|
+
"table",
|
|
134
|
+
"textarea"
|
|
135
|
+
],
|
|
136
|
+
"source": {
|
|
137
|
+
"root": "registry/nocobase-acl",
|
|
138
|
+
"target": "src/extensions/nocobase-acl",
|
|
139
|
+
"include": ["demo", "extension.tsx"]
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
}
|