@sanqianx/project-knowledge 5.0.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 +13 -0
- package/_site/_test/git-path-compatibility-test.js +80 -0
- package/_site/_test/product-diagnostics-ui-test.js +17 -3
- package/_site/_test/product-import-ui-test.js +12 -0
- package/_site/lib/git-path.js +14 -0
- package/_site/lib/hook-manager.js +4 -4
- package/_site/lib/post-commit-automation.js +1 -1
- package/_site/lib/project-lifecycle-service.js +11 -6
- package/_site/lib/requirement-recorder.js +2 -2
- package/_site/lib/server-app.js +2 -2
- package/_site/scripts/hook-trigger.js +3 -2
- package/docs/fresh-v5-start.md +6 -0
- package/module-runtime-manifest.json +5 -5
- package/package.json +2 -2
- package/plugins/project-knowledge/.claude-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.codex-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.mcp.json +1 -1
- package/ui/index.html +1 -1
- package/ui/product.js +16 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [5.0.1] - 2026-10-10
|
|
4
|
+
|
|
5
|
+
- Fix project import on Git versions before 2.31: query hooks and common Git
|
|
6
|
+
directories without `--path-format`, then resolve relative paths against the
|
|
7
|
+
Git command's working directory. Reject empty, multi-line or echoed-option
|
|
8
|
+
responses before writing hooks or repository identities.
|
|
9
|
+
- Apply the same correction to commit capture and pin event-bridge console
|
|
10
|
+
0.7.1 so importing a project and capturing its next commit use consistent
|
|
11
|
+
workspace metadata. No previous data is migrated.
|
|
12
|
+
- Keep import failure feedback visible with its error code and operation ID.
|
|
13
|
+
Persist the actual failed transaction stage; system diagnostics default to
|
|
14
|
+
all projects so failures of not-yet-registered imports are visible/exportable.
|
|
15
|
+
|
|
3
16
|
## [5.0.0] - 2026-10-10
|
|
4
17
|
|
|
5
18
|
- Fresh-data-only web product. Default state moves to `~/.project-knowledge-v5`;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const assert = require('assert/strict');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
const { resolveGitPath } = require('../lib/git-path');
|
|
7
|
+
const { StorageLayout } = require('../lib/storage-layout');
|
|
8
|
+
const { SettingsStore } = require('../lib/settings-store');
|
|
9
|
+
const { ProjectRegistryStore } = require('../lib/project-registry-store');
|
|
10
|
+
const { ProjectStore } = require('../lib/project-store');
|
|
11
|
+
const { ProjectLifecycleService } = require('../lib/project-lifecycle-service');
|
|
12
|
+
const { BridgeAdapter } = require('../lib/bridge-adapter');
|
|
13
|
+
const { Logger, LogRepository } = require('../lib/structured-logger');
|
|
14
|
+
const hooks = require('../lib/hook-manager');
|
|
15
|
+
|
|
16
|
+
(async () => {
|
|
17
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'kb-git-path-'));
|
|
18
|
+
const repo = path.join(root, '中文 空格项目'); fs.mkdirSync(repo);
|
|
19
|
+
const git = (args, cwd = repo) => {
|
|
20
|
+
const result = spawnSync('git', args, { cwd, encoding: 'utf8', windowsHide: true });
|
|
21
|
+
assert.equal(result.status, 0, result.stderr); return result.stdout.trim();
|
|
22
|
+
};
|
|
23
|
+
for (const invalid of ['', '--path-format=absolute\n.git/hooks', '.git/hooks\nother', '--unsupported', '\0']) {
|
|
24
|
+
assert.throws(() => resolveGitPath(repo, invalid), { code: 'INVALID_ARGUMENT' });
|
|
25
|
+
}
|
|
26
|
+
assert.equal(resolveGitPath(repo, '.git/hooks\r\n'), path.join(repo, '.git', 'hooks'));
|
|
27
|
+
git(['init', '-q']); git(['config', 'user.name', 'Git Compatibility']); git(['config', 'user.email', 'git-compat@example.invalid']);
|
|
28
|
+
fs.writeFileSync(path.join(repo, 'README.md'), '# baseline\n'); git(['add', '.']); git(['commit', '-qm', 'baseline']);
|
|
29
|
+
const layout = new StorageLayout({ dataDir: path.join(root, 'data') });
|
|
30
|
+
const settingsStore = new SettingsStore({ layout });
|
|
31
|
+
const registryStore = new ProjectRegistryStore({ layout });
|
|
32
|
+
const projectStore = new ProjectStore({ layout });
|
|
33
|
+
await settingsStore.initialize({ knowledge: { rootPath: path.join(root, 'knowledge') } });
|
|
34
|
+
await registryStore.initialize();
|
|
35
|
+
const bridgeAdapter = new BridgeAdapter({ dataDir: layout.getDataDir(), bridgeHomeDir: path.join(root, 'bridge') });
|
|
36
|
+
assert.ok(bridgeAdapter.isAvailable());
|
|
37
|
+
const logger = new Logger({ layout });
|
|
38
|
+
const triggerScriptPath = path.resolve(__dirname, '../scripts/hook-trigger.js');
|
|
39
|
+
const options = { layout, settingsStore, registryStore, projectStore, bridgeAdapter, logger, triggerScriptPath };
|
|
40
|
+
const lifecycle = new ProjectLifecycleService(options);
|
|
41
|
+
const input = { localPath: repo, knowledgePath: path.join(root, 'knowledge', 'fresh'), projectId: 'project-git-compat' };
|
|
42
|
+
const preflight = await lifecycle.preflightImport(input);
|
|
43
|
+
assert.equal(preflight.ready, true);
|
|
44
|
+
assert.equal(preflight.checks.git.commonDir, path.join(repo, '.git'));
|
|
45
|
+
const imported = await lifecycle.importProject(input);
|
|
46
|
+
assert.equal(imported.hook.hookPath, path.join(repo, '.git', 'hooks', 'post-commit'));
|
|
47
|
+
assert.equal(imported.config.repoIdentity.commonDir, path.join(repo, '.git'));
|
|
48
|
+
const nested = path.join(repo, 'src'); fs.mkdirSync(nested);
|
|
49
|
+
assert.equal((await bridgeAdapter.resolveRepoIdentity(nested)).repoIdentity.commonDir, imported.config.repoIdentity.commonDir);
|
|
50
|
+
fs.appendFileSync(path.join(repo, 'README.md'), 'commit after import\n'); git(['add', '.']); git(['commit', '-qm', 'captured update']);
|
|
51
|
+
const sha = git(['rev-parse', 'HEAD']);
|
|
52
|
+
const journal = await bridgeAdapter.readEvents({ after: 0, limit: 100 });
|
|
53
|
+
const boundary = journal.events.find(event => event.commitSha === sha && event.schema === 'git-commit-boundary/v1');
|
|
54
|
+
assert.ok(boundary, 'real Git post-commit Hook must append a durable boundary');
|
|
55
|
+
assert.equal(boundary.repoIdentity.commonDir, path.join(repo, '.git'));
|
|
56
|
+
assert.equal(boundary.projectId, input.projectId);
|
|
57
|
+
await lifecycle.deleteProject(input.projectId);
|
|
58
|
+
const brokenRegistry = Object.create(registryStore);
|
|
59
|
+
brokenRegistry.add = async () => { throw new Error('diagnostic registry failure'); };
|
|
60
|
+
await assert.rejects(new ProjectLifecycleService({ ...options, registryStore: brokenRegistry }).importProject(input), /Project import failed/);
|
|
61
|
+
const failed = new LogRepository({ layout }).query({ event: 'project.import.failed' }).entries[0];
|
|
62
|
+
assert.equal(failed.error.message, 'diagnostic registry failure');
|
|
63
|
+
assert.equal(failed.phase, 'hook-verified');
|
|
64
|
+
const transaction = JSON.parse(fs.readFileSync(lifecycle.transactionPath(failed.operationId), 'utf8'));
|
|
65
|
+
assert.equal(transaction.phase, 'failed'); assert.equal(transaction.failurePhase, 'hook-verified');
|
|
66
|
+
assert.equal(hooks.readHookStatus({ repoPath: repo }).installed, false);
|
|
67
|
+
const worktree = path.join(root, '关联 工作树'); git(['worktree', 'add', '-qb', 'linked', worktree]);
|
|
68
|
+
const sharedHook = hooks.installHook({ repoPath: worktree, projectId: 'project-worktree', triggerScriptPath });
|
|
69
|
+
assert.equal(path.dirname(sharedHook.hookPath), path.join(repo, '.git', 'hooks'));
|
|
70
|
+
hooks.uninstallHook({ repoPath: worktree, projectId: 'project-worktree' });
|
|
71
|
+
for (const custom of ['.custom hooks', path.join(root, '绝对 hooks')]) {
|
|
72
|
+
git(['config', 'core.hooksPath', custom], worktree);
|
|
73
|
+
const installed = hooks.installHook({ repoPath: worktree, projectId: 'project-worktree', triggerScriptPath });
|
|
74
|
+
assert.equal(path.dirname(installed.hookPath), path.resolve(worktree, custom));
|
|
75
|
+
assert.equal(hooks.readHookStatus({ repoPath: worktree }).installed, true);
|
|
76
|
+
hooks.uninstallHook({ repoPath: worktree, projectId: 'project-worktree' });
|
|
77
|
+
}
|
|
78
|
+
await logger.close();
|
|
79
|
+
console.log(`git-path-compatibility-test PASS (${git(['--version'])}; import → real Hook → journal; worktrees/custom hooks; diagnostic phase)`);
|
|
80
|
+
})().catch(error => { console.error(error.stack || error.message); process.exitCode = 1; });
|
|
@@ -8,10 +8,17 @@ const { spawnServer } = require('./helpers/spawn-server');
|
|
|
8
8
|
const { findChrome } = require('./helpers/find-chrome');
|
|
9
9
|
const { launchCdpBrowser, waitFor } = require('./helpers/cdp-browser');
|
|
10
10
|
const { createLoggingUiFixture } = require('./helpers/logging-ui-fixture');
|
|
11
|
+
const { Logger } = require('../lib/structured-logger');
|
|
12
|
+
const { StorageLayout } = require('../lib/storage-layout');
|
|
11
13
|
const ROOT = path.resolve(__dirname, '../..');
|
|
12
14
|
|
|
13
15
|
async function main() {
|
|
14
|
-
const fixture = await createLoggingUiFixture({ totalLogs: 240 });
|
|
16
|
+
const fixture = await createLoggingUiFixture({ totalLogs: 240, freshProduct: true });
|
|
17
|
+
const logger = new Logger({ layout: new StorageLayout({ dataDir: fixture.dataDir }) });
|
|
18
|
+
await logger.error('project.import.failed', 'Import failed for an unregistered project', {
|
|
19
|
+
projectId: 'project-import-not-registered', operationId: 'op-import-diagnostic',
|
|
20
|
+
error: Object.assign(new Error('Git Hook path resolution failed'), { code: 'ENOENT' })
|
|
21
|
+
});
|
|
15
22
|
const offline = http.createServer((req, res) => { res.writeHead(503, { 'content-type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: { message: '验收中的离线模块' } })); });
|
|
16
23
|
await new Promise(resolve => offline.listen(0, '127.0.0.1', resolve));
|
|
17
24
|
const offlineUrl = `http://127.0.0.1:${offline.address().port}`;
|
|
@@ -35,6 +42,13 @@ async function main() {
|
|
|
35
42
|
assert.ok(await run("document.getElementById('activityUpdates').textContent.includes('离线')"), 'offline cannot masquerade as empty/success');
|
|
36
43
|
await click('#openSystem'); await click('#openLogs');
|
|
37
44
|
await waitFor(() => run("document.querySelectorAll('#logsContent .log-entry').length === 100"), 'bounded diagnostic rendering');
|
|
45
|
+
assert.equal(await run("document.getElementById('logsScope').value"), 'all');
|
|
46
|
+
await run("document.getElementById('logsSearch').value='op-import-diagnostic'"); await click('#loadLogs');
|
|
47
|
+
await waitFor(() => run("document.getElementById('logsContent').textContent.includes('ENOENT')"), 'unregistered import visible despite active project');
|
|
48
|
+
await run("document.getElementById('logsScope').value='project'; document.getElementById('logsScope').dispatchEvent(new Event('change'))");
|
|
49
|
+
await waitFor(() => run("document.querySelectorAll('#logsContent .log-entry').length === 0"), 'current project excludes failed new import');
|
|
50
|
+
await run("document.getElementById('logsScope').value='all'; document.getElementById('logsScope').dispatchEvent(new Event('change'))");
|
|
51
|
+
await waitFor(() => run("document.getElementById('logsContent').textContent.includes('op-import-diagnostic')"), 'global diagnostic scope restored');
|
|
38
52
|
assert.equal(await run("document.querySelectorAll('#logsDialog img, #logsDialog script').length"), 0);
|
|
39
53
|
await run("document.getElementById('logsSearch').value='<img src=x onerror=window.__xss=1>'"); await click('#loadLogs');
|
|
40
54
|
await waitFor(() => run("document.querySelectorAll('#logsContent .log-entry').length === 1"), 'real server query filter');
|
|
@@ -59,8 +73,8 @@ async function main() {
|
|
|
59
73
|
// The registry does not own knowledge bytes. Use its actual double-directory binding.
|
|
60
74
|
const project = (await (await fetch(base + '/api/projects/aggregated')).json()).projects[0];
|
|
61
75
|
const retained = path.join(project.knowledgePath, 'keep.md'); fs.writeFileSync(retained, '# 保留知识\n');
|
|
62
|
-
await
|
|
63
|
-
await waitFor(() => run("document.getElementById('
|
|
76
|
+
await click('[data-action="unbind"]'); await click('#confirmUnbind');
|
|
77
|
+
await waitFor(() => run("document.getElementById('unbindFeedback').textContent.includes('无法安全停止写入')"), 'offline writer prevents unsafe unbind');
|
|
64
78
|
assert.equal((await (await fetch(base + '/api/projects/aggregated')).json()).projects.length, 1);
|
|
65
79
|
assert.deepEqual(fs.readdirSync(fixture.repo.path), codeFiles); assert.ok(fs.existsSync(retained));
|
|
66
80
|
assert.equal(browser.exceptions.length, 0, JSON.stringify(browser.exceptions));
|
|
@@ -66,6 +66,18 @@ async function main() {
|
|
|
66
66
|
await waitFor(() => run("document.getElementById('importDialog').open && !document.getElementById('confirmImport').disabled"), 'setup returns to ready import');
|
|
67
67
|
const values = await run("Object.fromEntries(['importCode','importKnowledge','importName','importDomain'].map(id=>[id,document.getElementById(id).value]))");
|
|
68
68
|
assert.deepEqual(values, { importCode: repo, importKnowledge: knowledge, importName: '回归项目', importDomain: domainId });
|
|
69
|
+
const racedHook = path.join(repo, '.git', 'hooks', 'post-commit');
|
|
70
|
+
fs.writeFileSync(racedHook, '#!/bin/sh\necho third-party fixture\n');
|
|
71
|
+
await click('#confirmImport');
|
|
72
|
+
await waitFor(() => run("document.getElementById('preflightStatus').textContent.includes('HOOK_CONFLICT') && document.getElementById('preflightStatus').textContent.includes('操作 ID:op-')"), 'durable import failure feedback');
|
|
73
|
+
await new Promise(resolve => setTimeout(resolve, 750));
|
|
74
|
+
assert.ok(await run("document.getElementById('preflightStatus').textContent.includes('HOOK_CONFLICT')"), 'failed import must not turn back into a passed preflight');
|
|
75
|
+
assert.equal((await api('/api/projects/aggregated')).projects.length, 0);
|
|
76
|
+
assert.equal(fs.readFileSync(racedHook, 'utf8'), '#!/bin/sh\necho third-party fixture\n');
|
|
77
|
+
const failureLogs = (await api('/api/logs?event=project.import.failed')).entries;
|
|
78
|
+
assert.equal(failureLogs.length, 1); assert.equal(failureLogs[0].error.code, 'HOOK_CONFLICT');
|
|
79
|
+
assert.ok(await run(`document.getElementById('preflightStatus').textContent.includes(${JSON.stringify(failureLogs[0].operationId)})`), 'UI operation ID resolves to the persisted failure');
|
|
80
|
+
fs.unlinkSync(racedHook);
|
|
69
81
|
await click('#confirmImport');
|
|
70
82
|
await waitFor(() => run("!document.getElementById('importDialog').open && document.getElementById('projectDetail').textContent.includes('回归项目')"), 'import feedback', 30000);
|
|
71
83
|
const selected = await run("document.getElementById('projectSelect').value");
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { DomainError } = require('./contracts');
|
|
3
|
+
|
|
4
|
+
function resolveGitPath(cwd, output) {
|
|
5
|
+
const value = String(output || '').trim();
|
|
6
|
+
// rev-parse may echo unsupported flags with exit code 0. Never let a
|
|
7
|
+
// multi-line response become a filesystem path or a repository identity.
|
|
8
|
+
if (!value || /[\r\n\0]/.test(value) || value.startsWith('--')) {
|
|
9
|
+
throw new DomainError('INVALID_ARGUMENT', 'Git 返回了无效的目录路径,请检查 Git 安装。', { status: 400 });
|
|
10
|
+
}
|
|
11
|
+
return path.resolve(cwd, value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = { resolveGitPath };
|
|
@@ -3,6 +3,7 @@ const path = require('path');
|
|
|
3
3
|
const { spawnSync } = require('child_process');
|
|
4
4
|
const AtomicFile = require('./atomic-file');
|
|
5
5
|
const { DomainError, validateProjectId } = require('./contracts');
|
|
6
|
+
const { resolveGitPath } = require('./git-path');
|
|
6
7
|
|
|
7
8
|
const HOOK_NAME = 'post-commit';
|
|
8
9
|
const HOOK_MARKER = '# PROJECT-KNOWLEDGE-HOOK ';
|
|
@@ -28,13 +29,12 @@ function runGit(repoPath, args) {
|
|
|
28
29
|
function resolveGitHooksDir(_gitDir, repoPath) {
|
|
29
30
|
if (!repoPath) throw new DomainError('INVALID_ARGUMENT', 'repoPath is required.');
|
|
30
31
|
const absolute = path.resolve(repoPath);
|
|
31
|
-
const hooksPath = runGit(absolute, ['rev-parse', '--
|
|
32
|
-
|
|
33
|
-
return path.resolve(absolute, hooksPath);
|
|
32
|
+
const hooksPath = runGit(absolute, ['rev-parse', '--git-path', 'hooks']);
|
|
33
|
+
return resolveGitPath(absolute, hooksPath);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
function repoGitDir(repoPath) {
|
|
37
|
-
try { return runGit(path.resolve(repoPath), ['rev-parse', '--
|
|
37
|
+
try { return resolveGitPath(path.resolve(repoPath), runGit(path.resolve(repoPath), ['rev-parse', '--git-dir'])); }
|
|
38
38
|
catch { return null; }
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -27,7 +27,7 @@ async function handlePostCommitEvent(event = {}, deps = {}) {
|
|
|
27
27
|
if (!resolved.registryStore.readDisplaySnapshot(projectId)) throw new DomainError('PROJECT_NOT_FOUND', 'Hook projectId is not registered.', { status: 404 });
|
|
28
28
|
const config = resolved.projectStore.readConfig(projectId);
|
|
29
29
|
const runtimeRoot = path.resolve(runGit(path.resolve(event.repoRoot), ['rev-parse', '--show-toplevel']).stdout);
|
|
30
|
-
const commonDir = path.resolve(runGit(runtimeRoot, ['rev-parse', '--
|
|
30
|
+
const commonDir = path.resolve(runtimeRoot, runGit(runtimeRoot, ['rev-parse', '--git-common-dir']).stdout);
|
|
31
31
|
if (config.repoIdentity && config.repoIdentity.commonDir
|
|
32
32
|
&& !resolved.layout.pathsEqual(config.repoIdentity.commonDir, commonDir)) {
|
|
33
33
|
if (fs.existsSync(config.repoPath)) {
|
|
@@ -6,6 +6,7 @@ const { DomainError, createId } = require('./contracts');
|
|
|
6
6
|
const AtomicFile = require('./atomic-file');
|
|
7
7
|
const hookManagerDefault = require('./hook-manager');
|
|
8
8
|
const { resolveEffectiveAiProfile } = require('./ai-profile-resolver');
|
|
9
|
+
const { resolveGitPath } = require('./git-path');
|
|
9
10
|
|
|
10
11
|
function slugify(value) {
|
|
11
12
|
const slug = String(value || '').trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
@@ -86,12 +87,12 @@ class ProjectLifecycleService {
|
|
|
86
87
|
checks.git = { ok: false, status: 'non-git', plannedInit: true, message: 'Directory is not a Git repository. Import will run `git init` before proceeding.' };
|
|
87
88
|
} else {
|
|
88
89
|
const head = runGit(localPath, ['rev-parse', '--verify', 'HEAD'], { allowFailure: true });
|
|
89
|
-
const commonDir = runGit(localPath, ['rev-parse', '--
|
|
90
|
+
const commonDir = runGit(localPath, ['rev-parse', '--git-common-dir']);
|
|
90
91
|
const branch = runGit(localPath, ['branch', '--show-current'], { allowFailure: true });
|
|
91
92
|
repoInspection = {
|
|
92
93
|
ok: true,
|
|
93
94
|
repoPath: top.ok ? path.resolve(top.stdout) : path.resolve(localPath),
|
|
94
|
-
commonDir: commonDir.ok ?
|
|
95
|
+
commonDir: commonDir.ok ? resolveGitPath(localPath, commonDir.stdout) : '',
|
|
95
96
|
headCommit: head.ok ? head.stdout : null,
|
|
96
97
|
branch: branch.ok ? branch.stdout : '',
|
|
97
98
|
emptyRepo: !head.ok,
|
|
@@ -270,9 +271,9 @@ class ProjectLifecycleService {
|
|
|
270
271
|
journal.gitInitialized = initialized.ok;
|
|
271
272
|
}
|
|
272
273
|
const top = runGit(repoPath, ['rev-parse', '--show-toplevel']).stdout;
|
|
273
|
-
const commonDir = runGit(repoPath, ['rev-parse', '--
|
|
274
|
+
const commonDir = runGit(repoPath, ['rev-parse', '--git-common-dir']).stdout;
|
|
274
275
|
const head = runGit(repoPath, ['rev-parse', '--verify', 'HEAD'], { allowFailure: true });
|
|
275
|
-
return { repoPath: path.resolve(top), commonDir:
|
|
276
|
+
return { repoPath: path.resolve(top), commonDir: resolveGitPath(repoPath, commonDir), head: head.ok ? head.stdout : null };
|
|
276
277
|
}
|
|
277
278
|
|
|
278
279
|
async importProject(input = {}) {
|
|
@@ -376,14 +377,18 @@ class ProjectLifecycleService {
|
|
|
376
377
|
await log.info?.('project.import.completed', 'Project import completed.', { projectId, phase: 'registry-commit', durationMs: Date.now() - Date.parse(journal.startedAt) });
|
|
377
378
|
return { ok: true, operationId, projectId, config: created.config, state: created.state, hook, gitInitialized: journal.gitInitialized === true };
|
|
378
379
|
} catch (error) {
|
|
380
|
+
journal.failurePhase = journal.phase;
|
|
379
381
|
const rollbackWarnings = await this.rollbackImport(journal);
|
|
380
382
|
journal.phase = 'failed';
|
|
381
383
|
journal.failedAt = new Date().toISOString();
|
|
382
384
|
journal.error = { code: error.code || '', message: String(error.message || error) };
|
|
383
385
|
journal.rollbackWarnings = rollbackWarnings;
|
|
384
386
|
this.writeJournal(journal);
|
|
385
|
-
await log.error?.('project.import.failed', 'Project import failed.', { projectId: journal.projectId || '', phase: journal.
|
|
386
|
-
if (error instanceof DomainError)
|
|
387
|
+
await log.error?.('project.import.failed', 'Project import failed.', { projectId: journal.projectId || '', phase: journal.failurePhase, error, context: { rollbackWarnings } });
|
|
388
|
+
if (error instanceof DomainError) {
|
|
389
|
+
if (!error.operationId) error.operationId = operationId;
|
|
390
|
+
throw error;
|
|
391
|
+
}
|
|
387
392
|
throw new DomainError('INVALID_ARGUMENT', 'Project import failed.', { status: 500, operationId, cause: error, details: { rollbackWarnings } });
|
|
388
393
|
}
|
|
389
394
|
}
|
|
@@ -98,9 +98,9 @@ class GitReader {
|
|
|
98
98
|
const top = topResult.status === 0 && String(topResult.stdout || '').trim()
|
|
99
99
|
? path.resolve(String(topResult.stdout).trim())
|
|
100
100
|
: null;
|
|
101
|
-
const commonResult = this.run(repoPath, ['rev-parse', '--
|
|
101
|
+
const commonResult = this.run(repoPath, ['rev-parse', '--git-common-dir']);
|
|
102
102
|
const commonDir = commonResult.status === 0 && String(commonResult.stdout || '').trim()
|
|
103
|
-
? path.resolve(String(commonResult.stdout).trim())
|
|
103
|
+
? path.resolve(repoPath, String(commonResult.stdout).trim())
|
|
104
104
|
: null;
|
|
105
105
|
let repoIdentity = null;
|
|
106
106
|
if (top && bridgeIdentityModule) {
|
package/_site/lib/server-app.js
CHANGED
|
@@ -234,12 +234,12 @@ async function inspectGitRepository(repoPath) {
|
|
|
234
234
|
execGit(root, ['rev-parse', '--verify', 'HEAD']),
|
|
235
235
|
execGit(root, ['branch', '--show-current']),
|
|
236
236
|
execGit(root, ['status', '--porcelain=v1', '--untracked-files=normal']),
|
|
237
|
-
execGit(root, ['rev-parse', '--
|
|
237
|
+
execGit(root, ['rev-parse', '--git-common-dir']),
|
|
238
238
|
]);
|
|
239
239
|
return {
|
|
240
240
|
repoStatus: head.ok ? 'ok' : 'empty',
|
|
241
241
|
repoPath: top.ok ? path.resolve(top.stdout.trim()) : root,
|
|
242
|
-
commonDir: commonDir.ok ? path.resolve(commonDir.stdout.trim()) : '',
|
|
242
|
+
commonDir: commonDir.ok ? path.resolve(root, commonDir.stdout.trim()) : '',
|
|
243
243
|
headCommit: head.ok ? head.stdout.trim() : null,
|
|
244
244
|
branch: branch.ok ? branch.stdout.trim() : '',
|
|
245
245
|
dirty: status.ok && Boolean(status.stdout.trim()),
|
|
@@ -8,6 +8,7 @@ const { readLiveEndpoint } = require('../lib/runtime-endpoint');
|
|
|
8
8
|
const { StorageLayout } = require('../lib/storage-layout');
|
|
9
9
|
const { Logger } = require('../lib/structured-logger');
|
|
10
10
|
const { BridgeAdapter } = require('../lib/bridge-adapter');
|
|
11
|
+
const { resolveGitPath } = require('../lib/git-path');
|
|
11
12
|
|
|
12
13
|
let bridgeIdentityModule = null;
|
|
13
14
|
try {
|
|
@@ -96,11 +97,11 @@ function post(body) {
|
|
|
96
97
|
if (toplevel) {
|
|
97
98
|
let workspaceRoot = path.resolve(toplevel);
|
|
98
99
|
try { workspaceRoot = fs.realpathSync.native(workspaceRoot) || workspaceRoot; } catch { workspaceRoot = path.resolve(toplevel); }
|
|
99
|
-
const commonDir = git(['rev-parse', '--
|
|
100
|
+
const commonDir = git(['rev-parse', '--git-common-dir']);
|
|
100
101
|
const remoteRaw = git(['config', '--get', 'remote.origin.url']);
|
|
101
102
|
repoIdentity = bridgeIdentityModule.buildRepoIdentityV1({
|
|
102
103
|
workspaceRoot,
|
|
103
|
-
commonDir: commonDir ?
|
|
104
|
+
commonDir: commonDir ? resolveGitPath(repoRoot, commonDir) : null,
|
|
104
105
|
remote: remoteRaw ? bridgeIdentityModule.normalizeGitUrl(remoteRaw) : null,
|
|
105
106
|
});
|
|
106
107
|
}
|
package/docs/fresh-v5-start.md
CHANGED
|
@@ -17,6 +17,12 @@ project-knowledge
|
|
|
17
17
|
|
|
18
18
|
如果模块是自己独立启动的旧进程,请在它原来的终端中停止后重启新版外壳。外壳不会擅自杀死其他服务;版本或运行期鉴权不一致时显示具体连接阶段,不接管旧服务数据。
|
|
19
19
|
|
|
20
|
+
5.0.1 修复旧版 Git 的项目导入失败(日志中的路径包含
|
|
21
|
+
`--path-format=absolute` 和换行)。升级 5.0.0 → 5.0.1 不需要清空新版数据或
|
|
22
|
+
重新配置模型。失败项目可以直接重试导入;原代码、第三方 Hook 与旧知识文件不删除。
|
|
23
|
+
导入失败会在弹窗保留错误码和操作 ID,系统诊断默认查询“系统与所有项目”,
|
|
24
|
+
可按操作 ID 查找导入事务的真实失败阶段和原始错误。
|
|
25
|
+
|
|
20
26
|
## 解绑与恢复
|
|
21
27
|
|
|
22
28
|
“解绑并保留数据”从项目列表移除登记、取消未开始任务并卸下本项目托管 Hook,保留代码、知识、会话及证据。实际分析或索引写入尚未结束时仍禁止解绑,防止丢失写入状态。队列本身不再被当成正在运行。
|
|
@@ -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": "5.0.
|
|
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": {
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"@huggingface/transformers": "3.8.1",
|
|
81
81
|
"@lancedb/lancedb": "0.31.0",
|
|
82
82
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
83
|
-
"@sanqianx/ai-coding-event-bridge-console": "0.7.
|
|
83
|
+
"@sanqianx/ai-coding-event-bridge-console": "0.7.1",
|
|
84
84
|
"@sanqianx/vector-hub": "1.0.7",
|
|
85
85
|
"buffer": "^6.0.3",
|
|
86
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": "5.0.
|
|
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">
|
package/ui/product.js
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
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); }
|
|
@@ -188,16 +188,17 @@
|
|
|
188
188
|
const epoch = ++logEpoch;
|
|
189
189
|
logStream?.close(); logStream = null;
|
|
190
190
|
const params = new URLSearchParams({ pageSize: '100', q: $('logsSearch').value.trim() });
|
|
191
|
-
|
|
191
|
+
const projectOnly = $('logsScope').value === 'project' && Boolean(state.projectId);
|
|
192
|
+
if (projectOnly) params.set('projectId', state.projectId);
|
|
192
193
|
const result = await api(`/api/logs?${params}`);
|
|
193
194
|
if (epoch !== logEpoch || !$('logsDialog').open) return;
|
|
194
195
|
$('logsDialog').dataset.streamCursor = result.streamCursor || '';
|
|
195
196
|
$('logsContent').replaceChildren();
|
|
196
197
|
for (const record of [...(result.entries || [])].reverse()) appendLog(record);
|
|
197
|
-
$('logsStatus').textContent = `${(result.entries || []).length} 条诊断记录 ·
|
|
198
|
+
$('logsStatus').textContent = `${(result.entries || []).length} 条诊断记录 · ${projectOnly ? '当前项目' : '系统与所有项目'}`;
|
|
198
199
|
if (!$('logsSearch').value.trim()) {
|
|
199
200
|
const streamParams = new URLSearchParams({ streamCursor: result.streamCursor || '' });
|
|
200
|
-
if (
|
|
201
|
+
if (projectOnly) streamParams.set('projectId', state.projectId);
|
|
201
202
|
logStream = new EventSource(`/api/logs/stream?${streamParams}`);
|
|
202
203
|
logStream.addEventListener('logs/appended', event => { if (epoch === logEpoch && $('logsDialog').open) appendLog(JSON.parse(event.data).record); });
|
|
203
204
|
}
|
|
@@ -350,6 +351,7 @@
|
|
|
350
351
|
on('engineDialog', 'close', () => post('engine', { type: 'kb:visibility', visible: false }));
|
|
351
352
|
on('openLogs', 'click', async () => { closeSystem(); $('logsDialog').showModal(); await loadLogs(); });
|
|
352
353
|
on('logsDialog', 'close', () => { logEpoch++; logStream?.close(); logStream = null; });
|
|
354
|
+
on('logsScope', 'change', loadLogs);
|
|
353
355
|
on('copyReference', 'click', async () => {
|
|
354
356
|
const text = $('referenceMeta').textContent + '\n\n' + $('referenceContent').textContent;
|
|
355
357
|
try { await navigator.clipboard.writeText(text); }
|
|
@@ -357,7 +359,7 @@
|
|
|
357
359
|
toast('已复制知识与来源。');
|
|
358
360
|
});
|
|
359
361
|
on('loadLogs', 'click', loadLogs);
|
|
360
|
-
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); });
|
|
361
363
|
on('createDomain', 'click', () => { $('domainForm').reset(); $('domainDialog').showModal(); });
|
|
362
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(); });
|
|
363
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(); });
|
|
@@ -388,8 +390,15 @@
|
|
|
388
390
|
on('importForm', 'submit', async e => {
|
|
389
391
|
e.preventDefault(); if (!state.preflight?.ready || state.preflight.plannedGitInit && !$('importForm').elements.confirmGitInit.checked || state.preflight.checks.hook?.replacementProjectId && !$('importForm').elements.confirmReplaceHook.checked) return;
|
|
390
392
|
$('confirmImport').disabled = true; $('confirmImport').textContent = '正在登记…';
|
|
391
|
-
|
|
392
|
-
|
|
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(); }
|
|
393
402
|
});
|
|
394
403
|
window.addEventListener('hashchange', () => setView(location.hash.slice(1)));
|
|
395
404
|
window.addEventListener('offline', () => {
|