@sanqianx/project-knowledge 5.0.27 → 5.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/_modules/knowledge-engine/README.md +2 -0
- package/_modules/knowledge-engine/package.json +1 -1
- package/_modules/knowledge-engine/src/memories.js +50 -5
- package/_modules/knowledge-engine/src/server.js +5 -0
- package/_modules/knowledge-engine/src/store.js +12 -2
- package/_site/_test/helpers/qa-r91-harness.cjs +531 -0
- package/module-runtime-manifest.json +30 -29
- package/package.json +2 -2
- package/plugins/project-knowledge/.claude-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.codex-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.mcp.json +1 -1
- package/ui/index.html +1 -0
- package/ui/product.css +1 -1
- package/ui/product.js +28 -2
- package/vendor-manifest.json +7 -7
|
@@ -69,6 +69,7 @@ node src/bin.js serve [--port 5790] [--host 127.0.0.1] [--data DIR]
|
|
|
69
69
|
| POST | `/api/bridge/notify` | 唤醒 journal 消费;通知丢失仍由轮询恢复 |
|
|
70
70
|
| GET / PATCH | `/api/memories`、`/api/memories/:id` | 按 status/scopeType/scopeId 筛选;一次编辑内容、适用范围和确认状态;来源证据只读 |
|
|
71
71
|
| POST | `/api/memories` | `{ text, brainId?, scopeType?, scopeId? }` → 手动添加已确认记忆;默认全局,领域/项目必须指定已登记目标,不接受伪造来源 |
|
|
72
|
+
| DELETE | `/api/memories/:id` | 软删除为无正文墓碑:默认列表与新任务不再使用,原始对话与知识来源保留;重复删除返回 404;完整契约见下方记忆删除边界 |
|
|
72
73
|
| POST | `/api/scopes` | 外壳投影智脑、领域共享目录;受运行期鉴权保护 |
|
|
73
74
|
| POST | `/api/runtime-config` | 运行期模型配置,仅保留在内存;受鉴权保护 |
|
|
74
75
|
| GET | `/api/runtime-auth` | 本机服务确认运行期鉴权;受鉴权保护,不对浏览器反代 |
|
|
@@ -109,6 +110,7 @@ node src/bin.js serve [--port 5790] [--host 127.0.0.1] [--data DIR]
|
|
|
109
110
|
- 长期记忆统一保存在已绑定的智脑共享目录:`memories/confirmed.md`(全局)、`memories/domains/<id>.md`、`memories/projects/<id>.md`。不创建第二份项目知识目录。现有 v5 缺少范围字段的记忆仍按全局解释;不读取 pre-v5 状态、不重扫历史。
|
|
110
111
|
- 手动记忆以用户确认作为来源,记录 `sourceType: manual`、`evidence: null`,不伪造提交或分析运行。内容限制为 1–2000 字符,同范围内容去重;创建、编辑、投影与恢复复用引擎受控写入。空项目列表也可添加全局记忆,但必须已绑定共享目录;无需模型分析或 Git 提交,索引状态独立报告。
|
|
111
112
|
- 任务检索只允许当前项目、当前所属领域与全局的已确认记忆,即使检索整个智脑也不放宽专属记忆范围。过滤在排名之前完成,`get` 同样校验;向量旧内容不能绕过当前状态和文件一致性校验。已确认记录修改范围会同步清理旧投影;写入失败显示待同步并自动重试,未同步文件不能进入上下文。
|
|
113
|
+
- 删除记忆写入无正文墓碑记录:删除后默认列表与新任务上下文不再使用该记忆,原始对话与知识来源保持保留;同文本的自动候选按墓碑去重不会复活,同一适用范围仍可手动重加;重复删除返回 404;派生记忆文件写入失败时如实记录待同步并自动重试,不伪造一致状态。
|
|
112
114
|
- 旧格式不读取、不自动迁移。损坏的正式状态不覆盖、不清空。
|
|
113
115
|
- 引擎状态复用受控原子写入:独占随机临时文件、文件同步、Windows 短暂共享冲突有界重试;永久失败保留原状态并清理本次临时文件,绝不先删正式状态。
|
|
114
116
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanqianx/ai-coding-knowledge-engine",
|
|
3
|
-
"version": "0.2.7-rc.
|
|
3
|
+
"version": "0.2.7-rc.12",
|
|
4
4
|
"description": "Knowledge engine module for project-knowledge: commit-boundary triggered knowledge analysis/generation (trigger plane) and retrieval for development assistants (retrieval plane).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "src/server.js",
|
|
@@ -96,7 +96,7 @@ class MemoryService {
|
|
|
96
96
|
}
|
|
97
97
|
list({ brainId, status, scopeType, scopeId } = {}) {
|
|
98
98
|
if (scopeType && !TYPES.includes(scopeType)) throw new DomainError('INVALID_ARGUMENT', '记忆筛选范围无效。');
|
|
99
|
-
return this.store.state.memories.filter(m => (!brainId || m.brainId === brainId) && (!status || m.status === status) && (!scopeType || this.scopeOf(m).type === scopeType) && (!scopeId || this.scopeOf(m).id === scopeId)).map(m => this.project(m));
|
|
99
|
+
return this.store.state.memories.filter(m => (m.status !== 'deleted' || status === 'deleted') && (!brainId || m.brainId === brainId) && (!status || m.status === status) && (!scopeType || this.scopeOf(m).type === scopeType) && (!scopeId || this.scopeOf(m).id === scopeId)).map(m => this.project(m));
|
|
100
100
|
}
|
|
101
101
|
capture(run) {
|
|
102
102
|
const report = { accepted: 0, deduplicated: 0, rejected: [] };
|
|
@@ -123,7 +123,15 @@ class MemoryService {
|
|
|
123
123
|
const scopeType = raw.scopeType;
|
|
124
124
|
const scopeId = scopeType === 'global' ? brainId : project.domainId;
|
|
125
125
|
const key = fingerprint(brainId, scopeType, scopeId, text);
|
|
126
|
-
|
|
126
|
+
// Tombstones never hold text, so they dedup through their fingerprints:
|
|
127
|
+
// the fresh pair covers the record's last scope and text, while the kept
|
|
128
|
+
// original capture key still covers a pre-edit/pre-move scope.
|
|
129
|
+
const duplicateOf = m => {
|
|
130
|
+
if (m.captureFingerprint === key) return true;
|
|
131
|
+
if (m.status === 'deleted') return m.fingerprint === key || m.originalCaptureFingerprint === key;
|
|
132
|
+
return fingerprint(m.brainId || 'brain-default', this.scopeOf(m).type, this.scopeOf(m).id, m.text) === key || !m.scopeType && m.evidence?.projectId === project.projectId && normalized(m.originalText || m.text) === normalized(text);
|
|
133
|
+
};
|
|
134
|
+
if (next.some(duplicateOf)) { report.deduplicated++; continue; }
|
|
127
135
|
next.push({ id: `memory-${crypto.randomUUID()}`, fingerprint: key, captureFingerprint: key, brainId, domainId: project.domainId || null, scopeType, scopeId, text, originalText: text, status: 'candidate', sourceEventId: provenance.sourceEventId, sourceHash: provenance.sourceHash, eligibility: provenance.eligibility, evidence: { projectId: project.projectId, commitSha: run.commitSha, runId: run.runId, entryId, quote }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() });
|
|
128
136
|
report.accepted++;
|
|
129
137
|
}
|
|
@@ -145,7 +153,9 @@ class MemoryService {
|
|
|
145
153
|
this.validateScope(type, targetId, brainId);
|
|
146
154
|
if (!this.brainScope(brainId)) throw new DomainError('INVALID_ARGUMENT', '请先绑定智脑共享目录。', { status: 409 });
|
|
147
155
|
const text = input.text.trim(), key = fingerprint(brainId, type, targetId, text);
|
|
148
|
-
|
|
156
|
+
// An explicit manual add is the user changing their mind after a delete:
|
|
157
|
+
// tombstones never block it, only another live record in the same scope does.
|
|
158
|
+
if (this.store.state.memories.some(m => m.status !== 'deleted' && fingerprint(m.brainId || 'brain-default', this.scopeOf(m).type, this.scopeOf(m).id, m.text) === key)) throw new DomainError('MEMORY_DUPLICATE', '该范围内已存在相同记忆。', { status: 409 });
|
|
149
159
|
// User confirmation is the provenance; never invent a model run or Git commit.
|
|
150
160
|
const now = new Date().toISOString();
|
|
151
161
|
const memory = { id: `memory-${crypto.randomUUID()}`, fingerprint: key, brainId, scopeType: type, scopeId: targetId, sourceType: 'manual', text, originalText: text, status: 'confirmed', evidence: null, createdAt: now, updatedAt: now };
|
|
@@ -162,7 +172,7 @@ class MemoryService {
|
|
|
162
172
|
if (Object.keys(input).some(key => !['text', 'status', 'scopeType', 'scopeId'].includes(key))) throw new DomainError('INVALID_ARGUMENT', '来源证据和文件路径不能修改。');
|
|
163
173
|
const job = AtomicFile.withFileLock(this.lock(), async () => {
|
|
164
174
|
const memory = this.store.state.memories.find(m => m.id === id);
|
|
165
|
-
if (!memory) throw new DomainError('PROJECT_NOT_FOUND', '记忆不存在。', { status: 404 });
|
|
175
|
+
if (!memory || memory.status === 'deleted') throw new DomainError('PROJECT_NOT_FOUND', '记忆不存在。', { status: 404 });
|
|
166
176
|
if (input.text !== undefined && (typeof input.text !== 'string' || !input.text.trim() || input.text.length > 2000)) throw new DomainError('INVALID_ARGUMENT', '记忆内容必须为 1–2000 个字符。');
|
|
167
177
|
if (input.status !== undefined && !['confirmed', 'ignored'].includes(input.status)) throw new DomainError('INVALID_ARGUMENT', '记忆状态无效。');
|
|
168
178
|
if (memory.status === 'ignored') throw new DomainError('INVALID_ARGUMENT', '该候选已忽略。', { status: 409 });
|
|
@@ -172,7 +182,7 @@ class MemoryService {
|
|
|
172
182
|
if (input.status !== 'ignored' || input.scopeType !== undefined || input.scopeId !== undefined) this.validateScope(type, targetId, brainId);
|
|
173
183
|
if ((input.status || memory.status) === 'confirmed' && !this.brainScope(brainId)) throw new DomainError('INVALID_ARGUMENT', '请先绑定智脑共享目录。', { status: 409 });
|
|
174
184
|
const text = input.text?.trim() || memory.text, key = fingerprint(brainId, type, targetId, text);
|
|
175
|
-
if (this.store.state.memories.some(m => m.id !== id && fingerprint(m.brainId || 'brain-default', this.scopeOf(m).type, this.scopeOf(m).id, m.text) === key)) throw new DomainError('MEMORY_DUPLICATE', '该范围内已存在相同记忆。', { status: 409 });
|
|
185
|
+
if (this.store.state.memories.some(m => m.id !== id && m.status !== 'deleted' && fingerprint(m.brainId || 'brain-default', this.scopeOf(m).type, this.scopeOf(m).id, m.text) === key)) throw new DomainError('MEMORY_DUPLICATE', '该范围内已存在相同记忆。', { status: 409 });
|
|
176
186
|
const next = { ...memory, originalText: memory.originalText || memory.text, captureFingerprint: memory.captureFingerprint || fingerprint(brainId, old.type, old.id, memory.originalText || memory.text), fingerprint: key, scopeType: type, scopeId: targetId, text, status: input.status || memory.status, updatedAt: new Date().toISOString() };
|
|
177
187
|
const before = this.store.state.memories;
|
|
178
188
|
this.store.state.memories = before.map(m => m.id === id ? next : m);
|
|
@@ -182,6 +192,41 @@ class MemoryService {
|
|
|
182
192
|
this.writes.add(job);
|
|
183
193
|
try { return await job; } finally { this.writes.delete(job); }
|
|
184
194
|
}
|
|
195
|
+
async remove(id) {
|
|
196
|
+
if (this.closed) throw new DomainError('PROJECT_BUSY', '引擎正在关闭,请稍后重试。', { status: 409 });
|
|
197
|
+
if (typeof id !== 'string' || !id.trim()) throw new DomainError('INVALID_ARGUMENT', '记忆 ID 无效。');
|
|
198
|
+
const job = AtomicFile.withFileLock(this.lock(), async () => {
|
|
199
|
+
const memory = this.store.state.memories.find(m => m.id === id);
|
|
200
|
+
if (!memory || memory.status === 'deleted') throw new DomainError('PROJECT_NOT_FOUND', '记忆不存在。', { status: 404 });
|
|
201
|
+
const brainId = memory.brainId || 'brain-default', scope = this.scopeOf(memory);
|
|
202
|
+
// A tombstone keeps only the dedup fingerprints and audit metadata; the
|
|
203
|
+
// text and quote are dropped so a deleted memory never returns through
|
|
204
|
+
// lists, markdown, retrieval or context injection, and the fingerprints
|
|
205
|
+
// stop the same candidate from coming back on the next capture. Legacy
|
|
206
|
+
// records may predate originalText/captureFingerprint/createdAt, so both
|
|
207
|
+
// keys are always computed from the live record, and a stored original
|
|
208
|
+
// capture key from before an edit or scope move is preserved separately.
|
|
209
|
+
const now = new Date().toISOString();
|
|
210
|
+
const captureKey = fingerprint(brainId, scope.type, scope.id, memory.originalText || memory.text);
|
|
211
|
+
const originalCaptureKey = typeof memory.captureFingerprint === 'string' && /^[a-f0-9]{64}$/.test(memory.captureFingerprint) && memory.captureFingerprint !== captureKey ? memory.captureFingerprint : null;
|
|
212
|
+
const tombstone = {
|
|
213
|
+
id: memory.id, brainId, scopeType: scope.type, scopeId: scope.id,
|
|
214
|
+
...(memory.sourceType !== undefined ? { sourceType: memory.sourceType } : {}),
|
|
215
|
+
status: 'deleted',
|
|
216
|
+
fingerprint: fingerprint(brainId, scope.type, scope.id, memory.text),
|
|
217
|
+
captureFingerprint: captureKey,
|
|
218
|
+
...(originalCaptureKey ? { originalCaptureFingerprint: originalCaptureKey } : {}),
|
|
219
|
+
evidence: memory.evidence ? { projectId: memory.evidence.projectId, commitSha: memory.evidence.commitSha, runId: memory.evidence.runId || null, entryId: memory.evidence.entryId || '' } : null,
|
|
220
|
+
createdAt: typeof memory.createdAt === 'string' && Number.isFinite(Date.parse(memory.createdAt)) ? memory.createdAt : now, updatedAt: now, deletedAt: now,
|
|
221
|
+
};
|
|
222
|
+
const before = this.store.state.memories;
|
|
223
|
+
this.store.state.memories = before.map(m => m.id === id ? tombstone : m);
|
|
224
|
+
try { this.store.save(); } catch (error) { this.store.state.memories = before; throw error; }
|
|
225
|
+
await this.materialize(); return this.project(tombstone);
|
|
226
|
+
});
|
|
227
|
+
this.writes.add(job);
|
|
228
|
+
try { return await job; } finally { this.writes.delete(job); }
|
|
229
|
+
}
|
|
185
230
|
content(brainId, entry = 'memories/confirmed.md') {
|
|
186
231
|
return '# 已确认的长期记忆\n\n' + this.confirmed(brainId, entry).map(m => {
|
|
187
232
|
const scope = this.scopeOf(m);
|
|
@@ -115,6 +115,11 @@ async function api(method, pathname, search, body, store, coordinator, ctx) {
|
|
|
115
115
|
coordinator.emit('progress', store.addEvent({ type: 'memory.updated', summary: memory.status === 'confirmed' ? '已确认长期记忆' : memory.status === 'ignored' ? '已忽略记忆候选' : '已编辑记忆候选' }));
|
|
116
116
|
return { memory };
|
|
117
117
|
}
|
|
118
|
+
if (method === 'DELETE' && memoryMatch) {
|
|
119
|
+
const memory = await coordinator.memories.remove(decodeURIComponent(memoryMatch[1]));
|
|
120
|
+
coordinator.emit('progress', store.addEvent({ type: 'memory.deleted', summary: '已删除长期记忆' }));
|
|
121
|
+
return { memory };
|
|
122
|
+
}
|
|
118
123
|
if (method === 'POST' && pathname === '/api/projects') {
|
|
119
124
|
const result = store.addProject(body);
|
|
120
125
|
if (!result.reused) store.addEvent({ type: 'project.register', projectId: body.projectId, summary: `登记 ${result.project.name}` });
|
|
@@ -40,8 +40,18 @@ class EngineStore {
|
|
|
40
40
|
for (const [journalId, sequence] of Object.entries(raw.journalCursors || {})) if (!/^(global|sha256:[a-f0-9]{64})$/.test(journalId) || !Number.isSafeInteger(sequence) || sequence < 0) throw new Error('Invalid journal cursor');
|
|
41
41
|
for (const scope of raw.sharedScopes || []) if (!scope || !['brain', 'domain'].includes(scope.kind) || !path.isAbsolute(scope.knowledgePath || '')) throw new Error('Invalid shared scope');
|
|
42
42
|
for (const memory of raw.memories || []) {
|
|
43
|
-
if (!memory || !['candidate', 'confirmed', 'ignored'].includes(memory.status) ||
|
|
44
|
-
if (memory.
|
|
43
|
+
if (!memory || !['candidate', 'confirmed', 'ignored', 'deleted'].includes(memory.status) || memory.sourceType !== undefined && !['manual', 'agent'].includes(memory.sourceType)) throw new Error('Invalid memory record');
|
|
44
|
+
if (memory.status === 'deleted') {
|
|
45
|
+
// Tombstones carry no text: the dedup fingerprint and audit metadata
|
|
46
|
+
// are the whole record, so validate those instead of the live shape.
|
|
47
|
+
validateProjectId(memory.brainId);
|
|
48
|
+
// R101: the `memory-` prefix is only a generation convention; records
|
|
49
|
+
// written before it (any stable non-empty string id, ASCII or
|
|
50
|
+
// Unicode) load and delete legally, and the id never reaches a file
|
|
51
|
+
// path — so the tombstone validates the same id contract they had.
|
|
52
|
+
if (typeof memory.id !== 'string' || !memory.id || typeof memory.fingerprint !== 'string' || !/^[a-f0-9]{64}$/.test(memory.fingerprint) || memory.captureFingerprint !== undefined && !/^[a-f0-9]{64}$/.test(memory.captureFingerprint) || memory.originalCaptureFingerprint !== undefined && !/^[a-f0-9]{64}$/.test(memory.originalCaptureFingerprint) || memory.evidence !== null && (!memory.evidence.projectId || !/^[a-f0-9]{40}$/.test(memory.evidence.commitSha || '')) || !Number.isFinite(Date.parse(memory.createdAt)) || !Number.isFinite(Date.parse(memory.deletedAt))) throw new Error('Invalid deleted memory record');
|
|
53
|
+
} else if (typeof memory.text !== 'string') throw new Error('Invalid memory record');
|
|
54
|
+
else if (memory.sourceType === 'manual') {
|
|
45
55
|
validateProjectId(memory.brainId);
|
|
46
56
|
if (memory.status === 'candidate' || !memory.text.trim() || memory.text.length > 2000 || !memory.scopeType || memory.evidence !== null || !Number.isFinite(Date.parse(memory.createdAt)) || !Number.isFinite(Date.parse(memory.updatedAt))) throw new Error('Invalid manual memory record');
|
|
47
57
|
} else if (!memory.evidence?.projectId || !/^[a-f0-9]{40}$/.test(memory.evidence.commitSha || '')) throw new Error('Invalid memory evidence');
|
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// _site/_test/helpers/qa-r91-harness.cjs — P-MEMORY-DELETE-R91
|
|
3
|
+
//
|
|
4
|
+
// Copy of helpers/parallel-memory-shell.js (the P-MEMORY-01 lane harness)
|
|
5
|
+
// with exactly one product difference: the in-process knowledge engine is
|
|
6
|
+
// loaded from the R91 SOURCE CHECKOUT (sibling knowledge-engine repository,
|
|
7
|
+
// override with QA_R91_ENGINE_SRC) instead of the root's _modules pin, so
|
|
8
|
+
// the browser exercises the lane's own delete implementation. The installed
|
|
9
|
+
// core modules and the _modules pin are never touched.
|
|
10
|
+
//
|
|
11
|
+
// Everything else keeps the original lane discipline: own port/data dir/
|
|
12
|
+
// knowledge root/bridge journal home, declared stand-in analyzer, token
|
|
13
|
+
// pass-through proxy in front of the engine, real HTTP module services,
|
|
14
|
+
// real Chrome in its own profile, and no contact with the live
|
|
15
|
+
// 5757/5790/8787/8790 endpoints, the real HOME or the real memories.
|
|
16
|
+
//
|
|
17
|
+
// R99 (P-MEMORY-RELEASE-PREP-R99): startHarness({ productRoot }) — or the
|
|
18
|
+
// QA_R91_PRODUCT_ROOT env — runs the SAME isolated acceptance against an
|
|
19
|
+
// explicitly chosen OFFICIAL installed prefix: the spawned product server,
|
|
20
|
+
// its cwd and the static UI come from <productRoot>/_site/lib/server-app.js,
|
|
21
|
+
// and the in-process engine defaults to the prefix's vendored
|
|
22
|
+
// _modules/knowledge-engine (QA_R91_ENGINE_SRC still overrides). Helpers,
|
|
23
|
+
// isolation and stand-in analyzers stay canonical. Invalid roots are
|
|
24
|
+
// rejected by resolveProductRoot BEFORE any spawn; the default source-root
|
|
25
|
+
// behaviour is unchanged.
|
|
26
|
+
|
|
27
|
+
const assert = require('node:assert/strict');
|
|
28
|
+
const crypto = require('node:crypto');
|
|
29
|
+
const fs = require('node:fs');
|
|
30
|
+
const path = require('node:path');
|
|
31
|
+
const { spawnSync } = require('node:child_process');
|
|
32
|
+
|
|
33
|
+
const { spawnServer } = require('../helpers/spawn-server');
|
|
34
|
+
const { findChrome } = require('../helpers/find-chrome');
|
|
35
|
+
const { launchCdpBrowser, waitFor } = require('../helpers/cdp-browser');
|
|
36
|
+
const { attachNetworkCapture } = require('../helpers/cdp-network-capture');
|
|
37
|
+
const { createModuleFixture, createUpstreamProxy } = require('../helpers/module-fixture');
|
|
38
|
+
const freePort = require('../helpers/free-port');
|
|
39
|
+
const { createMemoryStandinAnalyzer } = require('./parallel-memory-analyzer');
|
|
40
|
+
const { createEventBridgeStandin } = require('./parallel-memory-event-bridge');
|
|
41
|
+
const { applyDriverIsolation } = require('./parallel-qa-engine-endpoint-isolate');
|
|
42
|
+
const { assertOwnedEndpoint, createEngineVectorStandin } = require('./parallel-qa-engine-endpoint-guard');
|
|
43
|
+
|
|
44
|
+
const ROOT = path.resolve(__dirname, '../../..');
|
|
45
|
+
const LIVE_PORTS = new Set([5757, 5760, 5790, 8787, 8790]);
|
|
46
|
+
const ENGINE_SRC_ENV = process.env.QA_R91_ENGINE_SRC;
|
|
47
|
+
const ENGINE_SOURCE_ROOT = path.resolve(ENGINE_SRC_ENV || path.join(ROOT, '..', 'knowledge-engine'));
|
|
48
|
+
const ENGINE_SOURCE_SERVER = path.join(ENGINE_SOURCE_ROOT, 'src', 'server.js');
|
|
49
|
+
// R99: captured at MODULE LOAD, before applyDriverIsolation can replace the
|
|
50
|
+
// process environment (the isolate helper wipes process.env). These are the
|
|
51
|
+
// REAL system locations a product root must never live in: the live npm
|
|
52
|
+
// global prefix, per-user program installs, Program Files, and any prefix
|
|
53
|
+
// surfaced through NPM_CONFIG_PREFIX. Reading them later would see the
|
|
54
|
+
// ISOLATED values and the protection would silently vanish.
|
|
55
|
+
const REAL_ENV_MARKERS = {
|
|
56
|
+
appdata: process.env.APPDATA,
|
|
57
|
+
localAppdata: process.env.LOCALAPPDATA,
|
|
58
|
+
programFiles: process.env.ProgramFiles,
|
|
59
|
+
programFilesX86: process.env['ProgramFiles(x86)'],
|
|
60
|
+
npmConfigPrefix: process.env.NPM_CONFIG_PREFIX,
|
|
61
|
+
};
|
|
62
|
+
const PRODUCT_ROOT_ENV = process.env.QA_R91_PRODUCT_ROOT;
|
|
63
|
+
const DENY_ROOTS_ENV = process.env.QA_R99_DENY_ROOTS;
|
|
64
|
+
|
|
65
|
+
const PRODUCT_SITE_ENTRY = path.join('_site', 'lib', 'server-app.js');
|
|
66
|
+
|
|
67
|
+
function invalidProductRoot(reason, detail) {
|
|
68
|
+
return Object.assign(new Error(`QA_R91_PRODUCT_ROOT ${reason}: ${detail}`), { code: 'INVALID_PRODUCT_ROOT', reason });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function pathInside(parent, child) {
|
|
72
|
+
const rel = path.relative(path.resolve(parent), path.resolve(child));
|
|
73
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// R99: validates an explicitly chosen product root for official-artifact
|
|
77
|
+
// acceptance. A product root is where a REAL installed prefix lives: the
|
|
78
|
+
// spawned server, its cwd and its static UI all come from
|
|
79
|
+
// <root>/_site/lib/server-app.js. Location rules run BEFORE the existence
|
|
80
|
+
// check, so a protected location is rejected deterministically even when the
|
|
81
|
+
// path does not exist yet. The canonical source checkout stays reachable
|
|
82
|
+
// only through the DEFAULT behaviour (no productRoot).
|
|
83
|
+
function resolveProductRoot(candidate) {
|
|
84
|
+
if (typeof candidate !== 'string' || candidate.trim() === '') {
|
|
85
|
+
throw invalidProductRoot('NOT_ABSOLUTE', `the product root must be a non-empty absolute path, got ${JSON.stringify(candidate)}`);
|
|
86
|
+
}
|
|
87
|
+
if (!path.isAbsolute(candidate)) {
|
|
88
|
+
throw invalidProductRoot('NOT_ABSOLUTE', `relative product root ${JSON.stringify(candidate)} — the official prefix must be given as an absolute path`);
|
|
89
|
+
}
|
|
90
|
+
const root = path.resolve(candidate);
|
|
91
|
+
|
|
92
|
+
const protectedLocations = [];
|
|
93
|
+
if (REAL_ENV_MARKERS.appdata) protectedLocations.push({ label: 'the live npm global prefix', dir: path.join(REAL_ENV_MARKERS.appdata, 'npm') });
|
|
94
|
+
if (REAL_ENV_MARKERS.localAppdata) protectedLocations.push({ label: 'per-user program installs', dir: path.join(REAL_ENV_MARKERS.localAppdata, 'Programs') });
|
|
95
|
+
if (REAL_ENV_MARKERS.programFiles) protectedLocations.push({ label: 'Program Files', dir: REAL_ENV_MARKERS.programFiles });
|
|
96
|
+
if (REAL_ENV_MARKERS.programFilesX86) protectedLocations.push({ label: 'Program Files (x86)', dir: REAL_ENV_MARKERS.programFilesX86 });
|
|
97
|
+
if (REAL_ENV_MARKERS.npmConfigPrefix) protectedLocations.push({ label: 'NPM_CONFIG_PREFIX', dir: REAL_ENV_MARKERS.npmConfigPrefix });
|
|
98
|
+
for (const deny of (DENY_ROOTS_ENV || '').split(path.delimiter).map(entry => entry.trim()).filter(Boolean)) {
|
|
99
|
+
protectedLocations.push({ label: 'QA_R99_DENY_ROOTS', dir: deny });
|
|
100
|
+
}
|
|
101
|
+
for (const spot of protectedLocations) {
|
|
102
|
+
if (pathInside(spot.dir, root)) throw invalidProductRoot('PROTECTED_LOCATION', `${root} is inside ${spot.label} ${spot.dir} — the real installed prefix must never be a test root`);
|
|
103
|
+
}
|
|
104
|
+
// A product root must be a separate installed location: the canonical
|
|
105
|
+
// source checkout and the engine source checkout are not official prefixes
|
|
106
|
+
// (and overlapping them in either direction would blur source/default runs).
|
|
107
|
+
for (const spot of [{ label: 'the canonical test repository', dir: ROOT }, { label: 'the engine source checkout', dir: ENGINE_SOURCE_ROOT }]) {
|
|
108
|
+
if (pathInside(spot.dir, root) || pathInside(root, spot.dir)) throw invalidProductRoot('PROTECTED_SOURCE_ROOT', `${root} overlaps ${spot.label} ${spot.dir}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!fs.existsSync(root)) throw invalidProductRoot('MISSING_ROOT', `${root} does not exist`);
|
|
112
|
+
const siteEntry = path.join(root, PRODUCT_SITE_ENTRY);
|
|
113
|
+
if (!fs.existsSync(siteEntry)) throw invalidProductRoot('MISSING_SITE_ENTRY', `${siteEntry} not found — not a product prefix`);
|
|
114
|
+
let packageJson;
|
|
115
|
+
try {
|
|
116
|
+
packageJson = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
117
|
+
} catch (error) {
|
|
118
|
+
throw invalidProductRoot('INVALID_PACKAGE', `package.json unreadable: ${error.message}`);
|
|
119
|
+
}
|
|
120
|
+
if (!packageJson || typeof packageJson.name !== 'string' || !packageJson.name || typeof packageJson.version !== 'string' || !packageJson.version) {
|
|
121
|
+
throw invalidProductRoot('INVALID_PACKAGE', `package.json must carry name and version, got ${JSON.stringify({ name: packageJson && packageJson.name, version: packageJson && packageJson.version })}`);
|
|
122
|
+
}
|
|
123
|
+
return { root, packageJson, siteEntry, engineRoot: path.join(root, '_modules', 'knowledge-engine') };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function r91CreateEngineServer(options = {}, engineSourceRoot = ENGINE_SOURCE_ROOT) {
|
|
127
|
+
// Same fail-closed endpoint discipline as the R5 guard, but the module is
|
|
128
|
+
// the R91 source checkout under review (or, under a product root, the
|
|
129
|
+
// prefix's vendored engine).
|
|
130
|
+
const engineServerPath = path.join(engineSourceRoot, 'src', 'server.js');
|
|
131
|
+
if (!fs.existsSync(engineServerPath)) throw new Error(`R91 engine source not found: ${engineServerPath}`);
|
|
132
|
+
assertOwnedEndpoint(options.vectorUrl, { role: 'vectorUrl' });
|
|
133
|
+
if (options.bridge === undefined) assertOwnedEndpoint(options.bridgeUrl, { role: 'bridgeUrl' });
|
|
134
|
+
const { createEngineServer } = require(engineServerPath);
|
|
135
|
+
return createEngineServer(options);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function git(cwd, args) {
|
|
139
|
+
const result = spawnSync('git', ['-C', cwd, ...args], { encoding: 'utf8', windowsHide: true, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } });
|
|
140
|
+
assert.equal(result.status, 0, `git ${args.join(' ')} failed: ${result.stderr}`);
|
|
141
|
+
return String(result.stdout || '').trim();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** A real Git repository with real commits: the product imports it for real. */
|
|
145
|
+
function makeRepo(root, name, files = { 'README.md': `# ${name}\n` }) {
|
|
146
|
+
const repo = path.join(root, name);
|
|
147
|
+
fs.mkdirSync(repo, { recursive: true });
|
|
148
|
+
git(repo, ['init', '-q', '-b', 'main']);
|
|
149
|
+
git(repo, ['config', 'user.email', 'qa-r91@local.invalid']);
|
|
150
|
+
git(repo, ['config', 'user.name', 'QA R91 Regression']);
|
|
151
|
+
git(repo, ['config', 'commit.gpgsign', 'false']);
|
|
152
|
+
for (const [file, content] of Object.entries(files)) fs.writeFileSync(path.join(repo, file), content, 'utf8');
|
|
153
|
+
git(repo, ['add', '.']);
|
|
154
|
+
git(repo, ['commit', '-qm', 'baseline']);
|
|
155
|
+
return { path: repo, headCommit: git(repo, ['rev-parse', 'HEAD']) };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function commitFile(repo, file, content, message = 'follow-up') {
|
|
159
|
+
const full = path.join(repo.path, file);
|
|
160
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
161
|
+
// R94: every fixture commit must be MEANINGFUL. Re-committing identical
|
|
162
|
+
// bytes leaves nothing to stage and fails the git assert; instead the file
|
|
163
|
+
// gains a distinct revision line so the repository always moves forward
|
|
164
|
+
// with a real, distinct commit.
|
|
165
|
+
let revision = 0;
|
|
166
|
+
do {
|
|
167
|
+
fs.writeFileSync(full, revision === 0 ? content : `${content}\n// qa-revision-${revision}\n`, 'utf8');
|
|
168
|
+
revision += 1;
|
|
169
|
+
} while (git(repo.path, ['status', '--porcelain']).length === 0 && revision < 50);
|
|
170
|
+
git(repo.path, ['add', '.']);
|
|
171
|
+
git(repo.path, ['commit', '-qm', message]);
|
|
172
|
+
repo.headCommit = git(repo.path, ['rev-parse', 'HEAD']);
|
|
173
|
+
return repo.headCommit;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function startHarness({ tag, evidenceDir, productRoot, viewport = { width: 1600, height: 900 } } = {}) {
|
|
177
|
+
// R99: validate an explicit product root BEFORE isolation and BEFORE any
|
|
178
|
+
// spawn — a bad root fails synchronously here, no child process starts.
|
|
179
|
+
// Without productRoot (option or QA_R91_PRODUCT_ROOT env) the default
|
|
180
|
+
// source-root behaviour is byte-for-byte unchanged.
|
|
181
|
+
const requestedProductRoot = productRoot || PRODUCT_ROOT_ENV || null;
|
|
182
|
+
const product = requestedProductRoot ? resolveProductRoot(requestedProductRoot) : null;
|
|
183
|
+
// Engine source: an explicit QA_R91_ENGINE_SRC always wins; under a product
|
|
184
|
+
// root the default becomes the prefix's vendored engine; otherwise the
|
|
185
|
+
// sibling source checkout (unchanged default).
|
|
186
|
+
const harnessEngineSource = ENGINE_SRC_ENV ? ENGINE_SOURCE_ROOT : (product ? product.engineRoot : ENGINE_SOURCE_ROOT);
|
|
187
|
+
if (product && !fs.existsSync(path.join(harnessEngineSource, 'src', 'server.js'))) {
|
|
188
|
+
throw invalidProductRoot('MISSING_PRODUCT_ENGINE', `no vendored engine under the official prefix: ${path.join(harnessEngineSource, 'src', 'server.js')} (set QA_R91_ENGINE_SRC to override)`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const applied = applyDriverIsolation({ prefix: `pk-qa-r91-${tag}`, captureChrome: true });
|
|
192
|
+
const tempRoot = applied.layout.temp;
|
|
193
|
+
|
|
194
|
+
const fixture = fs.mkdtempSync(path.join(tempRoot, `pk-qa-r91-${tag}-`));
|
|
195
|
+
const evidence = evidenceDir || path.join(fixture, 'evidence');
|
|
196
|
+
fs.mkdirSync(evidence, { recursive: true });
|
|
197
|
+
const dataDir = path.join(fixture, 'data');
|
|
198
|
+
const knowledgeRoot = path.join(fixture, 'knowledge');
|
|
199
|
+
fs.mkdirSync(knowledgeRoot, { recursive: true });
|
|
200
|
+
|
|
201
|
+
const plan = { document: '', memoryCandidates: [] };
|
|
202
|
+
const engineToken = crypto.randomBytes(24).toString('hex');
|
|
203
|
+
const engineVector = createEngineVectorStandin({ label: 'r91-engine-vector' });
|
|
204
|
+
await engineVector.ready;
|
|
205
|
+
const terminal = createModuleFixture({ kind: 'terminal', tag: `${tag}-terminal` });
|
|
206
|
+
const vectorHub = createModuleFixture({ kind: 'vectorHub', tag: `${tag}-vectorhub` });
|
|
207
|
+
const eventBridge = await createEventBridgeStandin({ tag: `${tag}-eventbridge` });
|
|
208
|
+
await terminal.start();
|
|
209
|
+
await vectorHub.start();
|
|
210
|
+
await eventBridge.start();
|
|
211
|
+
|
|
212
|
+
const engineDataDir = path.join(fixture, 'engine');
|
|
213
|
+
// R94: `let`, not `const` — restartEngine reassigns this handle so cleanup
|
|
214
|
+
// and every in-process helper always act on the LIVE engine (closing the
|
|
215
|
+
// closed original left the restarted server handle open and hung Node).
|
|
216
|
+
let engine = r91CreateEngineServer({
|
|
217
|
+
dataDir: engineDataDir,
|
|
218
|
+
runtimeToken: engineToken,
|
|
219
|
+
debug: true,
|
|
220
|
+
consume: false,
|
|
221
|
+
analyzer: createMemoryStandinAnalyzer({ plan }),
|
|
222
|
+
vectorUrl: engineVector.url,
|
|
223
|
+
bridgeUrl: eventBridge.url,
|
|
224
|
+
}, harnessEngineSource);
|
|
225
|
+
await engine.ready;
|
|
226
|
+
await new Promise(resolve => engine.server.listen(0, '127.0.0.1', resolve));
|
|
227
|
+
const enginePort = engine.server.address().port;
|
|
228
|
+
assert.equal(LIVE_PORTS.has(enginePort), false, 'the lane engine must never use a live port');
|
|
229
|
+
const engineUrl = `http://127.0.0.1:${enginePort}`;
|
|
230
|
+
const engineProxy = createUpstreamProxy({ target: engineUrl, token: engineToken, tag: 'engine' });
|
|
231
|
+
await engineProxy.start();
|
|
232
|
+
|
|
233
|
+
const port = await freePort();
|
|
234
|
+
const base = `http://127.0.0.1:${port}`;
|
|
235
|
+
const server = spawnServer({
|
|
236
|
+
// R99: under a product root the SERVER, its cwd and the static UI come
|
|
237
|
+
// from the official prefix; helpers and fixtures stay canonical.
|
|
238
|
+
root: product ? product.root : ROOT,
|
|
239
|
+
port,
|
|
240
|
+
dataDir,
|
|
241
|
+
tag: `qa-r91-${tag}`,
|
|
242
|
+
analysisOwner: 'engine',
|
|
243
|
+
extraEnv: {
|
|
244
|
+
KB_MODULES_REGISTER: '1',
|
|
245
|
+
KB_MODULES_AUTOSTART: '0',
|
|
246
|
+
KB_TERMINAL_URL: terminal.url,
|
|
247
|
+
KB_VECTORHUB_URL: vectorHub.url,
|
|
248
|
+
KB_EVENTBRIDGE_URL: eventBridge.url,
|
|
249
|
+
KB_KNOWLEDGE_ENGINE_URL: engineProxy.url,
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const productApi = async (method, route, body) => {
|
|
254
|
+
const response = await fetch(base + route, {
|
|
255
|
+
method,
|
|
256
|
+
headers: body === undefined ? undefined : { 'Content-Type': 'application/json' },
|
|
257
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
258
|
+
signal: AbortSignal.timeout(20000),
|
|
259
|
+
});
|
|
260
|
+
return { status: response.status, body: await response.json().catch(() => null) };
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const engineApi = async (method, route, body) => {
|
|
264
|
+
const response = await fetch(engineUrl + route, {
|
|
265
|
+
method,
|
|
266
|
+
headers: {
|
|
267
|
+
'Content-Type': 'application/json',
|
|
268
|
+
...(engineToken ? { Authorization: `Bearer ${engineToken}` } : {}),
|
|
269
|
+
},
|
|
270
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
271
|
+
signal: AbortSignal.timeout(20000),
|
|
272
|
+
});
|
|
273
|
+
return { status: response.status, body: await response.json().catch(() => null) };
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
await waitFor(async () => {
|
|
277
|
+
try { return (await productApi('GET', '/api/health')).status === 200; } catch { return false; }
|
|
278
|
+
}, 'the isolated product instance', 30000);
|
|
279
|
+
const settings = await productApi('PATCH', '/api/settings', { knowledge: { rootPath: knowledgeRoot } });
|
|
280
|
+
assert.equal(settings.status, 200, `the isolated instance accepts its own knowledge root: ${JSON.stringify(settings.body)}`);
|
|
281
|
+
|
|
282
|
+
let browser = null;
|
|
283
|
+
let network = null;
|
|
284
|
+
try {
|
|
285
|
+
browser = await launchCdpBrowser({
|
|
286
|
+
chrome: findChrome(),
|
|
287
|
+
profileDir: path.join(fixture, 'browser'),
|
|
288
|
+
url: `${base}/`,
|
|
289
|
+
width: viewport.width,
|
|
290
|
+
height: viewport.height,
|
|
291
|
+
});
|
|
292
|
+
await browser.setViewport(viewport.width, viewport.height);
|
|
293
|
+
await browser.send('Network.enable');
|
|
294
|
+
network = attachNetworkCapture(browser);
|
|
295
|
+
} catch (error) {
|
|
296
|
+
await server.cleanup();
|
|
297
|
+
await engineProxy.close();
|
|
298
|
+
await terminal.close();
|
|
299
|
+
await vectorHub.close();
|
|
300
|
+
await eventBridge.close();
|
|
301
|
+
await engine.close().catch(() => {});
|
|
302
|
+
fs.rmSync(fixture, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const run = expression => browser.evaluate(expression);
|
|
307
|
+
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
308
|
+
|
|
309
|
+
const reload = async () => {
|
|
310
|
+
const startedAt = Date.now();
|
|
311
|
+
await run('window.__pkShellReloadMark = Date.now(), true');
|
|
312
|
+
await browser.send('Page.reload', {});
|
|
313
|
+
await waitFor(
|
|
314
|
+
() => run("document.readyState === 'complete' && typeof window.__pkShellReloadMark === 'undefined' && document.querySelector('.view.active') !== null"),
|
|
315
|
+
'the reloaded page is the fresh document and its boot finished',
|
|
316
|
+
20000,
|
|
317
|
+
);
|
|
318
|
+
return Date.now() - startedAt;
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const selectOption = async (selector, value) => {
|
|
322
|
+
const changed = await run(`(() => {
|
|
323
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
324
|
+
if (!el) return { missing: true };
|
|
325
|
+
el.value = ${JSON.stringify(value)};
|
|
326
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
327
|
+
return { value: el.value };
|
|
328
|
+
})()`);
|
|
329
|
+
assert.ok(changed && !changed.missing, `the select ${selector} must exist`);
|
|
330
|
+
assert.equal(changed.value, value, `the select ${selector} must accept the value ${value}`);
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const nativeClick = async selector => {
|
|
334
|
+
const probe = await run(`(() => {
|
|
335
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
336
|
+
if (!el) return { missing: true };
|
|
337
|
+
const rect = el.getBoundingClientRect();
|
|
338
|
+
if (rect.width <= 0 || rect.height <= 0) return { invisible: true };
|
|
339
|
+
const x = rect.left + rect.width / 2;
|
|
340
|
+
const y = rect.top + rect.height / 2;
|
|
341
|
+
const hit = document.elementFromPoint(x, y);
|
|
342
|
+
return { x, y, hitIsTarget: Boolean(hit && (hit === el || el.contains(hit))), label: (el.textContent || '').trim().slice(0, 40) };
|
|
343
|
+
})()`);
|
|
344
|
+
assert.ok(probe && !probe.missing, `the control ${selector} must exist in the page`);
|
|
345
|
+
assert.ok(!probe.invisible, `the control ${selector} must be visible`);
|
|
346
|
+
assert.ok(probe.hitIsTarget, `the control ${selector} must be the hit target; something else is on top`);
|
|
347
|
+
const common = { x: probe.x, y: probe.y, button: 'left', clickCount: 1 };
|
|
348
|
+
await browser.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: probe.x, y: probe.y, button: 'none', clickCount: 0 });
|
|
349
|
+
await browser.send('Input.dispatchMouseEvent', { type: 'mousePressed', ...common });
|
|
350
|
+
await browser.send('Input.dispatchMouseEvent', { type: 'mouseReleased', ...common });
|
|
351
|
+
return probe;
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const typeText = async text => {
|
|
355
|
+
for (const character of String(text)) {
|
|
356
|
+
const upper = character.toUpperCase();
|
|
357
|
+
const isLower = character >= 'a' && character <= 'z';
|
|
358
|
+
const isUpper = character >= 'A' && character <= 'Z';
|
|
359
|
+
const isDigit = character >= '0' && character <= '9';
|
|
360
|
+
const code = isLower ? `Key${upper}` : isUpper ? `Key${character}` : isDigit ? `Digit${character}` : '';
|
|
361
|
+
const keyCode = isLower ? upper.charCodeAt(0) : isUpper || isDigit ? character.charCodeAt(0) : 0;
|
|
362
|
+
const base = { key: character, code, windowsVirtualKeyCode: keyCode, nativeVirtualKeyCode: keyCode };
|
|
363
|
+
await browser.send('Input.dispatchKeyEvent', { type: 'keyDown', ...base, text: character, unmodifiedText: character });
|
|
364
|
+
await browser.send('Input.dispatchKeyEvent', { type: 'keyUp', ...base });
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const importProject = async (repo, displayName, { domainId } = {}) => {
|
|
369
|
+
const knowledgePath = path.join(knowledgeRoot, displayName);
|
|
370
|
+
fs.mkdirSync(knowledgePath, { recursive: true });
|
|
371
|
+
const result = await productApi('POST', '/api/projects/import', { localPath: repo.path, knowledgePath, displayName });
|
|
372
|
+
assert.equal(result.status, 201, `the project import must succeed: ${JSON.stringify(result.body)}`);
|
|
373
|
+
const projectId = result.body.projectId;
|
|
374
|
+
if (domainId) {
|
|
375
|
+
const patched = await productApi('PATCH', `/api/projects/${encodeURIComponent(projectId)}`, { domainId });
|
|
376
|
+
assert.equal(patched.status, 200, `the domain binding must succeed: ${JSON.stringify(patched.body)}`);
|
|
377
|
+
}
|
|
378
|
+
return { projectId, project: result.body.project, knowledgePath, modules: result.body.modules };
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const addDomain = async name => {
|
|
382
|
+
const result = await productApi('POST', '/api/domains', { name });
|
|
383
|
+
assert.equal(result.status, 201, `the brain domain must be accepted: ${JSON.stringify(result.body)}`);
|
|
384
|
+
return result.body.domain;
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const brainScopePath = () => {
|
|
388
|
+
const scope = engine.store.state.sharedScopes.find(entry => entry.kind === 'brain');
|
|
389
|
+
assert.ok(scope, 'the engine must know the brain scope');
|
|
390
|
+
return scope.knowledgePath;
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const productMemories = async () => {
|
|
394
|
+
const snapshot = await productApi('GET', '/api/engine/snapshot?summary=1&limit=50');
|
|
395
|
+
assert.equal(snapshot.status, 200, `the product snapshot must be readable: ${JSON.stringify(snapshot.body)}`);
|
|
396
|
+
return snapshot.body.memories || [];
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const injectedMemories = async projectId => {
|
|
400
|
+
const context = await engineApi('POST', '/api/context', { projectId, query: '长期记忆', task: 'qa-r91' });
|
|
401
|
+
assert.equal(context.status, 200, `the engine context must be readable: ${JSON.stringify(context.body)}`);
|
|
402
|
+
return context.body.memories || [];
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const memoryCandidate = ({ text, event, scopeType = 'global' }) => ({
|
|
406
|
+
text,
|
|
407
|
+
quote: text,
|
|
408
|
+
scopeType,
|
|
409
|
+
sourceEventId: event.eventId,
|
|
410
|
+
sourceHash: event.contentHash,
|
|
411
|
+
eligibility: {
|
|
412
|
+
durable: true,
|
|
413
|
+
crossProject: true,
|
|
414
|
+
reason: '人工明确表达的长期偏好',
|
|
415
|
+
scopeReason: scopeType === 'global' ? '适用于所有领域与项目' : '适用于指定领域',
|
|
416
|
+
},
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const ingestUserEvent = async ({ projectId, commitSha, content, inputOrigin = 'human' }) => {
|
|
420
|
+
const id = `event-${crypto.randomUUID()}`;
|
|
421
|
+
const turn = `turn-${crypto.randomUUID()}`;
|
|
422
|
+
const result = await engine.coordinator.conversations.appendEvent(projectId, {
|
|
423
|
+
eventId: id,
|
|
424
|
+
sequence: 1,
|
|
425
|
+
source: 'claude-code',
|
|
426
|
+
eventType: 'user_prompt',
|
|
427
|
+
role: 'user',
|
|
428
|
+
inputOrigin,
|
|
429
|
+
content,
|
|
430
|
+
sessionId: `session-${projectId}`,
|
|
431
|
+
turnId: turn,
|
|
432
|
+
capturedAt: new Date().toISOString(),
|
|
433
|
+
identityConfidence: 'exact',
|
|
434
|
+
explicitCommitSha: commitSha,
|
|
435
|
+
});
|
|
436
|
+
return { eventId: id, turnId: turn, contentHash: result.event.contentHash, inputOrigin: result.event.inputOrigin };
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const triggerRun = async (projectId, commitSha, timeoutMs = 60000) => {
|
|
440
|
+
const started = await engineApi('POST', '/api/runs', { projectId, commitSha });
|
|
441
|
+
assert.equal(started.status, 200, `the run must be accepted: ${JSON.stringify(started.body)}`);
|
|
442
|
+
const runId = started.body.run.runId;
|
|
443
|
+
const deadline = Date.now() + timeoutMs;
|
|
444
|
+
let last = null;
|
|
445
|
+
while (Date.now() < deadline) {
|
|
446
|
+
const current = await engineApi('GET', `/api/runs/${encodeURIComponent(runId)}`);
|
|
447
|
+
last = current.body && current.body.run;
|
|
448
|
+
if (last && ['succeeded', 'failed'].includes(last.state)) {
|
|
449
|
+
if (last.state === 'failed') throw new Error(`the run failed: ${JSON.stringify(last.error)}`);
|
|
450
|
+
return last;
|
|
451
|
+
}
|
|
452
|
+
if (last && last.promotedAt && last.memoryCapture) return last;
|
|
453
|
+
await wait(150);
|
|
454
|
+
}
|
|
455
|
+
throw new Error(`the run did not finish in time: ${JSON.stringify(last)}`);
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const produceCandidate = async ({ repo, projectId, text, scopeType = 'global', file = 'src/candidate.js' }) => {
|
|
459
|
+
const head = commitFile(repo, file, `// ${text}\n`, 'candidate commit');
|
|
460
|
+
const event = await ingestUserEvent({ projectId, commitSha: head, content: `人工要求:${text}` });
|
|
461
|
+
plan.document = `# 冻结证据\n\n${text}\n`;
|
|
462
|
+
plan.memoryCandidates = [memoryCandidate({ text, event, scopeType })];
|
|
463
|
+
const run = await triggerRun(projectId, head);
|
|
464
|
+
return { run, event, commitSha: head, capture: run.memoryCapture };
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
// Restart the in-process engine on its own data dir and port: the shell's
|
|
468
|
+
// proxy keeps its target, so the browser sees a real engine restart.
|
|
469
|
+
// R94: the handle is REASSIGNED, so cleanup()/brainScopePath() always act
|
|
470
|
+
// on the live restarted engine, never on the already-closed original.
|
|
471
|
+
const restartEngine = async () => {
|
|
472
|
+
await engine.close();
|
|
473
|
+
engine = r91CreateEngineServer({
|
|
474
|
+
dataDir: engineDataDir,
|
|
475
|
+
runtimeToken: engineToken,
|
|
476
|
+
debug: true,
|
|
477
|
+
consume: false,
|
|
478
|
+
analyzer: createMemoryStandinAnalyzer({ plan }),
|
|
479
|
+
vectorUrl: engineVector.url,
|
|
480
|
+
bridgeUrl: eventBridge.url,
|
|
481
|
+
}, harnessEngineSource);
|
|
482
|
+
await engine.ready;
|
|
483
|
+
await new Promise(resolve => engine.server.listen(enginePort, '127.0.0.1', resolve));
|
|
484
|
+
return engine;
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
// R94: every cleanup step is bounded. A wedged child or an uncooperative
|
|
488
|
+
// close used to turn the whole driver into an unkillable lane process (the
|
|
489
|
+
// Node 17840 hang was exactly this); each step now gets a deadline and a
|
|
490
|
+
// failure is reported instead of blocking the teardown forever.
|
|
491
|
+
const cleanupStep = async (label, task, deadlineMs = 15000) => {
|
|
492
|
+
let timer = null;
|
|
493
|
+
try {
|
|
494
|
+
await Promise.race([
|
|
495
|
+
Promise.resolve().then(task).catch(error => { console.error(`[qa-r91-harness] cleanup step ${label} failed:`, error && error.message); }),
|
|
496
|
+
new Promise(resolve => { timer = setTimeout(resolve, deadlineMs); if (typeof timer.unref === 'function') timer.unref(); }),
|
|
497
|
+
]);
|
|
498
|
+
} finally {
|
|
499
|
+
clearTimeout(timer);
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
const cleanup = async () => {
|
|
504
|
+
await cleanupStep('browser', () => browser?.close() || Promise.resolve());
|
|
505
|
+
await cleanupStep('server', () => server.cleanup());
|
|
506
|
+
await cleanupStep('engineProxy', () => engineProxy.close());
|
|
507
|
+
await cleanupStep('terminal', () => terminal.close());
|
|
508
|
+
await cleanupStep('vectorHub', () => vectorHub.close());
|
|
509
|
+
await cleanupStep('eventBridge', () => eventBridge.close());
|
|
510
|
+
await cleanupStep('engine', () => engine.close());
|
|
511
|
+
await cleanupStep('engineVector', () => engineVector.close());
|
|
512
|
+
fs.rmSync(fixture, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
return {
|
|
516
|
+
fixture, evidence, dataDir, knowledgeRoot, tempRoot, plan,
|
|
517
|
+
base, server, productApi, engineApi, engineUrl, enginePort, engineProxy, engineDataDir,
|
|
518
|
+
terminal, vectorHub, eventBridge, engineVector,
|
|
519
|
+
// R94: a getter, not a snapshot — harness.engine always resolves to the
|
|
520
|
+
// LIVE engine handle after restartEngine().
|
|
521
|
+
get engine() { return engine; },
|
|
522
|
+
browser, run, wait, waitFor, network, reload,
|
|
523
|
+
selectOption, nativeClick, typeText,
|
|
524
|
+
importProject, addDomain, ingestUserEvent, triggerRun,
|
|
525
|
+
brainScopePath, productMemories, injectedMemories,
|
|
526
|
+
memoryCandidate, produceCandidate, restartEngine,
|
|
527
|
+
cleanup,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
module.exports = { startHarness, makeRepo, commitFile, resolveProductRoot, ROOT, ENGINE_SOURCE_ROOT, REAL_ENV_MARKERS, PRODUCT_SITE_ENTRY };
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": "project-knowledge/module-content/v1",
|
|
3
3
|
"productFiles": {
|
|
4
|
-
"ui/index.html": "
|
|
4
|
+
"ui/index.html": "e7758ae86968ea78f6f7ad591def9695bb4551056d7a6ac54adad6aafe381733",
|
|
5
5
|
"ui/knowledge-panel.html": "dfd85f15759472252d797853263e9da0054b251c56bb9ccdc8a75e6301334ca8",
|
|
6
|
-
"ui/product.js": "
|
|
6
|
+
"ui/product.js": "8c0d9b64872f5770f14e525401f9cce8b5dcd3c4c78977c83ade6258b093c57d",
|
|
7
7
|
"ui/live-client.js": "0d81fa894c629bb839e1931fc3c5ebad4d831aae8232d6ce8c0f542e9e3559e8",
|
|
8
|
-
"ui/product.css": "
|
|
8
|
+
"ui/product.css": "d24e57fcd49024ca44a49b80099a60258a34f553029e60c203a54527645d7e27",
|
|
9
9
|
"ui/favicon.svg": "c48be946b224112ebb0238b44263f7a61bda31e531afa895c2e9801c0ff24f32",
|
|
10
10
|
"plugins/project-knowledge/hooks/claude.json": "d61510327e3e1facbb4c645228e9f50cad05a88b33d80c4c22e5ba9f8b9f5359",
|
|
11
11
|
"plugins/project-knowledge/hooks/codex.json": "216e03c07295606ef6ebb2c4f669ad0d790b5e7c4f3f34600488033db75e4239",
|
|
12
12
|
"plugins/project-knowledge/scripts/context-hook.cjs": "da666153ad5723592cce42d25337ae649f7d93e43fab76b3c6d1263933de063f",
|
|
13
13
|
"plugins/project-knowledge/scripts/mcp-launcher.cjs": "7c68830b5c73eb53ef030756a4de33987f7eab6b1a6cc15602835aac3f954893",
|
|
14
|
-
"plugins/project-knowledge/.claude-plugin/plugin.json": "
|
|
15
|
-
"plugins/project-knowledge/.codex-plugin/plugin.json": "
|
|
16
|
-
"plugins/project-knowledge/.mcp.json": "
|
|
14
|
+
"plugins/project-knowledge/.claude-plugin/plugin.json": "05540c83b56c758154c1b91e066f7e78ddfb98a4fab92e9f1b8dcea85bb9147e",
|
|
15
|
+
"plugins/project-knowledge/.codex-plugin/plugin.json": "065dceaa1cb91d3711f03887aba4bfbb4ff7f3cc6e21561b54b9ee79e33f74c4",
|
|
16
|
+
"plugins/project-knowledge/.mcp.json": "56b134a154c2a6b7814dd7efa52bdf9da279af45409b3df9be24c649f0ba616f",
|
|
17
17
|
"ui/version-info.js": "01a82217b031de54b80d6c7974da9c70888c4c90a046bd4248a808f7989ddce7",
|
|
18
18
|
"ui/version-info.css": "209fad8e42b661f72d2aba3280e1ca47768b5cefb776fe5459538d34fbad120f"
|
|
19
19
|
},
|
|
@@ -21,14 +21,14 @@
|
|
|
21
21
|
"terminal": {
|
|
22
22
|
"name": "@sanqianx/claude-ai-workbench",
|
|
23
23
|
"installName": "claude-ai-workbench",
|
|
24
|
-
"version": "0.13.
|
|
24
|
+
"version": "0.13.12",
|
|
25
25
|
"files": {
|
|
26
26
|
"LICENSE": "c1b9df1275e769f3dbab000d1e457a2d4b0f28eb5da6c77e48dc37eeba202ed7",
|
|
27
|
-
"README.md": "
|
|
27
|
+
"README.md": "557784a0c9c60e57c2ab0ad7d4d3a34f6c35198d021165e3adc961008baed056",
|
|
28
28
|
"apps/agent-terminal/app.css": "6198b425dc1957380e7adc25212b4dc9345b58331976d19b374d2b22b682a18b",
|
|
29
|
-
"apps/agent-terminal/app.js": "
|
|
29
|
+
"apps/agent-terminal/app.js": "4f469f3e83d9778bb70d1d3ed737b6269f007f0764ae9e7da8dcc2594b23033c",
|
|
30
30
|
"apps/agent-terminal/conversation-composer.js": "4f0135fa61f3a19de9bc518cb7001dcc333a793db6de643d4ee6eaa629489c58",
|
|
31
|
-
"apps/agent-terminal/index.html": "
|
|
31
|
+
"apps/agent-terminal/index.html": "8850d107bc2b9b118be09b4bdd0a57cbd56774ae2a9e68770fc907791ca0dd30",
|
|
32
32
|
"apps/agent-terminal/panel-host-proof.html": "b4b5c69cbd4d019002dd63b364523875f9b5e58c75c8fa873ed05d3717229fa6",
|
|
33
33
|
"apps/agent-terminal/panel-proof-content.html": "87f59cf76fc5e78aa24d51432d358aa15fc8242f469cabb164bf181d23f85778",
|
|
34
34
|
"apps/agent-terminal/right-dock-panel-registry.js": "07a4b6d31b1b2d678f92b6f0786cf49129708bd38ef4a15976d824150cbadec6",
|
|
@@ -38,30 +38,29 @@
|
|
|
38
38
|
"apps/ai-profile-settings-host/index.html": "bc158f68954506d2b8e43c690f28dbe931eff9a02b1524855ac17e74ea4df736",
|
|
39
39
|
"apps/ai-profile-settings-host/page.css": "8b75cbf7f1ea5aaa92f2889390720ece310a8fd4b9ca38c890fa447d4323a557",
|
|
40
40
|
"apps/ai-profile-settings-host/page.js": "8ab508cb50f9765d148cd93f4d4f4f440f94731dbf43850cfb51cca1c869b070",
|
|
41
|
-
"apps/minimal-test-host/README.md": "fa73136f4ee3865c2d91eb97b2e8c03679aa4688e44c1e1c8ec6e27d604b0de5",
|
|
42
41
|
"apps/minimal-test-host/host.css": "b870f0816a1fcd27b73ed20b4785b46f95729e55a6a5a56624234c74a012a915",
|
|
43
42
|
"apps/minimal-test-host/host.js": "c316871e616afe66898eb22a9fa71d8ed530886cdad1b1eb6ae870373994962a",
|
|
44
43
|
"apps/minimal-test-host/index.html": "a43ca39a89d9877bc658efc02f9de26ecd68356bd35078dfa440e11b7ba11394",
|
|
45
44
|
"apps/minimal-test-host/optional-mock/fault-injection.js": "4749c8210e5a2b9eab5c8b8188b9c9b2ce9dfc9bcb5eeb0d2d745042453f87f5",
|
|
45
|
+
"apps/minimal-test-host/README.md": "fa73136f4ee3865c2d91eb97b2e8c03679aa4688e44c1e1c8ec6e27d604b0de5",
|
|
46
46
|
"docs/sdk/FILE_LAYOUT.md": "17882f96505c8161bfb66876684b9d2d3f50021a111b8b844502d3cb4e97870a",
|
|
47
47
|
"docs/sdk/INTEGRATION.md": "d4c0c80d2d2fc610205102d703a429bfaa20f9f3a48f1718f5bab02ee90138df",
|
|
48
|
+
"docs/sdk/openapi.yaml": "3423cba522bfe441c28e0cb44f524a9bdd8fa7a2341ec1fc6b26d8af05940a37",
|
|
48
49
|
"docs/sdk/README.md": "7ab591d39c36a3b84cfdf487ff567040ec453975f3ef0fba86dc822094667b61",
|
|
49
50
|
"docs/sdk/SESSION_EVENTS.md": "a964f85616ae55e0743e5cf75bad43dee2f9055acad301b134b3553e825102d7",
|
|
50
|
-
"
|
|
51
|
-
"
|
|
51
|
+
"package.json": "8b4b3c26344c0b5b421c7b58ea8d6415673327a6237e7ba4e393837c871f3724",
|
|
52
|
+
"packages/client/index.d.ts": "4c4a00577ad427215d9b91d6794a98a4c0ede06bafcf7c9904e53363c7c8bf9d",
|
|
53
|
+
"packages/client/index.js": "5458fa35fd260178927ad6eb6e1a84cd8cce3e173260acf46872554e3e36ad65",
|
|
54
|
+
"packages/client/package.json": "f0974f7ecd19620b2917c7d19542e6d308a49e1c18326b2bcdfe1d4bba41ae46",
|
|
52
55
|
"packages/client/README.md": "e0962d561dad6b50837b0f8099f6931aad14d2dabb683556b88aae0ff485b675",
|
|
53
|
-
"packages/client/index.d.ts": "0366cc96ff2e83814d0dcad34899a26068f7ab5d0b15f9f1c02a7e976db28215",
|
|
54
|
-
"packages/client/index.js": "493938f0ff357aea6db136fca216c6cfcb542d51f6628e1994a179b5ec296a12",
|
|
55
|
-
"packages/client/package.json": "65ee054aa54e3de419922f99b219ce144bd71397d7c82d585415839765553b30",
|
|
56
|
-
"packages/contracts/README.md": "44d83a3cc508cec6c5c20dfa1f2c08d5ea03e3afb2853639fdc0a9cc8eb5b9c8",
|
|
57
56
|
"packages/contracts/index.d.ts": "2947ab3e5d4408afa7af5095e23733e983c6845994217cab202e737268f61da1",
|
|
58
|
-
"packages/contracts/index.js": "
|
|
59
|
-
"packages/contracts/package.json": "
|
|
60
|
-
"packages/
|
|
57
|
+
"packages/contracts/index.js": "253f530ce74028e77748a09b5d32d775a9fb3e7f09c867613b06e074db7b8ab9",
|
|
58
|
+
"packages/contracts/package.json": "6e4b5300a7d37612f0e18d2b9f46dd0f6f7c197cd241a047f4169c138cc236d7",
|
|
59
|
+
"packages/contracts/README.md": "44d83a3cc508cec6c5c20dfa1f2c08d5ea03e3afb2853639fdc0a9cc8eb5b9c8",
|
|
61
60
|
"packages/core/index.d.ts": "c95b24a13f8aeeb128497833cf298790563baca6036ea5d29a9d8daff53199d7",
|
|
62
61
|
"packages/core/index.js": "825640d0edc7dd130f22da1f580d2cda355566a40b0b36e42e5f38909605bbfa",
|
|
63
|
-
"packages/core/package.json": "
|
|
64
|
-
"packages/
|
|
62
|
+
"packages/core/package.json": "cddc0172abace3ed839eac53337c9e0c39f62ab912a36d3dba092f125c9f91a0",
|
|
63
|
+
"packages/core/README.md": "e222c546104a168f1038d95c6c4efb8e5603edd6145ab2cab79cab977fb710e5",
|
|
65
64
|
"packages/server/bin/agent-terminal-server.js": "01ac54215f1fcbe64c4b78ce9cd83f66215177f67cdccddad30491c94f3b6746",
|
|
66
65
|
"packages/server/bin/claude-workbench-server.js": "73fc7f1022b8cd5bbe8aed0eb105223e83627960e7208cffa6c29a2879932f38",
|
|
67
66
|
"packages/server/bin/claude-workbench.js": "98dbd550d4d657056a75a190e7ee0dffd9f760fba58783f3ffb8d389ad7a32de",
|
|
@@ -88,9 +87,9 @@
|
|
|
88
87
|
"packages/server/lib/drivers/zcode-profile-runtime.js": "77c5ac83551cc424375918863d4a31039984492c3a1c987a615a072ac7cb1f14",
|
|
89
88
|
"packages/server/lib/event-stream.js": "cfbe8207594f004ed0ad65a907401eef833f641b94064fa88852b9ae8c15fe59",
|
|
90
89
|
"packages/server/lib/folder-picker.js": "da2caf8b6a4c00a22556fe355d28d735d78dd814fa0aa7edfc6c4cde8ca8c384",
|
|
91
|
-
"packages/server/lib/history-prepare-worker.js": "
|
|
90
|
+
"packages/server/lib/history-prepare-worker.js": "5e5a4e0998a906ac4a98439c85c00e5122f4074eee7972b3875cf3db8587b1a9",
|
|
92
91
|
"packages/server/lib/history-prepare.js": "27f9b67b1f4012d002c4464dfa837e5144dab9adcb5a41b0b9ae3b0d1d1c18b7",
|
|
93
|
-
"packages/server/lib/http-handler.js": "
|
|
92
|
+
"packages/server/lib/http-handler.js": "dedbd3938ec4b35c0730336d095ff8e08825217d1e3cdeccccbf0afc569f8fd9",
|
|
94
93
|
"packages/server/lib/json-file.js": "4252b57abcd0aafa45c584bac98fcbaf09174774addbb5a4798b381a7aeb35cc",
|
|
95
94
|
"packages/server/lib/knowledge-context.js": "c3d8a9545547c636e6cdf908be53bef4968e674d2aab235562e762e25c210c06",
|
|
96
95
|
"packages/server/lib/model-detection.js": "fa52213200c1f1a1f00e463ada57c415bf1bd4e2aedfebd04de50750509d06cf",
|
|
@@ -100,20 +99,22 @@
|
|
|
100
99
|
"packages/server/lib/project-store.js": "5225c78f3216340f01b99356496c8e2c2d9cfc6247954a3f00b273dd2cc5b692",
|
|
101
100
|
"packages/server/lib/runtime-loader.js": "33e152cb630d986581946928d7ab7d25d53e63647a433b0c1ff4d02c32a6298f",
|
|
102
101
|
"packages/server/lib/session-archive-store.js": "d3335b832a6e9fb9792991056f6596a997a4959efa6e62b60fb60133fb724196",
|
|
103
|
-
"packages/server/lib/session-history.js": "
|
|
102
|
+
"packages/server/lib/session-history-main-index.js": "1a408f2f644e4dfc600640e0146cd18e9727b2cacaac80f29e11b24ab31cd617",
|
|
103
|
+
"packages/server/lib/session-history.js": "e935e5fd560a72baed2fe32dfbb395f6a63926019c7e78296f44fe4c1f914c16",
|
|
104
104
|
"packages/server/lib/system-doctor.js": "d098a046f425e90ef25209be69d78beefdfd9c2af92f08068a9bfc52053a8f7d",
|
|
105
105
|
"packages/server/lib/ui-preferences-store.js": "e108cb22e7cc1f3438704913d5401e01de2c0e717eb2abd3bdc4727209393819",
|
|
106
106
|
"packages/server/lib/user-questions.js": "dc30f92095113279ab0bafe5aa4c16a48f97bad18bd13f0ca3ac74018254005a",
|
|
107
|
-
"packages/server/lib/workbench-service.js": "
|
|
108
|
-
"packages/server/package.json": "
|
|
109
|
-
"packages/
|
|
107
|
+
"packages/server/lib/workbench-service.js": "ea9fbf88139d3c476624354af0f237062b038ce201283d2d513eac6ef1fdcc42",
|
|
108
|
+
"packages/server/package.json": "dc9cc98f27b89a587b6f61a64956fa25b3e268228fca9eb593cc868820a761c8",
|
|
109
|
+
"packages/server/README.md": "a34cfaa4f4e859008b44e2be8e1aafb56e97fe83e6129c968a8562945fa843f3",
|
|
110
110
|
"packages/ui/ai-profile-settings.d.ts": "10cdc96fb20c1f47a18a984638a26bd69972fc0cacf7bd56f4f64ead593e7a36",
|
|
111
111
|
"packages/ui/ai-profile-settings.js": "3c7a75833a3c4f607739ecf71a0716e27c8594cecd9d84e2f8ac99498a59e0e7",
|
|
112
112
|
"packages/ui/claude-terminal.d.ts": "2e8b4ae14f69ec791e2aacbec48ea2bc6e0076e0445a38e99cc477cf2e0a3011",
|
|
113
113
|
"packages/ui/claude-terminal.js": "96bd217a4a7e4792ae3fbaaf46249ecee13b0f4e01fc6d25a1b8b22d8f8fe00c",
|
|
114
114
|
"packages/ui/index.d.ts": "e547ab39640fe7519fb367b78442dc8a2c51b2efd17b66f8b0a003b154b36ead",
|
|
115
115
|
"packages/ui/index.js": "f4394aabd395938f5c3fe587e430471d4033e83945a9b9c7e0149ad4040982c9",
|
|
116
|
-
"packages/ui/package.json": "
|
|
116
|
+
"packages/ui/package.json": "3250143f06bdcd9e61296428af523680ac2bb6c1d5535465bb880bd53c9e17d1",
|
|
117
|
+
"packages/ui/README.md": "793c4a9c286fee23f39062a1976ca3afe9d2816dcdb55e156ba940e1ef514f89",
|
|
117
118
|
"packages/ui/theme.css": "8f03a081272a7db11b196f7a70d0fafb1f8d954778f329f1ba53d110d9d5fd98",
|
|
118
119
|
"src/assets/claude-prompts.json": "cb111f2e0a45afe8a31c251a504d1ab5375966ed61291660e61fd2efd77f6d9f",
|
|
119
120
|
"src/backend/http/ai-profile-routes.js": "4b13a8e9ccbb0317f2ae7b3d468c060c33a9e1ffa49b781a495193a8eebaca68",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanqianx/project-knowledge",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.29",
|
|
4
4
|
"description": "Knowledge base manager with Git integration, AI-driven analysis, and bilingual (zh-CN/en-US) knowledge output",
|
|
5
5
|
"main": "_site/server.js",
|
|
6
6
|
"scripts": {
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"@sanqianx/vector-hub": "1.0.8-rc.8",
|
|
94
94
|
"buffer": "^6.0.3",
|
|
95
95
|
"cheerio": "^1.2.0",
|
|
96
|
-
"claude-ai-workbench": "npm:@sanqianx/claude-ai-workbench@0.13.
|
|
96
|
+
"claude-ai-workbench": "npm:@sanqianx/claude-ai-workbench@0.13.12",
|
|
97
97
|
"cross-spawn": "7.0.6",
|
|
98
98
|
"dotenv": "^17.4.2",
|
|
99
99
|
"gpt-tokenizer": "^3.4.0",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "project-knowledge",
|
|
4
4
|
"hooks": "./hooks/claude.json",
|
|
5
|
-
"version": "5.0.
|
|
5
|
+
"version": "5.0.29",
|
|
6
6
|
"description": "Read durable project decisions and implementation history from Project Knowledge.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "SanQianX",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"const fs=require('fs'),path=require('path'),os=require('os');const PKG='@sanqianx/project-knowledge',SC='project-knowledge/integration-runtime/v1';let f='';const root=process.env.CLAUDE_PLUGIN_ROOT||process.env.PLUGIN_ROOT;if(root){f=path.join(root,'scripts','mcp-launcher.cjs');if(!fs.existsSync(f))f=''}if(!f){const dd=process.env.KB_DATA_DIR||path.join(os.homedir(),'.project-knowledge-v5');try{const bp=path.join(dd,'integration-runtime.json');const st=fs.statSync(bp);if(st.isFile()&&st.size<=32768){const b=JSON.parse(fs.readFileSync(bp,'utf8'));if(b&&b.schema===SC&&b.packageName===PKG&&typeof b.packageRoot==='string'&&path.isAbsolute(b.packageRoot)){const c=path.join(b.packageRoot,'plugins','project-knowledge','scripts','mcp-launcher.cjs');const rr=fs.realpathSync(b.packageRoot),cr=fs.realpathSync(c);const rel=path.relative(rr,cr);if(rel&&rel!=='..'&&!rel.startsWith('..'+path.sep)&&!path.isAbsolute(rel)&&fs.statSync(cr).isFile())f=cr}}}catch(e){}}if(!f){console.error('[project-knowledge-mcp] 未能通过受信任插件根或已验证的安装绑定定位启动器,已停止;不访问网络,也不加载工作目录源码。');process.exit(127)}require(f);"
|
|
9
9
|
],
|
|
10
10
|
"env": {
|
|
11
|
-
"KB_PLUGIN_VERSION": "5.0.
|
|
11
|
+
"KB_PLUGIN_VERSION": "5.0.29"
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
}
|
package/ui/index.html
CHANGED
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
<dialog id="importDialog"><form id="importForm"><div class="dialog-heading"><h2>导入项目</h2><button type="button" data-close="importDialog" aria-label="关闭">×</button></div><p class="muted">绑定代码与新的空知识目录。只建立跟踪基线,不读取旧知识、不分析全部历史。</p><label>代码目录<div class="input-with-action"><input name="localPath" id="importCode" required placeholder="D:\SanQian.Xu\my-project"><button type="button" id="pickCode">选择</button></div></label><label>项目名称<input name="displayName" id="importName" required maxlength="200"></label><label>知识目录<div class="input-with-action"><input name="knowledgePath" id="importKnowledge" required placeholder="D:\SanQian.Xu\knowledge\my-project"><button type="button" id="pickKnowledge">选择</button></div></label><label>所属领域<select name="domainId" id="importDomain"></select></label><label id="gitInitChoice" class="checkbox" hidden><input type="checkbox" name="confirmGitInit">这个目录不是 Git 仓库,我确认初始化 Git。</label><label id="replaceHookChoice" class="checkbox" hidden><input type="checkbox" name="confirmReplaceHook">目录留有旧知识库的托管 Hook,我确认替换为新项目接线。不会覆盖第三方 Hook。</label><div id="preflightStatus" class="feedback" role="status">填写目录后检查导入条件。</div><button type="button" id="setupImportRoot" hidden>设置知识根目录</button><div class="dialog-actions"><button type="button" data-close="importDialog">取消</button><button type="submit" class="primary" id="confirmImport" disabled>导入项目</button></div></form></dialog>
|
|
59
59
|
<dialog id="domainDialog"><form id="domainForm"><div class="dialog-heading"><h2>创建领域</h2><button type="button" data-close="domainDialog" aria-label="关闭">×</button></div><label>领域名称<input name="name" required maxlength="100" placeholder="例如:开发工具"></label><p class="muted">领域用于共享相关项目的知识,不是新的代码项目。</p><div class="dialog-actions"><button type="button" data-close="domainDialog">取消</button><button class="primary" type="submit">创建领域</button></div></form></dialog>
|
|
60
60
|
<dialog id="memoryDialog"><form id="memoryForm"><div class="dialog-heading"><h2 id="memoryDialogTitle">审核记忆</h2><button type="button" data-close="memoryDialog" aria-label="关闭">×</button></div><p id="manualMemoryHint" class="muted" hidden>来源为手动添加。确认后保存为已确认记忆,无需等待提交或 Agent 提取。</p><label>记忆内容<textarea id="memoryText" maxlength="2000" required rows="5" placeholder="例如:对外错误信息使用中文;优先复用已有模块。"></textarea></label><label>适用范围<select id="memoryScope"><option value="global">全局:所有领域与项目</option><option value="domain">领域:指定领域内的项目</option><option value="project">项目:仅指定项目</option></select></label><label id="memoryTargetLabel">适用目标<select id="memoryTarget" required></select></label><p id="memoryScopeHint" class="muted"></p><details class="memory-evidence" id="memoryEvidenceDetails"><summary>原始总结与来源依据(只读)</summary><p id="memoryOriginal"></p><p id="memoryEvidence" class="muted"></p><blockquote id="memoryQuote"></blockquote></details><p id="memoryFeedback" class="feedback" hidden role="status"></p><div class="dialog-actions"><button type="button" data-close="memoryDialog">取消</button><button type="submit" id="saveMemoryCandidate" data-mode="candidate">保存候选</button><button type="submit" id="confirmMemory" class="primary" data-mode="confirmed">确认并保存</button></div></form></dialog>
|
|
61
|
+
<dialog id="memoryDeleteDialog"><div class="dialog-heading"><h2>删除记忆</h2><button type="button" data-close="memoryDeleteDialog" aria-label="关闭">×</button></div><p id="memoryDeleteText"></p><p id="memoryDeleteScope" class="muted"></p><p class="muted">删除后这条记忆不再作为长期记忆使用,也不会出现在记忆列表或新任务的参考内容中。原始对话和知识来源保持不变。之后仍可随时手动重新添加。</p><p id="memoryDeleteFeedback" class="feedback" hidden role="status"></p><div class="dialog-actions"><button type="button" data-close="memoryDeleteDialog">取消</button><button type="button" class="danger" id="confirmMemoryDelete">删除</button></div></dialog>
|
|
61
62
|
<dialog id="unbindDialog"><form id="unbindForm"><div class="dialog-heading"><h2>解绑项目</h2><button type="button" data-close="unbindDialog" aria-label="关闭">×</button></div><p id="unbindProjectName"></p><p class="muted">项目将从列表移除,取消尚未开始的维护任务。代码、知识文件、提交证据和会话历史仍保留。正在执行的知识写入结束前不会解绑。</p><p id="unbindFeedback" class="feedback" role="status"></p><div class="dialog-actions"><button type="button" data-close="unbindDialog">取消</button><button class="primary" type="submit" id="confirmUnbind">解绑并保留数据</button></div></form></dialog>
|
|
62
63
|
<dialog id="referenceDialog" class="reference-dialog"><div class="dialog-heading"><h2 id="referenceTitle">知识引用</h2><div class="actions"><button id="copyReference">复制引用</button><button data-close="referenceDialog" aria-label="关闭">×</button></div></div><p id="referenceMeta" class="muted"></p><pre id="referenceContent"></pre></dialog>
|
|
63
64
|
<dialog id="engineDialog" class="engine-dialog"><div class="dialog-heading"><h2>知识引擎诊断</h2><button data-close="engineDialog" aria-label="关闭">×</button></div><iframe id="engineFrame" title="知识引擎诊断" referrerpolicy="same-origin"></iframe></dialog>
|
package/ui/product.css
CHANGED
|
@@ -7,7 +7,7 @@ label>.detail-path{display:block;width:100%;margin:8px 0 0;color:var(--text)}
|
|
|
7
7
|
@media(prefers-reduced-motion:reduce){*,*::before,*::after{scroll-behavior:auto!important;animation:none!important;transition:none!important}}
|
|
8
8
|
.reference{overflow-wrap:anywhere}.reference small{min-width:0;max-width:100%}
|
|
9
9
|
html[data-theme=dark]{--page:#171a1f;--surface:#20252d;--surface2:#252b34;--surface3:#2c333e;--line:#39424f;--text:#e6ebf0;--muted:#a0aaba;--faint:#788393;--nav:#1b2028;--accent:#8aa8cf;--accent-soft:#293748;--good:#81b29a;--bad:#d88b8b;--bad-soft:#39292d;color-scheme:dark}
|
|
10
|
-
*{box-sizing:border-box}[hidden]{display:none!important}html,body{height:100%}body{margin:0;overflow:hidden;background:var(--page);color:var(--text);font:13px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei",sans-serif}button,input,select,textarea{font:inherit;color:inherit}button{cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:34px;padding:6px 12px;border:1px solid var(--line);border-radius:8px;background:var(--surface)}button:hover{background:var(--surface3)}button:disabled{opacity:.5;cursor:not-allowed}button.primary{background:var(--accent);color:#fff;border-color:var(--accent)}button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid var(--accent);outline-offset:3px}svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round;flex:none}h1,h2,h3,p{margin:0}h1{font-size:18px;font-weight:650}h2{font-size:16px;font-weight:600}h3{font-size:13px;font-weight:600}small,.muted{color:var(--muted)}small{font-size:11px}p{color:var(--muted);margin-top:8px}input,select,textarea{background:var(--surface);border:1px solid var(--line);border-radius:8px;padding:8px 12px;min-width:0}label{display:block;font-size:12px;margin:16px 0;color:var(--muted)}label>input,label>select,label>textarea{display:block;width:100%;margin-top:8px;color:var(--text)}pre{font:12px/1.8 Consolas,"Microsoft YaHei",monospace;white-space:pre-wrap;overflow-wrap:anywhere;color:var(--text)}
|
|
10
|
+
*{box-sizing:border-box}[hidden]{display:none!important}html,body{height:100%}body{margin:0;overflow:hidden;background:var(--page);color:var(--text);font:13px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei",sans-serif}button,input,select,textarea{font:inherit;color:inherit}button{cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:34px;padding:6px 12px;border:1px solid var(--line);border-radius:8px;background:var(--surface)}button:hover{background:var(--surface3)}button:disabled{opacity:.5;cursor:not-allowed}button.primary{background:var(--accent);color:#fff;border-color:var(--accent)}button.danger{background:var(--bad);color:#fff;border-color:var(--bad)}button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid var(--accent);outline-offset:3px}svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round;flex:none}h1,h2,h3,p{margin:0}h1{font-size:18px;font-weight:650}h2{font-size:16px;font-weight:600}h3{font-size:13px;font-weight:600}small,.muted{color:var(--muted)}small{font-size:11px}p{color:var(--muted);margin-top:8px}input,select,textarea{background:var(--surface);border:1px solid var(--line);border-radius:8px;padding:8px 12px;min-width:0}label{display:block;font-size:12px;margin:16px 0;color:var(--muted)}label>input,label>select,label>textarea{display:block;width:100%;margin-top:8px;color:var(--text)}pre{font:12px/1.8 Consolas,"Microsoft YaHei",monospace;white-space:pre-wrap;overflow-wrap:anywhere;color:var(--text)}
|
|
11
11
|
.app{height:100%;display:grid;grid-template-columns:232px minmax(0,1fr)}.sidebar{display:flex;flex-direction:column;border-right:1px solid var(--line);background:var(--nav);min-height:0}.brand{display:flex;align-items:center;gap:8px;padding:24px 20px 16px}.brand-mark{width:32px;height:32px;display:block;flex:none;object-fit:contain}.brand strong{font-size:14px}.brand small{display:block}.brain-label{padding:0 20px 16px;color:var(--muted);overflow-wrap:anywhere}.sidebar nav{display:grid;gap:8px;padding:8px 12px}.sidebar nav button{justify-content:flex-start;min-height:40px;text-align:left;border-color:transparent;background:transparent;color:var(--muted)}.sidebar nav button.active{border-color:var(--line);background:var(--surface);color:var(--text)}.count{margin-left:auto;background:var(--surface3);padding:0 6px;border-radius:4px}.system-button{margin: auto 12px 16px;justify-content:flex-start;min-height:56px;background:transparent;border-color:transparent;padding:8px}.system-button>span:nth-child(2){flex:1;text-align:left}.system-button b,.system-button small{display:block;font-size:11px}.dot{width:7px;height:7px;border-radius:50%;background:var(--faint);flex:none}.dot.up{background:var(--good)}.dot.issue{background:var(--bad)}main{min-width:0;min-height:0;display:flex;flex-direction:column}.topbar{height:72px;flex:none;display:flex;align-items:center;gap:16px;padding:0 24px;background:var(--surface);border-bottom:1px solid var(--line)}.spacer{flex:1}.topbar select{width:240px}.icon-button{padding:8px;min-width:34px}.content{position:relative;flex:1;min-height:0}.view{display:none;height:100%;overflow:auto;padding:32px}.view.active{display:block}.view.module-view{padding:0;overflow:hidden}.module-view.active{display:flex;flex-direction:column}.frame-container{position:relative;flex:1;min-width:0;min-height:0;overflow:hidden;background:var(--surface)}iframe{display:block;width:100%;height:100%;border:0;background:var(--surface)}.work-layout{display:grid;grid-template-columns:minmax(0,1fr);height:100%;min-height:0}.module-toolbar{flex:none;min-height:56px;display:flex;justify-content:space-between;align-items:center;gap:16px;padding:8px 24px;border-bottom:1px solid var(--line);background:var(--surface)}.segmented{display:flex;gap:4px}.segmented button{background:transparent;border-color:transparent;color:var(--muted)}.segmented button.active{background:var(--surface);border-color:var(--line);color:var(--text)}
|
|
12
12
|
.welcome{display:flex;align-items:center;justify-content:space-between;gap:24px;margin-bottom:32px}.welcome h2{font-size:24px;margin-top:8px}.actions{display:flex;gap:8px;align-items:center}.grid2{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(320px,1fr);gap:24px}.stack{display:grid;gap:24px}.panel{border:1px solid var(--line);background:var(--surface);border-radius:8px;padding:24px;min-width:0}.panel h2{margin-bottom:16px}.section-heading{display:flex;align-items:center;justify-content:space-between;gap:24px;margin-bottom:24px}.section-heading p{font-size:12px}.row{display:flex;align-items:center;gap:16px;padding:16px 0;border-bottom:1px solid var(--line)}.row:last-child{border-bottom:0}.row>div{flex:1;min-width:0}.row b{font-weight:600}.row small{display:block;margin-top:4px;overflow-wrap:anywhere}.row time{color:var(--muted);font-size:11px;flex:none}.status{font-size:11px;border:1px solid var(--line);border-radius:4px;padding:2px 8px;white-space:nowrap;color:var(--muted)}.status.up{color:var(--good)}.status.issue{color:var(--bad);background:var(--bad-soft)}.empty{padding:64px 24px;text-align:center;color:var(--muted);background:var(--surface);border-radius:8px;line-height:1.9}.empty button{display:block;margin:16px auto 0}.small-empty{padding:16px 0;text-align:left;background:transparent;font-size:12px}.hierarchy{display:grid;grid-template-columns:296px minmax(0,1fr);gap:24px}.tree h3{margin:24px 0 8px;color:var(--muted);font-size:11px}.tree button{width:100%;justify-content:flex-start;text-align:left;background:transparent;border-color:transparent;margin-top:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tree button.active{background:var(--accent-soft);color:var(--accent)}.detail-path{padding:12px 16px;background:var(--surface2);border:1px solid var(--line);border-radius:8px;margin:8px 0 24px;font:12px/1.8 Consolas,"Microsoft YaHei",monospace;overflow-wrap:anywhere}.detail-label{color:var(--muted);font-size:11px}.memory-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:24px;margin-top:24px}.memory-card{padding:24px;border:1px solid var(--line);border-radius:8px;background:var(--surface)}.memory-card p{color:var(--text);margin:0 0 16px;white-space:pre-wrap}.memory-card .actions{margin-top:16px}.reference{display:block;text-align:left;width:100%;background:transparent;border:0;border-bottom:1px solid var(--line);border-radius:0;padding:16px 0}.reference b,.reference small{display:block;text-align:left}.reference p{font-size:12px;text-align:left}.activity-updates{flex:1;overflow:auto;padding:24px 32px}.run{margin-bottom:16px;background:var(--surface);border:1px solid var(--line);border-radius:8px}.run summary{padding:16px 24px;cursor:pointer;display:flex;align-items:center;gap:16px}.run summary strong{flex:1}.run>div{padding:0 24px 24px}.run .error{color:var(--bad)}.operation{padding:8px 0;color:var(--muted)}
|
|
13
13
|
.drawer-backdrop{position:fixed;inset:0;background:rgba(10,18,25,.25);z-index:20}.drawer{position:fixed;right:0;top:0;height:100%;width:448px;background:var(--surface);border-left:1px solid var(--line);z-index:21;display:flex;flex-direction:column}.drawer>header{height:72px;display:flex;align-items:center;justify-content:space-between;padding:24px;border-bottom:1px solid var(--line)}.drawer-content{overflow:auto;flex:1;padding:24px}.drawer section{padding-bottom:24px;margin-bottom:24px;border-bottom:1px solid var(--line)}.drawer select{width:100%;margin:16px 0}.drawer .actions{flex-wrap:wrap}.drawer section:last-child{border-bottom:0}.input-with-action{display:flex;gap:8px;margin-top:8px}.input-with-action input{flex:1}.input-with-action button{flex:none}.checkbox{display:flex;align-items:center;gap:8px}.checkbox input{width:auto;margin:0}dialog{width:560px;max-width:90vw;max-height:90vh;overflow:auto;background:var(--surface);color:var(--text);border:1px solid var(--line);border-radius:12px;padding:24px}dialog::backdrop{background:rgba(10,18,25,.35)}.dialog-heading{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}.dialog-heading>button{border:0;background:transparent;font-size:20px}.dialog-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:24px}.feedback{padding:12px 16px;background:var(--surface2);border:1px solid var(--line);border-radius:8px;color:var(--muted);white-space:pre-wrap}.feedback.issue{color:var(--bad)}.reference-dialog{width:880px}.reference-dialog pre{margin-top:24px;max-height:60vh;overflow:auto}.engine-dialog{width:calc(100vw - 96px);height:calc(100vh - 96px);padding:0;overflow:hidden}.engine-dialog .dialog-heading{height:64px;padding:16px 24px;border-bottom:1px solid var(--line);margin:0}.engine-dialog iframe{height:calc(100% - 64px)}.toast{position:fixed;bottom:24px;right:24px;z-index:50;background:var(--surface);padding:12px 16px;border:1px solid var(--line);border-radius:8px;max-width:480px;color:var(--text)}
|
package/ui/product.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
const time = value => value ? new Date(value).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : '—';
|
|
25
25
|
const label = run => run.state === 'failed' && run.error?.code === 'MODEL_QUOTA_EXHAUSTED' ? '额度不足 · 待重试' : run.state === 'queued' && run.phase === 'waiting-previous' ? '等待前一条提交恢复' : ({ queued: '排队', running: '维护中', succeeded: '已更新 · 可检索', 'index-pending': '知识已更新 · 索引待修复', failed: '维护失败', cancelled: '已取消' })[run.state] || run.state;
|
|
26
26
|
const badge = (text, status = '') => `<span class="status ${esc(status)}">${esc(text)}</span>`;
|
|
27
|
-
let toastTimer, refreshTimer, preflightTimer, editingMemoryId, logStream, logEpoch = 0, referenceEpoch = 0, snapshotEpoch = 0, registryEpoch = 0, registryPromise = null, resumeImport = false, unbindProjectId = '', unbinding = false;
|
|
27
|
+
let toastTimer, refreshTimer, preflightTimer, editingMemoryId, deletingMemoryId, logStream, logEpoch = 0, referenceEpoch = 0, snapshotEpoch = 0, registryEpoch = 0, registryPromise = null, resumeImport = false, unbindProjectId = '', unbinding = false;
|
|
28
28
|
let captureEpoch = 0, captureBusy = false;
|
|
29
29
|
let registryStream = null, registryReceipt = null, registryRefreshTimer;
|
|
30
30
|
async function api(url, options = {}) {
|
|
@@ -261,7 +261,7 @@
|
|
|
261
261
|
all('[data-memory-tab]').forEach(button => button.classList.toggle('active', button.dataset.memoryTab === state.memoryTab));
|
|
262
262
|
const type = $('memoryScopeFilter').value, target = $('memoryTargetFilter').value;
|
|
263
263
|
const memories = state.snapshot.memories.filter(m => m.status === state.memoryTab && (!type || (m.scopeType || 'global') === type) && (!type || type === 'global' || !target || m.scopeId === target));
|
|
264
|
-
$('memoryContent').innerHTML = !state.engineAvailable ? '<div class="empty">知识引擎暂时不可用。未读取到的记忆不会显示为已确认。</div>' : memories.length ? `<div class="memory-grid">${memories.map(m => `<article class="memory-card" data-memory-card="${esc(m.id)}"><div class="memory-meta">${badge(memoryScopeName(m))}${m.targetAvailable === false ? badge('目标已失效,不会注入', 'issue') : m.status === 'confirmed' && m.syncStatus === 'pending' ? badge('写入待恢复', 'issue') : m.status === 'confirmed' && m.indexStatus === 'failed' ? badge('索引待恢复', 'issue') : m.status === 'confirmed' && m.indexStatus === 'pending' ? badge('索引更新中') : ''}</div><p>${esc(m.text)}</p><small>${m.sourceType === 'manual' ? `来源:手动添加 · ${esc(time(m.createdAt))}` : `来源:${esc(projectName(m.evidence.projectId))} · commit ${esc(m.evidence.commitSha.slice(0, 8))}<br>${esc(m.evidence.entryId)}`}</small><div class="actions">${m.sourceType === 'manual' ? '' : `<button data-memory-action="evidence" data-memory-id="${esc(m.id)}">查看依据</button>`}${m.status === 'candidate' ? `<button class="primary" data-memory-action="confirm" data-memory-id="${esc(m.id)}">审核确认</button><button data-memory-action="ignore" data-memory-id="${esc(m.id)}">忽略</button>` : `<button data-memory-action="edit" data-memory-id="${esc(m.id)}">编辑</button>${badge(m.syncStatus === 'pending' ? '待同步' : '已确认', m.syncStatus === 'pending' ? 'issue' : 'up')}`}
|
|
264
|
+
$('memoryContent').innerHTML = !state.engineAvailable ? '<div class="empty">知识引擎暂时不可用。未读取到的记忆不会显示为已确认。</div>' : memories.length ? `<div class="memory-grid">${memories.map(m => `<article class="memory-card" data-memory-card="${esc(m.id)}"><div class="memory-meta">${badge(memoryScopeName(m))}${m.targetAvailable === false ? badge('目标已失效,不会注入', 'issue') : m.status === 'confirmed' && m.syncStatus === 'pending' ? badge('写入待恢复', 'issue') : m.status === 'confirmed' && m.indexStatus === 'failed' ? badge('索引待恢复', 'issue') : m.status === 'confirmed' && m.indexStatus === 'pending' ? badge('索引更新中') : ''}</div><p>${esc(m.text)}</p><small>${m.sourceType === 'manual' ? `来源:手动添加 · ${esc(time(m.createdAt))}` : `来源:${esc(projectName(m.evidence.projectId))} · commit ${esc(m.evidence.commitSha.slice(0, 8))}<br>${esc(m.evidence.entryId)}`}</small><div class="actions">${m.sourceType === 'manual' ? '' : `<button data-memory-action="evidence" data-memory-id="${esc(m.id)}">查看依据</button>`}${m.status === 'candidate' ? `<button class="primary" data-memory-action="confirm" data-memory-id="${esc(m.id)}">审核确认</button><button data-memory-action="ignore" data-memory-id="${esc(m.id)}">忽略</button>` : `<button data-memory-action="edit" data-memory-id="${esc(m.id)}">编辑</button>${badge(m.syncStatus === 'pending' ? '待同步' : '已确认', m.syncStatus === 'pending' ? 'issue' : 'up')}`}<button data-memory-action="delete" data-memory-id="${esc(m.id)}">删除</button></div></article>`).join('')}</div>` : `<div class="empty">${type ? '此范围内没有匹配的记忆。' : state.memoryTab === 'confirmed' ? '还没有已确认的长期记忆。可以点击“添加记忆”记录约定。' : '候选箱为空。维护产物中的有据偏好会进入这里,等待你的确认。'}</div>`;
|
|
265
265
|
}
|
|
266
266
|
function memoryScopeName(memory) {
|
|
267
267
|
const type = memory.scopeType || 'global';
|
|
@@ -298,6 +298,15 @@
|
|
|
298
298
|
$('confirmMemory').textContent = memory.status === 'candidate' ? '确认并保存' : '保存修改';
|
|
299
299
|
$('memoryDialog').showModal();
|
|
300
300
|
}
|
|
301
|
+
function openDeleteMemory(memory) {
|
|
302
|
+
if (!state.engineAvailable) { toast('知识引擎暂时不可用,请恢复连接后重试。'); return; }
|
|
303
|
+
deletingMemoryId = memory.id;
|
|
304
|
+
$('memoryDeleteText').textContent = memory.text;
|
|
305
|
+
$('memoryDeleteScope').textContent = `适用范围:${memoryScopeName(memory)}`;
|
|
306
|
+
$('memoryDeleteFeedback').hidden = true;
|
|
307
|
+
$('confirmMemoryDelete').disabled = false;
|
|
308
|
+
$('memoryDeleteDialog').showModal();
|
|
309
|
+
}
|
|
301
310
|
function openAddMemory() {
|
|
302
311
|
if (!state.engineAvailable) { toast('知识引擎暂时不可用,请恢复连接后重试。'); return; }
|
|
303
312
|
if (!state.root) { toast('请先在设置中配置知识根目录,再添加记忆。'); return; }
|
|
@@ -674,6 +683,7 @@
|
|
|
674
683
|
const action = button.dataset.memoryAction;
|
|
675
684
|
if (action === 'evidence') await openReference({ ...memory.evidence, readFile: false, text: memory.evidence.quote, title: '记忆来源依据(冻结原文)' });
|
|
676
685
|
else if (action === 'edit' || action === 'confirm') openMemory(memory);
|
|
686
|
+
else if (action === 'delete') openDeleteMemory(memory);
|
|
677
687
|
else { await api(`/api/engine/memories/${encodeURIComponent(memory.id)}`, { method: 'PATCH', body: { status: 'ignored' } }); await refreshSnapshot(); }
|
|
678
688
|
}
|
|
679
689
|
};
|
|
@@ -732,6 +742,22 @@
|
|
|
732
742
|
on('domainForm', 'submit', async e => { e.preventDefault(); await api('/api/domains', { method: 'POST', body: Object.fromEntries(new FormData(e.target)) }); $('domainDialog').close(); await refreshRegistry(true); });
|
|
733
743
|
on('memoryScopeFilter', 'change', updateMemoryFilter); on('memoryTargetFilter', 'change', renderMemories);
|
|
734
744
|
on('addMemory', 'click', openAddMemory);
|
|
745
|
+
on('confirmMemoryDelete', 'click', async () => {
|
|
746
|
+
if (!deletingMemoryId || $('confirmMemoryDelete').disabled) return;
|
|
747
|
+
$('confirmMemoryDelete').disabled = true;
|
|
748
|
+
$('memoryDeleteFeedback').hidden = true;
|
|
749
|
+
try {
|
|
750
|
+
await api(`/api/engine/memories/${encodeURIComponent(deletingMemoryId)}`, { method: 'DELETE' });
|
|
751
|
+
$('memoryDeleteDialog').close();
|
|
752
|
+
await refreshSnapshot();
|
|
753
|
+
toast('记忆已删除,不再作为长期记忆使用。');
|
|
754
|
+
} catch (error) {
|
|
755
|
+
$('confirmMemoryDelete').disabled = false;
|
|
756
|
+
$('memoryDeleteFeedback').textContent = error.message;
|
|
757
|
+
$('memoryDeleteFeedback').hidden = false;
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
on('memoryDeleteDialog', 'close', () => { deletingMemoryId = ''; $('confirmMemoryDelete').disabled = false; });
|
|
735
761
|
on('memoryScope', 'change', () => {
|
|
736
762
|
const memory = state.snapshot.memories.find(m => m.id === editingMemoryId), type = $('memoryScope').value;
|
|
737
763
|
const source = state.projects.find(p => p.projectId === memory?.evidence?.projectId) || activeProject();
|
package/vendor-manifest.json
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
"modules": {
|
|
4
4
|
"knowledge-engine": {
|
|
5
5
|
"name": "@sanqianx/ai-coding-knowledge-engine",
|
|
6
|
-
"version": "0.2.7-rc.
|
|
6
|
+
"version": "0.2.7-rc.12",
|
|
7
7
|
"root": "_modules/knowledge-engine",
|
|
8
8
|
"files": {
|
|
9
|
-
"README.md": "
|
|
10
|
-
"package.json": "
|
|
9
|
+
"README.md": "5fcf21c2fb819b431df34d5da05ee7bd861c7069c9d24c1b630e8a5e4ac3455c",
|
|
10
|
+
"package.json": "a3d61ae5aeda5f820b678f464b223ab80a5d60f024dd2df9dc12533de9d86e03",
|
|
11
11
|
"src/analysis-contract.js": "6d4b9fde0e05d9a80d43e96d0c27febf082ecdb19312faf0280d6c9b16ac71b1",
|
|
12
12
|
"src/analysis-failures.js": "eb1e5a476b1e3e3c87dfd9955707c85151707799883595bcb5e9cc3e26a991c4",
|
|
13
13
|
"src/analysis-output.js": "95a3c19dcc9ab85d1dba7fee5b9aec80f9c000dfba89da3fd0be25a81e2fb680",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"src/headless-analyzer.js": "df570864136981cf98f3f69d047fa5729fbbfdeeaee0c6bfad26dab941dbbdae",
|
|
21
21
|
"src/index-request.js": "78f26b872299005499fff4f2d1f9e8f986784f2ff36a5cf2be2b1e7b7c7d9378",
|
|
22
22
|
"src/knowledge-targets.js": "aa44ec285d16d7b1c0b63bf084058a1d6f867fa9929c753b6b0cbb24f7b20ee2",
|
|
23
|
-
"src/memories.js": "
|
|
23
|
+
"src/memories.js": "b661474b51688a7ec9bc9d79bdc628070f7e128effbf7b86be9520398ead00a5",
|
|
24
24
|
"src/memory-eligibility.js": "48af31f23f81140dbc706ff83a39ba9f411314d87d01e5accfaf419c7a74e0ea",
|
|
25
25
|
"src/retrieval-policy.json": "3205fb4eb114a839a74f902e6fae6a61e1460958e5a9d7e29add32b60ba4e05a",
|
|
26
26
|
"src/retrieval-ranking.js": "061b64f5bffec3269851830b30ab51abc3260274e5207ea4864ae33d9cb22809",
|
|
@@ -43,13 +43,13 @@
|
|
|
43
43
|
"src/runtime/layout.js": "e47fe54c605218493ce46f6289798d9db2a3ecec95089d288ed03d3b75cf871a",
|
|
44
44
|
"src/runtime/markdown-knowledge-indexer.js": "ac094e947e52cbaeb781c0e1a9437b52b4e46bda9ae92fdd5c626ee13eb0b8be",
|
|
45
45
|
"src/runtime/scanner.js": "2b5443a1b782b4b73458de907a06a34e6bb6bfa9ba45d20c443ef8272ada97e6",
|
|
46
|
-
"src/server.js": "
|
|
47
|
-
"src/store.js": "
|
|
46
|
+
"src/server.js": "1d89378d889d563082e7ca0ade198ba312332288d5d253a9379c24b542f39b75",
|
|
47
|
+
"src/store.js": "66f573a58662f0a726c2ef62ccc1697616e9d93066af9f65a87c2c1f6b16829d",
|
|
48
48
|
"ui/app.css": "7ad370c269e60a7b5ec907d81a607b2d16bd07b2d7c22a81f102dab51eeb2889",
|
|
49
49
|
"ui/app.js": "e9ecb0125d5f2a8bb30a58c4d278fe0737ee2540ecfb065f97f453495cfc792d",
|
|
50
50
|
"ui/index.html": "ce3565cfefd4838b11d37a91a906ef26ca5a1834844fe3f7c897b56851a3c418"
|
|
51
51
|
},
|
|
52
|
-
"sourceCommit": "
|
|
52
|
+
"sourceCommit": "949d1d7066082e5c3ddcf04af653f5624be9add6"
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
}
|