@yolk_vat-y/dsh-project-memory 0.5.11 → 0.5.13
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 +225 -1
- package/LICENSE +1 -1
- package/README.md +38 -35
- package/README.zh-CN.md +38 -35
- package/client/client.js +73 -73
- package/client/client.js.map +1 -1
- package/package.json +12 -11
- package/scripts/bench-load.mjs +47 -0
- package/scripts/bench.mjs +35 -4
- package/src/audit.js +29 -3
- package/src/auto-inject.js +5 -3
- package/src/enhancer.js +170 -86
- package/src/llm.js +4 -1
- package/src/store.js +139 -22
- package/src/tools/index-repo.js +12 -2
- package/src/tools/query-memory.js +26 -3
- package/src/util/fs.js +65 -1
- package/src/watch.js +41 -7
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yolk_vat-y/dsh-project-memory",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.13",
|
|
4
4
|
"description": "Persistent project memory for dsh agents: index docs (PDF/Markdown/text) and code symbols into a searchable per-workspace store, recall them with cited sources, and keep experience entries (problems -> solutions) searchable on demand.",
|
|
5
|
+
"author": "00080000 <3388065969@qq.com>",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"main": "src/index.js",
|
|
7
8
|
"files": [
|
|
@@ -18,7 +19,7 @@
|
|
|
18
19
|
"url": "https://github.com/00080000/dsh-project-memory.git"
|
|
19
20
|
},
|
|
20
21
|
"scripts": {
|
|
21
|
-
"test": "node test/run-test.mjs && node test/taskbridge.test.mjs && node test/insight-store.test.mjs && node test/reflection-pipeline.test.mjs && node test/auto-inject.test.mjs && node test/insight-actions.test.mjs && node test/host-contract.test.mjs && node test/llm-route.test.mjs && node test/doc-index.test.mjs && node test/client-hints.test.mjs && node test/recall.test.mjs && node test/readiness.test.mjs && node test/insight-derive.test.mjs && node test/readiness-eval.test.mjs && node test/ops.test.mjs && node test/injection-audit.test.mjs && node test/injection-budget.test.mjs && node test/injection-scenarios.test.mjs && node test/bugfix-0.5.7.test.mjs && node test/client-icons.test.mjs && node test/client-slash.test.mjs && node test/workflow-command.test.mjs && node test/client-session-id.test.mjs && node test/task-view.test.mjs && node test/root-guards.test.mjs && node test/store-gitignore.test.mjs",
|
|
22
|
+
"test": "node test/run-test.mjs && node test/taskbridge.test.mjs && node test/insight-store.test.mjs && node test/reflection-pipeline.test.mjs && node test/auto-inject.test.mjs && node test/insight-actions.test.mjs && node test/host-contract.test.mjs && node test/llm-route.test.mjs && node test/doc-index.test.mjs && node test/client-hints.test.mjs && node test/recall.test.mjs && node test/readiness.test.mjs && node test/insight-derive.test.mjs && node test/readiness-eval.test.mjs && node test/ops.test.mjs && node test/injection-audit.test.mjs && node test/injection-budget.test.mjs && node test/injection-scenarios.test.mjs && node test/bugfix-0.5.7.test.mjs && node test/client-icons.test.mjs && node test/client-slash.test.mjs && node test/workflow-command.test.mjs && node test/client-session-id.test.mjs && node test/task-view.test.mjs && node test/root-guards.test.mjs && node test/store-gitignore.test.mjs && node test/store-cache.test.mjs && node test/enhancer.test.mjs",
|
|
22
23
|
"eval:injection": "node test/injection-scenarios.test.mjs",
|
|
23
24
|
"typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.client.json",
|
|
24
25
|
"selfcheck:triggers": "node test/injection-scenarios.test.mjs --selfcheck",
|
|
@@ -48,13 +49,13 @@
|
|
|
48
49
|
"pdfjs-dist": "^4.10.38"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
52
|
-
"@deepseek-ai/dsh-client-locale": "0.
|
|
53
|
-
"@deepseek-ai/dsh-client-store": "0.
|
|
54
|
-
"@deepseek-ai/dsh-client-ui-primitives": "0.
|
|
55
|
-
"@deepseek-ai/dsh-client-ui-slots": "0.
|
|
56
|
-
"@deepseek-ai/dsh-llm": "0.
|
|
57
|
-
"@deepseek-ai/dsh-tools": "0.
|
|
52
|
+
"@deepseek-ai/cordis": "^4.0.4",
|
|
53
|
+
"@deepseek-ai/dsh-client-locale": "0.2.0-rc.1",
|
|
54
|
+
"@deepseek-ai/dsh-client-store": "0.2.0-rc.1",
|
|
55
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.1",
|
|
56
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.1",
|
|
57
|
+
"@deepseek-ai/dsh-llm": "0.2.0-rc.1",
|
|
58
|
+
"@deepseek-ai/dsh-tools": "0.2.0-rc.1",
|
|
58
59
|
"@deepseek-ai/schemastery": "^3.18.2",
|
|
59
60
|
"@tsdown/css": "0.22.14",
|
|
60
61
|
"@types/react": "^18.3.31",
|
|
@@ -65,8 +66,8 @@
|
|
|
65
66
|
},
|
|
66
67
|
"peerDependencies": {
|
|
67
68
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
68
|
-
"@deepseek-ai/dsh-llm": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-rc.1 <0.2.0-0",
|
|
69
|
-
"@deepseek-ai/dsh-tools": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-rc.1 <0.2.0-0",
|
|
69
|
+
"@deepseek-ai/dsh-llm": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
70
|
+
"@deepseek-ai/dsh-tools": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
70
71
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
71
72
|
"typescript": "^5.0.0 || ^6.0.0"
|
|
72
73
|
},
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 在**独立进程**里量一次 store 冷加载的耗时与常驻内存,输出一行 JSON。
|
|
4
|
+
*
|
|
5
|
+
* 为什么要单独一个进程:`scripts/bench.mjs` 在同一个进程里已经有一个 store(索引用的那个),
|
|
6
|
+
* 再 load 第二个副本量出来的堆是两份的。这里从空进程冷加载,数字才是「一个 store 常驻多少」。
|
|
7
|
+
* 必须带 `--expose-gc` 运行(bench.mjs 会自己加),否则量不到稳定值。
|
|
8
|
+
*
|
|
9
|
+
* 用法:node --expose-gc scripts/bench-load.mjs <storeDir>
|
|
10
|
+
*/
|
|
11
|
+
import { performance } from 'node:perf_hooks'
|
|
12
|
+
|
|
13
|
+
import { ProjectMemoryStore } from '../src/store.js'
|
|
14
|
+
|
|
15
|
+
const dir = process.argv[2]
|
|
16
|
+
if (!dir) {
|
|
17
|
+
console.error('usage: node --expose-gc scripts/bench-load.mjs <storeDir>')
|
|
18
|
+
process.exit(2)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const gc = typeof global.gc === 'function' ? global.gc : () => {}
|
|
22
|
+
const mb = (n) => Math.round((n / 1048576) * 10) / 10
|
|
23
|
+
const snap = () => {
|
|
24
|
+
const m = process.memoryUsage()
|
|
25
|
+
return { heapMb: mb(m.heapUsed), rssMb: mb(m.rss) }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
gc()
|
|
29
|
+
const t0 = performance.now()
|
|
30
|
+
const store = new ProjectMemoryStore(dir).load()
|
|
31
|
+
const loadMs = performance.now() - t0
|
|
32
|
+
gc()
|
|
33
|
+
const afterLoad = snap()
|
|
34
|
+
|
|
35
|
+
// allEntries() 会在内存里物化 searchText(派生字段不落盘),这一步才是查询前的水位
|
|
36
|
+
const entries = store.allEntries()
|
|
37
|
+
gc()
|
|
38
|
+
const afterEntries = snap()
|
|
39
|
+
|
|
40
|
+
console.log(
|
|
41
|
+
JSON.stringify({
|
|
42
|
+
loadMs: Number(loadMs.toFixed(2)),
|
|
43
|
+
entries: entries.length,
|
|
44
|
+
afterLoad,
|
|
45
|
+
afterEntries,
|
|
46
|
+
}),
|
|
47
|
+
)
|
package/scripts/bench.mjs
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* 独立基准:给一个真实项目路径,量「冷索引 / 冷加载 / 热查询 / 单文件热重索引 /
|
|
3
|
+
* 独立基准:给一个真实项目路径,量「冷索引 / 冷加载 / 热查询 / 单文件热重索引 / 存储体积 / 常驻内存」。
|
|
4
4
|
*
|
|
5
5
|
* 特点:
|
|
6
6
|
* - **不需要运行 dsh**,只 import 插件自己的 src 模块,走的就是线上那条索引与检索路径;
|
|
7
7
|
* - **不碰被测项目**:结果写进临时目录,跑完删除(`--keep` 可保留);
|
|
8
8
|
* - 索引期零模型调用、零网络请求,所以在任何机器上都能复现。
|
|
9
|
+
* - 常驻内存(load 后 / allEntries() 后)由一个 `--expose-gc` 的子进程量(`scripts/bench-load.mjs`),
|
|
10
|
+
* 否则同一个进程里已经有索引用的那个 store,量出来是两份。
|
|
9
11
|
*
|
|
10
12
|
* 用法:
|
|
11
13
|
* node scripts/bench.mjs [projectPath] [--json] [--samples 100] [--no-pdf] [--keep] [--max-files 20000]
|
|
@@ -15,10 +17,12 @@
|
|
|
15
17
|
* [{ "query": "jwt token validation", "expect": "src/utils/auth.ts" }, ...]
|
|
16
18
|
* `expect` 按子串匹配结果的 sourcePath / title / id(大小写不敏感)。
|
|
17
19
|
*/
|
|
20
|
+
import { execFileSync } from 'node:child_process'
|
|
18
21
|
import { cpSync, existsSync, mkdtempSync, readdirSync, rmSync, statSync } from 'node:fs'
|
|
19
22
|
import os from 'node:os'
|
|
20
23
|
import path from 'node:path'
|
|
21
24
|
import { performance } from 'node:perf_hooks'
|
|
25
|
+
import { fileURLToPath } from 'node:url'
|
|
22
26
|
|
|
23
27
|
import { ProjectMemoryStore } from '../src/store.js'
|
|
24
28
|
import { buildDocEntries } from '../src/doc-pipeline.js'
|
|
@@ -94,6 +98,7 @@ function stats(xs) {
|
|
|
94
98
|
|
|
95
99
|
function dirSize(dir) {
|
|
96
100
|
let bytes = 0
|
|
101
|
+
let allocated = 0
|
|
97
102
|
let files = 0
|
|
98
103
|
const stack = [dir]
|
|
99
104
|
while (stack.length) {
|
|
@@ -108,7 +113,11 @@ function dirSize(dir) {
|
|
|
108
113
|
if (e.isDirectory()) stack.push(p)
|
|
109
114
|
else {
|
|
110
115
|
try {
|
|
111
|
-
|
|
116
|
+
const st = statSync(p)
|
|
117
|
+
bytes += st.size
|
|
118
|
+
// 一个源文件一个碎分片 → 每个文件至少占一个块(通常 4 KB),`du` 会明显大于
|
|
119
|
+
// 内容字节和。两个数都给出来,别把「内容大小」当成「占盘大小」。
|
|
120
|
+
allocated += st.blocks ? st.blocks * 512 : st.size
|
|
112
121
|
files++
|
|
113
122
|
} catch {
|
|
114
123
|
/* ignore */
|
|
@@ -116,7 +125,7 @@ function dirSize(dir) {
|
|
|
116
125
|
}
|
|
117
126
|
}
|
|
118
127
|
}
|
|
119
|
-
return { bytes, files }
|
|
128
|
+
return { bytes, allocated, files }
|
|
120
129
|
}
|
|
121
130
|
|
|
122
131
|
const kb = (n) => `${(n / 1024).toFixed(1)} KB`
|
|
@@ -206,6 +215,16 @@ const onDisk = dirSize(tmpStore)
|
|
|
206
215
|
const coldDir = `${tmpStore}-cold`
|
|
207
216
|
cpSync(tmpStore, coldDir, { recursive: true })
|
|
208
217
|
const coldLoad = timeSync(() => new ProjectMemoryStore(coldDir).load())
|
|
218
|
+
// 常驻内存必须在独立进程里量:本进程已经拿着索引那个 store,再 load 一个副本得到的是两份
|
|
219
|
+
const memory = (() => {
|
|
220
|
+
try {
|
|
221
|
+
const loadScript = path.join(path.dirname(fileURLToPath(import.meta.url)), 'bench-load.mjs')
|
|
222
|
+
const out = execFileSync(process.execPath, ['--expose-gc', loadScript, coldDir], { encoding: 'utf8' })
|
|
223
|
+
return JSON.parse(out.trim().split('\n').pop())
|
|
224
|
+
} catch (err) {
|
|
225
|
+
return { error: err.message }
|
|
226
|
+
}
|
|
227
|
+
})()
|
|
209
228
|
|
|
210
229
|
// ---------- 4. 热查询 / 冷查询 ----------
|
|
211
230
|
const queryPool = entries.filter((e) => typeof e.title === 'string' && e.title.trim().length >= 4)
|
|
@@ -328,10 +347,12 @@ const result = {
|
|
|
328
347
|
store: {
|
|
329
348
|
dir: tmpStore,
|
|
330
349
|
bytes: onDisk.bytes,
|
|
350
|
+
allocatedBytes: onDisk.allocated,
|
|
331
351
|
files: onDisk.files,
|
|
332
352
|
bytesPerEntry: entries.length ? Math.round(onDisk.bytes / entries.length) : 0,
|
|
333
353
|
},
|
|
334
354
|
coldLoadMs: round(coldLoad.ms),
|
|
355
|
+
memory,
|
|
335
356
|
query: {
|
|
336
357
|
samples: queries.length,
|
|
337
358
|
idfRebuildMs: round(idfCold.ms),
|
|
@@ -363,7 +384,17 @@ if (opts.json) {
|
|
|
363
384
|
console.log(` read+hash ${result.index.readHashMs} ms · extract ${result.index.extractMs} ms · commit ${result.index.commitMs} ms`)
|
|
364
385
|
console.log(` per file p50 ${result.index.perFileMs.p50} ms · p95 ${result.index.perFileMs.p95} ms`)
|
|
365
386
|
console.log(` note: read+hash depends on the OS page cache — run it twice and say which run you quote`)
|
|
366
|
-
console.log(
|
|
387
|
+
console.log(
|
|
388
|
+
`store ${mb(result.store.bytes)} content · ${mb(result.store.allocatedBytes)} on disk · ${result.store.bytesPerEntry} bytes/entry · cold load ${result.coldLoadMs} ms`,
|
|
389
|
+
)
|
|
390
|
+
if (memory && !memory.error) {
|
|
391
|
+
console.log(
|
|
392
|
+
`memory isolated child: after load heap ${memory.afterLoad.heapMb} MB / RSS ${memory.afterLoad.rssMb} MB` +
|
|
393
|
+
` · after allEntries() heap ${memory.afterEntries.heapMb} MB / RSS ${memory.afterEntries.rssMb} MB`,
|
|
394
|
+
)
|
|
395
|
+
} else if (memory && memory.error) {
|
|
396
|
+
console.log(`memory unavailable (${memory.error})`)
|
|
397
|
+
}
|
|
367
398
|
console.log(`\n-- query (shipped scorer: IDF cache + streaming BM25 + CJK phrase boost) --`)
|
|
368
399
|
console.log(`idf rebuild (first query after write) ${result.query.idfRebuildMs} ms`)
|
|
369
400
|
console.log(`cold query (rebuild + rank) ${result.query.coldMs} ms`)
|
package/src/audit.js
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
// - 只在确有决策(本条真的进了上下文)时写一行:dropped 单独出现不写,否则每步刷屏;
|
|
9
9
|
// - 任何异常(目录不可写、磁盘满、记录序列化失败)一律吞掉,绝不影响宿主请求;
|
|
10
10
|
// - 单文件超限就地轮转一份 `.1`,不引入新的清理线程或后台任务。
|
|
11
|
-
import { appendFileSync, mkdirSync, renameSync, statSync } from 'node:fs'
|
|
11
|
+
import { appendFileSync, existsSync, mkdirSync, renameSync, statSync } from 'node:fs'
|
|
12
12
|
import path from 'node:path'
|
|
13
|
+
import { hasProjectMarker } from './util/fs.js'
|
|
13
14
|
|
|
14
15
|
export const AUDIT_FILE = 'injection-audit.jsonl'
|
|
15
16
|
const DEFAULT_MAX_BYTES = 256 * 1024
|
|
@@ -75,10 +76,34 @@ export function shadowRecordFrom(input) {
|
|
|
75
76
|
* 追加一行影子记录。返回是否写入成功(仅供测试断言)。与主审计同约定:任何异常一律吞掉,
|
|
76
77
|
* 绝不影响宿主请求。
|
|
77
78
|
*/
|
|
78
|
-
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 审计**只为"已经存在"或"结论明确"的根创建 store 目录**。
|
|
82
|
+
*
|
|
83
|
+
* 本插件里会在"什么都没索引过"的空目录里造出 `.dsh-project-memory` 的就是下面那句
|
|
84
|
+
* `mkdirSync`(`load()` 不建目录,`ensureSelfIgnore()` 也要求目录已存在)。于是任何一次
|
|
85
|
+
* "**在容器目录里**开会话"都会留下一个只装着审计日志的空 store —— 而那个目录随后还会被
|
|
86
|
+
* `resolveProjectMemoryRoot()` 当成项目根。审计是旁路,没有 store 就没有可对照的候选。
|
|
87
|
+
*
|
|
88
|
+
* 两个放行条件:
|
|
89
|
+
* - store 目录已存在 → 真被索引/写过,照旧;
|
|
90
|
+
* - 该根**自身带项目标记**(`hasProjectMarker`)→ 是"声明过的项目",即便还没落过盘,
|
|
91
|
+
* 也该留下第一步的记录(`rootNotice` 用的是同一个判据,两处口径一致)。
|
|
92
|
+
* "根是推定的(无标记)且还没有内容"→ 跳过。这正是容器目录的那种情况。
|
|
93
|
+
*
|
|
94
|
+
* @param {string} memoryDir `<root>/.dsh-project-memory`
|
|
95
|
+
* @param {string} [root] 项目根;省略时退回 `path.dirname(memoryDir)`
|
|
96
|
+
*/
|
|
97
|
+
function auditDirReady(memoryDir, root) {
|
|
98
|
+
if (existsSync(memoryDir)) return true
|
|
99
|
+
return hasProjectMarker(root || path.dirname(memoryDir))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function appendShadowAudit(memoryDir, record, cfg, root) {
|
|
79
103
|
const c = cfg || {}
|
|
80
104
|
if (c.enabled === false) return false
|
|
81
105
|
if (!memoryDir || !record) return false
|
|
106
|
+
if (!auditDirReady(memoryDir, root)) return false
|
|
82
107
|
try {
|
|
83
108
|
mkdirSync(memoryDir, { recursive: true })
|
|
84
109
|
const file = shadowFileFor(memoryDir)
|
|
@@ -124,10 +149,11 @@ export function auditRecordFrom(input) {
|
|
|
124
149
|
* @param {object} record {@link auditRecordFrom} 的产物
|
|
125
150
|
* @param {{enabled?: boolean, maxBytes?: number}} [cfg] {@link cfgAudit} 的产物
|
|
126
151
|
*/
|
|
127
|
-
export function appendInjectionAudit(memoryDir, record, cfg) {
|
|
152
|
+
export function appendInjectionAudit(memoryDir, record, cfg, root) {
|
|
128
153
|
const c = cfg || {}
|
|
129
154
|
if (c.enabled === false) return false
|
|
130
155
|
if (!memoryDir || !record) return false
|
|
156
|
+
if (!auditDirReady(memoryDir, root)) return false
|
|
131
157
|
try {
|
|
132
158
|
mkdirSync(memoryDir, { recursive: true })
|
|
133
159
|
const file = auditFileFor(memoryDir)
|
package/src/auto-inject.js
CHANGED
|
@@ -619,7 +619,7 @@ async function injectForStep({ payload, decision, config, cfg, audit, shadow, se
|
|
|
619
619
|
reasons: built.reasons,
|
|
620
620
|
candidates: built.candidates,
|
|
621
621
|
silence: silence.reason,
|
|
622
|
-
}), shadow)
|
|
622
|
+
}), shadow, root)
|
|
623
623
|
|
|
624
624
|
// 记忆根通告:根是**推定**的(会话工作目录,且该目录没有任何项目标记)时,
|
|
625
625
|
// 第一次注入前告诉模型"记忆现在存在哪、怎么改"。它是状态声明(与常驻任务卡同类),
|
|
@@ -648,7 +648,9 @@ async function injectForStep({ payload, decision, config, cfg, audit, shadow, se
|
|
|
648
648
|
if (built.reasons.length) {
|
|
649
649
|
try {
|
|
650
650
|
if (recordHit({ store, globalStore, ids: built.reasons.map((r) => r.id) })) {
|
|
651
|
-
|
|
651
|
+
// 只有这个根里真有东西才落盘。否则一次**全局** insight 的命中就会为一个从没被
|
|
652
|
+
// 索引过的容器目录建出整个 store(`save()` 会 mkdir)。审计侧同样不建,见 audit.js。
|
|
653
|
+
if (store.hasContent) store.save()
|
|
652
654
|
globalStore.commit()
|
|
653
655
|
}
|
|
654
656
|
} catch {
|
|
@@ -667,7 +669,7 @@ async function injectForStep({ payload, decision, config, cfg, audit, shadow, se
|
|
|
667
669
|
dropped: built.dropped,
|
|
668
670
|
budget: { items: quota.items, chars: quota.chars, lastStep: Number.isFinite(quota.lastStep) ? quota.lastStep : null },
|
|
669
671
|
silence: silence.reason,
|
|
670
|
-
}), audit)
|
|
672
|
+
}), audit, root)
|
|
671
673
|
|
|
672
674
|
return { ...decision, messages: [...decision.messages, injectionMessage(injectedText)] }
|
|
673
675
|
}
|
package/src/enhancer.js
CHANGED
|
@@ -109,32 +109,78 @@ export function isTypeScriptFile(filePath) {
|
|
|
109
109
|
ext === '.mjs' || ext === '.cjs' || ext === '.mts' || ext === '.cts'
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* 路径归一化。
|
|
114
|
+
*
|
|
115
|
+
* 旧实现在 host 里用 `fileName === filePath` 认自己的文件。TypeScript 内部一律走
|
|
116
|
+
* `normalizePath`(`\`→`/`、消解 `.`/`..`、绝对化),而 `path.join` 给出的是平台原生形态 ——
|
|
117
|
+
* Windows 上是反斜杠。两边字符串不同 → `getSourceFile` 返回 undefined → `deepParseWithTS`
|
|
118
|
+
* 静默返回 `[]`:增强整层失效且无日志。2026-09-26 的 Windows CI 就是这样挂的
|
|
119
|
+
* (`test/enhancer.test.mjs` 的「增强结果落进了 store」),同一份代码在 POSIX 上从不现形。
|
|
120
|
+
*
|
|
121
|
+
* 用 TS 自己的 `normalizePath` 而不是自己拼:它才是权威口径。**该函数只在运行时导出、
|
|
122
|
+
* `typescript.d.ts` 里没有声明**,peer 范围覆盖 TS 5/6/7,所以必须做 `typeof` 兜底。
|
|
123
|
+
*/
|
|
124
|
+
function canonicalTsPath(fileName) {
|
|
125
|
+
const s = String(fileName)
|
|
126
|
+
return typeof ts?.normalizePath === 'function' ? ts.normalizePath(s) : s.replace(/\\/g, '/')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 跟随 ts.sys 的实际大小写语义;拿不到时按平台回落。 */
|
|
130
|
+
function tsPathCaseSensitive() {
|
|
131
|
+
if (ts && ts.sys && typeof ts.sys.useCaseSensitiveFileNames === 'boolean') {
|
|
132
|
+
return ts.sys.useCaseSensitiveFileNames
|
|
133
|
+
}
|
|
134
|
+
return process.platform !== 'win32' && process.platform !== 'darwin'
|
|
135
|
+
}
|
|
136
|
+
|
|
112
137
|
export function deepParseWithTS(filePath, content) {
|
|
113
138
|
if (!ts) return null
|
|
114
139
|
|
|
140
|
+
const caseSensitive = tsPathCaseSensitive()
|
|
141
|
+
const target = caseSensitive ? canonicalTsPath(filePath) : canonicalTsPath(filePath).toLowerCase()
|
|
142
|
+
const isTarget = (fileName) => {
|
|
143
|
+
const a = canonicalTsPath(fileName)
|
|
144
|
+
return (caseSensitive ? a : a.toLowerCase()) === target
|
|
145
|
+
}
|
|
146
|
+
// 预建唯一的 SourceFile:它就是"这个文件"的真身,host 只需把它交出去。
|
|
147
|
+
// 后面按**对象身份**确认 TS 真的收下了它,不依赖 TS 内部怎么命名。
|
|
148
|
+
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true)
|
|
149
|
+
|
|
115
150
|
// Create a compiler host that provides the source file from memory
|
|
116
151
|
const host = {
|
|
117
|
-
getSourceFile: (fileName, languageVersion, onError) =>
|
|
118
|
-
if (fileName === filePath) {
|
|
119
|
-
return ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true)
|
|
120
|
-
}
|
|
121
|
-
return undefined
|
|
122
|
-
},
|
|
152
|
+
getSourceFile: (fileName, languageVersion, onError) => (isTarget(fileName) ? sourceFile : undefined),
|
|
123
153
|
getDefaultLibFileName: (options) => ts.getDefaultLibFileName(options),
|
|
124
|
-
|
|
125
|
-
getCanonicalFileName: (fileName) => fileName,
|
|
154
|
+
getCanonicalFileName: (fileName) => (caseSensitive ? canonicalTsPath(fileName) : canonicalTsPath(fileName).toLowerCase()),
|
|
126
155
|
getCurrentDirectory: () => process.cwd(),
|
|
127
156
|
getNewLine: () => '\n',
|
|
128
|
-
useCaseSensitiveFileNames: () =>
|
|
129
|
-
fileExists: (fileName) => fileName
|
|
130
|
-
readFile: (fileName) => fileName
|
|
157
|
+
useCaseSensitiveFileNames: () => caseSensitive,
|
|
158
|
+
fileExists: (fileName) => isTarget(fileName),
|
|
159
|
+
readFile: (fileName) => (isTarget(fileName) ? content : undefined),
|
|
131
160
|
directoryExists: () => true,
|
|
132
161
|
getDirectories: () => [],
|
|
133
162
|
}
|
|
134
163
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
164
|
+
// 刻意不加载默认 lib(`noLib: true`):host 手里只有这一个文件的内容,加载 lib 需要把
|
|
165
|
+
// lib.d.ts 及其引用的 6 个文件(lib.es5 / lib.decorators / lib.decorators.legacy / lib.dom /
|
|
166
|
+
// lib.webworker.importscripts / lib.scripthost,共 ~2.1MB 文本)一并读盘并重解析。
|
|
167
|
+
// 实测:每个 program 重解析 p50=102ms/文件;把解析好的 SourceFile 缓存跨 program 复用
|
|
168
|
+
// (或走 ts.DocumentRegistry)是一次性 ~100ms + 每文件 ~1ms。本版不做,代价是全局类型
|
|
169
|
+
// (Promise/Array/DOM)在 typeSig 里塌成 any/unknown —— 见 CHANGELOG「已知限制(本版未做)」。
|
|
170
|
+
// 旧实现在这里留的是 `getDefaultLibLocation: () => ts.getDefaultLibFilePath({})`,返回的是
|
|
171
|
+
// **文件**路径而不是目录,于是这层加载既没生效、也没人把它写下来。现在把"不载"写成声明。
|
|
172
|
+
const program = ts.createProgram([filePath], { allowJs: true, noLib: true }, host)
|
|
173
|
+
// 按**对象身份**确认 TS 收下了这个 SourceFile。`program.getSourceFile(filePath)` 也能问,
|
|
174
|
+
// 但它内部用 `toPath(name, cwd, getCanonicalFileName)` 拼 key,对相对路径还会拼上 cwd ——
|
|
175
|
+
// 正是上面那条脆弱路径。`sourceFile.parent` 恒为 undefined,不能用它判断。
|
|
176
|
+
if (program.getSourceFileByPath(sourceFile.path) !== sourceFile) {
|
|
177
|
+
// 走到这里 = host 没认出 root path(本该有结果却没有)。**刻意不写终端**:
|
|
178
|
+
// · 代价:watch 每 15s 一轮、每个不受支持的 root 都会撞上它,"逐轮一条"去重后仍会变成
|
|
179
|
+
// "每个文件一条",在真实终端里就是刷屏;
|
|
180
|
+
// · 收益为 0:返回 [] 只是"这轮没有增强结果",不会产出错误结果,调用方本来就按空处理。
|
|
181
|
+
// 这个不变量的看护在 test/enhancer.test.mjs(「路径形态」那组 + 末尾的静默契约),不靠日志。
|
|
182
|
+
return []
|
|
183
|
+
}
|
|
138
184
|
const checker = program.getTypeChecker()
|
|
139
185
|
|
|
140
186
|
const symbols = []
|
|
@@ -171,60 +217,79 @@ export function deepParseWithTS(filePath, content) {
|
|
|
171
217
|
return sourceFile.getLineAndCharacterOfPosition(pos).line + 1
|
|
172
218
|
}
|
|
173
219
|
|
|
220
|
+
/**
|
|
221
|
+
* 参数列表的渲染:用**源码文本**,不是 checker 的类型串。
|
|
222
|
+
*
|
|
223
|
+
* 旧实现拼 `${p.name.getText()}: ${type}`,两个副作用都被「L2 会就地重写 L1 文本」放大:
|
|
224
|
+
* · 无注解的参数一律写成 `: any`(本仓库 51 个 .js/.mjs 实测:86% 的参数如此),把 L1 正则
|
|
225
|
+
* 已经提取到的 `chunkChars = 3000` 换成纯噪音;
|
|
226
|
+
* · `= 默认值` / `?` / `...rest` / 解构模式全被丢掉(实测 52 个带默认值的条目里丢了 36 个)。
|
|
227
|
+
* 源码文本两者都保得住,还省掉每个参数一次 checker 调用。单参数限长 80,整行仍由
|
|
228
|
+
* oneLineDeclaration 收到 200 字符。
|
|
229
|
+
*/
|
|
230
|
+
function renderParams(node) {
|
|
231
|
+
return node.parameters.map((p) => oneLineDeclaration(p.getText(), 80)).join(', ')
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* 返回类型只在**有信息量**时落进文本:`any`(无注解 JS 的常态)、`unknown`(未载 lib 时 async
|
|
236
|
+
* 的推导结果)、`{}`("任意非空值")都只会把一行声明撑长,旧实现无条件写 `): any`。
|
|
237
|
+
* `void` 保留 —— 它至少说明"没有返回值"。
|
|
238
|
+
*/
|
|
239
|
+
function renderReturn(type) {
|
|
240
|
+
const s = getTypeStr(type)
|
|
241
|
+
return s === 'any' || s === 'unknown' || s === '{}' ? '' : `: ${s}`
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 函数表达式/箭头函数的名字。旧白名单只有 `const x = fn` / `{ m: fn }` / `class { m = fn }`,
|
|
246
|
+
* 于是 CJS 最常见的 `module.exports.foo = function () {}` 产不出符号(它的父节点是
|
|
247
|
+
* BinaryExpression)—— 而 isTypeScriptFile() 明确把 `.cjs` 列为可增强对象,等于承诺了拿不到。
|
|
248
|
+
*/
|
|
249
|
+
function callableName(node) {
|
|
250
|
+
const parent = node.parent
|
|
251
|
+
if (!parent) return null
|
|
252
|
+
if (ts.isVariableDeclaration(parent) || ts.isPropertyAssignment(parent) || ts.isPropertyDeclaration(parent)) {
|
|
253
|
+
return parent.name?.getText() || '(anonymous)'
|
|
254
|
+
}
|
|
255
|
+
if (ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
256
|
+
const left = parent.left
|
|
257
|
+
if (ts.isPropertyAccessExpression(left)) {
|
|
258
|
+
// `module.exports = function () {}`(CJS 的默认导出):只取最后一段会得到名字
|
|
259
|
+
// "exports",既看不出它是默认导出,也容易与 `exports.foo` 混淆。保留全名。
|
|
260
|
+
if (ts.isIdentifier(left.expression) && left.expression.text === 'module' && left.name.text === 'exports') {
|
|
261
|
+
return 'module.exports'
|
|
262
|
+
}
|
|
263
|
+
return left.name.getText()
|
|
264
|
+
}
|
|
265
|
+
if (ts.isIdentifier(left)) return left.getText()
|
|
266
|
+
if (ts.isElementAccessExpression(left) && ts.isStringLiteralLike(left.argumentExpression)) {
|
|
267
|
+
return left.argumentExpression.text
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return null
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** 三个 callable 分支共用:名字与种类由调用方给,类型串的渲染只此一处。 */
|
|
274
|
+
function pushCallable(node, name, kind) {
|
|
275
|
+
const typeParams = node.typeParameters?.map((tp) => tp.getText()) || []
|
|
276
|
+
const generics = typeParams.length ? `<${typeParams.join(', ')}>` : ''
|
|
277
|
+
symbols.push({
|
|
278
|
+
name,
|
|
279
|
+
kind,
|
|
280
|
+
typeSig: `${generics}(${renderParams(node)})${renderReturn(getReturnType(getSignature(node)))}`,
|
|
281
|
+
line: getLine(node)
|
|
282
|
+
})
|
|
283
|
+
}
|
|
284
|
+
|
|
174
285
|
function visit(node) {
|
|
175
286
|
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
176
|
-
|
|
177
|
-
const returnType = getReturnType(signature)
|
|
178
|
-
const typeParams = node.typeParameters?.map(tp => tp.getText()) || []
|
|
179
|
-
const params = node.parameters.map(p => {
|
|
180
|
-
const type = p.type ? getTypeStr(checker.getTypeAtLocation(p.type)) : 'any'
|
|
181
|
-
return `${p.name.getText()}: ${type}`
|
|
182
|
-
})
|
|
183
|
-
const returnTypeStr = getTypeStr(returnType)
|
|
184
|
-
const generics = typeParams.length ? `<${typeParams.join(', ')}>` : ''
|
|
185
|
-
symbols.push({
|
|
186
|
-
name: node.name.getText(),
|
|
187
|
-
kind: 'function',
|
|
188
|
-
typeSig: `${generics}(${params.join(', ')}): ${returnTypeStr}`,
|
|
189
|
-
line: getLine(node)
|
|
190
|
-
})
|
|
287
|
+
pushCallable(node, node.name.getText(), 'function')
|
|
191
288
|
} else if (ts.isMethodDeclaration(node) && node.name) {
|
|
192
|
-
|
|
193
|
-
const returnType = getReturnType(signature)
|
|
194
|
-
const typeParams = node.typeParameters?.map(tp => tp.getText()) || []
|
|
195
|
-
const params = node.parameters.map(p => {
|
|
196
|
-
const type = p.type ? getTypeStr(checker.getTypeAtLocation(p.type)) : 'any'
|
|
197
|
-
return `${p.name.getText()}: ${type}`
|
|
198
|
-
})
|
|
199
|
-
const returnTypeStr = getTypeStr(returnType)
|
|
200
|
-
const generics = typeParams.length ? `<${typeParams.join(', ')}>` : ''
|
|
201
|
-
symbols.push({
|
|
202
|
-
name: node.name.getText(),
|
|
203
|
-
kind: 'method',
|
|
204
|
-
typeSig: `${generics}(${params.join(', ')}): ${returnTypeStr}`,
|
|
205
|
-
line: getLine(node)
|
|
206
|
-
})
|
|
289
|
+
pushCallable(node, node.name.getText(), 'method')
|
|
207
290
|
} else if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
|
|
208
|
-
const
|
|
209
|
-
if (
|
|
210
|
-
const signature = getSignature(node)
|
|
211
|
-
const returnType = getReturnType(signature)
|
|
212
|
-
const typeParams = node.typeParameters?.map(tp => tp.getText()) || []
|
|
213
|
-
const params = node.parameters.map(p => {
|
|
214
|
-
const type = p.type ? getTypeStr(checker.getTypeAtLocation(p.type)) : 'any'
|
|
215
|
-
return `${p.name.getText()}: ${type}`
|
|
216
|
-
})
|
|
217
|
-
const returnTypeStr = getTypeStr(returnType)
|
|
218
|
-
const generics = typeParams.length ? `<${typeParams.join(', ')}>` : ''
|
|
219
|
-
const name = parent.name?.getText() || '(anonymous)'
|
|
220
|
-
const kind = ts.isArrowFunction(node) ? 'arrow' : 'function'
|
|
221
|
-
symbols.push({
|
|
222
|
-
name,
|
|
223
|
-
kind,
|
|
224
|
-
typeSig: `${generics}(${params.join(', ')}): ${returnTypeStr}`,
|
|
225
|
-
line: getLine(node)
|
|
226
|
-
})
|
|
227
|
-
}
|
|
291
|
+
const name = callableName(node)
|
|
292
|
+
if (name) pushCallable(node, name, ts.isArrowFunction(node) ? 'arrow' : 'function')
|
|
228
293
|
} else if (ts.isClassDeclaration(node) && node.name) {
|
|
229
294
|
const typeParams = node.typeParameters?.map(tp => tp.getText()) || []
|
|
230
295
|
const generics = typeParams.length ? `<${typeParams.join(', ')}>` : ''
|
|
@@ -266,10 +331,9 @@ export function deepParseWithTS(filePath, content) {
|
|
|
266
331
|
}
|
|
267
332
|
}
|
|
268
333
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
334
|
+
// 用预建的那个(上面已按对象身份确认它进了 program),不再按名字回查——
|
|
335
|
+
// 回查依赖 TS 内部的命名,正是 Windows 上出问题的那一步。
|
|
336
|
+
visit(sourceFile)
|
|
273
337
|
return symbols
|
|
274
338
|
}
|
|
275
339
|
|
|
@@ -280,7 +344,10 @@ export function enqueueEnhance(store, relPath, filePath, priority = PRIORITY.BAT
|
|
|
280
344
|
try {
|
|
281
345
|
content = readFileSync(filePath, 'utf8')
|
|
282
346
|
} catch {
|
|
283
|
-
//
|
|
347
|
+
// 文件在"检测到变化"和"入队"之间消失了(编辑器原子保存 = 写临时文件 + rename,正好落在
|
|
348
|
+
// 这个窗口)。**刻意不写终端**:这不是故障而是常态,watch 每 15s 一轮还会反复重试同一个
|
|
349
|
+
// 文件 —— 即使按文件去重也仍会刷屏,而"这轮不增强"不会产出任何错误结果(返回 resolved,
|
|
350
|
+
// 与成功路径对调用方无差别)。诊断靠断言,不靠日志。
|
|
284
351
|
return Promise.resolve()
|
|
285
352
|
}
|
|
286
353
|
const cacheKey = getCacheKey(content)
|
|
@@ -303,7 +370,23 @@ export function enqueueEnhance(store, relPath, filePath, priority = PRIORITY.BAT
|
|
|
303
370
|
}
|
|
304
371
|
}
|
|
305
372
|
|
|
306
|
-
|
|
373
|
+
// 先登记队列条目,再启动任务。
|
|
374
|
+
//
|
|
375
|
+
// 任务体可能在**同步阶段**就跑完:`deepParseWithTS` 是同步的,没有增强结果时根本走不到
|
|
376
|
+
// 那个 `await`,于是 `finally` 立刻执行。旧实现在任务启动**之后**才登记条目,并在 finally
|
|
377
|
+
// 里用 `enhanceQueue.findIndex(q => q.promise === p)` 反查自己 ——
|
|
378
|
+
// · 队列为空时 `findIndex` 的回调根本不会被调用,`p` 不被求值,看不出问题;
|
|
379
|
+
// · 队列非空(真实的 watch 轮询里必然如此)时回调被调用,读到尚未初始化的 `const p`
|
|
380
|
+
// → `ReferenceError: Cannot access 'p' before initialization`。
|
|
381
|
+
// `scheduleProcess` 会 await 这个 promise,但 watch 路径上抛出的那一个没人接,未处理的
|
|
382
|
+
// rejection 直接把宿主打成 `dsh: fatal load failure`。
|
|
383
|
+
//
|
|
384
|
+
// 现在:条目先入队,finally 按**对象身份**摘除自己,完全不引用尚未初始化的绑定。
|
|
385
|
+
const entry = { relPath, filePath, priority, cacheKey, promise: null }
|
|
386
|
+
if (existingIdx >= 0) enhanceQueue[existingIdx] = entry
|
|
387
|
+
else enhanceQueue.push(entry)
|
|
388
|
+
|
|
389
|
+
entry.promise = (async () => {
|
|
307
390
|
try {
|
|
308
391
|
const content = readFileSync(filePath, 'utf8')
|
|
309
392
|
const enhanced = deepParseWithTS(filePath, content)
|
|
@@ -313,23 +396,15 @@ export function enqueueEnhance(store, relPath, filePath, priority = PRIORITY.BAT
|
|
|
313
396
|
} catch (err) {
|
|
314
397
|
console.warn(`[dsh-project-memory] enhance failed for ${relPath}: ${err.message}`)
|
|
315
398
|
} finally {
|
|
316
|
-
//
|
|
317
|
-
const idx = enhanceQueue.
|
|
399
|
+
// 只摘自己那一条:同 relPath 的新任务可能已经把它替换掉了(那种情况这里是 -1)。
|
|
400
|
+
const idx = enhanceQueue.indexOf(entry)
|
|
318
401
|
if (idx >= 0) enhanceQueue.splice(idx, 1)
|
|
319
402
|
}
|
|
320
403
|
})()
|
|
321
|
-
|
|
322
|
-
// Update or add to queue
|
|
323
|
-
const existingIdx2 = enhanceQueue.findIndex(q => q.relPath === relPath)
|
|
324
|
-
if (existingIdx2 >= 0) {
|
|
325
|
-
enhanceQueue[existingIdx2] = { relPath, filePath, priority, cacheKey, promise: p }
|
|
326
|
-
} else {
|
|
327
|
-
enhanceQueue.push({ relPath, filePath, priority, cacheKey, promise: p })
|
|
328
|
-
}
|
|
329
404
|
enhanceQueue.sort((a, b) => a.priority - b.priority)
|
|
330
405
|
|
|
331
406
|
scheduleProcess()
|
|
332
|
-
return
|
|
407
|
+
return entry.promise
|
|
333
408
|
}
|
|
334
409
|
|
|
335
410
|
function scheduleProcess() {
|
|
@@ -368,7 +443,9 @@ function applyEnhancedSymbols(fn, relPath, enhanced) {
|
|
|
368
443
|
...e,
|
|
369
444
|
// 一行声明(限长)。typeSig 只是构建期的中间量,不落进 entry:它没有读取方,
|
|
370
445
|
// 且 interface 的 typeSig 就是整个类型体,是符号条目变胖的主因。
|
|
371
|
-
|
|
446
|
+
// 分隔符与 L1 的 buildSymbol 保持一致(` — `):L2 是**就地重写** L1 文本,
|
|
447
|
+
// 用不同的分隔符会让同一个文件里的条目混排两种格式。
|
|
448
|
+
text: oneLineDeclaration(`${enh.name}${enh.typeSig} — ${relPath}:${enh.line}`),
|
|
372
449
|
typeSig: undefined,
|
|
373
450
|
enhanced: true
|
|
374
451
|
}
|
|
@@ -392,7 +469,7 @@ function applyEnhancedSymbols(fn, relPath, enhanced) {
|
|
|
392
469
|
type: 'symbol',
|
|
393
470
|
title: `${s.name} (${s.kind})`,
|
|
394
471
|
keywords: [s.name, s.kind],
|
|
395
|
-
text: oneLineDeclaration(`${s.name}${s.typeSig}
|
|
472
|
+
text: oneLineDeclaration(`${s.name}${s.typeSig} — ${relPath}:${s.line}`),
|
|
396
473
|
enhanced: true
|
|
397
474
|
})
|
|
398
475
|
}
|
|
@@ -408,17 +485,24 @@ function applyEnhancedSymbols(fn, relPath, enhanced) {
|
|
|
408
485
|
}
|
|
409
486
|
}
|
|
410
487
|
|
|
488
|
+
// 三个入口都不 await 任务,所以必须各自挂一个 catch:任务体内部已经兜底,但
|
|
489
|
+
// **未处理的 rejection 会把整个宿主打成 `dsh: fatal load failure`**(2026-09-26 的事故就是
|
|
490
|
+
// 这个形状:一个 TDZ 从 watch 路径逃出去,dsh web 起不来)。这条路径不该有任何机会向上抛。
|
|
491
|
+
const fireAndForget = (p) => {
|
|
492
|
+
if (p && typeof p.catch === 'function') p.catch(() => {})
|
|
493
|
+
}
|
|
494
|
+
|
|
411
495
|
export function onFileObserved(store, relPath, filePath, config, root) {
|
|
412
496
|
if (!isTypeScriptFile(filePath)) return
|
|
413
|
-
enqueueEnhance(store, relPath, filePath, PRIORITY.ACTIVE, config, root)
|
|
497
|
+
fireAndForget(enqueueEnhance(store, relPath, filePath, PRIORITY.ACTIVE, config, root))
|
|
414
498
|
}
|
|
415
499
|
|
|
416
500
|
export function onFileChanged(store, relPath, filePath, config, root) {
|
|
417
501
|
if (!isTypeScriptFile(filePath)) return
|
|
418
|
-
enqueueEnhance(store, relPath, filePath, PRIORITY.RECENT, config, root)
|
|
502
|
+
fireAndForget(enqueueEnhance(store, relPath, filePath, PRIORITY.RECENT, config, root))
|
|
419
503
|
}
|
|
420
504
|
|
|
421
505
|
export function onFileIndexed(store, relPath, filePath, config, root) {
|
|
422
506
|
if (!isTypeScriptFile(filePath)) return
|
|
423
|
-
enqueueEnhance(store, relPath, filePath, PRIORITY.BATCH, config, root)
|
|
507
|
+
fireAndForget(enqueueEnhance(store, relPath, filePath, PRIORITY.BATCH, config, root))
|
|
424
508
|
}
|
package/src/llm.js
CHANGED
|
@@ -36,7 +36,10 @@ export async function chatText(llm, system, user, { timeoutMs = 120000, route }
|
|
|
36
36
|
} finally {
|
|
37
37
|
clearTimeout(timer)
|
|
38
38
|
}
|
|
39
|
-
|
|
39
|
+
// 0.2.0 起 BlockAssembler.message(source) 的 source 是**必填**(Omit<ModelMessageSource,'kind'>);
|
|
40
|
+
// 不传在运行时仍能跑(createAssistantMessage 只做 `{kind:'model', ...source}`),但得到的
|
|
41
|
+
// assistant 消息会缺 provider/model 归属。我们本来就知道路由,直接如实带上。
|
|
42
|
+
return textOf(assembler.message({ provider: route.provider, model: route.model }))
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
export function parseStructuredJson(text) {
|