@sanqianx/project-knowledge 5.0.20-rc.1 → 5.0.21
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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +6 -1
- package/_site/_test/agent-capture-mcp-ui-test.js +2 -1
- package/_site/_test/agent-capture-ui-test.js +2 -1
- package/_site/_test/knowledge-panel-lifecycle-test.js +154 -0
- package/_site/_test/knowledge-panel-ui-contract-test.js +110 -112
- package/_site/_test/workspace-ui-contract-test.js +31 -31
- package/module-runtime-manifest.json +128 -126
- package/package.json +2 -2
- package/plugins/project-knowledge/.claude-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.codex-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.mcp.json +1 -1
- package/ui/index.html +1 -1
- package/ui/knowledge-panel.html +11 -14
- package/ui/product.css +2 -10
- package/ui/product.js +73 -56
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
|
-
//
|
|
15
|
-
// a
|
|
16
|
-
|
|
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.
|
|
77
|
-
|
|
78
|
-
|
|
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;
|
|
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
|
-
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
}
|
|
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
|
-
//
|
|
140
|
-
//
|
|
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 (
|
|
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
|
-
|
|
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) {
|
|
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();
|
|
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);
|