@yolk_vat-y/dsh-project-memory 0.4.4 → 0.5.0
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/CHANGELOG.md +29 -0
- package/README.md +11 -2
- package/README.zh-CN.md +11 -2
- package/client/client.js +798 -81
- package/client/client.js.map +1 -1
- package/package.json +2 -2
- package/src/auto-inject.js +214 -0
- package/src/client/MemoryView.tsx +349 -0
- package/src/client/TaskCommandNode.tsx +30 -11
- package/src/client/TaskComponents.tsx +48 -1
- package/src/client/TaskPanel.module.css +122 -0
- package/src/client/TaskPanel.tsx +74 -14
- package/src/client/client.ts +4 -4
- package/src/client/locales.ts +68 -0
- package/src/client/task-data-store.ts +21 -5
- package/src/commands/insight-actions.js +390 -0
- package/src/commands/task-actions.js +8 -0
- package/src/commands/tasks.js +1 -0
- package/src/global-seed.js +41 -0
- package/src/index.js +38 -4
- package/src/insight-store.js +558 -0
- package/src/project-profile.js +78 -0
- package/src/reflection-pipeline.js +146 -0
- package/src/similarity.js +52 -0
- package/src/store.js +73 -0
- package/src/tools/lesson-tools.js +101 -0
- package/src/tools/task-tools.js +12 -2
- package/src/types.js +56 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
// PR 3 服务器侧:/insight 命令(记忆视图:task 草稿 / project / global)
|
|
2
|
+
// 动作:list / confirm / promote / demote / archive / restore / delete
|
|
3
|
+
// 纯函数 actInsight / listInsights 操作传入的 store/gs 内存对象,持久化由调用方
|
|
4
|
+
// (命令 handler:store.commit + gs.commit)统一负责,便于单测。
|
|
5
|
+
import { ProjectMemoryStore } from '../store.js'
|
|
6
|
+
import { memoryRootFor } from '../util/fs.js'
|
|
7
|
+
import { cfgInsight, GlobalStore, normalizeInsight, defaultGlobalFile, saveInsight, INSIGHT_KINDS } from '../insight-store.js'
|
|
8
|
+
import { projectRootFor } from '../setup/taskbridge.js'
|
|
9
|
+
|
|
10
|
+
function insSummary(ins, extra = {}) {
|
|
11
|
+
const s = ins || {}
|
|
12
|
+
return {
|
|
13
|
+
id: s.id,
|
|
14
|
+
kind: s.kind || 'lesson',
|
|
15
|
+
scope: s.scope || 'project',
|
|
16
|
+
title: s.title || '',
|
|
17
|
+
// 摘要按 kind 暴露“非标题正文”,编辑表单据此精确预填(不再用单一 body 猜)
|
|
18
|
+
fix: s.fix && s.fix !== s.title ? s.fix : undefined,
|
|
19
|
+
solution: s.solution && s.solution !== s.title ? s.solution : undefined,
|
|
20
|
+
reason: s.reason && s.reason !== s.title ? s.reason : undefined,
|
|
21
|
+
pattern: s.pattern && s.pattern !== s.title ? s.pattern : undefined,
|
|
22
|
+
problem: s.problem && s.problem !== s.title ? s.problem : undefined,
|
|
23
|
+
choice: s.choice && s.choice !== s.title ? s.choice : undefined,
|
|
24
|
+
body: (() => {
|
|
25
|
+
if (s.kind === 'procedure' && Array.isArray(s.steps) && s.steps.length) {
|
|
26
|
+
return `步骤 ${s.steps.length} 条:${String(s.steps[0]).slice(0, 80)}`
|
|
27
|
+
}
|
|
28
|
+
const byKind = s.kind === 'lesson' ? s.fix : s.kind === 'decision' ? s.reason : s.kind === 'experience' ? s.solution : s.body
|
|
29
|
+
const cands = [byKind, s.fix, s.solution, s.reason, s.body, s.choice, s.pattern, s.problem]
|
|
30
|
+
for (const v of cands) if (v && v !== s.title) return v
|
|
31
|
+
return ''
|
|
32
|
+
})(),
|
|
33
|
+
draft: s.draft === true,
|
|
34
|
+
archived: s.archived === true,
|
|
35
|
+
confidence: typeof s.confidence === 'number' ? s.confidence : null,
|
|
36
|
+
hitCount: s.hitCount || 0,
|
|
37
|
+
members: (s.sourceTaskIds || []).length,
|
|
38
|
+
files: (s.files || []).length,
|
|
39
|
+
symbols: (s.symbols || []).length,
|
|
40
|
+
trigger: s.trigger ? { keywords: s.trigger.keywords || [], scope: s.trigger.scope || [] } : null,
|
|
41
|
+
steps: Array.isArray(s.steps) ? s.steps.slice(0, 50) : null,
|
|
42
|
+
updatedAt: s.updatedAt || '',
|
|
43
|
+
...extra,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function findTaskInsight(store, id) {
|
|
48
|
+
for (const t of store.getTasks() || []) {
|
|
49
|
+
const hit = (t.insights || []).find((i) => i.id === id)
|
|
50
|
+
if (hit) return { task: t, insight: hit }
|
|
51
|
+
}
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 列出某 scope 的记忆(task 聚合所有任务草稿;project/global 各自池)。 */
|
|
56
|
+
export function listInsights(store, gs, scope) {
|
|
57
|
+
const s = scope || 'project'
|
|
58
|
+
const items = []
|
|
59
|
+
if (s === 'task') {
|
|
60
|
+
for (const t of store.getTasks() || []) {
|
|
61
|
+
for (const i of t.insights || []) items.push(insSummary(i, { taskId: t.id, taskTitle: t.title }))
|
|
62
|
+
}
|
|
63
|
+
} else if (s === 'global') {
|
|
64
|
+
for (const i of (gs ? gs.items() : [])) items.push(insSummary(i))
|
|
65
|
+
} else {
|
|
66
|
+
for (const i of store.insightItems()) items.push(insSummary(i))
|
|
67
|
+
}
|
|
68
|
+
return { scope: s, count: items.length, items }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 记忆动作(内存变更)。返回 { ok, text };失败不动任何数据。
|
|
73
|
+
* action: confirm(draft→正式) | promote(向上移一级) | demote(向下移一级) |
|
|
74
|
+
* archive | restore | delete;scope: task|project|global(列表里条目的当前 scope)。
|
|
75
|
+
* demote 到 task 需要 taskId;promote/global 之上无层级时返回错误。
|
|
76
|
+
*/
|
|
77
|
+
export function actInsight({ store, gs, action, scope, id, taskId, cfg }) {
|
|
78
|
+
const act = action || ''
|
|
79
|
+
const s = scope || 'project'
|
|
80
|
+
if (!id) return { ok: false, text: '需要 id' }
|
|
81
|
+
const invalid = () => ({ ok: false, text: `insight ${id} 不在 ${s} 层(可能已移动/删除)` })
|
|
82
|
+
|
|
83
|
+
if (s === 'task') {
|
|
84
|
+
const found = findTaskInsight(store, id)
|
|
85
|
+
if (!found) return invalid()
|
|
86
|
+
const { task, insight } = found
|
|
87
|
+
if (act === 'confirm') {
|
|
88
|
+
insight.draft = false
|
|
89
|
+
insight.updatedAt = new Date().toISOString()
|
|
90
|
+
store.markTasksDirty()
|
|
91
|
+
return { ok: true, text: `已审核:${insight.title}` }
|
|
92
|
+
}
|
|
93
|
+
if (act === 'delete') {
|
|
94
|
+
task.insights = (task.insights || []).filter((i) => i.id !== id)
|
|
95
|
+
store.markTasksDirty()
|
|
96
|
+
return { ok: true, text: `已删除任务级条目 ${id}` }
|
|
97
|
+
}
|
|
98
|
+
if (act === 'archive' || act === 'restore') {
|
|
99
|
+
insight.archived = act === 'archive'
|
|
100
|
+
insight.updatedAt = new Date().toISOString()
|
|
101
|
+
store.markTasksDirty()
|
|
102
|
+
return { ok: true, text: `已${act === 'archive' ? '归档' : '恢复'}` }
|
|
103
|
+
}
|
|
104
|
+
if (act === 'promote') {
|
|
105
|
+
// task → project(手动提升,无视自动阈值)
|
|
106
|
+
const items = store.insightItems()
|
|
107
|
+
const copy = normalizeInsight(insight, { scope: 'project', source: insight.source, nowIso: new Date().toISOString() })
|
|
108
|
+
copy.id = id
|
|
109
|
+
copy.draft = false
|
|
110
|
+
copy.confidence = Math.max(insight.confidence || 0, (cfg && cfg.promoteConfidence) || 0.7)
|
|
111
|
+
copy.movedFrom = { scope: 'task', id, at: new Date().toISOString() }
|
|
112
|
+
copy.createdAt = insight.createdAt || copy.createdAt
|
|
113
|
+
copy.hitCount = insight.hitCount || 0
|
|
114
|
+
items.push(copy)
|
|
115
|
+
store.replaceInsightItems(items)
|
|
116
|
+
task.insights = (task.insights || []).filter((i) => i.id !== id)
|
|
117
|
+
store.markTasksDirty()
|
|
118
|
+
return { ok: true, text: `已提升到 project:${copy.title}` }
|
|
119
|
+
}
|
|
120
|
+
return { ok: false, text: `task 级不支持动作: ${act}` }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (s === 'project') {
|
|
124
|
+
const items = store.insightItems()
|
|
125
|
+
const idx = items.findIndex((i) => i.id === id)
|
|
126
|
+
if (idx === -1) return invalid()
|
|
127
|
+
const ins = items[idx]
|
|
128
|
+
if (act === 'archive' || act === 'restore') {
|
|
129
|
+
ins.archived = act === 'archive'
|
|
130
|
+
ins.updatedAt = new Date().toISOString()
|
|
131
|
+
store.replaceInsightItems(items)
|
|
132
|
+
return { ok: true, text: `已${act === 'archive' ? '归档' : '恢复'}` }
|
|
133
|
+
}
|
|
134
|
+
if (act === 'delete') {
|
|
135
|
+
items.splice(idx, 1)
|
|
136
|
+
store.replaceInsightItems(items)
|
|
137
|
+
return { ok: true, text: `已删除 project 条目 ${id}` }
|
|
138
|
+
}
|
|
139
|
+
if (act === 'promote') {
|
|
140
|
+
// project → global
|
|
141
|
+
if (!gs) return { ok: false, text: 'global 存储不可用' }
|
|
142
|
+
const now = new Date().toISOString()
|
|
143
|
+
const copy = normalizeInsight(ins, { scope: 'global', source: ins.source, nowIso: now })
|
|
144
|
+
copy.id = id
|
|
145
|
+
copy.draft = false
|
|
146
|
+
copy.movedFrom = { scope: 'project', id, at: now }
|
|
147
|
+
copy.createdAt = ins.createdAt || copy.createdAt
|
|
148
|
+
copy.hitCount = ins.hitCount || 0
|
|
149
|
+
gs.items().push(copy)
|
|
150
|
+
gs.markDirty()
|
|
151
|
+
items.splice(idx, 1)
|
|
152
|
+
store.replaceInsightItems(items)
|
|
153
|
+
return { ok: true, text: `已提升到 global:${copy.title}` }
|
|
154
|
+
}
|
|
155
|
+
if (act === 'demote') {
|
|
156
|
+
// project → task(需目标任务)
|
|
157
|
+
const task = store.getTask(taskId)
|
|
158
|
+
if (!task) return { ok: false, text: '降级到任务需要 taskId' }
|
|
159
|
+
const now = new Date().toISOString()
|
|
160
|
+
const copy = normalizeInsight(ins, { scope: 'task', source: ins.source, nowIso: now })
|
|
161
|
+
copy.id = `${id}_d${Date.now().toString(36)}`
|
|
162
|
+
copy.movedFrom = { scope: 'project', id, at: now }
|
|
163
|
+
copy.createdAt = ins.createdAt || copy.createdAt
|
|
164
|
+
if (!Array.isArray(task.insights)) task.insights = []
|
|
165
|
+
task.insights.push(copy)
|
|
166
|
+
store.markTasksDirty()
|
|
167
|
+
items.splice(idx, 1)
|
|
168
|
+
store.replaceInsightItems(items)
|
|
169
|
+
return { ok: true, text: `已降级到任务 ${task.title}` }
|
|
170
|
+
}
|
|
171
|
+
return { ok: false, text: `project 级不支持动作: ${act}` }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (s === 'global') {
|
|
175
|
+
if (!gs) return { ok: false, text: 'global 存储不可用' }
|
|
176
|
+
const items = gs.items()
|
|
177
|
+
const idx = items.findIndex((i) => i.id === id)
|
|
178
|
+
if (idx === -1) return invalid()
|
|
179
|
+
const ins = items[idx]
|
|
180
|
+
if (act === 'archive' || act === 'restore') {
|
|
181
|
+
ins.archived = act === 'archive'
|
|
182
|
+
ins.updatedAt = new Date().toISOString()
|
|
183
|
+
gs.markDirty()
|
|
184
|
+
return { ok: true, text: `已${act === 'archive' ? '归档' : '恢复'}` }
|
|
185
|
+
}
|
|
186
|
+
if (act === 'delete') {
|
|
187
|
+
items.splice(idx, 1)
|
|
188
|
+
gs.markDirty()
|
|
189
|
+
return { ok: true, text: `已删除 global 条目 ${id}` }
|
|
190
|
+
}
|
|
191
|
+
if (act === 'demote') {
|
|
192
|
+
// global → project
|
|
193
|
+
const now = new Date().toISOString()
|
|
194
|
+
const pjItems = store.insightItems()
|
|
195
|
+
const copy = normalizeInsight(ins, { scope: 'project', source: ins.source, nowIso: now })
|
|
196
|
+
copy.id = `${id}_d${Date.now().toString(36)}`
|
|
197
|
+
copy.movedFrom = { scope: 'global', id, at: now }
|
|
198
|
+
copy.createdAt = ins.createdAt || copy.createdAt
|
|
199
|
+
pjItems.push(copy)
|
|
200
|
+
store.replaceInsightItems(pjItems)
|
|
201
|
+
items.splice(idx, 1)
|
|
202
|
+
gs.markDirty()
|
|
203
|
+
return { ok: true, text: `已降级到 project:${copy.title}` }
|
|
204
|
+
}
|
|
205
|
+
return { ok: false, text: `global 级不支持动作: ${act}` }
|
|
206
|
+
}
|
|
207
|
+
return { ok: false, text: `未知 scope: ${s}` }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** 新建/保存记忆(project/global)。内部走与 save_lesson 同一 saveInsight 语义(去重合并)。 */
|
|
211
|
+
export function saveMemoryItem({ store, gs, cfg, scope, fields }) {
|
|
212
|
+
const s = scope === 'global' ? 'global' : 'project'
|
|
213
|
+
if (s === 'global' && !gs) return { ok: false, text: 'global 存储不可用' }
|
|
214
|
+
const res = saveInsight({ store, globalStore: gs, raw: fields, scope: s, cfg, source: 'user' })
|
|
215
|
+
if (!res.ok) return res
|
|
216
|
+
return { ok: true, text: `${s} 记忆已保存(${res.action})` }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const EDIT_FIELDS = new Set(['title', 'kind', 'body', 'pattern', 'fix', 'problem', 'solution', 'choice', 'reason', 'confidence', 'steps', 'trigger', 'tags', 'files', 'symbols'])
|
|
220
|
+
|
|
221
|
+
// kind 各自的“内容字段”;切 kind 时清掉不再属于它的旧字段(否则旧经验文本会残留显示)
|
|
222
|
+
const KIND_CONTENT = {
|
|
223
|
+
lesson: ['pattern', 'fix'],
|
|
224
|
+
decision: ['choice', 'reason'],
|
|
225
|
+
procedure: ['steps', 'trigger'],
|
|
226
|
+
experience: ['problem', 'solution'],
|
|
227
|
+
}
|
|
228
|
+
const ALL_CONTENT = ['body', 'pattern', 'fix', 'problem', 'solution', 'choice', 'reason', 'steps', 'trigger']
|
|
229
|
+
|
|
230
|
+
/** 编辑既有条目(title/kind/正文/trigger 等白名单字段)。任务级按 id 在任务池定位。 */
|
|
231
|
+
export function editMemoryItem({ store, gs, scope, id, fields }) {
|
|
232
|
+
if (!fields || typeof fields !== 'object') return { ok: false, text: 'edit 需要字段对象' }
|
|
233
|
+
const applyTo = (item) => {
|
|
234
|
+
let changed = false
|
|
235
|
+
const oldKind = item.kind
|
|
236
|
+
for (const key of Object.keys(fields)) {
|
|
237
|
+
const v = fields[key]
|
|
238
|
+
if (!EDIT_FIELDS.has(key)) continue
|
|
239
|
+
if (key === 'kind') {
|
|
240
|
+
if (!INSIGHT_KINDS.includes(v) || v === item.kind) continue
|
|
241
|
+
item.kind = v
|
|
242
|
+
changed = true
|
|
243
|
+
continue
|
|
244
|
+
}
|
|
245
|
+
if (key === 'trigger') {
|
|
246
|
+
if (!v || typeof v !== 'object') continue
|
|
247
|
+
item.trigger = {
|
|
248
|
+
keywords: Array.isArray(v.keywords) ? v.keywords.map(String) : [],
|
|
249
|
+
...(Array.isArray(v.scope) ? { scope: v.scope.map(String) } : {}),
|
|
250
|
+
}
|
|
251
|
+
changed = true
|
|
252
|
+
continue
|
|
253
|
+
}
|
|
254
|
+
if (key === 'steps' || key === 'tags' || key === 'files' || key === 'symbols') {
|
|
255
|
+
if (!Array.isArray(v)) continue
|
|
256
|
+
item[key] = v.map(String)
|
|
257
|
+
changed = true
|
|
258
|
+
continue
|
|
259
|
+
}
|
|
260
|
+
if (key === 'confidence') {
|
|
261
|
+
const n = Number(v)
|
|
262
|
+
if (!Number.isFinite(n)) continue
|
|
263
|
+
item.confidence = Math.max(0, Math.min(1, n))
|
|
264
|
+
changed = true
|
|
265
|
+
continue
|
|
266
|
+
}
|
|
267
|
+
const s = v === null || v === undefined ? '' : String(v)
|
|
268
|
+
if (key === 'title' && !s.trim()) continue
|
|
269
|
+
item[key] = s
|
|
270
|
+
changed = true
|
|
271
|
+
}
|
|
272
|
+
// 切 kind → 清理不再属于新 kind 的旧内容字段
|
|
273
|
+
if (changed && item.kind !== oldKind && KIND_CONTENT[item.kind]) {
|
|
274
|
+
const keep = new Set(KIND_CONTENT[item.kind])
|
|
275
|
+
const isProcedure = item.kind === 'procedure'
|
|
276
|
+
for (const f of ALL_CONTENT) {
|
|
277
|
+
if (keep.has(f)) continue
|
|
278
|
+
if (isProcedure && f === 'body') continue // procedure 保留通用 body
|
|
279
|
+
delete item[f]
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (changed) item.updatedAt = new Date().toISOString()
|
|
283
|
+
return changed
|
|
284
|
+
}
|
|
285
|
+
const s = scope || 'project'
|
|
286
|
+
if (s === 'task') {
|
|
287
|
+
const found = findTaskInsight(store, id)
|
|
288
|
+
if (!found) return { ok: false, text: `insight ${id} 不在任务层` }
|
|
289
|
+
if (!applyTo(found.insight)) return { ok: false, text: '没有可更新的字段' }
|
|
290
|
+
store.markTasksDirty()
|
|
291
|
+
return { ok: true, text: '已更新任务级记忆' }
|
|
292
|
+
}
|
|
293
|
+
const items = s === 'global' ? (gs ? gs.items() : null) : store.insightItems()
|
|
294
|
+
if (!items) return { ok: false, text: '存储不可用' }
|
|
295
|
+
const item = items.find((i) => i.id === id)
|
|
296
|
+
if (!item) return { ok: false, text: `insight ${id} 不在 ${s} 层` }
|
|
297
|
+
if (!applyTo(item)) return { ok: false, text: '没有可更新的字段' }
|
|
298
|
+
if (s === 'global') gs.markDirty()
|
|
299
|
+
else store.replaceInsightItems(items)
|
|
300
|
+
return { ok: true, text: '已更新记忆条目' }
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function jsonText(payload, note) {
|
|
304
|
+
return `${note}\n\n\`\`\`json\n${JSON.stringify(payload)}\n\`\`\``
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** /insight <verb> <args…>:list [task|project|global] | confirm/promote/demote/archive/restore/delete <id> [taskId] */
|
|
308
|
+
export function insightCommandDefinition(config, ctx) {
|
|
309
|
+
return {
|
|
310
|
+
name: 'insight',
|
|
311
|
+
description: '记忆视图动作(面板按钮调用):/insight list project|global|task;/insight <confirm|promote|demote|archive|restore|delete> <scope> <id> [taskId]',
|
|
312
|
+
input: { hint: 'list [scope] | <动作> <scope> <id> [taskId]' },
|
|
313
|
+
handler: (invocation) => {
|
|
314
|
+
try {
|
|
315
|
+
const agent = invocation?.agent
|
|
316
|
+
const sid = agent?.id || agent?.session?.id
|
|
317
|
+
const session = sid && agent?.ctx ? agent.ctx.sessions?.get(sid) : (agent?.session || null)
|
|
318
|
+
const cwd = session?.header?.cwd || agent?.session?.header?.cwd
|
|
319
|
+
const lineRaw = (invocation?.rawInput || '').trim()
|
|
320
|
+
const raw = lineRaw.split(/\s+/).filter(Boolean)
|
|
321
|
+
const root = projectRootFor(cwd)
|
|
322
|
+
if (!raw[0]) return { kind: 'error', text: '[insight] 用法见面板' }
|
|
323
|
+
const verb = raw[0]
|
|
324
|
+
if (verb === 'list') {
|
|
325
|
+
const scope = raw[1] === 'task' || raw[1] === 'global' || raw[1] === 'project' ? raw[1] : 'project'
|
|
326
|
+
const store = new ProjectMemoryStore(memoryRootFor(root, config.memoryDir)).load()
|
|
327
|
+
const gs = new GlobalStore(cfgInsight(config).globalFile || defaultGlobalFile()).load()
|
|
328
|
+
const payload = listInsights(store, gs, scope)
|
|
329
|
+
return { kind: 'success', text: jsonText(payload, `记忆 ${scope}:${payload.count} 条`) }
|
|
330
|
+
}
|
|
331
|
+
if (verb === 'save') {
|
|
332
|
+
// /insight save <scope> <json>(新建/合并;project|global)
|
|
333
|
+
const scope = raw[1]
|
|
334
|
+
if (scope !== 'project' && scope !== 'global') return { kind: 'error', text: '[insight] save 仅支持 project|global 作用域' }
|
|
335
|
+
const scopeIdx = lineRaw.indexOf(scope, verb.length)
|
|
336
|
+
const json = scopeIdx === -1 ? '' : lineRaw.slice(scopeIdx + scope.length).trim()
|
|
337
|
+
let fields
|
|
338
|
+
try {
|
|
339
|
+
fields = JSON.parse(json)
|
|
340
|
+
} catch {
|
|
341
|
+
return { kind: 'error', text: '[insight] save 需要 JSON 参数(由面板生成)' }
|
|
342
|
+
}
|
|
343
|
+
if (!fields || typeof fields !== 'object' || (!fields.title && !fields.pattern && !fields.topic)) {
|
|
344
|
+
return { kind: 'error', text: '[insight] save JSON 需要 title/pattern/topic' }
|
|
345
|
+
}
|
|
346
|
+
const store = new ProjectMemoryStore(memoryRootFor(root, config.memoryDir)).load()
|
|
347
|
+
const gs = new GlobalStore(cfgInsight(config).globalFile || defaultGlobalFile()).load()
|
|
348
|
+
const res = saveMemoryItem({ store, gs, cfg: cfgInsight(config), scope, fields })
|
|
349
|
+
store.commit(() => 0)
|
|
350
|
+
gs.commit()
|
|
351
|
+
return res.ok ? { kind: 'success', text: res.text } : { kind: 'error', text: res.text }
|
|
352
|
+
}
|
|
353
|
+
if (verb === 'edit') {
|
|
354
|
+
// /insight edit <scope> <id> <json>
|
|
355
|
+
const scope = raw[1]
|
|
356
|
+
const id = raw[2]
|
|
357
|
+
if (!id) return { kind: 'error', text: '[insight] edit 需要 id' }
|
|
358
|
+
const at = lineRaw.indexOf(id, 0)
|
|
359
|
+
const json = at === -1 ? '' : lineRaw.slice(at + id.length).trim()
|
|
360
|
+
let fields
|
|
361
|
+
try {
|
|
362
|
+
fields = JSON.parse(json)
|
|
363
|
+
} catch {
|
|
364
|
+
return { kind: 'error', text: '[insight] edit 需要 JSON 参数(由面板生成)' }
|
|
365
|
+
}
|
|
366
|
+
const store = new ProjectMemoryStore(memoryRootFor(root, config.memoryDir)).load()
|
|
367
|
+
const gs = new GlobalStore(cfgInsight(config).globalFile || defaultGlobalFile()).load()
|
|
368
|
+
const res = editMemoryItem({ store, gs, scope, id, fields })
|
|
369
|
+
store.commit(() => 0)
|
|
370
|
+
gs.commit()
|
|
371
|
+
return res.ok ? { kind: 'success', text: res.text } : { kind: 'error', text: res.text }
|
|
372
|
+
}
|
|
373
|
+
const scope = raw[1]
|
|
374
|
+
const id = raw[2]
|
|
375
|
+
const taskId = raw[3]
|
|
376
|
+
if (!['confirm', 'promote', 'demote', 'archive', 'restore', 'delete'].includes(verb) || !id) {
|
|
377
|
+
return { kind: 'error', text: '[insight] 用法: /insight <动作|save|edit> …' }
|
|
378
|
+
}
|
|
379
|
+
const store = new ProjectMemoryStore(memoryRootFor(root, config.memoryDir)).load()
|
|
380
|
+
const gs = new GlobalStore(cfgInsight(config).globalFile || defaultGlobalFile()).load()
|
|
381
|
+
const res = actInsight({ store, gs, action: verb, scope, id, taskId, cfg: cfgInsight(config) })
|
|
382
|
+
store.commit(() => 0) // 项目侧(tasks/insights)落盘
|
|
383
|
+
gs.commit() // global 侧若有变更落盘
|
|
384
|
+
return res.ok ? { kind: 'success', text: res.text } : { kind: 'error', text: res.text }
|
|
385
|
+
} catch (err) {
|
|
386
|
+
return { kind: 'error', text: `[insight] ${err?.message || err}` }
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
}
|
|
390
|
+
}
|
|
@@ -6,6 +6,7 @@ import { memoryRootFor } from '../util/fs.js'
|
|
|
6
6
|
import { ProjectMemoryStore } from '../store.js'
|
|
7
7
|
import { projectRootFor, adoptStepsToSession, shouldAdoptToHost } from '../setup/taskbridge.js'
|
|
8
8
|
import { renderTaskSnapshot, buildTaskPayload } from './tasks.js'
|
|
9
|
+
import { fireReflect } from '../reflection-pipeline.js'
|
|
9
10
|
|
|
10
11
|
const VERBS = { switch: 'switch', archive: 'archive' }
|
|
11
12
|
|
|
@@ -127,6 +128,7 @@ export function taskCommandDefinition(config, ctx) {
|
|
|
127
128
|
if (verb === VERBS.switch) {
|
|
128
129
|
const task = store.getTask(taskId)
|
|
129
130
|
if (!task) return { kind: 'error', text: `[task] 找不到任务: ${taskId}(/tasks 查看)` }
|
|
131
|
+
const prevBound = sid ? store.getBoundTaskId(sid) : null
|
|
130
132
|
if (task.archived) task.archived = false
|
|
131
133
|
if (sid) {
|
|
132
134
|
store.setBinding(sid, task.id)
|
|
@@ -134,6 +136,10 @@ export function taskCommandDefinition(config, ctx) {
|
|
|
134
136
|
task.updatedAt = new Date().toISOString()
|
|
135
137
|
task.lastActiveAt = task.updatedAt
|
|
136
138
|
store.save()
|
|
139
|
+
// 反思钩子:切走旧任务异步收割(默认关)
|
|
140
|
+
if (prevBound && prevBound !== task.id) {
|
|
141
|
+
fireReflect(config, { llm: ctx?.llm }, root, prevBound, 'switch-away')
|
|
142
|
+
}
|
|
137
143
|
// 反向接管:切换成功后把任务步骤推成宿主 todo/write(dsh 清单跟随)
|
|
138
144
|
if (shouldAdoptToHost(config)) {
|
|
139
145
|
adoptStepsToSession(session, task)
|
|
@@ -149,6 +155,8 @@ export function taskCommandDefinition(config, ctx) {
|
|
|
149
155
|
task.archived = true
|
|
150
156
|
task.updatedAt = new Date().toISOString()
|
|
151
157
|
store.save()
|
|
158
|
+
// 反思钩子:归档即收割(默认关)
|
|
159
|
+
fireReflect(config, { llm: ctx?.llm }, root, taskId, 'archive')
|
|
152
160
|
const note = `已归档: ${describeTask(task)}(select_task 可恢复)`
|
|
153
161
|
return withTaskSnapshot(config, cwd, sid, store, note)
|
|
154
162
|
}
|
package/src/commands/tasks.js
CHANGED
|
@@ -25,6 +25,7 @@ export function buildTaskPayload(tasks, boundId, archived) {
|
|
|
25
25
|
lastActiveAt: t.lastActiveAt,
|
|
26
26
|
updatedAt: t.updatedAt,
|
|
27
27
|
archived: t.archived || false,
|
|
28
|
+
insights: (t.insights || []).filter((i) => !i.archived).map((i) => ({ id: i.id, kind: i.kind || 'lesson', title: i.title || '', draft: i.draft === true })),
|
|
28
29
|
})),
|
|
29
30
|
boundId: boundId || null,
|
|
30
31
|
archived,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// global.json 幂等初始化与种子(v0.5)
|
|
2
|
+
// - ensureGlobalInit:确保文档结构与文件存在(幂等,仅补缺失 id)
|
|
3
|
+
// - 不做任何内置"个人经验"种子(WSL 发包等属用户数据,由用户/测试提供 seeds)
|
|
4
|
+
import { GlobalStore, defaultGlobalFile } from './insight-store.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 确保 global 存储文件/结构就绪。
|
|
8
|
+
* @param {object} [config] plugin config(insight.globalFile 可覆盖默认路径)
|
|
9
|
+
* @param {Array<object>} [seeds] 种子条目(需含稳定 id);只补缺失 id,绝不覆盖
|
|
10
|
+
* @returns {{ store: GlobalStore, file: string, created: boolean, seeded: number }}
|
|
11
|
+
*/
|
|
12
|
+
export function ensureGlobalInit(config = {}, seeds = []) {
|
|
13
|
+
const c = (config.insight || {})
|
|
14
|
+
const file = typeof c.globalFile === 'string' && c.globalFile ? c.globalFile : defaultGlobalFile()
|
|
15
|
+
const store = new GlobalStore(file).load()
|
|
16
|
+
let created = false
|
|
17
|
+
if (!store.doc.createdAt) {
|
|
18
|
+
store.doc.createdAt = new Date().toISOString()
|
|
19
|
+
created = true
|
|
20
|
+
}
|
|
21
|
+
let seeded = 0
|
|
22
|
+
for (const seed of seeds || []) {
|
|
23
|
+
if (!seed || !seed.id) continue
|
|
24
|
+
if (store.items().some((it) => it.id === seed.id)) continue
|
|
25
|
+
const now = new Date().toISOString()
|
|
26
|
+
store.items().push({
|
|
27
|
+
kind: 'procedure',
|
|
28
|
+
...seed,
|
|
29
|
+
scope: seed.scope || 'global',
|
|
30
|
+
source: seed.source || 'seed',
|
|
31
|
+
createdAt: seed.createdAt || now,
|
|
32
|
+
updatedAt: seed.updatedAt || now,
|
|
33
|
+
})
|
|
34
|
+
seeded++
|
|
35
|
+
}
|
|
36
|
+
if (created || seeded) store.markDirty()
|
|
37
|
+
store.commit(() => undefined)
|
|
38
|
+
return { store, file, created, seeded }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { defaultGlobalFile }
|
package/src/index.js
CHANGED
|
@@ -11,8 +11,11 @@ import { setupLazyIndexing } from './lazy.js'
|
|
|
11
11
|
import { initTypeScript } from './enhancer.js'
|
|
12
12
|
import { setupTaskbridge } from './setup/taskbridge.js'
|
|
13
13
|
import { listTasksTool, selectTaskTool, archiveTaskTool, showTaskPanelTool } from './tools/task-tools.js'
|
|
14
|
+
import { lessonTool } from './tools/lesson-tools.js'
|
|
15
|
+
import { installAutoInject } from './auto-inject.js'
|
|
14
16
|
import { tasksCommandDefinition } from './commands/tasks.js'
|
|
15
17
|
import { taskCommandDefinition } from './commands/task-actions.js'
|
|
18
|
+
import { insightCommandDefinition } from './commands/insight-actions.js'
|
|
16
19
|
|
|
17
20
|
export const name = 'dsh-project-memory'
|
|
18
21
|
export const inject = ['llm', 'tools']
|
|
@@ -37,6 +40,31 @@ export const Config = Schema.object({
|
|
|
37
40
|
// 任务成为会话绑定(select_task / /task switch)时,把任务步骤推成宿主 todo/write 快照
|
|
38
41
|
syncHostOnAdopt: Schema.boolean().default(true),
|
|
39
42
|
}).default({}),
|
|
43
|
+
// v0.5 单一 insight 实体:分层去重/强化/提升/容量/归档(详见 PLAN-v0.5.0.md)
|
|
44
|
+
insight: Schema.object({
|
|
45
|
+
dedupOverlap: Schema.number().default(0.7),
|
|
46
|
+
reinforceBand: Schema.number().default(0.65),
|
|
47
|
+
maxProject: Schema.number().default(100),
|
|
48
|
+
maxGlobalProcedures: Schema.number().default(200),
|
|
49
|
+
promoteConfidence: Schema.number().default(0.7),
|
|
50
|
+
globalPromoteTasks: Schema.number().default(3),
|
|
51
|
+
decayDays: Schema.number().default(90),
|
|
52
|
+
globalFile: Schema.string(),
|
|
53
|
+
}).default({}),
|
|
54
|
+
// PR 1b:反思管线(LLM 消费点,默认关)。产出只写 task 级草稿,见 PLAN §3
|
|
55
|
+
reflection: Schema.object({
|
|
56
|
+
enabled: Schema.boolean().default(false),
|
|
57
|
+
cooldownMs: Schema.number().default(1800000),
|
|
58
|
+
maxLessonsPerReflect: Schema.number().default(3),
|
|
59
|
+
maxDecisionsPerReflect: Schema.number().default(2),
|
|
60
|
+
}).default({}),
|
|
61
|
+
// PR 2:静默注入(entry 常驻块 + relevance 门控)。无项目 root 可解析时零副作用
|
|
62
|
+
autoContext: Schema.object({
|
|
63
|
+
enabled: Schema.boolean().default(true),
|
|
64
|
+
entryOn: Schema.boolean().default(true),
|
|
65
|
+
maxTokens: Schema.number().default(400),
|
|
66
|
+
relevanceMin: Schema.number().default(0.25),
|
|
67
|
+
}).default({}),
|
|
40
68
|
})
|
|
41
69
|
|
|
42
70
|
export function apply(ctx, config) {
|
|
@@ -61,18 +89,19 @@ export function apply(ctx, config) {
|
|
|
61
89
|
// TaskBridge:任务实体 + 宿主 todo 同步
|
|
62
90
|
setupTaskbridge(ctx, config)
|
|
63
91
|
ctx.tools.register(listTasksTool(config))
|
|
64
|
-
ctx.tools.register(selectTaskTool(config))
|
|
65
|
-
ctx.tools.register(archiveTaskTool(config))
|
|
92
|
+
ctx.tools.register(selectTaskTool(config, { llm: ctx.llm, ctx }))
|
|
93
|
+
ctx.tools.register(archiveTaskTool(config, { llm: ctx.llm, ctx }))
|
|
66
94
|
ctx.tools.register(showTaskPanelTool(config))
|
|
67
95
|
|
|
68
|
-
// /tasks、/task 用户命令(宿主 commands 服务存在时注册,feature-detect 降级)
|
|
96
|
+
// /tasks、/task、/insight 用户命令(宿主 commands 服务存在时注册,feature-detect 降级)
|
|
69
97
|
try {
|
|
70
98
|
ctx.inject(['commands'], (commandsCtx) => {
|
|
71
99
|
commandsCtx.commands.register(tasksCommandDefinition(config, ctx))
|
|
72
100
|
commandsCtx.commands.register(taskCommandDefinition(config, ctx))
|
|
101
|
+
commandsCtx.commands.register(insightCommandDefinition(config, ctx))
|
|
73
102
|
})
|
|
74
103
|
} catch (err) {
|
|
75
|
-
console.error(`[dsh-project-memory] /tasks,/task registration skipped: ${err.message}`)
|
|
104
|
+
console.error(`[dsh-project-memory] /tasks,/task,/insight registration skipped: ${err.message}`)
|
|
76
105
|
}
|
|
77
106
|
|
|
78
107
|
ctx.tools.register(indexDocTool(ctx, config))
|
|
@@ -80,9 +109,14 @@ export function apply(ctx, config) {
|
|
|
80
109
|
ctx.tools.register(queryMemoryTool(ctx, config))
|
|
81
110
|
ctx.tools.register(rememberTool(config))
|
|
82
111
|
ctx.tools.register(forgetTool(config))
|
|
112
|
+
ctx.tools.register(lessonTool(config))
|
|
83
113
|
ctx.tools.register(watchRepoTool(watchManager, config))
|
|
84
114
|
ctx.tools.register(statsTool(config))
|
|
85
115
|
|
|
116
|
+
// PR 2:注册 agent/pre-step 监听,向每步请求的 enter 决策追加记忆消息(默认开;
|
|
117
|
+
// 任何异常/无会话 cwd → 交回默认决策,零副作用)
|
|
118
|
+
installAutoInject(ctx, config)
|
|
119
|
+
|
|
86
120
|
if (config.autoIndexOnFirstUse) {
|
|
87
121
|
ctx.effect(async () => {
|
|
88
122
|
const root = process.cwd()
|