flare-chat-core 0.2.3 → 0.2.4

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 (122) hide show
  1. package/dist/index.js +6343 -0
  2. package/package.json +19 -22
  3. package/docs/CAPABILITY-INVENTORY.md +0 -42
  4. package/docs/CHAT-CORE-BOUNDARY.md +0 -47
  5. package/docs/CORE-APP-REALIGNMENT-WORKLOAD-2026-04-18.md +0 -86
  6. package/docs/SSOT-CHAT-CORE-BOUNDARY.md +0 -73
  7. package/docs/SSOT-CHAT-CORE-DATAFLOW.md +0 -97
  8. package/index.html +0 -12
  9. package/scripts/check.sh +0 -15
  10. package/src/adapters/index.js +0 -6
  11. package/src/adapters/message-api.adapter.js +0 -59
  12. package/src/adapters/session-api.adapter.js +0 -133
  13. package/src/adapters/session-message-api.http.js +0 -161
  14. package/src/adapters/session-message-api.js +0 -34
  15. package/src/adapters/session-message-api.normalize-source-record.test.mjs +0 -180
  16. package/src/adapters/session-message-api.normalizers.js +0 -153
  17. package/src/adapters/source-api.adapter.js +0 -135
  18. package/src/adapters/sse-client.js +0 -244
  19. package/src/adapters/sse-event-dispatcher.js +0 -121
  20. package/src/app/App.jsx +0 -11
  21. package/src/app/AppProviders.jsx +0 -12
  22. package/src/app/ChatWorkspaceScreen.jsx +0 -33
  23. package/src/app/WorkspaceLayout.jsx +0 -190
  24. package/src/app/components/AppCanvasPanel.jsx +0 -64
  25. package/src/app/components/TriggerThresholdPopoverContent.jsx +0 -122
  26. package/src/app/components/WorkspaceBodySection.jsx +0 -156
  27. package/src/app/components/WorkspaceMainPane.jsx +0 -121
  28. package/src/app/components/WorkspaceSessionPane.jsx +0 -70
  29. package/src/app/components/WorkspaceTopBarSection.jsx +0 -71
  30. package/src/app/core-chat-entry/ComposerSectionNode.jsx +0 -241
  31. package/src/app/core-chat-entry/attachmentSendRefs.js +0 -154
  32. package/src/app/core-chat-entry/attachmentSendRefs.test.mjs +0 -101
  33. package/src/app/core-chat-entry/composerActionRouter.js +0 -26
  34. package/src/app/core-chat-entry/constants.js +0 -108
  35. package/src/app/core-chat-entry/selectors.js +0 -28
  36. package/src/app/core-chat-entry/useAppActionErrorGuards.js +0 -68
  37. package/src/app/core-chat-entry/useChatCorePipelines.js +0 -110
  38. package/src/app/core-chat-entry/useComposerModeSuggestion.js +0 -89
  39. package/src/app/core-chat-entry/useDevCapabilityStatusNote.js +0 -22
  40. package/src/app/core-chat-entry/useProjectNameEditing.js +0 -41
  41. package/src/app/core-chat-entry/useProjectSourceUpload.js +0 -341
  42. package/src/app/core-chat-entry/useRealApiReadinessGate.js +0 -103
  43. package/src/app/core-chat-entry/useUnavailableActionError.js +0 -29
  44. package/src/app/core-chat-entry/useWorkspaceCanvasController.jsx +0 -177
  45. package/src/app/core-chat-entry/useWorkspaceCanvasProjection.jsx +0 -171
  46. package/src/app/core-chat-entry/useWorkspaceComposerController.jsx +0 -199
  47. package/src/app/core-chat-entry/useWorkspaceController.jsx +0 -226
  48. package/src/app/core-chat-entry/useWorkspacePanels.js +0 -55
  49. package/src/app/hooks/useComposerAttachmentSync.js +0 -223
  50. package/src/app/hooks/useComposerChooserHandlers.js +0 -52
  51. package/src/app/hooks/useSendWithContextRefs.js +0 -140
  52. package/src/app/hooks/useSendWithContextRefs.test.mjs +0 -29
  53. package/src/app/hooks/useUserThresholdProfile.js +0 -121
  54. package/src/app/index.js +0 -1
  55. package/src/app/selectors/assistantTextSelector.js +0 -73
  56. package/src/app/selectors/canvasEvidenceSummarySelector.js +0 -28
  57. package/src/app/selectors/canvasReportTemplateSelector.js +0 -28
  58. package/src/app/selectors/canvasTabsSelector.js +0 -58
  59. package/src/app/selectors/evidenceProjectionSelector.js +0 -175
  60. package/src/app/selectors/evidenceProjectionSelector.test.mjs +0 -107
  61. package/src/app/selectors/modeSuggestionSelector.js +0 -50
  62. package/src/chat-core/app/mockRuntime.js +0 -291
  63. package/src/chat-core/app/useAppStream.js +0 -187
  64. package/src/chat-core/app/useAppStream.refs.test.mjs +0 -44
  65. package/src/chat-core/app/useAppStream.request-body.test.mjs +0 -116
  66. package/src/chat-core/app/useCoreChatApp.js +0 -115
  67. package/src/chat-core/facade/useBasicConversationFacade.js +0 -280
  68. package/src/chat-core/index.js +0 -14
  69. package/src/chat-core/input/useChatInput.js +0 -103
  70. package/src/chat-core/messages/buildTimelineItems.analysis-route.test.mjs +0 -36
  71. package/src/chat-core/messages/buildTimelineItems.js +0 -201
  72. package/src/chat-core/messages/buildTimelineItems.knowledge-citation.test.mjs +0 -182
  73. package/src/chat-core/messages/contextUsageDefaults.js +0 -3
  74. package/src/chat-core/messages/contextUsageViewModel.js +0 -147
  75. package/src/chat-core/messages/contextUsageViewModel.test.mjs +0 -74
  76. package/src/chat-core/messages/useContextUsageViewModel.js +0 -41
  77. package/src/chat-core/orchestration/useBasicSendHandler.js +0 -55
  78. package/src/chat-core/pipelines/build-action-request.js +0 -46
  79. package/src/chat-core/pipelines/build-stream-request.js +0 -74
  80. package/src/chat-core/pipelines/entity-extraction.js +0 -159
  81. package/src/chat-core/pipelines/preprocess-message.js +0 -16
  82. package/src/chat-core/pipelines/stream-persist-utils.js +0 -32
  83. package/src/chat-core/pipelines/transport/send-mock-stream.js +0 -86
  84. package/src/chat-core/pipelines/transport/send-real-stream.js +0 -330
  85. package/src/chat-core/pipelines/transport/send-real-stream.test.mjs +0 -27
  86. package/src/chat-core/pipelines/transport/send-sourcing-search.js +0 -86
  87. package/src/chat-core/pipelines/transport/send-sourcing-search.test.mjs +0 -14
  88. package/src/chat-core/pipelines/transport/sourcing-response-templates.js +0 -55
  89. package/src/chat-core/pipelines/transport/sourcing-search-api.js +0 -155
  90. package/src/chat-core/runtime/runtimeMode.js +0 -69
  91. package/src/chat-core/session/chatSessionActionTypes.js +0 -24
  92. package/src/chat-core/session/chatSessionReducer.js +0 -352
  93. package/src/chat-core/session/chatSessionReducer.streaming-done.test.mjs +0 -39
  94. package/src/chat-core/session/index.js +0 -2
  95. package/src/chat-core/session/sessionActionsMessages.js +0 -44
  96. package/src/chat-core/session/sessionActionsSessionCrud.js +0 -131
  97. package/src/chat-core/session/sessionActionsStreaming.js +0 -80
  98. package/src/chat-core/session/sessionActionsUiState.js +0 -51
  99. package/src/chat-core/session/useChatSessionReducer.js +0 -131
  100. package/src/chat-core/session/useSessionListController.js +0 -67
  101. package/src/chat-core/stream/sse-client.js +0 -1
  102. package/src/chat-core/stream/sse-event-dispatcher.js +0 -1
  103. package/src/chat-core/stream/sse-events.js +0 -1
  104. package/src/chat-core/stream/useSSEStream.js +0 -1
  105. package/src/chat-core/stream/useStreamSendController.js +0 -46
  106. package/src/contracts/context-ssot.js +0 -47
  107. package/src/contracts/index.js +0 -1
  108. package/src/contracts/sse-events/base-parsers.js +0 -79
  109. package/src/contracts/sse-events/domain-parsers.js +0 -3
  110. package/src/contracts/sse-events/internal-normalizers.js +0 -143
  111. package/src/contracts/sse-events/parsers-intake.js +0 -235
  112. package/src/contracts/sse-events/parsers-runtime.js +0 -37
  113. package/src/contracts/sse-events/parsers-sourcing.js +0 -179
  114. package/src/contracts/sse-events/patch-event-parser.js +0 -121
  115. package/src/contracts/sse-events/runtime-parsers.js +0 -79
  116. package/src/contracts/sse-events.js +0 -4
  117. package/src/index.js +0 -6
  118. package/src/main.jsx +0 -28
  119. package/src/orchestration/index.js +0 -6
  120. package/src/orchestration/useSSEStream.js +0 -221
  121. package/src/state/index.js +0 -4
  122. package/vite.config.js +0 -36
@@ -1,68 +0,0 @@
1
- import { useCallback, useMemo, useState } from 'react';
2
-
3
- function normalizeError(error, fallbackMessage) {
4
- if (error instanceof Error) {
5
- return error;
6
- }
7
- return new Error(String(error || fallbackMessage));
8
- }
9
-
10
- export function useAppActionErrorGuards({
11
- blocked = false,
12
- blockedError,
13
- sessionListError = null,
14
- composerError = null,
15
- onCreateProject,
16
- onCreateSession,
17
- onSend,
18
- } = {}) {
19
- const [actionError, setActionError] = useState(null);
20
-
21
- const handleCreateProject = useCallback(async () => {
22
- if (blocked) {
23
- setActionError(blockedError);
24
- return;
25
- }
26
- try {
27
- setActionError(null);
28
- await onCreateProject?.();
29
- } catch (nextError) {
30
- setActionError(normalizeError(nextError, '创建项目失败'));
31
- }
32
- }, [blocked, blockedError, onCreateProject]);
33
-
34
- const handleCreateSession = useCallback(async () => {
35
- if (blocked) {
36
- setActionError(blockedError);
37
- return;
38
- }
39
- try {
40
- setActionError(null);
41
- await onCreateSession?.();
42
- } catch (nextError) {
43
- setActionError(normalizeError(nextError, '创建会话失败'));
44
- }
45
- }, [blocked, blockedError, onCreateSession]);
46
-
47
- const handleSend = useCallback(async (options = {}) => {
48
- if (blocked) {
49
- setActionError(blockedError);
50
- return;
51
- }
52
- setActionError(null);
53
- const sendResult = await onSend?.(options);
54
- if (sendResult instanceof Error) {
55
- setActionError(sendResult);
56
- }
57
- return sendResult;
58
- }, [blocked, blockedError, onSend]);
59
-
60
- return useMemo(() => ({
61
- actionError,
62
- handleCreateProject,
63
- handleCreateSession,
64
- handleSend,
65
- sessionPaneError: actionError || sessionListError || null,
66
- streamError: actionError || composerError || null,
67
- }), [actionError, composerError, handleCreateProject, handleCreateSession, handleSend, sessionListError]);
68
- }
@@ -1,110 +0,0 @@
1
- import { useMemo } from 'react';
2
- import { useCoreChatApp } from '../../chat-core/app/useCoreChatApp.js';
3
- import { useAppStream } from '../../chat-core/app/useAppStream.js';
4
- import { createMockRuntime } from '../../chat-core/app/mockRuntime.js';
5
- import { createSessionMessageAPI } from '../../adapters/session-message-api.js';
6
- import { useRealApiReadinessGate } from './useRealApiReadinessGate.js';
7
-
8
- export function useChatCorePipelines({
9
- functionType,
10
- defaultSessionTitle,
11
- projectId,
12
- userId,
13
- backendMode,
14
- kernelBaseUrl,
15
- apiBaseUrl,
16
- apiToken,
17
- sessionAPI,
18
- messageAPI,
19
- }) {
20
- const normalizedApiBaseUrl = String(apiBaseUrl || '').trim();
21
- const runtime = useMemo(() => createMockRuntime({
22
- defaultFunctionType: functionType,
23
- defaultSessionTitle,
24
- }), [defaultSessionTitle, functionType]);
25
-
26
- const scope = useMemo(() => ({
27
- projectId,
28
- userId,
29
- }), [projectId, userId]);
30
-
31
- const listParams = useMemo(() => ({
32
- status: 'active',
33
- page: 1,
34
- page_size: 20,
35
- function_type: functionType,
36
- project_id: projectId,
37
- }), [functionType, projectId]);
38
-
39
- const shouldUseRealAPI = backendMode === 'real' && !sessionAPI && !messageAPI;
40
- const apiReadinessGate = useRealApiReadinessGate({
41
- enabled: shouldUseRealAPI,
42
- apiBaseUrl: normalizedApiBaseUrl,
43
- apiToken,
44
- });
45
- const blockedApiError = useMemo(() => (
46
- apiReadinessGate.error || new Error('API 不可用,请检查配置与服务状态')
47
- ), [apiReadinessGate.error]);
48
- const blockedSessionAPI = useMemo(() => ({
49
- list: async () => ({ sessions: [] }),
50
- get: async () => {
51
- throw blockedApiError;
52
- },
53
- create: async () => {
54
- throw blockedApiError;
55
- },
56
- update: async () => {
57
- throw blockedApiError;
58
- },
59
- }), [blockedApiError]);
60
- const blockedMessageAPI = useMemo(() => ({
61
- list: async () => {
62
- throw blockedApiError;
63
- },
64
- }), [blockedApiError]);
65
- const realApi = useMemo(() => {
66
- if (!shouldUseRealAPI || apiReadinessGate.blocked) {
67
- return null;
68
- }
69
- return createSessionMessageAPI({
70
- baseUrl: normalizedApiBaseUrl,
71
- token: apiToken,
72
- });
73
- }, [apiReadinessGate.blocked, apiToken, normalizedApiBaseUrl, shouldUseRealAPI]);
74
- const resolvedSessionAPI = sessionAPI
75
- || realApi?.sessionAPI
76
- || (shouldUseRealAPI ? blockedSessionAPI : runtime.sessionAPI);
77
- const resolvedMessageAPI = messageAPI
78
- || realApi?.messageAPI
79
- || (shouldUseRealAPI ? blockedMessageAPI : runtime.messageAPI);
80
- const useRuntimePersistence = !shouldUseRealAPI && !sessionAPI && !messageAPI;
81
-
82
- const stream = useAppStream({
83
- runtime,
84
- backendMode,
85
- apiBaseUrl: normalizedApiBaseUrl,
86
- kernelBaseUrl,
87
- scope,
88
- persistExchange: useRuntimePersistence ? runtime.appendExchange : null,
89
- });
90
-
91
- const { viewModel } = useCoreChatApp({
92
- sessionAPI: resolvedSessionAPI,
93
- messageAPI: resolvedMessageAPI,
94
- stream,
95
- functionType,
96
- defaultSessionTitle,
97
- listParams,
98
- createSessionPayload: {
99
- project_id: projectId,
100
- user_id: userId,
101
- },
102
- });
103
-
104
- return {
105
- apiReadinessGate,
106
- blockedApiError,
107
- realApi,
108
- viewModel,
109
- };
110
- }
@@ -1,89 +0,0 @@
1
- import { useMemo } from 'react';
2
-
3
- function normalizeText(value) {
4
- return String(value || '').trim().toLowerCase();
5
- }
6
-
7
- function hasAnyKeyword(content, keywords = []) {
8
- return keywords.some((keyword) => content.includes(String(keyword || '').trim().toLowerCase()));
9
- }
10
-
11
- function buildSuggestionFromInput({
12
- inputValue,
13
- activeModeKey,
14
- tipRules = [],
15
- contextFlags = {},
16
- }) {
17
- const resolvedModeKey = String(activeModeKey || '').trim();
18
- if (resolvedModeKey !== 'auto') {
19
- return null;
20
- }
21
- const normalizedInput = normalizeText(inputValue);
22
- const normalizedRules = Array.isArray(tipRules) ? tipRules : [];
23
- if (!normalizedInput && normalizedRules.length === 0) {
24
- return null;
25
- }
26
- const matchedRule = normalizedRules.find((rule) => {
27
- const keywords = Array.isArray(rule?.keywords) ? rule.keywords : [];
28
- const hasKeywordMatch = normalizedInput && hasAnyKeyword(normalizedInput, keywords);
29
- const contextKey = String(rule?.whenContextKey || '').trim();
30
- const hasContextMatch = contextKey ? contextFlags?.[contextKey] === true : false;
31
- return hasKeywordMatch || hasContextMatch;
32
- });
33
- if (!matchedRule) {
34
- return null;
35
- }
36
- return {
37
- source: String(matchedRule.source || 'local_input_tip'),
38
- modeKey: String(matchedRule.modeKey || '').trim(),
39
- label: String(matchedRule.label || '').trim(),
40
- reason: String(matchedRule.reason || '').trim(),
41
- actionText: String(matchedRule.actionText || '').trim(),
42
- statusOnly: matchedRule.statusOnly === true,
43
- };
44
- }
45
-
46
- export default function useComposerModeSuggestion({
47
- inputValue,
48
- activeModeKey,
49
- modeSuggestion,
50
- modeSuggestionAction,
51
- tipRules = [],
52
- contextFlags = {},
53
- }) {
54
- return useMemo(() => {
55
- if (modeSuggestionAction && typeof modeSuggestionAction === 'object') {
56
- return {
57
- modeSuggestion: modeSuggestion || null,
58
- modeSuggestionAction,
59
- fromFallback: false,
60
- };
61
- }
62
- const fallbackSuggestion = buildSuggestionFromInput({
63
- inputValue,
64
- activeModeKey,
65
- tipRules,
66
- contextFlags,
67
- });
68
- if (!fallbackSuggestion) {
69
- return {
70
- modeSuggestion: modeSuggestion || null,
71
- modeSuggestionAction: null,
72
- fromFallback: false,
73
- };
74
- }
75
- return {
76
- modeSuggestion: fallbackSuggestion,
77
- modeSuggestionAction: {
78
- source: 'mode_suggestion',
79
- text: fallbackSuggestion.actionText || `启用${fallbackSuggestion.label || fallbackSuggestion.modeKey || ''}`,
80
- action: {
81
- action_type: 'switch_mode',
82
- target_mode: fallbackSuggestion.modeKey,
83
- label: fallbackSuggestion.actionText || '',
84
- },
85
- },
86
- fromFallback: true,
87
- };
88
- }, [activeModeKey, contextFlags, inputValue, modeSuggestion, modeSuggestionAction, tipRules]);
89
- }
@@ -1,22 +0,0 @@
1
- import { useMemo } from 'react';
2
-
3
- export function useDevCapabilityStatusNote({
4
- isDev = false,
5
- mode = '',
6
- labels = {},
7
- } = {}) {
8
- return useMemo(() => {
9
- if (!isDev) {
10
- return '';
11
- }
12
- const normalizedMode = String(mode || '').trim().toLowerCase();
13
- if (normalizedMode === 'persistent') {
14
- return String(labels.persistent || '开发态:资料能力已接入持久化链路');
15
- }
16
- if (normalizedMode === 'local_fallback') {
17
- return String(labels.localFallback || '开发态:资料能力当前为本地回退模式');
18
- }
19
- return String(labels.unknown || '开发态:资料能力状态未知');
20
- }, [isDev, labels.localFallback, labels.persistent, labels.unknown, mode]);
21
- }
22
-
@@ -1,41 +0,0 @@
1
- import { useState } from 'react';
2
-
3
- export function useProjectNameEditing({ defaultProjectName } = {}) {
4
- const initialName = String(defaultProjectName || '').trim() || '本地演示项目';
5
- const [projectNameEditing, setProjectNameEditing] = useState(false);
6
- const [projectNameDraft, setProjectNameDraft] = useState(initialName);
7
- const [projectDisplayName, setProjectDisplayName] = useState(initialName);
8
-
9
- const handleProjectNameStartEdit = (projectSlot) => {
10
- if (!projectSlot?.key) {
11
- return;
12
- }
13
- setProjectNameDraft(String(projectDisplayName || '').trim());
14
- setProjectNameEditing(true);
15
- };
16
-
17
- const handleProjectNameCancel = () => {
18
- setProjectNameEditing(false);
19
- setProjectNameDraft('');
20
- };
21
-
22
- const handleProjectNameSave = () => {
23
- const nextName = String(projectNameDraft || '').trim();
24
- if (nextName) {
25
- setProjectDisplayName(nextName);
26
- }
27
- handleProjectNameCancel();
28
- };
29
-
30
- return {
31
- projectNameEditing,
32
- projectNameDraft,
33
- setProjectNameDraft,
34
- projectDisplayName,
35
- handleProjectNameStartEdit,
36
- handleProjectNameCancel,
37
- handleProjectNameSave,
38
- };
39
- }
40
-
41
- export default useProjectNameEditing;
@@ -1,341 +0,0 @@
1
- import { useCallback, useEffect, useRef, useState } from 'react';
2
-
3
- const projectSourceListSingleflight = new Map();
4
- const SOURCE_STATUS_TERMINAL = new Set(['ready', 'failed', 'error']);
5
-
6
- function normalizeError(error, fallbackMessage) {
7
- if (error instanceof Error) {
8
- return error;
9
- }
10
- return new Error(String(error || fallbackMessage));
11
- }
12
-
13
- function buildSourceItem(file, index, scopedProjectId) {
14
- return {
15
- id: `project-source-${Date.now()}-${index}`,
16
- project_id: scopedProjectId,
17
- message_id: null,
18
- scope: 'project',
19
- source: 'local_upload',
20
- filename: String(file?.name || '').trim() || `资料 ${index + 1}`,
21
- mime_type: String(file?.type || '').trim(),
22
- size: Number.isFinite(file?.size) ? file.size : null,
23
- created_at: new Date().toISOString(),
24
- status: 'ready',
25
- error_message: '',
26
- persisted_to_project: false,
27
- };
28
- }
29
-
30
- function buildSourceFileSignature(item = {}) {
31
- return `${item.filename || ''}::${item.size || ''}::${item.mime_type || ''}`;
32
- }
33
-
34
- function parseSourceList(response) {
35
- if (Array.isArray(response?.sources)) {
36
- return response.sources;
37
- }
38
- if (Array.isArray(response?.data?.sources)) {
39
- return response.data.sources;
40
- }
41
- return [];
42
- }
43
-
44
- function normalizeSourceStatus(status) {
45
- return String(status || '').trim().toLowerCase();
46
- }
47
-
48
- function hasPendingSourceProcessing(items = []) {
49
- if (!Array.isArray(items) || items.length === 0) {
50
- return false;
51
- }
52
- return items.some((item) => !SOURCE_STATUS_TERMINAL.has(normalizeSourceStatus(item?.status)));
53
- }
54
-
55
- function createUploadProgressItem(file, index) {
56
- return {
57
- id: `upload-progress-${Date.now()}-${index}`,
58
- filename: String(file?.name || '').trim() || `资料 ${index + 1}`,
59
- progress: 0,
60
- stage: 'reading',
61
- error_message: '',
62
- };
63
- }
64
-
65
- function readFileWithProgress(file, onProgress) {
66
- return new Promise((resolve, reject) => {
67
- const reader = new FileReader();
68
- reader.onprogress = (event) => {
69
- if (!event.lengthComputable) {
70
- return;
71
- }
72
- const percent = (event.loaded / event.total) * 80;
73
- onProgress(percent);
74
- };
75
- reader.onload = () => {
76
- onProgress(80);
77
- resolve();
78
- };
79
- reader.onerror = () => {
80
- reject(reader.error || new Error('读取文件失败'));
81
- };
82
- reader.readAsArrayBuffer(file);
83
- });
84
- }
85
-
86
- export function useProjectSourceUpload({
87
- projectId = '',
88
- userId = '',
89
- sourceAPI = null,
90
- shouldLoadSources = true,
91
- } = {}) {
92
- const fileInputRef = useRef(null);
93
- const [sourceItems, setSourceItems] = useState([]);
94
- const [sourceActionError, setSourceActionError] = useState(null);
95
- const [sourceUploadLoading, setSourceUploadLoading] = useState(false);
96
- const [sourceUploadProgressItems, setSourceUploadProgressItems] = useState([]);
97
- const [sourceSyncLoading, setSourceSyncLoading] = useState(false);
98
- const [sourceRemovingId, setSourceRemovingId] = useState('');
99
- const [sourceEndpointUnavailable, setSourceEndpointUnavailable] = useState(false);
100
-
101
- const canUsePersistentSourceAPI = (
102
- typeof sourceAPI?.listProjectSources === 'function'
103
- && typeof sourceAPI?.uploadSourceFile === 'function'
104
- && typeof sourceAPI?.deleteProjectSource === 'function'
105
- && !sourceEndpointUnavailable
106
- );
107
-
108
- const loadProjectSources = useCallback(async ({ silent = false } = {}) => {
109
- const scopedProjectId = String(projectId || '').trim();
110
- if (!canUsePersistentSourceAPI || !scopedProjectId) {
111
- return;
112
- }
113
- if (!silent) {
114
- setSourceSyncLoading(true);
115
- setSourceActionError(null);
116
- }
117
- try {
118
- const requestKey = scopedProjectId;
119
- let pendingRequest = projectSourceListSingleflight.get(requestKey);
120
- if (!pendingRequest) {
121
- pendingRequest = sourceAPI.listProjectSources(scopedProjectId)
122
- .finally(() => {
123
- projectSourceListSingleflight.delete(requestKey);
124
- });
125
- projectSourceListSingleflight.set(requestKey, pendingRequest);
126
- }
127
- const response = await pendingRequest;
128
- setSourceItems(parseSourceList(response));
129
- setSourceEndpointUnavailable(false);
130
- } catch (error) {
131
- if (Number(error?.status) === 404) {
132
- setSourceEndpointUnavailable(true);
133
- if (!silent) {
134
- setSourceActionError(null);
135
- }
136
- return;
137
- }
138
- if (!silent) {
139
- setSourceActionError(normalizeError(error, '加载资料失败'));
140
- }
141
- } finally {
142
- if (!silent) {
143
- setSourceSyncLoading(false);
144
- }
145
- }
146
- }, [canUsePersistentSourceAPI, projectId, sourceAPI]);
147
-
148
- useEffect(() => {
149
- if (!shouldLoadSources) {
150
- return;
151
- }
152
- loadProjectSources();
153
- }, [loadProjectSources, shouldLoadSources]);
154
-
155
- useEffect(() => {
156
- if (!shouldLoadSources) {
157
- return undefined;
158
- }
159
- if (!canUsePersistentSourceAPI) {
160
- return undefined;
161
- }
162
- if (!hasPendingSourceProcessing(sourceItems)) {
163
- return undefined;
164
- }
165
- const timer = setInterval(() => {
166
- void loadProjectSources({ silent: true });
167
- }, 1500);
168
- return () => {
169
- clearInterval(timer);
170
- };
171
- }, [canUsePersistentSourceAPI, loadProjectSources, shouldLoadSources, sourceItems]);
172
-
173
- const appendLocalSourceFiles = useCallback((nativeFiles = []) => {
174
- setSourceItems((prev) => {
175
- const existingSignatures = new Set(prev.map((item) => buildSourceFileSignature(item)));
176
- const nextItems = nativeFiles
177
- .map((file, index) => buildSourceItem(file, index, projectId))
178
- .filter((item) => {
179
- const signature = buildSourceFileSignature(item);
180
- if (existingSignatures.has(signature)) {
181
- return false;
182
- }
183
- existingSignatures.add(signature);
184
- return true;
185
- });
186
- return [...nextItems, ...prev];
187
- });
188
- }, [projectId]);
189
-
190
- const uploadSourceFiles = useCallback(async (nativeFiles = []) => {
191
- const scopedProjectId = String(projectId || '').trim();
192
- if (nativeFiles.length === 0) {
193
- return;
194
- }
195
- setSourceUploadLoading(true);
196
- setSourceActionError(null);
197
- const progressItems = nativeFiles.map((file, index) => createUploadProgressItem(file, index));
198
- setSourceUploadProgressItems((prev) => [...progressItems, ...prev]);
199
- const updateProgressItem = (itemId, patch = {}) => {
200
- setSourceUploadProgressItems((prev) => prev.map((item) => (item.id === itemId ? { ...item, ...patch } : item)));
201
- };
202
- try {
203
- if (canUsePersistentSourceAPI && scopedProjectId) {
204
- for (let index = 0; index < nativeFiles.length; index += 1) {
205
- const file = nativeFiles[index];
206
- const progressItem = progressItems[index];
207
- await readFileWithProgress(file, (percent) => {
208
- updateProgressItem(progressItem.id, {
209
- stage: 'reading',
210
- progress: percent,
211
- });
212
- });
213
- updateProgressItem(progressItem.id, {
214
- stage: 'uploading',
215
- progress: 80,
216
- });
217
- await sourceAPI.uploadSourceFile({
218
- project_id: scopedProjectId,
219
- user_id: String(userId || '').trim() || null,
220
- file,
221
- filename: file.name,
222
- onProgress: (percent) => {
223
- const safePercent = Number.isFinite(percent) ? percent : 0;
224
- const mappedPercent = 80 + (Math.max(0, Math.min(100, safePercent)) * 0.2);
225
- updateProgressItem(progressItem.id, {
226
- stage: 'uploading',
227
- progress: mappedPercent,
228
- });
229
- },
230
- });
231
- updateProgressItem(progressItem.id, {
232
- stage: 'done',
233
- progress: 100,
234
- });
235
- }
236
- await loadProjectSources();
237
- return;
238
- }
239
- for (let index = 0; index < nativeFiles.length; index += 1) {
240
- const file = nativeFiles[index];
241
- const progressItem = progressItems[index];
242
- await readFileWithProgress(file, (percent) => {
243
- updateProgressItem(progressItem.id, {
244
- stage: 'reading',
245
- progress: percent,
246
- });
247
- });
248
- updateProgressItem(progressItem.id, {
249
- stage: 'uploading',
250
- progress: 95,
251
- });
252
- appendLocalSourceFiles([file]);
253
- updateProgressItem(progressItem.id, {
254
- stage: 'done',
255
- progress: 100,
256
- });
257
- }
258
- } catch (error) {
259
- const errorMessage = String(error?.message || '添加资料失败');
260
- setSourceUploadProgressItems((prev) => prev.map((item) => (item.stage === 'done'
261
- ? item
262
- : {
263
- ...item,
264
- stage: 'error',
265
- error_message: errorMessage,
266
- })));
267
- setSourceActionError(normalizeError(error, '添加资料失败'));
268
- } finally {
269
- setSourceUploadLoading(false);
270
- }
271
- }, [appendLocalSourceFiles, canUsePersistentSourceAPI, loadProjectSources, projectId, sourceAPI, userId]);
272
-
273
- const handleOpenSourcePicker = useCallback((nativeFiles = []) => {
274
- if (sourceUploadLoading) {
275
- return;
276
- }
277
- if (Array.isArray(nativeFiles) && nativeFiles.length > 0) {
278
- void uploadSourceFiles(nativeFiles);
279
- return;
280
- }
281
- setSourceActionError(null);
282
- fileInputRef.current?.click();
283
- }, [sourceUploadLoading, uploadSourceFiles]);
284
-
285
- const handleSourceFileChange = useCallback((event) => {
286
- const nativeFiles = Array.from(event?.target?.files || []);
287
- event.target.value = '';
288
- void uploadSourceFiles(nativeFiles);
289
- }, [uploadSourceFiles]);
290
-
291
- const handleRemoveSource = useCallback(async (sourceId) => {
292
- const scopedProjectId = String(projectId || '').trim();
293
- const normalizedSourceId = String(sourceId || '').trim();
294
- if (!normalizedSourceId) {
295
- return;
296
- }
297
- setSourceActionError(null);
298
- setSourceRemovingId(normalizedSourceId);
299
- try {
300
- if (canUsePersistentSourceAPI && scopedProjectId) {
301
- await sourceAPI.deleteProjectSource(scopedProjectId, normalizedSourceId);
302
- await loadProjectSources();
303
- } else {
304
- setSourceItems((prev) => prev.filter((item) => {
305
- const currentSourceId = String(item?.source_id || item?.id || '').trim();
306
- return currentSourceId !== normalizedSourceId;
307
- }));
308
- }
309
- } catch (error) {
310
- setSourceActionError(normalizeError(error, '删除资料失败'));
311
- } finally {
312
- setSourceRemovingId('');
313
- }
314
- }, [canUsePersistentSourceAPI, loadProjectSources, projectId, sourceAPI]);
315
-
316
- const handleRetrySource = useCallback(() => {
317
- if (canUsePersistentSourceAPI) {
318
- loadProjectSources();
319
- return;
320
- }
321
- setSourceActionError(null);
322
- }, [canUsePersistentSourceAPI, loadProjectSources]);
323
-
324
- const handleViewSourceDetail = useCallback(() => {}, []);
325
-
326
- return {
327
- fileInputRef,
328
- sourceCapabilityMode: canUsePersistentSourceAPI ? 'persistent' : 'local_fallback',
329
- sourceActionError,
330
- sourceItems,
331
- sourceRemovingId,
332
- sourceSyncLoading,
333
- sourceUploadLoading,
334
- sourceUploadProgressItems,
335
- handleOpenSourcePicker,
336
- handleSourceFileChange,
337
- handleRemoveSource,
338
- handleRetrySource,
339
- handleViewSourceDetail,
340
- };
341
- }