plugin-file-preview-auth 1.3.5 → 1.3.6

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 (105) hide show
  1. package/client-v2.d.ts +2 -0
  2. package/client-v2.js +1 -0
  3. package/dist/client/713.79a55458f5b67f39.js +30 -0
  4. package/dist/client/823.8b0ab22c181d4523.js +10 -0
  5. package/dist/client/828.ae8e47a2e7a3bc9e.js +49 -0
  6. package/dist/client/892.a568eb42fd6f0047.js +10 -0
  7. package/dist/client/index.js +1 -1
  8. package/dist/client-v2/index.js +10 -0
  9. package/dist/externalVersion.js +8 -7
  10. package/dist/node_modules/@aws-sdk/client-s3/dist-cjs/index.js +3086 -3725
  11. package/dist/node_modules/@aws-sdk/client-s3/node_modules/.bin/fxparser +16 -0
  12. package/dist/node_modules/@aws-sdk/client-s3/node_modules/.bin/fxparser.cmd +17 -0
  13. package/dist/node_modules/@aws-sdk/client-s3/node_modules/.bin/fxparser.ps1 +28 -0
  14. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/signature-v4-multi-region/dist-cjs/index.js +110 -0
  15. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/signature-v4-multi-region/dist-es/SignatureV4MultiRegion.js +66 -0
  16. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/signature-v4-multi-region/dist-es/index.js +2 -0
  17. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/signature-v4-multi-region/dist-es/signature-v4-crt-container.js +3 -0
  18. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/signature-v4-multi-region/dist-types/SignatureV4MultiRegion.d.ts +30 -0
  19. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/signature-v4-multi-region/dist-types/index.d.ts +5 -0
  20. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/signature-v4-multi-region/dist-types/signature-v4-crt-container.d.ts +28 -0
  21. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/signature-v4-multi-region/dist-types/ts3.4/SignatureV4MultiRegion.d.ts +40 -0
  22. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/signature-v4-multi-region/dist-types/ts3.4/index.d.ts +2 -0
  23. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/signature-v4-multi-region/dist-types/ts3.4/signature-v4-crt-container.d.ts +20 -0
  24. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/signature-v4-multi-region/package.json +57 -0
  25. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-cjs/AdaptiveRetryStrategy.js +1 -0
  26. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-cjs/ConfiguredRetryStrategy.js +1 -0
  27. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-cjs/DefaultRateLimiter.js +1 -0
  28. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-cjs/StandardRetryStrategy.js +1 -0
  29. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-cjs/config.js +1 -0
  30. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-cjs/constants.js +1 -0
  31. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-cjs/defaultRetryBackoffStrategy.js +1 -0
  32. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-cjs/defaultRetryToken.js +1 -0
  33. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-cjs/index.js +358 -0
  34. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-cjs/types.js +1 -0
  35. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-es/AdaptiveRetryStrategy.js +24 -0
  36. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-es/ConfiguredRetryStrategy.js +18 -0
  37. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-es/DefaultRateLimiter.js +100 -0
  38. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-es/StandardRetryStrategy.js +65 -0
  39. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-es/config.js +7 -0
  40. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-es/constants.js +9 -0
  41. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-es/defaultRetryBackoffStrategy.js +14 -0
  42. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-es/defaultRetryToken.js +11 -0
  43. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-es/index.js +7 -0
  44. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-es/types.js +1 -0
  45. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/AdaptiveRetryStrategy.d.ts +33 -0
  46. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/ConfiguredRetryStrategy.d.ts +32 -0
  47. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/DefaultRateLimiter.d.ts +49 -0
  48. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/StandardRetryStrategy.d.ts +26 -0
  49. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/config.d.ts +20 -0
  50. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/constants.d.ts +59 -0
  51. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/defaultRetryBackoffStrategy.d.ts +5 -0
  52. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/defaultRetryToken.d.ts +9 -0
  53. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/index.d.ts +7 -0
  54. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/ts3.4/AdaptiveRetryStrategy.d.ts +33 -0
  55. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/ts3.4/ConfiguredRetryStrategy.d.ts +32 -0
  56. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/ts3.4/DefaultRateLimiter.d.ts +49 -0
  57. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/ts3.4/StandardRetryStrategy.d.ts +26 -0
  58. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/ts3.4/config.d.ts +20 -0
  59. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/ts3.4/constants.d.ts +59 -0
  60. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/ts3.4/defaultRetryBackoffStrategy.d.ts +5 -0
  61. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/ts3.4/defaultRetryToken.d.ts +9 -0
  62. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/ts3.4/index.d.ts +7 -0
  63. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/ts3.4/types.d.ts +19 -0
  64. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/types.d.ts +19 -0
  65. package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/package.json +68 -0
  66. package/dist/node_modules/@aws-sdk/client-s3/package.json +1 -1
  67. package/dist/node_modules/xlsx/package.json +1 -1
  68. package/dist/server/ocr/tesseract-runner.js +3 -1
  69. package/dist/server/plugin.js +22 -4
  70. package/package.json +57 -45
  71. package/src/client/AIFilePreviewAction.tsx +282 -0
  72. package/src/client/__tests__/ocr-utils.test.ts +85 -0
  73. package/src/client/client.d.ts +258 -0
  74. package/src/client/index.tsx +1807 -0
  75. package/src/client/locale.ts +21 -0
  76. package/src/client-v2/index.tsx +1 -0
  77. package/src/client-v2/plugin.tsx +7 -0
  78. package/{dist/index.d.ts → src/index.ts} +11 -10
  79. package/src/locale/en-US.json +14 -0
  80. package/src/locale/vi-VN.json +14 -0
  81. package/src/locale/zh-CN.json +14 -0
  82. package/src/server/__tests__/smoke.test.ts +17 -0
  83. package/src/server/collections/attachment-ocr-results.ts +40 -0
  84. package/{dist/server/collections/file-preview-auth.d.ts → src/server/collections/file-preview-auth.ts} +15 -14
  85. package/src/server/excel-parser-handler.ts +128 -0
  86. package/{dist/server/index.d.ts → src/server/index.ts} +10 -9
  87. package/src/server/migrations/20260528000000-move-ocr-fields-out-of-attachments.ts +39 -0
  88. package/src/server/ocr/tesseract-runner.ts +389 -0
  89. package/src/server/ocr/tesseract-worker.ts +235 -0
  90. package/src/server/plugin.ts +1470 -0
  91. package/dist/client/166.17caa11c2ba40313.js +0 -10
  92. package/dist/client/351.0f0ce45c92425c8f.js +0 -10
  93. package/dist/client/374.96762d13b15e7467.js +0 -30
  94. package/dist/client/514.2a8b6aa0d2fcd4b2.js +0 -49
  95. package/dist/client/AIFilePreviewAction.d.ts +0 -42
  96. package/dist/client/index.d.ts +0 -14
  97. package/dist/client/locale.d.ts +0 -10
  98. package/dist/node_modules/xlsx/node_modules/.bin/crc32 +0 -15
  99. package/dist/node_modules/xlsx/node_modules/.bin/crc32.cmd +0 -7
  100. package/dist/server/collections/attachment-ocr-results.d.ts +0 -2
  101. package/dist/server/excel-parser-handler.d.ts +0 -60
  102. package/dist/server/migrations/20260528000000-move-ocr-fields-out-of-attachments.d.ts +0 -5
  103. package/dist/server/ocr/tesseract-runner.d.ts +0 -34
  104. package/dist/server/ocr/tesseract-worker.d.ts +0 -27
  105. package/dist/server/plugin.d.ts +0 -54
@@ -325,10 +325,14 @@ class PluginFilePreviewAuthServer extends import_server.Plugin {
325
325
  const params = ctx.action.params || {};
326
326
  const reqBody = ctx.request.body || {};
327
327
  const values = params.values || {};
328
- const attachmentId = values.attachmentId || reqBody.attachmentId;
329
- if (!attachmentId) {
328
+ const rawAttachmentId = values.attachmentId || reqBody.attachmentId;
329
+ if (!rawAttachmentId) {
330
330
  ctx.throw(400, "attachmentId is required");
331
331
  }
332
+ const attachmentId = normalizeOcrAttachmentId(rawAttachmentId);
333
+ if (!attachmentId) {
334
+ ctx.throw(400, "attachmentId must be a numeric attachment record id");
335
+ }
332
336
  this.assertAuthenticated(ctx);
333
337
  const repo = ctx.db.getRepository("attachments");
334
338
  const attachment = await repo.findOne({ filterByTk: attachmentId });
@@ -352,10 +356,14 @@ class PluginFilePreviewAuthServer extends import_server.Plugin {
352
356
  const reqQuery = ctx.request.query || {};
353
357
  const reqBody = ctx.request.body || {};
354
358
  const values = params.values || {};
355
- const attachmentId = values.attachmentId || params.attachmentId || reqQuery.attachmentId || reqBody.attachmentId;
356
- if (!attachmentId) {
359
+ const rawAttachmentId = values.attachmentId || params.attachmentId || reqQuery.attachmentId || reqBody.attachmentId;
360
+ if (!rawAttachmentId) {
357
361
  ctx.throw(400, "attachmentId is required");
358
362
  }
363
+ const attachmentId = normalizeOcrAttachmentId(rawAttachmentId);
364
+ if (!attachmentId) {
365
+ ctx.throw(400, "attachmentId must be a numeric attachment record id");
366
+ }
359
367
  this.assertAuthenticated(ctx);
360
368
  const repo = ctx.db.getRepository("attachments");
361
369
  const attachment = await repo.findOne({ filterByTk: attachmentId });
@@ -1160,6 +1168,16 @@ function isLikelyRecordId(value) {
1160
1168
  const text = String(value);
1161
1169
  return !text.includes("/") && !text.startsWith("http://") && !text.startsWith("https://");
1162
1170
  }
1171
+ function normalizeOcrAttachmentId(value) {
1172
+ if (typeof value === "number") {
1173
+ return Number.isInteger(value) && value > 0 ? value : null;
1174
+ }
1175
+ if (typeof value === "string") {
1176
+ const trimmed = value.trim();
1177
+ return /^\d+$/.test(trimmed) ? trimmed : null;
1178
+ }
1179
+ return null;
1180
+ }
1163
1181
  function getUrlCandidates(value) {
1164
1182
  if (!value) return [];
1165
1183
  const original = decodePossiblyEncodedUrl(String(value));
package/package.json CHANGED
@@ -1,45 +1,57 @@
1
- {
2
- "name": "plugin-file-preview-auth",
3
- "displayName": "Authenticated File Previewer",
4
- "displayName.vi-VN": "Xem trước file có xác thực",
5
- "displayName.zh-CN": "认证文件预览",
6
- "description": "Preview PDF, image, and text files with Bearer token authentication via blob URLs.",
7
- "description.vi-VN": "Xem trước file PDF, hình ảnh và văn bản với xác thực Bearer token qua blob URL.",
8
- "description.zh-CN": "通过 Bearer 令牌认证和 Blob URL 预览 PDF、图片和文本文件。",
9
- "version": "1.3.5",
10
- "main": "dist/server/index.js",
11
- "files": [
12
- "dist",
13
- "public",
14
- "client.js",
15
- "client.d.ts",
16
- "server.js",
17
- "server.d.ts",
18
- "README.md"
19
- ],
20
- "peerDependencies": {
21
- "@nocobase/client": "2.x",
22
- "@nocobase/database": "2.x",
23
- "@nocobase/plugin-ai": "2.x",
24
- "@nocobase/plugin-file-manager": "2.x",
25
- "@nocobase/server": "2.x",
26
- "@nocobase/utils": "2.x"
27
- },
28
- "peerDependenciesMeta": {
29
- "@nocobase/plugin-ai": {
30
- "optional": true
31
- }
32
- },
33
- "devDependencies": {},
34
- "keywords": [
35
- "File manager",
36
- "File preview",
37
- "Authentication"
38
- ],
39
- "license": "Apache-2.0",
40
- "dependencies": {
41
- "docx-preview": "^0.3.3",
42
- "react-pptx-preview-kit": "^0.1.9",
43
- "xlsx": "^0.18.5"
44
- }
45
- }
1
+ {
2
+ "name": "plugin-file-preview-auth",
3
+ "displayName": "Authenticated File Previewer",
4
+ "displayName.vi-VN": "Xem trước file có xác thực",
5
+ "displayName.zh-CN": "认证文件预览",
6
+ "description": "Preview PDF, image, and text files with Bearer token authentication via blob URLs.",
7
+ "description.vi-VN": "Xem trước file PDF, hình ảnh và văn bản với xác thực Bearer token qua blob URL.",
8
+ "description.zh-CN": "通过 Bearer 令牌认证和 Blob URL 预览 PDF、图片和文本文件。",
9
+ "version": "1.3.6",
10
+ "main": "dist/server/index.js",
11
+ "files": [
12
+ "dist",
13
+ "public",
14
+ "client.js",
15
+ "client.d.ts",
16
+ "server.js",
17
+ "server.d.ts",
18
+ "README.md",
19
+ "client-v2.js",
20
+ "client-v2.d.ts",
21
+ "src"
22
+ ],
23
+ "peerDependencies": {
24
+ "@aws-sdk/client-s3": "*",
25
+ "@nocobase/client": "2.x",
26
+ "@nocobase/database": "2.x",
27
+ "@nocobase/plugin-ai": "2.x",
28
+ "@nocobase/plugin-file-manager": "2.x",
29
+ "@nocobase/server": "2.x",
30
+ "@nocobase/utils": "2.x",
31
+ "@nocobase/client-v2": "2.x",
32
+ "@nocobase/flow-engine": "2.x"
33
+ },
34
+ "peerDependenciesMeta": {
35
+ "@nocobase/plugin-ai": {
36
+ "optional": true
37
+ }
38
+ },
39
+ "devDependencies": {},
40
+ "keywords": [
41
+ "File manager",
42
+ "File preview",
43
+ "Authentication"
44
+ ],
45
+ "license": "Apache-2.0",
46
+ "dependencies": {
47
+ "docx-preview": "^0.3.3",
48
+ "react-pptx-preview-kit": "^0.1.9",
49
+ "xlsx": "^0.18.5"
50
+ },
51
+ "nocobase": {
52
+ "supportedVersions": [
53
+ "2.x"
54
+ ],
55
+ "editionLevel": 0
56
+ }
57
+ }
@@ -0,0 +1,282 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
11
+ import { FileTextOutlined, RobotOutlined } from '@ant-design/icons';
12
+ import { Button, Dropdown, Space, Tooltip, message } from 'antd';
13
+ import type { MenuProps } from 'antd';
14
+ import type { Application } from '@nocobase/client';
15
+ import { useChatBoxActions, useAIConfigRepository, type AIEmployee } from '@nocobase/plugin-ai/client';
16
+ import { useT } from './locale';
17
+
18
+ export const FILE_PREVIEW_WORK_CONTEXT_TYPE = 'file-preview';
19
+
20
+ const AI_EMPLOYEE_STORAGE_KEY = 'plugin-file-preview-auth.aiEmployee';
21
+
22
+ function getFileDisplayName(file: any): string {
23
+ if (!file) return 'file';
24
+ if (file.title && file.extname) return `${file.title}${file.extname}`;
25
+ return file.filename || file.name || file.title || 'file';
26
+ }
27
+
28
+ function getFileContextUid(file: any): string {
29
+ const stableValue = file?.id ?? file?.uid ?? file?.url ?? file?.path ?? getFileDisplayName(file);
30
+ return `file-preview:${String(stableValue)}`;
31
+ }
32
+
33
+ function normalizePreviewFile(file: any) {
34
+ return {
35
+ id: file?.id,
36
+ uid: file?.uid,
37
+ url: file?.url,
38
+ preview: file?.preview,
39
+ filename: file?.filename || file?.name,
40
+ name: file?.name || file?.filename,
41
+ title: file?.title,
42
+ extname: file?.extname,
43
+ mimetype: file?.mimetype,
44
+ size: file?.size,
45
+ path: file?.path,
46
+ storageId: file?.storageId ?? file?.storage_id ?? file?.storage?.id,
47
+ storage_id: file?.storage_id,
48
+ storageType: file?.storageType || file?.storage?.type,
49
+ storageName: file?.storageName || file?.storage?.name,
50
+ storage: file?.storage,
51
+ collectionName: file?.collectionName,
52
+ };
53
+ }
54
+
55
+ export function createFilePreviewWorkContext(file: any) {
56
+ return {
57
+ type: FILE_PREVIEW_WORK_CONTEXT_TYPE,
58
+ uid: getFileContextUid(file),
59
+ title: getFileDisplayName(file),
60
+ content: {
61
+ source: 'plugin-file-preview-auth',
62
+ file: normalizePreviewFile(file),
63
+ },
64
+ };
65
+ }
66
+
67
+ function getStoredAIEmployeeUsername() {
68
+ try {
69
+ return window.localStorage.getItem(AI_EMPLOYEE_STORAGE_KEY) || '';
70
+ } catch {
71
+ return '';
72
+ }
73
+ }
74
+
75
+ function setStoredAIEmployeeUsername(username: string) {
76
+ try {
77
+ window.localStorage.setItem(AI_EMPLOYEE_STORAGE_KEY, username);
78
+ } catch {
79
+ // Ignore storage restrictions in embedded/sandboxed clients.
80
+ }
81
+ }
82
+
83
+ function getEmployeeLabel(employee: AIEmployee) {
84
+ return employee?.nickname || employee?.username || '';
85
+ }
86
+
87
+ class AIFilePreviewActionBoundary extends React.Component<{ children: React.ReactNode }, { hasError: boolean }> {
88
+ constructor(props: { children: React.ReactNode }) {
89
+ super(props);
90
+ this.state = { hasError: false };
91
+ }
92
+
93
+ static getDerivedStateFromError() {
94
+ return { hasError: true };
95
+ }
96
+
97
+ render() {
98
+ if (this.state.hasError) {
99
+ return null;
100
+ }
101
+ return this.props.children;
102
+ }
103
+ }
104
+
105
+ const AIFilePreviewActionInner: React.FC<{ file: any }> = ({ file }) => {
106
+ const t = useT();
107
+ const aiConfigRepository = useAIConfigRepository();
108
+ const { triggerTask } = useChatBoxActions();
109
+ const [employees, setEmployees] = useState<AIEmployee[]>([]);
110
+ const [loading, setLoading] = useState(false);
111
+ const [asking, setAsking] = useState(false);
112
+
113
+ useEffect(() => {
114
+ let cancelled = false;
115
+ if (!aiConfigRepository?.getAIEmployees) {
116
+ return;
117
+ }
118
+
119
+ const cached = aiConfigRepository.aiEmployees || [];
120
+ if (cached.length) {
121
+ setEmployees([...cached]);
122
+ return;
123
+ }
124
+
125
+ setLoading(true);
126
+ aiConfigRepository
127
+ .getAIEmployees()
128
+ .then((list) => {
129
+ if (!cancelled) {
130
+ setEmployees([...(list || [])]);
131
+ setLoading(false);
132
+ }
133
+ })
134
+ .catch(() => {
135
+ if (!cancelled) {
136
+ setEmployees([]);
137
+ setLoading(false);
138
+ }
139
+ });
140
+
141
+ return () => {
142
+ cancelled = true;
143
+ };
144
+ }, [aiConfigRepository]);
145
+
146
+ const orderedEmployees = useMemo(() => {
147
+ const selected = getStoredAIEmployeeUsername();
148
+ if (!selected) {
149
+ return employees;
150
+ }
151
+ return [...employees].sort((a, b) => {
152
+ if (a.username === selected) return -1;
153
+ if (b.username === selected) return 1;
154
+ return 0;
155
+ });
156
+ }, [employees]);
157
+
158
+ const openAIChat = useCallback(
159
+ async (employee: AIEmployee) => {
160
+ if (!employee || !file) {
161
+ return;
162
+ }
163
+
164
+ setAsking(true);
165
+ try {
166
+ setStoredAIEmployeeUsername(employee.username);
167
+ await triggerTask({
168
+ aiEmployee: employee,
169
+ tasks: [
170
+ {
171
+ title: getFileDisplayName(file),
172
+ message: {
173
+ user: t('Please help me analyze the file currently open in preview.'),
174
+ workContext: [createFilePreviewWorkContext(file)],
175
+ },
176
+ autoSend: false,
177
+ },
178
+ ],
179
+ });
180
+ } catch {
181
+ message.error(t('Failed to open AI chat'));
182
+ } finally {
183
+ setAsking(false);
184
+ }
185
+ },
186
+ [file, t, triggerTask],
187
+ );
188
+
189
+ const menuItems: MenuProps['items'] = orderedEmployees.map((employee) => ({
190
+ key: employee.username,
191
+ label: getEmployeeLabel(employee),
192
+ onClick: () => openAIChat(employee),
193
+ }));
194
+
195
+ if (!loading && !orderedEmployees.length) {
196
+ return null;
197
+ }
198
+
199
+ if (orderedEmployees.length === 1) {
200
+ return (
201
+ <Tooltip title={t('Ask AI')}>
202
+ <Button
203
+ type="text"
204
+ size="small"
205
+ icon={<RobotOutlined />}
206
+ loading={asking || loading}
207
+ onClick={(event) => {
208
+ event.stopPropagation();
209
+ openAIChat(orderedEmployees[0]);
210
+ }}
211
+ >
212
+ {t('Ask AI')}
213
+ </Button>
214
+ </Tooltip>
215
+ );
216
+ }
217
+
218
+ return (
219
+ <Tooltip title={t('Ask AI')}>
220
+ <Dropdown menu={{ items: menuItems }} trigger={['click']} placement="bottomRight" disabled={asking || loading}>
221
+ <Button
222
+ type="text"
223
+ size="small"
224
+ icon={<RobotOutlined />}
225
+ loading={asking || loading}
226
+ onClick={(event) => event.stopPropagation()}
227
+ >
228
+ {t('Ask AI')}
229
+ </Button>
230
+ </Dropdown>
231
+ </Tooltip>
232
+ );
233
+ };
234
+
235
+ export const AIFilePreviewAction: React.FC<{ file: any }> = ({ file }) => {
236
+ return (
237
+ <AIFilePreviewActionBoundary>
238
+ <AIFilePreviewActionInner file={file} />
239
+ </AIFilePreviewActionBoundary>
240
+ );
241
+ };
242
+
243
+ export function registerFilePreviewAIWorkContext(app: Application) {
244
+ let aiPlugin: any;
245
+ try {
246
+ aiPlugin = app.pm.get('ai') as any;
247
+ } catch {
248
+ return;
249
+ }
250
+ const aiManager = aiPlugin?.aiManager;
251
+ if (!aiManager?.registerWorkContext) {
252
+ return;
253
+ }
254
+
255
+ const options = {
256
+ name: FILE_PREVIEW_WORK_CONTEXT_TYPE,
257
+ tag: {
258
+ Component: ({ item }: { item: any }) => (
259
+ <Space>
260
+ <FileTextOutlined />
261
+ <span>{item?.title || ''}</span>
262
+ </Space>
263
+ ),
264
+ },
265
+ chatbox: {
266
+ Component: ({ item }: { item: any }) => (
267
+ <Space>
268
+ <FileTextOutlined />
269
+ <span>{item?.title || ''}</span>
270
+ </Space>
271
+ ),
272
+ },
273
+ };
274
+
275
+ try {
276
+ if (!aiManager.getWorkContext?.(FILE_PREVIEW_WORK_CONTEXT_TYPE)) {
277
+ aiManager.registerWorkContext(FILE_PREVIEW_WORK_CONTEXT_TYPE, options);
278
+ }
279
+ } catch {
280
+ // Duplicate registration can happen during hot reload. It is harmless.
281
+ }
282
+ }
@@ -0,0 +1,85 @@
1
+ import { extractOcrStatusRecord, getOcrAttachmentId, isOcrCompleteStatus, normalizeOcrAttachmentId } from '../index';
2
+
3
+ describe('file preview OCR client utils', () => {
4
+ describe('normalizeOcrAttachmentId', () => {
5
+ it('accepts numeric attachment ids', () => {
6
+ expect(normalizeOcrAttachmentId(12)).toBe(12);
7
+ expect(normalizeOcrAttachmentId('12')).toBe('12');
8
+ expect(normalizeOcrAttachmentId(' 12 ')).toBe('12');
9
+ });
10
+
11
+ it('rejects URLs and upload-only ids', () => {
12
+ expect(normalizeOcrAttachmentId('http://localhost/storage/uploads/test.pdf')).toBeNull();
13
+ expect(normalizeOcrAttachmentId('/storage/uploads/test.pdf')).toBeNull();
14
+ expect(normalizeOcrAttachmentId('rc-upload-1710000000000-1')).toBeNull();
15
+ });
16
+ });
17
+
18
+ describe('getOcrAttachmentId', () => {
19
+ it('uses response.id when file.id is a storage URL', () => {
20
+ expect(
21
+ getOcrAttachmentId({
22
+ id: 'http://localhost/storage/uploads/yyyy-test%20(1)-lcqadd.pdf',
23
+ uid: 'rc-upload-1',
24
+ response: {
25
+ id: 42,
26
+ },
27
+ }),
28
+ ).toBe(42);
29
+ });
30
+
31
+ it('falls back to a numeric uid for persisted attachment records', () => {
32
+ expect(
33
+ getOcrAttachmentId({
34
+ id: '/storage/uploads/yyyy-test.pdf',
35
+ uid: '88',
36
+ }),
37
+ ).toBe('88');
38
+ });
39
+ });
40
+
41
+ describe('isOcrCompleteStatus', () => {
42
+ it('only treats result statuses as completed', () => {
43
+ expect(isOcrCompleteStatus('waiting-verify')).toBe(true);
44
+ expect(isOcrCompleteStatus('success')).toBe(true);
45
+ expect(isOcrCompleteStatus('no-ocr')).toBe(false);
46
+ expect(isOcrCompleteStatus('pending-ocr')).toBe(false);
47
+ expect(isOcrCompleteStatus('failed')).toBe(false);
48
+ });
49
+ });
50
+
51
+ describe('extractOcrStatusRecord', () => {
52
+ it('unwraps NocoBase double data responses', () => {
53
+ const record = extractOcrStatusRecord({
54
+ data: {
55
+ data: {
56
+ id: 4,
57
+ attachmentId: 57,
58
+ status: 'waiting-verify',
59
+ data: { pages: [] },
60
+ error: null,
61
+ },
62
+ },
63
+ });
64
+
65
+ expect(record?.id).toBe(4);
66
+ expect(record?.attachmentId).toBe(57);
67
+ expect(record?.status).toBe('waiting-verify');
68
+ });
69
+
70
+ it('unwraps action responses with ok and data', () => {
71
+ const record = extractOcrStatusRecord({
72
+ data: {
73
+ ok: true,
74
+ data: {
75
+ id: 5,
76
+ status: 'pending-ocr',
77
+ },
78
+ },
79
+ });
80
+
81
+ expect(record?.id).toBe(5);
82
+ expect(record?.status).toBe('pending-ocr');
83
+ });
84
+ });
85
+ });