@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.
Files changed (186) hide show
  1. package/README.MD +22 -3
  2. package/package.json +4 -2
  3. package/registry/nocobase-acl/components/role-options.ts +7 -0
  4. package/registry/nocobase-acl/components/role-switcher.tsx +9 -4
  5. package/registry/nocobase-acl/demo/boundary-api.tsx +87 -16
  6. package/registry/nocobase-acl/demo/components.tsx +32 -11
  7. package/registry/nocobase-acl/demo/index.tsx +45 -14
  8. package/registry/nocobase-acl/demo/lazy-route.tsx +2 -7
  9. package/registry/nocobase-acl/demo/prompt-generator.tsx +52 -33
  10. package/registry/nocobase-acl/demo/role-switcher-prompt-generator.tsx +28 -9
  11. package/registry/nocobase-acl/extension.tsx +1 -1
  12. package/registry/nocobase-acl/tests/acl-regression.mjs +7 -6
  13. package/registry/nocobase-ai/components/chat/chat-messages.tsx +3 -4
  14. package/registry/nocobase-ai/components/chat/conversation-list.tsx +2 -4
  15. package/registry/nocobase-ai/components/tools/business-report-dialog.tsx +16 -19
  16. package/registry/nocobase-ai/components/tools/chart-renderer.tsx +3 -14
  17. package/registry/nocobase-ai/components/tools/echarts-preview.tsx +2 -5
  18. package/registry/nocobase-ai/demo/configuration-gate.tsx +9 -12
  19. package/registry/nocobase-ai/extension.tsx +2 -7
  20. package/registry/nocobase-i18n/README.md +53 -0
  21. package/registry/nocobase-i18n/components/index.ts +1 -0
  22. package/registry/nocobase-i18n/components/language-switcher.tsx +138 -0
  23. package/registry/nocobase-i18n/demo/index.tsx +209 -0
  24. package/registry/nocobase-i18n/demo/prompt-generator.tsx +155 -0
  25. package/registry/nocobase-i18n/extension.tsx +46 -0
  26. package/registry/nocobase-i18n/i18n-provider.ts +9 -0
  27. package/registry/nocobase-i18n/index.ts +6 -0
  28. package/registry/nocobase-i18n/locale-store.ts +134 -0
  29. package/registry/nocobase-i18n/locales/en-US.ts +89 -0
  30. package/registry/nocobase-i18n/locales/zh-CN.ts +85 -0
  31. package/registry/nocobase-i18n/provider.tsx +32 -0
  32. package/registry/nocobase-i18n/runtime.ts +118 -0
  33. package/registry/nocobase-i18n/server-resources.ts +184 -0
  34. package/registry/nocobase-i18n/tests/i18n-regression.mjs +42 -0
  35. package/registry.config.json +34 -14
  36. package/scripts/registry.mjs +5 -0
  37. package/src/App.tsx +27 -86
  38. package/src/app/extension.ts +3 -1
  39. package/src/app/extensions.tsx +21 -0
  40. package/src/components/access-control/acl-gate.tsx +4 -10
  41. package/src/components/app-shell/breadcrumb.tsx +39 -3
  42. package/src/components/app-shell/document-title-handler.tsx +11 -20
  43. package/src/components/app-shell/header.tsx +18 -3
  44. package/src/components/app-shell/loading-overlay.tsx +2 -4
  45. package/src/components/app-shell/loading-state.tsx +15 -0
  46. package/src/components/app-shell/sidebar.tsx +75 -47
  47. package/src/components/data-table/data-table-pagination.tsx +27 -7
  48. package/src/components/data-table/data-table-styles.ts +57 -0
  49. package/src/components/data-table/data-table.tsx +14 -59
  50. package/src/components/resources/resource-label.ts +101 -0
  51. package/src/components/resources/views/create-view.tsx +17 -14
  52. package/src/components/resources/views/edit-view.tsx +17 -15
  53. package/src/components/resources/views/list-view.tsx +20 -10
  54. package/src/components/resources/views/show-view.tsx +20 -18
  55. package/src/extensions/nocobase-acl/README.md +19 -0
  56. package/src/extensions/nocobase-acl/components/acl-boundary.tsx +106 -0
  57. package/src/extensions/nocobase-acl/components/index.ts +3 -0
  58. package/src/extensions/nocobase-acl/components/role-options.ts +36 -0
  59. package/src/extensions/nocobase-acl/components/role-switcher.tsx +135 -0
  60. package/src/extensions/nocobase-acl/demo/boundary-api.tsx +186 -0
  61. package/src/extensions/nocobase-acl/demo/components.tsx +231 -0
  62. package/src/extensions/nocobase-acl/demo/index.tsx +505 -0
  63. package/src/extensions/nocobase-acl/demo/lazy-route.tsx +10 -0
  64. package/src/extensions/nocobase-acl/demo/prompt-generator.tsx +134 -0
  65. package/src/extensions/nocobase-acl/demo/role-switcher-prompt-generator.tsx +198 -0
  66. package/src/extensions/nocobase-acl/demo/scenario-section.tsx +37 -0
  67. package/src/extensions/nocobase-acl/extension.tsx +71 -0
  68. package/src/extensions/nocobase-acl/index.ts +1 -0
  69. package/src/extensions/nocobase-ai/README.md +70 -0
  70. package/src/extensions/nocobase-ai/adapters/react-hook-form.ts +20 -0
  71. package/src/extensions/nocobase-ai/assets/nocobase-ai-chat.svg +50 -0
  72. package/src/extensions/nocobase-ai/components/ai-root-provider.tsx +32 -0
  73. package/src/extensions/nocobase-ai/components/chat/ai-employee-avatar.tsx +31 -0
  74. package/src/extensions/nocobase-ai/components/chat/chat-attachment.tsx +92 -0
  75. package/src/extensions/nocobase-ai/components/chat/chat-compact.tsx +90 -0
  76. package/src/extensions/nocobase-ai/components/chat/chat-composer.tsx +429 -0
  77. package/src/extensions/nocobase-ai/components/chat/chat-empty-state.tsx +51 -0
  78. package/src/extensions/nocobase-ai/components/chat/chat-header.tsx +88 -0
  79. package/src/extensions/nocobase-ai/components/chat/chat-history-dialog.tsx +77 -0
  80. package/src/extensions/nocobase-ai/components/chat/chat-message.tsx +252 -0
  81. package/src/extensions/nocobase-ai/components/chat/chat-messages.tsx +173 -0
  82. package/src/extensions/nocobase-ai/components/chat/chat-window.tsx +118 -0
  83. package/src/extensions/nocobase-ai/components/chat/conversation-list.tsx +343 -0
  84. package/src/extensions/nocobase-ai/components/chat/markdown-message.tsx +165 -0
  85. package/src/extensions/nocobase-ai/components/chat/model-select-options.tsx +30 -0
  86. package/src/extensions/nocobase-ai/components/chat/reasoning-panel.tsx +29 -0
  87. package/src/extensions/nocobase-ai/components/chat/sub-agent-conversation.tsx +142 -0
  88. package/src/extensions/nocobase-ai/components/chat/tool-call-card.tsx +316 -0
  89. package/src/extensions/nocobase-ai/components/chat/user-prompt-editor.tsx +99 -0
  90. package/src/extensions/nocobase-ai/components/chat/work-context-chip.tsx +114 -0
  91. package/src/extensions/nocobase-ai/components/index.ts +59 -0
  92. package/src/extensions/nocobase-ai/components/page-elements/ai-form.tsx +44 -0
  93. package/src/extensions/nocobase-ai/components/page-elements/page-element-provider.tsx +469 -0
  94. package/src/extensions/nocobase-ai/components/surfaces/chat-dialog.tsx +31 -0
  95. package/src/extensions/nocobase-ai/components/surfaces/chat-inline.tsx +18 -0
  96. package/src/extensions/nocobase-ai/components/surfaces/chat-page.tsx +18 -0
  97. package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +54 -0
  98. package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel.tsx +74 -0
  99. package/src/extensions/nocobase-ai/components/surfaces/chat-surface-actions.tsx +54 -0
  100. package/src/extensions/nocobase-ai/components/tools/builtin-tool-renderers.tsx +31 -0
  101. package/src/extensions/nocobase-ai/components/tools/business-report-dialog.tsx +364 -0
  102. package/src/extensions/nocobase-ai/components/tools/business-report-renderer.tsx +122 -0
  103. package/src/extensions/nocobase-ai/components/tools/business-report-utils.tsx +282 -0
  104. package/src/extensions/nocobase-ai/components/tools/chart-renderer.tsx +99 -0
  105. package/src/extensions/nocobase-ai/components/tools/echarts-preview.tsx +59 -0
  106. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-advanced.ts +18 -0
  107. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-common.ts +20 -0
  108. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-components.ts +34 -0
  109. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +24 -0
  110. package/src/extensions/nocobase-ai/components/tools/echarts-runtime.ts +138 -0
  111. package/src/extensions/nocobase-ai/components/tools/sub-agent-renderer.tsx +67 -0
  112. package/src/extensions/nocobase-ai/components/tools/suggestions-renderer.tsx +59 -0
  113. package/src/extensions/nocobase-ai/components/tools/tool-renderer-provider.tsx +55 -0
  114. package/src/extensions/nocobase-ai/components/tools/tool-renderer-utils.ts +18 -0
  115. package/src/extensions/nocobase-ai/components/tools/workflow-renderer.tsx +108 -0
  116. package/src/extensions/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +73 -0
  117. package/src/extensions/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +149 -0
  118. package/src/extensions/nocobase-ai/demo/configuration-gate.tsx +49 -0
  119. package/src/extensions/nocobase-ai/demo/container-showcase.tsx +210 -0
  120. package/src/extensions/nocobase-ai/demo/floating.tsx +134 -0
  121. package/src/extensions/nocobase-ai/demo/index.tsx +346 -0
  122. package/src/extensions/nocobase-ai/demo/interaction-showcase.tsx +151 -0
  123. package/src/extensions/nocobase-ai/demo/page-context-prompt-generator.tsx +355 -0
  124. package/src/extensions/nocobase-ai/demo/page-context.tsx +884 -0
  125. package/src/extensions/nocobase-ai/demo/page-element-showcase.tsx +197 -0
  126. package/src/extensions/nocobase-ai/demo/prompt-card.tsx +41 -0
  127. package/src/extensions/nocobase-ai/demo/prompt-generator.tsx +452 -0
  128. package/src/extensions/nocobase-ai/demo/shortcut.tsx +1207 -0
  129. package/src/extensions/nocobase-ai/demo/tool-cards.tsx +530 -0
  130. package/src/extensions/nocobase-ai/extension.tsx +158 -0
  131. package/src/extensions/nocobase-ai/global-ai-chat.tsx +213 -0
  132. package/src/extensions/nocobase-ai/index.ts +3 -0
  133. package/src/extensions/nocobase-ai/providers/ai-provider.tsx +323 -0
  134. package/src/extensions/nocobase-ai/providers/avatars.ts +817 -0
  135. package/src/extensions/nocobase-ai/providers/chat-context.tsx +109 -0
  136. package/src/extensions/nocobase-ai/providers/chat-controller.ts +84 -0
  137. package/src/extensions/nocobase-ai/providers/chat-message-utils.ts +198 -0
  138. package/src/extensions/nocobase-ai/providers/chat-provider.tsx +871 -0
  139. package/src/extensions/nocobase-ai/providers/chat-reducer.ts +147 -0
  140. package/src/extensions/nocobase-ai/providers/chat-task-utils.ts +105 -0
  141. package/src/extensions/nocobase-ai/providers/chat-transport.ts +184 -0
  142. package/src/extensions/nocobase-ai/providers/form-registry.tsx +271 -0
  143. package/src/extensions/nocobase-ai/providers/frontend-tool-registry.tsx +231 -0
  144. package/src/extensions/nocobase-ai/providers/index.ts +61 -0
  145. package/src/extensions/nocobase-ai/providers/model.ts +32 -0
  146. package/src/extensions/nocobase-ai/providers/page-context.tsx +136 -0
  147. package/src/extensions/nocobase-ai/providers/stream-coalescer.ts +60 -0
  148. package/src/extensions/nocobase-ai/providers/stream-event-utils.ts +82 -0
  149. package/src/extensions/nocobase-ai/providers/stream-parser.ts +61 -0
  150. package/src/extensions/nocobase-ai/providers/sub-agent-stream.ts +316 -0
  151. package/src/extensions/nocobase-ai/providers/types.ts +183 -0
  152. package/src/extensions/nocobase-ai/providers/ui-message-stream.ts +463 -0
  153. package/src/extensions/nocobase-ai/providers/use-automatic-tool-approval.ts +94 -0
  154. package/src/extensions/nocobase-ai/providers/use-chat-attachments.ts +206 -0
  155. package/src/extensions/nocobase-ai/providers/use-chat-message-actions.ts +554 -0
  156. package/src/extensions/nocobase-ai/providers/use-chat-runtime.ts +210 -0
  157. package/src/extensions/nocobase-ai/providers/use-chat-work-context.ts +93 -0
  158. package/src/extensions/nocobase-ai/providers/use-conversation-catalog.ts +108 -0
  159. package/src/extensions/nocobase-ai/providers/use-conversation-history.ts +178 -0
  160. package/src/extensions/nocobase-ai/services/index.ts +7 -0
  161. package/src/extensions/nocobase-ai/services/nocobase-ai-service.ts +454 -0
  162. package/src/extensions/nocobase-ai/services/types.ts +82 -0
  163. package/src/index.tsx +2 -3
  164. package/src/lib/i18n.ts +161 -0
  165. package/src/lib/nocobase/client.ts +19 -2
  166. package/src/locales/en-US.ts +82 -0
  167. package/src/locales/index.ts +13 -0
  168. package/src/locales/zh-CN.ts +79 -0
  169. package/src/pages/users/create.tsx +98 -0
  170. package/src/pages/users/edit.tsx +92 -0
  171. package/src/pages/users/form-context.ts +57 -0
  172. package/src/pages/users/form-fields.tsx +211 -0
  173. package/src/pages/users/list.tsx +205 -0
  174. package/src/pages/users/show.tsx +187 -0
  175. package/src/pages/users/types.ts +20 -0
  176. package/src/providers/constants.ts +1 -0
  177. package/src/pages/blog-posts/create.tsx +0 -177
  178. package/src/pages/blog-posts/edit.tsx +0 -197
  179. package/src/pages/blog-posts/list.tsx +0 -131
  180. package/src/pages/blog-posts/show.tsx +0 -81
  181. package/src/pages/categories/create.tsx +0 -73
  182. package/src/pages/categories/edit.tsx +0 -73
  183. package/src/pages/categories/index.ts +0 -4
  184. package/src/pages/categories/list.tsx +0 -86
  185. package/src/pages/categories/show.tsx +0 -37
  186. /package/src/pages/{blog-posts → users}/index.ts +0 -0
@@ -0,0 +1,454 @@
1
+ import { nocobaseClient, type NocoBaseClient } from "@/lib/nocobase/client";
2
+ import type {
3
+ AIChatMessage,
4
+ AIConversation,
5
+ AIEmployee,
6
+ AIModel,
7
+ } from "../providers/types";
8
+ import type {
9
+ AIConversationActiveState,
10
+ AIService,
11
+ CreateAIConversationOptions,
12
+ } from "./types";
13
+ import type { UpdateToolCallDecisionOptions } from "./types";
14
+ import {
15
+ getToolCallState,
16
+ getToolProviderMetadata,
17
+ type NocoBaseToolCall,
18
+ } from "../providers/stream-event-utils";
19
+
20
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
21
+ !!value && typeof value === "object" && !Array.isArray(value);
22
+
23
+ const parseToolInput = (value: unknown) => {
24
+ if (typeof value !== "string") return value;
25
+ try {
26
+ return JSON.parse(value) as unknown;
27
+ } catch {
28
+ return value;
29
+ }
30
+ };
31
+
32
+ const toAttachment = (
33
+ value: unknown,
34
+ index: number,
35
+ resolveUrl: (value: string) => string
36
+ ) => {
37
+ if (!isRecord(value)) return undefined;
38
+ const filename = value.filename ?? value.name ?? value.title;
39
+ if (typeof filename !== "string") return undefined;
40
+ return {
41
+ ...value,
42
+ uid: String(value.id ?? value.uid ?? `${filename}-${index}`),
43
+ filename,
44
+ status: "done" as const,
45
+ size: typeof value.size === "number" ? value.size : undefined,
46
+ mimetype:
47
+ typeof value.mimetype === "string"
48
+ ? value.mimetype
49
+ : typeof value.type === "string"
50
+ ? value.type
51
+ : undefined,
52
+ url: typeof value.url === "string" ? resolveUrl(value.url) : undefined,
53
+ preview:
54
+ typeof value.preview === "string" ? resolveUrl(value.preview) : undefined,
55
+ };
56
+ };
57
+
58
+ const toHistoryMessage = (
59
+ value: unknown,
60
+ index: number,
61
+ resolveUrl: (value: string) => string
62
+ ): AIChatMessage => {
63
+ const message = isRecord(value) ? value : {};
64
+ const content = isRecord(message.content) ? message.content : {};
65
+ const rawServerMessageId = content.messageId ?? message.messageId;
66
+ const serverMessageId =
67
+ typeof rawServerMessageId === "number" ||
68
+ (typeof rawServerMessageId === "string" && /^\d+$/.test(rawServerMessageId))
69
+ ? String(rawServerMessageId)
70
+ : undefined;
71
+ const messageId =
72
+ serverMessageId ?? String(message.key ?? `history-${index}`);
73
+ const text =
74
+ typeof content.content === "string"
75
+ ? content.content
76
+ : typeof message.content === "string"
77
+ ? message.content
78
+ : "";
79
+ const attachments = Array.isArray(content.attachments)
80
+ ? content.attachments
81
+ .map((attachment, attachmentIndex) =>
82
+ toAttachment(attachment, attachmentIndex, resolveUrl)
83
+ )
84
+ .filter((attachment) => attachment !== undefined)
85
+ : [];
86
+ const parts: AIChatMessage["parts"] = [];
87
+ const reasoning = isRecord(content.reasoning) ? content.reasoning : undefined;
88
+ if (typeof reasoning?.content === "string" && reasoning.content) {
89
+ parts.push({ type: "reasoning", text: reasoning.content, state: "done" });
90
+ }
91
+ if (text) parts.push({ type: "text", text, state: "done" });
92
+ const toolCalls = Array.isArray(content.tool_calls)
93
+ ? content.tool_calls
94
+ : Array.isArray(message.toolCalls)
95
+ ? message.toolCalls
96
+ : [];
97
+ for (const rawToolCall of toolCalls) {
98
+ if (!isRecord(rawToolCall)) continue;
99
+ const toolCallId = String(rawToolCall.id ?? `tool-${crypto.randomUUID()}`);
100
+ const toolName = String(rawToolCall.name ?? "tool");
101
+ const toolCall = rawToolCall as NocoBaseToolCall;
102
+ const { failed, completed } = getToolCallState(toolCall);
103
+ const callProviderMetadata = getToolProviderMetadata(toolCall);
104
+ parts.push(
105
+ failed
106
+ ? {
107
+ type: "dynamic-tool",
108
+ toolCallId,
109
+ toolName,
110
+ state: "output-error",
111
+ input: parseToolInput(rawToolCall.args ?? {}),
112
+ errorText: String(rawToolCall.content ?? "Tool call failed"),
113
+ callProviderMetadata,
114
+ }
115
+ : completed
116
+ ? {
117
+ type: "dynamic-tool",
118
+ toolCallId,
119
+ toolName,
120
+ state: "output-available",
121
+ input: parseToolInput(rawToolCall.args ?? {}),
122
+ output: rawToolCall.content,
123
+ callProviderMetadata,
124
+ }
125
+ : {
126
+ type: "dynamic-tool",
127
+ toolCallId,
128
+ toolName,
129
+ state: "input-available",
130
+ input: parseToolInput(rawToolCall.args ?? {}),
131
+ callProviderMetadata,
132
+ }
133
+ );
134
+ }
135
+ const subAgentConversations = Array.isArray(content.subAgentConversations)
136
+ ? content.subAgentConversations
137
+ : [];
138
+ for (const [
139
+ conversationIndex,
140
+ rawConversation,
141
+ ] of subAgentConversations.entries()) {
142
+ if (!isRecord(rawConversation)) continue;
143
+ const rawMessages = Array.isArray(rawConversation.messages)
144
+ ? rawConversation.messages
145
+ : [];
146
+ const messages = rawMessages.map((item, messageIndex) =>
147
+ toHistoryMessage(item, messageIndex, resolveUrl)
148
+ );
149
+ const username =
150
+ typeof rawConversation.username === "string"
151
+ ? rawConversation.username
152
+ : messages.find((item) => item.metadata?.employeeUsername)?.metadata
153
+ ?.employeeUsername ?? "sub-agent";
154
+ const sessionId = String(
155
+ rawConversation.sessionId ??
156
+ `sub-agent-history-${index}-${conversationIndex}`
157
+ );
158
+ parts.push({
159
+ type: "data-subAgent",
160
+ id: sessionId,
161
+ data: {
162
+ sessionId,
163
+ username,
164
+ status:
165
+ rawConversation.status === "completed" ? "completed" : "pending",
166
+ messages,
167
+ },
168
+ });
169
+ }
170
+ for (const attachment of attachments) {
171
+ if (!attachment.url && !attachment.preview) continue;
172
+ parts.push({
173
+ type: "file",
174
+ mediaType: attachment.mimetype ?? "application/octet-stream",
175
+ filename: attachment.filename,
176
+ url: attachment.url ?? attachment.preview ?? "",
177
+ });
178
+ }
179
+ const rawRole = String(message.role ?? "assistant");
180
+ const role =
181
+ rawRole === "user" || rawRole === "system" ? rawRole : "assistant";
182
+ return {
183
+ id: messageId,
184
+ role,
185
+ metadata: {
186
+ ...(serverMessageId ? { serverMessageId } : {}),
187
+ ...(rawRole !== "user" && rawRole !== "system"
188
+ ? { employeeUsername: rawRole }
189
+ : {}),
190
+ createdAt:
191
+ typeof message.createdAt === "string" ? message.createdAt : undefined,
192
+ attachments,
193
+ workContext: Array.isArray(content.workContext)
194
+ ? content.workContext
195
+ : undefined,
196
+ },
197
+ parts,
198
+ };
199
+ };
200
+
201
+ export class NocoBaseAIService implements AIService {
202
+ constructor(private readonly client: NocoBaseClient = nocobaseClient) {}
203
+
204
+ async listEmployees() {
205
+ const employees = await this.client.action<AIEmployee[]>(
206
+ "aiEmployees",
207
+ "listByUser"
208
+ );
209
+ return employees
210
+ .filter((employee) => employee?.username)
211
+ .map((employee) => ({
212
+ ...employee,
213
+ nickname: employee.nickname ?? employee.username,
214
+ description: employee.description ?? employee.bio,
215
+ }));
216
+ }
217
+
218
+ async listModels() {
219
+ const services = await this.client.action<
220
+ Array<{
221
+ llmService: string;
222
+ llmServiceTitle: string;
223
+ enabledModels?: Array<{ label: string; value: string }>;
224
+ supportWebSearch?: boolean;
225
+ isToolConflict?: boolean;
226
+ }>
227
+ >("ai", "listAllEnabledModels");
228
+ return services.flatMap((service) =>
229
+ (service.enabledModels ?? []).map<AIModel>((model) => ({
230
+ value: model.value,
231
+ label: model.label,
232
+ llmService: service.llmService,
233
+ llmServiceTitle: service.llmServiceTitle,
234
+ supportWebSearch: service.supportWebSearch,
235
+ isToolConflict: service.isToolConflict,
236
+ }))
237
+ );
238
+ }
239
+
240
+ async updateEmployeeUserPrompt(username: string, prompt: string) {
241
+ await this.client.action("aiEmployees", "updateUserPrompt", {
242
+ body: { aiEmployee: username, prompt },
243
+ });
244
+ }
245
+
246
+ async listConversations(keyword = "") {
247
+ const normalizedKeyword = keyword.trim();
248
+ const response = await this.client.action<
249
+ { data?: unknown[]; rows?: unknown[] } | unknown[]
250
+ >("aiConversations", "list", {
251
+ query: {
252
+ sort: "-createdAt",
253
+ appends: "aiEmployee",
254
+ page: 1,
255
+ pageSize: 50,
256
+ filter: normalizedKeyword
257
+ ? JSON.stringify({ title: { $includes: normalizedKeyword } })
258
+ : undefined,
259
+ },
260
+ });
261
+ const rows = Array.isArray(response)
262
+ ? response
263
+ : response.data ?? response.rows ?? [];
264
+ return rows.flatMap<AIConversation>((value) => {
265
+ if (!isRecord(value) || typeof value.sessionId !== "string") return [];
266
+ const employee = isRecord(value.aiEmployee)
267
+ ? value.aiEmployee
268
+ : undefined;
269
+ const options = isRecord(value.options) ? value.options : undefined;
270
+ const modelSettings = isRecord(options?.modelSettings)
271
+ ? options.modelSettings
272
+ : undefined;
273
+ return [
274
+ {
275
+ id: value.sessionId,
276
+ title:
277
+ typeof value.title === "string" && value.title
278
+ ? value.title
279
+ : "New conversation",
280
+ employeeUsername: String(
281
+ employee?.username ?? value.aiEmployeeUsername ?? ""
282
+ ),
283
+ updatedAt:
284
+ typeof value.updatedAt === "string"
285
+ ? value.updatedAt
286
+ : new Date().toISOString(),
287
+ unread: value.read === false,
288
+ model:
289
+ typeof modelSettings?.model === "string"
290
+ ? {
291
+ llmService:
292
+ typeof modelSettings.llmService === "string"
293
+ ? modelSettings.llmService
294
+ : undefined,
295
+ model: modelSettings.model,
296
+ }
297
+ : undefined,
298
+ },
299
+ ];
300
+ });
301
+ }
302
+
303
+ async getConversationMessages(
304
+ sessionId: string,
305
+ options: { updateRead?: boolean } = {}
306
+ ) {
307
+ const response = await this.client.action<
308
+ { data?: unknown[]; rows?: unknown[] } | unknown[]
309
+ >("aiConversations", "getMessages", {
310
+ query: {
311
+ sessionId,
312
+ paginate: false,
313
+ updateRead: options.updateRead === true,
314
+ },
315
+ body: {},
316
+ });
317
+ const rows = Array.isArray(response)
318
+ ? response
319
+ : response.data ?? response.rows ?? [];
320
+ return [...rows]
321
+ .reverse()
322
+ .filter((value) => {
323
+ if (!isRecord(value)) return true;
324
+ return value.role !== "tool" && value.role !== "system";
325
+ })
326
+ .map((value, index) =>
327
+ toHistoryMessage(value, index, (url) => this.client.resolveUrl(url))
328
+ );
329
+ }
330
+
331
+ async getConversationActiveState(sessionId: string) {
332
+ const response = await this.client.action<{
333
+ llmActiveState?: unknown;
334
+ }>("aiConversations", "get", {
335
+ query: { filter: JSON.stringify({ sessionId }) },
336
+ });
337
+ const state = response?.llmActiveState;
338
+ return state === "idle" || state === "streaming" || state === "invoking"
339
+ ? (state as AIConversationActiveState)
340
+ : undefined;
341
+ }
342
+
343
+ async updateConversationTitle(sessionId: string, title: string) {
344
+ await this.client.action("aiConversations", "update", {
345
+ method: "PUT",
346
+ query: { filterByTk: sessionId },
347
+ body: { title },
348
+ });
349
+ }
350
+
351
+ async destroyConversation(sessionId: string) {
352
+ await this.client.action("aiConversations", "destroy", {
353
+ method: "DELETE",
354
+ query: { filterByTk: sessionId },
355
+ });
356
+ }
357
+
358
+ async uploadFile(file: File, signal?: AbortSignal) {
359
+ const formData = new FormData();
360
+ formData.append("file", file);
361
+ const response = await this.client.action<Record<string, unknown>>(
362
+ "aiFiles",
363
+ "create",
364
+ {
365
+ body: formData,
366
+ signal,
367
+ unwrap: "deep-data",
368
+ }
369
+ );
370
+ return {
371
+ ...response,
372
+ ...(typeof response.url === "string"
373
+ ? { url: this.client.resolveUrl(response.url) }
374
+ : {}),
375
+ ...(typeof response.preview === "string"
376
+ ? { preview: this.client.resolveUrl(response.preview) }
377
+ : {}),
378
+ };
379
+ }
380
+
381
+ async createConversation(options: CreateAIConversationOptions) {
382
+ const response = await this.client.action<{ sessionId: string }>(
383
+ "aiConversations",
384
+ "create",
385
+ {
386
+ body: {
387
+ aiEmployee: options.employee,
388
+ systemMessage: options.systemMessage,
389
+ skillSettings: options.skillSettings,
390
+ modelSettings: {
391
+ llmService: options.model.llmService,
392
+ model: options.model.value,
393
+ },
394
+ },
395
+ }
396
+ );
397
+ return response.sessionId;
398
+ }
399
+
400
+ sendMessagesStream(body: unknown, signal?: AbortSignal) {
401
+ return this.client.stream("aiConversations:sendMessages", {
402
+ body,
403
+ signal,
404
+ });
405
+ }
406
+
407
+ resendMessagesStream(body: unknown, signal?: AbortSignal) {
408
+ return this.client.stream("aiConversations:resendMessages", {
409
+ body,
410
+ signal,
411
+ });
412
+ }
413
+
414
+ async updateToolCallDecision(options: UpdateToolCallDecisionOptions) {
415
+ const result = await this.client.action<{
416
+ updated: number;
417
+ toolCalls: Array<{
418
+ id: string;
419
+ name: string;
420
+ invokeStatus?: string;
421
+ status?: string;
422
+ auto?: boolean;
423
+ execution?: string;
424
+ willInterrupt?: boolean;
425
+ args?: unknown;
426
+ }>;
427
+ }>("aiConversations", "updateUserDecision", {
428
+ body: options,
429
+ });
430
+ return {
431
+ ...result,
432
+ toolCalls: result.toolCalls.map((toolCall) => ({
433
+ ...toolCall,
434
+ args: parseToolInput(toolCall.args),
435
+ })),
436
+ };
437
+ }
438
+
439
+ resumeToolCallStream(body: unknown, signal?: AbortSignal) {
440
+ return this.client.stream("aiConversations:resumeToolCall", {
441
+ body,
442
+ signal,
443
+ });
444
+ }
445
+
446
+ resumeConversationStream(sessionId: string, signal?: AbortSignal) {
447
+ return this.client.stream("aiConversations:resumeStream", {
448
+ body: { sessionId },
449
+ signal,
450
+ });
451
+ }
452
+ }
453
+
454
+ export const nocobaseAIService = new NocoBaseAIService();
@@ -0,0 +1,82 @@
1
+ import type {
2
+ AIChatMessage,
3
+ AIConversation,
4
+ AIEmployee,
5
+ AIModel,
6
+ } from "../providers/types";
7
+
8
+ export type CreateAIConversationOptions = {
9
+ employee: AIEmployee;
10
+ model: AIModel;
11
+ systemMessage?: string;
12
+ skillSettings?: {
13
+ skills?: string[];
14
+ tools?: string[];
15
+ };
16
+ };
17
+
18
+ export type UpdateToolCallDecisionOptions = {
19
+ sessionId: string;
20
+ messageId: string;
21
+ toolCallId: string;
22
+ userDecision:
23
+ | { type: "approve" }
24
+ | { type: "reject"; message?: string }
25
+ | {
26
+ type: "edit";
27
+ editedAction: { name: string; args: unknown };
28
+ };
29
+ };
30
+
31
+ export type UpdatedToolCall = {
32
+ id: string;
33
+ name: string;
34
+ invokeStatus?: string;
35
+ status?: string;
36
+ auto?: boolean;
37
+ execution?: string;
38
+ willInterrupt?: boolean;
39
+ args?: unknown;
40
+ };
41
+
42
+ export type AIConversationActiveState = "idle" | "streaming" | "invoking";
43
+
44
+ export interface AIService {
45
+ listEmployees(): Promise<AIEmployee[]>;
46
+ listModels(): Promise<AIModel[]>;
47
+ updateEmployeeUserPrompt(username: string, prompt: string): Promise<void>;
48
+ listConversations(keyword?: string): Promise<AIConversation[]>;
49
+ getConversationMessages(
50
+ sessionId: string,
51
+ options?: { updateRead?: boolean }
52
+ ): Promise<AIChatMessage[]>;
53
+ getConversationActiveState(
54
+ sessionId: string
55
+ ): Promise<AIConversationActiveState | undefined>;
56
+ updateConversationTitle(sessionId: string, title: string): Promise<void>;
57
+ destroyConversation(sessionId: string): Promise<void>;
58
+ uploadFile(
59
+ file: File,
60
+ signal?: AbortSignal
61
+ ): Promise<Record<string, unknown>>;
62
+ createConversation(options: CreateAIConversationOptions): Promise<string>;
63
+ sendMessagesStream(
64
+ body: unknown,
65
+ signal?: AbortSignal
66
+ ): Promise<ReadableStream<Uint8Array>>;
67
+ resendMessagesStream(
68
+ body: unknown,
69
+ signal?: AbortSignal
70
+ ): Promise<ReadableStream<Uint8Array>>;
71
+ updateToolCallDecision(
72
+ options: UpdateToolCallDecisionOptions
73
+ ): Promise<{ updated: number; toolCalls: UpdatedToolCall[] }>;
74
+ resumeToolCallStream(
75
+ body: unknown,
76
+ signal?: AbortSignal
77
+ ): Promise<ReadableStream<Uint8Array>>;
78
+ resumeConversationStream(
79
+ sessionId: string,
80
+ signal?: AbortSignal
81
+ ): Promise<ReadableStream<Uint8Array>>;
82
+ }
package/src/index.tsx CHANGED
@@ -1,6 +1,7 @@
1
1
  import React from "react";
2
2
  import { createRoot } from "react-dom/client";
3
3
 
4
+ import "./locales";
4
5
  import App from "./App";
5
6
 
6
7
  const container = document.getElementById("root") as HTMLElement;
@@ -12,6 +13,4 @@ const root = createRoot(container);
12
13
  // </React.StrictMode>
13
14
  // );
14
15
 
15
- root.render(
16
- <App />
17
- );
16
+ root.render(<App />);
@@ -0,0 +1,161 @@
1
+ export type TranslationOptions = Record<string, unknown> & {
2
+ defaultValue?: string;
3
+ ns?: string | string[];
4
+ };
5
+
6
+ export type TranslationResolver = (
7
+ key: string,
8
+ options?: TranslationOptions,
9
+ defaultMessage?: string
10
+ ) => string;
11
+
12
+ export type TranslationExpression = {
13
+ key: string;
14
+ options?: TranslationOptions;
15
+ };
16
+
17
+ export type TranslationResource = Record<string, string | number | boolean>;
18
+
19
+ export type TranslationResourceBundle = Record<string, TranslationResource>;
20
+
21
+ type TranslationResourceListener = (
22
+ namespace: string,
23
+ resources: TranslationResourceBundle
24
+ ) => void;
25
+
26
+ let translationResolver: TranslationResolver | undefined;
27
+ const translationResources = new Map<string, TranslationResourceBundle>();
28
+ const translationResourceListeners = new Set<TranslationResourceListener>();
29
+
30
+ const quotedValuePattern = `(?:"(?:\\\\.|[^"\\\\])*"|'(?:\\\\.|[^'\\\\])*')`;
31
+ const translationExpressionPattern = new RegExp(
32
+ `^\\s*\\{\\{\\s*t\\(\\s*(${quotedValuePattern})\\s*(?:,\\s*(\\{[\\s\\S]*\\}))?\\s*\\)\\s*\\}\\}\\s*$`
33
+ );
34
+
35
+ function decodeQuotedValue(value: string) {
36
+ if (value.startsWith('"')) {
37
+ try {
38
+ return JSON.parse(value) as string;
39
+ } catch {
40
+ return value.slice(1, -1);
41
+ }
42
+ }
43
+
44
+ return value
45
+ .slice(1, -1)
46
+ .replace(/\\'/g, "'")
47
+ .replace(/\\"/g, '"')
48
+ .replace(/\\n/g, "\n")
49
+ .replace(/\\r/g, "\r")
50
+ .replace(/\\t/g, "\t")
51
+ .replace(/\\\\/g, "\\");
52
+ }
53
+
54
+ function parsePrimitive(value: string): unknown {
55
+ const normalized = value.trim();
56
+ if (normalized.startsWith('"') || normalized.startsWith("'")) {
57
+ return decodeQuotedValue(normalized);
58
+ }
59
+ if (normalized === "true") return true;
60
+ if (normalized === "false") return false;
61
+ if (normalized === "null") return null;
62
+ if (/^-?\d+(?:\.\d+)?$/.test(normalized)) return Number(normalized);
63
+ return undefined;
64
+ }
65
+
66
+ function parseOptions(source?: string): TranslationOptions | undefined {
67
+ if (!source) return undefined;
68
+
69
+ const options: TranslationOptions = {};
70
+ const propertyPattern = new RegExp(
71
+ `([A-Za-z_$][\\w$]*)\\s*:\\s*(${quotedValuePattern}|-?\\d+(?:\\.\\d+)?|true|false|null)`,
72
+ "g"
73
+ );
74
+
75
+ for (const match of source.matchAll(propertyPattern)) {
76
+ const value = parsePrimitive(match[2]);
77
+ if (typeof value !== "undefined") options[match[1]] = value;
78
+ }
79
+
80
+ const namespaceArray = source.match(/\bns\s*:\s*\[([^\]]*)\]/);
81
+ if (namespaceArray) {
82
+ const namespaces = [
83
+ ...namespaceArray[1].matchAll(new RegExp(quotedValuePattern, "g")),
84
+ ].map((match) => decodeQuotedValue(match[0]));
85
+ if (namespaces.length) options.ns = namespaces;
86
+ }
87
+
88
+ return Object.keys(options).length ? options : undefined;
89
+ }
90
+
91
+ export function parseTranslationExpression(
92
+ value: string
93
+ ): TranslationExpression | undefined {
94
+ const match = translationExpressionPattern.exec(value);
95
+ if (!match) return undefined;
96
+
97
+ return {
98
+ key: decodeQuotedValue(match[1]),
99
+ options: parseOptions(match[2]),
100
+ };
101
+ }
102
+
103
+ export function setTranslationResolver(resolver?: TranslationResolver) {
104
+ translationResolver = resolver;
105
+ return () => {
106
+ if (translationResolver === resolver) translationResolver = undefined;
107
+ };
108
+ }
109
+
110
+ export function registerTranslationResources(
111
+ namespace: string,
112
+ resources: TranslationResourceBundle
113
+ ) {
114
+ const current = translationResources.get(namespace) ?? {};
115
+ const merged = Object.fromEntries(
116
+ [...new Set([...Object.keys(current), ...Object.keys(resources)])].map(
117
+ (locale) => [
118
+ locale,
119
+ {
120
+ ...(current[locale] ?? {}),
121
+ ...(resources[locale] ?? {}),
122
+ },
123
+ ]
124
+ )
125
+ );
126
+
127
+ translationResources.set(namespace, merged);
128
+ translationResourceListeners.forEach((listener) =>
129
+ listener(namespace, resources)
130
+ );
131
+ }
132
+
133
+ export function getTranslationResources() {
134
+ return [...translationResources.entries()];
135
+ }
136
+
137
+ export function subscribeTranslationResources(
138
+ listener: TranslationResourceListener
139
+ ) {
140
+ translationResourceListeners.add(listener);
141
+ return () => translationResourceListeners.delete(listener);
142
+ }
143
+
144
+ export function resolveTranslatableText(
145
+ value: unknown,
146
+ options?: TranslationOptions
147
+ ): string {
148
+ if (typeof value !== "string") return value == null ? "" : String(value);
149
+
150
+ const expression = parseTranslationExpression(value);
151
+ if (!expression) {
152
+ return translationResolver && options
153
+ ? translationResolver(value, options, value)
154
+ : value;
155
+ }
156
+
157
+ const defaultMessage = expression.options?.defaultValue ?? expression.key;
158
+ return translationResolver
159
+ ? translationResolver(expression.key, expression.options, defaultMessage)
160
+ : defaultMessage;
161
+ }