flare-chat-core 0.2.2 → 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 -125
  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 -109
  27. package/src/app/components/WorkspaceMainPane.jsx +0 -113
  28. package/src/app/components/WorkspaceSessionPane.jsx +0 -48
  29. package/src/app/components/WorkspaceTopBarSection.jsx +0 -65
  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 -233
  72. package/src/chat-core/messages/buildTimelineItems.knowledge-citation.test.mjs +0 -183
  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,233 +0,0 @@
1
- const CITATION_HIT_SCORE_THRESHOLD = 0.05;
2
-
3
- function hasRenderableCitation(citation = {}) {
4
- const score = Number(citation?.score);
5
- if (Number.isFinite(score)) {
6
- return score >= CITATION_HIT_SCORE_THRESHOLD;
7
- }
8
- const matchedText = String(citation?.matched_text || citation?.hit_reason || '').trim();
9
- if (matchedText.length >= 2) {
10
- return true;
11
- }
12
- const snippet = String(citation?.snippet || '').trim();
13
- return snippet.length >= 12;
14
- }
15
-
16
- function normalizeCitationPayload(payload) {
17
- if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
18
- return null;
19
- }
20
- const citations = Array.isArray(payload.citations) ? payload.citations : [];
21
- const filtered = citations.filter((citation) => hasRenderableCitation(citation));
22
- if (filtered.length === 0) {
23
- return null;
24
- }
25
- return {
26
- ...payload,
27
- citations: filtered,
28
- };
29
- }
30
-
31
- export function buildTimelineItems(state = {}, options = {}) {
32
- const messages = Array.isArray(state.messages) ? state.messages : [];
33
- const uiCards = Array.isArray(state.uiCards) ? state.uiCards : [];
34
- const executionCards = Array.isArray(state.executionCards) ? state.executionCards : [];
35
- const streaming = state.streaming || {};
36
- const loading = Boolean(options?.loading);
37
-
38
- const items = [];
39
- let latestAssistantMessageId = '';
40
- for (let index = messages.length - 1; index >= 0; index -= 1) {
41
- const candidate = messages[index];
42
- if (String(candidate?.role || '').trim() === 'assistant') {
43
- latestAssistantMessageId = String(candidate?.message_id || `msg-fallback-${index}`).trim();
44
- break;
45
- }
46
- }
47
-
48
- if (messages.length === 0) {
49
- return {
50
- isEmpty: true,
51
- items: [],
52
- };
53
- }
54
-
55
- messages.forEach((msg, index) => {
56
- const messageId = msg.message_id || `msg-fallback-${index}`;
57
- const primaryFlow = (
58
- msg.primary_flow
59
- && typeof msg.primary_flow === 'object'
60
- && !Array.isArray(msg.primary_flow)
61
- ) ? msg.primary_flow : null;
62
- const chooserState = (
63
- primaryFlow?.chooser_state
64
- && typeof primaryFlow.chooser_state === 'object'
65
- && !Array.isArray(primaryFlow.chooser_state)
66
- ) ? primaryFlow.chooser_state : null;
67
- const authoritativeRoundId = String(primaryFlow?.round_id || '').trim();
68
- items.push({
69
- id: messageId,
70
- type: 'message',
71
- role: msg.role,
72
- content: msg.content,
73
- createdAt: msg.created_at,
74
- client_request_id: msg.client_request_id || '',
75
- intake_session_id: msg.intake_session_id || '',
76
- roundKey: msg.round_key || authoritativeRoundId || '',
77
- roundId: authoritativeRoundId,
78
- roundState: String(primaryFlow?.round_state || '').trim(),
79
- chooserKind: String(chooserState?.kind || '').trim(),
80
- chooserVisible: chooserState?.visible === true,
81
- highlights: msg.highlights ?? [],
82
- sourceTypes: msg.sourceTypes ?? [],
83
- attachments: Array.isArray(msg.attachments) ? msg.attachments : [],
84
- contextUsage: (
85
- msg.context_usage
86
- && typeof msg.context_usage === 'object'
87
- && !Array.isArray(msg.context_usage)
88
- ) ? msg.context_usage : null,
89
- });
90
-
91
- if (msg.role === 'assistant') {
92
- const historicalThinking = Boolean(
93
- msg.agent_status
94
- || msg.execution_trace
95
- || String(msg.thinking_trace || '').trim()
96
- );
97
- if (historicalThinking && messageId === latestAssistantMessageId) {
98
- items.push({
99
- id: `thinking-${messageId}`,
100
- type: 'thinking',
101
- agentStatus: msg.agent_status || {
102
- status: 'completed',
103
- agent: '助手',
104
- },
105
- thinkingTrace: String(msg.thinking_trace || '').trim(),
106
- executionTrace: msg.execution_trace || {
107
- status: 'completed',
108
- step_id: 'historical_trace',
109
- },
110
- executionCards: [],
111
- roundKey: String(msg.round_key || authoritativeRoundId || '').trim(),
112
- loading: false,
113
- });
114
- }
115
- if (msg.knowledge_search && typeof msg.knowledge_search === 'object') {
116
- items.push({
117
- id: `knowledge-search-${messageId}`,
118
- type: 'knowledge_search',
119
- payload: msg.knowledge_search,
120
- roundKey: String(msg.round_key || authoritativeRoundId || '').trim(),
121
- });
122
- }
123
- if (msg.sourcing_candidates && typeof msg.sourcing_candidates === 'object') {
124
- items.push({
125
- id: `sourcing-candidates-history-${messageId}`,
126
- type: 'sourcing_candidates',
127
- payload: msg.sourcing_candidates,
128
- roundKey: String(msg.round_key || authoritativeRoundId || '').trim(),
129
- });
130
- }
131
- const historicalCitationPayload = normalizeCitationPayload(msg.knowledge_citation);
132
- if (historicalCitationPayload) {
133
- items.push({
134
- id: `knowledge-citation-${messageId}`,
135
- type: 'knowledge_citation',
136
- payload: historicalCitationPayload,
137
- roundKey: String(msg.round_key || authoritativeRoundId || '').trim(),
138
- });
139
- }
140
- }
141
- });
142
-
143
- if (uiCards.length > 0) {
144
- items.push({
145
- id: 'conversation-ui-cards',
146
- type: 'ui_cards',
147
- cards: uiCards,
148
- });
149
- }
150
-
151
- if (
152
- streaming.knowledgeSearch
153
- && typeof streaming.knowledgeSearch === 'object'
154
- && !Array.isArray(streaming.knowledgeSearch)
155
- ) {
156
- items.push({
157
- id: `knowledge-search-${String(streaming.knowledgeSearch.run_id || 'latest')}`,
158
- type: 'knowledge_search',
159
- payload: streaming.knowledgeSearch,
160
- roundKey: String(streaming.roundKey || '').trim(),
161
- });
162
- }
163
-
164
- if (
165
- streaming.sourcingCandidates
166
- && typeof streaming.sourcingCandidates === 'object'
167
- && !Array.isArray(streaming.sourcingCandidates)
168
- ) {
169
- const analysisRoute = (
170
- streaming.sourcingCandidates.analysis_route
171
- && typeof streaming.sourcingCandidates.analysis_route === 'object'
172
- && !Array.isArray(streaming.sourcingCandidates.analysis_route)
173
- )
174
- ? streaming.sourcingCandidates.analysis_route
175
- : (
176
- streaming.analysisRoute
177
- && typeof streaming.analysisRoute === 'object'
178
- && !Array.isArray(streaming.analysisRoute)
179
- )
180
- ? streaming.analysisRoute
181
- : null;
182
- items.push({
183
- id: `sourcing-candidates-${String(streaming.sourcingCandidates.run_id || 'latest')}`,
184
- type: 'sourcing_candidates',
185
- payload: streaming.sourcingCandidates,
186
- analysisRoute,
187
- roundKey: String(streaming.roundKey || '').trim(),
188
- });
189
- }
190
-
191
- const streamingCitationPayload = normalizeCitationPayload(streaming.knowledgeCitation);
192
- if (streamingCitationPayload) {
193
- items.push({
194
- id: `knowledge-citation-${String(streamingCitationPayload.run_id || 'latest')}`,
195
- type: 'knowledge_citation',
196
- payload: streamingCitationPayload,
197
- roundKey: String(streaming.roundKey || '').trim(),
198
- });
199
- }
200
-
201
- if (streaming.content) {
202
- items.push({
203
- id: 'streaming-message',
204
- type: 'streaming',
205
- content: streaming.content,
206
- roundKey: String(streaming.roundKey || '').trim(),
207
- });
208
- }
209
-
210
- const hasThinkingSignal = Boolean(streaming.agentStatus || streaming.thinkingTrace || streaming.executionTrace);
211
- if (hasThinkingSignal || loading) {
212
- items.push({
213
- id: hasThinkingSignal ? 'thinking-bubble' : 'thinking-bubble-loading',
214
- type: 'thinking',
215
- agentStatus: streaming.agentStatus || {
216
- status: 'running',
217
- agent: '助手',
218
- },
219
- thinkingTrace: streaming.thinkingTrace,
220
- executionTrace: streaming.executionTrace,
221
- executionCards,
222
- roundKey: String(streaming.roundKey || '').trim(),
223
- loading,
224
- });
225
- }
226
-
227
- return {
228
- isEmpty: false,
229
- items,
230
- };
231
- }
232
-
233
- export default buildTimelineItems;
@@ -1,183 +0,0 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { buildTimelineItems } from './buildTimelineItems.js';
4
-
5
- test('buildTimelineItems appends knowledge_citation artifact from streaming state', () => {
6
- const timeline = buildTimelineItems({
7
- messages: [
8
- {
9
- message_id: 'm1',
10
- role: 'user',
11
- content: 'hello',
12
- created_at: '2026-04-19T00:00:00Z',
13
- },
14
- ],
15
- streaming: {
16
- content: '',
17
- knowledgeCitation: {
18
- run_id: 'retrieval-1',
19
- citations: [{
20
- citation_id: 'cite_1',
21
- source_id: 'src-1',
22
- matched_text: '公司20周年庆典礼品采购',
23
- score: 0.23,
24
- }],
25
- },
26
- roundKey: 'rk-1',
27
- },
28
- });
29
-
30
- const citationItem = timeline.items.find((item) => item.type === 'knowledge_citation');
31
- assert.ok(citationItem);
32
- assert.equal(citationItem.id, 'knowledge-citation-retrieval-1');
33
- assert.equal(citationItem.payload.run_id, 'retrieval-1');
34
- assert.equal(citationItem.payload.citations[0].source_id, 'src-1');
35
- });
36
-
37
- test('buildTimelineItems skips knowledge_citation artifact when citations are non-hit noise', () => {
38
- const timeline = buildTimelineItems({
39
- messages: [
40
- {
41
- message_id: 'm1',
42
- role: 'user',
43
- content: 'hello',
44
- created_at: '2026-04-19T00:00:00Z',
45
- },
46
- ],
47
- streaming: {
48
- content: '',
49
- knowledgeCitation: {
50
- run_id: 'retrieval-noise',
51
- citations: [
52
- {
53
- citation_id: 'cite_noise',
54
- source_id: 'src-noise',
55
- score: 0.0001,
56
- snippet: '周年庆',
57
- },
58
- ],
59
- },
60
- roundKey: 'rk-1',
61
- },
62
- });
63
-
64
- const citationItem = timeline.items.find((item) => item.type === 'knowledge_citation');
65
- assert.equal(citationItem, undefined);
66
- });
67
-
68
- test('buildTimelineItems appends knowledge_search artifact from streaming state', () => {
69
- const timeline = buildTimelineItems({
70
- messages: [
71
- {
72
- message_id: 'm1',
73
- role: 'user',
74
- content: 'hello',
75
- created_at: '2026-04-19T00:00:00Z',
76
- },
77
- ],
78
- streaming: {
79
- content: '',
80
- knowledgeSearch: {
81
- run_id: 'search-1',
82
- query: '上海大型erp供应商',
83
- results: [{ id: 'vendor-1', name: 'Vendor A' }],
84
- },
85
- roundKey: 'rk-1',
86
- },
87
- });
88
-
89
- const searchItem = timeline.items.find((item) => item.type === 'knowledge_search');
90
- assert.ok(searchItem);
91
- assert.equal(searchItem.id, 'knowledge-search-search-1');
92
- assert.equal(searchItem.payload.query, '上海大型erp供应商');
93
- });
94
-
95
- test('buildTimelineItems appends sourcing_candidates artifact from streaming state', () => {
96
- const timeline = buildTimelineItems({
97
- messages: [
98
- {
99
- message_id: 'm1',
100
- role: 'user',
101
- content: 'hello',
102
- created_at: '2026-04-19T00:00:00Z',
103
- },
104
- ],
105
- streaming: {
106
- content: '',
107
- sourcingCandidates: {
108
- run_id: 'source-1',
109
- candidates: [{ supplier_id: 'sup-1', supplier_name: 'Vendor A' }],
110
- },
111
- roundKey: 'rk-1',
112
- },
113
- });
114
-
115
- const sourcingItem = timeline.items.find((item) => item.type === 'sourcing_candidates');
116
- assert.ok(sourcingItem);
117
- assert.equal(sourcingItem.id, 'sourcing-candidates-source-1');
118
- });
119
-
120
- test('buildTimelineItems keeps executionCards on thinking item for step-by-step display', () => {
121
- const timeline = buildTimelineItems({
122
- messages: [
123
- {
124
- message_id: 'm1',
125
- role: 'user',
126
- content: 'hello',
127
- created_at: '2026-04-19T00:00:00Z',
128
- },
129
- ],
130
- executionCards: [
131
- { step_id: 'context_binding', label: '上下文绑定', detail: '绑定 2 个来源', status: 'completed' },
132
- ],
133
- streaming: {
134
- content: '',
135
- executionTrace: { step_id: 'llm', label: '生成回复', status: 'running' },
136
- roundKey: 'rk-2',
137
- },
138
- });
139
-
140
- const thinkingItem = timeline.items.find((item) => item.type === 'thinking');
141
- assert.ok(thinkingItem);
142
- assert.equal(Array.isArray(thinkingItem.executionCards), true);
143
- assert.equal(thinkingItem.executionCards.length, 1);
144
- assert.equal(thinkingItem.executionCards[0].step_id, 'context_binding');
145
- });
146
-
147
- test('buildTimelineItems keeps only latest assistant historical thinking item', () => {
148
- const timeline = buildTimelineItems({
149
- messages: [
150
- {
151
- message_id: 'u1',
152
- role: 'user',
153
- content: 'hello',
154
- created_at: '2026-04-19T00:00:00Z',
155
- },
156
- {
157
- message_id: 'a1',
158
- role: 'assistant',
159
- content: 'first answer',
160
- created_at: '2026-04-19T00:00:01Z',
161
- execution_trace: { step_id: 'knowledge_search', status: 'completed' },
162
- },
163
- {
164
- message_id: 'u2',
165
- role: 'user',
166
- content: 'next',
167
- created_at: '2026-04-19T00:00:02Z',
168
- },
169
- {
170
- message_id: 'a2',
171
- role: 'assistant',
172
- content: 'second answer',
173
- created_at: '2026-04-19T00:00:03Z',
174
- execution_trace: { step_id: 'knowledge_search', status: 'completed' },
175
- },
176
- ],
177
- streaming: {},
178
- });
179
-
180
- const thinkingItems = timeline.items.filter((item) => item.type === 'thinking');
181
- assert.equal(thinkingItems.length, 1);
182
- assert.equal(thinkingItems[0].id, 'thinking-a2');
183
- });
@@ -1,3 +0,0 @@
1
- export const CONTEXT_USAGE_WINDOW_TOKEN_LIMIT = 258000;
2
- export const CONTEXT_USAGE_TOKEN_ESTIMATE_DIVISOR = 2.4;
3
- export const CONTEXT_USAGE_BASE_TOKENS_PER_MESSAGE = 32;
@@ -1,147 +0,0 @@
1
- function coercePositiveInteger(value) {
2
- const parsed = Number(value);
3
- if (!Number.isFinite(parsed)) {
4
- return 0;
5
- }
6
- return Math.max(0, Math.round(parsed));
7
- }
8
-
9
- function extractTimelineMessageText(item) {
10
- if (!item || typeof item !== 'object') {
11
- return '';
12
- }
13
- const candidates = [item.content, item.text, item.message, item.value];
14
- for (const candidate of candidates) {
15
- const normalized = String(candidate || '').replace(/\s+/g, ' ').trim();
16
- if (normalized) {
17
- return normalized;
18
- }
19
- }
20
- return '';
21
- }
22
-
23
- function extractContextUsageFromTrace(trace) {
24
- if (!trace || typeof trace !== 'object' || Array.isArray(trace)) {
25
- return null;
26
- }
27
- if (String(trace.step_id || '').trim() !== 'context_usage') {
28
- return null;
29
- }
30
- const detail = String(trace.detail || '').trim();
31
- if (!detail) {
32
- return null;
33
- }
34
- const modeMatch = detail.match(/mode=([^;/]+)\/([^;]+)/);
35
- const profileMatch = detail.match(/profile=([^;]+)/);
36
- const tokenMatch = detail.match(/tokens≈(\d+)/);
37
- return {
38
- mode_key: modeMatch ? String(modeMatch[1] || '').trim() : '',
39
- mode_state: modeMatch ? String(modeMatch[2] || '').trim() : '',
40
- profile: profileMatch ? String(profileMatch[1] || '').trim() : '',
41
- token_estimate: {
42
- total_prompt_tokens_estimate: tokenMatch ? coercePositiveInteger(tokenMatch[1]) : 0,
43
- },
44
- };
45
- }
46
-
47
- function resolveLatestTimelineContextUsage(items) {
48
- const timelineItems = Array.isArray(items) ? items : [];
49
- for (let index = timelineItems.length - 1; index >= 0; index -= 1) {
50
- const item = timelineItems[index];
51
- if (String(item?.type || '').trim() === 'message' && String(item?.role || '').trim() === 'assistant') {
52
- const usage = item?.contextUsage;
53
- if (usage && typeof usage === 'object' && !Array.isArray(usage)) {
54
- return { source: 'backend', usage };
55
- }
56
- }
57
- if (String(item?.type || '').trim() === 'thinking') {
58
- const usage = extractContextUsageFromTrace(item?.executionTrace);
59
- if (usage) {
60
- return { source: 'stream_trace', usage };
61
- }
62
- }
63
- }
64
- return { source: 'estimate', usage: null };
65
- }
66
-
67
- function buildLayerSummaryLabel(layers) {
68
- if (!Array.isArray(layers) || layers.length === 0) {
69
- return '';
70
- }
71
- const normalizedLayers = layers.filter((layer) => layer && typeof layer === 'object');
72
- if (normalizedLayers.length === 0) {
73
- return '';
74
- }
75
- const includedLayers = normalizedLayers.filter((layer) => layer.included !== false).length;
76
- return `layers ${includedLayers}/${normalizedLayers.length}`;
77
- }
78
-
79
- function buildBudgetStatusLabel({ tokens, limit }) {
80
- if (!Number.isFinite(tokens) || !Number.isFinite(limit) || limit <= 0) {
81
- return '';
82
- }
83
- const ratio = tokens / limit;
84
- if (ratio >= 1) {
85
- return '超限';
86
- }
87
- if (ratio >= 0.85) {
88
- return '临界';
89
- }
90
- if (ratio >= 0.6) {
91
- return '偏高';
92
- }
93
- return '正常';
94
- }
95
-
96
- export function buildContextUsageViewModel({
97
- timelineItems,
98
- fallbackTokenLimit,
99
- tokenEstimateDivisor = 2.4,
100
- baseTokensPerMessage = 32,
101
- }) {
102
- const normalizedTimelineItems = Array.isArray(timelineItems) ? timelineItems : [];
103
- const messageItems = normalizedTimelineItems.filter((item) => String(item?.type || '').trim() === 'message');
104
- const messageTexts = messageItems.map((item) => extractTimelineMessageText(item)).filter(Boolean);
105
- const messageCount = messageTexts.length;
106
- const textCharCount = messageTexts.reduce((total, text) => total + text.length, 0);
107
- const { source, usage } = resolveLatestTimelineContextUsage(timelineItems);
108
- const sourceLabel = source === 'backend'
109
- ? '后端'
110
- : (source === 'stream_trace' ? '流式' : '估算');
111
- const totalTokens = coercePositiveInteger(usage?.token_estimate?.total_prompt_tokens_estimate);
112
- const tokenLimit = coercePositiveInteger(usage?.budget_tokens?.total_tokens) || coercePositiveInteger(fallbackTokenLimit);
113
- const byChars = tokenEstimateDivisor > 0 ? Math.round(textCharCount / tokenEstimateDivisor) : 0;
114
- const byMessages = messageCount * coercePositiveInteger(baseTokensPerMessage);
115
- const estimatedTokens = totalTokens > 0 ? totalTokens : Math.max(byChars, byMessages, 0);
116
- const windowPercent = tokenLimit > 0
117
- ? Math.max(0, Math.min(100, Math.round((estimatedTokens / tokenLimit) * 100)))
118
- : 0;
119
- const modeKey = String(usage?.mode_key || '').trim();
120
- const modeState = String(usage?.mode_state || '').trim();
121
- const profile = String(usage?.profile || '').trim();
122
- const status = buildBudgetStatusLabel({ tokens: estimatedTokens, limit: tokenLimit });
123
- const layerSummary = buildLayerSummaryLabel(usage?.layers);
124
- const profileLabel = profile ? ` · ${profile}` : '';
125
- const modeLabel = modeKey ? ` · ${modeKey}${modeState ? `/${modeState}` : ''}` : '';
126
- const statusLabel = status ? ` · ${status}` : '';
127
- const layerLabel = layerSummary ? ` · ${layerSummary}` : '';
128
- const budgetLabel = tokenLimit > 0
129
- ? `${estimatedTokens.toLocaleString('zh-CN')} / ${tokenLimit.toLocaleString('zh-CN')}`
130
- : `${estimatedTokens.toLocaleString('zh-CN')}`;
131
- return {
132
- source,
133
- usage,
134
- modeKey,
135
- modeState,
136
- profile,
137
- status,
138
- layerSummary,
139
- messageCount,
140
- textCharCount,
141
- totalTokens,
142
- estimatedTokens,
143
- tokenLimit,
144
- windowPercent,
145
- text: `${sourceLabel}${profileLabel}${modeLabel}${statusLabel}${layerLabel} · ${budgetLabel}`,
146
- };
147
- }
@@ -1,74 +0,0 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { buildContextUsageViewModel } from './contextUsageViewModel.js';
4
-
5
- test('buildContextUsageViewModel prefers backend usage over stream trace', () => {
6
- const result = buildContextUsageViewModel({
7
- timelineItems: [
8
- {
9
- type: 'thinking',
10
- executionTrace: {
11
- step_id: 'context_usage',
12
- detail: 'mode=auto/running; profile=medium; tokens≈444',
13
- },
14
- },
15
- {
16
- type: 'message',
17
- role: 'assistant',
18
- content: 'done',
19
- contextUsage: {
20
- profile: 'heavy',
21
- token_estimate: {
22
- total_prompt_tokens_estimate: 123,
23
- },
24
- layers: [
25
- { included: true },
26
- { included: false },
27
- ],
28
- },
29
- },
30
- ],
31
- fallbackTokenLimit: 1000,
32
- });
33
-
34
- assert.equal(result.source, 'backend');
35
- assert.equal(result.estimatedTokens, 123);
36
- assert.equal(result.profile, 'heavy');
37
- assert.equal(result.layerSummary, 'layers 1/2');
38
- });
39
-
40
- test('buildContextUsageViewModel falls back to stream trace then estimate', () => {
41
- const traceResult = buildContextUsageViewModel({
42
- timelineItems: [
43
- {
44
- type: 'thinking',
45
- executionTrace: {
46
- step_id: 'context_usage',
47
- detail: 'mode=requirement/collecting; profile=light; tokens≈333',
48
- },
49
- },
50
- ],
51
- fallbackTokenLimit: 1000,
52
- });
53
-
54
- assert.equal(traceResult.source, 'stream_trace');
55
- assert.equal(traceResult.estimatedTokens, 333);
56
- assert.equal(traceResult.modeKey, 'requirement');
57
-
58
- const estimateResult = buildContextUsageViewModel({
59
- timelineItems: [
60
- {
61
- type: 'message',
62
- role: 'user',
63
- content: '需要一份采购计划。',
64
- },
65
- ],
66
- fallbackTokenLimit: 1000,
67
- tokenEstimateDivisor: 2,
68
- baseTokensPerMessage: 10,
69
- });
70
-
71
- assert.equal(estimateResult.source, 'estimate');
72
- assert.ok(estimateResult.estimatedTokens > 0);
73
- assert.equal(estimateResult.tokenLimit, 1000);
74
- });
@@ -1,41 +0,0 @@
1
- import { useMemo } from 'react';
2
- import { buildTimelineItems } from './buildTimelineItems.js';
3
- import { buildContextUsageViewModel } from './contextUsageViewModel.js';
4
-
5
- export function useContextUsageViewModel({
6
- timelineItems,
7
- chatSessionState,
8
- fallbackTokenLimit = 12000,
9
- tokenEstimateDivisor = 2.4,
10
- baseTokensPerMessage = 32,
11
- loading = false,
12
- }) {
13
- const resolvedTimelineItems = useMemo(() => {
14
- if (Array.isArray(timelineItems)) {
15
- return timelineItems;
16
- }
17
- const timeline = buildTimelineItems({
18
- messages: chatSessionState?.messages,
19
- uiCards: chatSessionState?.uiCards,
20
- executionCards: chatSessionState?.executionCards,
21
- streaming: chatSessionState?.streaming,
22
- }, { loading });
23
- return timeline.items;
24
- }, [
25
- chatSessionState?.executionCards,
26
- chatSessionState?.messages,
27
- chatSessionState?.streaming,
28
- chatSessionState?.uiCards,
29
- loading,
30
- timelineItems,
31
- ]);
32
-
33
- return useMemo(() => buildContextUsageViewModel({
34
- timelineItems: resolvedTimelineItems,
35
- fallbackTokenLimit,
36
- tokenEstimateDivisor,
37
- baseTokensPerMessage,
38
- }), [resolvedTimelineItems, fallbackTokenLimit, tokenEstimateDivisor, baseTokensPerMessage]);
39
- }
40
-
41
- export default useContextUsageViewModel;