flare-chat-core 0.2.0 → 0.2.2

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 (120) hide show
  1. package/README.md +28 -0
  2. package/docs/CAPABILITY-INVENTORY.md +42 -0
  3. package/docs/CHAT-CORE-BOUNDARY.md +47 -0
  4. package/docs/CORE-APP-REALIGNMENT-WORKLOAD-2026-04-18.md +86 -0
  5. package/docs/SSOT-CHAT-CORE-BOUNDARY.md +73 -0
  6. package/docs/SSOT-CHAT-CORE-DATAFLOW.md +97 -0
  7. package/index.html +12 -0
  8. package/package.json +24 -2
  9. package/src/adapters/index.js +6 -0
  10. package/src/adapters/message-api.adapter.js +59 -0
  11. package/src/adapters/session-api.adapter.js +133 -0
  12. package/src/adapters/session-message-api.http.js +161 -0
  13. package/src/adapters/session-message-api.js +34 -0
  14. package/src/adapters/session-message-api.normalize-source-record.test.mjs +180 -0
  15. package/src/adapters/session-message-api.normalizers.js +153 -0
  16. package/src/adapters/source-api.adapter.js +135 -0
  17. package/src/adapters/sse-client.js +244 -0
  18. package/src/adapters/sse-event-dispatcher.js +121 -0
  19. package/src/app/App.jsx +11 -0
  20. package/src/app/AppProviders.jsx +12 -0
  21. package/src/app/ChatWorkspaceScreen.jsx +33 -0
  22. package/src/app/WorkspaceLayout.jsx +125 -0
  23. package/src/app/components/AppCanvasPanel.jsx +64 -0
  24. package/src/app/components/TriggerThresholdPopoverContent.jsx +122 -0
  25. package/src/app/components/WorkspaceBodySection.jsx +109 -0
  26. package/src/app/components/WorkspaceMainPane.jsx +113 -0
  27. package/src/app/components/WorkspaceSessionPane.jsx +48 -0
  28. package/src/app/components/WorkspaceTopBarSection.jsx +65 -0
  29. package/src/app/core-chat-entry/ComposerSectionNode.jsx +241 -0
  30. package/src/app/core-chat-entry/attachmentSendRefs.js +154 -0
  31. package/src/app/core-chat-entry/attachmentSendRefs.test.mjs +101 -0
  32. package/src/app/core-chat-entry/composerActionRouter.js +26 -0
  33. package/src/app/core-chat-entry/constants.js +108 -0
  34. package/src/app/core-chat-entry/selectors.js +28 -0
  35. package/src/app/core-chat-entry/useAppActionErrorGuards.js +68 -0
  36. package/src/app/core-chat-entry/useChatCorePipelines.js +110 -0
  37. package/src/app/core-chat-entry/useComposerModeSuggestion.js +89 -0
  38. package/src/app/core-chat-entry/useDevCapabilityStatusNote.js +22 -0
  39. package/src/app/core-chat-entry/useProjectNameEditing.js +41 -0
  40. package/src/app/core-chat-entry/useProjectSourceUpload.js +341 -0
  41. package/src/app/core-chat-entry/useRealApiReadinessGate.js +103 -0
  42. package/src/app/core-chat-entry/useUnavailableActionError.js +29 -0
  43. package/src/app/core-chat-entry/useWorkspaceCanvasController.jsx +177 -0
  44. package/src/app/core-chat-entry/useWorkspaceCanvasProjection.jsx +171 -0
  45. package/src/app/core-chat-entry/useWorkspaceComposerController.jsx +199 -0
  46. package/src/app/core-chat-entry/useWorkspaceController.jsx +226 -0
  47. package/src/app/core-chat-entry/useWorkspacePanels.js +55 -0
  48. package/src/app/hooks/useComposerAttachmentSync.js +223 -0
  49. package/src/app/hooks/useComposerChooserHandlers.js +52 -0
  50. package/src/app/hooks/useSendWithContextRefs.js +140 -0
  51. package/src/app/hooks/useSendWithContextRefs.test.mjs +29 -0
  52. package/src/app/hooks/useUserThresholdProfile.js +121 -0
  53. package/src/app/index.js +1 -0
  54. package/src/app/selectors/assistantTextSelector.js +73 -0
  55. package/src/app/selectors/canvasEvidenceSummarySelector.js +28 -0
  56. package/src/app/selectors/canvasReportTemplateSelector.js +28 -0
  57. package/src/app/selectors/canvasTabsSelector.js +58 -0
  58. package/src/app/selectors/evidenceProjectionSelector.js +175 -0
  59. package/src/app/selectors/evidenceProjectionSelector.test.mjs +107 -0
  60. package/src/app/selectors/modeSuggestionSelector.js +50 -0
  61. package/src/chat-core/app/mockRuntime.js +291 -0
  62. package/src/chat-core/app/useAppStream.js +187 -0
  63. package/src/chat-core/app/useAppStream.refs.test.mjs +44 -0
  64. package/src/chat-core/app/useAppStream.request-body.test.mjs +116 -0
  65. package/src/chat-core/app/useCoreChatApp.js +115 -0
  66. package/src/chat-core/facade/useBasicConversationFacade.js +280 -0
  67. package/src/chat-core/index.js +9 -1
  68. package/src/chat-core/messages/buildTimelineItems.analysis-route.test.mjs +36 -0
  69. package/src/chat-core/messages/buildTimelineItems.js +172 -11
  70. package/src/chat-core/messages/buildTimelineItems.knowledge-citation.test.mjs +183 -0
  71. package/src/chat-core/messages/contextUsageDefaults.js +3 -0
  72. package/src/chat-core/messages/contextUsageViewModel.js +147 -0
  73. package/src/chat-core/messages/contextUsageViewModel.test.mjs +74 -0
  74. package/src/chat-core/messages/useContextUsageViewModel.js +41 -0
  75. package/src/chat-core/orchestration/useBasicSendHandler.js +55 -0
  76. package/src/chat-core/pipelines/build-action-request.js +46 -0
  77. package/src/chat-core/pipelines/build-stream-request.js +74 -0
  78. package/src/chat-core/pipelines/entity-extraction.js +159 -0
  79. package/src/chat-core/pipelines/preprocess-message.js +16 -0
  80. package/src/chat-core/pipelines/stream-persist-utils.js +32 -0
  81. package/src/chat-core/pipelines/transport/send-mock-stream.js +86 -0
  82. package/src/chat-core/pipelines/transport/send-real-stream.js +330 -0
  83. package/src/chat-core/pipelines/transport/send-real-stream.test.mjs +27 -0
  84. package/src/chat-core/pipelines/transport/send-sourcing-search.js +86 -0
  85. package/src/chat-core/pipelines/transport/send-sourcing-search.test.mjs +14 -0
  86. package/src/chat-core/pipelines/transport/sourcing-response-templates.js +55 -0
  87. package/src/chat-core/pipelines/transport/sourcing-search-api.js +155 -0
  88. package/src/chat-core/runtime/runtimeMode.js +69 -0
  89. package/src/chat-core/session/chatSessionActionTypes.js +24 -0
  90. package/src/chat-core/session/chatSessionReducer.js +352 -0
  91. package/src/chat-core/session/chatSessionReducer.streaming-done.test.mjs +39 -0
  92. package/src/chat-core/session/index.js +2 -0
  93. package/src/chat-core/session/sessionActionsMessages.js +44 -0
  94. package/src/chat-core/session/sessionActionsSessionCrud.js +131 -0
  95. package/src/chat-core/session/sessionActionsStreaming.js +80 -0
  96. package/src/chat-core/session/sessionActionsUiState.js +51 -0
  97. package/src/chat-core/session/useChatSessionReducer.js +67 -390
  98. package/src/chat-core/session/useSessionListController.js +67 -0
  99. package/src/chat-core/stream/sse-client.js +1 -142
  100. package/src/chat-core/stream/sse-event-dispatcher.js +1 -0
  101. package/src/chat-core/stream/sse-events.js +1 -598
  102. package/src/chat-core/stream/useSSEStream.js +1 -273
  103. package/src/chat-core/stream/useStreamSendController.js +46 -0
  104. package/src/contracts/context-ssot.js +47 -0
  105. package/src/contracts/index.js +1 -0
  106. package/src/contracts/sse-events/base-parsers.js +79 -0
  107. package/src/contracts/sse-events/domain-parsers.js +3 -0
  108. package/src/contracts/sse-events/internal-normalizers.js +143 -0
  109. package/src/contracts/sse-events/parsers-intake.js +235 -0
  110. package/src/contracts/sse-events/parsers-runtime.js +37 -0
  111. package/src/contracts/sse-events/parsers-sourcing.js +179 -0
  112. package/src/contracts/sse-events/patch-event-parser.js +121 -0
  113. package/src/contracts/sse-events/runtime-parsers.js +79 -0
  114. package/src/contracts/sse-events.js +4 -0
  115. package/src/index.js +5 -0
  116. package/src/main.jsx +28 -0
  117. package/src/orchestration/index.js +6 -0
  118. package/src/orchestration/useSSEStream.js +221 -0
  119. package/src/state/index.js +4 -0
  120. package/vite.config.js +36 -0
@@ -0,0 +1,159 @@
1
+ const ENTITY_TYPES = {
2
+ VENDOR: 'vendor',
3
+ PRODUCT: 'product',
4
+ ORG: 'org',
5
+ };
6
+
7
+ const PRODUCT_TOKENS = [
8
+ 'ERP',
9
+ 'CRM',
10
+ 'SCM',
11
+ 'MES',
12
+ 'PLM',
13
+ 'SRM',
14
+ 'WMS',
15
+ 'HRM',
16
+ ];
17
+
18
+ const KNOWN_ENTITY_DEFS = [
19
+ { type: ENTITY_TYPES.VENDOR, text: '用友', aliases: ['用友', '用友网络', 'yonyou'] },
20
+ { type: ENTITY_TYPES.VENDOR, text: '金蝶', aliases: ['金蝶', 'kingdee'] },
21
+ { type: ENTITY_TYPES.VENDOR, text: 'SAP', aliases: ['sap', 'sap erp', '思爱普'] },
22
+ { type: ENTITY_TYPES.VENDOR, text: 'Oracle', aliases: ['oracle', 'oracle erp', '甲骨文'] },
23
+ { type: ENTITY_TYPES.VENDOR, text: '鼎捷', aliases: ['鼎捷', 'digiwin'] },
24
+ { type: ENTITY_TYPES.VENDOR, text: 'Microsoft Dynamics', aliases: ['microsoft dynamics', 'dynamics 365'] },
25
+ { type: ENTITY_TYPES.VENDOR, text: 'Infor', aliases: ['infor'] },
26
+ { type: ENTITY_TYPES.VENDOR, text: 'Workday', aliases: ['workday'] },
27
+ { type: ENTITY_TYPES.VENDOR, text: 'Sage', aliases: ['sage'] },
28
+ { type: ENTITY_TYPES.VENDOR, text: 'Odoo', aliases: ['odoo'] },
29
+ { type: ENTITY_TYPES.VENDOR, text: 'Epicor', aliases: ['epicor'] },
30
+ { type: ENTITY_TYPES.VENDOR, text: 'IFS', aliases: ['ifs'] },
31
+ ...PRODUCT_TOKENS.map((token) => ({ type: ENTITY_TYPES.PRODUCT, text: token, aliases: [token] })),
32
+ ];
33
+
34
+ function normalizeEntityKey(value) {
35
+ return String(value || '')
36
+ .trim()
37
+ .toLowerCase()
38
+ .replace(/[\u3000\s]+/g, ' ')
39
+ .replace(/^[,.;:!?()[\]{}"'`~]+|[,.;:!?()[\]{}"'`~]+$/g, '');
40
+ }
41
+
42
+ function escapeRegExp(value) {
43
+ return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
44
+ }
45
+
46
+ function hasCjk(value) {
47
+ return /[\u3400-\u9fff]/.test(String(value || ''));
48
+ }
49
+
50
+ function includesAlias(text, alias) {
51
+ const sourceText = String(text || '');
52
+ const sourceAlias = String(alias || '').trim();
53
+ if (!sourceText || !sourceAlias) {
54
+ return false;
55
+ }
56
+ if (hasCjk(sourceAlias)) {
57
+ return sourceText.includes(sourceAlias);
58
+ }
59
+ const pattern = new RegExp(`\\b${escapeRegExp(sourceAlias)}\\b`, 'i');
60
+ return pattern.test(sourceText);
61
+ }
62
+
63
+ function inferEntityType(text, fallbackType = ENTITY_TYPES.ORG) {
64
+ const normalized = normalizeEntityKey(text);
65
+ if (PRODUCT_TOKENS.some((token) => normalizeEntityKey(token) === normalized)) {
66
+ return ENTITY_TYPES.PRODUCT;
67
+ }
68
+ return fallbackType;
69
+ }
70
+
71
+ function normalizeEntityItem(item) {
72
+ if (typeof item === 'string') {
73
+ const normalized = normalizeEntityKey(item);
74
+ if (!normalized) return null;
75
+ return {
76
+ text: String(item).trim(),
77
+ normalized,
78
+ type: inferEntityType(item),
79
+ };
80
+ }
81
+ if (!item || typeof item !== 'object' || Array.isArray(item)) {
82
+ return null;
83
+ }
84
+ const text = String(item.text || item.name || item.value || '').trim();
85
+ const normalized = normalizeEntityKey(item.normalized || text);
86
+ if (!text && !normalized) {
87
+ return null;
88
+ }
89
+ return {
90
+ text: text || normalized,
91
+ normalized,
92
+ type: String(item.type || inferEntityType(text || normalized)).trim().toLowerCase() || ENTITY_TYPES.ORG,
93
+ };
94
+ }
95
+
96
+ function dedupeEntities(list = []) {
97
+ const seen = new Set();
98
+ const normalized = [];
99
+ list.forEach((item) => {
100
+ const normalizedItem = normalizeEntityItem(item);
101
+ if (!normalizedItem || !normalizedItem.normalized) {
102
+ return;
103
+ }
104
+ const key = `${normalizedItem.type}:${normalizedItem.normalized}`;
105
+ if (seen.has(key)) {
106
+ return;
107
+ }
108
+ seen.add(key);
109
+ normalized.push(normalizedItem);
110
+ });
111
+ return normalized;
112
+ }
113
+
114
+ function extractKnownEntities(text = '') {
115
+ const sourceText = String(text || '');
116
+ if (!sourceText.trim()) {
117
+ return [];
118
+ }
119
+ return KNOWN_ENTITY_DEFS
120
+ .filter((entity) => entity.aliases.some((alias) => includesAlias(sourceText, alias)))
121
+ .map((entity) => ({
122
+ text: entity.text,
123
+ normalized: normalizeEntityKey(entity.text),
124
+ type: entity.type,
125
+ }));
126
+ }
127
+
128
+ function extractOrgLikeEntities(text = '') {
129
+ const sourceText = String(text || '');
130
+ if (!sourceText.trim()) {
131
+ return [];
132
+ }
133
+ const matches = [];
134
+ const cnPattern = /([\u4e00-\u9fa5]{2,24}(?:公司|集团|科技|信息|股份|研究院))/g;
135
+ const enPattern = /\b([A-Z][A-Za-z0-9&.-]{1,}(?:\s+[A-Z][A-Za-z0-9&.-]{1,}){0,3}\s+(?:Inc|Corp|Corporation|Ltd|LLC|Group|Systems|Technologies|Technology))\b/g;
136
+ for (const match of sourceText.matchAll(cnPattern)) {
137
+ matches.push({ text: match[1], type: ENTITY_TYPES.ORG });
138
+ }
139
+ for (const match of sourceText.matchAll(enPattern)) {
140
+ matches.push({ text: match[1], type: ENTITY_TYPES.ORG });
141
+ }
142
+ return matches;
143
+ }
144
+
145
+ export function extractEntitiesFromText(text = '') {
146
+ return dedupeEntities([
147
+ ...extractKnownEntities(text),
148
+ ...extractOrgLikeEntities(text),
149
+ ]);
150
+ }
151
+
152
+ export function resolveEntities({ text = '', entities = [] } = {}) {
153
+ return dedupeEntities([
154
+ ...(Array.isArray(entities) ? entities : []),
155
+ ...extractEntitiesFromText(text),
156
+ ]);
157
+ }
158
+
159
+ export { ENTITY_TYPES };
@@ -0,0 +1,16 @@
1
+ import { extractEntitiesFromText } from './entity-extraction.js';
2
+
3
+ export function normalizeIntentText(value) {
4
+ return String(value || '').trim().toLowerCase();
5
+ }
6
+
7
+ export function detectSourcingIntent(text) {
8
+ const normalized = normalizeIntentText(text);
9
+ if (!normalized) return false;
10
+ const signals = ['寻源', '供应商', '比价', '采购', '招标', 'source', 'vendor', 'supplier', 'sourcing'];
11
+ return signals.some((token) => normalized.includes(token));
12
+ }
13
+
14
+ export function extractMessageEntities(text) {
15
+ return extractEntitiesFromText(text);
16
+ }
@@ -0,0 +1,32 @@
1
+ export function resolvePersistExchangeResult(result, fallbackSessionId = '') {
2
+ const fallback = String(fallbackSessionId || '').trim();
3
+ if (result && typeof result === 'object' && !Array.isArray(result)) {
4
+ return {
5
+ sessionId: String(result.sessionId || result.session_id || fallback).trim(),
6
+ title: String(result.title || '').trim(),
7
+ };
8
+ }
9
+ return {
10
+ sessionId: String(result || fallback).trim(),
11
+ title: '',
12
+ };
13
+ }
14
+
15
+ export function collectSourceIdsFromRefs(refs = []) {
16
+ if (!Array.isArray(refs)) {
17
+ return [];
18
+ }
19
+ const ids = [];
20
+ const seen = new Set();
21
+ refs.forEach((item) => {
22
+ const sourceId = typeof item === 'string'
23
+ ? String(item).trim()
24
+ : String(item?.source_id || item?.id || '').trim();
25
+ if (!sourceId || seen.has(sourceId)) {
26
+ return;
27
+ }
28
+ seen.add(sourceId);
29
+ ids.push(sourceId);
30
+ });
31
+ return ids;
32
+ }
@@ -0,0 +1,86 @@
1
+ import { resolvePersistExchangeResult } from '../stream-persist-utils.js';
2
+
3
+ function chunkText(content, chunkSize = 16) {
4
+ if (!content) {
5
+ return [];
6
+ }
7
+
8
+ const result = [];
9
+ for (let index = 0; index < content.length; index += chunkSize) {
10
+ result.push(content.slice(index, index + chunkSize));
11
+ }
12
+
13
+ return result;
14
+ }
15
+
16
+ export async function sendMockStreamMessage({
17
+ runtime,
18
+ content,
19
+ handlers = {},
20
+ options = {},
21
+ scope = {},
22
+ persistExchange,
23
+ schedule,
24
+ }) {
25
+ const sessionId = String(options.sessionIdOverride || '').trim();
26
+ const enabledCapabilities = Array.isArray(options.enabledCapabilities)
27
+ ? options.enabledCapabilities
28
+ : [];
29
+ const finalReply = runtime.buildAssistantReply(content, enabledCapabilities);
30
+ const chunks = chunkText(finalReply);
31
+ const resolvedProjectId = String(scope?.projectId || '').trim();
32
+ const resolvedUserId = String(scope?.userId || '').trim();
33
+
34
+ await new Promise((resolve) => {
35
+ let delay = 120;
36
+ chunks.forEach((chunk) => {
37
+ schedule(delay, () => {
38
+ handlers.onContent?.(chunk);
39
+ });
40
+ delay += 70;
41
+ });
42
+
43
+ schedule(delay + 40, () => {
44
+ const persistedResult = (typeof persistExchange === 'function'
45
+ ? persistExchange(sessionId, content, finalReply, {
46
+ functionType: options.modeKey || 'chat_component_debug',
47
+ projectId: resolvedProjectId || null,
48
+ userId: resolvedUserId || null,
49
+ })
50
+ : String(sessionId || '').trim()
51
+ );
52
+ const persistedSession = resolvePersistExchangeResult(persistedResult, sessionId);
53
+ const resolvedSessionId = persistedSession.sessionId;
54
+ handlers.onPatchEvent?.({
55
+ session: {
56
+ session_id: resolvedSessionId,
57
+ turn_id: '',
58
+ },
59
+ payload: persistedSession.title
60
+ ? {
61
+ session: {
62
+ title: persistedSession.title,
63
+ },
64
+ }
65
+ : {},
66
+ patch_scope: ['session'],
67
+ event_type: 'ack',
68
+ final: false,
69
+ invalid: false,
70
+ });
71
+ handlers.onComplete?.(finalReply, {
72
+ sessionId: resolvedSessionId,
73
+ ...(persistedSession.title
74
+ ? {
75
+ payload: {
76
+ session: {
77
+ title: persistedSession.title,
78
+ },
79
+ },
80
+ }
81
+ : {}),
82
+ });
83
+ resolve();
84
+ });
85
+ });
86
+ }
@@ -0,0 +1,330 @@
1
+ import {
2
+ parseAgentStatus,
3
+ parseExecutionTrace,
4
+ parseKnowledgeSearch,
5
+ parseThinkingTrace,
6
+ parseSourcingCandidates,
7
+ parseKnowledgeCitation,
8
+ } from '../../../contracts/sse-events.js';
9
+ import { debugCoreLog, getCoreRuntimeMode } from '../../runtime/runtimeMode.js';
10
+ import { buildRealStreamRequestBody, resolvePayloadExtra } from '../build-stream-request.js';
11
+ import { buildRealActionRequestBody } from '../build-action-request.js';
12
+ import { collectSourceIdsFromRefs, resolvePersistExchangeResult } from '../stream-persist-utils.js';
13
+ import { requestSourcingSearch } from './send-sourcing-search.js';
14
+
15
+ function joinUrl(baseUrl = '', path = '') {
16
+ if (!baseUrl) {
17
+ return path || '';
18
+ }
19
+ if (!path) {
20
+ return baseUrl;
21
+ }
22
+ return `${baseUrl.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`;
23
+ }
24
+
25
+ export function shouldUseSourcingSearchPath(options = {}, content = '') {
26
+ void content;
27
+ const modeKey = String(options?.modeKey || options?.manualModeKey || '').trim();
28
+ if (modeKey !== 'intelligent_sourcing') {
29
+ return false;
30
+ }
31
+ const payloadExtra = resolvePayloadExtra(options);
32
+ if (payloadExtra.append_mode === true) {
33
+ return true;
34
+ }
35
+ if (payloadExtra.force_sourcing_search === true) {
36
+ return true;
37
+ }
38
+ return false;
39
+ }
40
+
41
+ export async function sendRealStreamMessage({
42
+ apiBaseUrl,
43
+ content,
44
+ handlers = {},
45
+ options = {},
46
+ scope = {},
47
+ persistExchange,
48
+ signal,
49
+ }) {
50
+ const resolvedProjectId = String(scope?.projectId || '').trim();
51
+ const resolvedUserId = String(scope?.userId || '').trim();
52
+ const sessionId = String(options.sessionIdOverride || '').trim();
53
+ const requestBody = buildRealStreamRequestBody({
54
+ content,
55
+ options,
56
+ scope: {
57
+ projectId: resolvedProjectId,
58
+ userId: resolvedUserId,
59
+ },
60
+ });
61
+
62
+ const runtimeMode = getCoreRuntimeMode();
63
+ // Dev-only debug: show request-side mode and key stream switches.
64
+ debugCoreLog('stream.send.start', {
65
+ mode: runtimeMode.mode,
66
+ sessionId,
67
+ modeKey: String(options?.modeKey || ''),
68
+ manualModeKey: String(options?.manualModeKey || ''),
69
+ knowledgeRefsCount: Array.isArray(requestBody?.knowledge_refs) ? requestBody.knowledge_refs.length : 0,
70
+ });
71
+ const requestSourceIds = collectSourceIdsFromRefs([
72
+ ...(Array.isArray(requestBody?.context_refs) ? requestBody.context_refs : []),
73
+ ...(Array.isArray(requestBody?.knowledge_refs) ? requestBody.knowledge_refs : []),
74
+ ]);
75
+ const resolvedModeKey = String(options?.modeKey || options?.manualModeKey || '').trim() || 'auto';
76
+ handlers.onExecutionTrace?.({
77
+ session_id: sessionId || '',
78
+ step_id: 'context_binding',
79
+ agent: 'context_router',
80
+ stage: 'orchestration',
81
+ label: '上下文绑定',
82
+ detail: requestSourceIds.length > 0
83
+ ? `mode=${resolvedModeKey}; 绑定 ${requestSourceIds.length} 个来源: ${requestSourceIds.join(', ')}`
84
+ : `mode=${resolvedModeKey}; 未绑定来源,按常规上下文流程处理`,
85
+ reason: '',
86
+ status: 'running',
87
+ mode_key: resolvedModeKey,
88
+ sources: requestSourceIds.map((sourceId) => ({ source_id: sourceId })),
89
+ timestamp: Date.now(),
90
+ });
91
+
92
+ let finalContent = '';
93
+ let resolvedSessionId = sessionId;
94
+ let latestContextUsage = null;
95
+
96
+ if (shouldUseSourcingSearchPath(options, content)) {
97
+ await requestSourcingSearch({
98
+ apiBaseUrl,
99
+ content,
100
+ options,
101
+ scope: { projectId: resolvedProjectId, userId: resolvedUserId },
102
+ handlers,
103
+ persistExchange,
104
+ resolvedSessionId,
105
+ });
106
+ return;
107
+ }
108
+
109
+ const response = await fetch(joinUrl(apiBaseUrl, '/chat/stream'), {
110
+ method: 'POST',
111
+ headers: { 'Content-Type': 'application/json' },
112
+ body: JSON.stringify(requestBody),
113
+ signal,
114
+ });
115
+
116
+ if (!response.ok) {
117
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
118
+ }
119
+
120
+ if (!response.body) {
121
+ throw new Error('stream response body is empty');
122
+ }
123
+
124
+ const reader = response.body.getReader();
125
+ const decoder = new TextDecoder();
126
+ let buffer = '';
127
+ let currentEvent = '';
128
+ let dataLines = [];
129
+
130
+ const emitEvent = () => {
131
+ if (!currentEvent || dataLines.length === 0) {
132
+ currentEvent = '';
133
+ dataLines = [];
134
+ return;
135
+ }
136
+
137
+ const rawData = dataLines.join('\n');
138
+ let payload = {};
139
+ try {
140
+ payload = JSON.parse(rawData);
141
+ } catch {
142
+ payload = {};
143
+ }
144
+ // Dev-only debug: trace the incoming SSE event types and payload outline.
145
+ debugCoreLog('stream.event', {
146
+ type: currentEvent,
147
+ keys: Object.keys(payload || {}),
148
+ });
149
+
150
+ if (currentEvent === 'content') {
151
+ const chunk = String(payload?.content || '');
152
+ finalContent += chunk;
153
+ handlers.onContent?.(chunk);
154
+ } else if (currentEvent === 'agent_status') {
155
+ handlers.onAgentStatus?.(parseAgentStatus(payload));
156
+ } else if (currentEvent === 'thinking_trace') {
157
+ handlers.onThinkingTrace?.(parseThinkingTrace(payload)?.trace || '');
158
+ } else if (currentEvent === 'execution_trace') {
159
+ handlers.onExecutionTrace?.(parseExecutionTrace(payload));
160
+ } else if (currentEvent === 'context_usage') {
161
+ latestContextUsage = (
162
+ payload
163
+ && typeof payload === 'object'
164
+ && !Array.isArray(payload)
165
+ ) ? { ...payload } : null;
166
+ const modeKey = String(payload?.mode_key || '').trim();
167
+ const modeState = String(payload?.mode_state || '').trim();
168
+ const profile = String(payload?.profile || '').trim();
169
+ const totalTokens = Number(payload?.token_estimate?.total_prompt_tokens_estimate || 0);
170
+ handlers.onExecutionTrace?.({
171
+ session_id: String(payload?.session_id || '').trim(),
172
+ step_id: 'context_usage',
173
+ agent: 'context_router',
174
+ stage: 'orchestration',
175
+ label: '上下文预算评估',
176
+ detail: `mode=${modeKey || 'default'}/${modeState || 'default'}; profile=${profile || 'default'}; tokens≈${totalTokens}`,
177
+ reason: '',
178
+ status: 'completed',
179
+ timestamp: Number.isFinite(Number(payload?.ts)) ? Number(payload.ts) : Date.now(),
180
+ });
181
+ } else if (currentEvent === 'knowledge_search') {
182
+ handlers.onKnowledgeSearch?.(parseKnowledgeSearch(payload));
183
+ } else if (currentEvent === 'sourcing_candidates') {
184
+ handlers.onSourcingCandidates?.(parseSourcingCandidates(payload));
185
+ } else if (currentEvent === 'knowledge_citation') {
186
+ handlers.onKnowledgeCitation?.(parseKnowledgeCitation(payload));
187
+ } else if (currentEvent === 'patch_event') {
188
+ const patchSessionId = String(payload?.session?.session_id || '').trim();
189
+ if (patchSessionId) {
190
+ resolvedSessionId = patchSessionId;
191
+ }
192
+ handlers.onPatchEvent?.(payload);
193
+ } else if (currentEvent === 'complete') {
194
+ let resolvedSessionTitle = '';
195
+ if (typeof persistExchange === 'function') {
196
+ const persistedResult = persistExchange(resolvedSessionId, content, finalContent, {
197
+ functionType: 'chat_component_debug',
198
+ projectId: resolvedProjectId || null,
199
+ userId: resolvedUserId || null,
200
+ });
201
+ const persistedSession = resolvePersistExchangeResult(persistedResult, resolvedSessionId);
202
+ resolvedSessionId = persistedSession.sessionId;
203
+ resolvedSessionTitle = persistedSession.title;
204
+ }
205
+ handlers.onPatchEvent?.({
206
+ session: {
207
+ session_id: resolvedSessionId,
208
+ turn_id: '',
209
+ },
210
+ payload: resolvedSessionTitle
211
+ ? {
212
+ session: {
213
+ title: resolvedSessionTitle,
214
+ },
215
+ }
216
+ : {},
217
+ patch_scope: ['session'],
218
+ event_type: 'final',
219
+ final: true,
220
+ invalid: false,
221
+ });
222
+ handlers.onComplete?.(finalContent, {
223
+ sessionId: resolvedSessionId,
224
+ ...(latestContextUsage
225
+ ? {
226
+ context_usage: latestContextUsage,
227
+ }
228
+ : {}),
229
+ ...(resolvedSessionTitle
230
+ ? {
231
+ payload: {
232
+ session: {
233
+ title: resolvedSessionTitle,
234
+ },
235
+ },
236
+ }
237
+ : {}),
238
+ });
239
+ debugCoreLog('stream.send.complete', {
240
+ sessionId: resolvedSessionId,
241
+ finalContentLength: finalContent.length,
242
+ });
243
+ }
244
+
245
+ currentEvent = '';
246
+ dataLines = [];
247
+ };
248
+
249
+ while (true) {
250
+ const { done, value } = await reader.read();
251
+ if (done) {
252
+ break;
253
+ }
254
+
255
+ buffer += decoder.decode(value, { stream: true });
256
+ const lines = buffer.split('\n');
257
+ buffer = lines.pop() || '';
258
+
259
+ for (const line of lines) {
260
+ const normalized = line.replace(/\r$/, '');
261
+ if (!normalized) {
262
+ emitEvent();
263
+ continue;
264
+ }
265
+ if (normalized.startsWith('event:')) {
266
+ currentEvent = normalized.slice(6).trim();
267
+ continue;
268
+ }
269
+ if (normalized.startsWith('data:')) {
270
+ dataLines.push(normalized.slice(5).trim());
271
+ }
272
+ }
273
+ }
274
+
275
+ emitEvent();
276
+ handlers.onExecutionTrace?.({
277
+ session_id: resolvedSessionId || '',
278
+ step_id: 'context_binding',
279
+ agent: 'context_router',
280
+ stage: 'orchestration',
281
+ label: '上下文绑定',
282
+ detail: requestSourceIds.length > 0
283
+ ? `已完成来源绑定(${requestSourceIds.length})`
284
+ : '已完成常规上下文绑定',
285
+ reason: '',
286
+ status: 'completed',
287
+ sources: requestSourceIds.map((sourceId) => ({ source_id: sourceId })),
288
+ timestamp: Date.now(),
289
+ });
290
+ }
291
+
292
+ export async function sendRealStreamAction({
293
+ apiBaseUrl,
294
+ action,
295
+ handlers = {},
296
+ options = {},
297
+ scope = {},
298
+ signal,
299
+ }) {
300
+ const requestBody = buildRealActionRequestBody({
301
+ action,
302
+ options,
303
+ scope,
304
+ });
305
+
306
+ const response = await fetch(joinUrl(apiBaseUrl, '/chat/action'), {
307
+ method: 'POST',
308
+ headers: { 'Content-Type': 'application/json' },
309
+ body: JSON.stringify(requestBody),
310
+ signal,
311
+ });
312
+
313
+ if (!response.ok) {
314
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
315
+ }
316
+
317
+ const payload = await response.json();
318
+ const result = (
319
+ payload?.result
320
+ && typeof payload.result === 'object'
321
+ && !Array.isArray(payload.result)
322
+ ) ? payload.result : {};
323
+ const content = String(result.message || payload?.message || '').trim();
324
+
325
+ if (content) {
326
+ handlers.onContent?.(content);
327
+ }
328
+ handlers.onComplete?.(content);
329
+ return payload;
330
+ }
@@ -0,0 +1,27 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { shouldUseSourcingSearchPath } from './send-real-stream.js';
4
+
5
+ test('shouldUseSourcingSearchPath keeps sourcing mode on stream path by default', () => {
6
+ const shouldUse = shouldUseSourcingSearchPath({
7
+ modeKey: 'intelligent_sourcing',
8
+ payloadExtra: {},
9
+ }, '来源1');
10
+ assert.equal(shouldUse, false);
11
+ });
12
+
13
+ test('shouldUseSourcingSearchPath uses direct path in append rounds', () => {
14
+ const shouldUse = shouldUseSourcingSearchPath({
15
+ modeKey: 'intelligent_sourcing',
16
+ payloadExtra: { append_mode: true },
17
+ }, '能不能换一批');
18
+ assert.equal(shouldUse, true);
19
+ });
20
+
21
+ test('shouldUseSourcingSearchPath uses direct path only with explicit force flag', () => {
22
+ const shouldUse = shouldUseSourcingSearchPath({
23
+ modeKey: 'intelligent_sourcing',
24
+ payloadExtra: { force_sourcing_search: true },
25
+ }, '任意输入');
26
+ assert.equal(shouldUse, true);
27
+ });