@sanqianx/project-knowledge 5.0.19 → 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 +5 -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-failures.js +39 -0
- package/_modules/knowledge-engine/src/analysis-worker.js +6 -2
- package/_modules/knowledge-engine/src/coordinator.js +7 -0
- package/_modules/knowledge-engine/src/headless-analyzer.js +2 -1
- 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 -35
- package/_site/_test/candidate-product-assets-test.js +13 -13
- 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 -15
- 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 -32
- 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 -33
- 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 -63
- 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 -38
- 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 -22
- 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 +12 -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/data-dir.js +13 -13
- package/_site/lib/embedding-service.js +85 -85
- package/_site/lib/engine-connection.js +93 -93
- 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 -78
- package/_site/lib/llm-client.js +12 -12
- package/_site/lib/model-context-windows.js +82 -82
- package/_site/lib/module-bridge.js +874 -874
- 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 -16
- 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/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 -89
- 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 -83
- 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 -122
- 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 -25
- package/docs/team-knowledge-mode-a-plan.md +73 -73
- package/module-runtime-manifest.json +53 -33
- package/package.json +3 -2
- package/plugins/project-knowledge/.claude-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.codex-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.mcp.json +1 -1
- package/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 +146 -4
- package/vendor-manifest.json +55 -54
|
@@ -114,6 +114,10 @@ node src/bin.js serve [--port 5790] [--host 127.0.0.1] [--data DIR]
|
|
|
114
114
|
|
|
115
115
|
## 验证与尚未验收项
|
|
116
116
|
|
|
117
|
+
默认维护模型额度或余额不足时,不会成功生成知识;提交任务仍先持久化。SDK 明确报告 billing 错误时记录 `MODEL_QUOTA_EXHAUSTED`,请求限流单独记录 `MODEL_RATE_LIMITED`,不把任意 HTTP 429 当作余额耗尽。同项目后续提交保持排队,失败与排队记录跨重启保留;恢复额度或更换默认维护模型后需显式重试前一条失败提交,不会自动消耗额度反复重试。
|
|
118
|
+
|
|
119
|
+
`/api/snapshot` 的兼容扩展字段 `maintenance` 包含执行启用状态、事件消费启用状态及按当前项目过滤的 journal 故障。建单前的消费故障不会伪造提交运行;外壳展示“提交事件消费受阻”,不能将零条运行当作没有提交。故障变化和恢复通过现有 progress 流通知,稳定故障不产生重复刷新事件。未启动服务时,Hook 已捕获的事件保留于 journal,服务消费后才生成分析任务。
|
|
120
|
+
|
|
117
121
|
`npm test` 覆盖真实 Git/Hook、真实分项目 journal、真实 vector-hub 文件索引、晋升、重复抑制、FIFO/并发、记忆、旧格式拒绝、新版恢复及进程隔离。自动测试中的模型和 embedding 是可控替身。
|
|
118
122
|
|
|
119
123
|
`npm run test:web` 使用兄弟源码仓库启动隔离的五服务网页环境,只有模型与 embedding 是替身。单一 1920×1080 视口验证双目录 UI 导入、真实 Hook 到自动刷新、原生消息流与中止、快速项目切换/草稿/历史、迟到检索及文件响应、领域检索、记忆落盘、四 iframe 身份与主题、唯一进度连接、诊断关闭和保留数据解绑;不会在用户代码仓库提交。该脚本是开发验收工具,不属于引擎生产运行依赖。
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const failures = {
|
|
4
|
+
MODEL_QUOTA_EXHAUSTED: '默认维护模型的额度或余额不足。提交证据与失败记录已保留;恢复额度或更换默认模型后,请重试此提交。',
|
|
5
|
+
MODEL_RATE_LIMITED: '默认维护模型请求受限。提交证据与失败记录已保留,请稍后重试。',
|
|
6
|
+
MODEL_AUTH_FAILED: '默认维护模型认证失败。提交证据与失败记录已保留,请检查凭据后重试。',
|
|
7
|
+
AI_RUNTIME_REQUIRED: '后台分析运行时不可用,请修复安装后重试。',
|
|
8
|
+
ANALYSIS_OUTPUT_MISSING: '后台分析未生成有效文件,提交证据与失败记录已保留,请重试。',
|
|
9
|
+
MODEL_FAILED: '后台 Agent 分析失败。提交证据与失败记录已保留,请检查默认维护模型后重试。',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Provider text can echo credentials. Classify structured codes only; neither
|
|
13
|
+
// raw messages nor unknown codes may cross the worker IPC boundary.
|
|
14
|
+
function classifyAnalysisFailure(input) {
|
|
15
|
+
const codes = new Set(), statuses = new Set(), seen = new Set();
|
|
16
|
+
function collect(value, depth = 0) {
|
|
17
|
+
if (typeof value === 'string') { codes.add(value.toLowerCase()); return; }
|
|
18
|
+
if (!value || typeof value !== 'object' || depth > 3 || seen.has(value)) return;
|
|
19
|
+
seen.add(value);
|
|
20
|
+
for (const key of ['code', 'type']) if (typeof value[key] === 'string') codes.add(value[key].toLowerCase());
|
|
21
|
+
if (value.status !== undefined) statuses.add(Number(value.status));
|
|
22
|
+
collect(value.error, depth + 1); collect(value.cause, depth + 1);
|
|
23
|
+
}
|
|
24
|
+
collect(input);
|
|
25
|
+
const has = (...values) => values.some(value => codes.has(value));
|
|
26
|
+
if (has('billing_error', 'insufficient_quota', 'quota_exceeded', 'insufficient_balance', 'model_quota_exhausted')) return 'MODEL_QUOTA_EXHAUSTED';
|
|
27
|
+
if (has('authentication_failed', 'authentication_error', 'model_auth_failed') || statuses.has(401)) return 'MODEL_AUTH_FAILED';
|
|
28
|
+
if (has('rate_limit', 'rate_limit_error', 'model_rate_limited') || statuses.has(429)) return 'MODEL_RATE_LIMITED';
|
|
29
|
+
if (has('module_not_found', 'ai_runtime_required')) return 'AI_RUNTIME_REQUIRED';
|
|
30
|
+
if (has('analysis_output_missing')) return 'ANALYSIS_OUTPUT_MISSING';
|
|
31
|
+
return 'MODEL_FAILED';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function analysisFailure(code) {
|
|
35
|
+
code = Object.hasOwn(failures, code) ? code : 'MODEL_FAILED';
|
|
36
|
+
return Object.assign(new Error(failures[code]), { code, retryable: true });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = { classifyAnalysisFailure, analysisFailure };
|
|
@@ -4,6 +4,7 @@ const { pathToFileURL } = require('node:url');
|
|
|
4
4
|
const { evaluateAutomationToolUse } = require('./runtime/automation-config');
|
|
5
5
|
const { RuntimeLayout } = require('./runtime/layout');
|
|
6
6
|
const { ANALYSIS_SYSTEM_INSTRUCTIONS, requireAnalysisOutput } = require('./analysis-contract');
|
|
7
|
+
const { classifyAnalysisFailure, analysisFailure } = require('./analysis-failures');
|
|
7
8
|
|
|
8
9
|
const abortController = new AbortController();
|
|
9
10
|
process.once('SIGTERM', () => abortController.abort());
|
|
@@ -11,6 +12,7 @@ process.once('disconnect', () => abortController.abort());
|
|
|
11
12
|
process.on('message', message => { if (message?.type === 'cancel') abortController.abort(); });
|
|
12
13
|
|
|
13
14
|
process.once('message', async job => {
|
|
15
|
+
let failureCode = 'MODEL_FAILED';
|
|
14
16
|
try {
|
|
15
17
|
const { query } = await import(pathToFileURL(require.resolve('@anthropic-ai/claude-agent-sdk')).href);
|
|
16
18
|
const layout = new RuntimeLayout(job.stagingPath);
|
|
@@ -40,15 +42,17 @@ process.once('message', async job => {
|
|
|
40
42
|
}] }] },
|
|
41
43
|
} });
|
|
42
44
|
for await (const message of turn) {
|
|
45
|
+
if (message.type === 'assistant' && message.error) failureCode = classifyAnalysisFailure(message.error);
|
|
43
46
|
if (message.type === 'result') completed = message.subtype === 'success' && !message.is_error;
|
|
44
47
|
}
|
|
45
|
-
if (!completed) throw
|
|
48
|
+
if (!completed) throw analysisFailure(failureCode);
|
|
46
49
|
requireAnalysisOutput(job.manifestPath);
|
|
47
50
|
process.send({ type: 'complete' }, () => process.exit(0));
|
|
48
51
|
} catch (error) {
|
|
49
52
|
// SDK/provider messages can echo credentials. Only controlled codes leave
|
|
50
53
|
// this isolated process; raw output is never written to state or logs.
|
|
51
|
-
const
|
|
54
|
+
const classified = classifyAnalysisFailure(error);
|
|
55
|
+
const code = classified === 'MODEL_FAILED' ? failureCode : classified;
|
|
52
56
|
process.send({ type: 'failure', code }, () => process.exit(1));
|
|
53
57
|
}
|
|
54
58
|
});
|
|
@@ -137,6 +137,7 @@ class EngineCoordinator extends EventEmitter {
|
|
|
137
137
|
return job;
|
|
138
138
|
}
|
|
139
139
|
async drainJournals() {
|
|
140
|
+
const prior = JSON.stringify([...this.journalFailures.values()]);
|
|
140
141
|
this.draining = true;
|
|
141
142
|
try {
|
|
142
143
|
await this.bridge.registerConsumer('knowledge-engine', { journals: ['*'] });
|
|
@@ -157,8 +158,14 @@ class EngineCoordinator extends EventEmitter {
|
|
|
157
158
|
finally {
|
|
158
159
|
this.journalError = this.journalFailures.size ? '事件消费异常,未消费证据与游标已保留,请查看诊断日志。' : null;
|
|
159
160
|
this.draining = false;
|
|
161
|
+
if (JSON.stringify([...this.journalFailures.values()]) !== prior) this.emit('progress', this.store.addEvent({ type: 'knowledge.journal', summary: this.journalError ? '提交事件消费受阻,请查看维护状态' : '提交事件消费已恢复' }));
|
|
160
162
|
}
|
|
161
163
|
}
|
|
164
|
+
maintenanceStatus(projectId) {
|
|
165
|
+
const project = projectId ? this.store.getProject(projectId) : null;
|
|
166
|
+
const failures = [...this.journalFailures.values()].filter(failure => !projectId || failure.journalId === 'connection' || (failure.journalId === 'global' && !failure.projectId) || failure.projectId === projectId || failure.journalId === project?.journalId);
|
|
167
|
+
return { projectId: projectId || null, executionEnabled: this.executionEnabled && (!projectId || !!project && (!this.allowedProjects || this.allowedProjects.has(projectId))), consuming: this.consume, journal: { error: failures.length ? this.journalError : null, failures } };
|
|
168
|
+
}
|
|
162
169
|
async recordJournalFailure(journalId, error) {
|
|
163
170
|
const context = error.journalContext || {};
|
|
164
171
|
const failure = { journalId, sequence: context.sequence ?? null, projectId: context.projectId || null, code: error.code || 'JOURNAL_FAILED', message: this.safeMessage(error.message) };
|
|
@@ -4,6 +4,7 @@ const path = require('node:path');
|
|
|
4
4
|
const { frameFrozenEvidence, memoryCandidateContract } = require('./analysis-contract');
|
|
5
5
|
const { finalizeAnalysisOutput } = require('./analysis-output');
|
|
6
6
|
const { freezeKnowledgeTargets } = require('./knowledge-targets');
|
|
7
|
+
const { analysisFailure } = require('./analysis-failures');
|
|
7
8
|
|
|
8
9
|
class HeadlessAnalyzer {
|
|
9
10
|
get sourceReadMode() { return 'isolated-frozen-evidence'; }
|
|
@@ -42,7 +43,7 @@ class HeadlessAnalyzer {
|
|
|
42
43
|
const timer = setTimeout(() => stop(Object.assign(new Error('知识分析超时。'), { code: 'MODEL_TIMEOUT', retryable: true })), this.timeoutMs);
|
|
43
44
|
child.on('message', message => {
|
|
44
45
|
if (message?.type === 'complete') stop();
|
|
45
|
-
else if (message?.type === 'failure') stop(
|
|
46
|
+
else if (message?.type === 'failure') stop(analysisFailure(message.code));
|
|
46
47
|
});
|
|
47
48
|
child.on('error', () => stop(Object.assign(new Error('无法启动后台分析进程。'), { code: 'WORKER_START_FAILED', retryable: true })));
|
|
48
49
|
// A completion message is not an exit: retain the directory lock until
|
|
@@ -143,7 +143,7 @@ async function api(method, pathname, search, body, store, coordinator, ctx) {
|
|
|
143
143
|
if (method === 'GET' && pathname === '/api/events') return { events: store.listEvents(search.get('limit')).filter(e => !search.get('projectId') || e.projectId === search.get('projectId')) };
|
|
144
144
|
if (method === 'POST' && pathname === '/api/events' && ctx.debug) return { event: store.addEvent(body) };
|
|
145
145
|
if (method === 'POST' && pathname === '/api/bridge/notify') { if (coordinator.consume) coordinator.drain(); return { accepted: true }; }
|
|
146
|
-
if (method === 'GET' && pathname === '/api/snapshot') return { projects: store.listProjects(), ...(search.get('summary') === '1' ? store.listRunPage({ projectId: search.get('projectId'), limit: search.get('limit') || 50, before: search.get('before') }) : { runs: store.listRuns(search.get('projectId')) }), events: store.listEvents(50), memories: coordinator.memories.list(), sharedScopes: store.state.sharedScopes };
|
|
146
|
+
if (method === 'GET' && pathname === '/api/snapshot') return { projects: store.listProjects(), ...(search.get('summary') === '1' ? store.listRunPage({ projectId: search.get('projectId'), limit: search.get('limit') || 50, before: search.get('before') }) : { runs: store.listRuns(search.get('projectId')) }), events: store.listEvents(50), memories: coordinator.memories.list(), sharedScopes: store.state.sharedScopes, maintenance: coordinator.maintenanceStatus(search.get('projectId')) };
|
|
147
147
|
throw httpError(404, 'NOT_FOUND', '接口不存在。');
|
|
148
148
|
}
|
|
149
149
|
|
|
@@ -1,67 +1,67 @@
|
|
|
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
|
-
|
|
16
|
-
const HOSTILE_DETAIL = '<img src=x onerror="window.__kbXssProbe=1"> INJECT-STATUS-MARKER sk-abcdef123456';
|
|
17
|
-
|
|
18
|
-
function clientFixture(client, label) {
|
|
19
|
-
const mcpCapable = client === 'claude' || client === 'codex';
|
|
20
|
-
return {
|
|
21
|
-
client, label, available: false, configFound: false,
|
|
22
|
-
knowledgeIntegration: { ok: true, state: 'installed' },
|
|
23
|
-
automaticContext: { supported: client !== 'opencode', configured: false, state: 'check-failed', detail: HOSTILE_DETAIL },
|
|
24
|
-
developmentCapture: { ok: true, state: 'installed', installed: true, detail: HOSTILE_DETAIL },
|
|
25
|
-
mcpStartup: mcpCapable ? { state: 'failed', checkedAt: new Date().toISOString(), scope: 'client:' + client, code: 'PROBE_TIMEOUT', expectedVersion: '9.9.9-test', detail: HOSTILE_DETAIL } : undefined,
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function main() {
|
|
30
|
-
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'kb-agent-mcp-ui-'));
|
|
31
|
-
const dataDir = path.join(temp, 'data');
|
|
32
|
-
const homeDir = path.join(temp, 'home');
|
|
33
|
-
const eventsHome = path.join(dataDir, 'events');
|
|
34
|
-
const offline = http.createServer((req, res) => { res.writeHead(503, { 'content-type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: { message: 'offline stub' } })); });
|
|
35
|
-
await new Promise(resolve => offline.listen(0, '127.0.0.1', resolve));
|
|
36
|
-
const offlineUrl = 'http://127.0.0.1:' + offline.address().port;
|
|
37
|
-
const runtime = createRuntime({ dataDir, analysisOwner: 'engine', bridgeAdapter: new BridgeAdapter({ dataDir, bridgeHomeDir: eventsHome }), moduleBridge: new ModuleBridge({ terminalUrl: offlineUrl, engineUrl: offlineUrl, eventBridgeUrl: offlineUrl, vectorHubUrl: offlineUrl }) });
|
|
38
|
-
await initializeRuntime(runtime);
|
|
39
|
-
const server = http.createServer(createRequestHandler(runtime, { integrationOptions: { homeDir, env: {}, commandResolver: () => '' } }));
|
|
40
|
-
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
41
|
-
const base = 'http://127.0.0.1:' + server.address().port;
|
|
42
|
-
const payload = JSON.stringify({ bridge: { ok: true }, scan: { state: 'ready' }, clients: [clientFixture('claude', 'Claude Code'), clientFixture('codex', 'Codex'), clientFixture('zcode', 'ZCode'), clientFixture('opencode', 'OpenCode')] });
|
|
43
|
-
let browser;
|
|
44
|
-
try {
|
|
45
|
-
browser = await launchCdpBrowser({ chrome: findChrome(), profileDir: path.join(temp, 'browser'), url: base, width: 1440, height: 900 });
|
|
46
|
-
const run = expression => browser.evaluate(expression);
|
|
47
|
-
const click = selector => run('document.querySelector(' + JSON.stringify(selector) + ').click()');
|
|
48
|
-
await run('(() => { const original = window.fetch; window.__originalFetch = original; window.fetch = (input, init) => { const url = String(input && input.url || input); if (url.includes(\'/api/integrations/status\')) { return Promise.resolve({ ok: true, json: async () => (' + payload + ') }); } return original(input, init); }; })()');
|
|
49
|
-
await click('#openSystem');
|
|
50
|
-
await waitFor(() => run("document.querySelectorAll('#agentCaptureList .agent-capture-row').length === 4"), 'capture rows render');
|
|
51
|
-
assert(await run("document.getElementById('agentCaptureList').textContent.includes('PROBE_TIMEOUT')"), 'the MCP startup failure code must be rendered for MCP-capable clients');
|
|
52
|
-
const claudeRowHtml = await run("document.querySelector('#agentCaptureList .agent-capture-row').innerHTML");
|
|
53
|
-
assert(claudeRowHtml.includes('<img'), 'hostile mcpStartup detail must be HTML-escaped, got: ' + claudeRowHtml.slice(0, 300));
|
|
54
|
-
assert(!claudeRowHtml.includes('<img'), 'no raw hostile markup may reach the DOM');
|
|
55
|
-
assert.equal(await run('window.__kbXssProbe'), undefined, 'the hostile onerror must never execute');
|
|
56
|
-
const smalls = await run("[...document.querySelectorAll('#agentCaptureList small')].map(el => el.textContent).join('|')");
|
|
57
|
-
assert(smalls.split('|').some(text => text.startsWith('MCP')), 'the MCP diagnostic is its own line item, got: ' + smalls);
|
|
58
|
-
assert.equal(browser.exceptions.length, 0, JSON.stringify(browser.exceptions));
|
|
59
|
-
console.log('agent-capture-mcp-ui-test PASS (MCP line rendered separately; hostile detail escaped; no XSS execution)');
|
|
60
|
-
} finally {
|
|
61
|
-
await browser?.close(); server.closeAllConnections(); await new Promise(resolve => server.close(resolve));
|
|
62
|
-
offline.closeAllConnections(); await new Promise(resolve => offline.close(resolve)); await runtime.logger.close();
|
|
63
|
-
assert.equal(path.dirname(temp), path.resolve(os.tmpdir())); assert.ok(path.basename(temp).startsWith('kb-agent-mcp-ui-'));
|
|
64
|
-
fs.rmSync(temp, { recursive: true, force: true, maxRetries: 12, retryDelay: 200 });
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
main().catch(error => { console.error(error.stack); process.exitCode = 1; });
|
|
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
|
+
|
|
16
|
+
const HOSTILE_DETAIL = '<img src=x onerror="window.__kbXssProbe=1"> INJECT-STATUS-MARKER sk-abcdef123456';
|
|
17
|
+
|
|
18
|
+
function clientFixture(client, label) {
|
|
19
|
+
const mcpCapable = client === 'claude' || client === 'codex';
|
|
20
|
+
return {
|
|
21
|
+
client, label, available: false, configFound: false,
|
|
22
|
+
knowledgeIntegration: { ok: true, state: 'installed' },
|
|
23
|
+
automaticContext: { supported: client !== 'opencode', configured: false, state: 'check-failed', detail: HOSTILE_DETAIL },
|
|
24
|
+
developmentCapture: { ok: true, state: 'installed', installed: true, detail: HOSTILE_DETAIL },
|
|
25
|
+
mcpStartup: mcpCapable ? { state: 'failed', checkedAt: new Date().toISOString(), scope: 'client:' + client, code: 'PROBE_TIMEOUT', expectedVersion: '9.9.9-test', detail: HOSTILE_DETAIL } : undefined,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function main() {
|
|
30
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'kb-agent-mcp-ui-'));
|
|
31
|
+
const dataDir = path.join(temp, 'data');
|
|
32
|
+
const homeDir = path.join(temp, 'home');
|
|
33
|
+
const eventsHome = path.join(dataDir, 'events');
|
|
34
|
+
const offline = http.createServer((req, res) => { res.writeHead(503, { 'content-type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: { message: 'offline stub' } })); });
|
|
35
|
+
await new Promise(resolve => offline.listen(0, '127.0.0.1', resolve));
|
|
36
|
+
const offlineUrl = 'http://127.0.0.1:' + offline.address().port;
|
|
37
|
+
const runtime = createRuntime({ dataDir, analysisOwner: 'engine', bridgeAdapter: new BridgeAdapter({ dataDir, bridgeHomeDir: eventsHome }), moduleBridge: new ModuleBridge({ terminalUrl: offlineUrl, engineUrl: offlineUrl, eventBridgeUrl: offlineUrl, vectorHubUrl: offlineUrl }) });
|
|
38
|
+
await initializeRuntime(runtime);
|
|
39
|
+
const server = http.createServer(createRequestHandler(runtime, { integrationOptions: { homeDir, env: {}, commandResolver: () => '' } }));
|
|
40
|
+
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
41
|
+
const base = 'http://127.0.0.1:' + server.address().port;
|
|
42
|
+
const payload = JSON.stringify({ bridge: { ok: true }, scan: { state: 'ready' }, clients: [clientFixture('claude', 'Claude Code'), clientFixture('codex', 'Codex'), clientFixture('zcode', 'ZCode'), clientFixture('opencode', 'OpenCode')] });
|
|
43
|
+
let browser;
|
|
44
|
+
try {
|
|
45
|
+
browser = await launchCdpBrowser({ chrome: findChrome(), profileDir: path.join(temp, 'browser'), url: base, width: 1440, height: 900 });
|
|
46
|
+
const run = expression => browser.evaluate(expression);
|
|
47
|
+
const click = selector => run('document.querySelector(' + JSON.stringify(selector) + ').click()');
|
|
48
|
+
await run('(() => { const original = window.fetch; window.__originalFetch = original; window.fetch = (input, init) => { const url = String(input && input.url || input); if (url.includes(\'/api/integrations/status\')) { return Promise.resolve({ ok: true, json: async () => (' + payload + ') }); } return original(input, init); }; })()');
|
|
49
|
+
await click('#openSystem');
|
|
50
|
+
await waitFor(() => run("document.querySelectorAll('#agentCaptureList .agent-capture-row').length === 4"), 'capture rows render');
|
|
51
|
+
assert(await run("document.getElementById('agentCaptureList').textContent.includes('PROBE_TIMEOUT')"), 'the MCP startup failure code must be rendered for MCP-capable clients');
|
|
52
|
+
const claudeRowHtml = await run("document.querySelector('#agentCaptureList .agent-capture-row').innerHTML");
|
|
53
|
+
assert(claudeRowHtml.includes('<img'), 'hostile mcpStartup detail must be HTML-escaped, got: ' + claudeRowHtml.slice(0, 300));
|
|
54
|
+
assert(!claudeRowHtml.includes('<img'), 'no raw hostile markup may reach the DOM');
|
|
55
|
+
assert.equal(await run('window.__kbXssProbe'), undefined, 'the hostile onerror must never execute');
|
|
56
|
+
const smalls = await run("[...document.querySelectorAll('#agentCaptureList small')].map(el => el.textContent).join('|')");
|
|
57
|
+
assert(smalls.split('|').some(text => text.startsWith('MCP')), 'the MCP diagnostic is its own line item, got: ' + smalls);
|
|
58
|
+
assert.equal(browser.exceptions.length, 0, JSON.stringify(browser.exceptions));
|
|
59
|
+
console.log('agent-capture-mcp-ui-test PASS (MCP line rendered separately; hostile detail escaped; no XSS execution)');
|
|
60
|
+
} finally {
|
|
61
|
+
await browser?.close(); server.closeAllConnections(); await new Promise(resolve => server.close(resolve));
|
|
62
|
+
offline.closeAllConnections(); await new Promise(resolve => offline.close(resolve)); await runtime.logger.close();
|
|
63
|
+
assert.equal(path.dirname(temp), path.resolve(os.tmpdir())); assert.ok(path.basename(temp).startsWith('kb-agent-mcp-ui-'));
|
|
64
|
+
fs.rmSync(temp, { recursive: true, force: true, maxRetries: 12, retryDelay: 200 });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
main().catch(error => { console.error(error.stack); process.exitCode = 1; });
|
|
@@ -1,75 +1,75 @@
|
|
|
1
|
-
const assert = require('assert');
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const os = require('os');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const { spawnServer } = require('./helpers/spawn-server');
|
|
6
|
-
|
|
7
|
-
const ROOT = path.resolve(__dirname, '..', '..');
|
|
8
|
-
const PORT = 7795;
|
|
9
|
-
const BASE = `http://127.0.0.1:${PORT}`;
|
|
10
|
-
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pkb-ai-v2-'));
|
|
11
|
-
|
|
12
|
-
async function request(method, pathname, body) {
|
|
13
|
-
const response = await fetch(`${BASE}${pathname}`, {
|
|
14
|
-
method,
|
|
15
|
-
headers: body === undefined ? undefined : { 'content-type': 'application/json' },
|
|
16
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
17
|
-
});
|
|
18
|
-
return { status: response.status, text: await response.text() };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function json(method, pathname, body) {
|
|
22
|
-
const result = await request(method, pathname, body);
|
|
23
|
-
return { ...result, data: JSON.parse(result.text || '{}') };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async function waitForServer() {
|
|
27
|
-
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
28
|
-
try { if ((await fetch(`${BASE}/api/health`)).ok) return; } catch {}
|
|
29
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
30
|
-
}
|
|
31
|
-
throw new Error('server did not start');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
(async () => {
|
|
35
|
-
const spawned = spawnServer({ root: ROOT, port: PORT, dataDir, tag: 'ai-profile-v2', extraEnv: { KB_SKIP_MIGRATION: '1' } });
|
|
36
|
-
try {
|
|
37
|
-
await waitForServer();
|
|
38
|
-
let result = await json('PUT', '/api/ai-profiles', {
|
|
39
|
-
schema: 'ai-profiles/v1', defaultProfileId: 'secure', profiles: [{
|
|
40
|
-
id: 'secure', name: 'Secure', enabled: true, implementation: 'claude-code-agent', apiKeyUpdate: { mode: 'replace', value: 'sk-secret-1234' },
|
|
41
|
-
}],
|
|
42
|
-
});
|
|
43
|
-
assert.strictEqual(result.status, 200, result.text);
|
|
44
|
-
assert.strictEqual(result.data.config.profiles[0].hasApiKey, true);
|
|
45
|
-
assert.strictEqual(result.data.config.profiles[0].apiKeyMasked, '****1234');
|
|
46
|
-
assert(!result.text.includes('sk-secret-1234'));
|
|
47
|
-
|
|
48
|
-
result = await json('GET', '/api/ai-profiles');
|
|
49
|
-
assert.strictEqual(result.status, 200);
|
|
50
|
-
assert(!result.text.includes('sk-secret-1234'));
|
|
51
|
-
assert(!/"apiKey"\s*:/.test(result.text));
|
|
52
|
-
|
|
53
|
-
result = await json('PUT', '/api/ai-profiles', {
|
|
54
|
-
schema: 'ai-profiles/v1', defaultProfileId: 'secure', profiles: [{ id: 'secure', name: 'Renamed', enabled: true, implementation: 'claude-code-agent', apiKeyUpdate: { mode: 'preserve' } }],
|
|
55
|
-
});
|
|
56
|
-
assert.strictEqual(result.status, 200);
|
|
57
|
-
let settings = JSON.parse(fs.readFileSync(path.join(dataDir, 'settings.json'), 'utf8'));
|
|
58
|
-
assert.strictEqual(settings.ai.profiles[0].apiKey, 'sk-secret-1234');
|
|
59
|
-
|
|
60
|
-
result = await json('PUT', '/api/ai-profiles', {
|
|
61
|
-
schema: 'ai-profiles/v1', defaultProfileId: 'secure', profiles: [{ id: 'secure', name: 'Renamed', enabled: true, implementation: 'claude-code-agent', apiKeyUpdate: { mode: 'clear' } }],
|
|
62
|
-
});
|
|
63
|
-
assert.strictEqual(result.status, 200);
|
|
64
|
-
settings = JSON.parse(fs.readFileSync(path.join(dataDir, 'settings.json'), 'utf8'));
|
|
65
|
-
assert(!Object.prototype.hasOwnProperty.call(settings.ai.profiles[0], 'apiKey'));
|
|
66
|
-
|
|
67
|
-
result = await json('PUT', '/api/ai-profiles', { profiles: [{ id: '../bad', apiKeyUpdate: { mode: 'clear' } }] });
|
|
68
|
-
assert.strictEqual(result.status, 400);
|
|
69
|
-
assert.strictEqual(result.data.error.code, 'INVALID_ARGUMENT');
|
|
70
|
-
assert(!Object.prototype.hasOwnProperty.call(result.data.error, 'stack'));
|
|
71
|
-
console.log('ai-profile-test PASS');
|
|
72
|
-
} finally {
|
|
73
|
-
await spawned.cleanup();
|
|
74
|
-
}
|
|
75
|
-
})().catch(error => { console.error(error); process.exitCode = 1; });
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnServer } = require('./helpers/spawn-server');
|
|
6
|
+
|
|
7
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
8
|
+
const PORT = 7795;
|
|
9
|
+
const BASE = `http://127.0.0.1:${PORT}`;
|
|
10
|
+
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pkb-ai-v2-'));
|
|
11
|
+
|
|
12
|
+
async function request(method, pathname, body) {
|
|
13
|
+
const response = await fetch(`${BASE}${pathname}`, {
|
|
14
|
+
method,
|
|
15
|
+
headers: body === undefined ? undefined : { 'content-type': 'application/json' },
|
|
16
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
17
|
+
});
|
|
18
|
+
return { status: response.status, text: await response.text() };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function json(method, pathname, body) {
|
|
22
|
+
const result = await request(method, pathname, body);
|
|
23
|
+
return { ...result, data: JSON.parse(result.text || '{}') };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function waitForServer() {
|
|
27
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
28
|
+
try { if ((await fetch(`${BASE}/api/health`)).ok) return; } catch {}
|
|
29
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
30
|
+
}
|
|
31
|
+
throw new Error('server did not start');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
(async () => {
|
|
35
|
+
const spawned = spawnServer({ root: ROOT, port: PORT, dataDir, tag: 'ai-profile-v2', extraEnv: { KB_SKIP_MIGRATION: '1' } });
|
|
36
|
+
try {
|
|
37
|
+
await waitForServer();
|
|
38
|
+
let result = await json('PUT', '/api/ai-profiles', {
|
|
39
|
+
schema: 'ai-profiles/v1', defaultProfileId: 'secure', profiles: [{
|
|
40
|
+
id: 'secure', name: 'Secure', enabled: true, implementation: 'claude-code-agent', apiKeyUpdate: { mode: 'replace', value: 'sk-secret-1234' },
|
|
41
|
+
}],
|
|
42
|
+
});
|
|
43
|
+
assert.strictEqual(result.status, 200, result.text);
|
|
44
|
+
assert.strictEqual(result.data.config.profiles[0].hasApiKey, true);
|
|
45
|
+
assert.strictEqual(result.data.config.profiles[0].apiKeyMasked, '****1234');
|
|
46
|
+
assert(!result.text.includes('sk-secret-1234'));
|
|
47
|
+
|
|
48
|
+
result = await json('GET', '/api/ai-profiles');
|
|
49
|
+
assert.strictEqual(result.status, 200);
|
|
50
|
+
assert(!result.text.includes('sk-secret-1234'));
|
|
51
|
+
assert(!/"apiKey"\s*:/.test(result.text));
|
|
52
|
+
|
|
53
|
+
result = await json('PUT', '/api/ai-profiles', {
|
|
54
|
+
schema: 'ai-profiles/v1', defaultProfileId: 'secure', profiles: [{ id: 'secure', name: 'Renamed', enabled: true, implementation: 'claude-code-agent', apiKeyUpdate: { mode: 'preserve' } }],
|
|
55
|
+
});
|
|
56
|
+
assert.strictEqual(result.status, 200);
|
|
57
|
+
let settings = JSON.parse(fs.readFileSync(path.join(dataDir, 'settings.json'), 'utf8'));
|
|
58
|
+
assert.strictEqual(settings.ai.profiles[0].apiKey, 'sk-secret-1234');
|
|
59
|
+
|
|
60
|
+
result = await json('PUT', '/api/ai-profiles', {
|
|
61
|
+
schema: 'ai-profiles/v1', defaultProfileId: 'secure', profiles: [{ id: 'secure', name: 'Renamed', enabled: true, implementation: 'claude-code-agent', apiKeyUpdate: { mode: 'clear' } }],
|
|
62
|
+
});
|
|
63
|
+
assert.strictEqual(result.status, 200);
|
|
64
|
+
settings = JSON.parse(fs.readFileSync(path.join(dataDir, 'settings.json'), 'utf8'));
|
|
65
|
+
assert(!Object.prototype.hasOwnProperty.call(settings.ai.profiles[0], 'apiKey'));
|
|
66
|
+
|
|
67
|
+
result = await json('PUT', '/api/ai-profiles', { profiles: [{ id: '../bad', apiKeyUpdate: { mode: 'clear' } }] });
|
|
68
|
+
assert.strictEqual(result.status, 400);
|
|
69
|
+
assert.strictEqual(result.data.error.code, 'INVALID_ARGUMENT');
|
|
70
|
+
assert(!Object.prototype.hasOwnProperty.call(result.data.error, 'stack'));
|
|
71
|
+
console.log('ai-profile-test PASS');
|
|
72
|
+
} finally {
|
|
73
|
+
await spawned.cleanup();
|
|
74
|
+
}
|
|
75
|
+
})().catch(error => { console.error(error); process.exitCode = 1; });
|
|
@@ -1,91 +1,91 @@
|
|
|
1
|
-
// Tests for ai-vendor-presets: 4 vendors with full fields, name/id lookup.
|
|
2
|
-
|
|
3
|
-
const { AI_VENDOR_PRESETS, listVendorPresetNames, findVendorPresetByName } = require('../lib/ai-vendor-presets');
|
|
4
|
-
|
|
5
|
-
function assert(cond, msg) { if (!cond) throw new Error(msg); }
|
|
6
|
-
|
|
7
|
-
(() => {
|
|
8
|
-
try {
|
|
9
|
-
// 1. Exactly 4 vendors
|
|
10
|
-
assert(Array.isArray(AI_VENDOR_PRESETS), 'AI_VENDOR_PRESETS should be an array');
|
|
11
|
-
assert(AI_VENDOR_PRESETS.length === 4, `expected 4 vendors, got ${AI_VENDOR_PRESETS.length}`);
|
|
12
|
-
|
|
13
|
-
// 2. IDs unique and match the expected set
|
|
14
|
-
const ids = AI_VENDOR_PRESETS.map(v => v.id).sort();
|
|
15
|
-
assert(JSON.stringify(ids) === JSON.stringify(['anthropic', 'deepseek', 'minimax', 'zhipu']),
|
|
16
|
-
`vendor ids should be anthropic/deepseek/minimax/zhipu, got ${JSON.stringify(ids)}`);
|
|
17
|
-
|
|
18
|
-
// 3. Every preset has the required fields
|
|
19
|
-
const required = ['id', 'name', 'baseUrl', 'mainModel', 'thinkingModel',
|
|
20
|
-
'haikuModel', 'sonnetModel', 'opusModel', 'contextWindow', 'website'];
|
|
21
|
-
for (const v of AI_VENDOR_PRESETS) {
|
|
22
|
-
for (const f of required) {
|
|
23
|
-
assert(v[f] !== undefined && v[f] !== null && v[f] !== '',
|
|
24
|
-
`vendor ${v.id} missing or empty field: ${f}`);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// 4. baseUrl: https://, no trailing slash
|
|
29
|
-
for (const v of AI_VENDOR_PRESETS) {
|
|
30
|
-
assert(/^https?:\/\//.test(v.baseUrl), `${v.id} baseUrl must start with http(s)://`);
|
|
31
|
-
assert(!/\/$/.test(v.baseUrl), `${v.id} baseUrl must NOT have trailing slash`);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// 5. contextWindow is a positive integer
|
|
35
|
-
for (const v of AI_VENDOR_PRESETS) {
|
|
36
|
-
assert(Number.isInteger(v.contextWindow) && v.contextWindow > 0,
|
|
37
|
-
`${v.id} contextWindow must be a positive integer, got ${v.contextWindow}`);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// 6. Specific known values
|
|
41
|
-
const minimax = findVendorPresetByName('MiniMax');
|
|
42
|
-
assert(minimax && minimax.baseUrl === 'https://api.minimaxi.com/anthropic',
|
|
43
|
-
`minimax baseUrl wrong: ${minimax && minimax.baseUrl}`);
|
|
44
|
-
assert(minimax && minimax.mainModel === 'MiniMax-M3',
|
|
45
|
-
`minimax mainModel wrong: ${minimax && minimax.mainModel}`);
|
|
46
|
-
assert(minimax && minimax.contextWindow === 1048576,
|
|
47
|
-
`minimax contextWindow should be 1M`);
|
|
48
|
-
|
|
49
|
-
const anthropic = findVendorPresetByName('Anthropic Claude');
|
|
50
|
-
assert(anthropic && anthropic.baseUrl === 'https://api.anthropic.com',
|
|
51
|
-
`anthropic baseUrl wrong`);
|
|
52
|
-
assert(anthropic && anthropic.contextWindow === 200000,
|
|
53
|
-
`anthropic contextWindow should be 200K`);
|
|
54
|
-
|
|
55
|
-
const zhipu = findVendorPresetByName('智谱 GLM');
|
|
56
|
-
assert(zhipu && zhipu.baseUrl === 'https://open.bigmodel.cn/api/anthropic',
|
|
57
|
-
`zhipu baseUrl wrong: ${zhipu && zhipu.baseUrl}`);
|
|
58
|
-
|
|
59
|
-
const deepseek = findVendorPresetByName('DeepSeek');
|
|
60
|
-
assert(deepseek && deepseek.baseUrl === 'https://api.deepseek.com/anthropic',
|
|
61
|
-
`deepseek baseUrl wrong: ${deepseek && deepseek.baseUrl}`);
|
|
62
|
-
assert(deepseek && deepseek.mainModel === 'deepseek-chat',
|
|
63
|
-
`deepseek mainModel wrong`);
|
|
64
|
-
assert(deepseek && deepseek.thinkingModel === 'deepseek-reasoner',
|
|
65
|
-
`deepseek thinkingModel wrong`);
|
|
66
|
-
|
|
67
|
-
// 7. Case-insensitive lookup by name
|
|
68
|
-
assert(findVendorPresetByName('minimax').id === 'minimax', 'lowercase name lookup');
|
|
69
|
-
assert(findVendorPresetByName('MINIMAX').id === 'minimax', 'uppercase name lookup');
|
|
70
|
-
|
|
71
|
-
// 8. Lookup by id also works
|
|
72
|
-
assert(findVendorPresetByName('minimax').id === 'minimax', 'id lookup');
|
|
73
|
-
|
|
74
|
-
// 9. Unknown / empty / null returns null (no false positive)
|
|
75
|
-
assert(findVendorPresetByName('Totally Unknown Vendor') === null, 'unknown vendor → null');
|
|
76
|
-
assert(findVendorPresetByName('') === null, 'empty name → null');
|
|
77
|
-
assert(findVendorPresetByName(null) === null, 'null name → null');
|
|
78
|
-
assert(findVendorPresetByName(undefined) === null, 'undefined name → null');
|
|
79
|
-
|
|
80
|
-
// 10. listVendorPresetNames returns all 4 names in stable order
|
|
81
|
-
const names = listVendorPresetNames();
|
|
82
|
-
assert(names.length === 4, `expected 4 names, got ${names.length}`);
|
|
83
|
-
assert(names.every(n => typeof n === 'string' && n.length > 0),
|
|
84
|
-
'all names should be non-empty strings');
|
|
85
|
-
|
|
86
|
-
console.log('ai-vendor-presets test passed');
|
|
87
|
-
} catch (e) {
|
|
88
|
-
console.error('ai-vendor-presets test failed:', e.message);
|
|
89
|
-
process.exitCode = 1;
|
|
90
|
-
}
|
|
91
|
-
})();
|
|
1
|
+
// Tests for ai-vendor-presets: 4 vendors with full fields, name/id lookup.
|
|
2
|
+
|
|
3
|
+
const { AI_VENDOR_PRESETS, listVendorPresetNames, findVendorPresetByName } = require('../lib/ai-vendor-presets');
|
|
4
|
+
|
|
5
|
+
function assert(cond, msg) { if (!cond) throw new Error(msg); }
|
|
6
|
+
|
|
7
|
+
(() => {
|
|
8
|
+
try {
|
|
9
|
+
// 1. Exactly 4 vendors
|
|
10
|
+
assert(Array.isArray(AI_VENDOR_PRESETS), 'AI_VENDOR_PRESETS should be an array');
|
|
11
|
+
assert(AI_VENDOR_PRESETS.length === 4, `expected 4 vendors, got ${AI_VENDOR_PRESETS.length}`);
|
|
12
|
+
|
|
13
|
+
// 2. IDs unique and match the expected set
|
|
14
|
+
const ids = AI_VENDOR_PRESETS.map(v => v.id).sort();
|
|
15
|
+
assert(JSON.stringify(ids) === JSON.stringify(['anthropic', 'deepseek', 'minimax', 'zhipu']),
|
|
16
|
+
`vendor ids should be anthropic/deepseek/minimax/zhipu, got ${JSON.stringify(ids)}`);
|
|
17
|
+
|
|
18
|
+
// 3. Every preset has the required fields
|
|
19
|
+
const required = ['id', 'name', 'baseUrl', 'mainModel', 'thinkingModel',
|
|
20
|
+
'haikuModel', 'sonnetModel', 'opusModel', 'contextWindow', 'website'];
|
|
21
|
+
for (const v of AI_VENDOR_PRESETS) {
|
|
22
|
+
for (const f of required) {
|
|
23
|
+
assert(v[f] !== undefined && v[f] !== null && v[f] !== '',
|
|
24
|
+
`vendor ${v.id} missing or empty field: ${f}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 4. baseUrl: https://, no trailing slash
|
|
29
|
+
for (const v of AI_VENDOR_PRESETS) {
|
|
30
|
+
assert(/^https?:\/\//.test(v.baseUrl), `${v.id} baseUrl must start with http(s)://`);
|
|
31
|
+
assert(!/\/$/.test(v.baseUrl), `${v.id} baseUrl must NOT have trailing slash`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 5. contextWindow is a positive integer
|
|
35
|
+
for (const v of AI_VENDOR_PRESETS) {
|
|
36
|
+
assert(Number.isInteger(v.contextWindow) && v.contextWindow > 0,
|
|
37
|
+
`${v.id} contextWindow must be a positive integer, got ${v.contextWindow}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 6. Specific known values
|
|
41
|
+
const minimax = findVendorPresetByName('MiniMax');
|
|
42
|
+
assert(minimax && minimax.baseUrl === 'https://api.minimaxi.com/anthropic',
|
|
43
|
+
`minimax baseUrl wrong: ${minimax && minimax.baseUrl}`);
|
|
44
|
+
assert(minimax && minimax.mainModel === 'MiniMax-M3',
|
|
45
|
+
`minimax mainModel wrong: ${minimax && minimax.mainModel}`);
|
|
46
|
+
assert(minimax && minimax.contextWindow === 1048576,
|
|
47
|
+
`minimax contextWindow should be 1M`);
|
|
48
|
+
|
|
49
|
+
const anthropic = findVendorPresetByName('Anthropic Claude');
|
|
50
|
+
assert(anthropic && anthropic.baseUrl === 'https://api.anthropic.com',
|
|
51
|
+
`anthropic baseUrl wrong`);
|
|
52
|
+
assert(anthropic && anthropic.contextWindow === 200000,
|
|
53
|
+
`anthropic contextWindow should be 200K`);
|
|
54
|
+
|
|
55
|
+
const zhipu = findVendorPresetByName('智谱 GLM');
|
|
56
|
+
assert(zhipu && zhipu.baseUrl === 'https://open.bigmodel.cn/api/anthropic',
|
|
57
|
+
`zhipu baseUrl wrong: ${zhipu && zhipu.baseUrl}`);
|
|
58
|
+
|
|
59
|
+
const deepseek = findVendorPresetByName('DeepSeek');
|
|
60
|
+
assert(deepseek && deepseek.baseUrl === 'https://api.deepseek.com/anthropic',
|
|
61
|
+
`deepseek baseUrl wrong: ${deepseek && deepseek.baseUrl}`);
|
|
62
|
+
assert(deepseek && deepseek.mainModel === 'deepseek-chat',
|
|
63
|
+
`deepseek mainModel wrong`);
|
|
64
|
+
assert(deepseek && deepseek.thinkingModel === 'deepseek-reasoner',
|
|
65
|
+
`deepseek thinkingModel wrong`);
|
|
66
|
+
|
|
67
|
+
// 7. Case-insensitive lookup by name
|
|
68
|
+
assert(findVendorPresetByName('minimax').id === 'minimax', 'lowercase name lookup');
|
|
69
|
+
assert(findVendorPresetByName('MINIMAX').id === 'minimax', 'uppercase name lookup');
|
|
70
|
+
|
|
71
|
+
// 8. Lookup by id also works
|
|
72
|
+
assert(findVendorPresetByName('minimax').id === 'minimax', 'id lookup');
|
|
73
|
+
|
|
74
|
+
// 9. Unknown / empty / null returns null (no false positive)
|
|
75
|
+
assert(findVendorPresetByName('Totally Unknown Vendor') === null, 'unknown vendor → null');
|
|
76
|
+
assert(findVendorPresetByName('') === null, 'empty name → null');
|
|
77
|
+
assert(findVendorPresetByName(null) === null, 'null name → null');
|
|
78
|
+
assert(findVendorPresetByName(undefined) === null, 'undefined name → null');
|
|
79
|
+
|
|
80
|
+
// 10. listVendorPresetNames returns all 4 names in stable order
|
|
81
|
+
const names = listVendorPresetNames();
|
|
82
|
+
assert(names.length === 4, `expected 4 names, got ${names.length}`);
|
|
83
|
+
assert(names.every(n => typeof n === 'string' && n.length > 0),
|
|
84
|
+
'all names should be non-empty strings');
|
|
85
|
+
|
|
86
|
+
console.log('ai-vendor-presets test passed');
|
|
87
|
+
} catch (e) {
|
|
88
|
+
console.error('ai-vendor-presets test failed:', e.message);
|
|
89
|
+
process.exitCode = 1;
|
|
90
|
+
}
|
|
91
|
+
})();
|