@sanqianx/project-knowledge 5.0.0 → 5.0.2

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.
@@ -9,7 +9,7 @@
9
9
  "plugins": [
10
10
  {
11
11
  "name": "project-knowledge",
12
- "version": "5.0.0",
12
+ "version": "5.0.2",
13
13
  "source": "./plugins/project-knowledge",
14
14
  "description": "Search durable project decisions and implementation history through MCP."
15
15
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## [5.0.2] - 2026-10-10
4
+
5
+ - Add ZCode capture to the unified CLI and web System Status connector setup,
6
+ including repair, status and safe removal. Keep unverified ZCode knowledge
7
+ plugin support explicitly separate from working capture hooks.
8
+ - Correct command/config discovery labels and stop treating consumer
9
+ registration or disabled hooks as proof of capture. Report partial failures
10
+ with their operation ID and persist connector errors from both web and CLI.
11
+ - Pin event-bridge 0.7.2: external hooks resolve their own Bridge home without
12
+ inherited environment variables; ZCode events preserve their source. Repair
13
+ older entries in place and retain third-party hooks within mixed groups.
14
+
15
+ ## [5.0.1] - 2026-10-10
16
+
17
+ - Fix project import on Git versions before 2.31: query hooks and common Git
18
+ directories without `--path-format`, then resolve relative paths against the
19
+ Git command's working directory. Reject empty, multi-line or echoed-option
20
+ responses before writing hooks or repository identities.
21
+ - Apply the same correction to commit capture and pin event-bridge console
22
+ 0.7.1 so importing a project and capturing its next commit use consistent
23
+ workspace metadata. No previous data is migrated.
24
+ - Keep import failure feedback visible with its error code and operation ID.
25
+ Persist the actual failed transaction stage; system diagnostics default to
26
+ all projects so failures of not-yet-registered imports are visible/exportable.
27
+
3
28
  ## [5.0.0] - 2026-10-10
4
29
 
5
30
  - Fresh-data-only web product. Default state moves to `~/.project-knowledge-v5`;
package/README.md CHANGED
@@ -30,6 +30,11 @@ project-knowledge
30
30
  agent conversations in real Git workspaces are captured into each
31
31
  imported project's Development Conversation.
32
32
  It also registers the host-level `project-knowledge` Bridge consumer.
33
+ ZCode is supported for Development Capture only; an unverified knowledge
34
+ plugin is never reported as installed. Use `install --ide zcode --capture-only`
35
+ or System Status → 外部 Agent 采集 in the web UI. Per-client repair/removal
36
+ preserves unrelated hooks and existing records. Failures include an operation
37
+ ID and persist to the local system diagnostic log.
33
38
  No client UI needs to be opened; third-party hooks and configs are
34
39
  preserved, and a Codex notify conflict is reported instead of overwritten.
35
40
  3. `project-knowledge` starts a fresh v5 local backend. Import projects again;
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const http = require('node:http');
7
+ const { createRequire } = require('node:module');
8
+ const coreRoot = path.resolve(process.env.KB_CAPTURE_TEST_CORE_ROOT || path.join(__dirname, '../..'));
9
+ const fromCore = createRequire(path.join(coreRoot, 'package.json'));
10
+ const { createRuntime, initializeRuntime, createRequestHandler } = fromCore('./_site/lib/server-app');
11
+ const { BridgeAdapter } = fromCore('./_site/lib/bridge-adapter');
12
+ const { ModuleBridge } = fromCore('./_site/lib/module-bridge');
13
+ const { findChrome } = require('./helpers/find-chrome');
14
+ const { launchCdpBrowser, waitFor } = require('./helpers/cdp-browser');
15
+ const { Journal } = fromCore('@sanqianx/ai-coding-event-bridge-console/src/bridge');
16
+ const { spawnSync } = require('node:child_process');
17
+
18
+ async function main() {
19
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'kb-agent-capture-ui-'));
20
+ const dataDir = path.join(temp, 'data');
21
+ const homeDir = path.join(temp, '用户 空格');
22
+ const eventsHome = path.join(dataDir, 'events');
23
+ const configFile = path.join(homeDir, '.zcode', 'cli', 'config.json');
24
+ fs.mkdirSync(path.dirname(configFile), { recursive: true });
25
+ fs.writeFileSync(configFile, JSON.stringify({ provider: { name: 'retain' } }));
26
+ const offline = http.createServer((req, res) => { res.writeHead(503, { 'content-type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: { message: '本轮测试中非采集模块离线' } })); });
27
+ await new Promise(resolve => offline.listen(0, '127.0.0.1', resolve));
28
+ const offlineUrl = `http://127.0.0.1:${offline.address().port}`;
29
+ const runtime = createRuntime({ dataDir, analysisOwner: 'engine', bridgeAdapter: new BridgeAdapter({ dataDir, bridgeHomeDir: eventsHome }), moduleBridge: new ModuleBridge({ terminalUrl: offlineUrl, engineUrl: offlineUrl, eventBridgeUrl: offlineUrl, vectorHubUrl: offlineUrl }) });
30
+ await initializeRuntime(runtime);
31
+ const server = http.createServer(createRequestHandler(runtime, { integrationOptions: { homeDir, env: {}, commandResolver: () => '' } }));
32
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
33
+ const base = `http://127.0.0.1:${server.address().port}`;
34
+ let browser;
35
+ try {
36
+ browser = await launchCdpBrowser({ chrome: findChrome(), profileDir: path.join(temp, 'browser'), url: base, width: 1920, height: 1080 });
37
+ await browser.setViewport(1920, 1080);
38
+ const run = expression => browser.evaluate(expression);
39
+ const click = selector => run(`document.querySelector(${JSON.stringify(selector)}).click()`);
40
+ await click('#openSystem');
41
+ await waitFor(() => run("document.querySelectorAll('#agentCaptureList .agent-capture-row').length === 4"), 'four capture statuses');
42
+ assert.equal(await run("document.querySelector('[data-agent-client=claude]').disabled"), true);
43
+ assert.equal(await run("document.querySelector('[data-agent-client=zcode]').disabled"), false);
44
+ assert(await run("document.getElementById('agentCaptureList').textContent.includes('检测到配置')"));
45
+ await click('[data-agent-client=zcode][data-agent-capture=install]');
46
+ await waitFor(() => run("document.querySelector('[data-agent-client=zcode][data-agent-capture=uninstall]') !== null"), 'installed status reloaded from backend');
47
+ const config = JSON.parse(fs.readFileSync(configFile));
48
+ assert.equal(config.hooks.enabled, true);
49
+ const hook = config.hooks.events.UserPromptSubmit[0].hooks[0];
50
+ const env = { ...process.env }; delete env.AI_CODING_EVENT_BRIDGE_HOME; delete env.AI_CODING_EVENT_BRIDGE_CAPTURE;
51
+ const child = spawnSync(process.execPath, hook.args, { env, input: JSON.stringify({ hook_event_name: 'UserPromptSubmit', session_id: 'ui-zcode-session', cwd: temp, prompt: '从网页接入后写入真实 journal' }), encoding: 'utf8', timeout: 15000 });
52
+ assert.equal(child.status, 0, child.stderr);
53
+ const events = await new Journal(path.join(eventsHome, 'journal')).readEvents({});
54
+ assert.equal(events.length, 1); assert.equal(events[0].source, 'zcode');
55
+ await click('[data-agent-client=zcode][data-agent-capture=install]');
56
+ await waitFor(() => run("!document.querySelector('[data-agent-client=zcode]').disabled"), 'repair response');
57
+ assert.equal(JSON.parse(fs.readFileSync(configFile)).hooks.events.UserPromptSubmit.length, 1);
58
+ assert.equal(await run('document.documentElement.scrollWidth > innerWidth'), false);
59
+ const artifacts = path.resolve(process.env.KB_CAPTURE_TEST_ARTIFACTS || path.join(__dirname, '../..', '.agent-state/acceptance/agent-capture'));
60
+ await browser.screenshot(path.join(artifacts, '1920-light.png'));
61
+ await click('#closeSystem'); await click('#themeToggle'); await click('#openSystem');
62
+ await waitFor(() => run("document.querySelector('[data-agent-client=zcode][data-agent-capture=uninstall]') !== null"), 'theme retains capture status');
63
+ await browser.screenshot(path.join(artifacts, '1920-dark.png'));
64
+ fs.writeFileSync(configFile, '{invalid JSON');
65
+ await click('[data-agent-client=zcode][data-agent-capture=install]');
66
+ await waitFor(() => run("document.getElementById('agentCaptureFeedback').textContent.includes('操作 ID')"), 'partial failure rendered');
67
+ assert(await run("document.getElementById('agentCaptureFeedback').classList.contains('issue')"));
68
+ const logs = (await (await fetch(base + '/api/logs?event=integration.capture.failed')).json()).entries;
69
+ assert(logs.length > 0 && logs[0].error.message.includes('valid JSON'));
70
+ fs.writeFileSync(configFile, JSON.stringify(config));
71
+ await click('#refreshAgentCapture');
72
+ await waitFor(() => run("document.querySelector('[data-agent-client=zcode][data-agent-capture=uninstall]') !== null"), 'manual status retry');
73
+ await click('[data-agent-client=zcode][data-agent-capture=uninstall]');
74
+ await waitFor(() => run("!document.querySelector('[data-agent-client=zcode]').disabled && document.querySelector('[data-agent-client=zcode][data-agent-capture=uninstall]') === null"), 'removed capture status');
75
+ assert.equal(JSON.parse(fs.readFileSync(configFile)).provider.name, 'retain');
76
+ assert.equal((await new Journal(path.join(eventsHome, 'journal')).readEvents({})).length, 1, 'removal retains prior evidence');
77
+ assert.equal(browser.exceptions.length, 0, JSON.stringify(browser.exceptions));
78
+ console.log(`agent-capture-ui-test PASS (1920x1080; real install/repair/Hook/journal/error logs/removal/light+dark; core=${coreRoot})`);
79
+ } finally {
80
+ await browser?.close(); server.closeAllConnections(); await new Promise(resolve => server.close(resolve));
81
+ offline.closeAllConnections(); await new Promise(resolve => offline.close(resolve)); await runtime.logger.close();
82
+ fs.rmSync(temp, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
83
+ }
84
+ }
85
+ main().catch(error => { console.error(error.stack); process.exitCode = 1; });
@@ -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 run('window.confirm = () => true'); await click('[data-action="unbind"]');
63
- await waitFor(() => run("document.getElementById('toast').textContent.includes('无法安全停止写入')"), 'offline writer prevents unsafe unbind');
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,107 @@
1
+ const assert = require('assert');
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+ const path = require('path');
5
+ const http = require('http');
6
+ const { spawnSync } = require('child_process');
7
+ const { IntegrationManager, normalizeClient, SUPPORTED_CLIENTS } = require('../lib/integration-installer');
8
+ const { createRuntime, createRequestHandler } = require('../lib/server-app');
9
+ const { BridgeAdapter } = require('../lib/bridge-adapter');
10
+ const { Logger, LogRepository } = require('../lib/structured-logger');
11
+ const { StorageLayout } = require('../lib/storage-layout');
12
+ const { runCli } = require('../../bin/project-knowledge-integrations');
13
+
14
+ (async () => {
15
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-zcode-capture-'));
16
+ const homeDir = path.join(temp, '用户 空格');
17
+ const bridgeHomeDir = path.join(temp, 'events');
18
+ const configFile = path.join(homeDir, '.zcode', 'cli', 'config.json');
19
+ const layout = new StorageLayout({ dataDir: path.join(temp, 'data') });
20
+ const logger = new Logger({ layout });
21
+ const options = { homeDir, env: {}, bridgeHomeDir, zcodeConfigFile: configFile, commandResolver: () => '', logger };
22
+ const manager = new IntegrationManager(options);
23
+ let server;
24
+ try {
25
+ assert(SUPPORTED_CLIENTS.includes('zcode'));
26
+ assert.equal(normalizeClient(' ZCode '), 'zcode');
27
+ assert.deepEqual(manager.detectClients(), []);
28
+ const before = await manager.statusAll();
29
+ assert.equal(before.clients.length, 4, 'status includes tools not found without installing them');
30
+ assert.equal(before.clients.find(a => a.client === 'zcode').developmentCapture.installed, false);
31
+ assert(!fs.existsSync(configFile), 'status and uninstall must not manufacture user config');
32
+ await manager.operateCapture('uninstall', ['zcode']);
33
+ assert(!fs.existsSync(configFile));
34
+
35
+ fs.mkdirSync(path.dirname(configFile), { recursive: true });
36
+ const thirdParty = { type: 'process', command: 'other', args: ['third-party'] };
37
+ fs.writeFileSync(configFile, JSON.stringify({ provider: { name: '保留' }, hooks: { enabled: false, events: { Stop: [{ hooks: [thirdParty] }] } } }));
38
+ assert.deepEqual(manager.detectClients(), ['zcode'], 'desktop ZCode config is discoverable without a PATH CLI');
39
+ const installed = await manager.installAll({ clients: ['zcode'], captureOnly: true });
40
+ assert(installed.bridge.ok && installed.clients[0].developmentCapture.ok);
41
+ assert.equal(installed.clients[0].knowledgeIntegration, undefined);
42
+ const config = JSON.parse(fs.readFileSync(configFile));
43
+ assert.equal(config.hooks.enabled, true);
44
+ assert.equal(config.provider.name, '保留');
45
+ assert(config.hooks.events.Stop[0].hooks.some(h => h.command === 'other'));
46
+ await manager.installAll({ clients: ['zcode'], captureOnly: true });
47
+ const twice = JSON.parse(fs.readFileSync(configFile));
48
+ assert.equal(twice.hooks.events.UserPromptSubmit.length, 1);
49
+ assert.equal(twice.hooks.events.Stop.length, 2);
50
+ const status = await manager.statusAll({ clients: ['zcode'] });
51
+ assert(status.clients[0].configFound && status.clients[0].developmentCapture.installed);
52
+ assert.equal(status.clients[0].knowledgeIntegration.installed, false);
53
+ assert.equal(status.clients[0].knowledgeIntegration.supported, false, 'capture never claims an unverified retrieval plugin');
54
+ assert.equal(await runCli(['update', '--ide', 'zcode', '--capture-only', '--json'], { manager }), 0);
55
+ const dry = new IntegrationManager({ ...options, dryRun: true });
56
+ const untouched = fs.readFileSync(configFile, 'utf8');
57
+ await runCli(['uninstall', '--ide', 'zcode', '--capture-only', '--dry-run'], { manager: dry });
58
+ assert.equal(fs.readFileSync(configFile, 'utf8'), untouched);
59
+
60
+ const runtime = createRuntime({ dataDir: layout.getDataDir(), logger, analysisOwner: 'engine', bridgeAdapter: new BridgeAdapter({ dataDir: layout.getDataDir(), bridgeHomeDir }) });
61
+ server = http.createServer(createRequestHandler(runtime, { integrationOptions: options }));
62
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
63
+ const base = `http://127.0.0.1:${server.address().port}`;
64
+ const request = async (route, body) => {
65
+ const response = await fetch(base + route, body ? { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) } : {});
66
+ assert.equal(response.status, 200);
67
+ return response.json();
68
+ };
69
+ const apiInstall = await request('/api/integrations/install', { clients: ['zcode'], captureOnly: true });
70
+ assert.equal(apiInstall.complete, true);
71
+ assert(apiInstall.operationId);
72
+ const apiStatus = await request('/api/integrations/status');
73
+ assert(apiStatus.clients.find(a => a.client === 'zcode').developmentCapture.installed);
74
+ fs.writeFileSync(configFile, '{invalid JSON');
75
+ const failed = await request('/api/integrations/install', { clients: ['zcode'], captureOnly: true });
76
+ assert.equal(failed.complete, false, 'HTTP 200 does not imply capture succeeded');
77
+ assert.equal(failed.clients[0].developmentCapture.ok, false);
78
+ assert(failed.clients[0].developmentCapture.error.code);
79
+ await logger.flush();
80
+ const errors = new LogRepository({ layout }).query({ event: 'integration.capture.failed' }).entries;
81
+ assert(errors.some(entry => entry.operationId === failed.operationId && entry.context.client === 'zcode' && entry.error.message.includes('valid JSON')), 'actual failed phase and operation ID are persisted');
82
+ fs.writeFileSync(configFile, JSON.stringify(twice));
83
+ const removed = await request('/api/integrations/uninstall', { clients: ['zcode'], captureOnly: true });
84
+ assert.equal(removed.complete, true);
85
+ assert.deepEqual(removed.clients, [], 'capture-only removal does not invoke knowledge plugin operations');
86
+ assert.equal(removed.capture[0].state, 'removed');
87
+ const after = JSON.parse(fs.readFileSync(configFile));
88
+ assert.equal(after.provider.name, '保留');
89
+ assert.deepEqual(after.hooks.events.Stop[0].hooks, [thirdParty]);
90
+ assert.equal((await manager.statusAll({ clients: ['zcode'] })).clients[0].developmentCapture.installed, false);
91
+ const facade = manager._bridgeFacade();
92
+ assert(await facade.getConsumer('project-knowledge'), 'removing one connector retains the host consumer');
93
+
94
+ const cliEnv = { ...process.env, USERPROFILE: homeDir, HOME: homeDir, KB_DATA_DIR: layout.getDataDir(), AI_CODING_EVENT_BRIDGE_HOME: bridgeHomeDir };
95
+ const cli = spawnSync(process.execPath, [path.resolve(__dirname, '../../bin/project-knowledge-integrations.js'), 'install', '--ide', 'zcode', '--capture-only', '--json'], { env: cliEnv, encoding: 'utf8', timeout: 15000 });
96
+ assert.equal(cli.status, 0, cli.stderr);
97
+ assert(JSON.parse(cli.stdout).clients[0].developmentCapture.installed);
98
+ await logger.flush();
99
+ assert(new LogRepository({ layout }).query({ event: 'integration.capture.completed' }).entries.length > 0, 'CLI operations also write durable logs');
100
+ console.log('zcode-capture-integration-test PASS');
101
+ } finally {
102
+ if (server) { server.closeAllConnections(); await new Promise(resolve => server.close(resolve)); }
103
+ await logger.close();
104
+ // This test owns a single mkdtemp sandbox, never real Agent configs.
105
+ fs.rmSync(temp, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
106
+ }
107
+ })().catch(error => { console.error(error.stack); process.exitCode = 1; });
@@ -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', '--path-format=absolute', '--git-path', 'hooks']);
32
- if (!hooksPath) throw new DomainError('HOOK_INVALID', 'Git returned an empty hooks path.', { status: 400 });
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', '--path-format=absolute', '--git-dir']); }
37
+ try { return resolveGitPath(path.resolve(repoPath), runGit(path.resolve(repoPath), ['rev-parse', '--git-dir'])); }
38
38
  catch { return null; }
39
39
  }
40
40
 
@@ -20,17 +20,19 @@ try {
20
20
  const INTEGRATION_NAME = 'project-knowledge';
21
21
  const MARKETPLACE_NAME = 'project-knowledge';
22
22
  const DEFAULT_MARKETPLACE_SOURCE = 'SanQianX/project-knowledge-base';
23
- const SUPPORTED_CLIENTS = ['claude', 'opencode', 'codex'];
23
+ const SUPPORTED_CLIENTS = ['claude', 'opencode', 'codex', 'zcode'];
24
24
  const DEFAULT_BRIDGE_CONSUMER = 'project-knowledge';
25
25
  const CLIENT_LABELS = {
26
26
  claude: 'Claude Code',
27
27
  opencode: 'OpenCode',
28
28
  codex: 'Codex',
29
+ zcode: 'ZCode',
29
30
  };
30
31
  const CLIENT_METHOD_SUFFIX = {
31
32
  claude: 'Claude',
32
33
  opencode: 'OpenCode',
33
34
  codex: 'Codex',
35
+ zcode: 'ZCode',
34
36
  };
35
37
 
36
38
  function normalizeClient(value) {
@@ -323,26 +325,28 @@ class IntegrationManager {
323
325
  this.commandResolver = options.commandResolver || defaultCommandResolver;
324
326
  this.dryRun = Boolean(options.dryRun);
325
327
  this.logger = options.logger || null;
328
+ this.operationId = options.operationId || '';
326
329
  this.planned = [];
327
330
  this.bridge = options.bridgeModule || bridgeModule;
328
331
  this.bridgeConsumerName = options.bridgeConsumerName || DEFAULT_BRIDGE_CONSUMER;
329
332
  this.bridgeHomeDir = options.bridgeHomeDir
330
333
  || this.env.AI_CODING_EVENT_BRIDGE_HOME
331
334
  || path.join(resolveDataDirPath({ dataDir: this.env.KB_DATA_DIR || '', homeDir: this.homeDir }), 'events');
332
- this.claudeSettingsFile = options.claudeSettingsFile || '';
333
- this.codexConfigFile = options.codexConfigFile || '';
334
- this.openCodePluginsDir = options.openCodePluginsDir || '';
335
+ this.claudeSettingsFile = options.claudeSettingsFile || path.join(this.homeDir, '.claude', 'settings.json');
336
+ this.codexConfigFile = options.codexConfigFile || path.join(this.homeDir, '.codex', 'config.toml');
337
+ this.openCodePluginsDir = options.openCodePluginsDir || path.join(openCodeConfigDir(this.homeDir, this.env), 'plugin');
338
+ this.zcodeConfigFile = options.zcodeConfigFile || path.join(this.homeDir, '.zcode', 'cli', 'config.json');
335
339
  }
336
340
 
337
341
  _bridgeFacade() {
338
342
  return this.bridge.createBridge({ homeDir: this.bridgeHomeDir });
339
343
  }
340
344
 
341
- _captureInstallerOptions() {
345
+ _captureInstallerOptions(client) {
342
346
  return {
343
347
  homeDir: this.bridgeHomeDir,
344
348
  settingsFile: this.claudeSettingsFile || undefined,
345
- configFile: this.codexConfigFile || undefined,
349
+ configFile: client === 'zcode' ? this.zcodeConfigFile : this.codexConfigFile,
346
350
  pluginsDir: this.openCodePluginsDir || undefined,
347
351
  };
348
352
  }
@@ -382,6 +386,42 @@ class IntegrationManager {
382
386
 
383
387
  // ---- Development Capture connectors (Bridge hooks / notify / plugin) ----
384
388
 
389
+ async captureOperation(operation, client, options = {}) {
390
+ const startedAt = Date.now();
391
+ const context = { client, operation };
392
+ try {
393
+ const result = await this[`${operation}Capture${CLIENT_METHOD_SUFFIX[client]}`](options);
394
+ const ok = !result.conflict && result.state !== 'failed';
395
+ if (operation !== 'status' || !ok) this.log(ok ? 'info' : 'error', `integration.capture.${ok ? 'completed' : 'failed'}`, ok ? 'Agent 采集接线操作完成。' : 'Agent 采集接线未完成。', { operationId: this.operationId, durationMs: Date.now() - startedAt, error: ok ? undefined : Object.assign(new Error(result.detail), { code: 'CAPTURE_CONFLICT' }), context });
396
+ return { ok, ...result, ...(!ok ? { error: { code: 'CAPTURE_CONFLICT', message: result.detail } } : {}) };
397
+ } catch (error) {
398
+ this.log('error', 'integration.capture.failed', 'Agent 采集接线操作失败。', { operationId: this.operationId, durationMs: Date.now() - startedAt, error, context });
399
+ return { ok: false, state: 'failed', installed: false, detail: error.message, error: { code: error.code || 'CAPTURE_FAILED', message: error.message } };
400
+ }
401
+ }
402
+
403
+ async installCaptureZCode() {
404
+ if (this.dryRun) {
405
+ this.planned.push({ command: 'bridge', args: ['installZcodeHook', this.bridgeHomeDir] });
406
+ return { state: 'planned', installed: false, detail: '计划接入 ZCode 会话采集,未修改配置。' };
407
+ }
408
+ await this.bridge.installers.zcode.installZcodeHook(this._captureInstallerOptions('zcode'));
409
+ const status = await this.statusCaptureZCode();
410
+ if (!status.installed) throw new Error('ZCode Hook 写入后校验失败,请检查 hooks.enabled 与事件配置。');
411
+ return status;
412
+ }
413
+
414
+ async statusCaptureZCode() {
415
+ const status = await this.bridge.installers.zcode.statusZcodeHook(this._captureInstallerOptions('zcode'));
416
+ return { state: status.installed ? 'installed' : 'not-installed', installed: status.installed, conflict: false, detail: status.installed ? '已配置用户输入与回答采集;请重新打开 ZCode 会话。' : 'ZCode 会话采集尚未接入。' };
417
+ }
418
+
419
+ async uninstallCaptureZCode() {
420
+ if (this.dryRun) return { state: 'planned', detail: '计划移除 ZCode 采集,未修改配置。' };
421
+ await this.bridge.installers.zcode.uninstallZcodeHook(this._captureInstallerOptions('zcode'));
422
+ return { state: 'removed', installed: false, detail: '已移除 ZCode 托管采集,保留第三方配置与已有记录。' };
423
+ }
424
+
385
425
  async installCaptureClaude() {
386
426
  if (this.dryRun) {
387
427
  this.planned.push({ command: 'bridge', args: ['installClaudeCodeHook', this.bridgeHomeDir] });
@@ -397,7 +437,7 @@ class IntegrationManager {
397
437
  state: status.installed ? 'installed' : 'not-installed',
398
438
  installed: status.installed,
399
439
  conflict: false,
400
- detail: status.installed ? `managed events: ${status.managedEvents.join(', ')}` : 'managed hooks not installed',
440
+ detail: status.installed ? '已配置用户输入与回答采集。' : '会话采集 Hook 尚未配置。',
401
441
  };
402
442
  }
403
443
 
@@ -453,7 +493,7 @@ class IntegrationManager {
453
493
  state: status.installed ? 'installed' : status.thirdParty ? 'conflict' : 'not-installed',
454
494
  installed: Boolean(status.installed),
455
495
  conflict: Boolean(status.thirdParty),
456
- detail: status.installed ? 'managed notify installed' : status.thirdParty ? 'third-party notify present' : 'notify not installed',
496
+ detail: status.installed ? '已配置提交回合后的会话采集。' : status.thirdParty ? '已有第三方通知配置,不会覆盖;请查看诊断记录。' : '会话采集通知尚未配置。',
457
497
  };
458
498
  }
459
499
 
@@ -481,7 +521,7 @@ class IntegrationManager {
481
521
  state: status.installed ? 'installed' : 'not-installed',
482
522
  installed: Boolean(status.installed),
483
523
  conflict: false,
484
- detail: status.installed ? 'managed plugin installed' : `plugin not installed (${status.thirdPartyFiles} third-party files preserved)`,
524
+ detail: status.installed ? '已配置会话采集插件。' : '会话采集插件尚未配置,保留其他插件。',
485
525
  };
486
526
  }
487
527
 
@@ -510,9 +550,21 @@ class IntegrationManager {
510
550
  }
511
551
 
512
552
  detectClients() {
513
- return SUPPORTED_CLIENTS.filter(client => Boolean(this.executable(client)));
553
+ return SUPPORTED_CLIENTS.filter(client => Boolean(this.executable(client)) || client === 'zcode' && this.captureConfigFound(client));
514
554
  }
515
555
 
556
+ captureConfigFound(client) {
557
+ const target = { claude: this.claudeSettingsFile, codex: this.codexConfigFile, opencode: path.dirname(this.openCodePluginsDir), zcode: this.zcodeConfigFile }[client];
558
+ try { const stat = fs.statSync(target); return client === 'opencode' ? stat.isDirectory() : stat.isFile(); } catch { return false; }
559
+ }
560
+
561
+ // Capture support does not imply a verified MCP/plugin installer. Never
562
+ // present ZCode knowledge retrieval as installed merely because hooks exist.
563
+ statusZCode() { return { state: 'not-supported', supported: false, installed: false, detail: 'ZCode 当前支持会话采集;知识检索插件未接入,不会自动安装。' }; }
564
+ installZCode() { return this.statusZCode(); }
565
+ updateZCode() { return this.statusZCode(); }
566
+ uninstallZCode() { return this.statusZCode(); }
567
+
516
568
  execute(client, args, options = {}) {
517
569
  const startedAt = Date.now();
518
570
  const command = this.executable(client);
@@ -805,13 +857,7 @@ class IntegrationManager {
805
857
  const results = [];
806
858
  for (const rawClient of clients) {
807
859
  const client = normalizeClient(rawClient);
808
- const suffix = CLIENT_METHOD_SUFFIX[client];
809
- try {
810
- const result = await this[`${operation}Capture${suffix}`](options);
811
- results.push({ client, label: CLIENT_LABELS[client], ok: true, ...result });
812
- } catch (error) {
813
- results.push({ client, label: CLIENT_LABELS[client], ok: false, state: 'failed', detail: error.message });
814
- }
860
+ results.push({ client, label: CLIENT_LABELS[client], ...(await this.captureOperation(operation, client, options)) });
815
861
  }
816
862
  return results;
817
863
  }
@@ -839,11 +885,7 @@ class IntegrationManager {
839
885
  }
840
886
  }
841
887
  if (options.knowledgeOnly !== true) {
842
- try {
843
- entry.developmentCapture = { ok: true, ...(await this[`installCapture${suffix}`]()) };
844
- } catch (error) {
845
- entry.developmentCapture = { ok: false, state: 'failed', detail: error.message };
846
- }
888
+ entry.developmentCapture = await this.captureOperation('install', client);
847
889
  }
848
890
  summary.clients.push(entry);
849
891
  }
@@ -851,7 +893,7 @@ class IntegrationManager {
851
893
  }
852
894
 
853
895
  async statusAll(options = {}) {
854
- const clients = options.clients && options.clients.length ? options.clients.map(normalizeClient) : this.detectClients();
896
+ const clients = options.clients && options.clients.length ? options.clients.map(normalizeClient) : SUPPORTED_CLIENTS;
855
897
  const summary = { clients: [], bridge: null };
856
898
  try {
857
899
  summary.bridge = { ok: true, ...(await this.bridgeStatus()) };
@@ -860,17 +902,15 @@ class IntegrationManager {
860
902
  }
861
903
  for (const client of clients) {
862
904
  const suffix = CLIENT_METHOD_SUFFIX[client];
863
- const entry = { client, label: CLIENT_LABELS[client], available: Boolean(this.executable(client)) };
864
- try {
865
- entry.knowledgeIntegration = { ok: true, ...(await this[`status${suffix}`]()) };
866
- } catch (error) {
867
- entry.knowledgeIntegration = { ok: false, state: 'failed', detail: error.message };
868
- }
869
- try {
870
- entry.developmentCapture = { ok: true, ...(await this[`statusCapture${suffix}`]()) };
871
- } catch (error) {
872
- entry.developmentCapture = { ok: false, state: 'failed', detail: error.message };
905
+ const entry = { client, label: CLIENT_LABELS[client], available: Boolean(this.executable(client)), configFound: this.captureConfigFound(client) };
906
+ if (!options.captureOnly) {
907
+ try {
908
+ entry.knowledgeIntegration = { ok: true, ...(await this[`status${suffix}`]()) };
909
+ } catch (error) {
910
+ entry.knowledgeIntegration = { ok: false, state: 'failed', detail: error.message };
911
+ }
873
912
  }
913
+ entry.developmentCapture = await this.captureOperation('status', client);
874
914
  summary.clients.push(entry);
875
915
  }
876
916
  return summary;
@@ -881,6 +921,7 @@ class IntegrationManager {
881
921
  async disableAllCapture(options = {}) {
882
922
  const clients = options.clients && options.clients.length ? options.clients.map(normalizeClient) : SUPPORTED_CLIENTS;
883
923
  const results = await this.operateCapture('uninstall', clients, { unregisterConsumer: false });
924
+ if (results.some(result => !result.ok)) return { clients: results, consumer: { ok: false, state: 'kept', detail: '部分采集未能移除,保留宿主消费者,请修复失败项后重试。' } };
884
925
  let consumer = null;
885
926
  try {
886
927
  consumer = { ok: true, ...(await this.unregisterBridgeConsumer()) };
@@ -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', '--path-format=absolute', '--git-common-dir']).stdout);
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', '--path-format=absolute', '--git-common-dir']);
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 ? path.resolve(commonDir.stdout) : '',
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', '--path-format=absolute', '--git-common-dir']).stdout;
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: path.resolve(commonDir), head: head.ok ? head.stdout : null };
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.phase, error, context: { rollbackWarnings } });
386
- if (error instanceof DomainError) throw error;
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', '--path-format=absolute', '--git-common-dir']);
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) {
@@ -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', '--path-format=absolute', '--git-common-dir']),
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()),
@@ -1292,13 +1292,13 @@ function createRequestHandler(runtime, options = {}) {
1292
1292
  // Unified Integration Setup surface: one action manages Knowledge
1293
1293
  // Integration and Development Capture per client, with separate status.
1294
1294
  if (method === 'GET' && pathname === '/api/integrations/status') {
1295
- const manager = new IntegrationManager({ rootDir: runtime.rootDir });
1296
- const summary = await manager.statusAll({});
1295
+ const manager = new IntegrationManager({ ...options.integrationOptions, rootDir: runtime.rootDir, logger: runtime.logger, operationId: requestOperationId, bridgeHomeDir: runtime.bridgeAdapter.hookBinding().bridgeHomeDir });
1296
+ const summary = await manager.statusAll({ captureOnly: url.searchParams.get('captureOnly') === '1' });
1297
1297
  return send(res, 200, { ok: true, ...summary });
1298
1298
  }
1299
1299
  if (method === 'POST' && pathname === '/api/integrations/install') {
1300
1300
  const body = await readJsonBody(req);
1301
- const manager = new IntegrationManager({ rootDir: runtime.rootDir });
1301
+ const manager = new IntegrationManager({ ...options.integrationOptions, rootDir: runtime.rootDir, logger: runtime.logger, operationId: requestOperationId, bridgeHomeDir: runtime.bridgeAdapter.hookBinding().bridgeHomeDir });
1302
1302
  const clients = Array.isArray(body.clients) && body.clients.length
1303
1303
  ? body.clients.map(client => String(client))
1304
1304
  : undefined;
@@ -1308,12 +1308,13 @@ function createRequestHandler(runtime, options = {}) {
1308
1308
  captureOnly: body.captureOnly === true,
1309
1309
  scope: body.scope || 'user',
1310
1310
  });
1311
- await runtime.logger.info('integrations.install_completed', 'Integration setup completed.', { context: { clients: summary.clients.map(entry => entry.client) } });
1312
- return send(res, 200, { ok: true, ...summary });
1311
+ const complete = summary.bridge.ok && summary.clients.every(entry => (!entry.knowledgeIntegration || entry.knowledgeIntegration.ok) && (!entry.developmentCapture || entry.developmentCapture.ok));
1312
+ await runtime.logger[complete ? 'info' : 'error'](`integrations.install_${complete ? 'completed' : 'failed'}`, complete ? 'Agent 接入完成。' : 'Agent 接入未完成,请查看各工具的失败原因。', { operationId: requestOperationId, context: { clients: summary.clients.map(entry => entry.client) } });
1313
+ return send(res, 200, { ok: true, complete, operationId: requestOperationId, ...summary });
1313
1314
  }
1314
1315
  if (method === 'POST' && pathname === '/api/integrations/uninstall') {
1315
1316
  const body = await readJsonBody(req);
1316
- const manager = new IntegrationManager({ rootDir: runtime.rootDir });
1317
+ const manager = new IntegrationManager({ ...options.integrationOptions, rootDir: runtime.rootDir, logger: runtime.logger, operationId: requestOperationId, bridgeHomeDir: runtime.bridgeAdapter.hookBinding().bridgeHomeDir });
1317
1318
  if (body.disableAllCapture === true) {
1318
1319
  const summary = await manager.disableAllCapture({});
1319
1320
  return send(res, 200, { ok: true, ...summary });
@@ -1321,9 +1322,9 @@ function createRequestHandler(runtime, options = {}) {
1321
1322
  const clients = Array.isArray(body.clients) && body.clients.length
1322
1323
  ? body.clients.map(client => String(client))
1323
1324
  : undefined;
1324
- const knowledgeResults = await manager.operate('uninstall', clients && clients.length ? clients : manager.detectClients(), { scope: body.scope || 'user' });
1325
+ const knowledgeResults = body.captureOnly === true ? [] : await manager.operate('uninstall', clients && clients.length ? clients : manager.detectClients(), { scope: body.scope || 'user' });
1325
1326
  const captureResults = body.knowledgeOnly === true ? [] : await manager.operateCapture('uninstall', clients && clients.length ? clients : manager.detectClients(), {});
1326
- return send(res, 200, { ok: true, clients: knowledgeResults, capture: captureResults });
1327
+ return send(res, 200, { ok: true, complete: [...knowledgeResults, ...captureResults].every(entry => entry.ok), operationId: requestOperationId, clients: knowledgeResults, capture: captureResults });
1327
1328
  }
1328
1329
 
1329
1330
  if (method === 'GET' && pathname === '/api/ai-profiles') {
@@ -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', '--path-format=absolute', '--git-common-dir']);
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 ? path.resolve(commonDir) : null,
104
+ commonDir: commonDir ? resolveGitPath(repoRoot, commonDir) : null,
104
105
  remote: remoteRaw ? bridgeIdentityModule.normalizeGitUrl(remoteRaw) : null,
105
106
  });
106
107
  }
@@ -5,6 +5,8 @@ const {
5
5
  SUPPORTED_CLIENTS,
6
6
  normalizeClient,
7
7
  } = require('../_site/lib/integration-installer');
8
+ const { Logger } = require('../_site/lib/structured-logger');
9
+ const { createId } = require('../_site/lib/contracts');
8
10
 
9
11
  function printHelp() {
10
12
  console.log(`project-knowledge-integrations
@@ -23,6 +25,7 @@ Clients:
23
25
  claude Claude Code plugin (MCP + Skill) + managed Bridge hooks
24
26
  opencode OpenCode MCP + global Skill + managed Bridge plugin
25
27
  codex Codex plugin (MCP + Skill) + managed Bridge notify
28
+ zcode ZCode managed Bridge hooks (capture only; no verified MCP installer)
26
29
  all All supported clients
27
30
 
28
31
  Options:
@@ -99,7 +102,7 @@ function selectClients(requested, manager) {
99
102
 
100
103
  function printCapabilityLine(prefix, capability) {
101
104
  if (!capability) return;
102
- const mark = capability.ok ? 'OK' : 'FAIL';
105
+ const mark = capability.supported === false ? 'INFO' : capability.ok ? 'OK' : 'FAIL';
103
106
  console.log(` [${mark}] ${prefix}: ${capability.state} - ${capability.detail}`);
104
107
  }
105
108
 
@@ -109,10 +112,18 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
109
112
  printHelp();
110
113
  return 0;
111
114
  }
115
+ const logger = dependencies.manager ? null : new Logger();
112
116
  const manager = dependencies.manager || new IntegrationManager({
117
+ logger,
118
+ operationId: createId('op'),
113
119
  dryRun: options.dryRun,
114
120
  marketplaceSource: options.marketplaceSource || undefined,
115
121
  });
122
+ try { return await runOperation(options, manager); }
123
+ finally { if (logger) await logger.close(); }
124
+ }
125
+
126
+ async function runOperation(options, manager) {
116
127
  const clients = selectClients(options.clients, manager);
117
128
 
118
129
  if (options.operation === 'disable-capture') {
@@ -130,7 +141,7 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
130
141
  return summary.clients.some(result => !result.ok) || !summary.consumer.ok ? 1 : 0;
131
142
  }
132
143
 
133
- if (options.operation === 'install') {
144
+ if (options.operation === 'install' || options.operation === 'update' && options.captureOnly) {
134
145
  // One action configures Knowledge Integration + Development Capture
135
146
  // together (per-client flags narrow the scope). No client UI is needed.
136
147
  const summary = await manager.installAll({ clients, knowledgeOnly: options.knowledgeOnly, captureOnly: options.captureOnly, scope: options.scope });
@@ -85,6 +85,23 @@ vector-hub 是唯一派生索引服务,外壳不再执行第二套索引写入
85
85
 
86
86
  ## 需求记录与查询
87
87
 
88
+ ### 外部 Agent 会话采集
89
+
90
+ 在网页左下角打开“系统状态 → 外部 Agent 采集”,对使用的工具点击“接入”或
91
+ “修复”;“移除采集”只移除托管 Hook,不删除历史记录或第三方配置。
92
+ “检测到命令/检测到配置”与“已配置采集”是不同状态,未检测到不等于没有安装。
93
+ 配置完成后重新打开外部 Agent 会话,再到活动记录验证新会话是否出现。
94
+
95
+ ```powershell
96
+ project-knowledge-integrations status --all
97
+ project-knowledge-integrations install --ide zcode --capture-only
98
+ project-knowledge-integrations uninstall --ide zcode --capture-only
99
+ ```
100
+
101
+ ZCode 接入当前仅包含会话采集,不会冒充已安装 MCP/知识检索插件。
102
+ 接入失败会显示操作 ID,并写入数据目录 `logs/system/YYYY-MM-DD.jsonl`;
103
+ 网页“系统诊断记录”可查看和导出。注册消费者本身不能代替安装 Agent Hook。
104
+
88
105
  Claude Code、Codex、OpenCode 集成可在编码前追加用户真实需求元数据。记录写入
89
106
  项目 `requirements.jsonl`,不会触发分析或写知识。
90
107
 
@@ -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,9 +1,9 @@
1
1
  {
2
2
  "schema": "project-knowledge/module-content/v1",
3
3
  "productFiles": {
4
- "ui/index.html": "fd79551591729f6874c95089e479d46f208ab3b45cf45ba21eff1f6967757f7d",
5
- "ui/product.js": "d5fca94df54075d598f271e01ea41ac9cce3a9847ea4f246e4f20c47daed43ed",
6
- "ui/product.css": "1e68f087cec85a69f4497492d31fd1dd8712cf5176b0c721ce765de36ce606e1"
4
+ "ui/index.html": "e7291caab778eba3c5499d8286804ad1f4ef31c87a957f69c8fb77befc01321b",
5
+ "ui/product.js": "1a6ddedd97b00c3f798c13a0a55b0f63790e701483492bc97d28cae4034efaf8",
6
+ "ui/product.css": "73554abf3f951ea01a2bfe2df81748b702a6ca4488eea16bfe5afe04ef1dea70"
7
7
  },
8
8
  "modules": {
9
9
  "terminal": {
@@ -174,17 +174,17 @@
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.0",
177
+ "version": "0.7.2",
178
178
  "files": {
179
179
  "LICENSE": "94a3bded67f76ec9f6333ad3370111bcb6e5cadbeb6f306a8112f9543320ecc2",
180
- "README.md": "847242d93395aae17b42bbaa7fc8e36e6f89edecf11a1639b94b34c98729e1a9",
181
- "console/index.html": "680e6393c9ef88ea35c1eabff537e0b10f7a91c55986f18bca6bfe2a333eed6e",
182
- "package.json": "63b9846552b8ac3b7e361bd86bb1d9389d55e7232934aaa939b168e43ad650b7",
183
- "src/agent-detect.js": "06d58150c4295f7d5980ac47fd6fb1a680f1b7c27fd3dcd3f57049825dc5f092",
180
+ "README.md": "4699558a9498f93a83587523ef5cae019d2146867f355134ea27100ac6245769",
181
+ "console/index.html": "bdc61db0ca9e6b5d8886418c1c03df141f39ab722f50d0c16bba75fe57ac46f0",
182
+ "package.json": "f6b965a9ce9440039d96767c7d126a8149cffa56559693955cca15b0707bcd60",
183
+ "src/agent-detect.js": "2a4752218e6cdb4151ae9796b23558c2dd94b55217a20fa1f1a76f506bb2f781",
184
184
  "src/bin.js": "e443cfabb8df1a694c9e2415a292db81f183d9e31b7fe8f5f9a14d2a09d03052",
185
185
  "src/bridge/bin/commit-boundary.js": "2c87126d9d6467ddae13acc1d0997f2124a68174738b8cd6f8614e7eeccb3d0d",
186
186
  "src/bridge/connectors/capture-guard.js": "aa9c51a6012966c5b6eb66a40b61ada61fd2cfd43fd4f6985d7b655421e8a1e3",
187
- "src/bridge/connectors/claude-code/hook-entry.js": "abc224d4f76d372bc02649c5e1005674d089a3dbd83e09ce4c35a33b3a75c937",
187
+ "src/bridge/connectors/claude-code/hook-entry.js": "c88b9ace27e4d148ee7b04724ec2f990a0c71c6e59a4d22afb4c9a8e2e72048e",
188
188
  "src/bridge/connectors/claude-code/notify.js": "4325684ac92c1a2e531aefc5cbcbc886dc2b58080c410ee5e879574593f077cd",
189
189
  "src/bridge/connectors/codex/cursor-store.js": "077784e9c6b895dde95247706381dbaca68892e1382f3a957c3e90efb51059f5",
190
190
  "src/bridge/connectors/codex/hook-entry.js": "d180a3713609b251ba8a9fdffbcf202346b1098ce4e07cd3ae696216a9f56e96",
@@ -199,21 +199,21 @@
199
199
  "src/bridge/core/journal-router.js": "5fe48ae4240034f030807c80bdba35c6ef2cb7bfa3fa310105ce0fb77d272131",
200
200
  "src/bridge/core/journal.js": "13c72396c970e6acd9a97900a926daa108aded01d8bb3735213e0feff8aa20b6",
201
201
  "src/bridge/core/lock.js": "484988ce593d138183e7d94648ece97c88661e7fa8379f0ef7335a634fe15ba3",
202
- "src/bridge/core/normalizer.js": "34b4023efada0cfdcaaba98775b072041eaf623eb7fde48953654dc27d470ae1",
202
+ "src/bridge/core/normalizer.js": "e8eaa3582538c8f45dfc8d3ceb21c1e2f46347917d086436cfd361d32f62302e",
203
203
  "src/bridge/core/paths.js": "6e829c00105d3b9214942f33af6225b338c668ff4cdda5c3ab92cb759d54be30",
204
204
  "src/bridge/core/project-registry.js": "8c5cc8e18d55af1a0c2d682c25ea1c37eb6f4687cf3eaa5997fc57532560d866",
205
- "src/bridge/core/repo-context.js": "ea6c9638efcb7abdb45916747a4984a97c6df31cf3fc601d221e1730fa01409b",
206
- "src/bridge/core/runtime-home.js": "e68a3c927b02ca27299755c60c8f709f7ce97c9f4c5080deefe518bf60d9d989",
205
+ "src/bridge/core/repo-context.js": "1627d416fd6aaa43d8c4be8ce5097bb69244b1afe7f1ce5e9f71956c5542ce3f",
206
+ "src/bridge/core/runtime-home.js": "f2bfffba20c8256259cecc1014261897a4d6f721807c04b2e4d2b3dc973f14e1",
207
207
  "src/bridge/core/semver.js": "7f87ed40f3af3ae0480abc9934946358f0caa5d951472d5892b088aa468ec0f1",
208
208
  "src/bridge/core/turn-identity.js": "4ee79410d0122ca93cea565591c183db6bff64b0a467f50a4e590de4ee78c399",
209
209
  "src/bridge/index.js": "7f56d08fdcfa815a45f50c73b27d0627497ac364da55cd8e6635351f734b57b2",
210
210
  "src/bridge/installers/claude-code/installer.js": "c0a8a39d654e886fad838a54a2acb535096384d6307c88eb5dc813992efefbd2",
211
211
  "src/bridge/installers/codex/installer.js": "0c8542c2320b7e7b654ad22163bd5ee26f3792545f521e096ee5c795f2ec9303",
212
212
  "src/bridge/installers/opencode/installer.js": "27f2ed3629e55b4ee083c6d0d4ceafc614145102ead2ba99630ad979c120ab08",
213
- "src/bridge/installers/zcode/installer.js": "8bca7dfd4a31cf632a86480282c28a665ee23dc3838d643dcd883db6cc053af7",
213
+ "src/bridge/installers/zcode/installer.js": "625343d6a9fc81b81fb3d1fcd12ac6cdf6d33fae45925631db88aaf74fe497ff",
214
214
  "src/bridge/query/conversation-query.js": "89db83f3f4bf5e2807603589da9fa03b4b494e1cb94afc772e57572ccdcb0854",
215
215
  "src/pick-folder.js": "70f55df999262b90433f2b4251aa69dd70a23b55a84756b5f1d24dbeacfbda31",
216
- "src/server.js": "b2b62e5e7404d82dfc6e749d1860e3cfa072a9f669e61777355eed33762e6b5d"
216
+ "src/server.js": "d521073673b4e1cc84cc9fab46aae5c07f3274f7cdcf20f78696b7808be5103c"
217
217
  }
218
218
  }
219
219
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanqianx/project-knowledge",
3
- "version": "5.0.0",
3
+ "version": "5.0.2",
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.0",
83
+ "@sanqianx/ai-coding-event-bridge-console": "0.7.2",
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.0",
4
+ "version": "5.0.2",
5
5
  "description": "Read durable project decisions and implementation history from Project Knowledge.",
6
6
  "author": {
7
7
  "name": "SanQianX",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-knowledge",
3
- "version": "5.0.0",
3
+ "version": "5.0.2",
4
4
  "description": "Read durable project decisions and implementation history from Project Knowledge.",
5
5
  "author": {
6
6
  "name": "SanQianX",
@@ -6,7 +6,7 @@
6
6
  "args": [
7
7
  "-y",
8
8
  "--package",
9
- "@sanqianx/project-knowledge@5.0.0",
9
+ "@sanqianx/project-knowledge@5.0.2",
10
10
  "project-knowledge-mcp"
11
11
  ]
12
12
  }
package/ui/index.html CHANGED
@@ -32,11 +32,12 @@
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="查找失败原因或提交 SHA" aria-label="筛选诊断记录"><button id="loadLogs">查询</button><button id="exportLogs">导出诊断记录</button></div><div id="logsStatus" class="muted"></div><div id="logsContent"></div></dialog>
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">
39
39
  <section><h3>模块连接</h3><div id="moduleHealth"></div><button id="repairWiring">修复当前项目接线</button><p class="muted" id="wiringFeedback"></p><p class="muted" id="captureStatus"></p></section>
40
+ <section id="agentCaptureSection"><div class="actions"><h3>外部 Agent 采集</h3><button id="refreshAgentCapture">检查状态</button></div><p class="muted">只接入你使用的工具。采集外部开发会话,不会重新安装 Agent,也不影响已有记录。</p><div id="agentCaptureList"></div><p id="agentCaptureFeedback" class="feedback" role="status"></p></section>
40
41
  <section><h3>后台维护模型</h3><p class="muted">使用持久化默认模型,不跟随聊天中的临时切换。</p><select id="maintenanceModel" aria-label="后台维护默认模型"></select><div class="actions"><button id="saveMaintenance">保存默认模型</button><button id="configureProviders">管理模型供应商</button></div><p id="configurationStatus" class="muted"></p></section>
41
42
  <section><h3>向量检索模型</h3><p id="embeddingStatus" class="muted">正在读取配置。</p><button id="openEmbedding">配置向量模型</button><p class="muted">凭据由工作台统一保存。未配置时仍可使用文本检索。</p></section>
42
43
  <section><h3>智脑设置</h3><label>名称<input id="brainName" maxlength="100"></label><label>知识根目录<div class="input-with-action"><input id="knowledgeRoot"><button id="pickRoot">选择</button></div></label><p class="muted">修改根目录不会移动已有项目知识或共享知识。</p><button id="saveBrainSettings">保存设置</button></section>
package/ui/product.css CHANGED
@@ -1,4 +1,5 @@
1
1
  :root{--page:#f3f5f7;--surface:#fff;--surface2:#f8f9fb;--surface3:#edf1f4;--line:#dce2e8;--text:#202733;--muted:#687587;--faint:#8e99a7;--nav:#eef1f4;--accent:#516f89;--accent-soft:#e6edf3;--good:#397761;--bad:#a05252;--bad-soft:#fbeeee;color-scheme:light}
2
+ .agent-capture-row{display:flex;align-items:center;gap:16px;padding:16px 0;border-bottom:1px solid var(--line)}.agent-capture-row>div:first-child{flex:1;min-width:0}.agent-capture-row small{display:block;color:var(--muted);margin-top:8px;overflow-wrap:anywhere}.agent-capture-row .actions{flex-shrink:0}#agentCaptureFeedback{white-space:pre-wrap}
2
3
  #logsDialog{display:flex;flex-direction:column;overflow:hidden}#logsDialog:not([open]){display:none}#logsContent{overflow:auto;min-height:0;flex:1;margin-top:16px}#logsContent .row{align-items:flex-start;cursor:pointer}#logsContent pre{flex:1;min-width:0;margin:0}#logsContent .log-entry{border-bottom:1px solid var(--line)}#logsContent .log-detail{padding:16px;background:var(--surface2);max-height:320px;overflow:auto}#logsContent [data-level=warn],#logsContent [data-level=error],#logsContent [data-level=fatal]{color:var(--bad)}#logsContent .row time,#logsContent .row pre{color:inherit}
3
4
  @media(prefers-reduced-motion:reduce){*,*::before,*::after{scroll-behavior:auto!important;animation:none!important;transition:none!important}}
4
5
  .knowledge-context{overflow-x:hidden}.reference{overflow-wrap:anywhere}.reference small{min-width:0;max-width:100%}
package/ui/product.js CHANGED
@@ -19,13 +19,14 @@
19
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
21
  let toastTimer, refreshTimer, preflightTimer, editingMemoryId, logStream, logEpoch = 0, referenceEpoch = 0, snapshotEpoch = 0, registryEpoch = 0, registryPromise = null, resumeImport = false, unbindProjectId = '', unbinding = false;
22
+ let captureEpoch = 0, captureBusy = false;
22
23
  async function api(url, options = {}) {
23
24
  const { allowNotReady, ...init } = options;
24
25
  if (init.body && typeof init.body !== 'string') init.body = JSON.stringify(init.body);
25
26
  if (init.body) init.headers = { 'Content-Type': 'application/json', ...init.headers };
26
27
  const response = await fetch(url, init);
27
28
  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 });
29
+ 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
30
  return data;
30
31
  }
31
32
  function toast(text) { $('toast').textContent = text; $('toast').hidden = false; clearTimeout(toastTimer); toastTimer = setTimeout(() => { $('toast').hidden = true; }, 4000); }
@@ -182,22 +183,51 @@
182
183
  async function openSystem() {
183
184
  $('systemDrawer').hidden = false; $('drawerBackdrop').hidden = false;
184
185
  $('brainName').value = state.brain.name; $('knowledgeRoot').value = state.root;
185
- await loadMaintenance();
186
+ await Promise.all([loadMaintenance(), loadAgentCapture()]);
187
+ }
188
+ async function loadAgentCapture() {
189
+ const epoch = ++captureEpoch;
190
+ try {
191
+ const result = await api('/api/integrations/status?captureOnly=1');
192
+ if (epoch !== captureEpoch) return;
193
+ $('agentCaptureList').innerHTML = result.clients.map(agent => {
194
+ const capture = agent.developmentCapture;
195
+ const found = agent.available || agent.configFound;
196
+ const software = agent.available ? '检测到命令' : agent.configFound ? '检测到配置' : '未检测到';
197
+ const status = !capture.ok ? '接线异常' : capture.installed ? '已配置采集' : '未接入';
198
+ return `<div class="agent-capture-row"><div><b>${esc(agent.label)}</b><small>${esc(software)} · ${esc(status)}</small><small>${esc(capture.detail)}</small></div><div class="actions"><button data-agent-capture="install" data-agent-client="${esc(agent.client)}" ${!found || captureBusy ? 'disabled' : ''}>${capture.installed ? '修复' : '接入'}</button>${capture.installed ? `<button data-agent-capture="uninstall" data-agent-client="${esc(agent.client)}" ${captureBusy ? 'disabled' : ''}>移除采集</button>` : ''}</div></div>`;
199
+ }).join('');
200
+ } catch (error) { if (epoch === captureEpoch) $('agentCaptureList').textContent = `无法检查采集状态:${error.message}`; }
201
+ }
202
+ async function changeAgentCapture(client, operation) {
203
+ if (captureBusy) return;
204
+ captureBusy = true; captureEpoch++;
205
+ all('[data-agent-capture]').forEach(button => { button.disabled = true; });
206
+ $('agentCaptureFeedback').textContent = operation === 'install' ? '正在配置采集 Hook…' : '正在移除托管采集,已有记录保留…';
207
+ try {
208
+ const result = await api(`/api/integrations/${operation}`, { method: 'POST', body: { clients: [client], captureOnly: true } });
209
+ const capture = operation === 'install' ? result.clients[0]?.developmentCapture : result.capture[0];
210
+ const success = result.complete === true && capture?.ok;
211
+ $('agentCaptureFeedback').textContent = success ? operation === 'install' ? '采集配置已保存。请重新打开外部 Agent 会话,实际记录可在活动页面查看。' : '已移除托管采集,第三方配置与已有记录保留。' : `接入操作未完成:${capture?.detail || result.bridge?.detail || '请检查系统诊断记录。'}\n操作 ID:${result.operationId || '—'}`;
212
+ $('agentCaptureFeedback').classList.toggle('issue', !success);
213
+ } catch (error) { $('agentCaptureFeedback').textContent = `操作失败:${error.message}`; $('agentCaptureFeedback').classList.add('issue'); }
214
+ finally { captureBusy = false; await loadAgentCapture(); post('eventBridge', { type: 'kb:refresh-agents' }); }
186
215
  }
187
216
  async function loadLogs() {
188
217
  const epoch = ++logEpoch;
189
218
  logStream?.close(); logStream = null;
190
219
  const params = new URLSearchParams({ pageSize: '100', q: $('logsSearch').value.trim() });
191
- if (state.projectId) params.set('projectId', state.projectId);
220
+ const projectOnly = $('logsScope').value === 'project' && Boolean(state.projectId);
221
+ if (projectOnly) params.set('projectId', state.projectId);
192
222
  const result = await api(`/api/logs?${params}`);
193
223
  if (epoch !== logEpoch || !$('logsDialog').open) return;
194
224
  $('logsDialog').dataset.streamCursor = result.streamCursor || '';
195
225
  $('logsContent').replaceChildren();
196
226
  for (const record of [...(result.entries || [])].reverse()) appendLog(record);
197
- $('logsStatus').textContent = `${(result.entries || []).length} 条诊断记录 · 当前项目`;
227
+ $('logsStatus').textContent = `${(result.entries || []).length} 条诊断记录 · ${projectOnly ? '当前项目' : '系统与所有项目'}`;
198
228
  if (!$('logsSearch').value.trim()) {
199
229
  const streamParams = new URLSearchParams({ streamCursor: result.streamCursor || '' });
200
- if (state.projectId) streamParams.set('projectId', state.projectId);
230
+ if (projectOnly) streamParams.set('projectId', state.projectId);
201
231
  logStream = new EventSource(`/api/logs/stream?${streamParams}`);
202
232
  logStream.addEventListener('logs/appended', event => { if (epoch === logEpoch && $('logsDialog').open) appendLog(JSON.parse(event.data).record); });
203
233
  }
@@ -306,6 +336,7 @@
306
336
  if (button.dataset.action === 'import') openImport();
307
337
  if (button.dataset.action === 'repair') await repair();
308
338
  if (button.dataset.action === 'system') await openSystem();
339
+ if (button.dataset.agentCapture) await changeAgentCapture(button.dataset.agentClient, button.dataset.agentCapture);
309
340
  if (button.dataset.action === 'unbind') { const p = activeProject(); if (p && !unbinding) { unbindProjectId = p.projectId; $('unbindProjectName').textContent = `解绑「${p.name}」?`; $('unbindFeedback').textContent = ''; $('unbindDialog').showModal(); } }
310
341
  if (button.dataset.retryRun) { await api(`/api/engine/runs/${encodeURIComponent(button.dataset.retryRun)}/retry`, { method: 'POST', body: {} }); await refreshSnapshot(); }
311
342
  if (button.dataset.contextIndex !== undefined) await openReference(state.contexts.get(state.projectId).references[Number(button.dataset.contextIndex)]);
@@ -324,6 +355,7 @@
324
355
  if (event.origin !== location.origin || !event.data || typeof event.data !== 'object') return;
325
356
  const key = Object.keys(frames).find(k => $(frames[k].id).contentWindow === event.source); if (!key) return;
326
357
  const message = event.data;
358
+ if (message.type === 'kb:open-integrations' && key === 'eventBridge') { openSystem().then(() => $('agentCaptureSection').scrollIntoView({ block: 'start' })).catch(error => toast(error.message)); return; }
327
359
  if (message.type === 'kb:ready') { frames[key].ready = true; frameContext(key); if (frames[key].openSettings) { post(key, { type: 'kb:open-settings' }); frames[key].openSettings = false; } }
328
360
  if (message.requestId && message.requestId !== frames[key].requestId) return;
329
361
  if (message.type === 'kb:knowledge' && key === 'terminal') {
@@ -335,6 +367,7 @@
335
367
  on('themeToggle', 'click', () => { document.documentElement.dataset.theme = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark'; localStorage.setItem('kb.theme', document.documentElement.dataset.theme); Object.keys(frames).forEach(key => post(key, { type: 'kb:theme', theme: document.documentElement.dataset.theme })); });
336
368
  on('openSystem', 'click', openSystem); on('closeSystem', 'click', closeSystem); on('drawerBackdrop', 'click', closeSystem);
337
369
  on('repairWiring', 'click', repair);
370
+ on('refreshAgentCapture', 'click', () => captureBusy ? undefined : loadAgentCapture());
338
371
  on('configureProviders', 'click', () => { closeSystem(); setView('work'); initializeFrame('terminal'); if (frames.terminal.ready) post('terminal', { type: 'kb:open-settings' }); else frames.terminal.openSettings = true; });
339
372
  on('saveMaintenance', 'click', async () => { await api('/api/terminal/maintenance-default', { method: 'PUT', body: { profileId: $('maintenanceModel').value || null } }); const result = await api('/api/modules/configure', { method: 'POST', body: {} }); $('configurationStatus').textContent = result.configuration.model ? '后台默认模型已保存并同步。' : '配置已保存,后台运行配置尚未同步,请检查模块连接。'; });
340
373
  on('openEmbedding', 'click', openEmbedding);
@@ -350,6 +383,7 @@
350
383
  on('engineDialog', 'close', () => post('engine', { type: 'kb:visibility', visible: false }));
351
384
  on('openLogs', 'click', async () => { closeSystem(); $('logsDialog').showModal(); await loadLogs(); });
352
385
  on('logsDialog', 'close', () => { logEpoch++; logStream?.close(); logStream = null; });
386
+ on('logsScope', 'change', loadLogs);
353
387
  on('copyReference', 'click', async () => {
354
388
  const text = $('referenceMeta').textContent + '\n\n' + $('referenceContent').textContent;
355
389
  try { await navigator.clipboard.writeText(text); }
@@ -357,7 +391,7 @@
357
391
  toast('已复制知识与来源。');
358
392
  });
359
393
  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); });
394
+ 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
395
  on('createDomain', 'click', () => { $('domainForm').reset(); $('domainDialog').showModal(); });
362
396
  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
397
  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 +422,15 @@
388
422
  on('importForm', 'submit', async e => {
389
423
  e.preventDefault(); if (!state.preflight?.ready || state.preflight.plannedGitInit && !$('importForm').elements.confirmGitInit.checked || state.preflight.checks.hook?.replacementProjectId && !$('importForm').elements.confirmReplaceHook.checked) return;
390
424
  $('confirmImport').disabled = true; $('confirmImport').textContent = '正在登记…';
391
- try { const result = await api('/api/projects/import', { method: 'POST', body: importInput() }); 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) ? '项目已导入,四个模块已连接。' : '项目已导入,部分接线待恢复。'); }
392
- finally { $('confirmImport').textContent = '导入项目'; if ($('importDialog').open) checkImport(); }
425
+ let failed = false, registered = false;
426
+ 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) ? '项目已导入,四个模块已连接。' : '项目已导入,部分接线待恢复。'); }
427
+ catch (error) {
428
+ failed = true;
429
+ if (registered) { toast(`项目已登记,但页面状态刷新失败:${error.message}`); return; }
430
+ $('preflightStatus').textContent = `导入失败:${error.message}\n错误码:${error.code || 'UNKNOWN'}${error.operationId ? `\n操作 ID:${error.operationId}` : ''}\n请在系统诊断记录中查看详细原因。`;
431
+ $('preflightStatus').classList.add('issue'); $('confirmImport').disabled = false;
432
+ }
433
+ finally { $('confirmImport').textContent = '导入项目'; if ($('importDialog').open && !failed) checkImport(); }
393
434
  });
394
435
  window.addEventListener('hashchange', () => setView(location.hash.slice(1)));
395
436
  window.addEventListener('offline', () => {