@sanqianx/project-knowledge 4.8.0 → 5.0.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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +32 -0
- package/README.md +11 -6
- package/_modules/knowledge-engine/README.md +9 -6
- package/_modules/knowledge-engine/package.json +1 -1
- package/_modules/knowledge-engine/src/coordinator.js +11 -3
- package/_modules/knowledge-engine/src/server.js +9 -10
- package/_modules/knowledge-engine/src/store.js +9 -12
- package/_modules/knowledge-engine/ui/app.js +1 -1
- package/_site/_test/fresh-hook-reimport-test.js +53 -0
- package/_site/_test/fresh-product-data-test.js +42 -0
- package/_site/_test/git-path-compatibility-test.js +80 -0
- package/_site/_test/module-model-configuration-test.js +2 -1
- package/_site/_test/product-diagnostics-ui-test.js +17 -3
- package/_site/_test/product-import-ui-test.js +12 -0
- package/_site/_test/tracking-start-test.js +1 -0
- package/_site/lib/data-dir.js +13 -81
- package/_site/lib/engine-connection.js +67 -0
- package/_site/lib/git-path.js +14 -0
- package/_site/lib/hook-manager.js +5 -5
- package/_site/lib/integration-installer.js +2 -1
- package/_site/lib/module-bridge.js +6 -16
- package/_site/lib/post-commit-automation.js +1 -1
- package/_site/lib/product-data.js +24 -0
- package/_site/lib/project-lifecycle-service.js +29 -8
- package/_site/lib/requirement-recorder.js +2 -2
- package/_site/lib/server-app.js +13 -24
- package/_site/scripts/hook-trigger.js +3 -2
- package/docs/README.zh-CN.md +8 -4
- package/docs/fresh-v5-start.md +34 -0
- package/module-runtime-manifest.json +5 -5
- package/package.json +3 -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 +3 -2
- package/ui/product.js +57 -22
- package/vendor-manifest.json +7 -8
- package/_modules/knowledge-engine/src/migration.js +0 -88
- package/_site/_test/data-dir-migration-test.js +0 -77
- package/_site/_test/knowledge-migration-test.js +0 -75
- package/_site/_test/legacy-forward-compat-test.js +0 -257
- package/_site/_test/legacy-project-upgrade-e2e-test.js +0 -363
- package/_site/_test/p0-data-migration-characterization-test.js +0 -37
- package/_site/_test/p0-e2e-gate-test.js +0 -347
- package/_site/_test/project-layout-v2-migration-test.js +0 -127
- package/_site/_test/server-runtime-migration-safety-test.js +0 -27
- package/_site/_test/v4122-upgrade-data-contract-test.js +0 -56
- package/_site/lib/data-state-classifier.js +0 -40
- package/_site/lib/engine-cutover.js +0 -97
- package/_site/lib/legacy-data-manifest.js +0 -26
- package/_site/lib/migration-service.js +0 -442
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# v5 从头开始
|
|
2
|
+
|
|
3
|
+
本版按用户确认不再迁移以前的知识库数据。旧文件留在原位置,但新版不会读取它们;没有对用户代码仓库或旧知识目录执行删除。
|
|
4
|
+
|
|
5
|
+
## 更新
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
project-knowledge stop
|
|
9
|
+
npm install -g @sanqianx/project-knowledge@latest --registry=https://registry.npmjs.org/
|
|
10
|
+
project-knowledge-integrations update
|
|
11
|
+
project-knowledge
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
需要 Node ≥22 和 Git。默认使用 `~/.project-knowledge-v5`,四模块的托管数据也在此目录内。若环境变量 `KB_DATA_DIR` 仍指向旧目录,请取消该设置或改为新的空目录。不要把旧 `projects.json`、设置或引擎状态复制进新目录。
|
|
15
|
+
|
|
16
|
+
在“系统状态”重新配置模型与后台默认模型、embedding 和知识根目录,然后在“领域与项目”导入代码目录及新的空知识目录。原代码目录无需移动。若残留旧知识库托管 Hook,导入预检会要求明确确认替换;不会覆盖第三方 Hook。导入建立 HEAD 基线,不分析已有历史。
|
|
17
|
+
|
|
18
|
+
如果模块是自己独立启动的旧进程,请在它原来的终端中停止后重启新版外壳。外壳不会擅自杀死其他服务;版本或运行期鉴权不一致时显示具体连接阶段,不接管旧服务数据。
|
|
19
|
+
|
|
20
|
+
5.0.1 修复旧版 Git 的项目导入失败(日志中的路径包含
|
|
21
|
+
`--path-format=absolute` 和换行)。升级 5.0.0 → 5.0.1 不需要清空新版数据或
|
|
22
|
+
重新配置模型。失败项目可以直接重试导入;原代码、第三方 Hook 与旧知识文件不删除。
|
|
23
|
+
导入失败会在弹窗保留错误码和操作 ID,系统诊断默认查询“系统与所有项目”,
|
|
24
|
+
可按操作 ID 查找导入事务的真实失败阶段和原始错误。
|
|
25
|
+
|
|
26
|
+
## 解绑与恢复
|
|
27
|
+
|
|
28
|
+
“解绑并保留数据”从项目列表移除登记、取消未开始任务并卸下本项目托管 Hook,保留代码、知识、会话及证据。实际分析或索引写入尚未结束时仍禁止解绑,防止丢失写入状态。队列本身不再被当成正在运行。
|
|
29
|
+
|
|
30
|
+
删除的是旧版本兼容路径,不是新版的故障恢复能力:v5 自身的重启恢复、提交去重、并发限制、失败重试和索引修复仍有效。`upgrade-recovery` 不再存在。
|
|
31
|
+
|
|
32
|
+
## 回归范围
|
|
33
|
+
|
|
34
|
+
旧格式迁移专用测试随旧实现删除,替换为旧目录拒绝且原字节不变、空状态启动、连接失败诊断与恢复、旧托管 Hook 的显式替换和回滚测试。真实提交链路继续由引擎 Git/Hook/journal/晋升/索引回归和五服务网页回归覆盖。模型替身和真实模型的结果分开记录;旧版本真实调用记录不计为本版通过。
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": "project-knowledge/module-content/v1",
|
|
3
3
|
"productFiles": {
|
|
4
|
-
"ui/index.html": "
|
|
5
|
-
"ui/product.js": "
|
|
4
|
+
"ui/index.html": "d090decbb17ffc17ac2ac0ec63b69979e0436d712959e5ca49a02d6d9de7e59d",
|
|
5
|
+
"ui/product.js": "3d04538ca38f6cc1b85ae0337d61c244b6769e4f15f9d029e8ab096991d17dfb",
|
|
6
6
|
"ui/product.css": "1e68f087cec85a69f4497492d31fd1dd8712cf5176b0c721ce765de36ce606e1"
|
|
7
7
|
},
|
|
8
8
|
"modules": {
|
|
@@ -174,12 +174,12 @@
|
|
|
174
174
|
"eventBridge": {
|
|
175
175
|
"name": "@sanqianx/ai-coding-event-bridge-console",
|
|
176
176
|
"installName": "@sanqianx/ai-coding-event-bridge-console",
|
|
177
|
-
"version": "0.7.
|
|
177
|
+
"version": "0.7.1",
|
|
178
178
|
"files": {
|
|
179
179
|
"LICENSE": "94a3bded67f76ec9f6333ad3370111bcb6e5cadbeb6f306a8112f9543320ecc2",
|
|
180
180
|
"README.md": "847242d93395aae17b42bbaa7fc8e36e6f89edecf11a1639b94b34c98729e1a9",
|
|
181
181
|
"console/index.html": "680e6393c9ef88ea35c1eabff537e0b10f7a91c55986f18bca6bfe2a333eed6e",
|
|
182
|
-
"package.json": "
|
|
182
|
+
"package.json": "a6c9fc47669a5aa64dd48d38d3fc2b4fb07322cfb41bbd42b0ac174f37560715",
|
|
183
183
|
"src/agent-detect.js": "06d58150c4295f7d5980ac47fd6fb1a680f1b7c27fd3dcd3f57049825dc5f092",
|
|
184
184
|
"src/bin.js": "e443cfabb8df1a694c9e2415a292db81f183d9e31b7fe8f5f9a14d2a09d03052",
|
|
185
185
|
"src/bridge/bin/commit-boundary.js": "2c87126d9d6467ddae13acc1d0997f2124a68174738b8cd6f8614e7eeccb3d0d",
|
|
@@ -202,7 +202,7 @@
|
|
|
202
202
|
"src/bridge/core/normalizer.js": "34b4023efada0cfdcaaba98775b072041eaf623eb7fde48953654dc27d470ae1",
|
|
203
203
|
"src/bridge/core/paths.js": "6e829c00105d3b9214942f33af6225b338c668ff4cdda5c3ab92cb759d54be30",
|
|
204
204
|
"src/bridge/core/project-registry.js": "8c5cc8e18d55af1a0c2d682c25ea1c37eb6f4687cf3eaa5997fc57532560d866",
|
|
205
|
-
"src/bridge/core/repo-context.js": "
|
|
205
|
+
"src/bridge/core/repo-context.js": "1627d416fd6aaa43d8c4be8ce5097bb69244b1afe7f1ce5e9f71956c5542ce3f",
|
|
206
206
|
"src/bridge/core/runtime-home.js": "e68a3c927b02ca27299755c60c8f709f7ce97c9f4c5080deefe518bf60d9d989",
|
|
207
207
|
"src/bridge/core/semver.js": "7f87ed40f3af3ae0480abc9934946358f0caa5d951472d5892b088aa468ec0f1",
|
|
208
208
|
"src/bridge/core/turn-identity.js": "4ee79410d0122ca93cea565591c183db6bff64b0a467f50a4e590de4ee78c399",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanqianx/project-knowledge",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.1",
|
|
4
4
|
"description": "Knowledge base manager with Git integration, AI-driven analysis, and bilingual (zh-CN/en-US) knowledge output",
|
|
5
5
|
"main": "_site/server.js",
|
|
6
6
|
"scripts": {
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"docs/project-registry-schema.md",
|
|
50
50
|
"docs/team-knowledge-mode-a-plan.md",
|
|
51
51
|
"docs/testing-strategy.md",
|
|
52
|
+
"docs/fresh-v5-start.md",
|
|
52
53
|
"README.md",
|
|
53
54
|
"NOTICE",
|
|
54
55
|
"CHANGELOG.md"
|
|
@@ -79,7 +80,7 @@
|
|
|
79
80
|
"@huggingface/transformers": "3.8.1",
|
|
80
81
|
"@lancedb/lancedb": "0.31.0",
|
|
81
82
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
82
|
-
"@sanqianx/ai-coding-event-bridge-console": "0.7.
|
|
83
|
+
"@sanqianx/ai-coding-event-bridge-console": "0.7.1",
|
|
83
84
|
"@sanqianx/vector-hub": "1.0.7",
|
|
84
85
|
"buffer": "^6.0.3",
|
|
85
86
|
"cheerio": "^1.2.0",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "project-knowledge",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "5.0.1",
|
|
5
5
|
"description": "Read durable project decisions and implementation history from Project Knowledge.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "SanQianX",
|
package/ui/index.html
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
</main>
|
|
33
33
|
</div>
|
|
34
34
|
<div id="drawerBackdrop" class="drawer-backdrop" hidden></div>
|
|
35
|
-
<dialog id="logsDialog" class="reference-dialog"><div class="dialog-heading"><h2>系统诊断记录</h2><button data-close="logsDialog" aria-label="关闭">×</button></div><div class="actions"><input id="logsSearch" placeholder="
|
|
35
|
+
<dialog id="logsDialog" class="reference-dialog"><div class="dialog-heading"><h2>系统诊断记录</h2><button data-close="logsDialog" aria-label="关闭">×</button></div><div class="actions"><select id="logsScope" aria-label="诊断范围"><option value="all">系统与所有项目</option><option value="project">当前项目</option></select><input id="logsSearch" placeholder="查找失败原因、操作 ID 或提交 SHA" aria-label="筛选诊断记录"><button id="loadLogs">查询</button><button id="exportLogs">导出诊断记录</button></div><div id="logsStatus" class="muted"></div><div id="logsContent"></div></dialog>
|
|
36
36
|
<aside class="drawer" id="systemDrawer" hidden aria-label="系统状态">
|
|
37
37
|
<header><h2>系统状态</h2><button id="closeSystem" class="icon-button" aria-label="关闭系统状态">×</button></header>
|
|
38
38
|
<div class="drawer-content">
|
|
@@ -54,9 +54,10 @@
|
|
|
54
54
|
<p id="embeddingFeedback" class="muted" role="status"></p>
|
|
55
55
|
<div class="dialog-actions"><button type="button" id="disableEmbedding">停用向量模型</button><button type="button" data-close="embeddingDialog">取消</button><button type="submit" class="primary">保存配置</button></div>
|
|
56
56
|
</form></dialog>
|
|
57
|
-
<dialog id="importDialog"><form id="importForm"><div class="dialog-heading"><h2>导入项目</h2><button type="button" data-close="importDialog" aria-label="关闭">×</button></div><p class="muted"
|
|
57
|
+
<dialog id="importDialog"><form id="importForm"><div class="dialog-heading"><h2>导入项目</h2><button type="button" data-close="importDialog" aria-label="关闭">×</button></div><p class="muted">绑定代码与新的空知识目录。只建立跟踪基线,不读取旧知识、不分析全部历史。</p><label>代码目录<div class="input-with-action"><input name="localPath" id="importCode" required placeholder="D:\SanQian.Xu\my-project"><button type="button" id="pickCode">选择</button></div></label><label>项目名称<input name="displayName" id="importName" required maxlength="200"></label><label>知识目录<div class="input-with-action"><input name="knowledgePath" id="importKnowledge" required placeholder="D:\SanQian.Xu\knowledge\my-project"><button type="button" id="pickKnowledge">选择</button></div></label><label>所属领域<select name="domainId" id="importDomain"></select></label><label id="gitInitChoice" class="checkbox" hidden><input type="checkbox" name="confirmGitInit">这个目录不是 Git 仓库,我确认初始化 Git。</label><label id="replaceHookChoice" class="checkbox" hidden><input type="checkbox" name="confirmReplaceHook">目录留有旧知识库的托管 Hook,我确认替换为新项目接线。不会覆盖第三方 Hook。</label><div id="preflightStatus" class="feedback" role="status">填写目录后检查导入条件。</div><button type="button" id="setupImportRoot" hidden>设置知识根目录</button><div class="dialog-actions"><button type="button" data-close="importDialog">取消</button><button type="submit" class="primary" id="confirmImport" disabled>导入项目</button></div></form></dialog>
|
|
58
58
|
<dialog id="domainDialog"><form id="domainForm"><div class="dialog-heading"><h2>创建领域</h2><button type="button" data-close="domainDialog" aria-label="关闭">×</button></div><label>领域名称<input name="name" required maxlength="100" placeholder="例如:开发工具"></label><p class="muted">领域用于共享相关项目的知识,不是新的代码项目。</p><div class="dialog-actions"><button type="button" data-close="domainDialog">取消</button><button class="primary" type="submit">创建领域</button></div></form></dialog>
|
|
59
59
|
<dialog id="memoryDialog"><form id="memoryForm"><div class="dialog-heading"><h2>编辑记忆</h2><button type="button" data-close="memoryDialog" aria-label="关闭">×</button></div><label>内容<textarea id="memoryText" maxlength="2000" required rows="5"></textarea></label><p id="memoryEvidence" class="muted"></p><div class="dialog-actions"><button type="button" data-close="memoryDialog">取消</button><button type="submit" class="primary">保存修改</button></div></form></dialog>
|
|
60
|
+
<dialog id="unbindDialog"><form id="unbindForm"><div class="dialog-heading"><h2>解绑项目</h2><button type="button" data-close="unbindDialog" aria-label="关闭">×</button></div><p id="unbindProjectName"></p><p class="muted">项目将从列表移除,取消尚未开始的维护任务。代码、知识文件、提交证据和会话历史仍保留。正在执行的知识写入结束前不会解绑。</p><p id="unbindFeedback" class="feedback" role="status"></p><div class="dialog-actions"><button type="button" data-close="unbindDialog">取消</button><button class="primary" type="submit" id="confirmUnbind">解绑并保留数据</button></div></form></dialog>
|
|
60
61
|
<dialog id="referenceDialog" class="reference-dialog"><div class="dialog-heading"><h2 id="referenceTitle">知识引用</h2><div class="actions"><button id="copyReference">复制引用</button><button data-close="referenceDialog" aria-label="关闭">×</button></div></div><p id="referenceMeta" class="muted"></p><pre id="referenceContent"></pre></dialog>
|
|
61
62
|
<dialog id="engineDialog" class="engine-dialog"><div class="dialog-heading"><h2>知识引擎诊断</h2><button data-close="engineDialog" aria-label="关闭">×</button></div><iframe id="engineFrame" title="知识引擎诊断" referrerpolicy="same-origin"></iframe></dialog>
|
|
62
63
|
<div id="toast" class="toast" role="status" aria-live="polite" hidden></div>
|
package/ui/product.js
CHANGED
|
@@ -16,16 +16,16 @@
|
|
|
16
16
|
const domainName = id => state.brain.domains.find(d => d.id === id)?.name || '未分组';
|
|
17
17
|
const projectName = id => state.projects.find(p => p.projectId === id)?.name || id;
|
|
18
18
|
const time = value => value ? new Date(value).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : '—';
|
|
19
|
-
const label = run => ({ queued: '排队', running: '维护中', succeeded: '已更新 · 可检索', 'index-pending': '知识已更新 · 索引待修复', failed: '维护失败' })[run.state] || run.state;
|
|
19
|
+
const label = run => ({ queued: '排队', running: '维护中', succeeded: '已更新 · 可检索', 'index-pending': '知识已更新 · 索引待修复', failed: '维护失败', cancelled: '已取消' })[run.state] || run.state;
|
|
20
20
|
const badge = (text, status = '') => `<span class="status ${esc(status)}">${esc(text)}</span>`;
|
|
21
|
-
let toastTimer, refreshTimer, preflightTimer, editingMemoryId, logStream, logEpoch = 0, referenceEpoch = 0, snapshotEpoch = 0, resumeImport = false;
|
|
21
|
+
let toastTimer, refreshTimer, preflightTimer, editingMemoryId, logStream, logEpoch = 0, referenceEpoch = 0, snapshotEpoch = 0, registryEpoch = 0, registryPromise = null, resumeImport = false, unbindProjectId = '', unbinding = false;
|
|
22
22
|
async function api(url, options = {}) {
|
|
23
23
|
const { allowNotReady, ...init } = options;
|
|
24
24
|
if (init.body && typeof init.body !== 'string') init.body = JSON.stringify(init.body);
|
|
25
25
|
if (init.body) init.headers = { 'Content-Type': 'application/json', ...init.headers };
|
|
26
26
|
const response = await fetch(url, init);
|
|
27
27
|
const data = await response.json();
|
|
28
|
-
if (!response.ok || data.ok === false && !allowNotReady) throw Object.assign(new Error(data.error?.message || data.message || (typeof data.error === 'string' ? data.error : `请求失败 (${response.status})`)), { code: data.error?.code || data.error });
|
|
28
|
+
if (!response.ok || data.ok === false && !allowNotReady) throw Object.assign(new Error(data.error?.message || data.message || (typeof data.error === 'string' ? data.error : `请求失败 (${response.status})`)), { code: data.error?.code || data.error, operationId: data.error?.operationId || response.headers.get('X-Operation-Id') || '' });
|
|
29
29
|
return data;
|
|
30
30
|
}
|
|
31
31
|
function toast(text) { $('toast').textContent = text; $('toast').hidden = false; clearTimeout(toastTimer); toastTimer = setTimeout(() => { $('toast').hidden = true; }, 4000); }
|
|
@@ -149,23 +149,29 @@
|
|
|
149
149
|
stream.addEventListener('progress', () => { if (state.stream === stream) scheduleRefresh(); });
|
|
150
150
|
stream.onerror = () => { if (state.stream === stream) $('activityConnection').textContent = '连接中断,正在恢复快照'; };
|
|
151
151
|
}
|
|
152
|
-
|
|
153
|
-
if (!navigator.onLine
|
|
154
|
-
|
|
152
|
+
function refreshRegistry(force = false) {
|
|
153
|
+
if (!navigator.onLine) return Promise.resolve();
|
|
154
|
+
if (registryPromise && !force) return registryPromise;
|
|
155
|
+
const epoch = ++registryEpoch;
|
|
156
|
+
const job = (async () => {
|
|
155
157
|
try {
|
|
156
158
|
const [brain, projects, health] = await Promise.all([api('/api/brain'), api('/api/projects/aggregated'), api('/api/modules/health')]);
|
|
159
|
+
if (epoch !== registryEpoch) return;
|
|
157
160
|
state.brain = brain.brain; state.root = brain.knowledgeRoot; state.projects = projects.projects; state.health = health.modules; state.ownership = health.ownership;
|
|
158
161
|
if (!activeProject()) { state.projectId = state.projects[0]?.projectId || ''; state.epoch++; Object.keys(frames).forEach(frameContext); }
|
|
159
162
|
renderProjectOptions(); renderSystem(); connectProgress(); await refreshSnapshot(); setView(state.view);
|
|
160
163
|
} catch (error) { toast(error.message); }
|
|
161
|
-
finally {
|
|
164
|
+
finally { if (epoch === registryEpoch) { registryPromise = null; connectProgress(); } }
|
|
165
|
+
})();
|
|
166
|
+
registryPromise = job;
|
|
167
|
+
return job;
|
|
162
168
|
}
|
|
163
169
|
function renderSystem() {
|
|
164
170
|
const down = Object.keys(modules).filter(key => !state.health[key]?.up);
|
|
165
171
|
const pending = state.ownership?.owner === 'engine' && !state.ownership.ready;
|
|
166
172
|
$('systemDot').className = `dot ${down.length || pending ? 'issue' : 'up'}`;
|
|
167
|
-
$('systemTitle').textContent = pending ? '
|
|
168
|
-
$('systemDetail').textContent = pending ? state.ownership.message || '
|
|
173
|
+
$('systemTitle').textContent = pending ? '知识引擎连接待恢复' : down.length ? '部分服务需要处理' : '系统连接正常';
|
|
174
|
+
$('systemDetail').textContent = pending ? `${state.ownership.message || '服务恢复后自动重试。'}${state.ownership.error ? ` · ${state.ownership.error.step} (${state.ownership.error.code})` : ''}` : down.length ? down.map(k => modules[k]).join('、') : '本机知识服务已连接';
|
|
169
175
|
$('moduleHealth').innerHTML = Object.entries(modules).map(([key, name]) => `<div class="row"><div><b>${esc(name)}</b><small>${esc(state.health[key]?.url || '')}</small></div>${badge(state.health[key]?.up ? '在线' : '离线', state.health[key]?.up ? 'up' : 'issue')}</div>`).join('');
|
|
170
176
|
if (!navigator.onLine) {
|
|
171
177
|
$('systemDot').className = 'dot issue'; $('systemTitle').textContent = '网页连接中断';
|
|
@@ -182,16 +188,17 @@
|
|
|
182
188
|
const epoch = ++logEpoch;
|
|
183
189
|
logStream?.close(); logStream = null;
|
|
184
190
|
const params = new URLSearchParams({ pageSize: '100', q: $('logsSearch').value.trim() });
|
|
185
|
-
|
|
191
|
+
const projectOnly = $('logsScope').value === 'project' && Boolean(state.projectId);
|
|
192
|
+
if (projectOnly) params.set('projectId', state.projectId);
|
|
186
193
|
const result = await api(`/api/logs?${params}`);
|
|
187
194
|
if (epoch !== logEpoch || !$('logsDialog').open) return;
|
|
188
195
|
$('logsDialog').dataset.streamCursor = result.streamCursor || '';
|
|
189
196
|
$('logsContent').replaceChildren();
|
|
190
197
|
for (const record of [...(result.entries || [])].reverse()) appendLog(record);
|
|
191
|
-
$('logsStatus').textContent = `${(result.entries || []).length} 条诊断记录 ·
|
|
198
|
+
$('logsStatus').textContent = `${(result.entries || []).length} 条诊断记录 · ${projectOnly ? '当前项目' : '系统与所有项目'}`;
|
|
192
199
|
if (!$('logsSearch').value.trim()) {
|
|
193
200
|
const streamParams = new URLSearchParams({ streamCursor: result.streamCursor || '' });
|
|
194
|
-
if (
|
|
201
|
+
if (projectOnly) streamParams.set('projectId', state.projectId);
|
|
195
202
|
logStream = new EventSource(`/api/logs/stream?${streamParams}`);
|
|
196
203
|
logStream.addEventListener('logs/appended', event => { if (epoch === logEpoch && $('logsDialog').open) appendLog(JSON.parse(event.data).record); });
|
|
197
204
|
}
|
|
@@ -267,9 +274,9 @@
|
|
|
267
274
|
$('importForm').reset(); state.importId = `project-${crypto.randomUUID()}`; state.preflight = null; state.preflightEpoch++;
|
|
268
275
|
$('importDomain').innerHTML = '<option value="">未分组</option>' + state.brain.domains.map(d => `<option value="${esc(d.id)}">${esc(d.name)}</option>`).join('');
|
|
269
276
|
$('importDomain').value = activeProject()?.domainId || '';
|
|
270
|
-
$('gitInitChoice').hidden = true; $('confirmImport').disabled = true; $('preflightStatus').textContent = '填写目录后检查导入条件。'; $('importDialog').showModal();
|
|
277
|
+
$('gitInitChoice').hidden = true; $('replaceHookChoice').hidden = true; $('confirmImport').disabled = true; $('preflightStatus').textContent = '填写目录后检查导入条件。'; $('importDialog').showModal();
|
|
271
278
|
}
|
|
272
|
-
function importInput() { return { projectId: state.importId, brainId: state.brain.id, domainId: $('importDomain').value || null, localPath: $('importCode').value.trim(), knowledgePath: $('importKnowledge').value.trim(), displayName: $('importName').value.trim(), knowledgeLanguage: 'zh-CN', confirmGitInit: $('importForm').elements.confirmGitInit.checked }; }
|
|
279
|
+
function importInput() { return { projectId: state.importId, brainId: state.brain.id, domainId: $('importDomain').value || null, localPath: $('importCode').value.trim(), knowledgePath: $('importKnowledge').value.trim(), displayName: $('importName').value.trim(), knowledgeLanguage: 'zh-CN', confirmGitInit: $('importForm').elements.confirmGitInit.checked, replaceManagedHookProjectId: $('importForm').elements.confirmReplaceHook.checked ? state.preflight?.checks.hook?.replacementProjectId : undefined }; }
|
|
273
280
|
const problemLabels = { KNOWLEDGE_ROOT_MISSING: '请在系统状态中设置知识根目录。', AI_PROFILE_REQUIRED: '请配置可用模型;后台默认模型在系统状态中设置。', DUPLICATE_PROJECT: '此项目已导入,请打开已有项目。', HOOK_CONFLICT: '存在第三方 Hook,不会覆盖。请先检查冲突。', PATH_MISSING: '代码目录不存在。', PATH_REQUIRED: '请选择代码目录。' };
|
|
274
281
|
async function checkImport() {
|
|
275
282
|
const epoch = ++state.preflightEpoch; $('confirmImport').disabled = true;
|
|
@@ -279,12 +286,12 @@
|
|
|
279
286
|
try {
|
|
280
287
|
const result = await api('/api/projects/preflight-import', { method: 'POST', body: importInput(), allowNotReady: true });
|
|
281
288
|
if (epoch !== state.preflightEpoch || !$('importDialog').open) return;
|
|
282
|
-
state.preflight = result; $('gitInitChoice').hidden = !result.plannedGitInit;
|
|
289
|
+
state.preflight = result; $('gitInitChoice').hidden = !result.plannedGitInit; $('replaceHookChoice').hidden = !result.checks.hook?.replacementProjectId;
|
|
283
290
|
const messages = result.problems.map(p => problemLabels[p.code] || p.message);
|
|
284
291
|
$('setupImportRoot').hidden = !result.problems.some(p => p.code === 'KNOWLEDGE_ROOT_MISSING');
|
|
285
292
|
$('preflightStatus').textContent = result.ready ? result.plannedGitInit ? '目录检查通过。请确认初始化 Git;不分析历史。' : '检查通过。只建立当前提交的跟踪基线,不分析历史。' : [...new Set(messages)].join('\n');
|
|
286
293
|
$('preflightStatus').classList.toggle('issue', !result.ready);
|
|
287
|
-
$('confirmImport').disabled = !result.ready || result.plannedGitInit && !$('importForm').elements.confirmGitInit.checked || !$('importKnowledge').value.trim();
|
|
294
|
+
$('confirmImport').disabled = !result.ready || result.plannedGitInit && !$('importForm').elements.confirmGitInit.checked || result.checks.hook?.replacementProjectId && !$('importForm').elements.confirmReplaceHook.checked || !$('importKnowledge').value.trim();
|
|
288
295
|
} catch (error) { if (epoch === state.preflightEpoch) $('preflightStatus').textContent = error.message; }
|
|
289
296
|
}
|
|
290
297
|
document.addEventListener('click', e => {
|
|
@@ -292,7 +299,7 @@
|
|
|
292
299
|
const execute = async () => {
|
|
293
300
|
if (button.dataset.view) setView(button.dataset.view);
|
|
294
301
|
if (button.dataset.go) { if (button.dataset.go === 'memory') state.memoryTab = 'candidate'; setView(button.dataset.go); }
|
|
295
|
-
if (button.dataset.close) $(button.dataset.close).close();
|
|
302
|
+
if (button.dataset.close && !(button.dataset.close === 'unbindDialog' && unbinding)) $(button.dataset.close).close();
|
|
296
303
|
if (button.dataset.selectProject) selectProject(button.dataset.selectProject);
|
|
297
304
|
if (button.dataset.scope) { state.scope = button.dataset.scope; all('[data-scope]').forEach(b => b.classList.toggle('active', b.dataset.scope === state.scope)); post('vectorHub', { type: 'kb:scope', scope: state.scope }); }
|
|
298
305
|
if (button.dataset.memoryTab) { state.memoryTab = button.dataset.memoryTab; renderMemories(); }
|
|
@@ -300,7 +307,7 @@
|
|
|
300
307
|
if (button.dataset.action === 'import') openImport();
|
|
301
308
|
if (button.dataset.action === 'repair') await repair();
|
|
302
309
|
if (button.dataset.action === 'system') await openSystem();
|
|
303
|
-
if (button.dataset.action === 'unbind') { const p = activeProject(); if (p &&
|
|
310
|
+
if (button.dataset.action === 'unbind') { const p = activeProject(); if (p && !unbinding) { unbindProjectId = p.projectId; $('unbindProjectName').textContent = `解绑「${p.name}」?`; $('unbindFeedback').textContent = ''; $('unbindDialog').showModal(); } }
|
|
304
311
|
if (button.dataset.retryRun) { await api(`/api/engine/runs/${encodeURIComponent(button.dataset.retryRun)}/retry`, { method: 'POST', body: {} }); await refreshSnapshot(); }
|
|
305
312
|
if (button.dataset.contextIndex !== undefined) await openReference(state.contexts.get(state.projectId).references[Number(button.dataset.contextIndex)]);
|
|
306
313
|
if (button.dataset.memoryAction) {
|
|
@@ -344,6 +351,7 @@
|
|
|
344
351
|
on('engineDialog', 'close', () => post('engine', { type: 'kb:visibility', visible: false }));
|
|
345
352
|
on('openLogs', 'click', async () => { closeSystem(); $('logsDialog').showModal(); await loadLogs(); });
|
|
346
353
|
on('logsDialog', 'close', () => { logEpoch++; logStream?.close(); logStream = null; });
|
|
354
|
+
on('logsScope', 'change', loadLogs);
|
|
347
355
|
on('copyReference', 'click', async () => {
|
|
348
356
|
const text = $('referenceMeta').textContent + '\n\n' + $('referenceContent').textContent;
|
|
349
357
|
try { await navigator.clipboard.writeText(text); }
|
|
@@ -351,19 +359,46 @@
|
|
|
351
359
|
toast('已复制知识与来源。');
|
|
352
360
|
});
|
|
353
361
|
on('loadLogs', 'click', loadLogs);
|
|
354
|
-
on('exportLogs', 'click', async () => { const params = new URLSearchParams({ q: $('logsSearch').value.trim() }); if (state.projectId) params.set('projectId', state.projectId); const response = await fetch(`/api/logs/export?${params}`); if (!response.ok) throw new Error('诊断导出失败。'); const blob = await response.blob(); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = 'knowledge-diagnostics.jsonl'; link.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); });
|
|
362
|
+
on('exportLogs', 'click', async () => { const params = new URLSearchParams({ q: $('logsSearch').value.trim() }); if ($('logsScope').value === 'project' && state.projectId) params.set('projectId', state.projectId); const response = await fetch(`/api/logs/export?${params}`); if (!response.ok) throw new Error('诊断导出失败。'); const blob = await response.blob(); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = 'knowledge-diagnostics.jsonl'; link.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); });
|
|
355
363
|
on('createDomain', 'click', () => { $('domainForm').reset(); $('domainDialog').showModal(); });
|
|
356
364
|
on('domainForm', 'submit', async e => { e.preventDefault(); await api('/api/domains', { method: 'POST', body: Object.fromEntries(new FormData(e.target)) }); $('domainDialog').close(); await refreshRegistry(); });
|
|
357
365
|
on('memoryForm', 'submit', async e => { e.preventDefault(); await api(`/api/engine/memories/${encodeURIComponent(editingMemoryId)}`, { method: 'PATCH', body: { text: $('memoryText').value } }); $('memoryDialog').close(); await refreshSnapshot(); });
|
|
366
|
+
on('unbindForm', 'submit', async e => {
|
|
367
|
+
e.preventDefault(); if (!unbindProjectId || unbinding) return;
|
|
368
|
+
const projectId = unbindProjectId;
|
|
369
|
+
unbinding = true; $('confirmUnbind').disabled = true; $('confirmUnbind').textContent = '正在解绑…';
|
|
370
|
+
$('unbindFeedback').textContent = '正在停止项目接线并移除登记,磁盘文件不会删除。';
|
|
371
|
+
try {
|
|
372
|
+
await api(`/api/projects/${encodeURIComponent(projectId)}`, { method: 'DELETE', body: { deleteKnowledge: false } });
|
|
373
|
+
state.contexts.delete(projectId);
|
|
374
|
+
state.projects = state.projects.filter(project => project.projectId !== projectId);
|
|
375
|
+
if (state.projectId === projectId) { state.projectId = state.projects[0]?.projectId || ''; state.epoch++; }
|
|
376
|
+
renderProjectOptions(); render(); Object.keys(frames).forEach(frameContext);
|
|
377
|
+
$('unbindDialog').close(); unbindProjectId = '';
|
|
378
|
+
// A late read started before DELETE cannot restore the removed project.
|
|
379
|
+
await refreshRegistry(true); localStorage.setItem('kb.project', state.projectId);
|
|
380
|
+
Object.keys(frames).forEach(frameContext);
|
|
381
|
+
toast('项目已从列表移除,代码、知识和会话仍保留。');
|
|
382
|
+
} catch (error) { $('unbindFeedback').textContent = error.message; }
|
|
383
|
+
finally { unbinding = false; $('confirmUnbind').disabled = false; $('confirmUnbind').textContent = '解绑并保留数据'; }
|
|
384
|
+
});
|
|
385
|
+
on('unbindDialog', 'cancel', e => { if (unbinding) e.preventDefault(); });
|
|
358
386
|
on('importForm', 'input', e => {
|
|
359
387
|
if (e.target.id === 'importCode') { const name = e.target.value.trim().split(/[\\/]/).filter(Boolean).pop() || ''; $('importName').value = name; if (state.root) $('importKnowledge').value = `${state.root.replace(/[\\/]+$/, '')}/${name}-${state.importId.slice(-6)}`; }
|
|
360
388
|
clearTimeout(preflightTimer); $('confirmImport').disabled = true; preflightTimer = setTimeout(checkImport, 250);
|
|
361
389
|
});
|
|
362
390
|
on('importForm', 'submit', async e => {
|
|
363
|
-
e.preventDefault(); if (!state.preflight?.ready || state.preflight.plannedGitInit && !$('importForm').elements.confirmGitInit.checked) return;
|
|
391
|
+
e.preventDefault(); if (!state.preflight?.ready || state.preflight.plannedGitInit && !$('importForm').elements.confirmGitInit.checked || state.preflight.checks.hook?.replacementProjectId && !$('importForm').elements.confirmReplaceHook.checked) return;
|
|
364
392
|
$('confirmImport').disabled = true; $('confirmImport').textContent = '正在登记…';
|
|
365
|
-
|
|
366
|
-
|
|
393
|
+
let failed = false, registered = false;
|
|
394
|
+
try { const result = await api('/api/projects/import', { method: 'POST', body: importInput() }); registered = true; state.projectId = result.projectId; state.epoch++; $('importDialog').close(); await refreshRegistry(); Object.keys(frames).forEach(frameContext); setView('projects'); toast(Object.values(result.modules).every(m => m.ok) ? '项目已导入,四个模块已连接。' : '项目已导入,部分接线待恢复。'); }
|
|
395
|
+
catch (error) {
|
|
396
|
+
failed = true;
|
|
397
|
+
if (registered) { toast(`项目已登记,但页面状态刷新失败:${error.message}`); return; }
|
|
398
|
+
$('preflightStatus').textContent = `导入失败:${error.message}\n错误码:${error.code || 'UNKNOWN'}${error.operationId ? `\n操作 ID:${error.operationId}` : ''}\n请在系统诊断记录中查看详细原因。`;
|
|
399
|
+
$('preflightStatus').classList.add('issue'); $('confirmImport').disabled = false;
|
|
400
|
+
}
|
|
401
|
+
finally { $('confirmImport').textContent = '导入项目'; if ($('importDialog').open && !failed) checkImport(); }
|
|
367
402
|
});
|
|
368
403
|
window.addEventListener('hashchange', () => setView(location.hash.slice(1)));
|
|
369
404
|
window.addEventListener('offline', () => {
|
package/vendor-manifest.json
CHANGED
|
@@ -3,19 +3,18 @@
|
|
|
3
3
|
"modules": {
|
|
4
4
|
"knowledge-engine": {
|
|
5
5
|
"name": "@sanqianx/ai-coding-knowledge-engine",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.2.0",
|
|
7
7
|
"root": "_modules/knowledge-engine",
|
|
8
8
|
"files": {
|
|
9
|
-
"README.md": "
|
|
10
|
-
"package.json": "
|
|
9
|
+
"README.md": "c12b9bc9aa49dce6eaad0e6f3fd611466ab1ef6b17d2bdfb69a6dab6a42ab5ed",
|
|
10
|
+
"package.json": "6740fa3dec72ebae49fa903a4b5f8ddc2adf52d11ba9f7c4bc1f36a603636578",
|
|
11
11
|
"src/analysis-contract.js": "90354d1e9b4eff74fdd9d47e097cdb5d47944785c2be66811f64e5958c94d8e7",
|
|
12
12
|
"src/analysis-worker.js": "a105cabe6e1573b2d986b1e7a5aaf615e2fb53524156fc8e4e2c0a71f2c97e55",
|
|
13
13
|
"src/bin.js": "22208907431d0f3a971a669c7e5eb44b3b7af9bbb36fd7ba17c3b90c4288bd70",
|
|
14
14
|
"src/bridge-client.js": "ae0dba21e4286694bcd9e064ca36564ec997969ae1eb1ce4f8a6665e447cbffe",
|
|
15
|
-
"src/coordinator.js": "
|
|
15
|
+
"src/coordinator.js": "e85f1f604d2090bd466244d73cf8e1862e0cbe436e62d4a2f7193afd2fac7843",
|
|
16
16
|
"src/headless-analyzer.js": "2473b28d4bf06088abbcafa21d491560a9ca0ab100024c912a36bc6f981d8576",
|
|
17
17
|
"src/memories.js": "0a8667e6daba99fa5022393951a28e96cdadf98af85b6af9a6be96c7f4e896fc",
|
|
18
|
-
"src/migration.js": "b018871727f3277e4a24350fef1911fbb89e2158578339210d88efc2e309be32",
|
|
19
18
|
"src/retrieval.js": "acbf76bb93fd13dac391c2e918891a56ace869db5583d2a5994641b6782496d6",
|
|
20
19
|
"src/runtime/README.md": "9be676eca76a7857b34d7357194e6f0ee733c22e61d962d53984715dbec7f673",
|
|
21
20
|
"src/runtime/atomic-file.js": "9846b4b69db5ded811acf19a1181ef93615d93efaa542b858e552a7f3124ad23",
|
|
@@ -35,10 +34,10 @@
|
|
|
35
34
|
"src/runtime/layout.js": "c48fa1db62e5df1edefe13218e470c5ceea4ef2781293512c2c18e5c2ba0c7e9",
|
|
36
35
|
"src/runtime/markdown-knowledge-indexer.js": "44a76a9adefad07e78f77b1106b10d6c1cc7873d76362df4c546892b21f3939c",
|
|
37
36
|
"src/runtime/scanner.js": "d99fa8d74a6fbbaae75ca49f62127e14cf9e48b1404e4b4d597ef39df5441410",
|
|
38
|
-
"src/server.js": "
|
|
39
|
-
"src/store.js": "
|
|
37
|
+
"src/server.js": "e7f0b07b7a8cf2623690b9d2d23492f2c95737dc974adfeea049610a4b5ded55",
|
|
38
|
+
"src/store.js": "71f0417c2c1e42b89429e3dee1adff7b2c09e86682cb3f4843ce6d9e5aafd7ec",
|
|
40
39
|
"ui/app.css": "7ad370c269e60a7b5ec907d81a607b2d16bd07b2d7c22a81f102dab51eeb2889",
|
|
41
|
-
"ui/app.js": "
|
|
40
|
+
"ui/app.js": "91ee6d8c31712e6af46de2eecbd72bbc5236c06b7cd055800831e7d57f84a0be",
|
|
42
41
|
"ui/index.html": "ce3565cfefd4838b11d37a91a906ef26ca5a1834844fe3f7c897b56851a3c418"
|
|
43
42
|
}
|
|
44
43
|
}
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const fs = require('node:fs');
|
|
3
|
-
const path = require('node:path');
|
|
4
|
-
const crypto = require('node:crypto');
|
|
5
|
-
const AtomicFile = require('./runtime/atomic-file');
|
|
6
|
-
const { validateClaim } = require('./runtime/commit-reconciler');
|
|
7
|
-
const { validateSha } = require('./runtime/scanner');
|
|
8
|
-
const { initialRuntime } = require('./store');
|
|
9
|
-
|
|
10
|
-
// Only frozen analysis artifacts cross this boundary, never shell settings,
|
|
11
|
-
// registries or credentials. Canonical knowledge files stay where they are.
|
|
12
|
-
async function importLegacyState(coordinator, input) {
|
|
13
|
-
const { store, layout } = coordinator;
|
|
14
|
-
if (!/^cutover-[a-f0-9-]{36}$/.test(input.migrationId || '') || !path.isAbsolute(input.sourceDataDir || '') || !Array.isArray(input.projects)) throw failure('INVALID_ARGUMENT');
|
|
15
|
-
if (store.state.migrations?.includes(input.migrationId)) return { reused: true };
|
|
16
|
-
if (coordinator.executionEnabled || coordinator.active.size) throw failure('ENGINE_NOT_PAUSED');
|
|
17
|
-
const backup = path.join(input.sourceDataDir, 'recovery', 'engine-cutover', input.migrationId);
|
|
18
|
-
if (!fs.existsSync(path.join(backup, 'snapshot.json'))) throw failure('MIGRATION_BACKUP_MISSING');
|
|
19
|
-
const snapshot = AtomicFile.readJsonStrict(path.join(backup, 'snapshot.json'));
|
|
20
|
-
if (snapshot.migrationId !== input.migrationId || snapshot.sourceDataDir !== input.sourceDataDir) throw failure('MIGRATION_BACKUP_MISMATCH');
|
|
21
|
-
input = snapshot;
|
|
22
|
-
const mappings = ['runs', 'claims', 'promotions', 'processed-commits'].map(kind => [path.join(input.sourceDataDir, 'runtime', kind), layout.getRuntimePath(kind)]);
|
|
23
|
-
mappings.push([path.join(input.sourceDataDir, 'recovery', 'promotions'), layout.getRecoveryPath('promotions')]);
|
|
24
|
-
for (const projected of input.projects) mappings.push([path.join(input.sourceDataDir, 'projects', projected.registration.projectId), layout.getProjectMetadataDir(projected.registration.projectId)]);
|
|
25
|
-
function remap(value) {
|
|
26
|
-
if (Array.isArray(value)) return value.map(remap);
|
|
27
|
-
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, remap(item)]));
|
|
28
|
-
if (typeof value === 'string' && path.isAbsolute(value) && !value.includes('\n')) {
|
|
29
|
-
const mapping = mappings.find(([from]) => layout.isPathInside(from, value));
|
|
30
|
-
if (mapping) return path.join(mapping[1], path.relative(mapping[0], value));
|
|
31
|
-
}
|
|
32
|
-
return value;
|
|
33
|
-
}
|
|
34
|
-
function copy(source, target) {
|
|
35
|
-
if (!fs.existsSync(source)) return;
|
|
36
|
-
const stat = fs.lstatSync(source);
|
|
37
|
-
if (stat.isSymbolicLink() || !layout.isPathInside(backup, source, { realpath: true }) || !layout.isPathInside(store.dataDir, target, { realpath: true })) throw failure('MIGRATION_UNSAFE_PATH');
|
|
38
|
-
if (stat.isDirectory()) {
|
|
39
|
-
for (const entry of fs.readdirSync(source)) copy(path.join(source, entry), path.join(target, entry));
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
if (!stat.isFile()) throw failure('MIGRATION_UNSAFE_PATH');
|
|
43
|
-
let bytes = fs.readFileSync(source);
|
|
44
|
-
if (source.endsWith('.json')) bytes = Buffer.from(JSON.stringify(remap(JSON.parse(bytes.toString('utf8'))), null, 2));
|
|
45
|
-
if (fs.existsSync(target)) {
|
|
46
|
-
if (!fs.readFileSync(target).equals(bytes)) throw failure('MIGRATION_ARTIFACT_CONFLICT');
|
|
47
|
-
} else AtomicFile.writeFileAtomic(target, bytes);
|
|
48
|
-
}
|
|
49
|
-
for (const kind of ['runs', 'claims', 'promotions', 'processed-commits']) copy(path.join(backup, 'runtime', kind), layout.getRuntimePath(kind));
|
|
50
|
-
copy(path.join(backup, 'recovery', 'promotions'), layout.getRecoveryPath('promotions'));
|
|
51
|
-
for (const { registration, runtime } of input.projects) {
|
|
52
|
-
const project = store.addProject(registration).project;
|
|
53
|
-
if (project.migrationId === input.migrationId) continue;
|
|
54
|
-
if (store.state.runs.some(run => run.projectId === project.projectId)) throw failure('MIGRATION_PROJECT_ALREADY_ACTIVE');
|
|
55
|
-
const next = initialRuntime(registration);
|
|
56
|
-
if (runtime.lastAnalyzedCommit) next.lastAnalyzedCommit = validateSha(runtime.lastAnalyzedCommit);
|
|
57
|
-
next.conversationBaselineCursor = runtime.conversationBaselineCursor ?? next.conversationBaselineCursor;
|
|
58
|
-
if (runtime.conversationBaselineCursors) {
|
|
59
|
-
if (Object.entries(runtime.conversationBaselineCursors).some(([journal, cursor]) => !/^(global|sha256:[a-f0-9]{64})$/.test(journal) || !Number.isSafeInteger(cursor) || cursor < 0)) throw failure('MIGRATION_INVALID_BASELINE');
|
|
60
|
-
next.conversationBaselineCursors = { ...runtime.conversationBaselineCursors };
|
|
61
|
-
}
|
|
62
|
-
next.analysis.consumedRequirementIds = (runtime.analysis?.consumedRequirementIds || []).filter(id => typeof id === 'string');
|
|
63
|
-
next.index.dirty = runtime.index?.dirty === true;
|
|
64
|
-
const claim = runtime.analysis?.activeClaim;
|
|
65
|
-
if (claim) {
|
|
66
|
-
validateClaim(claim, project.projectId, validateSha(claim.commitSha));
|
|
67
|
-
const claimKeys = ['schema', 'projectId', 'commitSha', 'parents', 'triggerFirstSeen', 'requirementIds', 'requirementBinding', 'conversationSnapshotHash', 'conversationTurnIds', 'conversationEventIds', 'promptTemplateVersion', 'promptHash', 'patchHash', 'evidenceHash', 'evidenceManifestHash', 'retrievalManifestHash', 'knowledgePath', 'runId', 'operationId', 'phase', 'attempt', 'fingerprint'];
|
|
68
|
-
next.analysis.activeClaim = remap(Object.fromEntries(claimKeys.filter(key => claim[key] !== undefined).map(key => [key, claim[key]])));
|
|
69
|
-
next.analysis.status = claim.phase;
|
|
70
|
-
}
|
|
71
|
-
for (const name of ['conversation-events.jsonl', 'commit-boundaries', 'commit-conversations']) copy(path.join(backup, 'projects', project.projectId, name), path.join(layout.getProjectMetadataDir(project.projectId), name));
|
|
72
|
-
project.runtime = next;
|
|
73
|
-
project.migrationId = input.migrationId;
|
|
74
|
-
store.save();
|
|
75
|
-
if (claim) {
|
|
76
|
-
const run = store.addRun({ projectId: project.projectId, commitSha: claim.commitSha, trigger: 'upgrade-recovery' });
|
|
77
|
-
store.updateRun(run.runId, { analysisRunId: claim.runId, phase: 'recovering' });
|
|
78
|
-
} else if (next.index.dirty && next.lastAnalyzedCommit) {
|
|
79
|
-
const run = store.addRun({ projectId: project.projectId, commitSha: next.lastAnalyzedCommit, trigger: 'upgrade-index-recovery' });
|
|
80
|
-
store.updateRun(run.runId, { promotedAt: new Date().toISOString(), phase: 'index-pending', state: 'index-pending', indexState: 'failed' });
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
store.state.migrations = [...(store.state.migrations || []), input.migrationId];
|
|
84
|
-
store.save();
|
|
85
|
-
return { migrated: input.projects.length, fingerprint: crypto.createHash('sha256').update(JSON.stringify(input.projects.map(p => [p.registration.projectId, p.runtime.lastAnalyzedCommit]))).digest('hex') };
|
|
86
|
-
}
|
|
87
|
-
function failure(code) { return Object.assign(new Error('运行迁移未完成,原数据与升级备份已保留。'), { code, status: 409 }); }
|
|
88
|
-
module.exports = { importLegacyState };
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
const assert = require('assert');
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const os = require('os');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const dataDir = require('../lib/data-dir');
|
|
6
|
-
const { LEGACY_ASSETS } = require('../lib/legacy-data-manifest');
|
|
7
|
-
|
|
8
|
-
function temp(name) { return fs.mkdtempSync(path.join(os.tmpdir(), `pk-data-dir-${name}-`)); }
|
|
9
|
-
function clean(dir) { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
10
|
-
function write(file, value = '{}') { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, value); }
|
|
11
|
-
|
|
12
|
-
(() => {
|
|
13
|
-
// Resolution must be diagnostic-safe: resolving never creates the candidate.
|
|
14
|
-
const missing = path.join(temp('resolve'), 'not-created');
|
|
15
|
-
try {
|
|
16
|
-
assert.equal(dataDir.resolveDataDirPath({ dataDir: missing }), path.resolve(missing));
|
|
17
|
-
assert.equal(fs.existsSync(missing), false);
|
|
18
|
-
dataDir.ensureDataDir(missing);
|
|
19
|
-
assert.equal(fs.existsSync(missing), true);
|
|
20
|
-
} finally { clean(path.dirname(missing)); }
|
|
21
|
-
|
|
22
|
-
const legacy = temp('legacy');
|
|
23
|
-
const target = temp('target');
|
|
24
|
-
const saved = process.env.KB_DATA_DIR;
|
|
25
|
-
try {
|
|
26
|
-
write(path.join(legacy, 'projects.json'), JSON.stringify({ old: { repoPath: 'C:/legacy' } }));
|
|
27
|
-
write(path.join(legacy, 'ai-profiles.json'), JSON.stringify({ profiles: [{ id: 'primary', apiKey: 'test-secret' }] }));
|
|
28
|
-
write(path.join(legacy, 'knowledge-store.json'), JSON.stringify({ rootPath: 'C:/knowledge' }));
|
|
29
|
-
write(path.join(legacy, 'embedding-config.json'), JSON.stringify({ modelId: 'model', localModelPath: 'C:/models', localFilesOnly: true }));
|
|
30
|
-
write(path.join(legacy, 'knowledge-scopes.json'), JSON.stringify({ scopes: ['p'] }));
|
|
31
|
-
write(path.join(legacy, 'models', 'nested', 'model.onnx'), 'model-bytes');
|
|
32
|
-
// An empty target registry is non-authoritative and must not suppress legacy data.
|
|
33
|
-
write(path.join(target, 'projects.json'), '{}');
|
|
34
|
-
process.env.KB_DATA_DIR = target;
|
|
35
|
-
dataDir._resetCache();
|
|
36
|
-
const result = dataDir.migrateFromLegacy({ legacyRoot: legacy });
|
|
37
|
-
assert.equal(result.ok, true);
|
|
38
|
-
assert.equal(result.migrated, true);
|
|
39
|
-
assert.equal(JSON.parse(fs.readFileSync(path.join(target, 'projects.json'))).old.repoPath, 'C:/legacy');
|
|
40
|
-
assert.equal(JSON.parse(fs.readFileSync(path.join(target, 'ai-profiles.json'))).profiles.length, 1);
|
|
41
|
-
assert.equal(JSON.parse(fs.readFileSync(path.join(target, 'embedding-config.json'))).modelId, 'model');
|
|
42
|
-
assert.equal(fs.readFileSync(path.join(target, 'models', 'nested', 'model.onnx'), 'utf8'), 'model-bytes');
|
|
43
|
-
assert(LEGACY_ASSETS.some(asset => asset.source === 'embedding-config.json'));
|
|
44
|
-
assert(LEGACY_ASSETS.some(asset => asset.source === 'models'));
|
|
45
|
-
} finally { process.env.KB_DATA_DIR = saved; dataDir._resetCache(); clean(legacy); clean(target); }
|
|
46
|
-
|
|
47
|
-
const source = temp('conflict-source');
|
|
48
|
-
const destination = temp('conflict-destination');
|
|
49
|
-
try {
|
|
50
|
-
write(path.join(source, 'projects.json'), JSON.stringify({ old: { repoPath: 'C:/old' } }));
|
|
51
|
-
write(path.join(destination, 'projects.json'), JSON.stringify({ newer: { repoPath: 'C:/new' } }));
|
|
52
|
-
process.env.KB_DATA_DIR = destination;
|
|
53
|
-
dataDir._resetCache();
|
|
54
|
-
const result = dataDir.migrateFromLegacy({ legacyRoot: source });
|
|
55
|
-
assert.equal(result.ok, false);
|
|
56
|
-
assert.equal(result.requiresManualRecovery, true);
|
|
57
|
-
assert.equal(JSON.parse(fs.readFileSync(path.join(destination, 'projects.json'))).newer.repoPath, 'C:/new');
|
|
58
|
-
} finally { process.env.KB_DATA_DIR = saved; dataDir._resetCache(); clean(source); clean(destination); }
|
|
59
|
-
|
|
60
|
-
// The installed package can contain empty scaffolding directories (for
|
|
61
|
-
// example projects/) while the user data directory contains real projects.
|
|
62
|
-
// Empty package scaffolding is not a legacy source and must not block startup.
|
|
63
|
-
const packageRoot = temp('empty-package-root');
|
|
64
|
-
const userData = temp('existing-user-data');
|
|
65
|
-
try {
|
|
66
|
-
fs.mkdirSync(path.join(packageRoot, 'projects'), { recursive: true });
|
|
67
|
-
write(path.join(userData, 'projects', 'project-real', 'config.json'), JSON.stringify({ id: 'project-real' }));
|
|
68
|
-
process.env.KB_DATA_DIR = userData;
|
|
69
|
-
dataDir._resetCache();
|
|
70
|
-
const result = dataDir.migrateFromLegacy({ legacyRoot: packageRoot });
|
|
71
|
-
assert.equal(result.ok, true, 'empty package scaffolding must not be a migration conflict');
|
|
72
|
-
assert.equal(result.migrated, false);
|
|
73
|
-
assert.equal(result.reason, 'no legacy assets found');
|
|
74
|
-
assert.equal(fs.existsSync(path.join(userData, 'projects', 'project-real', 'config.json')), true);
|
|
75
|
-
} finally { process.env.KB_DATA_DIR = saved; dataDir._resetCache(); clean(packageRoot); clean(userData); }
|
|
76
|
-
console.log('data-dir-migration-test PASS');
|
|
77
|
-
})();
|