@sanqianx/project-knowledge 4.6.1
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/.agents/plugins/marketplace.json +20 -0
- package/.claude-plugin/marketplace.json +17 -0
- package/CHANGELOG.md +780 -0
- package/LICENSE +201 -0
- package/NOTICE +5 -0
- package/README.md +266 -0
- package/_site/README.md +30 -0
- package/_site/_test/ai-profile-resolver-test.js +137 -0
- package/_site/_test/ai-profile-test.js +75 -0
- package/_site/_test/ai-vendor-presets-test.js +91 -0
- package/_site/_test/automation-queue-test.js +14 -0
- package/_site/_test/automation-ui-test.js +40 -0
- package/_site/_test/background-task-registry-test.js +43 -0
- package/_site/_test/baseline-schema-test.js +91 -0
- package/_site/_test/bin-cli-test.js +218 -0
- package/_site/_test/bridge-adapter-test.js +95 -0
- package/_site/_test/bridge-consumer-service-test.js +310 -0
- package/_site/_test/cdp-browser-failure-test.js +100 -0
- package/_site/_test/chat-claudecodeui-match-test.js +92 -0
- package/_site/_test/claude-executable-discovery-test.js +90 -0
- package/_site/_test/claude-session-lifecycle-sweep-test.js +252 -0
- package/_site/_test/claude-workbench-test.js +170 -0
- package/_site/_test/client-capture-restart-e2e-test.js +189 -0
- package/_site/_test/codex-conversation-projection-test.js +59 -0
- package/_site/_test/codex-notify-fanout-test.js +45 -0
- package/_site/_test/commit-boundary-freeze-test.js +147 -0
- package/_site/_test/commit-conversation-binding-test.js +99 -0
- package/_site/_test/commit-evidence-test.js +110 -0
- package/_site/_test/context-pack-test.js +198 -0
- package/_site/_test/conversation-api-test.js +119 -0
- package/_site/_test/conversation-store-test.js +136 -0
- package/_site/_test/cross-repo-isolation-e2e-test.js +187 -0
- package/_site/_test/data-dir-migration-test.js +77 -0
- package/_site/_test/desktop-browser-compat-test.js +28 -0
- package/_site/_test/desktop-hook-runtime-regression-test.js +204 -0
- package/_site/_test/embedding-config-test.js +32 -0
- package/_site/_test/explicit-commit-processor-test.js +37 -0
- package/_site/_test/fixtures/fake-browser-exit.js +10 -0
- package/_site/_test/fixtures/fake-browser-hang.js +10 -0
- package/_site/_test/fixtures/make-git-repos.js +89 -0
- package/_site/_test/folder-picker-output-test.js +19 -0
- package/_site/_test/full-integration-e2e-test.js +165 -0
- package/_site/_test/git-validation-test.js +81 -0
- package/_site/_test/hook-runtime-endpoint-test.js +88 -0
- package/_site/_test/hook-status-repair-api-test.js +152 -0
- package/_site/_test/hook-trigger-test.js +85 -0
- package/_site/_test/hook-worktree-test.js +42 -0
- package/_site/_test/import-preflight-api-test.js +233 -0
- package/_site/_test/import-ui-flow-test.js +171 -0
- package/_site/_test/index-builder-test.js +41 -0
- package/_site/_test/index-writer-concurrency-test.js +135 -0
- package/_site/_test/integration-adapters-test.js +243 -0
- package/_site/_test/integration-capture-setup-test.js +108 -0
- package/_site/_test/integration-surface-coverage-test.js +165 -0
- package/_site/_test/kb-framework-test.js +57 -0
- package/_site/_test/kbpath-follow-test.js +63 -0
- package/_site/_test/knowledge-db-test.js +144 -0
- package/_site/_test/knowledge-language-control-test.js +170 -0
- package/_site/_test/knowledge-maintenance-test.js +40 -0
- package/_site/_test/knowledge-migration-test.js +75 -0
- package/_site/_test/knowledge-promotion-recovery-test.js +260 -0
- package/_site/_test/knowledge-query-test.js +54 -0
- package/_site/_test/knowledge-retrieval-service-test.js +77 -0
- package/_site/_test/knowledge-scopes-test.js +37 -0
- package/_site/_test/knowledge-storage-location-test.js +28 -0
- package/_site/_test/knowledge-storage-startup-test.js +52 -0
- package/_site/_test/knowledge-store-logs-supervision-test.js +89 -0
- package/_site/_test/legacy-claude-session-alias-test.js +106 -0
- package/_site/_test/legacy-forward-compat-test.js +257 -0
- package/_site/_test/legacy-project-upgrade-e2e-test.js +363 -0
- package/_site/_test/legacy-routes-removed-test.js +47 -0
- package/_site/_test/legacy-workbench-exclusion-test.js +92 -0
- package/_site/_test/log-redaction-test.js +58 -0
- package/_site/_test/logging-api-test.js +89 -0
- package/_site/_test/logging-sse-no-gap-test.js +88 -0
- package/_site/_test/logging-ui-test.js +104 -0
- package/_site/_test/markdown-delta-overlay-test.js +85 -0
- package/_site/_test/markdown-maintenance-api-test.js +75 -0
- package/_site/_test/markdown-maintenance-test.js +11 -0
- package/_site/_test/mcp-server-test.js +149 -0
- package/_site/_test/model-context-windows-test.js +89 -0
- package/_site/_test/module-bridge-eventbridge-test.js +193 -0
- package/_site/_test/non-release-ci-test.js +34 -0
- package/_site/_test/offline-boundary-isolation-test.js +32 -0
- package/_site/_test/orphaned-claim-recovery-test.js +80 -0
- package/_site/_test/p0-data-migration-characterization-test.js +37 -0
- package/_site/_test/p0-e2e-gate-test.js +347 -0
- package/_site/_test/package-boundary-test.js +42 -0
- package/_site/_test/package-startup-test.js +56 -0
- package/_site/_test/packaged-ui-smoke-test.js +79 -0
- package/_site/_test/path-consistency-test.js +151 -0
- package/_site/_test/pending-sweep-test.js +9 -0
- package/_site/_test/post-commit-automation-test.js +87 -0
- package/_site/_test/pr-consumer-contract-test.js +55 -0
- package/_site/_test/project-control-panel-task14-test.js +63 -0
- package/_site/_test/project-delete-recovery-test.js +64 -0
- package/_site/_test/project-goal-editor-test.js +145 -0
- package/_site/_test/project-layout-v2-migration-test.js +127 -0
- package/_site/_test/project-lifecycle-transaction-test.js +106 -0
- package/_site/_test/project-remove-running-guard-test.js +79 -0
- package/_site/_test/project-store-test.js +90 -0
- package/_site/_test/prompt-settings-test.js +115 -0
- package/_site/_test/protected-architecture-gate-test.js +123 -0
- package/_site/_test/refactor-characterization-test.js +36 -0
- package/_site/_test/release-version-sync-test.js +98 -0
- package/_site/_test/requirement-binding-test.js +113 -0
- package/_site/_test/requirement-recorder-test.js +173 -0
- package/_site/_test/run-all-tests.js +156 -0
- package/_site/_test/runtime-endpoint-test.js +64 -0
- package/_site/_test/scanner-test.js +83 -0
- package/_site/_test/server-runtime-migration-safety-test.js +27 -0
- package/_site/_test/server-security-test.js +96 -0
- package/_site/_test/sessions-stream-test.js +92 -0
- package/_site/_test/shared-contracts-test.js +58 -0
- package/_site/_test/simple-import-test.js +97 -0
- package/_site/_test/startup-analysis-disabled-test.js +16 -0
- package/_site/_test/storage-foundation-test.js +78 -0
- package/_site/_test/structured-logger-test.js +101 -0
- package/_site/_test/task15-20-ui-flow-test.js +148 -0
- package/_site/_test/team-knowledge-store-sparse-checkout-test.js +196 -0
- package/_site/_test/team-knowledge-store-test.js +131 -0
- package/_site/_test/tracking-start-test.js +143 -0
- package/_site/_test/ui-i18n-toggle-test.js +114 -0
- package/_site/_test/ui-smoke-test.js +73 -0
- package/_site/_test/v4122-upgrade-data-contract-test.js +56 -0
- package/_site/_test/workbench-permission-test.js +118 -0
- package/_site/_test/workspace-ui-contract-test.js +43 -0
- package/_site/lib/ai-profile-resolver.js +78 -0
- package/_site/lib/ai-vendor-presets.js +83 -0
- package/_site/lib/ai-workspace.js +104 -0
- package/_site/lib/atomic-file.js +204 -0
- package/_site/lib/automation-config.js +140 -0
- package/_site/lib/bridge-adapter.js +256 -0
- package/_site/lib/bridge-consumer-service.js +402 -0
- package/_site/lib/claude-cli-runner.js +1599 -0
- package/_site/lib/commit-conversation-binder.js +246 -0
- package/_site/lib/commit-processing-ledger.js +27 -0
- package/_site/lib/commit-prompt.js +202 -0
- package/_site/lib/commit-reconciler.js +444 -0
- package/_site/lib/context-pack-builder.js +299 -0
- package/_site/lib/contracts.js +279 -0
- package/_site/lib/conversation-exclusions.js +204 -0
- package/_site/lib/conversation-query-service.js +161 -0
- package/_site/lib/conversation-store.js +385 -0
- package/_site/lib/data-dir.js +81 -0
- package/_site/lib/data-state-classifier.js +40 -0
- package/_site/lib/embedding-service.js +85 -0
- package/_site/lib/evidence-bundle.js +228 -0
- package/_site/lib/folder-picker-output.js +24 -0
- package/_site/lib/git-runner.js +62 -0
- package/_site/lib/github-team-store.js +1175 -0
- package/_site/lib/hook-manager.js +202 -0
- package/_site/lib/index-builder.js +157 -0
- package/_site/lib/index-service.js +173 -0
- package/_site/lib/integration-installer.js +907 -0
- package/_site/lib/kb-framework.js +191 -0
- package/_site/lib/kb-validator.js +124 -0
- package/_site/lib/knowledge-db.js +363 -0
- package/_site/lib/knowledge-promotion.js +435 -0
- package/_site/lib/knowledge-retrieval-service.js +401 -0
- package/_site/lib/knowledge-schema.js +121 -0
- package/_site/lib/knowledge-tool-runtime.js +359 -0
- package/_site/lib/legacy-data-manifest.js +26 -0
- package/_site/lib/llm-client.js +173 -0
- package/_site/lib/markdown-knowledge-indexer.js +230 -0
- package/_site/lib/migration-service.js +442 -0
- package/_site/lib/model-context-windows.js +82 -0
- package/_site/lib/module-bridge.js +496 -0
- package/_site/lib/post-commit-automation.js +141 -0
- package/_site/lib/project-lifecycle-service.js +511 -0
- package/_site/lib/project-registry-store.js +129 -0
- package/_site/lib/project-store.js +262 -0
- package/_site/lib/prompt-registry.js +148 -0
- package/_site/lib/requirement-adapters.js +10 -0
- package/_site/lib/requirement-binder.js +112 -0
- package/_site/lib/requirement-recorder.js +278 -0
- package/_site/lib/runtime-endpoint.js +160 -0
- package/_site/lib/scanner.js +298 -0
- package/_site/lib/server-app.js +1520 -0
- package/_site/lib/settings-store.js +113 -0
- package/_site/lib/storage-layout.js +162 -0
- package/_site/lib/structured-logger.js +574 -0
- package/_site/scripts/codex-notify-fanout.cjs +51 -0
- package/_site/scripts/folder-picker.ps1 +156 -0
- package/_site/scripts/hook-trigger.js +156 -0
- package/_site/scripts/sync-release-version.js +158 -0
- package/_site/server.js +12 -0
- package/_site/start.bat +26 -0
- package/_site/stop.bat +11 -0
- package/bin/project-knowledge-integrations.js +241 -0
- package/bin/project-knowledge-kb.js +65 -0
- package/bin/project-knowledge-mcp.js +195 -0
- package/bin/project-knowledge.js +439 -0
- package/claude-prompts.json +21 -0
- package/docs/README.zh-CN.md +175 -0
- package/docs/assets/architecture.svg +182 -0
- package/docs/assets/dashboard.png +0 -0
- package/docs/assets/draft-review.png +0 -0
- package/docs/assets/logo.svg +50 -0
- package/docs/assets/runs-drafts.png +0 -0
- package/docs/assets/settings.png +0 -0
- package/docs/project-registry-schema.md +33 -0
- package/docs/team-knowledge-mode-a-plan.md +73 -0
- package/docs/testing-strategy.md +33 -0
- package/package.json +100 -0
- package/plugins/project-knowledge/.claude-plugin/plugin.json +19 -0
- package/plugins/project-knowledge/.codex-plugin/plugin.json +38 -0
- package/plugins/project-knowledge/.mcp.json +14 -0
- package/plugins/project-knowledge/opencode/project-knowledge.md +3 -0
- package/plugins/project-knowledge/skills/project-knowledge/SKILL.md +28 -0
- package/plugins/project-knowledge/skills/project-knowledge/agents/openai.yaml +11 -0
- package/templates/change.md +30 -0
- package/templates/project-readme.md +52 -0
- package/ui/app.css +58 -0
- package/ui/app.js +607 -0
- package/ui/favicon.svg +38 -0
- package/ui/i18n.js +146 -0
- package/ui/index.html +162 -0
package/ui/app.js
ADDED
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
'use strict';
|
|
3
|
+
// Control Center shell: 连接与投影 —— 功能本体在模块(terminal :5760 / vector-hub :8787 / 会话浏览 :8790)。
|
|
4
|
+
const $ = id => document.getElementById(id);
|
|
5
|
+
const i18nApi = window.I18N || {};
|
|
6
|
+
const i18n = typeof i18nApi.t === 'function' ? i18nApi.t : key => key;
|
|
7
|
+
const setLanguage = typeof i18nApi.setLanguage === 'function' ? i18nApi.setLanguage : () => {};
|
|
8
|
+
const activeLanguage = typeof i18nApi.activeLanguage === 'function' ? i18nApi.activeLanguage : () => 'zh-CN';
|
|
9
|
+
const state = {
|
|
10
|
+
projects: [], activeProjectId: '', view: 'terminal', settings: 'logs', settingsOpen: false,
|
|
11
|
+
logs: [], logStream: null, logCursor: '', newLogs: 0,
|
|
12
|
+
messageCount: 0, pendingDeleteId: '',
|
|
13
|
+
modules: { terminal: { url: 'http://127.0.0.1:5760', up: false }, vectorHub: { url: 'http://127.0.0.1:8787', up: false }, eventBridge: { url: 'http://127.0.0.1:8790', up: false } },
|
|
14
|
+
sessionsByProject: new Map(), // projectId -> [{sessionId,title,agentId,updatedAt}]
|
|
15
|
+
fold: { term: false, vh: false, eb: false }, // false = 该模块项目列表折叠(默认)
|
|
16
|
+
linkRetries: { term: 0, vh: 0 }, // kb:select-project 竞态假阴性的有界重试计数
|
|
17
|
+
};
|
|
18
|
+
const today = () => { const d = new Date(), pad = n => String(n).padStart(2, '0'); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; };
|
|
19
|
+
const api = async (url, options = {}) => {
|
|
20
|
+
const response = await fetch(url, { ...options, headers: options.body ? { 'content-type': 'application/json', ...(options.headers || {}) } : options.headers });
|
|
21
|
+
const text = await response.text();
|
|
22
|
+
const body = text ? JSON.parse(text) : {};
|
|
23
|
+
if (!response.ok) throw Object.assign(new Error(body.error?.message || `请求失败 (${response.status})`), { body, status: response.status });
|
|
24
|
+
return body;
|
|
25
|
+
};
|
|
26
|
+
const activeProject = () => state.projects.find(project => project.projectId === state.activeProjectId) || null;
|
|
27
|
+
const setText = (node, value) => { node.textContent = value == null ? '' : String(value); };
|
|
28
|
+
const option = (value, label) => { const node = document.createElement('option'); node.value = value; node.textContent = label; return node; };
|
|
29
|
+
const formatTime = iso => { const d = new Date(iso); return Number.isNaN(d.getTime()) ? '—' : d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); };
|
|
30
|
+
async function copyText(value) {
|
|
31
|
+
const text = String(value == null ? '' : value);
|
|
32
|
+
if (navigator.clipboard?.writeText) { try { await navigator.clipboard.writeText(text); return; } catch {} }
|
|
33
|
+
const input = document.createElement('textarea'); input.value = text; input.setAttribute('readonly', ''); input.style.position = 'fixed'; input.style.opacity = '0'; document.body.append(input); input.select(); document.execCommand('copy'); input.remove();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* ===================== 嵌入协议(壳 ↔ 模块) ===================== */
|
|
37
|
+
const FRAME_OF = { term: 'fr-5760', vh: 'fr-8787', eb: 'fr-8790' };
|
|
38
|
+
// 模块 kb:ready/kb:projects 消息里的 app 标识 → 壳侧的模块键
|
|
39
|
+
const APP_KEY_OF = { 'agent-terminal': 'term', 'vector-hub': 'vh', 'event-bridge': 'eb' };
|
|
40
|
+
// 每个模块的联动状态 chip(会话浏览暂无联动协议,chip 缺省)
|
|
41
|
+
const CHIP_OF = { term: 'sync-5760', vh: 'sync-8787', eb: null };
|
|
42
|
+
function postTo(key, msg) {
|
|
43
|
+
const frame = $(FRAME_OF[key]);
|
|
44
|
+
if (frame && frame.contentWindow) frame.contentWindow.postMessage(msg, '*');
|
|
45
|
+
}
|
|
46
|
+
function syncFold(key) { postTo(key, { type: 'kb:sidebar', visible: state.fold[key] }); }
|
|
47
|
+
function renderFoldIcons() {
|
|
48
|
+
document.querySelectorAll('.nav-fold').forEach(node => {
|
|
49
|
+
const on = state.fold[node.dataset.fold];
|
|
50
|
+
node.classList.toggle('on', on);
|
|
51
|
+
node.title = `模块项目列表:${on ? '已展开,点击折叠' : '已折叠,点击展开'}`;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function linkProjectToModules(project) {
|
|
55
|
+
if (!project) return;
|
|
56
|
+
postTo('term', { type: 'kb:select-project', project: project.repoPath || project.projectId });
|
|
57
|
+
postTo('vh', { type: 'kb:select-project', project: (project.modules.vectorHub && project.modules.vectorHub.projectName) || project.projectId });
|
|
58
|
+
}
|
|
59
|
+
window.addEventListener('message', ev => {
|
|
60
|
+
const data = ev.data;
|
|
61
|
+
if (!data || typeof data !== 'object') return;
|
|
62
|
+
const moduleKey = APP_KEY_OF[data.app] || (data.app ? null : 'term');
|
|
63
|
+
if (data.type === 'kb:ready') {
|
|
64
|
+
if (!moduleKey) return;
|
|
65
|
+
setTimeout(() => { syncFold(moduleKey); linkProjectToModules(activeProject()); }, 150);
|
|
66
|
+
} else if (data.type === 'kb:projects') {
|
|
67
|
+
const chip = moduleKey ? $(CHIP_OF[moduleKey]) : null;
|
|
68
|
+
const project = activeProject();
|
|
69
|
+
if (!chip || !project) return;
|
|
70
|
+
if (data.ok) {
|
|
71
|
+
state.linkRetries[moduleKey] = 0;
|
|
72
|
+
chip.textContent = `联动: ${project.displayName}`;
|
|
73
|
+
chip.style.color = 'var(--good)'; chip.title = '';
|
|
74
|
+
} else {
|
|
75
|
+
chip.textContent = `未登记: ${project.displayName}`;
|
|
76
|
+
chip.style.color = 'var(--warnText)';
|
|
77
|
+
chip.title = '该模块还没有这个项目——在导入项目时勾选登记即可建立联动';
|
|
78
|
+
// 模块项目清单未就绪时 kb:ready 触发的首轮选中会扑空(假阴性):
|
|
79
|
+
// 有界重试补发,真未登记则几次后停下,chip 保持如实提示
|
|
80
|
+
if ((state.linkRetries[moduleKey] || 0) < 4) {
|
|
81
|
+
state.linkRetries[moduleKey] = (state.linkRetries[moduleKey] || 0) + 1;
|
|
82
|
+
const delay = [800, 2500, 6000, 10000][state.linkRetries[moduleKey] - 1] || 10000;
|
|
83
|
+
setTimeout(() => { if (activeProject()) linkProjectToModules(activeProject()); }, delay);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} else if (data.type === 'kb:sessions') {
|
|
87
|
+
if (!data.ok) return;
|
|
88
|
+
state.sessionsByProject.set(data.project, data.sessions || []);
|
|
89
|
+
renderProjectSessions(data.project);
|
|
90
|
+
} else if (data.type === 'kb:session' && data.ok) {
|
|
91
|
+
showView('terminal');
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
function renderBrandStatus() {
|
|
96
|
+
setText($('brand-root'), state.projects.length ? `${state.projects.length} 个项目 · 终端${state.modules.terminal.up ? '✓' : '×'} 检索${state.modules.vectorHub.up ? '✓' : '×'} 会话${state.modules.eventBridge.up ? '✓' : '×'}` : '暂无项目');
|
|
97
|
+
}
|
|
98
|
+
function probeModules() {
|
|
99
|
+
for (const [key, id] of [['term', 'st-5760'], ['vh', 'st-8787'], ['bridge', 'st-8790']]) {
|
|
100
|
+
const status = $(id);
|
|
101
|
+
if (!status) continue;
|
|
102
|
+
const info = key === 'term' ? state.modules.terminal : key === 'vh' ? state.modules.vectorHub : state.modules.eventBridge;
|
|
103
|
+
let attempts = 0;
|
|
104
|
+
const attempt = () => {
|
|
105
|
+
fetch(info.url + '/', { mode: 'no-cors', cache: 'no-store' })
|
|
106
|
+
.then(() => {
|
|
107
|
+
info.up = true;
|
|
108
|
+
status.classList.remove('down'); status.classList.add('ok');
|
|
109
|
+
status.querySelector('.st-text').textContent = `已连接 ${info.url.replace(/^https?:\/\//, '')}`;
|
|
110
|
+
renderBrandStatus();
|
|
111
|
+
})
|
|
112
|
+
.catch(() => {
|
|
113
|
+
info.up = false;
|
|
114
|
+
// 模块服务随壳自启,冷启动(node 起进程 + 服务监听)可能慢于首轮
|
|
115
|
+
// 探测:有界退避重试,避免一次性误报"未检测到"并停在那直到手动刷新
|
|
116
|
+
attempts += 1;
|
|
117
|
+
if (attempts <= 10) {
|
|
118
|
+
status.querySelector('.st-text').textContent = `连接中 ${info.url.replace(/^https?:\/\//, '')}…`;
|
|
119
|
+
setTimeout(attempt, [500, 1000, 2000, 3000][Math.min(attempts - 1, 3)]);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
status.classList.add('down');
|
|
123
|
+
status.querySelector('.st-text').textContent = `未检测到 ${info.url.replace(/^https?:\/\//, '')} — 界面将显示为空白`;
|
|
124
|
+
renderBrandStatus();
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
attempt();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function mountEmbeds() {
|
|
131
|
+
const termUrl = state.modules.terminal.url, vhUrl = state.modules.vectorHub.url, ebUrl = state.modules.eventBridge.url;
|
|
132
|
+
$('fr-5760').src = `${termUrl}/agent-terminal/?embed=1`;
|
|
133
|
+
$('fr-8787').src = `${vhUrl}/?embed=1`;
|
|
134
|
+
$('fr-8790').src = `${ebUrl}/?embed=1`;
|
|
135
|
+
$('open-5760').href = `${termUrl}/agent-terminal/`;
|
|
136
|
+
$('open-8787').href = `${vhUrl}/`;
|
|
137
|
+
$('open-8790').href = `${ebUrl}/`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* ===================== 项目栏(聚合投影) ===================== */
|
|
141
|
+
function projectDotClass(project) {
|
|
142
|
+
if (project.projectId === state.activeProjectId) return 'dot claude';
|
|
143
|
+
if (!project.modules.terminal.registered && !project.modules.vectorHub.registered && !project.modules.eventBridge.registered) return 'dot idle';
|
|
144
|
+
return 'dot good';
|
|
145
|
+
}
|
|
146
|
+
function renderProjects() {
|
|
147
|
+
const list = $('project-list');
|
|
148
|
+
list.replaceChildren();
|
|
149
|
+
$('mobile-project').replaceChildren();
|
|
150
|
+
$('logs-project').replaceChildren(option('', '全部项目'));
|
|
151
|
+
for (const project of state.projects) {
|
|
152
|
+
const wrap = document.createElement('div'); wrap.className = 'project-item'; wrap.dataset.projectId = project.projectId;
|
|
153
|
+
const card = document.createElement('button');
|
|
154
|
+
card.type = 'button';
|
|
155
|
+
card.className = `project-card${project.projectId === state.activeProjectId ? ' active' : ''}`;
|
|
156
|
+
card.dataset.projectId = project.projectId;
|
|
157
|
+
const line = document.createElement('div'); line.className = 'project-line';
|
|
158
|
+
const main = document.createElement('div');
|
|
159
|
+
const name = document.createElement('div'); name.className = 'project-name'; name.textContent = project.displayName;
|
|
160
|
+
const badges = document.createElement('div'); badges.className = 'module-badges';
|
|
161
|
+
const termBadge = document.createElement('span'); termBadge.className = `mbadge${project.modules.terminal.registered ? ' on' : ''}`; termBadge.textContent = '端'; termBadge.title = project.modules.terminal.registered ? '已登记到 Agent 终端' : '未登记到 Agent 终端';
|
|
162
|
+
const vhBadge = document.createElement('span'); vhBadge.className = `mbadge${project.modules.vectorHub.registered ? ' on' : ''}`; vhBadge.textContent = '检'; vhBadge.title = project.modules.vectorHub.registered ? '已登记到 vector-hub' : '未登记到 vector-hub';
|
|
163
|
+
const ebBadge = document.createElement('span'); ebBadge.className = `mbadge${project.modules.eventBridge && project.modules.eventBridge.registered ? ' on' : ''}`; ebBadge.textContent = '桥'; ebBadge.title = project.modules.eventBridge && project.modules.eventBridge.registered ? '已登记到会话浏览(ai-coding-event-bridge)' : '未登记到会话浏览(ai-coding-event-bridge)';
|
|
164
|
+
badges.append(termBadge, vhBadge, ebBadge);
|
|
165
|
+
main.append(name, badges);
|
|
166
|
+
const caret = document.createElement('span'); caret.className = 'proj-caret'; caret.textContent = '▸'; caret.title = '历史会话';
|
|
167
|
+
const dot = document.createElement('span'); dot.className = projectDotClass(project);
|
|
168
|
+
line.append(main, dot, caret);
|
|
169
|
+
const repo = document.createElement('div'); repo.className = 'project-path'; repo.textContent = project.repoPath || '';
|
|
170
|
+
card.append(line, repo);
|
|
171
|
+
card.addEventListener('click', event => { if (event.target.closest('.proj-caret')) return; selectProject(project.projectId); });
|
|
172
|
+
caret.addEventListener('click', event => {
|
|
173
|
+
event.stopPropagation();
|
|
174
|
+
const sessions = wrap.querySelector('.proj-sessions');
|
|
175
|
+
const expanded = !sessions.hidden;
|
|
176
|
+
sessions.hidden = expanded;
|
|
177
|
+
caret.classList.toggle('open', !expanded);
|
|
178
|
+
if (!expanded && !state.sessionsByProject.has(project.repoPath || project.projectId)) {
|
|
179
|
+
sessions.replaceChildren(sessionPlaceholder('正在向终端拉取会话…'));
|
|
180
|
+
// 拉取超时兜底:终端未连接/未登记时不至于永远停在"正在拉取"
|
|
181
|
+
const key = project.repoPath || project.projectId;
|
|
182
|
+
setTimeout(() => {
|
|
183
|
+
if (!state.sessionsByProject.has(key)) {
|
|
184
|
+
const host = sessions.querySelector('.sess-empty');
|
|
185
|
+
if (host) host.textContent = '未能获取会话(终端未连接或该项目未登记到终端)';
|
|
186
|
+
}
|
|
187
|
+
}, 8000);
|
|
188
|
+
postTo('term', { type: 'kb:list-sessions', project: key });
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
card.addEventListener('contextmenu', event => {
|
|
192
|
+
event.preventDefault();
|
|
193
|
+
openContextmenu(project.projectId, event.clientX, event.clientY);
|
|
194
|
+
});
|
|
195
|
+
const sessions = document.createElement('div'); sessions.className = 'proj-sessions'; sessions.hidden = true;
|
|
196
|
+
wrap.append(card, sessions);
|
|
197
|
+
list.append(wrap);
|
|
198
|
+
$('mobile-project').append(option(project.projectId, project.displayName));
|
|
199
|
+
$('logs-project').append(option(project.projectId, project.displayName));
|
|
200
|
+
}
|
|
201
|
+
$('mobile-project').value = state.activeProjectId;
|
|
202
|
+
updateProjectContext();
|
|
203
|
+
}
|
|
204
|
+
function sessionPlaceholder(text) { const div = document.createElement('div'); div.className = 'sess-empty'; div.textContent = text; return div; }
|
|
205
|
+
function renderProjectSessions(projectKey) {
|
|
206
|
+
const wrap = [...document.querySelectorAll('.project-item')].find(node => {
|
|
207
|
+
const project = state.projects.find(item => item.projectId === node.dataset.projectId);
|
|
208
|
+
return project && (project.repoPath || project.projectId) === projectKey;
|
|
209
|
+
});
|
|
210
|
+
if (!wrap) return;
|
|
211
|
+
const host = wrap.querySelector('.proj-sessions');
|
|
212
|
+
const sessions = state.sessionsByProject.get(projectKey) || [];
|
|
213
|
+
host.replaceChildren();
|
|
214
|
+
if (!sessions.length) { host.append(sessionPlaceholder('该模块还没有此项目的会话')); return; }
|
|
215
|
+
for (const session of sessions) {
|
|
216
|
+
const row = document.createElement('button'); row.type = 'button'; row.className = 'sess-row';
|
|
217
|
+
row.dataset.sessionId = session.sessionId;
|
|
218
|
+
const title = document.createElement('span'); title.className = 'sess-title'; title.textContent = session.title || session.sessionId;
|
|
219
|
+
const agent = document.createElement('span'); agent.className = 'sess-agent'; agent.textContent = session.agentId || '';
|
|
220
|
+
row.append(title, agent);
|
|
221
|
+
row.addEventListener('click', () => {
|
|
222
|
+
postTo('term', { type: 'kb:open-session', sessionId: session.sessionId });
|
|
223
|
+
showView('terminal');
|
|
224
|
+
});
|
|
225
|
+
host.append(row);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function updateProjectContext() {
|
|
229
|
+
const project = activeProject();
|
|
230
|
+
setText($('page-title'), ({ terminal: 'Agent 终端', search: '知识检索', bridge: '会话浏览', import: '导入项目' })[state.view] || 'Agent 终端');
|
|
231
|
+
linkProjectToModules(project);
|
|
232
|
+
}
|
|
233
|
+
function selectProject(projectId) {
|
|
234
|
+
state.activeProjectId = projectId;
|
|
235
|
+
renderProjects();
|
|
236
|
+
}
|
|
237
|
+
const viewTitles = { terminal: 'Agent 终端', search: '知识检索', bridge: '会话浏览', import: '导入项目' };
|
|
238
|
+
function showView(view) {
|
|
239
|
+
state.view = view;
|
|
240
|
+
document.querySelectorAll('.view').forEach(node => node.classList.toggle('active', node.id === `view-${view}`));
|
|
241
|
+
document.querySelectorAll('[data-go]').forEach(node => node.classList.toggle('active', node.dataset.go === view));
|
|
242
|
+
setText($('page-title'), viewTitles[view] || view);
|
|
243
|
+
hideContextmenu();
|
|
244
|
+
}
|
|
245
|
+
async function loadState() {
|
|
246
|
+
const aggregate = await api('/api/projects/aggregated');
|
|
247
|
+
state.projects = (aggregate.projects || []).map(project => ({
|
|
248
|
+
projectId: project.projectId,
|
|
249
|
+
displayName: project.name || project.projectId,
|
|
250
|
+
repoPath: project.workspacePath || '',
|
|
251
|
+
knowledgePath: project.knowledgePath || '',
|
|
252
|
+
modules: project.modules || { terminal: {}, vectorHub: {}, eventBridge: {} },
|
|
253
|
+
}));
|
|
254
|
+
if (!state.activeProjectId || !state.projects.some(project => project.projectId === state.activeProjectId)) state.activeProjectId = state.projects[0]?.projectId || '';
|
|
255
|
+
renderProjects();
|
|
256
|
+
}
|
|
257
|
+
async function refreshModulesHealth() {
|
|
258
|
+
try {
|
|
259
|
+
const body = await api('/api/modules/health');
|
|
260
|
+
state.modules.terminal = { ...state.modules.terminal, ...body.modules.terminal };
|
|
261
|
+
state.modules.vectorHub = { ...state.modules.vectorHub, ...body.modules.vectorHub };
|
|
262
|
+
state.modules.eventBridge = { ...state.modules.eventBridge, ...body.modules.eventBridge };
|
|
263
|
+
} catch { /* server down; probes keep their state */ }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/* ===================== 导入:只触发两个模块的登记(壳不自建设置面) ===================== */
|
|
267
|
+
async function submitImport(event) {
|
|
268
|
+
event.preventDefault();
|
|
269
|
+
const notice = $('import-notice') || (() => { const n = document.createElement('div'); n.id = 'import-notice'; n.className = 'notice'; n.hidden = true; document.querySelector('#import-form').appendChild(n); return n; })();
|
|
270
|
+
const payload = buildImportPayload();
|
|
271
|
+
try {
|
|
272
|
+
const body = await api('/api/projects/import', { method: 'POST', body: JSON.stringify(payload) });
|
|
273
|
+
const projectId = body.projectId || body.project?.projectId || '';
|
|
274
|
+
renderModuleRegistration(body.modules);
|
|
275
|
+
notice.hidden = false; notice.className = 'notice'; notice.textContent = '项目已导入并登记到模块。';
|
|
276
|
+
await loadState(); selectProject(projectId); showView('terminal'); event.target.reset(); renderFoldIcons();
|
|
277
|
+
} catch (error) { notice.hidden = false; notice.className = 'notice error'; notice.textContent = error.message; }
|
|
278
|
+
}
|
|
279
|
+
function renderModuleRegistration(modules) {
|
|
280
|
+
const node = $('pv-modules');
|
|
281
|
+
if (!modules) { setText(node, '待登记'); return; }
|
|
282
|
+
const term = modules.terminal && modules.terminal.ok ? '终端 ✓' : modules.terminal && modules.terminal.status === 'pending' ? '终端 待登记' : '终端 ✗';
|
|
283
|
+
const vh = modules.vectorHub && modules.vectorHub.ok ? '检索 ✓' : modules.vectorHub && modules.vectorHub.status === 'pending' ? '检索 待登记' : '检索 ✗';
|
|
284
|
+
const eb = modules.eventBridge && modules.eventBridge.ok ? '会话 ✓' : modules.eventBridge && modules.eventBridge.status === 'pending' ? '会话 待登记' : '会话 ✗';
|
|
285
|
+
setText(node, `${term} · ${vh} · ${eb}`);
|
|
286
|
+
}
|
|
287
|
+
function buildImportPayload() {
|
|
288
|
+
// 语言/配置走后端默认(zh-CN / 终端 profile 唯一源)——壳上不再暴露
|
|
289
|
+
return {
|
|
290
|
+
localPath: $('import-path').value,
|
|
291
|
+
knowledgePath: $('import-knowledge-path').value.trim() || null,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let preflightSeq = 0;
|
|
296
|
+
let preflightTimer = null;
|
|
297
|
+
function schedulePreflight() {
|
|
298
|
+
if (preflightTimer) clearTimeout(preflightTimer);
|
|
299
|
+
preflightTimer = setTimeout(runPreflight, 250);
|
|
300
|
+
}
|
|
301
|
+
async function runPreflight() {
|
|
302
|
+
const seq = ++preflightSeq;
|
|
303
|
+
const submit = $('import-submit');
|
|
304
|
+
const path = $('import-path').value.trim();
|
|
305
|
+
if (!path) {
|
|
306
|
+
submit.disabled = true;
|
|
307
|
+
setText($('pv-git-status'), '等待路径输入');
|
|
308
|
+
setText($('pv-knowledge-root'), $('import-knowledge-path').value.trim() || '—');
|
|
309
|
+
setText($('pv-modules'), '待登记');
|
|
310
|
+
$('import-preflight').hidden = true;
|
|
311
|
+
$('import-errors').hidden = true;
|
|
312
|
+
$('import-auto-init').hidden = true;
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
const result = await api('/api/projects/preflight-import', { method: 'POST', body: JSON.stringify(buildImportPayload()) });
|
|
317
|
+
if (seq !== preflightSeq) return;
|
|
318
|
+
renderPreflight(result);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
if (seq !== preflightSeq) return;
|
|
321
|
+
submit.disabled = true;
|
|
322
|
+
const err = $('import-errors');
|
|
323
|
+
err.hidden = false;
|
|
324
|
+
err.textContent = `Preflight 失败:${error.message}`;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function renderPreflight(result) {
|
|
328
|
+
const submit = $('import-submit');
|
|
329
|
+
submit.disabled = !result.ready;
|
|
330
|
+
setText($('pv-knowledge-root'), $('import-knowledge-path').value.trim() || (result.effective && result.effective.knowledgeRoot) || '—');
|
|
331
|
+
const gitCheck = result.checks && result.checks.git;
|
|
332
|
+
if (gitCheck && gitCheck.ok) {
|
|
333
|
+
const desc = gitCheck.emptyRepo ? 'Git 仓库(空仓库)' : gitCheck.headCommit ? `Git 仓库 @ ${gitCheck.headCommit.slice(0, 7)}` : 'Git 仓库';
|
|
334
|
+
setText($('pv-git-status'), desc);
|
|
335
|
+
} else if (gitCheck && gitCheck.plannedInit) setText($('pv-git-status'), '非 Git 目录(将自动初始化)');
|
|
336
|
+
else setText($('pv-git-status'), '路径无效');
|
|
337
|
+
$('import-auto-init').hidden = !result.plannedGitInit;
|
|
338
|
+
const preNotice = $('import-preflight');
|
|
339
|
+
if (result.ready) { preNotice.hidden = false; preNotice.className = 'notice'; preNotice.textContent = '前置条件就绪,可以导入。'; }
|
|
340
|
+
else preNotice.hidden = true;
|
|
341
|
+
const errNotice = $('import-errors');
|
|
342
|
+
if (result.problems && result.problems.length) {
|
|
343
|
+
errNotice.hidden = false; errNotice.className = 'notice error'; errNotice.replaceChildren();
|
|
344
|
+
const strong = document.createElement('strong'); strong.textContent = '导入前需要处理:'; errNotice.append(strong);
|
|
345
|
+
const list = document.createElement('ul'); list.style.margin = '6px 0 0 18px'; list.style.padding = '0';
|
|
346
|
+
for (const problem of result.problems) {
|
|
347
|
+
const li = document.createElement('li');
|
|
348
|
+
li.textContent = `${problem.message} [${problem.code}]`;
|
|
349
|
+
if (problem.action) { const hint = document.createElement('span'); hint.className = 'muted'; hint.style.marginLeft = '6px'; hint.textContent = `操作:${problem.action}`; li.append(hint); }
|
|
350
|
+
list.append(li);
|
|
351
|
+
}
|
|
352
|
+
errNotice.append(list);
|
|
353
|
+
} else { errNotice.hidden = true; errNotice.textContent = ''; }
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/* ===================== 日志(连接层运行记录) ===================== */
|
|
357
|
+
function logParams(includeCursor = false) {
|
|
358
|
+
const params = new URLSearchParams(); const date = $('logs-date').value || today();
|
|
359
|
+
params.set('from', date); params.set('to', date); params.set('pageSize', $('logs-limit').value || '500');
|
|
360
|
+
if ($('logs-project').value) params.set('projectId', $('logs-project').value);
|
|
361
|
+
if ($('logs-scope').value === 'important') params.set('levels', 'info,warn,error,fatal');
|
|
362
|
+
if ($('logs-scope').value === 'errors') params.set('levels', 'warn,error,fatal');
|
|
363
|
+
if ($('logs-search').value.trim()) params.set('q', $('logs-search').value.trim());
|
|
364
|
+
if (includeCursor && state.logCursor) params.set('streamCursor', state.logCursor);
|
|
365
|
+
return params;
|
|
366
|
+
}
|
|
367
|
+
let loadLogsSeq = 0; // 连续触发加载时丢弃过期响应,避免清空后的列表被旧渲染覆盖或重复开流
|
|
368
|
+
async function loadLogs() {
|
|
369
|
+
const seq = ++loadLogsSeq;
|
|
370
|
+
stopLogStream(); $('logs-date').value = $('logs-date').value || today();
|
|
371
|
+
$('record-list').replaceChildren(); $('record-empty').style.display = 'none';
|
|
372
|
+
setText($('logs-summary'), '正在读取运行记录…');
|
|
373
|
+
try {
|
|
374
|
+
const body = await api(`/api/logs?${logParams()}`);
|
|
375
|
+
if (seq !== loadLogsSeq) return;
|
|
376
|
+
state.logs = [...(body.entries || [])].reverse(); state.logCursor = body.streamCursor || ''; state.newLogs = 0; renderLogs();
|
|
377
|
+
requestAnimationFrame(() => { const list = $('record-list'); list.scrollTop = list.scrollHeight; });
|
|
378
|
+
if ($('logs-date').value === today()) startLogStream();
|
|
379
|
+
} catch (error) {
|
|
380
|
+
if (seq !== loadLogsSeq) return;
|
|
381
|
+
state.logs = []; renderLogs(); $('record-empty').style.display = 'block';
|
|
382
|
+
$('record-empty').textContent = error.message; setText($('logs-summary'), '读取失败');
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function createRecordEntry(record) {
|
|
386
|
+
const entry = document.createElement('div');
|
|
387
|
+
entry.className = 'record-entry'; entry.dataset.logId = record.id; entry.dataset.level = record.level;
|
|
388
|
+
const row = document.createElement('button'); row.type = 'button'; row.className = 'record-row';
|
|
389
|
+
const main = document.createElement('span'); main.className = 'record-main';
|
|
390
|
+
const title = document.createElement('span'); title.className = 'record-title'; title.textContent = record.message;
|
|
391
|
+
const meta = document.createElement('span'); meta.className = 'record-meta';
|
|
392
|
+
const parts = [
|
|
393
|
+
['record-project', record.projectDisplayName || record.projectId || '系统'],
|
|
394
|
+
['record-category', record.component || record.event || ''],
|
|
395
|
+
['record-duration', record.durationMs != null ? `${record.durationMs.toLocaleString()} ms` : ''],
|
|
396
|
+
];
|
|
397
|
+
parts.forEach(([className, value], index) => {
|
|
398
|
+
if (!value) return;
|
|
399
|
+
if (index) { const sep = document.createElement('span'); sep.className = 'record-meta-sep'; sep.textContent = '·'; meta.append(sep); }
|
|
400
|
+
const node = document.createElement('span'); node.className = className; node.textContent = value; meta.append(node);
|
|
401
|
+
});
|
|
402
|
+
main.append(title, meta);
|
|
403
|
+
const time = document.createElement('span'); time.className = 'record-time'; time.textContent = formatTime(record.ts);
|
|
404
|
+
row.append(main, time);
|
|
405
|
+
const detail = document.createElement('div'); detail.className = 'record-detail';
|
|
406
|
+
const grid = document.createElement('div'); grid.className = 'record-detail-grid';
|
|
407
|
+
for (const [key, value] of [
|
|
408
|
+
['level', String(record.level || '').toUpperCase()], ['component', record.component || '—'], ['event', record.event || '—'],
|
|
409
|
+
['operationId', record.operationId || '—'], ['runId', record.runId || '—'], ['commit', record.commitSha || '—'],
|
|
410
|
+
['phase', record.phase || '—'], ['attempt', String(record.attempt || 0)], ['durationMs', record.durationMs == null ? '—' : String(record.durationMs)],
|
|
411
|
+
]) {
|
|
412
|
+
const span = document.createElement('span'); const b = document.createElement('b'); b.textContent = `${key}:`;
|
|
413
|
+
span.append(b, document.createTextNode(` ${value}`)); grid.append(span);
|
|
414
|
+
}
|
|
415
|
+
const message = document.createElement('div'); message.className = 'record-detail-message'; message.textContent = record.message;
|
|
416
|
+
const actions = document.createElement('div'); actions.className = 'record-detail-actions';
|
|
417
|
+
const copy = document.createElement('button'); copy.type = 'button'; copy.className = 'btn'; copy.textContent = '复制详情';
|
|
418
|
+
copy.addEventListener('click', event => { event.stopPropagation(); copyText(message.textContent); });
|
|
419
|
+
actions.append(copy);
|
|
420
|
+
detail.append(grid, message, actions);
|
|
421
|
+
row.addEventListener('click', () => entry.classList.toggle('open'));
|
|
422
|
+
entry.append(row, detail);
|
|
423
|
+
return entry;
|
|
424
|
+
}
|
|
425
|
+
function renderLogs() {
|
|
426
|
+
const list = $('record-list'); const nearBottom = list.scrollHeight - list.scrollTop - list.clientHeight < 48;
|
|
427
|
+
const frag = document.createDocumentFragment();
|
|
428
|
+
for (const record of state.logs) frag.append(createRecordEntry(record));
|
|
429
|
+
list.replaceChildren(frag);
|
|
430
|
+
$('record-empty').style.display = state.logs.length ? 'none' : 'block';
|
|
431
|
+
const limit = Number($('logs-limit').value || 500);
|
|
432
|
+
setText($('logs-summary'), `已加载 ${state.logs.length.toLocaleString()} 条 · 显示上限 ${limit.toLocaleString()} 条`);
|
|
433
|
+
if (nearBottom) requestAnimationFrame(() => { list.scrollTop = list.scrollHeight; });
|
|
434
|
+
updateNewLogButton();
|
|
435
|
+
}
|
|
436
|
+
function appendLog(record) {
|
|
437
|
+
if (state.logs.some(item => item.id === record.id)) return;
|
|
438
|
+
const list = $('record-list'); const atBottom = list.scrollHeight - list.scrollTop - list.clientHeight < 48;
|
|
439
|
+
state.logs.push(record);
|
|
440
|
+
const limit = Number($('logs-limit').value || 500);
|
|
441
|
+
if (state.logs.length > limit) {
|
|
442
|
+
state.logs.splice(0, state.logs.length - limit);
|
|
443
|
+
while (list.children.length > state.logs.length) list.firstElementChild.remove();
|
|
444
|
+
}
|
|
445
|
+
$('record-empty').style.display = 'none';
|
|
446
|
+
list.append(createRecordEntry(record));
|
|
447
|
+
setText($('logs-summary'), `已加载 ${state.logs.length.toLocaleString()} 条 · 显示上限 ${limit.toLocaleString()} 条`);
|
|
448
|
+
if (!atBottom) state.newLogs += 1;
|
|
449
|
+
updateNewLogButton();
|
|
450
|
+
if (atBottom) requestAnimationFrame(() => { list.scrollTop = list.scrollHeight; });
|
|
451
|
+
}
|
|
452
|
+
function updateNewLogButton() { const button = $('new-records'); button.hidden = !state.newLogs; button.textContent = `有 ${state.newLogs} 条新记录`; }
|
|
453
|
+
function startLogStream() {
|
|
454
|
+
if (!state.logCursor) return;
|
|
455
|
+
const stream = new EventSource(`/api/logs/stream?${logParams(true)}`); state.logStream = stream;
|
|
456
|
+
const warning = $('logs-connection');
|
|
457
|
+
stream.addEventListener('logs/appended', event => { try { appendLog(JSON.parse(event.data).record); } catch {} });
|
|
458
|
+
stream.addEventListener('logs/ready', () => warning.classList.remove('show'));
|
|
459
|
+
stream.onerror = () => warning.classList.add('show');
|
|
460
|
+
}
|
|
461
|
+
function stopLogStream() {
|
|
462
|
+
if (state.logStream) { state.logStream.close(); state.logStream = null; }
|
|
463
|
+
$('logs-connection').classList.remove('show');
|
|
464
|
+
}
|
|
465
|
+
async function exportLogs() {
|
|
466
|
+
const response = await fetch(`/api/logs/export?${logParams()}`); if (!response.ok) throw new Error(`导出失败 (${response.status})`);
|
|
467
|
+
const blob = await response.blob(); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `project-knowledge-logs-${$('logs-date').value || today()}.jsonl`; document.body.append(link); link.click(); link.remove(); setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function toggleTheme() {
|
|
471
|
+
const next = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
|
|
472
|
+
document.documentElement.dataset.theme = next; localStorage.setItem('pk-theme', next);
|
|
473
|
+
setText($('theme-button'), next === 'dark' ? '浅色模式' : '深色模式');
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/* ===================== 设置抽屉(日志) ===================== */
|
|
477
|
+
const settingsTitles = {
|
|
478
|
+
logs: ['日志', '查看系统和项目的运行记录;异常会自动突出显示。'],
|
|
479
|
+
};
|
|
480
|
+
function openSettings(section = 'logs') { state.settingsOpen = true; $('settings-backdrop').classList.add('show'); $('settings-drawer').classList.add('show'); showSettings(section); }
|
|
481
|
+
function closeSettings() { state.settingsOpen = false; $('settings-backdrop').classList.remove('show'); $('settings-drawer').classList.remove('show'); stopLogStream(); }
|
|
482
|
+
function showSettings(section) {
|
|
483
|
+
state.settings = section;
|
|
484
|
+
document.querySelectorAll('[data-settings]').forEach(node => node.classList.toggle('active', node.dataset.settings === section));
|
|
485
|
+
document.querySelectorAll('.settings-section').forEach(node => node.classList.toggle('active', node.id === `settings-${section}`));
|
|
486
|
+
const [title, help] = settingsTitles[section] || ['', ''];
|
|
487
|
+
setText($('settings-title'), title); setText($('settings-help'), help);
|
|
488
|
+
if (section === 'logs') loadLogs(); else stopLogStream();
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/* ===================== 右键管理与删除 ===================== */
|
|
492
|
+
function openContextmenu(projectId, x, y) {
|
|
493
|
+
state.pendingDeleteId = projectId;
|
|
494
|
+
setText($('context-id'), projectId);
|
|
495
|
+
const menu = $('context-menu');
|
|
496
|
+
menu.classList.add('show');
|
|
497
|
+
menu.style.left = Math.min(x, window.innerWidth - 210) + 'px';
|
|
498
|
+
menu.style.top = Math.min(y, window.innerHeight - 110) + 'px';
|
|
499
|
+
}
|
|
500
|
+
function hideContextmenu() { $('context-menu').classList.remove('show'); }
|
|
501
|
+
function openDeleteDialog(projectId) {
|
|
502
|
+
const project = state.projects.find(item => item.projectId === projectId);
|
|
503
|
+
if (!project) return;
|
|
504
|
+
state.pendingDeleteId = projectId;
|
|
505
|
+
setText($('delete-copy'), `确认移除“${project.displayName}”?默认保留知识目录与会话记录。`);
|
|
506
|
+
$('delete-knowledge').checked = false; $('delete-confirm').value = ''; $('delete-confirm-field').hidden = true; $('delete-error').hidden = true;
|
|
507
|
+
$('delete-dialog').showModal();
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/* ===================== 事件绑定 ===================== */
|
|
511
|
+
document.querySelectorAll('[data-go]').forEach(node => node.addEventListener('click', () => showView(node.dataset.go)));
|
|
512
|
+
document.querySelectorAll('[data-settings]').forEach(node => node.addEventListener('click', () => showSettings(node.dataset.settings)));
|
|
513
|
+
for (const id of ['open-settings', 'mobile-settings']) $(id).addEventListener('click', () => openSettings());
|
|
514
|
+
$('close-settings').addEventListener('click', closeSettings); $('settings-backdrop').addEventListener('click', closeSettings);
|
|
515
|
+
$('theme-button').addEventListener('click', toggleTheme);
|
|
516
|
+
$('mobile-project').addEventListener('change', event => selectProject(event.target.value));
|
|
517
|
+
document.querySelectorAll('.nav-fold').forEach(node => node.addEventListener('click', event => {
|
|
518
|
+
event.stopPropagation();
|
|
519
|
+
const key = node.dataset.fold;
|
|
520
|
+
state.fold[key] = !state.fold[key];
|
|
521
|
+
renderFoldIcons(); syncFold(key);
|
|
522
|
+
}));
|
|
523
|
+
const uiLang = $('ui-language');
|
|
524
|
+
if (uiLang) {
|
|
525
|
+
uiLang.value = activeLanguage();
|
|
526
|
+
uiLang.addEventListener('change', event => { setLanguage(event.target.value); renderI18nLabels(); });
|
|
527
|
+
}
|
|
528
|
+
window.__PK_I18N_ONCHANGE__ = () => { if (uiLang) uiLang.value = activeLanguage(); renderI18nLabels(); };
|
|
529
|
+
function renderI18nLabels() {
|
|
530
|
+
document.querySelectorAll('[data-i18n]').forEach(node => {
|
|
531
|
+
const key = node.dataset.i18n;
|
|
532
|
+
if (key) node.textContent = i18n(key);
|
|
533
|
+
});
|
|
534
|
+
if ($('import-path') && $('import-path').placeholder) $('import-path').placeholder = i18n('app.import.path.placeholder');
|
|
535
|
+
}
|
|
536
|
+
$('import-form').addEventListener('submit', submitImport);
|
|
537
|
+
for (const id of ['import-path', 'import-knowledge-path']) {
|
|
538
|
+
$(id).addEventListener('input', schedulePreflight);
|
|
539
|
+
$(id).addEventListener('change', schedulePreflight);
|
|
540
|
+
}
|
|
541
|
+
// 目录选择:优先桌面端原生选择器;Web 模式经代理调用模块自带的原生选择器
|
|
542
|
+
//(① 用终端的 pick-folder,② 用 vector-hub 的)。两者都返回 {path},取消为 path:null。
|
|
543
|
+
async function pickFolderVia(endpoint) {
|
|
544
|
+
if (window.screenX !== undefined) {
|
|
545
|
+
const win = `${Math.round(window.screenX)},${Math.round(window.screenY)},${Math.round(window.outerWidth)},${Math.round(window.outerHeight)}`;
|
|
546
|
+
endpoint += (endpoint.includes('?') ? '&' : '?') + 'win=' + encodeURIComponent(win);
|
|
547
|
+
}
|
|
548
|
+
const response = await fetch(endpoint);
|
|
549
|
+
if (response.status === 501) throw new Error('此环境不支持目录选择,请手动输入路径。');
|
|
550
|
+
if (!response.ok) throw new Error(`目录选择失败 (${response.status})`);
|
|
551
|
+
return response.json();
|
|
552
|
+
}
|
|
553
|
+
function bindFolderPicker(buttonId, inputId, endpoint) {
|
|
554
|
+
$(buttonId).addEventListener('click', async () => {
|
|
555
|
+
const err = $('import-errors');
|
|
556
|
+
try {
|
|
557
|
+
$(buttonId).disabled = true;
|
|
558
|
+
const desktop = window.projectKnowledgeDesktop;
|
|
559
|
+
const result = desktop && typeof desktop.pickFolder === 'function'
|
|
560
|
+
? await desktop.pickFolder()
|
|
561
|
+
: await pickFolderVia(endpoint);
|
|
562
|
+
if (result && result.path) { $(inputId).value = result.path; schedulePreflight(); }
|
|
563
|
+
} catch (error) {
|
|
564
|
+
err.hidden = false; err.className = 'notice error';
|
|
565
|
+
err.textContent = error.message;
|
|
566
|
+
} finally {
|
|
567
|
+
$(buttonId).disabled = false;
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
bindFolderPicker('import-pick-folder', 'import-path', '/api/terminal/system/pick-folder');
|
|
572
|
+
bindFolderPicker('import-pick-knowledge', 'import-knowledge-path', '/api/vectorhub/system/pick-folder');
|
|
573
|
+
const importReset = $('import-reset');
|
|
574
|
+
if (importReset) importReset.addEventListener('click', () => setTimeout(schedulePreflight, 0));
|
|
575
|
+
let logTimer; for (const id of ['logs-date', 'logs-project', 'logs-scope', 'logs-limit']) $(id).addEventListener('change', loadLogs);
|
|
576
|
+
$('logs-search').addEventListener('input', () => { clearTimeout(logTimer); logTimer = setTimeout(loadLogs, 250); });
|
|
577
|
+
$('logs-export').addEventListener('click', () => exportLogs().catch(error => { const warning = $('logs-connection'); warning.classList.add('show'); warning.textContent = error.message; }));
|
|
578
|
+
$('new-records').addEventListener('click', () => { state.newLogs = 0; updateNewLogButton(); const list = $('record-list'); list.scrollTop = list.scrollHeight; });
|
|
579
|
+
$('remove-project').addEventListener('click', () => { hideContextmenu(); openDeleteDialog(state.pendingDeleteId); });
|
|
580
|
+
document.addEventListener('click', event => { if (!event.target.closest('.context-menu') && !event.target.closest('.project-card')) hideContextmenu(); });
|
|
581
|
+
$('delete-knowledge').addEventListener('change', event => { $('delete-confirm-field').hidden = !event.target.checked; });
|
|
582
|
+
$('delete-cancel').addEventListener('click', () => $('delete-dialog').close());
|
|
583
|
+
$('delete-submit').addEventListener('click', async () => {
|
|
584
|
+
const projectId = state.pendingDeleteId || activeProject()?.projectId; if (!projectId) return;
|
|
585
|
+
try {
|
|
586
|
+
// 先解除模块登记(keepSessions:本地会话记录与知识目录全部保留)
|
|
587
|
+
await api(`/api/projects/${encodeURIComponent(projectId)}/module-remove`, { method: 'POST' }).catch(() => {});
|
|
588
|
+
await api(`/api/projects/${encodeURIComponent(projectId)}`, { method: 'DELETE', body: JSON.stringify({ deleteKnowledge: $('delete-knowledge').checked, confirmationToken: $('delete-confirm').value }) });
|
|
589
|
+
$('delete-dialog').close(); state.pendingDeleteId = '';
|
|
590
|
+
await loadState();
|
|
591
|
+
} catch (error) { $('delete-error').hidden = false; $('delete-error').textContent = error.message; }
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
const storedTheme = localStorage.getItem('pk-theme') || 'light';
|
|
595
|
+
document.documentElement.dataset.theme = storedTheme;
|
|
596
|
+
setText($('theme-button'), storedTheme === 'dark' ? '浅色模式' : '深色模式');
|
|
597
|
+
$('logs-date').value = today();
|
|
598
|
+
window.__PK_APP__ = { getState: () => ({ projects: state.projects.length, activeProjectId: state.activeProjectId, view: state.view, settings: state.settings, settingsOpen: state.settingsOpen, logCount: state.logs.length, newLogs: state.newLogs, modules: state.modules, fold: state.fold }), openSettings, showSettings, loadLogs, selectProject, showView };
|
|
599
|
+
renderFoldIcons();
|
|
600
|
+
(async () => {
|
|
601
|
+
await refreshModulesHealth();
|
|
602
|
+
mountEmbeds();
|
|
603
|
+
probeModules();
|
|
604
|
+
await loadState().catch(error => setText($('brand-root'), error.message));
|
|
605
|
+
renderBrandStatus();
|
|
606
|
+
})();
|
|
607
|
+
})();
|
package/ui/favicon.svg
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="AI knowledge automation">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="bg" x1="8" y1="6" x2="56" y2="58" gradientUnits="userSpaceOnUse">
|
|
4
|
+
<stop offset="0" stop-color="#1f3148"/>
|
|
5
|
+
<stop offset=".54" stop-color="#111827"/>
|
|
6
|
+
<stop offset="1" stop-color="#06111f"/>
|
|
7
|
+
</linearGradient>
|
|
8
|
+
<linearGradient id="core" x1="21" y1="16" x2="45" y2="44" gradientUnits="userSpaceOnUse">
|
|
9
|
+
<stop offset="0" stop-color="#dff8ff"/>
|
|
10
|
+
<stop offset=".48" stop-color="#7dd3fc"/>
|
|
11
|
+
<stop offset="1" stop-color="#2f7d64"/>
|
|
12
|
+
</linearGradient>
|
|
13
|
+
<linearGradient id="book" x1="18" y1="35" x2="48" y2="54" gradientUnits="userSpaceOnUse">
|
|
14
|
+
<stop offset="0" stop-color="#f8fafc"/>
|
|
15
|
+
<stop offset="1" stop-color="#b8c6d8"/>
|
|
16
|
+
</linearGradient>
|
|
17
|
+
<filter id="glow" x="-40%" y="-40%" width="180%" height="180%">
|
|
18
|
+
<feGaussianBlur stdDeviation="1.8" result="blur"/>
|
|
19
|
+
<feMerge>
|
|
20
|
+
<feMergeNode in="blur"/>
|
|
21
|
+
<feMergeNode in="SourceGraphic"/>
|
|
22
|
+
</feMerge>
|
|
23
|
+
</filter>
|
|
24
|
+
</defs>
|
|
25
|
+
<rect x="4" y="4" width="56" height="56" rx="15" fill="url(#bg)"/>
|
|
26
|
+
<path d="M16 42c-6-9-3-22 7-28 9-6 21-4 28 5" fill="none" stroke="#38bdf8" stroke-width="3.5" stroke-linecap="round" opacity=".82"/>
|
|
27
|
+
<path d="M48 18h-8l5 6" fill="none" stroke="#38bdf8" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" opacity=".82"/>
|
|
28
|
+
<path d="M50 25c4 10-1 23-12 27-9 4-20 1-25-8" fill="none" stroke="#75b798" stroke-width="3.5" stroke-linecap="round" opacity=".86"/>
|
|
29
|
+
<path d="M13 44h8l-5-6" fill="none" stroke="#75b798" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" opacity=".86"/>
|
|
30
|
+
<path d="M32 13 45 21v15l-13 8-13-8V21z" fill="url(#core)" filter="url(#glow)"/>
|
|
31
|
+
<path d="M32 18v21M23 24l9-6 9 6M23 33l9 6 9-6" fill="none" stroke="#0f1724" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity=".9"/>
|
|
32
|
+
<circle cx="16" cy="20" r="4" fill="#c5a15d"/>
|
|
33
|
+
<circle cx="49" cy="43" r="4" fill="#38bdf8"/>
|
|
34
|
+
<path d="M20 42h25l5 5-6 6H19l-5-5z" fill="url(#book)"/>
|
|
35
|
+
<path d="M20 42 14 48l25 1 6-7" fill="#e2e8f0"/>
|
|
36
|
+
<path d="M19 53h25l6-6" fill="none" stroke="#64748b" stroke-width="2.7" stroke-linecap="round" stroke-linejoin="round"/>
|
|
37
|
+
<path d="M24 47h14" stroke="#4f6f8f" stroke-width="3" stroke-linecap="round"/>
|
|
38
|
+
</svg>
|