@sanqianx/project-knowledge 5.0.18 → 5.0.20-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +3 -3
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +14 -0
- package/LICENSE +201 -201
- package/NOTICE +5 -5
- package/README.md +349 -349
- package/_modules/knowledge-engine/README.md +4 -0
- package/_modules/knowledge-engine/src/analysis-contract.js +4 -3
- package/_modules/knowledge-engine/src/analysis-failures.js +39 -0
- package/_modules/knowledge-engine/src/analysis-worker.js +6 -2
- package/_modules/knowledge-engine/src/coordinator.js +8 -0
- package/_modules/knowledge-engine/src/headless-analyzer.js +2 -1
- package/_modules/knowledge-engine/src/memories.js +10 -5
- package/_modules/knowledge-engine/src/memory-eligibility.js +74 -0
- package/_modules/knowledge-engine/src/retrieval-ranking.js +19 -1
- package/_modules/knowledge-engine/src/runtime/commit-conversation-binder.js +1 -0
- package/_modules/knowledge-engine/src/runtime/commit-prompt.js +12 -9
- package/_modules/knowledge-engine/src/runtime/conversation-store.js +2 -0
- package/_modules/knowledge-engine/src/server.js +1 -1
- package/_site/_test/agent-capture-mcp-ui-test.js +67 -67
- package/_site/_test/ai-profile-test.js +75 -75
- package/_site/_test/ai-vendor-presets-test.js +91 -91
- package/_site/_test/automatic-context-test.js +70 -70
- package/_site/_test/bin-cli-test.js +230 -230
- package/_site/_test/bridge-adapter-test.js +95 -95
- package/_site/_test/bridge-consumer-service-test.js +310 -310
- package/_site/_test/candidate-bin-content-test.js +35 -0
- package/_site/_test/candidate-product-assets-test.js +13 -0
- package/_site/_test/candidate-release-contract-test.js +14 -14
- package/_site/_test/capture-hook-dedup-test.js +158 -158
- package/_site/_test/claude-executable-discovery-test.js +28 -28
- package/_site/_test/claude-session-lifecycle-sweep-test.js +252 -252
- package/_site/_test/cli-instance-management-test.js +62 -62
- package/_site/_test/client-capture-restart-e2e-test.js +189 -189
- package/_site/_test/codex-conversation-projection-test.js +60 -60
- package/_site/_test/codex-marketplace-scoped-path-test.js +186 -186
- package/_site/_test/codex-notify-capture-e2e-test.js +101 -101
- package/_site/_test/codex-notify-fanout-test.js +134 -134
- package/_site/_test/codex-notify-upgrade-test.js +253 -253
- package/_site/_test/commit-boundary-freeze-test.js +147 -147
- package/_site/_test/commit-conversation-binding-test.js +4 -4
- package/_site/_test/configuration-generation-test.js +22 -22
- package/_site/_test/context-pack-test.js +200 -200
- package/_site/_test/conversation-api-test.js +10 -10
- package/_site/_test/conversation-origin-test.js +15 -0
- package/_site/_test/cross-repo-isolation-e2e-test.js +187 -187
- package/_site/_test/embedding-config-test.js +32 -32
- package/_site/_test/fixtures/fake-browser-exit.js +10 -10
- package/_site/_test/fixtures/fake-browser-hang.js +10 -10
- package/_site/_test/fixtures/integration-busy-worker.cjs +6 -6
- package/_site/_test/fixtures/make-git-repos.js +92 -92
- package/_site/_test/fixtures/retrieval-gold-queries.json +32 -0
- package/_site/_test/fresh-hook-reimport-test.js +53 -53
- package/_site/_test/fresh-product-data-test.js +56 -56
- package/_site/_test/git-path-compatibility-test.js +80 -80
- package/_site/_test/git-validation-test.js +81 -81
- package/_site/_test/hook-worktree-test.js +42 -42
- package/_site/_test/index-builder-test.js +41 -41
- package/_site/_test/index-error-display-contract-test.js +10 -10
- package/_site/_test/integration-adapters-test.js +67 -67
- package/_site/_test/integration-capture-setup-test.js +118 -118
- package/_site/_test/integration-runtime-binding-a2-test.js +566 -566
- package/_site/_test/integration-runtime-binding-a3-test.js +227 -227
- package/_site/_test/integration-runtime-binding-test.js +473 -473
- package/_site/_test/integration-status-scan-test.js +29 -29
- package/_site/_test/kb-framework-test.js +57 -57
- package/_site/_test/kbpath-follow-test.js +63 -63
- package/_site/_test/knowledge-db-test.js +144 -144
- package/_site/_test/knowledge-engine-client-test.js +109 -109
- package/_site/_test/knowledge-maintenance-test.js +40 -40
- package/_site/_test/knowledge-panel-ui-contract-test.js +112 -0
- package/_site/_test/knowledge-scopes-test.js +37 -37
- package/_site/_test/knowledge-storage-location-test.js +28 -28
- package/_site/_test/launcher-readiness-test.js +80 -80
- package/_site/_test/legacy-claude-session-alias-test.js +106 -106
- package/_site/_test/legacy-routes-removed-test.js +47 -47
- package/_site/_test/legacy-workbench-exclusion-test.js +92 -92
- package/_site/_test/live-gateway-test.js +48 -48
- package/_site/_test/local-marketplace-test.js +16 -16
- package/_site/_test/log-redaction-test.js +58 -58
- package/_site/_test/markdown-maintenance-test.js +11 -11
- package/_site/_test/model-context-windows-test.js +89 -89
- package/_site/_test/module-agent-start-proxy-test.js +65 -65
- package/_site/_test/module-artifact-integrity-test.js +27 -27
- package/_site/_test/module-boundary-test.js +30 -30
- package/_site/_test/module-bridge-eventbridge-test.js +202 -202
- package/_site/_test/module-dynamic-port-test.js +118 -118
- package/_site/_test/module-index-proxy-test.js +33 -0
- package/_site/_test/module-model-configuration-test.js +44 -44
- package/_site/_test/module-process-lifecycle-test.js +35 -35
- package/_site/_test/module-stream-proxy-test.js +34 -34
- package/_site/_test/module-terminal-restart-test.js +63 -0
- package/_site/_test/module-wiring-unbind-race-test.js +59 -59
- package/_site/_test/native-context-integrations-test.js +86 -86
- package/_site/_test/orphaned-claim-recovery-test.js +80 -80
- package/_site/_test/package-boundary-test.js +43 -42
- package/_site/_test/package-startup-test.js +56 -56
- package/_site/_test/packaged-ui-smoke-test.js +88 -88
- package/_site/_test/pr-consumer-contract-test.js +55 -55
- package/_site/_test/product-brand-contract-test.js +25 -25
- package/_site/_test/product-diagnostics-ui-test.js +93 -93
- package/_site/_test/product-import-ui-test.js +104 -104
- package/_site/_test/product-progress-status-test.js +12 -1
- package/_site/_test/project-remove-running-guard-test.js +79 -79
- package/_site/_test/project-store-test.js +90 -90
- package/_site/_test/registry-events-test.js +38 -0
- package/_site/_test/release-version-sync-test.js +115 -115
- package/_site/_test/requirement-binding-test.js +113 -113
- package/_site/_test/runtime-endpoint-test.js +64 -64
- package/_site/_test/scanner-test.js +83 -83
- package/_site/_test/server-security-test.js +96 -96
- package/_site/_test/sessions-stream-test.js +93 -93
- package/_site/_test/shared-scope-root-binding-test.js +67 -67
- package/_site/_test/simple-import-test.js +98 -98
- package/_site/_test/startup-analysis-disabled-test.js +6 -6
- package/_site/_test/team-knowledge-store-sparse-checkout-test.js +196 -196
- package/_site/_test/team-knowledge-store-test.js +132 -132
- package/_site/_test/test-server-cleanup-test.js +35 -35
- package/_site/_test/test-server-module-isolation-test.js +37 -37
- package/_site/_test/tracking-start-test.js +35 -35
- package/_site/_test/workbench-context-receipts-test.js +22 -0
- package/_site/_test/workspace-ui-contract-test.js +31 -31
- package/_site/_test/zcode-capture-integration-test.js +108 -108
- package/_site/lib/ai-vendor-presets.js +83 -83
- package/_site/lib/ai-workspace.js +3 -3
- package/_site/lib/atomic-file.js +204 -204
- package/_site/lib/bridge-adapter.js +260 -260
- package/_site/lib/capture-hook-config.js +108 -108
- package/_site/lib/claude-cli-runner.js +89 -89
- package/_site/lib/cli-instances.js +43 -43
- package/_site/lib/codex-context-status.js +17 -17
- package/_site/lib/codex-notify-config.js +193 -193
- package/_site/lib/commit-conversation-binder.js +13 -12
- package/_site/lib/commit-reconciler.js +38 -38
- package/_site/lib/context-pack-builder.js +299 -299
- package/_site/lib/conversation-exclusions.js +204 -204
- package/_site/lib/conversation-query-service.js +17 -17
- package/_site/lib/conversation-store.js +2 -0
- package/_site/lib/data-dir.js +13 -13
- package/_site/lib/embedding-service.js +85 -85
- package/_site/lib/engine-connection.js +93 -90
- package/_site/lib/engine-knowledge-client.js +72 -72
- package/_site/lib/folder-picker-output.js +3 -3
- package/_site/lib/git-path.js +14 -14
- package/_site/lib/github-team-store.js +3 -3
- package/_site/lib/index-builder.js +157 -157
- package/_site/lib/integration-status-scan.js +99 -99
- package/_site/lib/integration-status-worker.js +14 -14
- package/_site/lib/kb-framework.js +15 -15
- package/_site/lib/knowledge-db.js +363 -363
- package/_site/lib/knowledge-schema.js +121 -121
- package/_site/lib/live-gateway.js +78 -76
- package/_site/lib/llm-client.js +12 -12
- package/_site/lib/model-context-windows.js +82 -82
- package/_site/lib/module-bridge.js +874 -865
- package/_site/lib/module-port-resolver.js +57 -57
- package/_site/lib/post-commit-automation.js +54 -54
- package/_site/lib/product-data.js +35 -35
- package/_site/lib/project-lifecycle-service.js +14 -14
- package/_site/lib/project-registry-store.js +129 -129
- package/_site/lib/prompt-registry.js +148 -148
- package/_site/lib/registry-events.js +16 -0
- package/_site/lib/requirement-adapters.js +10 -10
- package/_site/lib/requirement-binder.js +112 -112
- package/_site/lib/runtime-control.js +89 -89
- package/_site/lib/runtime-endpoint.js +5 -5
- package/_site/lib/server-app.js +10 -1
- package/_site/lib/zcode-context-integration.js +60 -60
- package/_site/scripts/codex-notify-fanout.cjs +95 -95
- package/_site/scripts/install-module-candidates.js +69 -69
- package/_site/scripts/pack-module-candidates.js +89 -72
- package/_site/scripts/sync-product-content.js +28 -28
- package/_site/scripts/sync-release-version.js +164 -164
- package/_site/scripts/vendor-modules.js +49 -49
- package/_site/scripts/verify-module-runtime.js +50 -50
- package/_site/scripts/verify-product-runtime.js +36 -36
- package/_site/scripts/verify-retrieval-gold.js +83 -0
- package/_site/server.js +15 -15
- package/_site/start.bat +26 -26
- package/_site/stop.bat +11 -11
- package/bin/project-knowledge-integrations.js +254 -254
- package/bin/project-knowledge-kb.js +67 -67
- package/bin/project-knowledge-mcp.js +8 -8
- package/bin/project-knowledge.js +170 -170
- package/claude-prompts.json +21 -21
- package/docs/README.zh-CN.md +205 -205
- package/docs/assets/architecture.svg +181 -181
- package/docs/assets/logo.svg +49 -49
- package/docs/fresh-v5-start.md +42 -42
- package/docs/leader-glm-workbench-playbook.md +122 -119
- package/docs/project-registry-schema.md +11 -11
- package/docs/release-5.0.16-acceptance.md +176 -176
- package/docs/release-5.0.17-acceptance.md +117 -117
- package/docs/release-5.0.19-acceptance.md +39 -0
- package/docs/team-knowledge-mode-a-plan.md +73 -73
- package/module-runtime-manifest.json +126 -104
- package/package.json +110 -108
- 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/plugins/project-knowledge/opencode/project-knowledge.md +3 -3
- package/plugins/project-knowledge/skills/project-knowledge/SKILL.md +28 -28
- package/templates/change.md +30 -30
- package/templates/project-readme.md +18 -18
- package/ui/index.html +1 -1
- package/ui/knowledge-panel.html +118 -0
- package/ui/product.css +8 -0
- package/ui/product.js +175 -8
- package/vendor-manifest.json +55 -53
|
@@ -1,108 +1,108 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const os = require('os');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const crypto = require('crypto');
|
|
7
|
-
const { writeFileAtomic } = require('./atomic-file');
|
|
8
|
-
|
|
9
|
-
const EVENTS = ['UserPromptSubmit', 'Stop'];
|
|
10
|
-
function normalized(file) {
|
|
11
|
-
const value = path.resolve(file).replace(/\\/g, '/');
|
|
12
|
-
return process.platform === 'win32' ? value.toLowerCase() : value;
|
|
13
|
-
}
|
|
14
|
-
function nodeCommand(command) { return /(?:^|[\\/])node(?:\.exe)?$/i.test(command || ''); }
|
|
15
|
-
|
|
16
|
-
// Only exact, known Bridge invocations are ours. A shell wrapper, extra
|
|
17
|
-
// argument or a lookalike filename never authorizes removing a user hook.
|
|
18
|
-
function bridgeBinding(args, options) {
|
|
19
|
-
if (!Array.isArray(args) || typeof args[0] !== 'string' || !nodeCommand(args[0]) || typeof args[1] !== 'string') return '';
|
|
20
|
-
if (args.length !== 2 && !(args.length === 4 && args[2] === '--source' && ['claude-code', 'claude', 'zcode', 'codex', 'opencode'].includes(args[3]))) return '';
|
|
21
|
-
const actual = normalized(args[1]);
|
|
22
|
-
const current = normalized(path.join(options.bridgeHomeDir, 'bin', 'bridge-hook.cjs'));
|
|
23
|
-
const legacy = normalized(path.join(options.homeDir || os.homedir(), '.ai-coding-event-bridge', 'bin', 'bridge-hook.cjs'));
|
|
24
|
-
return actual === current ? 'current' : actual === legacy ? 'legacy' : '';
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function hookArgs(hook) {
|
|
28
|
-
if (!hook || typeof hook !== 'object') return null;
|
|
29
|
-
if (hook.type === 'process' && Array.isArray(hook.args)) return [hook.command, ...hook.args];
|
|
30
|
-
if (hook.type !== 'command' || typeof hook.command !== 'string') return null;
|
|
31
|
-
const match = hook.command.trim().match(/^(?:"([^"]+)"|'([^']+)'|(\S+))\s+(?:"([^"]+)"|'([^']+)'|(\S+))(?:\s+--source\s+(claude-code|claude|zcode|codex|opencode))?$/);
|
|
32
|
-
return match ? [match[1] || match[2] || match[3], match[4] || match[5] || match[6], ...(match[7] ? ['--source', match[7]] : [])] : null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function configuration(file) {
|
|
36
|
-
const source = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
37
|
-
let config;
|
|
38
|
-
try { config = source ? JSON.parse(source) : {}; } catch { throw new Error('Agent 配置不是有效 JSON,未修改。'); }
|
|
39
|
-
if (!config || typeof config !== 'object' || Array.isArray(config)) throw new Error('Agent 配置必须是 JSON 对象,未修改。');
|
|
40
|
-
return { source, config };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function planCaptureHooks(file, client, options, removeCurrent = false) {
|
|
44
|
-
if (!['claude', 'zcode'].includes(client)) throw new Error('不支持的采集配置类型。');
|
|
45
|
-
const { source, config } = configuration(file);
|
|
46
|
-
const hooks = config.hooks;
|
|
47
|
-
if (hooks != null && (typeof hooks !== 'object' || Array.isArray(hooks))) throw new Error('Agent hooks 配置格式异常,未修改。');
|
|
48
|
-
const events = client === 'zcode' ? hooks?.events : hooks;
|
|
49
|
-
if (events != null && (typeof events !== 'object' || Array.isArray(events))) throw new Error('Agent 事件配置格式异常,未修改。');
|
|
50
|
-
const removed = [], counts = {};
|
|
51
|
-
for (const event of EVENTS) {
|
|
52
|
-
const groups = events?.[event];
|
|
53
|
-
if (groups == null) { counts[event] = { current: 0, legacy: 0 }; continue; }
|
|
54
|
-
if (!Array.isArray(groups)) throw new Error('Agent 事件列表格式异常,未修改。');
|
|
55
|
-
const count = { current: 0, legacy: 0 };
|
|
56
|
-
for (const group of groups) {
|
|
57
|
-
if (group?.hooks != null && !Array.isArray(group.hooks)) throw new Error('Agent Hook 列表格式异常,未修改。');
|
|
58
|
-
for (const hook of group?.hooks || []) { const binding = bridgeBinding(hookArgs(hook), options); if (binding) count[binding]++; }
|
|
59
|
-
}
|
|
60
|
-
counts[event] = count;
|
|
61
|
-
// Materialize the replacement first; never remove the only working old
|
|
62
|
-
// capture when installing the new runtime has failed.
|
|
63
|
-
if (!count.current) continue;
|
|
64
|
-
let keptCurrent = false;
|
|
65
|
-
const removedGroup = Symbol('empty-owned-group');
|
|
66
|
-
events[event] = groups.map((group, groupIndex) => {
|
|
67
|
-
if (!Array.isArray(group?.hooks)) return group;
|
|
68
|
-
const kept = group.hooks.filter((hook, hookIndex) => {
|
|
69
|
-
const binding = bridgeBinding(hookArgs(hook), options);
|
|
70
|
-
if (!binding) return true;
|
|
71
|
-
if (removeCurrent && binding === 'legacy') return true;
|
|
72
|
-
if (!removeCurrent && binding === 'current' && !keptCurrent) { keptCurrent = true; return true; }
|
|
73
|
-
// Back up only owned invocation fields, never unrelated config/env.
|
|
74
|
-
const saved = Object.fromEntries(['type', 'command', 'args', 'timeout', 'timeoutMs'].filter(key => hook[key] !== undefined).map(key => [key, hook[key]]));
|
|
75
|
-
removed.push({ event, groupIndex, hookIndex, hook: saved, binding });
|
|
76
|
-
return false;
|
|
77
|
-
});
|
|
78
|
-
return kept.length === group.hooks.length ? group : kept.length ? { ...group, hooks: kept } : removedGroup;
|
|
79
|
-
}).filter(group => group !== removedGroup);
|
|
80
|
-
}
|
|
81
|
-
return { source, config, counts, removed, repairNeeded: Object.values(counts).some(count => count.legacy > 0 || count.current > 1) };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function writePlan(file, client, plan) {
|
|
85
|
-
if (!plan.removed.length) return { changed: false, removedLegacy: 0, removedDuplicates: 0 };
|
|
86
|
-
const assertUnchanged = () => {
|
|
87
|
-
if (fs.readFileSync(file, 'utf8') !== plan.source) throw Object.assign(new Error('Agent 配置已被其他程序修改,请重试接入。'), { code: 'CONFIG_CHANGED' });
|
|
88
|
-
};
|
|
89
|
-
assertUnchanged();
|
|
90
|
-
const backupFile = `${file}.project-knowledge-capture-${Date.now()}-${crypto.randomBytes(4).toString('hex')}.bak`;
|
|
91
|
-
writeFileAtomic(backupFile, JSON.stringify({ schema: 'project-knowledge/capture-backup/v1', client, removed: plan.removed }, null, 2) + '\n');
|
|
92
|
-
const eol = plan.source.includes('\r\n') ? '\r\n' : '\n';
|
|
93
|
-
writeFileAtomic(file, (JSON.stringify(plan.config, null, 2) + '\n').replace(/\n/g, eol), { beforeRename: assertUnchanged });
|
|
94
|
-
return { changed: true, backupFile, removedLegacy: plan.removed.filter(row => row.binding === 'legacy').length, removedDuplicates: plan.removed.filter(row => row.binding === 'current').length };
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function repairCaptureHooks(file, client, options) {
|
|
98
|
-
const plan = planCaptureHooks(file, client, options);
|
|
99
|
-
if (EVENTS.some(event => !plan.counts[event].current)) throw new Error('当前 Bridge 入口未完整建立,保留旧采集接线。');
|
|
100
|
-
if (!fs.existsSync(path.join(options.bridgeHomeDir, 'bin', 'bridge-hook.cjs'))) throw new Error('当前 Bridge 运行入口缺失,保留旧采集接线。');
|
|
101
|
-
return writePlan(file, client, plan);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function removeCaptureHooks(file, client, options) {
|
|
105
|
-
return writePlan(file, client, planCaptureHooks(file, client, options, true));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
module.exports = { bridgeBinding, planCaptureHooks, repairCaptureHooks, removeCaptureHooks };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
const { writeFileAtomic } = require('./atomic-file');
|
|
8
|
+
|
|
9
|
+
const EVENTS = ['UserPromptSubmit', 'Stop'];
|
|
10
|
+
function normalized(file) {
|
|
11
|
+
const value = path.resolve(file).replace(/\\/g, '/');
|
|
12
|
+
return process.platform === 'win32' ? value.toLowerCase() : value;
|
|
13
|
+
}
|
|
14
|
+
function nodeCommand(command) { return /(?:^|[\\/])node(?:\.exe)?$/i.test(command || ''); }
|
|
15
|
+
|
|
16
|
+
// Only exact, known Bridge invocations are ours. A shell wrapper, extra
|
|
17
|
+
// argument or a lookalike filename never authorizes removing a user hook.
|
|
18
|
+
function bridgeBinding(args, options) {
|
|
19
|
+
if (!Array.isArray(args) || typeof args[0] !== 'string' || !nodeCommand(args[0]) || typeof args[1] !== 'string') return '';
|
|
20
|
+
if (args.length !== 2 && !(args.length === 4 && args[2] === '--source' && ['claude-code', 'claude', 'zcode', 'codex', 'opencode'].includes(args[3]))) return '';
|
|
21
|
+
const actual = normalized(args[1]);
|
|
22
|
+
const current = normalized(path.join(options.bridgeHomeDir, 'bin', 'bridge-hook.cjs'));
|
|
23
|
+
const legacy = normalized(path.join(options.homeDir || os.homedir(), '.ai-coding-event-bridge', 'bin', 'bridge-hook.cjs'));
|
|
24
|
+
return actual === current ? 'current' : actual === legacy ? 'legacy' : '';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function hookArgs(hook) {
|
|
28
|
+
if (!hook || typeof hook !== 'object') return null;
|
|
29
|
+
if (hook.type === 'process' && Array.isArray(hook.args)) return [hook.command, ...hook.args];
|
|
30
|
+
if (hook.type !== 'command' || typeof hook.command !== 'string') return null;
|
|
31
|
+
const match = hook.command.trim().match(/^(?:"([^"]+)"|'([^']+)'|(\S+))\s+(?:"([^"]+)"|'([^']+)'|(\S+))(?:\s+--source\s+(claude-code|claude|zcode|codex|opencode))?$/);
|
|
32
|
+
return match ? [match[1] || match[2] || match[3], match[4] || match[5] || match[6], ...(match[7] ? ['--source', match[7]] : [])] : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function configuration(file) {
|
|
36
|
+
const source = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
37
|
+
let config;
|
|
38
|
+
try { config = source ? JSON.parse(source) : {}; } catch { throw new Error('Agent 配置不是有效 JSON,未修改。'); }
|
|
39
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) throw new Error('Agent 配置必须是 JSON 对象,未修改。');
|
|
40
|
+
return { source, config };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function planCaptureHooks(file, client, options, removeCurrent = false) {
|
|
44
|
+
if (!['claude', 'zcode'].includes(client)) throw new Error('不支持的采集配置类型。');
|
|
45
|
+
const { source, config } = configuration(file);
|
|
46
|
+
const hooks = config.hooks;
|
|
47
|
+
if (hooks != null && (typeof hooks !== 'object' || Array.isArray(hooks))) throw new Error('Agent hooks 配置格式异常,未修改。');
|
|
48
|
+
const events = client === 'zcode' ? hooks?.events : hooks;
|
|
49
|
+
if (events != null && (typeof events !== 'object' || Array.isArray(events))) throw new Error('Agent 事件配置格式异常,未修改。');
|
|
50
|
+
const removed = [], counts = {};
|
|
51
|
+
for (const event of EVENTS) {
|
|
52
|
+
const groups = events?.[event];
|
|
53
|
+
if (groups == null) { counts[event] = { current: 0, legacy: 0 }; continue; }
|
|
54
|
+
if (!Array.isArray(groups)) throw new Error('Agent 事件列表格式异常,未修改。');
|
|
55
|
+
const count = { current: 0, legacy: 0 };
|
|
56
|
+
for (const group of groups) {
|
|
57
|
+
if (group?.hooks != null && !Array.isArray(group.hooks)) throw new Error('Agent Hook 列表格式异常,未修改。');
|
|
58
|
+
for (const hook of group?.hooks || []) { const binding = bridgeBinding(hookArgs(hook), options); if (binding) count[binding]++; }
|
|
59
|
+
}
|
|
60
|
+
counts[event] = count;
|
|
61
|
+
// Materialize the replacement first; never remove the only working old
|
|
62
|
+
// capture when installing the new runtime has failed.
|
|
63
|
+
if (!count.current) continue;
|
|
64
|
+
let keptCurrent = false;
|
|
65
|
+
const removedGroup = Symbol('empty-owned-group');
|
|
66
|
+
events[event] = groups.map((group, groupIndex) => {
|
|
67
|
+
if (!Array.isArray(group?.hooks)) return group;
|
|
68
|
+
const kept = group.hooks.filter((hook, hookIndex) => {
|
|
69
|
+
const binding = bridgeBinding(hookArgs(hook), options);
|
|
70
|
+
if (!binding) return true;
|
|
71
|
+
if (removeCurrent && binding === 'legacy') return true;
|
|
72
|
+
if (!removeCurrent && binding === 'current' && !keptCurrent) { keptCurrent = true; return true; }
|
|
73
|
+
// Back up only owned invocation fields, never unrelated config/env.
|
|
74
|
+
const saved = Object.fromEntries(['type', 'command', 'args', 'timeout', 'timeoutMs'].filter(key => hook[key] !== undefined).map(key => [key, hook[key]]));
|
|
75
|
+
removed.push({ event, groupIndex, hookIndex, hook: saved, binding });
|
|
76
|
+
return false;
|
|
77
|
+
});
|
|
78
|
+
return kept.length === group.hooks.length ? group : kept.length ? { ...group, hooks: kept } : removedGroup;
|
|
79
|
+
}).filter(group => group !== removedGroup);
|
|
80
|
+
}
|
|
81
|
+
return { source, config, counts, removed, repairNeeded: Object.values(counts).some(count => count.legacy > 0 || count.current > 1) };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function writePlan(file, client, plan) {
|
|
85
|
+
if (!plan.removed.length) return { changed: false, removedLegacy: 0, removedDuplicates: 0 };
|
|
86
|
+
const assertUnchanged = () => {
|
|
87
|
+
if (fs.readFileSync(file, 'utf8') !== plan.source) throw Object.assign(new Error('Agent 配置已被其他程序修改,请重试接入。'), { code: 'CONFIG_CHANGED' });
|
|
88
|
+
};
|
|
89
|
+
assertUnchanged();
|
|
90
|
+
const backupFile = `${file}.project-knowledge-capture-${Date.now()}-${crypto.randomBytes(4).toString('hex')}.bak`;
|
|
91
|
+
writeFileAtomic(backupFile, JSON.stringify({ schema: 'project-knowledge/capture-backup/v1', client, removed: plan.removed }, null, 2) + '\n');
|
|
92
|
+
const eol = plan.source.includes('\r\n') ? '\r\n' : '\n';
|
|
93
|
+
writeFileAtomic(file, (JSON.stringify(plan.config, null, 2) + '\n').replace(/\n/g, eol), { beforeRename: assertUnchanged });
|
|
94
|
+
return { changed: true, backupFile, removedLegacy: plan.removed.filter(row => row.binding === 'legacy').length, removedDuplicates: plan.removed.filter(row => row.binding === 'current').length };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function repairCaptureHooks(file, client, options) {
|
|
98
|
+
const plan = planCaptureHooks(file, client, options);
|
|
99
|
+
if (EVENTS.some(event => !plan.counts[event].current)) throw new Error('当前 Bridge 入口未完整建立,保留旧采集接线。');
|
|
100
|
+
if (!fs.existsSync(path.join(options.bridgeHomeDir, 'bin', 'bridge-hook.cjs'))) throw new Error('当前 Bridge 运行入口缺失,保留旧采集接线。');
|
|
101
|
+
return writePlan(file, client, plan);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function removeCaptureHooks(file, client, options) {
|
|
105
|
+
return writePlan(file, client, planCaptureHooks(file, client, options, true));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = { bridgeBinding, planCaptureHooks, repairCaptureHooks, removeCaptureHooks };
|
|
@@ -188,12 +188,12 @@ function toPersistedSession(session) {
|
|
|
188
188
|
aiProfileId: session.aiProfileId || null,
|
|
189
189
|
claudeSessionId: session.claudeSessionId,
|
|
190
190
|
source: session.source || 'manual',
|
|
191
|
-
automation: !!session.automation,
|
|
192
|
-
automationRunId: session.automationRunId || null,
|
|
193
|
-
metadata: session.metadata || {},
|
|
194
|
-
legacyProjectSlug: session.legacyProjectSlug || null,
|
|
195
|
-
legacySource: session.legacySource === true,
|
|
196
|
-
permissionMode: session.permissionMode || 'default',
|
|
191
|
+
automation: !!session.automation,
|
|
192
|
+
automationRunId: session.automationRunId || null,
|
|
193
|
+
metadata: session.metadata || {},
|
|
194
|
+
legacyProjectSlug: session.legacyProjectSlug || null,
|
|
195
|
+
legacySource: session.legacySource === true,
|
|
196
|
+
permissionMode: session.permissionMode || 'default',
|
|
197
197
|
startedAt: session.startedAt,
|
|
198
198
|
endedAt: session.endedAt,
|
|
199
199
|
exitCode: session.exitCode,
|
|
@@ -229,63 +229,63 @@ function readPersistedRecord(file) {
|
|
|
229
229
|
return null;
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
function scanPersistedRecords(projectSlug = null) {
|
|
233
|
-
let projectDirs = [];
|
|
234
|
-
try {
|
|
235
|
-
const registry = projectRegistryStore.read({ allowMissing: true });
|
|
236
|
-
projectDirs = registry.projectOrder
|
|
237
|
-
.filter(projectId => !projectSlug || projectId === projectSlug)
|
|
238
|
-
.map(projectId => {
|
|
239
|
-
const config = projectStore.readConfig(projectId);
|
|
240
|
-
const legacySlug = config.legacyExtensions && typeof config.legacyExtensions.slug === 'string'
|
|
241
|
-
? config.legacyExtensions.slug.trim()
|
|
242
|
-
: '';
|
|
243
|
-
return {
|
|
244
|
-
name: projectId,
|
|
245
|
-
kbPath: config.knowledgePath,
|
|
246
|
-
legacySlugs: legacySlug && legacySlug !== projectId ? [legacySlug] : [],
|
|
247
|
-
};
|
|
248
|
-
});
|
|
249
|
-
} catch { return []; }
|
|
250
|
-
const records = new Map();
|
|
251
|
-
for (const entry of projectDirs) {
|
|
252
|
-
const dirs = [
|
|
253
|
-
{ path: storageLayout.getRuntimePath('claude-sessions', entry.name), legacySlug: null },
|
|
254
|
-
{ path: path.join(entry.kbPath, '_ai', WORKBENCH_DIR), legacySlug: null },
|
|
255
|
-
];
|
|
256
|
-
try { dirs.push({ path: path.join(aiWorkspace.projectAIPath(entry.name), WORKBENCH_DIR), legacySlug: null }); } catch {
|
|
257
|
-
// The legacy workspace location is optional during v2 session discovery.
|
|
258
|
-
}
|
|
259
|
-
for (const legacySlug of entry.legacySlugs) {
|
|
260
|
-
try { dirs.push({ path: path.join(aiWorkspace.projectAIPath(legacySlug), WORKBENCH_DIR), legacySlug }); } catch {
|
|
261
|
-
// Invalid migration metadata is ignored without hiding canonical v2 sessions.
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
const visited = new Set();
|
|
265
|
-
for (const source of dirs) {
|
|
266
|
-
const dir = path.resolve(source.path);
|
|
267
|
-
if (visited.has(dir)) continue;
|
|
268
|
-
visited.add(dir);
|
|
269
|
-
let files = [];
|
|
270
|
-
try { files = fs.readdirSync(dir); } catch { continue; }
|
|
271
|
-
for (const file of files) {
|
|
272
|
-
if (!file.endsWith('.json')) continue;
|
|
273
|
-
const record = readPersistedRecord(path.join(dir, file));
|
|
274
|
-
if (!record || records.has(record.sessionId)) continue;
|
|
275
|
-
const acceptedSlugs = new Set([entry.name, ...entry.legacySlugs]);
|
|
276
|
-
if (record.projectSlug && !acceptedSlugs.has(record.projectSlug)) continue;
|
|
277
|
-
const originalProjectSlug = record.projectSlug || '';
|
|
278
|
-
records.set(record.sessionId, {
|
|
279
|
-
...record,
|
|
280
|
-
projectSlug: entry.name,
|
|
281
|
-
legacyProjectSlug: source.legacySlug || (originalProjectSlug !== entry.name ? originalProjectSlug : '') || record.legacyProjectSlug || null,
|
|
282
|
-
legacySource: source.legacySlug != null || originalProjectSlug !== entry.name || record.legacySource === true,
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
return [...records.values()].sort((a, b) => String(b.updatedAt || b.startedAt || '').localeCompare(String(a.updatedAt || a.startedAt || '')));
|
|
288
|
-
}
|
|
232
|
+
function scanPersistedRecords(projectSlug = null) {
|
|
233
|
+
let projectDirs = [];
|
|
234
|
+
try {
|
|
235
|
+
const registry = projectRegistryStore.read({ allowMissing: true });
|
|
236
|
+
projectDirs = registry.projectOrder
|
|
237
|
+
.filter(projectId => !projectSlug || projectId === projectSlug)
|
|
238
|
+
.map(projectId => {
|
|
239
|
+
const config = projectStore.readConfig(projectId);
|
|
240
|
+
const legacySlug = config.legacyExtensions && typeof config.legacyExtensions.slug === 'string'
|
|
241
|
+
? config.legacyExtensions.slug.trim()
|
|
242
|
+
: '';
|
|
243
|
+
return {
|
|
244
|
+
name: projectId,
|
|
245
|
+
kbPath: config.knowledgePath,
|
|
246
|
+
legacySlugs: legacySlug && legacySlug !== projectId ? [legacySlug] : [],
|
|
247
|
+
};
|
|
248
|
+
});
|
|
249
|
+
} catch { return []; }
|
|
250
|
+
const records = new Map();
|
|
251
|
+
for (const entry of projectDirs) {
|
|
252
|
+
const dirs = [
|
|
253
|
+
{ path: storageLayout.getRuntimePath('claude-sessions', entry.name), legacySlug: null },
|
|
254
|
+
{ path: path.join(entry.kbPath, '_ai', WORKBENCH_DIR), legacySlug: null },
|
|
255
|
+
];
|
|
256
|
+
try { dirs.push({ path: path.join(aiWorkspace.projectAIPath(entry.name), WORKBENCH_DIR), legacySlug: null }); } catch {
|
|
257
|
+
// The legacy workspace location is optional during v2 session discovery.
|
|
258
|
+
}
|
|
259
|
+
for (const legacySlug of entry.legacySlugs) {
|
|
260
|
+
try { dirs.push({ path: path.join(aiWorkspace.projectAIPath(legacySlug), WORKBENCH_DIR), legacySlug }); } catch {
|
|
261
|
+
// Invalid migration metadata is ignored without hiding canonical v2 sessions.
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const visited = new Set();
|
|
265
|
+
for (const source of dirs) {
|
|
266
|
+
const dir = path.resolve(source.path);
|
|
267
|
+
if (visited.has(dir)) continue;
|
|
268
|
+
visited.add(dir);
|
|
269
|
+
let files = [];
|
|
270
|
+
try { files = fs.readdirSync(dir); } catch { continue; }
|
|
271
|
+
for (const file of files) {
|
|
272
|
+
if (!file.endsWith('.json')) continue;
|
|
273
|
+
const record = readPersistedRecord(path.join(dir, file));
|
|
274
|
+
if (!record || records.has(record.sessionId)) continue;
|
|
275
|
+
const acceptedSlugs = new Set([entry.name, ...entry.legacySlugs]);
|
|
276
|
+
if (record.projectSlug && !acceptedSlugs.has(record.projectSlug)) continue;
|
|
277
|
+
const originalProjectSlug = record.projectSlug || '';
|
|
278
|
+
records.set(record.sessionId, {
|
|
279
|
+
...record,
|
|
280
|
+
projectSlug: entry.name,
|
|
281
|
+
legacyProjectSlug: source.legacySlug || (originalProjectSlug !== entry.name ? originalProjectSlug : '') || record.legacyProjectSlug || null,
|
|
282
|
+
legacySource: source.legacySlug != null || originalProjectSlug !== entry.name || record.legacySource === true,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return [...records.values()].sort((a, b) => String(b.updatedAt || b.startedAt || '').localeCompare(String(a.updatedAt || a.startedAt || '')));
|
|
288
|
+
}
|
|
289
289
|
|
|
290
290
|
function findPersistedRecord(sessionId) {
|
|
291
291
|
return scanPersistedRecords().find(record => record.sessionId === sessionId) || null;
|
|
@@ -343,13 +343,13 @@ function restoreSessionFromDisk(sessionId) {
|
|
|
343
343
|
pendingToolApproval: null,
|
|
344
344
|
restored: true,
|
|
345
345
|
historyCleared: !inheritConversation,
|
|
346
|
-
source: record.source || 'manual',
|
|
347
|
-
metadata: record.metadata || {},
|
|
348
|
-
automation: !!record.automation,
|
|
349
|
-
automationRunId: record.automationRunId || null,
|
|
350
|
-
legacyProjectSlug: record.legacyProjectSlug || null,
|
|
351
|
-
legacySource: record.legacySource === true,
|
|
352
|
-
permissionMode: record.permissionMode || 'default',
|
|
346
|
+
source: record.source || 'manual',
|
|
347
|
+
metadata: record.metadata || {},
|
|
348
|
+
automation: !!record.automation,
|
|
349
|
+
automationRunId: record.automationRunId || null,
|
|
350
|
+
legacyProjectSlug: record.legacyProjectSlug || null,
|
|
351
|
+
legacySource: record.legacySource === true,
|
|
352
|
+
permissionMode: record.permissionMode || 'default',
|
|
353
353
|
safetyPolicy: null,
|
|
354
354
|
};
|
|
355
355
|
sessions.set(sessionId, session);
|
|
@@ -491,12 +491,12 @@ function sessionSummary(s) {
|
|
|
491
491
|
model: s.model,
|
|
492
492
|
aiProfileId: s.aiProfileId || null,
|
|
493
493
|
claudeSessionId: s.claudeSessionId,
|
|
494
|
-
source: s.source || 'manual',
|
|
495
|
-
automation: !!s.automation,
|
|
496
|
-
automationRunId: s.automationRunId || null,
|
|
497
|
-
legacyProjectSlug: s.legacyProjectSlug || null,
|
|
498
|
-
legacySource: s.legacySource === true,
|
|
499
|
-
permissionMode: s.permissionMode || 'default',
|
|
494
|
+
source: s.source || 'manual',
|
|
495
|
+
automation: !!s.automation,
|
|
496
|
+
automationRunId: s.automationRunId || null,
|
|
497
|
+
legacyProjectSlug: s.legacyProjectSlug || null,
|
|
498
|
+
legacySource: s.legacySource === true,
|
|
499
|
+
permissionMode: s.permissionMode || 'default',
|
|
500
500
|
startedAt: s.startedAt,
|
|
501
501
|
endedAt: s.endedAt,
|
|
502
502
|
exitCode: s.exitCode,
|
|
@@ -1325,12 +1325,12 @@ function getState(sessionId) {
|
|
|
1325
1325
|
model: s.model,
|
|
1326
1326
|
aiProfileId: s.aiProfileId || null,
|
|
1327
1327
|
claudeSessionId: s.claudeSessionId,
|
|
1328
|
-
source: s.source || 'manual',
|
|
1329
|
-
automation: !!s.automation,
|
|
1330
|
-
automationRunId: s.automationRunId || null,
|
|
1331
|
-
legacyProjectSlug: s.legacyProjectSlug || null,
|
|
1332
|
-
legacySource: s.legacySource === true,
|
|
1333
|
-
permissionMode: s.permissionMode || 'default',
|
|
1328
|
+
source: s.source || 'manual',
|
|
1329
|
+
automation: !!s.automation,
|
|
1330
|
+
automationRunId: s.automationRunId || null,
|
|
1331
|
+
legacyProjectSlug: s.legacyProjectSlug || null,
|
|
1332
|
+
legacySource: s.legacySource === true,
|
|
1333
|
+
permissionMode: s.permissionMode || 'default',
|
|
1334
1334
|
startedAt: s.startedAt,
|
|
1335
1335
|
endedAt: s.endedAt,
|
|
1336
1336
|
exitCode: s.exitCode,
|
|
@@ -1506,13 +1506,13 @@ function demoteStaleActiveSessions(opts = {}) {
|
|
|
1506
1506
|
pendingToolApproval: null,
|
|
1507
1507
|
restored: true,
|
|
1508
1508
|
historyCleared: true,
|
|
1509
|
-
source: record.source || 'manual',
|
|
1510
|
-
metadata: record.metadata || {},
|
|
1511
|
-
automation: !!record.automation,
|
|
1512
|
-
automationRunId: record.automationRunId || null,
|
|
1513
|
-
legacyProjectSlug: record.legacyProjectSlug || null,
|
|
1514
|
-
legacySource: record.legacySource === true,
|
|
1515
|
-
permissionMode: record.permissionMode || 'default',
|
|
1509
|
+
source: record.source || 'manual',
|
|
1510
|
+
metadata: record.metadata || {},
|
|
1511
|
+
automation: !!record.automation,
|
|
1512
|
+
automationRunId: record.automationRunId || null,
|
|
1513
|
+
legacyProjectSlug: record.legacyProjectSlug || null,
|
|
1514
|
+
legacySource: record.legacySource === true,
|
|
1515
|
+
permissionMode: record.permissionMode || 'default',
|
|
1516
1516
|
safetyPolicy: null,
|
|
1517
1517
|
};
|
|
1518
1518
|
sessions.set(record.sessionId, rehydrated);
|
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const path = require('node:path');
|
|
3
|
-
const { execFileSync } = require('node:child_process');
|
|
4
|
-
const control = require('./runtime-control');
|
|
5
|
-
|
|
6
|
-
function url(info) { return `http://${info.host === '::1' ? '[::1]' : info.host}:${info.port}`; }
|
|
7
|
-
async function current(dataDir) {
|
|
8
|
-
try { const response = await control.request(dataDir, { action: 'status' }, 1500); return { ...response.instance, status: response.status }; }
|
|
9
|
-
catch (error) { if (['ENOENT', 'ECONNREFUSED', 'EPIPE'].includes(error.code)) return null; throw error; }
|
|
10
|
-
}
|
|
11
|
-
async function atPort(port) {
|
|
12
|
-
const response = await fetch(`http://127.0.0.1:${port}/api/runtime/identity`, { signal: AbortSignal.timeout(1200), headers: { Connection: 'close' } });
|
|
13
|
-
if (!response.ok) throw new Error(`端口 ${port} 不是可控制的知识库实例(HTTP ${response.status})。`);
|
|
14
|
-
const info = await response.json();
|
|
15
|
-
if (info.schema !== control.SCHEMA || info.name !== 'project-knowledge' || info.port !== port || !path.isAbsolute(info.dataDir || '')) throw new Error(`端口 ${port} 的实例身份无效,已拒绝控制。`);
|
|
16
|
-
const verified = await current(info.dataDir);
|
|
17
|
-
if (!verified || verified.pid !== info.pid || verified.port !== port || verified.startedAt !== info.startedAt) throw new Error(`端口 ${port} 的实例已变化,已拒绝控制。`);
|
|
18
|
-
return verified;
|
|
19
|
-
}
|
|
20
|
-
function listeningPorts() {
|
|
21
|
-
let output;
|
|
22
|
-
if (process.platform === 'win32') {
|
|
23
|
-
output = execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', "Get-NetTCPConnection -State Listen -ErrorAction Stop | Where-Object { $_.LocalAddress -in @('127.0.0.1','0.0.0.0','::1','::') } | Select-Object -ExpandProperty LocalPort -Unique"], { encoding: 'utf8', windowsHide: true, timeout: 10000 });
|
|
24
|
-
return [...new Set(output.split(/\s+/).map(Number).filter(n => Number.isInteger(n) && n > 0 && n <= 65535))];
|
|
25
|
-
}
|
|
26
|
-
if (process.platform === 'darwin') output = execFileSync('lsof', ['-nP', '-iTCP', '-sTCP:LISTEN', '-Fn'], { encoding: 'utf8', timeout: 5000 });
|
|
27
|
-
else output = execFileSync('ss', ['-tlnH'], { encoding: 'utf8', timeout: 5000 });
|
|
28
|
-
return [...new Set([...output.matchAll(/:(\d+)(?:\s|$)/gm)].map(match => Number(match[1])))];
|
|
29
|
-
}
|
|
30
|
-
async function all() {
|
|
31
|
-
const ports = listeningPorts(), instances = [], seen = new Set();
|
|
32
|
-
// Bounded read-only probes; no PID is ever killed by inventory discovery.
|
|
33
|
-
for (let i = 0; i < ports.length; i += 8) {
|
|
34
|
-
const found = await Promise.all(ports.slice(i, i + 8).map(port => atPort(port).catch(() => null)));
|
|
35
|
-
for (const info of found) if (info && !seen.has(info.pid + ':' + info.port)) { seen.add(info.pid + ':' + info.port); instances.push(info); }
|
|
36
|
-
}
|
|
37
|
-
return instances.sort((a, b) => a.port - b.port);
|
|
38
|
-
}
|
|
39
|
-
async function stop(info) {
|
|
40
|
-
const result = await control.request(info.dataDir, { action: 'stop', pid: info.pid, port: info.port, startedAt: info.startedAt });
|
|
41
|
-
if (result.status !== 'stopped') throw new Error('停止尚未确认。');
|
|
42
|
-
}
|
|
43
|
-
module.exports = { current, atPort, all, stop, url };
|
|
1
|
+
'use strict';
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const { execFileSync } = require('node:child_process');
|
|
4
|
+
const control = require('./runtime-control');
|
|
5
|
+
|
|
6
|
+
function url(info) { return `http://${info.host === '::1' ? '[::1]' : info.host}:${info.port}`; }
|
|
7
|
+
async function current(dataDir) {
|
|
8
|
+
try { const response = await control.request(dataDir, { action: 'status' }, 1500); return { ...response.instance, status: response.status }; }
|
|
9
|
+
catch (error) { if (['ENOENT', 'ECONNREFUSED', 'EPIPE'].includes(error.code)) return null; throw error; }
|
|
10
|
+
}
|
|
11
|
+
async function atPort(port) {
|
|
12
|
+
const response = await fetch(`http://127.0.0.1:${port}/api/runtime/identity`, { signal: AbortSignal.timeout(1200), headers: { Connection: 'close' } });
|
|
13
|
+
if (!response.ok) throw new Error(`端口 ${port} 不是可控制的知识库实例(HTTP ${response.status})。`);
|
|
14
|
+
const info = await response.json();
|
|
15
|
+
if (info.schema !== control.SCHEMA || info.name !== 'project-knowledge' || info.port !== port || !path.isAbsolute(info.dataDir || '')) throw new Error(`端口 ${port} 的实例身份无效,已拒绝控制。`);
|
|
16
|
+
const verified = await current(info.dataDir);
|
|
17
|
+
if (!verified || verified.pid !== info.pid || verified.port !== port || verified.startedAt !== info.startedAt) throw new Error(`端口 ${port} 的实例已变化,已拒绝控制。`);
|
|
18
|
+
return verified;
|
|
19
|
+
}
|
|
20
|
+
function listeningPorts() {
|
|
21
|
+
let output;
|
|
22
|
+
if (process.platform === 'win32') {
|
|
23
|
+
output = execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', "Get-NetTCPConnection -State Listen -ErrorAction Stop | Where-Object { $_.LocalAddress -in @('127.0.0.1','0.0.0.0','::1','::') } | Select-Object -ExpandProperty LocalPort -Unique"], { encoding: 'utf8', windowsHide: true, timeout: 10000 });
|
|
24
|
+
return [...new Set(output.split(/\s+/).map(Number).filter(n => Number.isInteger(n) && n > 0 && n <= 65535))];
|
|
25
|
+
}
|
|
26
|
+
if (process.platform === 'darwin') output = execFileSync('lsof', ['-nP', '-iTCP', '-sTCP:LISTEN', '-Fn'], { encoding: 'utf8', timeout: 5000 });
|
|
27
|
+
else output = execFileSync('ss', ['-tlnH'], { encoding: 'utf8', timeout: 5000 });
|
|
28
|
+
return [...new Set([...output.matchAll(/:(\d+)(?:\s|$)/gm)].map(match => Number(match[1])))];
|
|
29
|
+
}
|
|
30
|
+
async function all() {
|
|
31
|
+
const ports = listeningPorts(), instances = [], seen = new Set();
|
|
32
|
+
// Bounded read-only probes; no PID is ever killed by inventory discovery.
|
|
33
|
+
for (let i = 0; i < ports.length; i += 8) {
|
|
34
|
+
const found = await Promise.all(ports.slice(i, i + 8).map(port => atPort(port).catch(() => null)));
|
|
35
|
+
for (const info of found) if (info && !seen.has(info.pid + ':' + info.port)) { seen.add(info.pid + ':' + info.port); instances.push(info); }
|
|
36
|
+
}
|
|
37
|
+
return instances.sort((a, b) => a.port - b.port);
|
|
38
|
+
}
|
|
39
|
+
async function stop(info) {
|
|
40
|
+
const result = await control.request(info.dataDir, { action: 'stop', pid: info.pid, port: info.port, startedAt: info.startedAt });
|
|
41
|
+
if (result.status !== 'stopped') throw new Error('停止尚未确认。');
|
|
42
|
+
}
|
|
43
|
+
module.exports = { current, atPort, all, stop, url };
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Native read-only discovery, never trust-file edits or bypass flags. Reuse
|
|
4
|
-
// the module's verified stdio transport rather than a second RPC lifecycle.
|
|
5
|
-
async function readCodexHooks({ command, env, cwd }) {
|
|
6
|
-
const { createAppServerBridge } = require('claude-ai-workbench/packages/server/lib/bridges/app-server-bridge');
|
|
7
|
-
const { resolveCliInvocation } = require('claude-ai-workbench/packages/server/lib/drivers/cli-json-runner');
|
|
8
|
-
const bridge = createAppServerBridge({ command, args: ['app-server'], env, cwd, resolveInvocation: resolveCliInvocation, maxRestarts: 0, idleTimeoutMs: 0, requestTimeoutMs: 10000 });
|
|
9
|
-
try {
|
|
10
|
-
await bridge.start();
|
|
11
|
-
await bridge.request('initialize', { clientInfo: { name: 'project-knowledge-hook-status', version: '1' }, capabilities: { experimentalApi: true } });
|
|
12
|
-
await bridge.notify('initialized');
|
|
13
|
-
return await bridge.request('hooks/list', { cwds: [cwd] });
|
|
14
|
-
} finally { await bridge.close(); }
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
module.exports = { readCodexHooks };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Native read-only discovery, never trust-file edits or bypass flags. Reuse
|
|
4
|
+
// the module's verified stdio transport rather than a second RPC lifecycle.
|
|
5
|
+
async function readCodexHooks({ command, env, cwd }) {
|
|
6
|
+
const { createAppServerBridge } = require('claude-ai-workbench/packages/server/lib/bridges/app-server-bridge');
|
|
7
|
+
const { resolveCliInvocation } = require('claude-ai-workbench/packages/server/lib/drivers/cli-json-runner');
|
|
8
|
+
const bridge = createAppServerBridge({ command, args: ['app-server'], env, cwd, resolveInvocation: resolveCliInvocation, maxRestarts: 0, idleTimeoutMs: 0, requestTimeoutMs: 10000 });
|
|
9
|
+
try {
|
|
10
|
+
await bridge.start();
|
|
11
|
+
await bridge.request('initialize', { clientInfo: { name: 'project-knowledge-hook-status', version: '1' }, capabilities: { experimentalApi: true } });
|
|
12
|
+
await bridge.notify('initialized');
|
|
13
|
+
return await bridge.request('hooks/list', { cwds: [cwd] });
|
|
14
|
+
} finally { await bridge.close(); }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = { readCodexHooks };
|