@yolk_vat-y/dsh-project-memory 0.4.4 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/README.md +11 -2
- package/README.zh-CN.md +11 -2
- package/client/client.js +892 -107
- package/client/client.js.map +1 -1
- package/package.json +2 -2
- package/src/auto-inject.js +220 -0
- package/src/client/MemoryView.tsx +349 -0
- package/src/client/TaskCommandNode.tsx +30 -11
- package/src/client/TaskComponents.tsx +171 -21
- package/src/client/TaskPanel.module.css +151 -1
- 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
package/src/client/locales.ts
CHANGED
|
@@ -40,6 +40,40 @@ export const zh = {
|
|
|
40
40
|
'minibar.click-expand': '点击展开并同步',
|
|
41
41
|
'minibar.tasks': '个任务',
|
|
42
42
|
'minibar.no-task': '暂无任务 · 点击查看',
|
|
43
|
+
'view.cycle': '切换视图:任务 / 项目记忆 / 全局记忆',
|
|
44
|
+
'view.task': '任务',
|
|
45
|
+
'view.project': '项目记忆',
|
|
46
|
+
'view.global': '全局记忆',
|
|
47
|
+
'mem.scope.task': '本任务草稿',
|
|
48
|
+
'mem.scope.project': '项目记忆',
|
|
49
|
+
'mem.scope.global': '全局记忆',
|
|
50
|
+
'mem.empty': '暂无记忆条目(反思或 save_lesson 后出现)',
|
|
51
|
+
'mem.confirm': '审核',
|
|
52
|
+
'mem.promote': '提升',
|
|
53
|
+
'mem.demote': '降级',
|
|
54
|
+
'mem.archive': '归档',
|
|
55
|
+
'mem.restore': '恢复',
|
|
56
|
+
'mem.delete': '删除',
|
|
57
|
+
'mem.draft': '草稿',
|
|
58
|
+
'mem.archived': '已归档',
|
|
59
|
+
'mem.loading': '加载中…',
|
|
60
|
+
'mem.error': '同步失败',
|
|
61
|
+
'mem.kind.lesson': '坑',
|
|
62
|
+
'mem.kind.decision': '权衡',
|
|
63
|
+
'mem.kind.procedure': '指南',
|
|
64
|
+
'mem.kind.experience': '经验',
|
|
65
|
+
'mem.needs-bound': '需绑定任务后可降级',
|
|
66
|
+
'mem.new': '新建',
|
|
67
|
+
'mem.save': '保存',
|
|
68
|
+
'mem.cancel': '取消',
|
|
69
|
+
'mem.f.title': '标题(教训模式/主题/指南名)',
|
|
70
|
+
'mem.f.kind': '类型',
|
|
71
|
+
'mem.f.body': '做法 / 理由 / 解法',
|
|
72
|
+
'mem.f.steps': '步骤(每行一步)',
|
|
73
|
+
'mem.f.trigger': '作为 Skill:触发关键词(逗号分隔)',
|
|
74
|
+
'mem.saved': '已保存',
|
|
75
|
+
'mem.edit': '编辑',
|
|
76
|
+
'mem.section-label': '任务记忆',
|
|
43
77
|
}
|
|
44
78
|
|
|
45
79
|
export const en = {
|
|
@@ -81,6 +115,40 @@ export const en = {
|
|
|
81
115
|
'minibar.click-expand': 'Click to expand & sync',
|
|
82
116
|
'minibar.tasks': 'tasks',
|
|
83
117
|
'minibar.no-task': 'No tasks · click to view',
|
|
118
|
+
'view.cycle': 'Switch view: tasks / project memory / global memory',
|
|
119
|
+
'view.task': 'Tasks',
|
|
120
|
+
'view.project': 'Project Memory',
|
|
121
|
+
'view.global': 'Global Memory',
|
|
122
|
+
'mem.scope.task': 'Task drafts',
|
|
123
|
+
'mem.scope.project': 'Project memory',
|
|
124
|
+
'mem.scope.global': 'Global memory',
|
|
125
|
+
'mem.empty': 'No memory entries yet (created by reflection or save_lesson)',
|
|
126
|
+
'mem.confirm': 'Approve',
|
|
127
|
+
'mem.promote': 'Promote',
|
|
128
|
+
'mem.demote': 'Demote',
|
|
129
|
+
'mem.archive': 'Archive',
|
|
130
|
+
'mem.restore': 'Restore',
|
|
131
|
+
'mem.delete': 'Delete',
|
|
132
|
+
'mem.draft': 'draft',
|
|
133
|
+
'mem.archived': 'archived',
|
|
134
|
+
'mem.loading': 'Loading…',
|
|
135
|
+
'mem.error': 'Sync failed',
|
|
136
|
+
'mem.kind.lesson': 'lesson',
|
|
137
|
+
'mem.kind.decision': 'decision',
|
|
138
|
+
'mem.kind.procedure': 'procedure',
|
|
139
|
+
'mem.kind.experience': 'experience',
|
|
140
|
+
'mem.needs-bound': 'Bind a task to demote',
|
|
141
|
+
'mem.new': 'New',
|
|
142
|
+
'mem.save': 'Save',
|
|
143
|
+
'mem.cancel': 'Cancel',
|
|
144
|
+
'mem.f.title': 'Title (pattern / topic / procedure name)',
|
|
145
|
+
'mem.f.kind': 'Kind',
|
|
146
|
+
'mem.f.body': 'Fix / reason / solution',
|
|
147
|
+
'mem.f.steps': 'Steps (one per line)',
|
|
148
|
+
'mem.f.trigger': 'As Skill: trigger keywords (comma separated)',
|
|
149
|
+
'mem.saved': 'Saved',
|
|
150
|
+
'mem.edit': 'Edit',
|
|
151
|
+
'mem.section-label': 'Task memory',
|
|
84
152
|
}
|
|
85
153
|
|
|
86
154
|
export type LocaleDict = typeof zh
|
|
@@ -15,6 +15,13 @@ export interface TaskFile {
|
|
|
15
15
|
line?: number
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export interface TaskInsight {
|
|
19
|
+
id: string
|
|
20
|
+
kind: string
|
|
21
|
+
title: string
|
|
22
|
+
draft?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
export interface Task {
|
|
19
26
|
id: string
|
|
20
27
|
title: string
|
|
@@ -23,6 +30,7 @@ export interface Task {
|
|
|
23
30
|
lastActiveAt: string
|
|
24
31
|
updatedAt: string
|
|
25
32
|
archived?: boolean
|
|
33
|
+
insights?: TaskInsight[]
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
export interface TaskPayload {
|
|
@@ -68,8 +76,16 @@ let dataState: TaskDataState = {
|
|
|
68
76
|
|
|
69
77
|
const dataListeners = new Set<() => void>()
|
|
70
78
|
|
|
71
|
-
function setDataState(next: TaskDataState): void {
|
|
72
|
-
|
|
79
|
+
function setDataState(next: TaskDataState | ((prev: TaskDataState) => TaskDataState)): void {
|
|
80
|
+
// 兼容对象与函数式 updater(BroadcastChannel 曾误把 updater 当对象写入,
|
|
81
|
+
// 导致 dataState 变成函数、tasks 丢失 → 面板崩溃)。写入一律归一化为完整 shape。
|
|
82
|
+
const patch = typeof next === 'function' ? next(dataState) : next
|
|
83
|
+
dataState = {
|
|
84
|
+
tasks: Array.isArray(patch?.tasks) ? patch.tasks : [],
|
|
85
|
+
boundTaskId: patch?.boundTaskId ?? null,
|
|
86
|
+
archivedCount: typeof patch?.archivedCount === 'number' ? patch.archivedCount : 0,
|
|
87
|
+
lastUpdate: typeof patch?.lastUpdate === 'number' ? patch.lastUpdate : 0,
|
|
88
|
+
}
|
|
73
89
|
for (const listener of dataListeners) listener()
|
|
74
90
|
}
|
|
75
91
|
|
|
@@ -77,7 +93,7 @@ function broadcastDataUpdate(tasks: Task[], archivedCount: number): void {
|
|
|
77
93
|
if (!SYNC_CHANNEL) return
|
|
78
94
|
SYNC_CHANNEL.postMessage({
|
|
79
95
|
type: 'PROJECT_TASKS_UPDATED',
|
|
80
|
-
payload: { tasks, archivedCount },
|
|
96
|
+
payload: { tasks: Array.isArray(tasks) ? tasks : [], archivedCount },
|
|
81
97
|
})
|
|
82
98
|
}
|
|
83
99
|
|
|
@@ -88,8 +104,8 @@ if (SYNC_CHANNEL) {
|
|
|
88
104
|
const { tasks, archivedCount } = msg.payload
|
|
89
105
|
setDataState(prev => ({
|
|
90
106
|
...prev,
|
|
91
|
-
tasks: tasks
|
|
92
|
-
archivedCount: archivedCount
|
|
107
|
+
tasks: Array.isArray(tasks) ? tasks : [],
|
|
108
|
+
archivedCount: typeof archivedCount === 'number' ? archivedCount : prev.archivedCount,
|
|
93
109
|
lastUpdate: Date.now(),
|
|
94
110
|
}))
|
|
95
111
|
}
|
|
@@ -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 }
|