@sanqianx/project-knowledge 5.0.20 → 5.0.22

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.
package/ui/product.js CHANGED
@@ -11,11 +11,9 @@
11
11
  eventBridge: { id: 'bridgeFrame', url: '/modules/eventbridge/?embed=1&apiBase=/api/eventbridge', view: 'activity', ready: false },
12
12
  engine: { id: 'engineFrame', url: '/modules/engine/?embed=1&apiBase=/api/engine', ready: false },
13
13
  };
14
- // K1 knowledge panel: once the terminal reports kb:ready, the shell registers
15
- // a same-origin iframe provider and streams structured references into it.
16
- // The fixed knowledge column stays the fallback - it folds only after the
17
- // workbench confirms the panel rendered the matching content version.
18
- const knowledgePanel = { providerId: 'kb.knowledge.references', version: 0, registered: false, registering: false, failed: false, panelId: '', pending: null, collapsed: false };
14
+ // Register knowledge as an optional dock tab. Cached retrieval updates
15
+ // never open a tab or expand the dock; only user navigation does that.
16
+ const knowledgePanel = { providerId: 'kb.knowledge.references', version: 0, registered: false, registering: false, failed: false, error: null, panelId: '', pending: null, sending: null, queued: false, deliveredPanelId: '' };
19
17
  const panelRequests = new Map();
20
18
  let panelRequestSeq = 0;
21
19
  const state = { brain: { id: 'brain-default', name: '我的智脑', domains: [] }, root: '', projects: [], projectId: localStorage.getItem('kb.project') || '', view: 'home', scope: 'project', memoryTab: 'confirmed', activityTab: 'updates', health: {}, snapshot: { projects: [], runs: [], events: [], memories: [] }, engineAvailable: false, epoch: 0, contexts: new Map(), stream: null, refreshing: false, importId: '', preflight: null, preflightEpoch: 0 };
@@ -63,19 +61,13 @@
63
61
  post(key, { type: 'kb:select-project', project: ref || '', projectId: project?.projectId || '', requestId: frame.requestId });
64
62
  post(key, { type: 'kb:visibility', visible: frameVisible(key) });
65
63
  }
66
- function panelNotice(text) { $('panelNotice').textContent = text; $('panelNotice').hidden = !text; }
67
- function setKnowledgeCollapsed(collapsed) {
68
- knowledgePanel.collapsed = collapsed;
69
- $('workLayout').classList.toggle('kb-panel-collapsed', collapsed);
70
- }
71
- // Any failure (register/open/relay/ack) restores the original full-width
72
- // column with a non-blocking notice; drafts, events and tasks are untouched.
73
64
  function knowledgePanelFailed(reason) {
65
+ if (knowledgePanel.failed) return;
74
66
  if (knowledgePanel.pending) { clearTimeout(knowledgePanel.pending.timer); knowledgePanel.pending = null; }
75
67
  knowledgePanel.failed = true;
76
- knowledgePanel.registered = false;
77
- setKnowledgeCollapsed(false);
78
- panelNotice(`知识面板不可用(${reason}),已恢复固定知识栏。`);
68
+ knowledgePanel.error = reason;
69
+ knowledgePanel.sending = null; knowledgePanel.queued = false;
70
+ if (knowledgePanel.panelId) toast('知识引用暂时无法加载,请关闭引用标签后重试。');
79
71
  }
80
72
  function panelRequest(type, payload, timeoutMs = 6000) {
81
73
  if (!frames.terminal.ready) return Promise.resolve({ ok: false, code: 'NOT_READY', error: '终端尚未就绪' });
@@ -91,7 +83,8 @@
91
83
  function knowledgePayload(projectId) {
92
84
  const context = state.contexts.get(projectId);
93
85
  return {
94
- title: '本次知识引用', projectId: String(projectId || ''),
86
+ title: '本次知识引用', projectId: String(projectId || ''), projectName: state.projects.find(project => project.projectId === projectId)?.name || '',
87
+ theme: document.documentElement.dataset.theme === 'dark' ? 'dark' : 'light',
95
88
  status: context?.status || 'not-retrieved',
96
89
  references: (context?.references || []).slice(0, 50).map(r => ({
97
90
  entryId: String(r.entryId || ''), title: String(r.title || r.entryId || ''),
@@ -104,53 +97,92 @@
104
97
  async function resetKnowledgePanel() {
105
98
  if (knowledgePanel.registering) return;
106
99
  knowledgePanel.registering = true;
107
- knowledgePanel.registered = false; knowledgePanel.failed = false;
100
+ knowledgePanel.registered = false; knowledgePanel.failed = false; knowledgePanel.error = null;
108
101
  knowledgePanel.panelId = ''; knowledgePanel.version = 0;
102
+ knowledgePanel.ready = false;
103
+ knowledgePanel.sending = null; knowledgePanel.queued = false; knowledgePanel.deliveredPanelId = '';
109
104
  if (knowledgePanel.pending) { clearTimeout(knowledgePanel.pending.timer); knowledgePanel.pending = null; }
110
- setKnowledgeCollapsed(false);
111
105
  try {
112
106
  const reply = await panelRequest('kb:panel/register', { panel: {
113
107
  providerId: knowledgePanel.providerId, panelType: 'knowledge', title: '本次知识引用',
114
108
  url: '/knowledge-panel.html', capabilities: ['read-only'], multiInstance: 'single', appliesTo: { projects: '*' },
115
109
  } });
116
- if (reply.ok) { knowledgePanel.registered = true; panelNotice(''); void pushKnowledgeToPanel(); }
110
+ if (reply.ok) { knowledgePanel.registered = true; if (knowledgePanel.panelId) void pushKnowledgeToPanel(); }
117
111
  else if (!knowledgePanel.failed) knowledgePanelFailed('注册被拒绝');
118
112
  } catch (error) { if (!knowledgePanel.failed) knowledgePanelFailed(error.message); }
119
113
  finally { knowledgePanel.registering = false; }
120
114
  }
121
- async function pushKnowledgeToPanel(projectId = state.projectId) {
122
- if (!knowledgePanel.registered || knowledgePanel.failed || !projectId) return;
123
- const opened = await panelRequest('kb:panel/open', { providerId: knowledgePanel.providerId });
124
- if (!opened.ok) { knowledgePanelFailed('面板无法打开'); return; }
125
- if (opened.panelId) knowledgePanel.panelId = opened.panelId;
126
- const version = ++knowledgePanel.version;
127
- const relayed = await panelRequest('kb:panel/content', { panelId: knowledgePanel.panelId, providerId: knowledgePanel.providerId, version, content: knowledgePayload(projectId) });
128
- if (!relayed.ok) { knowledgePanelFailed('内容投递失败'); return; }
129
- const timer = setTimeout(() => {
130
- if (knowledgePanel.pending && knowledgePanel.pending.version === version) {
131
- knowledgePanel.pending = null; knowledgePanelFailed('面板未确认内容');
115
+ async function pushKnowledgeToPanel(projectId = state.projectId, panelId = knowledgePanel.panelId) {
116
+ if (!knowledgePanel.registered || knowledgePanel.failed || !projectId || !panelId) return;
117
+ if (knowledgePanel.ready === false) return;
118
+ if (knowledgePanel.sending) { knowledgePanel.queued = true; return; }
119
+ const transfer = { projectId, epoch: state.epoch };
120
+ knowledgePanel.sending = transfer;
121
+ const current = () => knowledgePanel.sending === transfer && state.epoch === transfer.epoch && state.projectId === projectId && knowledgePanel.registered && !knowledgePanel.failed;
122
+ try {
123
+ if (!current() || !panelId) return;
124
+ knowledgePanel.panelId = panelId;
125
+ const version = ++knowledgePanel.version;
126
+ if (knowledgePanel.pending) clearTimeout(knowledgePanel.pending.timer);
127
+ const timer = setTimeout(() => {
128
+ if (knowledgePanel.pending && knowledgePanel.pending.version === version) knowledgePanelFailed('面板未确认内容');
129
+ }, 8000);
130
+ // Install before posting: a fast frame can acknowledge before the
131
+ // relay response reaches the host.
132
+ knowledgePanel.pending = { version, projectId, panelId, timer };
133
+ const relayed = await panelRequest('kb:panel/content', { panelId: knowledgePanel.panelId, providerId: knowledgePanel.providerId, version, content: knowledgePayload(projectId) });
134
+ if (current() && !relayed.ok) knowledgePanelFailed('内容投递失败');
135
+ } finally {
136
+ if (knowledgePanel.sending === transfer) {
137
+ knowledgePanel.sending = null;
138
+ if (knowledgePanel.queued) { knowledgePanel.queued = false; void pushKnowledgeToPanel(); }
132
139
  }
133
- }, 8000);
134
- knowledgePanel.pending = { version, projectId, timer };
140
+ }
135
141
  }
136
142
  function handlePanelState(message) {
137
143
  const panel = (message.panels || []).find(p => p.providerId === knowledgePanel.providerId);
138
144
  if (!panel) {
139
- // The panel view was closed or the provider unregistered: the fixed
140
- // column is the only knowledge surface again and must come back.
141
- if (knowledgePanel.collapsed) setKnowledgeCollapsed(false);
145
+ // Closing the view only cancels delivery. Knowledge remains cached
146
+ // without creating another surface or reopening the user's dock.
142
147
  if (knowledgePanel.pending) { clearTimeout(knowledgePanel.pending.timer); knowledgePanel.pending = null; }
148
+ knowledgePanel.panelId = ''; knowledgePanel.deliveredPanelId = '';
149
+ knowledgePanel.ready = false;
150
+ knowledgePanel.failed = false; knowledgePanel.error = null;
151
+ knowledgePanel.sending = null; knowledgePanel.queued = false;
143
152
  return;
144
153
  }
145
- if (!knowledgePanel.pending) return;
154
+ if (panel.panelId !== knowledgePanel.panelId) {
155
+ knowledgePanel.failed = false; knowledgePanel.error = null;
156
+ knowledgePanel.deliveredPanelId = '';
157
+ }
146
158
  if (panel.panelId) knowledgePanel.panelId = panel.panelId;
159
+ knowledgePanel.ready = panel.ready !== false;
160
+ if (!knowledgePanel.ready) {
161
+ if (knowledgePanel.pending) { clearTimeout(knowledgePanel.pending.timer); knowledgePanel.pending = null; }
162
+ knowledgePanel.deliveredPanelId = '';
163
+ return;
164
+ }
165
+ if (panel.error && panel.error !== 'CONTENT_ACK_ERROR') { knowledgePanelFailed('面板加载失败'); return; }
166
+ if (!knowledgePanel.pending) {
167
+ if (!knowledgePanel.sending && knowledgePanel.deliveredPanelId !== panel.panelId) void pushKnowledgeToPanel(state.projectId, panel.panelId);
168
+ return;
169
+ }
147
170
  const pending = knowledgePanel.pending;
171
+ if (pending.projectId !== state.projectId) {
172
+ clearTimeout(pending.timer); knowledgePanel.pending = null;
173
+ if (!knowledgePanel.sending) void pushKnowledgeToPanel(state.projectId, panel.panelId);
174
+ return;
175
+ }
176
+ if (pending.panelId !== panel.panelId) {
177
+ clearTimeout(pending.timer); knowledgePanel.pending = null;
178
+ if (!knowledgePanel.sending) void pushKnowledgeToPanel(state.projectId, panel.panelId);
179
+ return;
180
+ }
148
181
  const ack = panel.content?.ack;
149
182
  if (ack && String(ack.version) === String(pending.version)) {
150
183
  clearTimeout(pending.timer);
151
184
  knowledgePanel.pending = null;
152
- // Fold the fallback column only for a version-matched, error-free ack.
153
- if (ack.ok && !panel.error) { setKnowledgeCollapsed(true); panelNotice(''); }
185
+ if (ack.ok && !panel.error) { knowledgePanel.deliveredPanelId = panel.panelId; }
154
186
  else knowledgePanelFailed(ack.error || '面板渲染失败');
155
187
  } else if (panel.error && panel.error !== 'CONTENT_ACK_ERROR') {
156
188
  clearTimeout(pending.timer);
@@ -185,9 +217,6 @@
185
217
  localStorage.setItem('kb.project', id);
186
218
  $('projectSelect').value = id;
187
219
  Object.keys(frames).forEach(frameContext);
188
- // A project switch re-scopes both surfaces: the fixed column expands again
189
- // and the panel re-receives the new project's content before folding.
190
- setKnowledgeCollapsed(false);
191
220
  if (knowledgePanel.registered && !knowledgePanel.failed && frames.terminal.ready) void pushKnowledgeToPanel(id);
192
221
  render(); setView(state.view);
193
222
  }
@@ -208,7 +237,6 @@
208
237
  if (state.view === 'projects') renderProjects();
209
238
  if (state.view === 'memory') renderMemories();
210
239
  if (state.view === 'activity') renderActivity();
211
- renderContext();
212
240
  renderSystem();
213
241
  }
214
242
  function renderHome(candidates) {
@@ -342,16 +370,6 @@
342
370
  catch { if (projectId === state.projectId) toast('运行详情读取失败,请重新展开重试。'); }
343
371
  finally { delete node.dataset.loading; }
344
372
  }, true);
345
- function renderContext() {
346
- const saved = state.contexts.get(state.projectId);
347
- const context = saved?.status === 'not-retrieved' ? null : saved;
348
- $('contextScope').textContent = context ? ({ ready: '已检索知识', 'file-fallback': '索引暂不可用,已检索原文件', unavailable: '知识服务不可用,未注入上下文', unregistered: '项目尚未登记知识库', disabled: '未启用知识连接' })[context.status] || '本次任务上下文' : '任务发送前自动检索';
349
- $('contextContent').classList.toggle('small-empty', !context?.references?.length);
350
- const seen = new Set();
351
- const references = (context?.references || []).map((r, index) => ({ ...r, index })).filter(r => { const key = `${r.spaceId}:${r.projectId}:${r.entryId}:${r.commitSha || ''}`; if (seen.has(key)) return false; seen.add(key); return true; });
352
- $('contextContent').classList.toggle('empty', !references.length);
353
- $('contextContent').innerHTML = references.length ? references.map(r => `<button class="reference" data-context-index="${r.index}"><b>${esc(r.title)}</b><small>${esc(projectName(r.projectId))} / ${esc(r.entryId)}${r.commitSha ? ` · ${esc(r.commitSha.slice(0, 8))}` : ''}</small><p>${esc(r.text.slice(0, 180))}</p></button>`).join('') : context ? '本次任务未找到可引用的知识。' : '发送任务后显示实际检索到的项目事实、领域经验和已确认记忆。';
354
- }
355
373
  async function openReference(reference) {
356
374
  const id = state.projectId;
357
375
  const epoch = ++referenceEpoch;
@@ -651,7 +669,6 @@
651
669
  if (button.dataset.action === 'unbind') { const p = activeProject(); if (p && !unbinding) { unbindProjectId = p.projectId; $('unbindProjectName').textContent = `解绑「${p.name}」?`; $('unbindFeedback').textContent = ''; $('unbindDialog').showModal(); } }
652
670
  if (button.dataset.retryRun) { await api(`/api/engine/runs/${encodeURIComponent(button.dataset.retryRun)}/retry`, { method: 'POST', body: {} }); await refreshSnapshot(); }
653
671
  if (button.dataset.runPage !== undefined) await loadActivityPage(button.dataset.runPage || null);
654
- if (button.dataset.contextIndex !== undefined) await openReference(state.contexts.get(state.projectId).references[Number(button.dataset.contextIndex)]);
655
672
  if (button.dataset.memoryAction) {
656
673
  const memory = state.snapshot.memories.find(m => m.id === button.dataset.memoryId); if (!memory) return;
657
674
  const action = button.dataset.memoryAction;
@@ -680,11 +697,11 @@
680
697
  if (message.requestId && message.requestId !== frames[key].requestId) return;
681
698
  if (message.type === 'kb:knowledge' && key === 'terminal') {
682
699
  const project = state.projects.find(p => p.modules.terminal.projectId === message.projectId);
683
- if (project) { state.contexts.set(project.projectId, message.knowledge); if (project.projectId === state.projectId) { renderContext(); void pushKnowledgeToPanel(project.projectId); } }
700
+ if (project) { state.contexts.set(project.projectId, message.knowledge); if (project.projectId === state.projectId) { void pushKnowledgeToPanel(project.projectId); } }
684
701
  }
685
702
  });
686
703
  on('projectSelect', 'change', e => selectProject(e.target.value));
687
- on('themeToggle', 'click', () => { document.documentElement.dataset.theme = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark'; localStorage.setItem('kb.theme', document.documentElement.dataset.theme); Object.keys(frames).forEach(key => post(key, { type: 'kb:theme', theme: document.documentElement.dataset.theme })); });
704
+ on('themeToggle', 'click', () => { document.documentElement.dataset.theme = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark'; localStorage.setItem('kb.theme', document.documentElement.dataset.theme); Object.keys(frames).forEach(key => post(key, { type: 'kb:theme', theme: document.documentElement.dataset.theme })); void pushKnowledgeToPanel(); });
688
705
  on('openSystem', 'click', openSystem); on('closeSystem', 'click', closeSystem); on('drawerBackdrop', 'click', closeSystem);
689
706
  on('repairWiring', 'click', repair);
690
707
  on('configureProviders', 'click', () => { closeSystem(); setView('work'); initializeFrame('terminal'); if (frames.terminal.ready) post('terminal', { type: 'kb:open-settings' }); else frames.terminal.openSettings = true; });
@@ -747,7 +764,7 @@
747
764
  state.contexts.delete(projectId);
748
765
  state.projects = state.projects.filter(project => project.projectId !== projectId);
749
766
  if (state.projectId === projectId) { state.projectId = state.projects[0]?.projectId || ''; state.epoch++; }
750
- renderProjectOptions(); setKnowledgeCollapsed(false); render(); Object.keys(frames).forEach(frameContext);
767
+ renderProjectOptions(); render(); Object.keys(frames).forEach(frameContext);
751
768
  $('unbindDialog').close(); unbindProjectId = '';
752
769
  // A late read started before DELETE cannot restore the removed project.
753
770
  await refreshRegistry(true); localStorage.setItem('kb.project', state.projectId);