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,29 +0,0 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import {
4
- isSourcingRefreshCommand,
5
- resolveSourcingPayloadExtra,
6
- } from './useSendWithContextRefs.js';
7
-
8
- test('isSourcingRefreshCommand detects supported refresh commands', () => {
9
- assert.equal(isSourcingRefreshCommand('换一批'), true);
10
- assert.equal(isSourcingRefreshCommand('能不能换一批'), true);
11
- assert.equal(isSourcingRefreshCommand('继续'), true);
12
- assert.equal(isSourcingRefreshCommand('重新检索'), false);
13
- });
14
-
15
- test('resolveSourcingPayloadExtra pins sourcing_query for refresh command', () => {
16
- const payloadExtra = resolveSourcingPayloadExtra({
17
- streamOptions: {
18
- payloadExtra: { source_scope: 'hybrid' },
19
- },
20
- isSourcingMode: true,
21
- currentInput: '换一批',
22
- latestSourcingQuery: '上海 ERP 供应商',
23
- latestSourcingResultIds: ['r1', 'r2', ''],
24
- });
25
- assert.equal(payloadExtra.sourcing_query, '上海 ERP 供应商');
26
- assert.equal(payloadExtra.append_mode, true);
27
- assert.equal(payloadExtra.lock_existing_order, true);
28
- assert.deepEqual(payloadExtra.base_result_ids, ['r1', 'r2']);
29
- });
@@ -1,121 +0,0 @@
1
- import { useEffect, useMemo, useState } from 'react';
2
-
3
- const DEFAULT_THRESHOLD_PROFILE = Object.freeze({
4
- evidenceStrictMode: true,
5
- evidenceHitScoreThreshold: 0.05,
6
- languageGuardEnabled: true,
7
- friendlyToneEnabled: true,
8
- questionModeSuggestionEnabled: true,
9
- });
10
-
11
- function resolveStorageKey({ userId, projectId }) {
12
- return `flare:threshold-profile:${String(userId || 'anonymous').trim()}:${String(projectId || 'default').trim()}`;
13
- }
14
-
15
- function sanitizeBoolean(value, fallback) {
16
- if (typeof value !== 'boolean') {
17
- return fallback;
18
- }
19
- return value;
20
- }
21
-
22
- function sanitizeThreshold(value) {
23
- const normalized = Number(value);
24
- if (!Number.isFinite(normalized)) {
25
- return DEFAULT_THRESHOLD_PROFILE.evidenceHitScoreThreshold;
26
- }
27
- if (normalized < 0) {
28
- return 0;
29
- }
30
- if (normalized > 1) {
31
- return 1;
32
- }
33
- return normalized;
34
- }
35
-
36
- function sanitizeProfile(payload) {
37
- const source = payload && typeof payload === 'object' ? payload : {};
38
- return {
39
- evidenceStrictMode: sanitizeBoolean(
40
- source.evidenceStrictMode,
41
- DEFAULT_THRESHOLD_PROFILE.evidenceStrictMode,
42
- ),
43
- evidenceHitScoreThreshold: sanitizeThreshold(source.evidenceHitScoreThreshold),
44
- languageGuardEnabled: sanitizeBoolean(
45
- source.languageGuardEnabled,
46
- DEFAULT_THRESHOLD_PROFILE.languageGuardEnabled,
47
- ),
48
- friendlyToneEnabled: sanitizeBoolean(
49
- source.friendlyToneEnabled,
50
- DEFAULT_THRESHOLD_PROFILE.friendlyToneEnabled,
51
- ),
52
- questionModeSuggestionEnabled: sanitizeBoolean(
53
- source.questionModeSuggestionEnabled,
54
- DEFAULT_THRESHOLD_PROFILE.questionModeSuggestionEnabled,
55
- ),
56
- };
57
- }
58
-
59
- function loadProfileFromStorage(storageKey) {
60
- if (typeof window === 'undefined' || !window.localStorage) {
61
- return DEFAULT_THRESHOLD_PROFILE;
62
- }
63
- const raw = window.localStorage.getItem(storageKey);
64
- if (!raw) {
65
- return DEFAULT_THRESHOLD_PROFILE;
66
- }
67
- try {
68
- return sanitizeProfile(JSON.parse(raw));
69
- } catch (_error) {
70
- return DEFAULT_THRESHOLD_PROFILE;
71
- }
72
- }
73
-
74
- export function useUserThresholdProfile({
75
- userId,
76
- projectId,
77
- } = {}) {
78
- const storageKey = useMemo(() => resolveStorageKey({ userId, projectId }), [projectId, userId]);
79
- const [profile, setProfile] = useState(() => loadProfileFromStorage(storageKey));
80
-
81
- useEffect(() => {
82
- setProfile(loadProfileFromStorage(storageKey));
83
- }, [storageKey]);
84
-
85
- useEffect(() => {
86
- if (typeof window === 'undefined' || !window.localStorage) {
87
- return;
88
- }
89
- window.localStorage.setItem(storageKey, JSON.stringify(profile));
90
- }, [profile, storageKey]);
91
-
92
- return {
93
- evidenceStrictMode: profile.evidenceStrictMode,
94
- setEvidenceStrictMode: (value) => setProfile((prev) => ({
95
- ...prev,
96
- evidenceStrictMode: Boolean(value),
97
- })),
98
- evidenceHitScoreThreshold: profile.evidenceHitScoreThreshold,
99
- setEvidenceHitScoreThreshold: (value) => setProfile((prev) => ({
100
- ...prev,
101
- evidenceHitScoreThreshold: sanitizeThreshold(value),
102
- })),
103
- languageGuardEnabled: profile.languageGuardEnabled,
104
- setLanguageGuardEnabled: (value) => setProfile((prev) => ({
105
- ...prev,
106
- languageGuardEnabled: Boolean(value),
107
- })),
108
- friendlyToneEnabled: profile.friendlyToneEnabled,
109
- setFriendlyToneEnabled: (value) => setProfile((prev) => ({
110
- ...prev,
111
- friendlyToneEnabled: Boolean(value),
112
- })),
113
- questionModeSuggestionEnabled: profile.questionModeSuggestionEnabled,
114
- setQuestionModeSuggestionEnabled: (value) => setProfile((prev) => ({
115
- ...prev,
116
- questionModeSuggestionEnabled: Boolean(value),
117
- })),
118
- };
119
- }
120
-
121
- export default useUserThresholdProfile;
package/src/app/index.js DELETED
@@ -1 +0,0 @@
1
- export { default as App } from './App.jsx';
@@ -1,73 +0,0 @@
1
- const SYSTEM_TRACE_TOKENS = ['mode=', 'intent=', 'source=', 'provider_status', 'profile=', 'tokens≈', 'goal='];
2
-
3
- export function sanitizeAssistantText(rawText) {
4
- const lines = String(rawText || '')
5
- .split('\n')
6
- .map((line) => String(line || '').trim())
7
- .filter(Boolean);
8
- const cleaned = lines.filter((line) => !SYSTEM_TRACE_TOKENS.some((token) => line.includes(token)));
9
- return cleaned.join('\n').trim();
10
- }
11
-
12
- function hasAssistantQuestionCue(text = '') {
13
- const normalized = String(text || '').trim();
14
- if (!normalized) {
15
- return false;
16
- }
17
- const hasQuestionMark = normalized.includes('?') || normalized.includes('?');
18
- const hasReplyCue = ['请告诉', '请确认', '请选择', '请补充', '请回复'].some((cue) => normalized.includes(cue));
19
- return hasQuestionMark || hasReplyCue;
20
- }
21
-
22
- function applyFriendlyTone(text = '', responseStylePolicy = {}) {
23
- const normalized = String(text || '').trim();
24
- if (!normalized) {
25
- return '';
26
- }
27
- const lines = normalized.split('\n');
28
- const firstLine = String(lines[0] || '').trim();
29
- const hasOpening = ['好的', '收到', '明白', '了解'].some((prefix) => firstLine.startsWith(prefix));
30
- const withOpening = hasOpening
31
- ? normalized
32
- : `${String(responseStylePolicy.friendlyPrefix || '').trim()}\n${normalized}`;
33
- const hasQuestion = hasAssistantQuestionCue(withOpening);
34
- const questionReplyHint = String(responseStylePolicy.questionReplyHint || '').trim();
35
- const hasEndingHint = questionReplyHint && withOpening.includes(questionReplyHint);
36
- if (hasQuestion && questionReplyHint && !hasEndingHint) {
37
- return `${withOpening}\n\n${questionReplyHint}`;
38
- }
39
- return withOpening;
40
- }
41
-
42
- export function selectRenderedTimelineItems({
43
- timelineItems,
44
- languageGuardEnabled,
45
- friendlyToneEnabled,
46
- responseStylePolicy,
47
- } = {}) {
48
- const resolvedTimeline = Array.isArray(timelineItems) ? timelineItems : [];
49
- if (!languageGuardEnabled && !friendlyToneEnabled) {
50
- return resolvedTimeline;
51
- }
52
-
53
- return resolvedTimeline.map((item) => {
54
- if (!item || typeof item !== 'object') {
55
- return item;
56
- }
57
- const shouldProcess = (item.type === 'message' && item.role === 'assistant') || item.type === 'streaming';
58
- if (!shouldProcess) {
59
- return item;
60
- }
61
- const normalizedContent = languageGuardEnabled
62
- ? sanitizeAssistantText(item.content)
63
- : String(item.content || '');
64
- return {
65
- ...item,
66
- content: friendlyToneEnabled
67
- ? applyFriendlyTone(normalizedContent, responseStylePolicy)
68
- : normalizedContent,
69
- };
70
- });
71
- }
72
-
73
- export default selectRenderedTimelineItems;
@@ -1,28 +0,0 @@
1
- function normalizeSourceType(value) {
2
- const rawType = String(value || '').trim().toLowerCase();
3
- if (rawType.includes('web')) {
4
- return 'web';
5
- }
6
- if (rawType.includes('mcp')) {
7
- return 'mcp';
8
- }
9
- return 'local';
10
- }
11
-
12
- export function selectCanvasEvidenceSummary(canvasEvidenceItems = []) {
13
- const sourceBreakdown = { local: 0, mcp: 0, web: 0 };
14
- const evidenceRows = Array.isArray(canvasEvidenceItems) ? canvasEvidenceItems : [];
15
-
16
- evidenceRows.forEach((row) => {
17
- const sourceType = normalizeSourceType(row?.source_type || row?.source);
18
- sourceBreakdown[sourceType] += 1;
19
- });
20
-
21
- return {
22
- exploredCount: evidenceRows.length,
23
- hitCount: evidenceRows.length,
24
- sourceBreakdown,
25
- };
26
- }
27
-
28
- export default selectCanvasEvidenceSummary;
@@ -1,28 +0,0 @@
1
- export function selectCanvasReportTemplate(timelineItems = []) {
2
- const items = Array.isArray(timelineItems) ? timelineItems : [];
3
- for (let index = items.length - 1; index >= 0; index -= 1) {
4
- const item = items[index];
5
- const payload = item?.payload && typeof item.payload === 'object' ? item.payload : null;
6
- if (!payload) {
7
- continue;
8
- }
9
- const rawTemplate = (
10
- payload.canvas_report_template
11
- || payload.report_template
12
- || payload.prompt_summary_template
13
- );
14
- if (!rawTemplate || typeof rawTemplate !== 'object' || Array.isArray(rawTemplate)) {
15
- continue;
16
- }
17
- const sectionOrder = Array.isArray(rawTemplate.sectionOrder)
18
- ? rawTemplate.sectionOrder
19
- : (Array.isArray(rawTemplate.section_order) ? rawTemplate.section_order : undefined);
20
- return {
21
- ...rawTemplate,
22
- ...(sectionOrder ? { sectionOrder } : {}),
23
- };
24
- }
25
- return null;
26
- }
27
-
28
- export default selectCanvasReportTemplate;
@@ -1,58 +0,0 @@
1
- function isSourcingEvidence(row) {
2
- return String(row?.evidence_kind || '').trim() === 'sourcing';
3
- }
4
-
5
- export function selectCanvasEvidencePresence(canvasEvidenceItems = []) {
6
- const evidenceRows = Array.isArray(canvasEvidenceItems) ? canvasEvidenceItems : [];
7
- return {
8
- hasSourcingEvidenceRows: evidenceRows.some((row) => isSourcingEvidence(row)),
9
- hasRetrievalEvidenceRows: evidenceRows.some((row) => !isSourcingEvidence(row)),
10
- };
11
- }
12
-
13
- export function selectCanvasTabs({
14
- isPlanMode,
15
- canvasReportTemplate,
16
- hasSourcingEvidenceRows,
17
- hasRetrievalEvidenceRows,
18
- resolvedUILabels,
19
- } = {}) {
20
- return [
21
- ...(isPlanMode ? [{ key: 'requirement', label: resolvedUILabels?.canvas_tab_result || '工作内容' }] : []),
22
- ...(canvasReportTemplate ? [{ key: 'analysis', label: '分析报告' }] : []),
23
- ...(hasSourcingEvidenceRows ? [{ key: 'sourcing', label: '寻源结果' }] : []),
24
- ...(hasRetrievalEvidenceRows ? [{ key: 'evidence', label: '检索证据' }] : []),
25
- ];
26
- }
27
-
28
- export function selectResolvedCanvasEvidenceTabKey({ hasSourcingEvidenceRows, canvasActiveTabKey } = {}) {
29
- return hasSourcingEvidenceRows && canvasActiveTabKey === 'sourcing'
30
- ? 'sourcing'
31
- : 'retrieval';
32
- }
33
-
34
- export function selectCanvasAutoTab({
35
- canvasEvidenceItems,
36
- canvasFocusEvidenceAnchorId,
37
- showCanvasPanel,
38
- canvasActiveTabKey,
39
- } = {}) {
40
- const evidenceRows = Array.isArray(canvasEvidenceItems) ? canvasEvidenceItems : [];
41
- if (!evidenceRows.length || !showCanvasPanel) {
42
- return '';
43
- }
44
- if (String(canvasFocusEvidenceAnchorId || '').trim()) {
45
- return 'evidence';
46
- }
47
- if (String(canvasActiveTabKey || '').trim()) {
48
- return '';
49
- }
50
- return evidenceRows.some((row) => isSourcingEvidence(row)) ? 'sourcing' : 'evidence';
51
- }
52
-
53
- export function selectShouldAutoOpenCanvasForSourcing({
54
- hasSourcingEvidenceRows,
55
- hadSourcingEvidence,
56
- } = {}) {
57
- return Boolean(hasSourcingEvidenceRows && !hadSourcingEvidence);
58
- }
@@ -1,175 +0,0 @@
1
- const DEFAULT_EVIDENCE_HIT_SCORE_THRESHOLD = 0.05;
2
- const NOISY_EVIDENCE_TERMS = new Set(['周年庆', '方案', '采购', '礼品', '需求', '项目']);
3
-
4
- function extractInformativeTerms(value) {
5
- const normalized = String(value || '').toLowerCase();
6
- if (!normalized) {
7
- return [];
8
- }
9
- const chinese = normalized.match(/[\u4e00-\u9fa5]{2,}/g) || [];
10
- const english = normalized.match(/[a-z][a-z0-9_]{2,}/g) || [];
11
- return Array.from(new Set([...chinese, ...english]))
12
- .map((term) => term.trim())
13
- .filter((term) => term && !NOISY_EVIDENCE_TERMS.has(term));
14
- }
15
-
16
- function resolveEvidenceHitScoreThreshold(value) {
17
- const normalized = Number(value);
18
- if (!Number.isFinite(normalized)) {
19
- return DEFAULT_EVIDENCE_HIT_SCORE_THRESHOLD;
20
- }
21
- if (normalized < 0) {
22
- return 0;
23
- }
24
- if (normalized > 1) {
25
- return 1;
26
- }
27
- return normalized;
28
- }
29
-
30
- function isEvidenceHit({ score, snippet, query, matchedTerms = [] }, strictMode = true, scoreThreshold = DEFAULT_EVIDENCE_HIT_SCORE_THRESHOLD) {
31
- if (Number.isFinite(Number(score))) {
32
- return Number(score) >= resolveEvidenceHitScoreThreshold(scoreThreshold);
33
- }
34
- const normalizedSnippet = String(snippet || '').trim();
35
- const normalizedQuery = String(query || '').trim();
36
- if (!normalizedSnippet || normalizedSnippet.length < 12) {
37
- return false;
38
- }
39
- const matchedTermList = Array.isArray(matchedTerms)
40
- ? matchedTerms.map((item) => String(item || '').trim()).filter(Boolean)
41
- : [];
42
- if (!strictMode) {
43
- if (!normalizedQuery || normalizedQuery.length < 2) {
44
- return true;
45
- }
46
- return normalizedSnippet.toLowerCase().includes(normalizedQuery.toLowerCase());
47
- }
48
- if (matchedTermList.length > 0) {
49
- return true;
50
- }
51
- const queryTerms = extractInformativeTerms(normalizedQuery);
52
- if (queryTerms.length === 0) {
53
- return false;
54
- }
55
- const snippetTerms = extractInformativeTerms(normalizedSnippet);
56
- if (snippetTerms.length === 0) {
57
- return false;
58
- }
59
- const overlapCount = queryTerms.filter((term) => snippetTerms.includes(term)).length;
60
- return overlapCount >= 2;
61
- }
62
-
63
- function normalizeSourceType(value) {
64
- const raw = String(value || '').trim().toLowerCase();
65
- if (raw.includes('web')) {
66
- return 'web';
67
- }
68
- if (raw.includes('mcp')) {
69
- return 'mcp';
70
- }
71
- return 'local';
72
- }
73
-
74
- function mapKnowledgeResultToEvidenceRow(result, index, query, runId) {
75
- return {
76
- key: String(result.id || result.record_id || result.source_id || `${runId}-${index}`),
77
- citation_id: String(result.citation_id || result.citationId || '').trim() || undefined,
78
- evidence_kind: 'retrieval',
79
- title: String(result.title || result.name || result.vendor_name || '证据条目'),
80
- source_type: normalizeSourceType(result.source_type || result.source),
81
- score: Number.isFinite(Number(result.score)) ? Number(result.score) : undefined,
82
- run_id: runId,
83
- query,
84
- snippet: String(result.snippet || result.summary || result.excerpt || result.content || '').trim(),
85
- matchedTerms: Array.isArray(result.matched_terms) ? result.matched_terms : [],
86
- source_url: String(result.source_url || result.url || '').trim() || undefined,
87
- };
88
- }
89
-
90
- function mapSourcingCandidateToEvidenceRow(candidate, index, query, runId) {
91
- const reasons = Array.isArray(candidate.match_reasons) ? candidate.match_reasons : [];
92
- const summary = String(candidate.summary || reasons[0] || '').trim();
93
- const firstRef = Array.isArray(candidate.evidence_refs) ? candidate.evidence_refs[0] : null;
94
- const canonicalUrl = String(candidate.canonical_url || firstRef?.url || candidate.source_url || '').trim();
95
- return {
96
- key: String(candidate.id || candidate.supplier_id || `${runId}-candidate-${index}`),
97
- result_id: String(candidate.result_id || candidate.id || candidate.supplier_id || `${runId}-candidate-${index}`),
98
- evidence_kind: 'sourcing',
99
- title: String(candidate.source_title || candidate.title || candidate.supplier_name || candidate.vendor_name || '候选供应商'),
100
- supplier_name: String(candidate.supplier_name || candidate.source_title || candidate.title || candidate.vendor_name || '候选供应商'),
101
- source_title: String(candidate.source_title || candidate.title || candidate.supplier_name || ''),
102
- source_domain: String(candidate.source_domain || ''),
103
- source_meta: (
104
- candidate?.source_meta
105
- && typeof candidate.source_meta === 'object'
106
- && !Array.isArray(candidate.source_meta)
107
- ) ? candidate.source_meta : {},
108
- canonical_url: canonicalUrl || undefined,
109
- source_type: normalizeSourceType(candidate.source_type || candidate.source),
110
- list_group: 'pending_verify',
111
- score: Number.isFinite(Number(candidate.match_score || candidate.score))
112
- ? Number(candidate.match_score || candidate.score)
113
- : undefined,
114
- confidence: candidate?.confidence ?? '',
115
- confidence_detail: (
116
- candidate?.confidence_detail
117
- && typeof candidate.confidence_detail === 'object'
118
- && !Array.isArray(candidate.confidence_detail)
119
- ) ? candidate.confidence_detail : null,
120
- run_id: runId,
121
- query,
122
- snippet: summary,
123
- matchedTerms: reasons,
124
- source_url: canonicalUrl || undefined,
125
- };
126
- }
127
-
128
- export function selectProjectedCanvasEvidenceItems({
129
- timelineItems,
130
- composerModeKey,
131
- evidenceStrictMode,
132
- evidenceHitScoreThreshold = DEFAULT_EVIDENCE_HIT_SCORE_THRESHOLD,
133
- } = {}) {
134
- const rows = [];
135
- const resolvedTimeline = Array.isArray(timelineItems) ? timelineItems : [];
136
-
137
- resolvedTimeline.forEach((item) => {
138
- const itemType = String(item?.type || '').trim();
139
- const payload = item?.payload && typeof item.payload === 'object' ? item.payload : {};
140
-
141
- if (itemType === 'knowledge_search') {
142
- const query = String(payload?.query || '').trim();
143
- const runId = String(payload?.run_id || '').trim();
144
- const results = Array.isArray(payload?.results) ? payload.results : [];
145
- results.forEach((result, index) => {
146
- if (!result || typeof result !== 'object') {
147
- return;
148
- }
149
- const nextRow = mapKnowledgeResultToEvidenceRow(result, index, query, runId);
150
- if (!isEvidenceHit(nextRow, evidenceStrictMode, evidenceHitScoreThreshold)) {
151
- return;
152
- }
153
- rows.push(nextRow);
154
- });
155
- return;
156
- }
157
-
158
- if (itemType === 'sourcing_candidates') {
159
- const query = String(payload?.query || payload?.message_excerpt || '').trim();
160
- const runId = String(payload?.run_id || '').trim();
161
- const candidates = Array.isArray(payload?.candidates) ? payload.candidates : [];
162
- candidates.forEach((candidate, index) => {
163
- if (!candidate || typeof candidate !== 'object') {
164
- return;
165
- }
166
- const nextRow = mapSourcingCandidateToEvidenceRow(candidate, index, query, runId);
167
- rows.push(nextRow);
168
- });
169
- }
170
- });
171
-
172
- return rows;
173
- }
174
-
175
- export default selectProjectedCanvasEvidenceItems;
@@ -1,107 +0,0 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { selectProjectedCanvasEvidenceItems } from './evidenceProjectionSelector.js';
4
-
5
- test('selectProjectedCanvasEvidenceItems rebuilds evidence rows from knowledge_citation timeline item', () => {
6
- const rows = selectProjectedCanvasEvidenceItems({
7
- timelineItems: [
8
- {
9
- type: 'knowledge_citation',
10
- payload: {
11
- run_id: 'retrieval-run-1',
12
- query: '总结附件',
13
- citations: [
14
- {
15
- citation_id: 'cite_1',
16
- title: '采购需求文档.pdf',
17
- source_id: 'src-123',
18
- source_type: 'local',
19
- score: 0.9,
20
- matched_text: '预算 50 万元',
21
- snippet: '预算 50 万元,交付截止 2026-05-30',
22
- },
23
- ],
24
- },
25
- },
26
- ],
27
- evidenceStrictMode: false,
28
- });
29
-
30
- assert.equal(rows.length, 1);
31
- assert.equal(rows[0].citation_id, 'cite_1');
32
- assert.equal(rows[0].run_id, 'retrieval-run-1');
33
- assert.equal(rows[0].title, '采购需求文档.pdf');
34
- assert.equal(rows[0].snippet.includes('预算'), true);
35
- });
36
-
37
- test('selectProjectedCanvasEvidenceItems de-duplicates duplicated citation/result keys', () => {
38
- const rows = selectProjectedCanvasEvidenceItems({
39
- timelineItems: [
40
- {
41
- type: 'knowledge_search',
42
- payload: {
43
- run_id: 'retrieval-run-2',
44
- query: '风险',
45
- results: [
46
- { id: 'same-key', title: 'A', snippet: '交付风险', score: 0.8 },
47
- ],
48
- },
49
- },
50
- {
51
- type: 'knowledge_citation',
52
- payload: {
53
- run_id: 'retrieval-run-2',
54
- query: '风险',
55
- citations: [
56
- { citation_id: 'same-key', title: 'A', matched_text: '交付风险', score: 0.9 },
57
- ],
58
- },
59
- },
60
- ],
61
- evidenceStrictMode: false,
62
- });
63
-
64
- assert.equal(rows.length, 1);
65
- assert.equal(rows[0].key, 'same-key');
66
- });
67
-
68
- test('selectProjectedCanvasEvidenceItems prefers attachment filename and mime over opaque source_id title', () => {
69
- const rows = selectProjectedCanvasEvidenceItems({
70
- timelineItems: [
71
- {
72
- type: 'message',
73
- role: 'user',
74
- attachments: [
75
- {
76
- source_id: 'src-abc123456789',
77
- filename: '公司采购需求.docx',
78
- mime_type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
79
- },
80
- ],
81
- },
82
- {
83
- type: 'knowledge_citation',
84
- payload: {
85
- run_id: 'retrieval-run-3',
86
- query: '总结',
87
- citations: [
88
- {
89
- citation_id: 'cite_2',
90
- title: 'src-abc123456789',
91
- source_id: 'src-abc123456789',
92
- score: 0.8,
93
- matched_text: '主要风险',
94
- },
95
- ],
96
- },
97
- },
98
- ],
99
- evidenceStrictMode: false,
100
- });
101
-
102
- assert.equal(rows.length, 1);
103
- assert.equal(
104
- rows[0].title,
105
- '公司采购需求.docx · Word 文档',
106
- );
107
- });
@@ -1,50 +0,0 @@
1
- function shouldSuggestPlanFromAssistantQuestion(text = '') {
2
- const normalized = String(text || '').trim();
3
- if (!normalized) {
4
- return false;
5
- }
6
- const hasQuestionMark = normalized.includes('?') || normalized.includes('?');
7
- const hasReplyCue = ['请告诉', '请确认', '请选择', '请补充', '请回复'].some((cue) => normalized.includes(cue));
8
- return hasQuestionMark || hasReplyCue;
9
- }
10
-
11
- export function selectLatestAssistantQuestionPendingReply(timelineItems = []) {
12
- const items = Array.isArray(timelineItems) ? timelineItems : [];
13
- for (let index = items.length - 1; index >= 0; index -= 1) {
14
- const item = items[index];
15
- if (item?.type !== 'message' || item?.role !== 'assistant') {
16
- continue;
17
- }
18
- return shouldSuggestPlanFromAssistantQuestion(item?.content || '');
19
- }
20
- return false;
21
- }
22
-
23
- export function selectModeSuggestionTipRules({
24
- baseRules,
25
- questionModeSuggestionEnabled,
26
- } = {}) {
27
- const resolvedBaseRules = Array.isArray(baseRules) ? baseRules : [];
28
- if (!questionModeSuggestionEnabled) {
29
- return resolvedBaseRules;
30
- }
31
- return [
32
- ...resolvedBaseRules,
33
- {
34
- id: 'assistant_question_followup',
35
- modeKey: 'requirement_canvas',
36
- label: 'Plan 模式',
37
- reason: '当前回复需要你补充信息,建议开启 Plan 模式,按模板快速回答。',
38
- actionText: '打开梳理模式',
39
- whenContextKey: 'assistant_question_pending_reply',
40
- },
41
- ];
42
- }
43
-
44
- export function selectModeSuggestionContextFlags({ latestAssistantQuestionPendingReply } = {}) {
45
- return {
46
- assistant_question_pending_reply: Boolean(latestAssistantQuestionPendingReply),
47
- };
48
- }
49
-
50
- export default selectLatestAssistantQuestionPendingReply;