@sanqianx/project-knowledge 5.0.13 → 5.0.14-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.
@@ -9,7 +9,7 @@
9
9
  "plugins": [
10
10
  {
11
11
  "name": "project-knowledge",
12
- "version": "5.0.13",
12
+ "version": "5.0.14-rc.1",
13
13
  "source": "./plugins/project-knowledge",
14
14
  "description": "Search durable project decisions and implementation history through MCP."
15
15
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [5.0.14] - 2026-10-13
4
+
5
+ - 长期记忆页增加“添加记忆”:填写内容并选择全局、领域或项目,用户确认后直接保存为已确认记忆。
6
+ - 手动来源独立标明,不伪造 Agent 分析、项目来源或 commit;复用引擎受控写入、持久化、共享索引和开发前上下文。
7
+ - 校验无效目标、重复内容和越界路径;取消不保存,保存与编辑自动刷新,写入失败保留记录并报告待恢复。
8
+ - 保留 5.0.13 的重复采集修复:已有旧 Hook 仍需在 Agent 接入中执行“接入/修复”,升级启动不等于已清理旧接线。
9
+
3
10
  ## [5.0.13] - 2026-10-13
4
11
 
5
12
  - 修复独立 bridge 与产品 bridge 同时接入导致同一项目生成两个知识目录:接入/修复只替换明确的旧自有入口,并去除当前入口的重复项。
@@ -64,6 +64,7 @@ node src/bin.js serve [--port 5790] [--host 127.0.0.1] [--data DIR]
64
64
  | GET / POST | `/api/events` | 真实事件查询 / 仅调试模式注入事件 |
65
65
  | POST | `/api/bridge/notify` | 唤醒 journal 消费;通知丢失仍由轮询恢复 |
66
66
  | GET / PATCH | `/api/memories`、`/api/memories/:id` | 按 status/scopeType/scopeId 筛选;一次编辑内容、适用范围和确认状态;来源证据只读 |
67
+ | POST | `/api/memories` | `{ text, brainId?, scopeType?, scopeId? }` → 手动添加已确认记忆;默认全局,领域/项目必须指定已登记目标,不接受伪造来源 |
67
68
  | POST | `/api/scopes` | 外壳投影智脑、领域共享目录;受运行期鉴权保护 |
68
69
  | POST | `/api/runtime-config` | 运行期模型配置,仅保留在内存;受鉴权保护 |
69
70
  | GET | `/api/runtime-auth` | 本机服务确认运行期鉴权;受鉴权保护,不对浏览器反代 |
@@ -95,6 +96,7 @@ node src/bin.js serve [--port 5790] [--host 127.0.0.1] [--data DIR]
95
96
  - 候选的 `entryId` 必须等于本次晋升清单中实际文档的 `operations[].path`,不是候选编号;引用必须与该文档原文匹配。无效候选不会伪造来源或阻塞有效知识更新,运行的 `memoryCapture` 与 `knowledge.memory.candidate_rejected` 日志记录安全的计数、序号和原因代码。
96
97
  - 记忆的适用范围为 `global/domain/project`,目标用 `scopeId` 指定;来源项目/领域不等于适用范围。新候选默认来源项目,分析器只有明确证据时才建议领域/全局;用户审核时可以编辑内容并选择当前智脑中的其他已有目标。原始总结与证据保持不变,同一范围内容去重,忽略和编辑后的原候选不会重复生成。
97
98
  - 长期记忆统一保存在已绑定的智脑共享目录:`memories/confirmed.md`(全局)、`memories/domains/<id>.md`、`memories/projects/<id>.md`。不创建第二份项目知识目录。现有 v5 缺少范围字段的记忆仍按全局解释;不读取 pre-v5 状态、不重扫历史。
99
+ - 手动记忆以用户确认作为来源,记录 `sourceType: manual`、`evidence: null`,不伪造提交或分析运行。内容限制为 1–2000 字符,同范围内容去重;创建、编辑、投影与恢复复用引擎受控写入。空项目列表也可添加全局记忆,但必须已绑定共享目录;无需模型分析或 Git 提交,索引状态独立报告。
98
100
  - 任务检索只允许当前项目、当前所属领域与全局的已确认记忆,即使检索整个智脑也不放宽专属记忆范围。过滤在排名之前完成,`get` 同样校验;向量旧内容不能绕过当前状态和文件一致性校验。已确认记录修改范围会同步清理旧投影;写入失败显示待同步并自动重试,未同步文件不能进入上下文。
99
101
  - 旧格式不读取、不自动迁移。损坏的正式状态不覆盖、不清空。
100
102
  - 引擎状态复用受控原子写入:独占随机临时文件、文件同步、Windows 短暂共享冲突有界重试;永久失败保留原状态并清理本次临时文件,绝不先删正式状态。
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanqianx/ai-coding-knowledge-engine",
3
- "version": "0.2.5-rc.3",
3
+ "version": "0.2.6",
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",
@@ -46,7 +46,7 @@ class EngineContext {
46
46
  let memorySize = 0;
47
47
  for (const memory of candidates) {
48
48
  const target = this.memories.scopeOf(memory);
49
- const item = { projectId: brain.id, spaceId: `brain:${brain.id}`, entryId: this.memories.entry(memory), commitSha: memory.evidence?.commitSha || null, memoryId: memory.id, memoryScopeType: target.type, memoryScopeId: target.id, sourceProjectId: memory.evidence?.projectId || null, updatedAt: updatedAt(memory), title: `${{ project: '项目', domain: '领域', global: '全局' }[target.type]}记忆`, text: clean(memory.text) };
49
+ const item = { projectId: brain.id, spaceId: `brain:${brain.id}`, entryId: this.memories.entry(memory), commitSha: memory.evidence?.commitSha || null, memoryId: memory.id, memoryScopeType: target.type, memoryScopeId: target.id, sourceProjectId: memory.evidence?.projectId || null, ...(memory.sourceType === 'manual' ? { sourceType: 'manual' } : {}), updatedAt: updatedAt(memory), title: `${{ project: '项目', domain: '领域', global: '全局' }[target.type]}记忆`, text: clean(memory.text) };
50
50
  if (memorySize + encode(item).length + 1 > 3000) continue;
51
51
  memories.push(item); memorySize += encode(item).length + 1;
52
52
  }
@@ -128,6 +128,30 @@ class MemoryService {
128
128
  }
129
129
  return report;
130
130
  }
131
+ async create(input) {
132
+ if (this.closed) throw new DomainError('PROJECT_BUSY', '引擎正在关闭,请稍后重试。', { status: 409 });
133
+ if (!input || typeof input !== 'object' || Array.isArray(input) || Object.keys(input).some(key => !['text', 'brainId', 'scopeType', 'scopeId'].includes(key))) throw new DomainError('INVALID_ARGUMENT', '只支持填写记忆内容与适用范围,不能指定来源证据。');
134
+ if (typeof input.text !== 'string' || !input.text.trim() || input.text.length > 2000) throw new DomainError('INVALID_ARGUMENT', '记忆内容必须为 1–2000 个字符。');
135
+ const brainId = input.brainId === undefined ? 'brain-default' : input.brainId;
136
+ validateProjectId(brainId);
137
+ const type = input.scopeType === undefined ? 'global' : input.scopeType;
138
+ const targetId = input.scopeId === undefined && type === 'global' ? brainId : input.scopeId;
139
+ const job = AtomicFile.withFileLock(this.lock(), async () => {
140
+ this.validateScope(type, targetId, brainId);
141
+ if (!this.brainScope(brainId)) throw new DomainError('INVALID_ARGUMENT', '请先绑定智脑共享目录。', { status: 409 });
142
+ const text = input.text.trim(), key = fingerprint(brainId, type, targetId, text);
143
+ if (this.store.state.memories.some(m => fingerprint(m.brainId || 'brain-default', this.scopeOf(m).type, this.scopeOf(m).id, m.text) === key)) throw new DomainError('MEMORY_DUPLICATE', '该范围内已存在相同记忆。', { status: 409 });
144
+ // User confirmation is the provenance; never invent a model run or Git commit.
145
+ const now = new Date().toISOString();
146
+ 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 };
147
+ const before = this.store.state.memories;
148
+ this.store.state.memories = [...before, memory];
149
+ try { this.store.save(); } catch (error) { this.store.state.memories = before; throw error; }
150
+ await this.materialize(); return this.project(memory);
151
+ });
152
+ this.writes.add(job);
153
+ try { return await job; } finally { this.writes.delete(job); }
154
+ }
131
155
  async update(id, input) {
132
156
  if (this.closed) throw new DomainError('PROJECT_BUSY', '引擎正在关闭,请稍后重试。', { status: 409 });
133
157
  if (Object.keys(input).some(key => !['text', 'status', 'scopeType', 'scopeId'].includes(key))) throw new DomainError('INVALID_ARGUMENT', '来源证据和文件路径不能修改。');
@@ -159,7 +183,8 @@ class MemoryService {
159
183
  // Preserve legacy global bytes until the user actually edits that record.
160
184
  const scopeText = m.scopeType ? `适用范围:${{ global: '全局', domain: '领域', project: '项目' }[scope.type]} · ${scope.id}\n\n` : '';
161
185
  const text = m.scopeType ? m.text.replace(/^\s*(#{1,6})\s/gm, '\\$1 ') : m.text;
162
- return `## ${m.id}\n\n${scopeText}${text}\n\n来源:${m.evidence.projectId} · ${m.evidence.commitSha} · ${m.evidence.entryId}\n`;
186
+ const source = m.sourceType === 'manual' ? '手动添加(用户确认)' : `${m.evidence.projectId} · ${m.evidence.commitSha} · ${m.evidence.entryId}`;
187
+ return `## ${m.id}\n\n${scopeText}${text}\n\n来源:${source}\n`;
163
188
  }).join('\n');
164
189
  }
165
190
  existingEntries(scope) {
@@ -121,7 +121,8 @@ class EngineRetrieval extends KnowledgeRetrievalService {
121
121
  const run = this.store.listRuns(row.scope_project_id).find(r => r.promotedAt && r.staging?.operations?.some(op => op.path === row.entry_id));
122
122
  const source = this.store.getProject(row.scope_project_id) || this.store.state.sharedScopes.find(s => s.id === row.scope_project_id);
123
123
  const memory = source?.kind === 'brain' && row.entry_id.startsWith('memories/') ? this.memories.memoryForChunk(source, row.entry_id, row.heading_path) : null;
124
- return { ...row, projectName: source?.name || row.scope_project_id, path: row.entry_id, snippet: row.chunk_text, score: row.relevance_score, projectId: row.scope_project_id, commitSha: memory?.evidence.commitSha || run?.commitSha || null, ...(memory ? { memoryScopeType: this.memories.scopeOf(memory).type, memoryScopeId: this.memories.scopeOf(memory).id, memoryId: memory.id } : {}) };
124
+ const commitSha = memory ? memory.evidence?.commitSha || null : run?.commitSha || null;
125
+ return { ...row, projectName: source?.name || row.scope_project_id, path: row.entry_id, snippet: row.chunk_text, score: row.relevance_score, projectId: row.scope_project_id, commitSha, ...(memory ? { memoryScopeType: this.memories.scopeOf(memory).type, memoryScopeId: this.memories.scopeOf(memory).id, memoryId: memory.id, ...(memory.sourceType === 'manual' ? { memorySourceType: 'manual' } : {}) } : {}) };
125
126
  });
126
127
  const slots = result.results.map((r, i) => r.memoryId ? i : -1).filter(i => i >= 0);
127
128
  const ordered = slots.map(i => result.results[i]).sort((a, b) => ({ project: 0, domain: 1, global: 2 }[a.memoryScopeType] - { project: 0, domain: 1, global: 2 }[b.memoryScopeType]) || b.score - a.score);
@@ -103,6 +103,11 @@ async function api(method, pathname, search, body, store, coordinator, ctx) {
103
103
  if (method === 'GET' && pathname === '/api/health') return { name: 'knowledge-engine', version: VERSION, dataSchemaVersion: store.state.schemaVersion, debug: ctx.debug, executionEnabled: coordinator.executionEnabled, uptimeSec: Math.floor((Date.now() - ctx.startedAt) / 1000), dataDir: ctx.dataDir, counts: { projects: store.state.projects.length, runs: store.state.runs.length, running: coordinator.active.size }, journal: { cursor: store.state.cursor, cursors: store.state.journalCursors, consumer: 'knowledge-engine', enabled: coordinator.consume, error: coordinator.journalError || null, failures: [...coordinator.journalFailures.values()] }, concurrency: coordinator.concurrency };
104
104
  if (method === 'GET' && pathname === '/api/projects') return { projects: store.listProjects() };
105
105
  if (method === 'GET' && pathname === '/api/memories') return { memories: coordinator.memories.list(Object.fromEntries(search)) };
106
+ if (method === 'POST' && pathname === '/api/memories') {
107
+ const memory = await coordinator.memories.create(body);
108
+ coordinator.emit('progress', store.addEvent({ type: 'memory.created', summary: '已手动添加长期记忆' }));
109
+ return { memory };
110
+ }
106
111
  const memoryMatch = pathname.match(/^\/api\/memories\/([^/]+)$/);
107
112
  if (method === 'PATCH' && memoryMatch) {
108
113
  const memory = await coordinator.memories.update(decodeURIComponent(memoryMatch[1]), body);
@@ -39,7 +39,13 @@ class EngineStore {
39
39
  }
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
- for (const memory of raw.memories || []) if (!memory || !['candidate', 'confirmed', 'ignored'].includes(memory.status) || typeof memory.text !== 'string' || !memory.evidence?.projectId || !/^[a-f0-9]{40}$/.test(memory.evidence.commitSha || '')) throw new Error('Invalid memory record');
42
+ for (const memory of raw.memories || []) {
43
+ if (!memory || !['candidate', 'confirmed', 'ignored'].includes(memory.status) || typeof memory.text !== 'string' || memory.sourceType !== undefined && !['manual', 'agent'].includes(memory.sourceType)) throw new Error('Invalid memory record');
44
+ if (memory.sourceType === 'manual') {
45
+ validateProjectId(memory.brainId);
46
+ 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
+ } else if (!memory.evidence?.projectId || !/^[a-f0-9]{40}$/.test(memory.evidence.commitSha || '')) throw new Error('Invalid memory evidence');
48
+ }
43
49
  for (const memory of raw.memories || []) if (memory.scopeType !== undefined || memory.scopeId !== undefined) {
44
50
  if (!['global', 'domain', 'project'].includes(memory.scopeType)) throw new Error('Invalid memory scope');
45
51
  validateProjectId(memory.scopeId);
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "schema": "project-knowledge/module-content/v1",
3
3
  "productFiles": {
4
- "ui/index.html": "2f05ea669e27cba542f80c700a4a0b59cc8db3f15f7a83604fc6049ac816c53d",
5
- "ui/product.js": "7b1495a5ead94d908c3f2380dd5fd1c2842c28e62a287b8aa6eaad1be22dfbf9",
4
+ "ui/index.html": "b9055cf2ff40a11286eb65f3a990284bbcc245685f60e210a33999a41fc2055e",
5
+ "ui/product.js": "811e496f4c240dd10d4d1849072914b20dde266f4abeceaa9c888f754bf49b61",
6
6
  "ui/product.css": "0c31d4937780b3a277689b4619208ad0e5dc59a84d7e27fc16b5f1815d59b47c",
7
7
  "ui/favicon.svg": "c48be946b224112ebb0238b44263f7a61bda31e531afa895c2e9801c0ff24f32",
8
8
  "plugins/project-knowledge/hooks/claude.json": "d61510327e3e1facbb4c645228e9f50cad05a88b33d80c4c22e5ba9f8b9f5359",
9
9
  "plugins/project-knowledge/scripts/context-hook.cjs": "b138fab6d8fcb35f8101ccfda565ccec694acb888f94347c5864c804ebb5e1a0",
10
- "plugins/project-knowledge/.claude-plugin/plugin.json": "cf50fe8dacbec068c46e531114c1f81576e63578692ab165795b96c69ace63a3",
11
- "plugins/project-knowledge/.mcp.json": "6b4e25e51cc24361810af627be6bda0a97cc416230056308fab56ed5ddc1d73b",
10
+ "plugins/project-knowledge/.claude-plugin/plugin.json": "5692e901adefdf64bf2a0df6de9dccbb730ab9ac6141788f9e7dc7d29940f71d",
11
+ "plugins/project-knowledge/.mcp.json": "eb7633355da3e8bd68fb0676d29fc40496f4412d0ab9675db6c55c2558f61b64",
12
12
  "plugins/project-knowledge/hooks/codex.json": "216e03c07295606ef6ebb2c4f669ad0d790b5e7c4f3f34600488033db75e4239",
13
- "plugins/project-knowledge/.codex-plugin/plugin.json": "33075d6e47ba58c443eda339706f57688efb9974fa3bbb08f7cc09604602a87f"
13
+ "plugins/project-knowledge/.codex-plugin/plugin.json": "1ad5124e54f8580665f7a11dd69f73bdf6b5a374da65adf8babbc7bf9acce709"
14
14
  },
15
15
  "modules": {
16
16
  "terminal": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanqianx/project-knowledge",
3
- "version": "5.0.13",
3
+ "version": "5.0.14-rc.1",
4
4
  "description": "Knowledge base manager with Git integration, AI-driven analysis, and bilingual (zh-CN/en-US) knowledge output",
5
5
  "main": "_site/server.js",
6
6
  "scripts": {
@@ -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.13",
5
+ "version": "5.0.14-rc.1",
6
6
  "description": "Read durable project decisions and implementation history from Project Knowledge.",
7
7
  "author": {
8
8
  "name": "SanQianX",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-knowledge",
3
- "version": "5.0.13",
3
+ "version": "5.0.14-rc.1",
4
4
  "description": "Read durable project decisions and implementation history from Project Knowledge.",
5
5
  "author": {
6
6
  "name": "SanQianX",
@@ -6,7 +6,7 @@
6
6
  "args": [
7
7
  "-y",
8
8
  "--package",
9
- "@sanqianx/project-knowledge@5.0.13",
9
+ "@sanqianx/project-knowledge@5.0.14-rc.1",
10
10
  "project-knowledge-mcp"
11
11
  ]
12
12
  }
package/ui/index.html CHANGED
@@ -25,7 +25,7 @@
25
25
  <section class="view" id="view-home"><div class="welcome"><div><small>工作概览</small><h2 id="homeTitle">我的智脑</h2><p>保存项目事实,连接相关经验。你的长期偏好,只在确认后使用。</p></div><div class="actions"><button class="primary" data-go="work">开始工作</button><button data-action="import">导入项目</button></div></div><div id="homeContent"></div></section>
26
26
  <section class="view module-view" id="view-work"><div id="workEmpty" class="empty" hidden>导入项目后,就可以在这里开始工作。<button data-action="import">导入项目</button></div><div class="work-layout" id="workLayout"><div class="frame-container"><iframe id="workFrame" title="Agent 工作台" referrerpolicy="same-origin"></iframe></div><aside class="knowledge-context"><header><h2>本次知识引用</h2><small id="contextScope">任务发送前自动检索</small></header><div id="contextContent" class="empty small-empty">发送任务后显示实际检索到的项目事实、领域经验和已确认记忆。</div></aside></div></section>
27
27
  <section class="view module-view" id="view-search"><div class="module-toolbar"><span>查找知识</span><div class="segmented" id="searchScope"><button data-scope="project">当前项目</button><button data-scope="domain">所属领域</button><button data-scope="brain">整个智脑</button></div></div><div id="searchEmpty" class="empty" hidden>尚未导入项目。<button data-action="import">导入项目</button></div><div class="frame-container"><iframe id="vectorFrame" title="知识检索" referrerpolicy="same-origin"></iframe></div></section>
28
- <section class="view" id="view-memory"><div class="section-heading"><div><h2>长期记忆</h2><p>审核内容与适用范围后确认。来源项目不等于适用项目。</p></div></div><div class="memory-toolbar"><div class="segmented" id="memoryTabs"><button data-memory-tab="confirmed">已确认</button><button data-memory-tab="candidate">候选箱 <span id="candidateCount"></span></button></div><div class="actions"><select id="memoryScopeFilter" aria-label="筛选记忆范围"><option value="">全部范围</option><option value="global">全局记忆</option><option value="domain">领域记忆</option><option value="project">项目记忆</option></select><select id="memoryTargetFilter" aria-label="筛选记忆目标" hidden></select></div></div><div id="memoryContent"></div></section>
28
+ <section class="view" id="view-memory"><div class="section-heading"><div><h2>长期记忆</h2><p>手动添加约定,或审核 Agent 提取的候选;确认内容与适用范围后生效。</p></div><button id="addMemory" class="primary" disabled>添加记忆</button></div><div class="memory-toolbar"><div class="segmented" id="memoryTabs"><button data-memory-tab="confirmed">已确认</button><button data-memory-tab="candidate">候选箱 <span id="candidateCount"></span></button></div><div class="actions"><select id="memoryScopeFilter" aria-label="筛选记忆范围"><option value="">全部范围</option><option value="global">全局记忆</option><option value="domain">领域记忆</option><option value="project">项目记忆</option></select><select id="memoryTargetFilter" aria-label="筛选记忆目标" hidden></select></div></div><div id="memoryContent"></div></section>
29
29
  <section class="view" id="view-projects"><div class="section-heading"><div><h2>领域与项目</h2><p>统一管理归属,项目知识仍保存在原来的目录。</p></div><div class="actions"><button id="createDomain">创建领域</button><button class="primary" data-action="import">导入项目</button></div></div><div class="hierarchy"><div class="panel tree" id="projectTree"></div><div class="panel" id="projectDetail"></div></div></section>
30
30
  <section class="view module-view" id="view-activity"><div class="module-toolbar"><div class="segmented" id="activityTabs"><button data-activity-tab="updates">知识更新</button><button data-activity-tab="sessions">开发会话与提交</button></div><span id="activityConnection" class="muted">正在连接更新</span></div><div class="activity-updates" id="activityUpdates"></div><div class="frame-container" id="activitySessions" hidden><iframe id="bridgeFrame" title="开发会话与提交" referrerpolicy="same-origin"></iframe></div></section>
31
31
  </div>
@@ -57,7 +57,7 @@
57
57
  </form></dialog>
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
- <dialog id="memoryDialog"><form id="memoryForm"><div class="dialog-heading"><h2 id="memoryDialogTitle">审核记忆</h2><button type="button" data-close="memoryDialog" aria-label="关闭">×</button></div><label>记忆内容<textarea id="memoryText" maxlength="2000" required rows="5"></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"><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>
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
61
  <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
62
  <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
63
  <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.js CHANGED
@@ -105,10 +105,11 @@
105
105
  $('projectDetail').innerHTML = `<h2>${esc(project.name)}</h2><div class="detail-label">代码目录</div><div class="detail-path">${esc(project.workspacePath)}</div><div class="detail-label">知识目录</div><div class="detail-path">${esc(project.knowledgePath)}</div><label>所属领域<select id="projectDomain"><option value="">未分组</option>${state.brain.domains.map(d => `<option value="${esc(d.id)}" ${d.id === project.domainId ? 'selected' : ''}>${esc(d.name)}</option>`).join('')}</select></label><h3>项目连接</h3>${Object.entries(modules).map(([key, name]) => `<div class="row"><div>${esc(name)}</div>${badge(project.modules[key]?.registered ? '已连接' : '待恢复', project.modules[key]?.registered ? 'up' : 'issue')}</div>`).join('')}<div class="actions"><button class="primary" data-go="work">开始工作</button><button data-action="repair">修复接线</button><button data-action="unbind">解绑并保留数据</button></div>`;
106
106
  }
107
107
  function renderMemories() {
108
+ $('addMemory').disabled = !state.engineAvailable;
108
109
  all('[data-memory-tab]').forEach(button => button.classList.toggle('active', button.dataset.memoryTab === state.memoryTab));
109
110
  const type = $('memoryScopeFilter').value, target = $('memoryTargetFilter').value;
110
111
  const memories = state.snapshot.memories.filter(m => m.status === state.memoryTab && (!type || (m.scopeType || 'global') === type) && (!type || type === 'global' || !target || m.scopeId === target));
111
- $('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>来源:${esc(projectName(m.evidence.projectId))} · commit ${esc(m.evidence.commitSha.slice(0, 8))}<br>${esc(m.evidence.entryId)}</small><div class="actions"><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')}`}</div></article>`).join('')}</div>` : `<div class="empty">${type ? '此范围内没有匹配的记忆。' : state.memoryTab === 'confirmed' ? '还没有已确认的长期记忆。' : '候选箱为空。维护产物中的有据偏好会进入这里,等待你的确认。'}</div>`;
112
+ $('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')}`}</div></article>`).join('')}</div>` : `<div class="empty">${type ? '此范围内没有匹配的记忆。' : state.memoryTab === 'confirmed' ? '还没有已确认的长期记忆。可以点击“添加记忆”记录约定。' : '候选箱为空。维护产物中的有据偏好会进入这里,等待你的确认。'}</div>`;
112
113
  }
113
114
  function memoryScopeName(memory) {
114
115
  const type = memory.scopeType || 'global';
@@ -135,12 +136,26 @@
135
136
  $('memoryText').value = memory.text; $('memoryScope').value = memory.scopeType || 'global';
136
137
  updateMemoryTarget($('memoryScope').value, memory.scopeId || memory.brainId);
137
138
  $('memoryOriginal').textContent = `原始总结:${memory.originalText || memory.text}`;
138
- $('memoryEvidence').textContent = `${projectName(memory.evidence.projectId)} · ${memory.evidence.commitSha} / ${memory.evidence.entryId}`;
139
- $('memoryQuote').textContent = memory.evidence.quote;
139
+ $('memoryEvidence').textContent = memory.evidence ? `${projectName(memory.evidence.projectId)} · ${memory.evidence.commitSha} / ${memory.evidence.entryId}` : '';
140
+ $('memoryQuote').textContent = memory.evidence?.quote || '';
141
+ $('memoryEvidenceDetails').hidden = memory.sourceType === 'manual'; $('manualMemoryHint').hidden = memory.sourceType !== 'manual';
140
142
  $('memoryFeedback').hidden = true; $('saveMemoryCandidate').hidden = memory.status !== 'candidate';
141
143
  $('confirmMemory').textContent = memory.status === 'candidate' ? '确认并保存' : '保存修改';
142
144
  $('memoryDialog').showModal();
143
145
  }
146
+ function openAddMemory() {
147
+ if (!state.engineAvailable) { toast('知识引擎暂时不可用,请恢复连接后重试。'); return; }
148
+ if (!state.root) { toast('请先在设置中配置知识根目录,再添加记忆。'); return; }
149
+ editingMemoryId = '';
150
+ const project = activeProject(), filter = $('memoryScopeFilter').value;
151
+ const type = filter || (project ? 'project' : 'global');
152
+ const target = $('memoryTargetFilter').value || (type === 'project' ? project?.projectId : type === 'domain' ? project?.domainId : '');
153
+ $('memoryForm').reset(); $('memoryDialogTitle').textContent = '添加记忆'; $('memoryScope').value = type;
154
+ updateMemoryTarget(type, target);
155
+ $('memoryEvidenceDetails').hidden = true; $('manualMemoryHint').hidden = false;
156
+ $('memoryFeedback').hidden = true; $('saveMemoryCandidate').hidden = true; $('confirmMemory').textContent = '确认并保存';
157
+ $('memoryDialog').showModal(); $('memoryText').focus();
158
+ }
144
159
  function renderActivity() {
145
160
  all('[data-activity-tab]').forEach(button => button.classList.toggle('active', button.dataset.activityTab === state.activityTab));
146
161
  $('activityUpdates').hidden = state.activityTab !== 'updates'; $('activitySessions').hidden = state.activityTab !== 'sessions';
@@ -455,21 +470,26 @@
455
470
  on('createDomain', 'click', () => { $('domainForm').reset(); $('domainDialog').showModal(); });
456
471
  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); });
457
472
  on('memoryScopeFilter', 'change', updateMemoryFilter); on('memoryTargetFilter', 'change', renderMemories);
473
+ on('addMemory', 'click', openAddMemory);
458
474
  on('memoryScope', 'change', () => {
459
475
  const memory = state.snapshot.memories.find(m => m.id === editingMemoryId), type = $('memoryScope').value;
460
- const source = state.projects.find(p => p.projectId === memory?.evidence.projectId);
476
+ const source = state.projects.find(p => p.projectId === memory?.evidence?.projectId) || activeProject();
461
477
  updateMemoryTarget(type, type === 'project' ? source?.projectId : type === 'domain' ? source?.domainId : '');
462
478
  });
463
479
  on('memoryForm', 'submit', async e => {
464
480
  e.preventDefault();
465
481
  if ($('confirmMemory').disabled) return;
466
- const type = $('memoryScope').value, body = { text: $('memoryText').value, scopeType: type, scopeId: type === 'global' ? state.brain.id : $('memoryTarget').value };
467
- if (e.submitter?.dataset.mode === 'confirmed') body.status = 'confirmed';
482
+ const creating = !editingMemoryId, type = $('memoryScope').value, body = { text: $('memoryText').value.trim(), scopeType: type, scopeId: type === 'global' ? state.brain.id : $('memoryTarget').value };
483
+ if (!body.text) { $('memoryFeedback').textContent = '请填写记忆内容。'; $('memoryFeedback').hidden = false; return; }
484
+ if (creating) body.brainId = state.brain.id;
485
+ else if (e.submitter?.dataset.mode === 'confirmed') body.status = 'confirmed';
486
+ $('memoryFeedback').hidden = true;
468
487
  $('confirmMemory').disabled = true; $('saveMemoryCandidate').disabled = true;
469
488
  try {
470
- const result = await api(`/api/engine/memories/${encodeURIComponent(editingMemoryId)}`, { method: 'PATCH', body });
489
+ const result = await api(creating ? '/api/engine/memories' : `/api/engine/memories/${encodeURIComponent(editingMemoryId)}`, { method: creating ? 'POST' : 'PATCH', body });
490
+ if (creating) { state.memoryTab = 'confirmed'; $('memoryScopeFilter').value = body.scopeType; updateMemoryFilter(); if (body.scopeType !== 'global') $('memoryTargetFilter').value = body.scopeId; }
471
491
  $('memoryDialog').close(); await refreshSnapshot();
472
- toast(result.memory.syncStatus === 'pending' ? '确认已保存,记忆文件待恢复;未同步内容不会注入任务。' : body.status === 'confirmed' ? '记忆内容与适用范围已确认。' : '候选已保存,尚未生效。');
492
+ toast(result.memory.syncStatus === 'pending' ? '确认已保存,记忆文件待恢复;未同步内容不会注入任务。' : creating ? '记忆已添加,按所选范围生效。' : result.memory.status === 'confirmed' ? '记忆内容与适用范围已确认。' : '候选已保存,尚未生效。');
473
493
  } catch (error) { $('memoryFeedback').textContent = error.message; $('memoryFeedback').hidden = false; }
474
494
  finally { $('confirmMemory').disabled = false; $('saveMemoryCandidate').disabled = false; }
475
495
  });
@@ -3,24 +3,24 @@
3
3
  "modules": {
4
4
  "knowledge-engine": {
5
5
  "name": "@sanqianx/ai-coding-knowledge-engine",
6
- "version": "0.2.5-rc.3",
6
+ "version": "0.2.6",
7
7
  "root": "_modules/knowledge-engine",
8
8
  "files": {
9
- "README.md": "62f620325d7776952fdd3d0c0a7ca9e4f6b95719533a83ca635077c0d9ed32d5",
10
- "package.json": "5355d1f9f9703601241b29eabf16072e4295cc860668cc2730d6a7f0591637fa",
9
+ "README.md": "e940e32f14a00a61aae6ee424679ac3c9fc48ba2c4b4171a9bf6fb44b27a74e6",
10
+ "package.json": "a39cd05d857ae2277a0a31019df5c72cee581e8e527355513b2123ed15f07dd2",
11
11
  "src/analysis-contract.js": "913a63b63fb4a4a9df071f95bbeb8f8c40ef87e2f8445ab0742086ced70ffbf7",
12
12
  "src/analysis-output.js": "95a3c19dcc9ab85d1dba7fee5b9aec80f9c000dfba89da3fd0be25a81e2fb680",
13
13
  "src/analysis-worker.js": "a105cabe6e1573b2d986b1e7a5aaf615e2fb53524156fc8e4e2c0a71f2c97e55",
14
14
  "src/bin.js": "22208907431d0f3a971a669c7e5eb44b3b7af9bbb36fd7ba17c3b90c4288bd70",
15
15
  "src/bridge-client.js": "ae0dba21e4286694bcd9e064ca36564ec997969ae1eb1ce4f8a6665e447cbffe",
16
- "src/context.js": "61de65d35ca5db558a1ba6a858cce2c81614659f5a883b2fc820d0b66282b305",
16
+ "src/context.js": "bbe7bba175b17e87f4b142afd6731df31e8791ed9c20f3cc7345a39e66398087",
17
17
  "src/coordinator.js": "8b9a572f415844db6c34dd62e485fb2c2ff50b655ff662802c5cab6207c2c8f0",
18
18
  "src/diagnostics.js": "f7b516ace61a1e9faaa6876930b9e2e258499a25857d1203f4e53d28bfd0fa1a",
19
19
  "src/headless-analyzer.js": "e01f81b8f24a4423c19dd8893be8c702a85edbdbb644322e3acab96c3ebaac7d",
20
20
  "src/index-request.js": "78f26b872299005499fff4f2d1f9e8f986784f2ff36a5cf2be2b1e7b7c7d9378",
21
21
  "src/knowledge-targets.js": "aa44ec285d16d7b1c0b63bf084058a1d6f867fa9929c753b6b0cbb24f7b20ee2",
22
- "src/memories.js": "6623fd27d45e297384082d078844aebd55bd0326bd657baae5919e85f9802d28",
23
- "src/retrieval.js": "2049f9a4d4ce2d8fb4ccf72a4420ec842b94a5a45936ec83a0a3091b92d9ef13",
22
+ "src/memories.js": "bba8e19380e906a80fd3e5d2a7f809d940466924578c8187d1f376f906f10f2b",
23
+ "src/retrieval.js": "10189819e44d7898d47ddc269cf6dd90b188ef6c3b280147eba24fd48b30a588",
24
24
  "src/runtime/README.md": "9be676eca76a7857b34d7357194e6f0ee733c22e61d962d53984715dbec7f673",
25
25
  "src/runtime/atomic-file.js": "9846b4b69db5ded811acf19a1181ef93615d93efaa542b858e552a7f3124ad23",
26
26
  "src/runtime/automation-config.js": "6498c5397be12b155829740595a1553d4dcd6eaa7db6ced3ef88fdc60218f00e",
@@ -39,13 +39,13 @@
39
39
  "src/runtime/layout.js": "c48fa1db62e5df1edefe13218e470c5ceea4ef2781293512c2c18e5c2ba0c7e9",
40
40
  "src/runtime/markdown-knowledge-indexer.js": "4d6b5b955849d274ca0614ef030c94526da7c39ab0874f02c168d0681f5b1406",
41
41
  "src/runtime/scanner.js": "d99fa8d74a6fbbaae75ca49f62127e14cf9e48b1404e4b4d597ef39df5441410",
42
- "src/server.js": "1d2ff083eb96bc10f86544079001692abd5e29fc3e26488bd7e8ee497f8a4ec6",
43
- "src/store.js": "b97e2d8045b67e67043b69622230bc01b0e8fcc45476a51c7c9a3ecf479a6447",
42
+ "src/server.js": "3aa7b6b3aec141f3d1fde63fbbd4ee8b3c95a92c851726a56717c3537fe4954b",
43
+ "src/store.js": "cd147926dc7744d4a4691c2facb5e99b611e7e400beaed7377ae5a69e41bb39f",
44
44
  "ui/app.css": "7ad370c269e60a7b5ec907d81a607b2d16bd07b2d7c22a81f102dab51eeb2889",
45
45
  "ui/app.js": "33a2ba38f0dafbbb2e30a1eb3fc93476bfcaf528e124e1a9b2edd2fb69221e9e",
46
46
  "ui/index.html": "ce3565cfefd4838b11d37a91a906ef26ca5a1834844fe3f7c897b56851a3c418"
47
47
  },
48
- "sourceCommit": "c0d2a46d9a985c4093e23e080789afe012ed526e"
48
+ "sourceCommit": "c8c600e6674e96caa3663612a5a9fab31eefe9be"
49
49
  }
50
50
  }
51
51
  }