@yolk_vat-y/dsh-project-memory 0.5.5 → 0.5.7
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 +71 -2
- package/README.md +27 -4
- package/README.zh-CN.md +26 -3
- package/package.json +4 -2
- package/src/audit.js +87 -0
- package/src/auto-inject.js +383 -197
- package/src/chunker.js +14 -6
- package/src/commands/insight-actions.js +18 -14
- package/src/commands/invocation.js +36 -0
- package/src/commands/task-actions.js +24 -39
- package/src/commands/tasks.js +18 -13
- package/src/enhancer.js +11 -2
- package/src/index-pipeline.js +119 -0
- package/src/insight-store.js +78 -29
- package/src/lazy.js +15 -46
- package/src/link.js +10 -1
- package/src/ops.js +210 -0
- package/src/parsers/pdfjs-parser.js +2 -4
- package/src/project-profile.js +12 -5
- package/src/readiness.js +305 -37
- package/src/recall.js +28 -8
- package/src/setup/taskbridge.js +11 -9
- package/src/store.js +56 -17
- package/src/symbols.js +58 -19
- package/src/tools/forget.js +2 -1
- package/src/tools/index-doc.js +4 -1
- package/src/tools/index-repo.js +33 -86
- package/src/tools/lesson-tools.js +31 -8
- package/src/tools/query-memory.js +17 -3
- package/src/tools/remember.js +3 -1
- package/src/tools/task-tools.js +4 -1
- package/src/tools/watch-repo.js +9 -5
- package/src/util/fs.js +14 -0
- package/src/util/session-cache.js +72 -0
- package/src/watch.js +39 -81
package/src/chunker.js
CHANGED
|
@@ -20,17 +20,25 @@ export function chunkText(text, chunkChars = 3000, maxChunks = 40) {
|
|
|
20
20
|
|
|
21
21
|
const chunks = []
|
|
22
22
|
for (const section of sections) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
const rawBlock = section.lines.join('\n')
|
|
24
|
+
if (!rawBlock.trim()) continue
|
|
25
|
+
// `.trim()` 会吃掉段首的空白与换行——把它们算进行号,否则整段的引用行号会偏低。
|
|
26
|
+
const lead = rawBlock.match(/^\s*/)[0]
|
|
27
|
+
let line = section.line + (lead.match(/\n/g) || []).length
|
|
28
|
+
let block = rawBlock.trim()
|
|
26
29
|
while (block.length > chunkChars) {
|
|
27
30
|
let splitAt = block.lastIndexOf('\n\n', chunkChars)
|
|
28
31
|
if (splitAt < chunkChars * 0.5) splitAt = block.lastIndexOf(' ', chunkChars)
|
|
29
32
|
if (splitAt < chunkChars * 0.5) splitAt = chunkChars
|
|
30
|
-
const
|
|
33
|
+
const rawPart = block.slice(0, splitAt)
|
|
34
|
+
const part = rawPart.trim()
|
|
31
35
|
if (part) chunks.push({ title: section.title, text: part, line })
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
// 行号按**原始**切片里的换行数推进,再加上被下一次 trim 吃掉的段首换行。
|
|
37
|
+
// 旧写法 `part.split('\n').length` 每次跨行切分都要多算一行,误差会累积。
|
|
38
|
+
const rest = block.slice(splitAt)
|
|
39
|
+
const nextLead = rest.match(/^\s*/)[0]
|
|
40
|
+
line += (rawPart.match(/\n/g) || []).length + (nextLead.match(/\n/g) || []).length
|
|
41
|
+
block = rest.trim()
|
|
34
42
|
if (chunks.length >= maxChunks) break
|
|
35
43
|
}
|
|
36
44
|
if (block) {
|
|
@@ -6,6 +6,7 @@ import { ProjectMemoryStore } from '../store.js'
|
|
|
6
6
|
import { memoryRootFor } from '../util/fs.js'
|
|
7
7
|
import { cfgInsight, GlobalStore, normalizeInsight, defaultGlobalFile, saveInsight, INSIGHT_KINDS } from '../insight-store.js'
|
|
8
8
|
import { projectRootFor } from '../setup/taskbridge.js'
|
|
9
|
+
import { fencedJson, invocationContext } from './invocation.js'
|
|
9
10
|
|
|
10
11
|
function insSummary(ins, extra = {}) {
|
|
11
12
|
const s = ins || {}
|
|
@@ -225,7 +226,9 @@ const KIND_CONTENT = {
|
|
|
225
226
|
procedure: ['steps', 'trigger'],
|
|
226
227
|
experience: ['problem', 'solution'],
|
|
227
228
|
}
|
|
228
|
-
|
|
229
|
+
// 切 kind 时要清掉的内容字段。**不含 trigger**:任何 kind 都可以带 authored trigger,
|
|
230
|
+
// 放进这里会在 lesson↔decision 之间切换时把自动注入能力一并删掉。
|
|
231
|
+
const ALL_CONTENT = ['body', 'pattern', 'fix', 'problem', 'solution', 'choice', 'reason', 'steps']
|
|
229
232
|
|
|
230
233
|
/** 编辑既有条目(title/kind/正文/trigger 等白名单字段)。任务级按 id 在任务池定位。 */
|
|
231
234
|
export function editMemoryItem({ store, gs, scope, id, fields }) {
|
|
@@ -244,10 +247,18 @@ export function editMemoryItem({ store, gs, scope, id, fields }) {
|
|
|
244
247
|
}
|
|
245
248
|
if (key === 'trigger') {
|
|
246
249
|
if (!v || typeof v !== 'object') continue
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
250
|
+
// 合并而非重建:只改传进来的成员,when/guard/prevents 等 authored 触发面必须原样保留,
|
|
251
|
+
// 否则一次 /insight edit 就会把自动注入的 trigger 降级成 legacy keywords。
|
|
252
|
+
const next = { ...(item.trigger || {}) }
|
|
253
|
+
if (Array.isArray(v.keywords)) next.keywords = v.keywords.map(String)
|
|
254
|
+
if (Array.isArray(v.scope)) next.scope = v.scope.map(String)
|
|
255
|
+
if (Array.isArray(v.actions)) next.actions = v.actions.map(String)
|
|
256
|
+
if (Array.isArray(v.paths)) next.paths = v.paths.map(String)
|
|
257
|
+
if (Array.isArray(v.symbols)) next.symbols = v.symbols.map(String)
|
|
258
|
+
if (v.when && typeof v.when === 'object') next.when = v.when
|
|
259
|
+
if (v.guard && typeof v.guard === 'object') next.guard = v.guard
|
|
260
|
+
if (typeof v.prevents === 'string') next.prevents = v.prevents
|
|
261
|
+
item.trigger = next
|
|
251
262
|
changed = true
|
|
252
263
|
continue
|
|
253
264
|
}
|
|
@@ -300,10 +311,6 @@ export function editMemoryItem({ store, gs, scope, id, fields }) {
|
|
|
300
311
|
return { ok: true, text: '已更新记忆条目' }
|
|
301
312
|
}
|
|
302
313
|
|
|
303
|
-
function jsonText(payload, note) {
|
|
304
|
-
return `${note}\n\n\`\`\`json\n${JSON.stringify(payload)}\n\`\`\``
|
|
305
|
-
}
|
|
306
|
-
|
|
307
314
|
/** /insight <verb> <args…>:list [task|project|global] | confirm/promote/demote/archive/restore/delete <id> [taskId] */
|
|
308
315
|
export function insightCommandDefinition(config, ctx) {
|
|
309
316
|
return {
|
|
@@ -312,10 +319,7 @@ export function insightCommandDefinition(config, ctx) {
|
|
|
312
319
|
input: { hint: 'list [scope] | <动作> <scope> <id> [taskId]' },
|
|
313
320
|
handler: (invocation) => {
|
|
314
321
|
try {
|
|
315
|
-
const
|
|
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
|
|
322
|
+
const { cwd } = invocationContext(invocation)
|
|
319
323
|
const lineRaw = (invocation?.rawInput || '').trim()
|
|
320
324
|
const raw = lineRaw.split(/\s+/).filter(Boolean)
|
|
321
325
|
const root = projectRootFor(cwd)
|
|
@@ -326,7 +330,7 @@ export function insightCommandDefinition(config, ctx) {
|
|
|
326
330
|
const store = new ProjectMemoryStore(memoryRootFor(root, config.memoryDir)).load()
|
|
327
331
|
const gs = new GlobalStore(cfgInsight(config).globalFile || defaultGlobalFile()).load()
|
|
328
332
|
const payload = listInsights(store, gs, scope)
|
|
329
|
-
return { kind: 'success', text:
|
|
333
|
+
return { kind: 'success', text: fencedJson(`记忆 ${scope}:${payload.count} 条`, JSON.stringify(payload)) }
|
|
330
334
|
}
|
|
331
335
|
if (verb === 'save') {
|
|
332
336
|
// /insight save <scope> <json>(新建/合并;project|global)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 命令处理器的公共接线:/tasks、/task、/insight 三个定义此前各自复制了
|
|
3
|
+
* 「解析 CommandInvocation → 会话与 cwd」和「拼 JSON 围栏载荷」两段代码。
|
|
4
|
+
* 集中在这里,语义只在一处维护。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 从命令调用里解出会话上下文。
|
|
9
|
+
*
|
|
10
|
+
* `sid` 优先取 `agent.id`;`session` 优先走 `sessions` 服务(拿到的是宿主实时会话,
|
|
11
|
+
* 带 append 等能力),服务缺失或不可用时退回 `agent.session`。
|
|
12
|
+
* @param {object} invocation - 宿主 CommandInvocation(handler 的入参)。
|
|
13
|
+
* @returns {{agent: object|undefined, sid: string|undefined, session: object|null, cwd: string|undefined}}
|
|
14
|
+
*/
|
|
15
|
+
export function invocationContext(invocation) {
|
|
16
|
+
const agent = invocation?.agent
|
|
17
|
+
const sid = agent?.id || agent?.session?.id
|
|
18
|
+
const fromService = sid && agent?.ctx ? agent.ctx.sessions?.get(sid) : null
|
|
19
|
+
// sessions 服务缺失**或查不到**时都退回 agent.session(文档承诺的降级路径)。
|
|
20
|
+
// 旧写法只在 agent.ctx 不存在时退回:服务在、但 get 返回 undefined 时 session 直接是 undefined,
|
|
21
|
+
// /task switch|todos|unbind 于是静默跳过 session.append(宿主 todo 同步失效)。
|
|
22
|
+
const session = fromService || agent?.session || null
|
|
23
|
+
const cwd = session?.header?.cwd || agent?.session?.header?.cwd
|
|
24
|
+
return { agent, sid, session, cwd }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 统一的命令输出载荷:人类可读摘要 + ```json 块。
|
|
29
|
+
* 客户端命令节点按尾部 JSON 解析并渲染面板,因此三个命令共用同一格式。
|
|
30
|
+
* @param {string} note - 人类可读摘要(可多行)。
|
|
31
|
+
* @param {string} payload - 已序列化的 JSON 载荷。
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
export function fencedJson(note, payload) {
|
|
35
|
+
return `${note}\n\n\`\`\`json\n${payload}\n\`\`\``
|
|
36
|
+
}
|
|
@@ -8,8 +8,7 @@ import { projectRootFor, adoptStepsToSession, shouldAdoptToHost } from '../setup
|
|
|
8
8
|
import { renderTaskSnapshot, buildTaskPayload } from './tasks.js'
|
|
9
9
|
import { fireReflect } from '../reflection-pipeline.js'
|
|
10
10
|
import { resolveRoute } from '../llm-route.js'
|
|
11
|
-
|
|
12
|
-
const VERBS = { switch: 'switch', archive: 'archive' }
|
|
11
|
+
import { fencedJson, invocationContext } from './invocation.js'
|
|
13
12
|
|
|
14
13
|
function describeTask(t) {
|
|
15
14
|
const done = (t.steps || []).filter((s) => s.status === 'completed').length
|
|
@@ -17,6 +16,23 @@ function describeTask(t) {
|
|
|
17
16
|
return `「${t.title}」(步骤 ${done}/${total})`
|
|
18
17
|
}
|
|
19
18
|
|
|
19
|
+
/**
|
|
20
|
+
* raw 去掉前 n 个空白分隔 token 后的**原始剩余文本**(保留内部空白与引号)。
|
|
21
|
+
* 旧写法 `raw.split(/\s+/).slice(n).join(' ')` 会把标题/JSON 里的连续空格压成一个,
|
|
22
|
+
* `/task rename <id> "a b"` 于是改名成 "a b"。
|
|
23
|
+
*/
|
|
24
|
+
function restAfter(raw, n) {
|
|
25
|
+
let i = 0
|
|
26
|
+
let seen = 0
|
|
27
|
+
while (i < raw.length && seen < n) {
|
|
28
|
+
while (i < raw.length && /\s/.test(raw[i])) i++
|
|
29
|
+
if (i >= raw.length) break
|
|
30
|
+
while (i < raw.length && !/\s/.test(raw[i])) i++
|
|
31
|
+
seen++
|
|
32
|
+
}
|
|
33
|
+
return raw.slice(i).replace(/^\s+/, '')
|
|
34
|
+
}
|
|
35
|
+
|
|
20
36
|
function withTaskSnapshot(config, cwd, sid, store, note) {
|
|
21
37
|
const tasks = store.getTasks()
|
|
22
38
|
const active = tasks.filter((t) => !t.archived)
|
|
@@ -24,7 +40,7 @@ function withTaskSnapshot(config, cwd, sid, store, note) {
|
|
|
24
40
|
const boundId = sid ? store.getBoundTaskId(sid) : null
|
|
25
41
|
const human = `项目 ${projectRootFor(cwd)}\n${note}\n任务: ${active.length} 套${archived ? `(归档 ${archived})` : ''}`
|
|
26
42
|
const payload = buildTaskPayload(active, boundId, archived)
|
|
27
|
-
return { kind: 'success', text:
|
|
43
|
+
return { kind: 'success', text: fencedJson(human, payload) }
|
|
28
44
|
}
|
|
29
45
|
|
|
30
46
|
export function taskCommandDefinition(config, ctx) {
|
|
@@ -34,10 +50,7 @@ export function taskCommandDefinition(config, ctx) {
|
|
|
34
50
|
input: { hint: 'switch|archive <任务id> | unbind' },
|
|
35
51
|
handler: (invocation) => {
|
|
36
52
|
try {
|
|
37
|
-
const
|
|
38
|
-
const sid = agent?.id || agent?.session?.id
|
|
39
|
-
const session = sid && agent?.ctx ? agent.ctx.sessions?.get(sid) : (agent?.session || null)
|
|
40
|
-
const cwd = session?.header?.cwd || agent?.session?.header?.cwd
|
|
53
|
+
const { session, sid, cwd } = invocationContext(invocation)
|
|
41
54
|
const raw = (invocation?.rawInput || '').trim()
|
|
42
55
|
const [verb, taskId, ...rest] = raw.split(/\s+/)
|
|
43
56
|
|
|
@@ -70,7 +83,7 @@ export function taskCommandDefinition(config, ctx) {
|
|
|
70
83
|
// /task rename <任务id> <json标题> —— 双击任务标题改名
|
|
71
84
|
if (verb === 'rename') {
|
|
72
85
|
if (!taskId) return { kind: 'error', text: '[task] rename 需要任务 id(由卡片双击调用)' }
|
|
73
|
-
const payload = raw
|
|
86
|
+
const payload = restAfter(raw, 2).trim()
|
|
74
87
|
let title = payload
|
|
75
88
|
try {
|
|
76
89
|
const parsed = JSON.parse(payload)
|
|
@@ -92,7 +105,7 @@ export function taskCommandDefinition(config, ctx) {
|
|
|
92
105
|
// /task todos <json> —— 卡片双击编辑步骤后,把整份新清单写回绑定任务并推宿主
|
|
93
106
|
// (与模型 todo_write 同一套数据,只是发起方是用户面板)
|
|
94
107
|
if (verb === 'todos') {
|
|
95
|
-
const rawJson = raw
|
|
108
|
+
const rawJson = restAfter(raw, 1).trim()
|
|
96
109
|
let todos
|
|
97
110
|
try {
|
|
98
111
|
todos = JSON.parse(rawJson)
|
|
@@ -126,7 +139,7 @@ export function taskCommandDefinition(config, ctx) {
|
|
|
126
139
|
return withTaskSnapshot(config, cwd, sid, store, note)
|
|
127
140
|
}
|
|
128
141
|
|
|
129
|
-
if (verb ===
|
|
142
|
+
if (verb === 'switch') {
|
|
130
143
|
const task = store.getTask(taskId)
|
|
131
144
|
if (!task) return { kind: 'error', text: `[task] 找不到任务: ${taskId}(/tasks 查看)` }
|
|
132
145
|
const prevBound = sid ? store.getBoundTaskId(sid) : null
|
|
@@ -149,7 +162,7 @@ export function taskCommandDefinition(config, ctx) {
|
|
|
149
162
|
return withTaskSnapshot(config, cwd, sid, store, note)
|
|
150
163
|
}
|
|
151
164
|
|
|
152
|
-
if (verb ===
|
|
165
|
+
if (verb === 'archive') {
|
|
153
166
|
const task = store.getTask(taskId)
|
|
154
167
|
if (!task) return { kind: 'error', text: `[task] 找不到任务: ${taskId}(/tasks 查看)` }
|
|
155
168
|
if (task.archived) return { kind: 'error', text: `[task] 已归档: ${task.title}` }
|
|
@@ -169,31 +182,3 @@ export function taskCommandDefinition(config, ctx) {
|
|
|
169
182
|
},
|
|
170
183
|
}
|
|
171
184
|
}
|
|
172
|
-
|
|
173
|
-
// 保留语义别名:/task-switch、/task-archive(仅当前端/旧入口用)
|
|
174
|
-
export function taskSwitchCommandDefinition(config) {
|
|
175
|
-
const base = taskCommandDefinition(config)
|
|
176
|
-
return {
|
|
177
|
-
...base,
|
|
178
|
-
name: 'task-switch',
|
|
179
|
-
handler: (invocation) => {
|
|
180
|
-
const id = (invocation?.rawInput || '').trim().split(/\s+/)[0]
|
|
181
|
-
return base.handler({ ...invocation, rawInput: id ? `switch ${id}` : '' })
|
|
182
|
-
},
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export function taskArchiveCommandDefinition(config) {
|
|
187
|
-
const base = taskCommandDefinition(config)
|
|
188
|
-
return {
|
|
189
|
-
...base,
|
|
190
|
-
name: 'task-archive',
|
|
191
|
-
handler: (invocation) => {
|
|
192
|
-
const id = (invocation?.rawInput || '').trim().split(/\s+/)[0]
|
|
193
|
-
return base.handler({ ...invocation, rawInput: id ? `archive ${id}` : '' })
|
|
194
|
-
},
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// renderTaskSnapshot 引用保留,供外部(测试/工具)使用
|
|
199
|
-
export { renderTaskSnapshot }
|
package/src/commands/tasks.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { memoryRootFor } from '../util/fs.js'
|
|
5
5
|
import { ProjectMemoryStore } from '../store.js'
|
|
6
6
|
import { projectRootFor } from '../setup/taskbridge.js'
|
|
7
|
+
import { fencedJson, invocationContext } from './invocation.js'
|
|
7
8
|
|
|
8
9
|
function timeAgo(iso) {
|
|
9
10
|
const diff = Date.now() - new Date(iso).getTime()
|
|
@@ -15,12 +16,21 @@ function timeAgo(iso) {
|
|
|
15
16
|
return `${Math.floor(h / 24)}天前`
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
/** 步骤在历史数据里可能是字符串,也可能是 {content|text, status}:统一取文本与状态。 */
|
|
20
|
+
function stepContent(s) {
|
|
21
|
+
if (typeof s === 'string') return s
|
|
22
|
+
return s?.content ?? s?.text ?? ''
|
|
23
|
+
}
|
|
24
|
+
function stepStatus(s) {
|
|
25
|
+
return typeof s === 'string' ? 'pending' : (s?.status || 'pending')
|
|
26
|
+
}
|
|
27
|
+
|
|
18
28
|
export function buildTaskPayload(tasks, boundId, archived) {
|
|
19
29
|
return JSON.stringify({
|
|
20
30
|
tasks: tasks.map((t) => ({
|
|
21
31
|
id: t.id,
|
|
22
32
|
title: t.title,
|
|
23
|
-
steps: (t.steps || []).map((s) => ({ content: s
|
|
33
|
+
steps: (t.steps || []).map((s) => ({ content: stepContent(s), status: stepStatus(s) })),
|
|
24
34
|
files: (t.files || []).map((f) => ({ path: f, line: undefined })),
|
|
25
35
|
lastActiveAt: t.lastActiveAt,
|
|
26
36
|
updatedAt: t.updatedAt,
|
|
@@ -47,18 +57,16 @@ export function renderTaskSnapshot(config, cwd, sid) {
|
|
|
47
57
|
|
|
48
58
|
if (!active.length) {
|
|
49
59
|
const payload = buildTaskPayload([], boundId, archived)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
text: `项目 ${root}\n任务记录: 0 套${archived ? `(归档 ${archived})` : ''}。让模型开始干活并维护 todo 清单后会自动建档。\n\n\`\`\`json\n${payload}\n\`\`\``,
|
|
53
|
-
}
|
|
60
|
+
const note = `项目 ${root}\n任务记录: 0 套${archived ? `(归档 ${archived})` : ''}。让模型开始干活并维护 todo 清单后会自动建档。`
|
|
61
|
+
return { kind: 'success', text: fencedJson(note, payload) }
|
|
54
62
|
}
|
|
55
63
|
const lines = [`项目 ${root}`, `任务: ${active.length} 套${archived ? `(归档 ${archived})` : ''}`, '']
|
|
56
64
|
for (const t of active) {
|
|
57
|
-
const done = (t.steps || []).filter((s) => s
|
|
65
|
+
const done = (t.steps || []).filter((s) => stepStatus(s) === 'completed').length
|
|
58
66
|
const total = (t.steps || []).length
|
|
59
67
|
const progress = total ? `${done}/${total}` : '无步骤'
|
|
60
|
-
const inProgress = (t.steps || []).find((s) => s
|
|
61
|
-
const step = inProgress ? ` · 当前: ${inProgress
|
|
68
|
+
const inProgress = (t.steps || []).find((s) => stepStatus(s) === 'in_progress')
|
|
69
|
+
const step = inProgress ? ` · 当前: ${stepContent(inProgress)}` : ''
|
|
62
70
|
const marker = bound && bound.id === t.id ? '(本会话绑定)' : ''
|
|
63
71
|
const files = (t.files || []).slice(0, 8)
|
|
64
72
|
const fileLine = files.length ? `\n 文件: ${files.join(', ')}${t.files.length > 8 ? ' …' : ''}` : ''
|
|
@@ -67,7 +75,7 @@ export function renderTaskSnapshot(config, cwd, sid) {
|
|
|
67
75
|
lines.push('', '续接/改名/归档:直接告诉模型(list_tasks / select_task / archive_task)。')
|
|
68
76
|
const humanText = lines.join('\n')
|
|
69
77
|
const payload = buildTaskPayload(active, boundId, archived)
|
|
70
|
-
return { kind: 'success', text:
|
|
78
|
+
return { kind: 'success', text: fencedJson(humanText, payload) }
|
|
71
79
|
}
|
|
72
80
|
|
|
73
81
|
export function tasksCommandDefinition(config, ctx) {
|
|
@@ -76,10 +84,7 @@ export function tasksCommandDefinition(config, ctx) {
|
|
|
76
84
|
description: '查看本项目的任务清单(几套任务、进度、涉及文件、当前绑定)',
|
|
77
85
|
handler: (invocation) => {
|
|
78
86
|
try {
|
|
79
|
-
const
|
|
80
|
-
const sid = agent?.id || agent?.session?.id
|
|
81
|
-
const session = sid && agent?.ctx ? agent.ctx.sessions?.get(sid) : (agent?.session || null)
|
|
82
|
-
const cwd = session?.header?.cwd || agent?.session?.header?.cwd
|
|
87
|
+
const { cwd, sid } = invocationContext(invocation)
|
|
83
88
|
return renderTaskSnapshot(config, cwd, sid)
|
|
84
89
|
} catch (err) {
|
|
85
90
|
return { kind: 'error', text: `[tasks] ${err?.message || err}` }
|
package/src/enhancer.js
CHANGED
|
@@ -19,7 +19,14 @@ function acceptTsVersion(mod) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export function initTypeScript(config) {
|
|
22
|
-
if (config?.enableTypeScript === false)
|
|
22
|
+
if (config?.enableTypeScript === false) {
|
|
23
|
+
// 显式关闭要把已解析的模块放掉:否则 enqueueEnhance 的 `if (!ts)` 仍会放行,
|
|
24
|
+
// 配置上关了、增强器照跑。
|
|
25
|
+
ts = null
|
|
26
|
+
tsPath = null
|
|
27
|
+
tsVersion = null
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
23
30
|
if (ts) return ts
|
|
24
31
|
|
|
25
32
|
// 1. 配置指定路径
|
|
@@ -265,7 +272,9 @@ export function deepParseWithTS(filePath, content) {
|
|
|
265
272
|
line: getLine(node)
|
|
266
273
|
})
|
|
267
274
|
} else if (ts.isTypeAliasDeclaration(node)) {
|
|
268
|
-
|
|
275
|
+
// checker.getTypeAtLocation(node.type) 对别名返回的是**别名自身**,于是
|
|
276
|
+
// `type X = { a: string }` 变成 `X= X -- f:1`。直接用类型节点的源码文本。
|
|
277
|
+
const typeStr = node.type ? node.type.getText().replace(/\s+/g, ' ').slice(0, 200) : 'any'
|
|
269
278
|
symbols.push({
|
|
270
279
|
name: node.name.getText(),
|
|
271
280
|
kind: 'type',
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// 索引一个文件的**唯一**实现。
|
|
2
|
+
//
|
|
3
|
+
// 这段逻辑以前在 index_repo(批量)、watch(轮询)、lazy(fs/observed 懒索引)里各抄一份,
|
|
4
|
+
// 三份随后各自打补丁、已经漂移:index_repo 多一道冗余的 dump 预检、体积超限时删旧记录,
|
|
5
|
+
// 而 watch/lazy 保留旧记录;terms 回填与单次读盘的写法也各不相同。
|
|
6
|
+
// 现在「这个文件该不该读、读出来该写什么条目」只在这里定义一次;
|
|
7
|
+
// 调用方只保留自己的编排(批量计数、watch 的 mtime 快照、lazy 的根解析与副作用)。
|
|
8
|
+
import { statSync } from 'node:fs'
|
|
9
|
+
import { isSupportedCode, isSupportedDoc, readFileForIndex } from './util/fs.js'
|
|
10
|
+
import { buildDocEntries } from './doc-pipeline.js'
|
|
11
|
+
import { docEntriesNeedBackfill } from './doc-index.js'
|
|
12
|
+
import { scanSymbols } from './symbols.js'
|
|
13
|
+
import { linkEntries } from './link.js'
|
|
14
|
+
|
|
15
|
+
/** 不索引的后缀。 */
|
|
16
|
+
export const UNSUPPORTED = 'unsupported'
|
|
17
|
+
/** 代码文件超过 maxFileSizeMb:读盘成本不抵符号表收益。 */
|
|
18
|
+
export const OVERSIZE = 'oversize'
|
|
19
|
+
/** 内容哈希未变,且无需回填词项。 */
|
|
20
|
+
export const UNCHANGED = 'unchanged'
|
|
21
|
+
/** 文档不可索引(dump / 抽取返回 null)→ 从 store 移除。 */
|
|
22
|
+
export const DROP = 'drop'
|
|
23
|
+
/** 有内容要写入 store。 */
|
|
24
|
+
export const WRITE = 'write'
|
|
25
|
+
|
|
26
|
+
/** 后缀 → 'code' | 'doc' | null。 */
|
|
27
|
+
export function fileKind(ext) {
|
|
28
|
+
if (isSupportedCode(ext)) return 'code'
|
|
29
|
+
if (isSupportedDoc(ext)) return 'doc'
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function exceedsSizeCap(kind, size, config) {
|
|
34
|
+
return kind === 'code' && !!config.maxFileSizeMb && size > config.maxFileSizeMb * 1024 * 1024
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 读取 + 判重 + 抽条目。抛出的异常(读盘失败、PDF 解析失败……)由调用方按各自策略处理:
|
|
39
|
+
* index_repo 收进 failures 汇总,watch 去重打印并可重试,lazy 记一行日志。
|
|
40
|
+
*
|
|
41
|
+
* @returns {{key: string, hash?: string, size?: number, type?: string, entries?: object[]}}
|
|
42
|
+
* key 为 UNSUPPORTED / OVERSIZE / UNCHANGED / DROP / WRITE 之一。
|
|
43
|
+
* @param {object} args
|
|
44
|
+
* @param {number} [args.size] 调用方已 statSync 时传入,避免重复 stat。
|
|
45
|
+
* @param {boolean} [args.force] index_repo 的 reindex:忽略哈希跳过。
|
|
46
|
+
*/
|
|
47
|
+
export async function planFileIndex({ rel, filePath, kind, config, record, existingEntries = [], size, force = false }) {
|
|
48
|
+
if (!kind) return { key: UNSUPPORTED }
|
|
49
|
+
const fileSize = typeof size === 'number' ? size : statSync(filePath).size
|
|
50
|
+
if (exceedsSizeCap(kind, fileSize, config)) return { key: OVERSIZE }
|
|
51
|
+
|
|
52
|
+
// 单次读盘:同一 buffer 供内容哈希与正文解码使用(未变更的文件不解码)。
|
|
53
|
+
const { hash, buffer } = readFileForIndex(filePath)
|
|
54
|
+
// 旧 store 的 doc 条目缺 terms → 即使哈希未变也重抽一次(一次性回填)。
|
|
55
|
+
const backfill = kind === 'doc' && docEntriesNeedBackfill(existingEntries)
|
|
56
|
+
if (!force && record && record.sha256 === hash && !backfill) return { key: UNCHANGED }
|
|
57
|
+
|
|
58
|
+
if (kind === 'code') {
|
|
59
|
+
return { key: WRITE, hash, size: fileSize, type: 'code', entries: scanSymbols(rel, filePath, buffer.toString('utf8')) }
|
|
60
|
+
}
|
|
61
|
+
// dump 判定在 buildDocEntries 内部(唯一真源):不可索引的文档返回 null。
|
|
62
|
+
const entries = await buildDocEntries(rel, filePath, {
|
|
63
|
+
chunkChars: config.chunkChars,
|
|
64
|
+
maxChunks: config.maxChunksPerFile,
|
|
65
|
+
maxFileSizeMb: config.maxFileSizeMb,
|
|
66
|
+
maxPdfPages: config.maxPdfPages,
|
|
67
|
+
})
|
|
68
|
+
return entries === null ? { key: DROP } : { key: WRITE, hash, size: fileSize, type: 'doc', entries }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* plan → 落盘用的更新记录。只接受 DROP / WRITE:UNCHANGED / OVERSIZE 由调用方自己跳过,
|
|
73
|
+
* 误传会直接抛错——静默写进一条 entries=undefined 的记录才是真正的坑。
|
|
74
|
+
* `expectedHash` 是 CAS 基准:扫描之后文件又被改动时,commit 会拒绝这次写入,
|
|
75
|
+
* 调用方据此回滚自己的快照(见 commitFileUpdates 的返回值)。
|
|
76
|
+
*/
|
|
77
|
+
export function toFileUpdate(rel, plan, record) {
|
|
78
|
+
if (plan.key === DROP) return { rel, drop: true }
|
|
79
|
+
if (plan.key !== WRITE) throw new Error(`toFileUpdate: unexpected plan key ${plan.key}`)
|
|
80
|
+
return {
|
|
81
|
+
rel,
|
|
82
|
+
expectedHash: record?.sha256 ?? null,
|
|
83
|
+
hash: plan.hash,
|
|
84
|
+
size: plan.size,
|
|
85
|
+
type: plan.type,
|
|
86
|
+
entries: plan.entries,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 一批更新一次性落盘:写入 / 移除 → 清掉本轮未见到的旧条目 → 重建链接。
|
|
92
|
+
* 单事务的好处是 store 只 save 一次,watch 每轮不会反复重写。
|
|
93
|
+
*
|
|
94
|
+
* @returns {{stale: string[], removed: number}} stale 是 CAS 失败(并发改动)的 rel,
|
|
95
|
+
* 调用方应让它们保持「未落快照」状态,下一轮重试。
|
|
96
|
+
*/
|
|
97
|
+
export function commitFileUpdates(store, { updates, unseen = null, link = true }) {
|
|
98
|
+
const stale = []
|
|
99
|
+
let removed = 0
|
|
100
|
+
store.commit((s) => {
|
|
101
|
+
for (const update of updates) {
|
|
102
|
+
if (update.drop) {
|
|
103
|
+
s.removeFile(update.rel)
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
if (!s.applyFileUpdate(update.rel, update)) stale.push(update.rel)
|
|
107
|
+
}
|
|
108
|
+
if (unseen) {
|
|
109
|
+
for (const rel of Object.keys(s.files)) {
|
|
110
|
+
if (!unseen.has(rel)) {
|
|
111
|
+
s.removeFile(rel)
|
|
112
|
+
removed++
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (link) linkEntries(s)
|
|
117
|
+
})
|
|
118
|
+
return { stale, removed }
|
|
119
|
+
}
|