@a9i5k4/dsh-auto-memory 3.0.0 → 3.1.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/README.md +30 -13
- package/README.zh-CN.md +30 -13
- package/docs/FRONTEND-CO-CREATION.md +191 -0
- package/docs/GM53-HOMEPAGE-PROMPT.md +323 -0
- package/docs/HANDBOOK.md +88 -52
- package/docs/HOMEPAGE-CONTENT-FOR-GM53.md +299 -0
- package/docs/PROMO-PROMPT-3.0.md +100 -0
- package/docs/USER-GUIDE.en.md +11 -11
- package/docs/USER-GUIDE.zh-CN.md +11 -11
- package/docs/WHITEPAPER.md +207 -0
- package/docs/screenshots/promo/promo-0-banner-v3.png +0 -0
- package/docs/screenshots/promo/promo-0-banner-v4.png +0 -0
- package/docs/screenshots/promo/promo-1b-auto-recall.png +0 -0
- package/lib/activation-host.js +69 -10
- package/lib/board-mode.js +1 -1
- package/lib/client.js +1697 -285
- package/lib/config-io.js +156 -0
- package/lib/context-bridge.js +3 -0
- package/lib/context-host.js +23 -10
- package/lib/degrade.js +385 -0
- package/lib/dsh-home.js +143 -0
- package/lib/episodic-store.js +142 -18
- package/lib/evidence-store.js +8 -1
- package/lib/fact-store.js +484 -43
- package/lib/hub-io.js +217 -0
- package/lib/index-sync.js +13 -1
- package/lib/index.js +1730 -202
- package/lib/intent-clean-safe.js +258 -40
- package/lib/l0-extract.js +231 -16
- package/lib/m4-corpus.js +8 -2
- package/lib/m7-index-sync-host.js +8 -1
- package/lib/memory-envelope.js +6 -1
- package/lib/memory-hub.js +164 -17
- package/lib/memory-index.js +4 -2
- package/lib/note-status-apply.js +118 -0
- package/lib/note-status.js +204 -0
- package/lib/procedure-store.js +333 -31
- package/lib/procedure-switch.js +38 -0
- package/lib/python-sidecar-client.js +314 -11
- package/lib/recall-fusion.js +83 -12
- package/lib/rules-edit.js +159 -0
- package/lib/semantic-decide.js +41 -8
- package/lib/semantic-js.js +51 -6
- package/lib/shadow-host.js +3 -5
- package/lib/skill-export-host.js +153 -0
- package/lib/skill-export.js +239 -0
- package/lib/storage-manage.js +6 -0
- package/lib/temporal-parse.js +191 -159
- package/lib/tier0-catalog.js +45 -3
- package/lib/wb-contract.js +198 -2
- package/lib/wb-sidecar.js +54 -3
- package/package.json +6 -2
- package/docs/internal/ACCEPT-35-LIVE.md +0 -143
- package/docs/internal/ACCEPTANCE-20260914.md +0 -90
- package/docs/internal/ARCH-REVIEW-BRIEF.md +0 -411
- package/docs/internal/ARCH-REVIEW-REQUEST.md +0 -201
- package/docs/internal/ARCH-REVIEW-ROUND2.md +0 -169
- package/docs/internal/ARCH-REVIEW-ROUND3.md +0 -206
- package/docs/internal/ART-DIRECTION-WIREFRAME.md +0 -181
- package/docs/internal/AUDIT-WB-GRAPH-FULL-20260916.md +0 -314
- package/docs/internal/CONCURRENCY-INVESTIGATION-20260917.md +0 -192
- package/docs/internal/CROSS-SESSION-SEARCH-PATH-DECISION.md +0 -72
- package/docs/internal/CROSS-SESSION-SEARCH-RESEARCH.md +0 -131
- package/docs/internal/CUA-VISION-FIX-NOTES.md +0 -78
- package/docs/internal/DECISIONS-20260914-SESSION.md +0 -269
- package/docs/internal/DESIGN-OVERHAUL-PRE-RESEARCH.md +0 -292
- package/docs/internal/DESIGN-P1-STATE-COMMIT-20260915.md +0 -219
- package/docs/internal/DIRECTION-CHECK-WB-GRAPH-20260916.md +0 -132
- package/docs/internal/FEEDBACK-TO-DSHAPI-RELAY.md +0 -13
- package/docs/internal/GH-DISCUSSION-5732-COMMENT.md +0 -74
- package/docs/internal/GPT-ACCEPTANCE-PROMPT-20260916.md +0 -352
- package/docs/internal/GPT-REVIEW-PROMPT.md +0 -216
- package/docs/internal/GROUP-DIGEST-SETUP.md +0 -62
- package/docs/internal/GROUP-LISTENER-SETUP.md +0 -49
- package/docs/internal/GROUP-WEBHOOK-SETUP.md +0 -93
- package/docs/internal/HANDOFF-TO-ZCODE.md +0 -168
- package/docs/internal/KICKOFF-P0.md +0 -254
- package/docs/internal/MASTER-PLAN-3.0.md +0 -411
- package/docs/internal/MEMORY-MUTATION-AND-INDEX-DESIGN.md +0 -85
- package/docs/internal/MERGE-CONFLICT-SCAN-20260914.md +0 -222
- package/docs/internal/NEXT-VERSION-TODO.md +0 -95
- package/docs/internal/OFFICIAL-DISCUSSION-DRAFT.md +0 -80
- package/docs/internal/PENDING-FIXES-20260916.md +0 -289
- package/docs/internal/RAG-KARPATHY-PROGRAM.md +0 -229
- package/docs/internal/RELEASE-PROCESS.md +0 -99
- package/docs/internal/REPORT-P0-NIGHTLY.md +0 -212
- package/docs/internal/REPORT-P5-ACCEPTANCE.md +0 -31
- package/docs/internal/REPORT-WB-GRAPH-NIGHTLY.md +0 -153
- package/docs/internal/REVIEW-WB-GRAPH-SELF.md +0 -81
- package/docs/internal/ROADMAP-20260917-WEEK.md +0 -305
- package/docs/internal/ROADMAP.md +0 -106
- package/docs/internal/RUN-P0-NIGHTLY.md +0 -227
- package/docs/internal/S10-CONSTRUCTION-HANDOFF-20260917.md +0 -175
- package/docs/internal/S10-GAPS-PLAIN-20260917.md +0 -125
- package/docs/internal/SEMANTIC-ARCHITECTURE-SPEC.md +0 -360
- package/docs/internal/SESSION-FILE-REPAIR-PROTOCOL.md +0 -90
- package/docs/internal/SUBAGENT-REPORT-ROUTING-PRE-RESEARCH.md +0 -261
- package/docs/internal/THREE-LAYER-CONTRACT.md +0 -210
- package/docs/internal/TODO-BACKLOG.md +0 -263
- package/docs/internal/TODO-GRAPH.html +0 -715
- package/docs/internal/TODO-GRAPH.html.bak-20260914-v2 +0 -493
- package/docs/internal/TODO-GRAPH.html.bak-20260915-alsfix +0 -710
- package/docs/internal/TODO-GRAPH.html.bak-20260915-p1 +0 -710
- package/docs/internal/TODO-GRAPH.html.bak-20260915-p6a-rev +0 -703
- package/docs/internal/TODO-GRAPH.html.bak-20260915-wshint +0 -710
- package/docs/internal/TODO-GRAPH.html.bak-20260916-batch +0 -715
- package/docs/internal/WB-FORMAT-CONVENTION.md +0 -112
- package/docs/internal/WB-GRAPH-DECISIONS-20260914.md +0 -71
- package/docs/internal/WB-GRAPH-INTEGRATION-PLAN.md +0 -386
- package/docs/internal/WB-GRAPH-RESEARCH-BRIEF.md +0 -118
- package/docs/internal/WB-GRAPH-RESEARCH-EXTERNAL.md +0 -228
- package/docs/internal/WB-GRAPH-RESEARCH-LOCAL.md +0 -190
- package/docs/internal/reviews/CLAIM-VERIFICATION-20260914.md +0 -56
- package/docs/internal/reviews/PLAN-gpt6astra-round2-20260914.md +0 -787
- package/docs/internal/reviews/REVIEW-gpt6astra-20260914.md +0 -112
- package/docs/internal/reviews/ROUND3-REVIEW-INTEGRATION-20260914.md +0 -230
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rules-edit.js —— 「用户级硬性约束」的**条目级**解析与增删改(R7 需求)。
|
|
3
|
+
*
|
|
4
|
+
* ## 背景(用户 2026-09-20 明确要求「这个功能必须要落在前端」)
|
|
5
|
+
*
|
|
6
|
+
* `[规则 — 用户级硬性约束 · 必须遵守]` 这一段是**每轮无条件注入**的硬约束,
|
|
7
|
+
* 真源 = `~/.dsh/memory/MEMORY.md`(见 `rules-layer.js:12` 的真源纪律)。
|
|
8
|
+
* 它**不走语义层** —— 即没有检索/打分/淘汰环节 ⇒ **过时条目不会被自动清理**;
|
|
9
|
+
* 而 AI(`memory_user`)也会往里写错东西。
|
|
10
|
+
* ⇒ 用户必须能**自己增/删/改**,否则错一条就每轮都被误导。
|
|
11
|
+
*
|
|
12
|
+
* ## 与既有渲染层的关系(重要:本模块不改变注入语义)
|
|
13
|
+
*
|
|
14
|
+
* `rules-layer.js` 的 `extractRulesLayerPre()` 负责**读**(把 MEMORY.md 解析成规则段),
|
|
15
|
+
* `renderRulesSectionPre()` 负责**渲染**。本模块只做**编辑定位**:
|
|
16
|
+
* 给定原文,找出「哪一段是规则段、每条规则在第几行」,以及产出改后的新全文。
|
|
17
|
+
* 它**不碰**注入、不碰检索、不碰 store。
|
|
18
|
+
*
|
|
19
|
+
* ## 条目模型
|
|
20
|
+
*
|
|
21
|
+
* 用户级 MEMORY.md 由若干 `## YYYY-MM-DD` 日期段 + `- ` 条目行组成。
|
|
22
|
+
* `rules-layer.js` 的渲染器只取 `- ` 开头的行(且跳过日期段)。
|
|
23
|
+
* ⇒ **一条规则 = 一个 `- ` 行**。本模块据此提供:
|
|
24
|
+
* - `listRuleItemsPre(text)` → [{ line, indent, text, source }]
|
|
25
|
+
* - `updateRuleItemPre(text, i, next)` / `removeRuleItemPre(text, i)` / `appendRuleItemPre(text, body)`
|
|
26
|
+
*
|
|
27
|
+
* `source` 的判定:条目若落在 `## YYYY-MM-DD` 段内 ⇒ 归该日期(多为 AI 写入);
|
|
28
|
+
* 落在段外(文件头部)⇒ 记为 `user`(多为用户手写)。这是 R7-3「标注来源」的依据。
|
|
29
|
+
*
|
|
30
|
+
* ## 纪律
|
|
31
|
+
* - 纯函数:输入文本、输出文本,**不做任何 IO**(IO 由调用方走既有 writeFull 事务)。
|
|
32
|
+
* - **删除 = 真删**(见 R7-4):本层渲染器不认任何状态标记,软删标记会被当正文注入模型。
|
|
33
|
+
* - 零运行时依赖。CRLF、无 BOM。
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** 日期段标题(与 rules-layer.js 的 DATE_SECTION_RE 同口径)。 */
|
|
37
|
+
export const RULE_DATE_SECTION_RE_V1 = /^##\s*\d{4}-\d{2}-\d{2}\s*$/
|
|
38
|
+
|
|
39
|
+
/** 条目行:允许前导缩进 + `- `(渲染器同样只看 `- ` 行)。 */
|
|
40
|
+
export const RULE_ITEM_RE_V1 = /^(\s*)-\s+(.*)$/
|
|
41
|
+
|
|
42
|
+
/** 单条规则长度上限(与写闸门同量级,防止一次塞进整篇文档)。 */
|
|
43
|
+
export const RULE_ITEM_MAX_CHARS_V1 = 2000
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 列出所有规则条目(**行号从 0 开始**,供后续增删改定位)。
|
|
47
|
+
*
|
|
48
|
+
* @param {string} text MEMORY.md 全文
|
|
49
|
+
* @returns {Array<{line:number, indent:string, text:string, source:string, dateSection:string|null}>}
|
|
50
|
+
*/
|
|
51
|
+
export function listRuleItemsPre(text) {
|
|
52
|
+
const lines = String(text == null ? '' : text).split(/\r?\n/)
|
|
53
|
+
const out = []
|
|
54
|
+
let curDate = null
|
|
55
|
+
for (let i = 0; i < lines.length; i++) {
|
|
56
|
+
const l = lines[i]
|
|
57
|
+
if (RULE_DATE_SECTION_RE_V1.test(l)) { curDate = l.replace(/^##\s*/, '').trim(); continue }
|
|
58
|
+
const m = RULE_ITEM_RE_V1.exec(l)
|
|
59
|
+
if (!m) continue
|
|
60
|
+
const body = m[2].trim()
|
|
61
|
+
if (!body) continue
|
|
62
|
+
out.push({
|
|
63
|
+
line: i,
|
|
64
|
+
indent: m[1] || '',
|
|
65
|
+
text: body,
|
|
66
|
+
// 落在日期段内 ⇒ 多为 AI(memory_user 会带日期标题)写入;否则视为用户手写
|
|
67
|
+
source: curDate ? 'ai' : 'user',
|
|
68
|
+
dateSection: curDate,
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
return out
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 内部:切行(保留原始行尾风格判定所需的信息交给 join 处理)。 */
|
|
75
|
+
function splitLinesPre(text) { return String(text == null ? '' : text).split(/\r?\n/) }
|
|
76
|
+
|
|
77
|
+
function joinLinesPre(lines) { return lines.join('\n') }
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* **替换**第 idx 条规则(按 listRuleItemsPre 的顺序)。
|
|
81
|
+
*
|
|
82
|
+
* @returns {{ok:boolean, text?:string, error?:string}}
|
|
83
|
+
*/
|
|
84
|
+
export function updateRuleItemPre(text, idx, nextBody) {
|
|
85
|
+
const items = listRuleItemsPre(text)
|
|
86
|
+
const body = String(nextBody == null ? '' : nextBody).trim()
|
|
87
|
+
if (!Number.isInteger(idx) || idx < 0 || idx >= items.length) return { ok: false, error: 'index-out-of-range' }
|
|
88
|
+
if (!body) return { ok: false, error: 'empty-body' }
|
|
89
|
+
if (body.length > RULE_ITEM_MAX_CHARS_V1) return { ok: false, error: 'too-long' }
|
|
90
|
+
if (/\r|\n/.test(body)) return { ok: false, error: 'multiline-not-allowed' }
|
|
91
|
+
const lines = splitLinesPre(text)
|
|
92
|
+
const it = items[idx]
|
|
93
|
+
lines[it.line] = it.indent + '- ' + body
|
|
94
|
+
return { ok: true, text: joinLinesPre(lines) }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* **删除**第 idx 条规则(真删,见 R7-4)。
|
|
99
|
+
*
|
|
100
|
+
* ⚠️ 本层渲染器(`renderRulesSectionPre`)不认任何状态标记 ⇒ 软删标记会被当正文注入模型;
|
|
101
|
+
* 故这里**直接移除该行**。不可撤销 —— 调用方必须做二次确认。
|
|
102
|
+
*/
|
|
103
|
+
export function removeRuleItemPre(text, idx) {
|
|
104
|
+
const items = listRuleItemsPre(text)
|
|
105
|
+
if (!Number.isInteger(idx) || idx < 0 || idx >= items.length) return { ok: false, error: 'index-out-of-range' }
|
|
106
|
+
const lines = splitLinesPre(text)
|
|
107
|
+
lines.splice(items[idx].line, 1)
|
|
108
|
+
return { ok: true, text: joinLinesPre(lines) }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* **追加**一条规则。
|
|
113
|
+
*
|
|
114
|
+
* 落点规则(与用户手写习惯一致,且保证渲染器一定能读到):
|
|
115
|
+
* - `dateSection` 为空 ⇒ 追加到**文件头部**(用户手写区,`source` 会是 `user`);
|
|
116
|
+
* - 给了 `dateSection` ⇒ 追加到该日期段的**末尾**(AI 写入区)。
|
|
117
|
+
*/
|
|
118
|
+
export function appendRuleItemPre(text, body, opts = {}) {
|
|
119
|
+
const b = String(body == null ? '' : body).trim()
|
|
120
|
+
if (!b) return { ok: false, error: 'empty-body' }
|
|
121
|
+
if (b.length > RULE_ITEM_MAX_CHARS_V1) return { ok: false, error: 'too-long' }
|
|
122
|
+
if (/\r|\n/.test(b)) return { ok: false, error: 'multiline-not-allowed' }
|
|
123
|
+
const lines = splitLinesPre(text)
|
|
124
|
+
const want = opts && opts.dateSection ? String(opts.dateSection).trim() : ''
|
|
125
|
+
const newLine = '- ' + b
|
|
126
|
+
if (!want) {
|
|
127
|
+
// 头部插入:跳过开头的空行,避免把条目顶到文件最上面的空行之前
|
|
128
|
+
let at = 0
|
|
129
|
+
while (at < lines.length && lines[at].trim() === '') at++
|
|
130
|
+
lines.splice(at, 0, newLine)
|
|
131
|
+
return { ok: true, text: joinLinesPre(lines) }
|
|
132
|
+
}
|
|
133
|
+
// 找目标日期段,插到该段内最后一个非空行之后
|
|
134
|
+
let secAt = -1
|
|
135
|
+
for (let i = 0; i < lines.length; i++) {
|
|
136
|
+
if (RULE_DATE_SECTION_RE_V1.test(lines[i]) && lines[i].replace(/^##\s*/, '').trim() === want) { secAt = i; break }
|
|
137
|
+
}
|
|
138
|
+
if (secAt < 0) return { ok: false, error: 'date-section-not-found' }
|
|
139
|
+
let end = secAt + 1
|
|
140
|
+
while (end < lines.length && !RULE_DATE_SECTION_RE_V1.test(lines[end])) end++
|
|
141
|
+
let ins = end
|
|
142
|
+
while (ins > secAt + 1 && lines[ins - 1].trim() === '') ins--
|
|
143
|
+
lines.splice(ins, 0, newLine)
|
|
144
|
+
return { ok: true, text: joinLinesPre(lines) }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* **预览**:把规则条目渲染成注入时会长的样子(R7-6)。
|
|
149
|
+
*
|
|
150
|
+
* 只做**只读**格式化,不引入 rules-layer.js 的其它依赖(避免循环 import 风险)。
|
|
151
|
+
*/
|
|
152
|
+
export function previewRulesPre(text) {
|
|
153
|
+
const items = listRuleItemsPre(text)
|
|
154
|
+
return items.map((x) => '- ' + x.text).join('\n')
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** 空规则段判定(给 UI 显示空态用)。 */
|
|
158
|
+
export function isEmptyRulesPre(text) { return listRuleItemsPre(text).length === 0 }
|
|
159
|
+
|
package/lib/semantic-decide.js
CHANGED
|
@@ -27,16 +27,49 @@ const PLAN_TOKENS = ['准备', '打算', '计划', '之后', '接下来', '继
|
|
|
27
27
|
const WS_RUN = /\s\s+/g
|
|
28
28
|
// Python `(?u)\b\w\w+\b` 中 \w 含 CJK;JS 的 \w 默认只含 ASCII,须显式加 CJK
|
|
29
29
|
// 否则中文词不被切分 → 无 gram → 中文意图全靠 intercept(严重偏差)。
|
|
30
|
-
|
|
30
|
+
// ★ issue #68 修复(2026-09-19):**加 `{2,}` 下限** —— Python 侧是 `\b\w\w+\b`(**≥2 字**),
|
|
31
|
+
// 旧实现允许 1 字词(`+`)⇒ 中文单字(如「好」「是」)在 JS 侧会生成 gram、Python 侧不会,
|
|
32
|
+
// 两版对**同一输入**给出不同 gram 集合 ⇒ 判定分叉。
|
|
33
|
+
const WORD_RE = /[A-Za-z0-9_\u4e00-\u9fff]{2,}/g
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 与 Python `normalize_text` 逐字对齐。
|
|
37
|
+
* ★ issue #68 修复(2026-09-19):旧实现有三处分叉,现全部按 **Python 为权威** 对齐:
|
|
38
|
+
* ① **整删非字母数字**:旧实现 `.replace(/[^…\s]/g, ' ')` 把标点变**空格** ⇒ `"a-b"` 得到 `"a b"`(两词),
|
|
39
|
+
* 而 Python 是 `''.join(ch for ch in text.lower() if ch.isalnum() or CJK)` ⇒ `"ab"`(**一词**);
|
|
40
|
+
* ② **`isalnum()` 语义**:Python 的 `str.isalnum()` 对**全角数字**(`123`)、带音标字母(`é`)为真,
|
|
41
|
+
* 而 JS 正则 `[a-z0-9]` 只认 ASCII ⇒ 这类字符在 JS 侧被丢弃、Python 侧保留;
|
|
42
|
+
* ③ **不再额外压空白**:Python 整删后**不插空格**,故 `"a b"` → `"a b"`(原样)而非 `"a b"`。
|
|
43
|
+
* 修法用 `codePointAt` + `isAlnumPythonish()` 复刻 `isalnum()`,而不是改正则字符类。
|
|
44
|
+
*/
|
|
45
|
+
function isAlnumPythonish(cp) {
|
|
46
|
+
// ASCII 字母数字
|
|
47
|
+
if (cp >= 0x30 && cp <= 0x39) return true // 0-9
|
|
48
|
+
if (cp >= 0x61 && cp <= 0x7a) return true // a-z(已 lower)
|
|
49
|
+
if (cp >= 0x41 && cp <= 0x5a) return true // A-Z(防御:未 lower 时)
|
|
50
|
+
// CJK 统一表意文字(与既有 \u4e00-\u9fff 口径一致)
|
|
51
|
+
if (cp >= 0x4e00 && cp <= 0x9fff) return true
|
|
52
|
+
// 全角数字 0-9
|
|
53
|
+
if (cp >= 0xff10 && cp <= 0xff19) return true
|
|
54
|
+
// 全角字母 a-z / A-Z(★ 首轮遗漏:`toLowerCase()` 会把 A(U+FF21) 折叠成 a(U+FF41),
|
|
55
|
+
// 若只覆盖数字,全角字母会在折叠后**整段消失**,与 Python `isalnum()` 仍为真的语义分叉)
|
|
56
|
+
if (cp >= 0xff21 && cp <= 0xff3a) return true // A-Z(防御:未 lower 时)
|
|
57
|
+
if (cp >= 0xff41 && cp <= 0xff5a) return true // a-z(lower 后的实际落点)
|
|
58
|
+
// 拉丁补充字母(含 é 等带音标字符,覆盖 Latin-1 Supplement 的字母区)
|
|
59
|
+
if (cp >= 0x00c0 && cp <= 0x00ff && cp !== 0x00d7 && cp !== 0x00f7) return true
|
|
60
|
+
if (cp >= 0x0100 && cp <= 0x017f) return true // Latin Extended-A
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
31
63
|
|
|
32
|
-
/** 与 Python normalize_text 对齐(大小写折叠 + 保留 [a-z0-9]+CJK + 去其他)。 */
|
|
33
64
|
function normalizeText(text) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
.
|
|
39
|
-
|
|
65
|
+
const lower = String(text || '').toLowerCase()
|
|
66
|
+
let out = ''
|
|
67
|
+
// 按码点遍历(避免代理对拆断),复刻 Python 的 `if ch.isalnum()` 整删语义
|
|
68
|
+
for (const ch of lower) {
|
|
69
|
+
const cp = ch.codePointAt(0)
|
|
70
|
+
if (isAlnumPythonish(cp)) out += ch
|
|
71
|
+
}
|
|
72
|
+
return out
|
|
40
73
|
}
|
|
41
74
|
|
|
42
75
|
/** char_wb n-gram 计数(与 Python _char_wb_ngram_counts 对齐)。 */
|
package/lib/semantic-js.js
CHANGED
|
@@ -17,6 +17,7 @@ import { existsSync, readFileSync, mkdirSync, readdirSync, renameSync, statSync,
|
|
|
17
17
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
18
18
|
import { createRequire } from 'node:module'
|
|
19
19
|
import path from 'node:path'
|
|
20
|
+
import { resolveDshHomePre } from './dsh-home.js'
|
|
20
21
|
import { homedir } from 'node:os'
|
|
21
22
|
|
|
22
23
|
export const JS_SEMANTIC_ENGINE_VERSION = 'js_semantic_engine_v1'
|
|
@@ -70,7 +71,8 @@ const E5_MODELS_SUBDIR = 'multilingual-e5-small'
|
|
|
70
71
|
function defaultModelsDirCandidates(pluginDir) {
|
|
71
72
|
// 用户目录优先(#15 后续/B 修复):~/.dsh/models/js-semantic/ 跨插件升级存活——
|
|
72
73
|
// 包目录(lib/models)在 npm 更新时会被整体重装,下载的 130MB 模型曾被冲掉。
|
|
73
|
-
|
|
74
|
+
// ★#86-3:统一口径
|
|
75
|
+
const dshHome = resolveDshHomePre()
|
|
74
76
|
return [
|
|
75
77
|
path.join(dshHome, 'models', 'js-semantic'),
|
|
76
78
|
path.join(pluginDir, 'models'),
|
|
@@ -161,9 +163,20 @@ export function deepScanPeerTransformers(roots) {
|
|
|
161
163
|
* semanticAssetProbe 共享实现(index.js 的 status API / 档位解析 / 引导卡同源):
|
|
162
164
|
* 模型资产=发行包 lib/models 优先,其次开发树;peer=resolvePeerTransformersDir。
|
|
163
165
|
* extraDirs 透传给 peer 探测(深度扫描热接入位)。返回形状与 v0.1.36 完全一致。
|
|
166
|
+
*
|
|
167
|
+
* ★ issue #70 修复(2026-09-19):新增 `degraded` 字段。
|
|
168
|
+
* 旧实现是纯 `existsSync` 探测 —— **资产在磁盘上存在 ≠ 引擎可用**:
|
|
169
|
+
* 一旦运行期 `ensureTier()` 失败(onnx 损坏 / 维度不符 / peer 加载失败),引擎置 `degraded`
|
|
170
|
+
* 并此后恒抛;但探针只回 `ready = assetPresent && peerPresent` ⇒
|
|
171
|
+
* **引导卡显示「✓ 就绪」、档位解析给出 c2,而实际每次检索都在静默走词法兜底**(状态自相矛盾)。
|
|
172
|
+
* 现把降级状态并进 `ready`(并单列 `degraded` 供诊断),使三条用户可见路径(引导卡 / 档位 / status)一致。
|
|
173
|
+
* @param {string} pluginDir
|
|
174
|
+
* @param {string[]} [extraDirs]
|
|
175
|
+
* @param {string} [degradedReason] 当前引擎的降级原因(空串=未降级)
|
|
164
176
|
*/
|
|
165
|
-
export function probeJsSemanticAssets(pluginDir, extraDirs) {
|
|
166
|
-
|
|
177
|
+
export function probeJsSemanticAssets(pluginDir, extraDirs, degradedReason) {
|
|
178
|
+
// ★#86-3:统一口径
|
|
179
|
+
const dshHome = resolveDshHomePre()
|
|
167
180
|
const modelCands = [
|
|
168
181
|
path.join(dshHome, 'models', 'js-semantic', E5_MODELS_SUBDIR, 'onnx', 'model_quantized.onnx'),
|
|
169
182
|
path.join(pluginDir, 'models', E5_MODELS_SUBDIR, 'onnx', 'model_quantized.onnx'),
|
|
@@ -172,10 +185,15 @@ export function probeJsSemanticAssets(pluginDir, extraDirs) {
|
|
|
172
185
|
const modelOnnx = modelCands.find((c) => existsSync(c)) || modelCands[0]
|
|
173
186
|
const assetPresent = existsSync(modelOnnx)
|
|
174
187
|
const peerDir = resolvePeerTransformersDir(pluginDir, extraDirs)
|
|
188
|
+
const filesReady = Boolean(assetPresent && peerDir)
|
|
189
|
+
const degraded = String(degradedReason || '')
|
|
175
190
|
return {
|
|
176
191
|
assetPresent,
|
|
177
192
|
peerPresent: Boolean(peerDir),
|
|
178
|
-
|
|
193
|
+
// ★ 资产齐备**且未降级**才算就绪(否则 UI 报"就绪"而实际降级)
|
|
194
|
+
ready: filesReady && !degraded,
|
|
195
|
+
filesReady,
|
|
196
|
+
degraded,
|
|
179
197
|
assetBytes: assetPresent ? statSync(modelOnnx).size : 0,
|
|
180
198
|
assetPath: modelOnnx,
|
|
181
199
|
}
|
|
@@ -213,6 +231,8 @@ export function createJsSemanticEnginePre(opts = {}) {
|
|
|
213
231
|
let tier = null // {embedQuery, embedPassages, model}
|
|
214
232
|
let tierPromise = null
|
|
215
233
|
let degraded = '' // 非空 = 初始化失败原因(调用方回退词法)
|
|
234
|
+
let degradedAt = 0 // ★ issue #70:最近一次置位的时刻(用于有界自动重试的冷却判定)
|
|
235
|
+
let statsRetries = 0 // ★ issue #70:因冷却到期而清空降级态的累计次数(诊断用)
|
|
216
236
|
let lastRankError = '' // 运行期排名失败原因(诊断用;rank 恒返回 null 由调用方回退)
|
|
217
237
|
let idx = { miv: null, entries: [] } // entries [{memoryId, vec}]
|
|
218
238
|
let rebuilding = null // 单飞行重建 promise
|
|
@@ -223,7 +243,21 @@ export function createJsSemanticEnginePre(opts = {}) {
|
|
|
223
243
|
|
|
224
244
|
async function ensureTier() {
|
|
225
245
|
if (tier) return tier
|
|
226
|
-
|
|
246
|
+
// ★ issue #70 修复(2026-09-19):`degraded` 由**单向闩锁**改为**有界自动重试**。
|
|
247
|
+
// 旧行为:首次 `ensureTier()` 失败 ⇒ `degraded` 置位 ⇒ 此后恒抛,
|
|
248
|
+
// **唯一清除出口是测试钩子 `_resetForTest`** ⇒ 用户补齐资产 / 修好 onnx 后,
|
|
249
|
+
// 本进程内**永久** C2 降级(只能重启宿主),且 UI 仍显示"✓ 就绪"(见 probeJsSemanticAssets 的修复)。
|
|
250
|
+
// 现行为:冷却期(默认 60s,可配 `degradedRetryMs`)过后允许**再试一次**;
|
|
251
|
+
// 重试仍失败则更新时间戳继续冷却 —— 既不会每轮都去加载大模型,也不会永久闩死。
|
|
252
|
+
if (degraded) {
|
|
253
|
+
const since = Date.now() - degradedAt
|
|
254
|
+
const retryMs = Number((opts && opts.degradedRetryMs) || 60000)
|
|
255
|
+
if (since < retryMs) throw new Error(degraded)
|
|
256
|
+
// 冷却已过 ⇒ 清空降级态、允许重建(tierPromise 也一并清掉,避免复用已失败的 promise)
|
|
257
|
+
degraded = ''
|
|
258
|
+
tierPromise = null
|
|
259
|
+
statsRetries++
|
|
260
|
+
}
|
|
227
261
|
if (!tierPromise) {
|
|
228
262
|
tierPromise = (async () => {
|
|
229
263
|
if (opts.injectEmbedder) return opts.injectEmbedder
|
|
@@ -268,6 +302,7 @@ export function createJsSemanticEnginePre(opts = {}) {
|
|
|
268
302
|
return tier
|
|
269
303
|
} catch (e) {
|
|
270
304
|
degraded = String(e && e.message || e).slice(0, 160)
|
|
305
|
+
degradedAt = Date.now() // ★ issue #70:记时刻,供冷却判定
|
|
271
306
|
tierPromise = null
|
|
272
307
|
throw e
|
|
273
308
|
}
|
|
@@ -305,6 +340,10 @@ export function createJsSemanticEnginePre(opts = {}) {
|
|
|
305
340
|
assetPresent: existsSync(path.join(modelsDir(), E5_MODELS_SUBDIR, 'onnx', 'model_quantized.onnx')),
|
|
306
341
|
ready: !!tier,
|
|
307
342
|
degraded,
|
|
343
|
+
// ★ issue #70:暴露降级时刻与重试计数,供面板/诊断判断"是否处于冷却期、下次何时可重试"
|
|
344
|
+
degradedAt: degraded ? degradedAt : 0,
|
|
345
|
+
degradedRetryMs: Number((opts && opts.degradedRetryMs) || 60000),
|
|
346
|
+
degradedRetries: statsRetries,
|
|
308
347
|
lastRankError,
|
|
309
348
|
model: tier ? tier.model : null,
|
|
310
349
|
indexedRecords: idx.entries.length,
|
|
@@ -357,7 +396,7 @@ export function createJsSemanticEnginePre(opts = {}) {
|
|
|
357
396
|
const t = await ensureTier()
|
|
358
397
|
return t.embedPassages(Array.isArray(texts) ? texts : [])
|
|
359
398
|
},
|
|
360
|
-
_resetForTest() { tier = null; tierPromise = null; degraded = ''; idx = { miv: null, entries: [] }; rebuilding = null },
|
|
399
|
+
_resetForTest() { tier = null; tierPromise = null; degraded = ''; degradedAt = 0; statsRetries = 0; idx = { miv: null, entries: [] }; rebuilding = null },
|
|
361
400
|
}
|
|
362
401
|
}
|
|
363
402
|
|
|
@@ -413,6 +452,12 @@ export function createSemanticDownloaderPre(opts = {}) {
|
|
|
413
452
|
const total = Number(res.headers.get('content-length')) || fileRec.bytes
|
|
414
453
|
mkdirSync(tmpDir(), { recursive: true })
|
|
415
454
|
const dst = path.join(tmpDir(), path.basename(fileRec.rel))
|
|
455
|
+
// ★ 每次尝试从零开始(2026-09-19 上游 PR #79 / issue #65 同步落地):
|
|
456
|
+
// tmp 只在 run() 开始清一次,但 fetchToFile 会被**多个镜像依次调用**;
|
|
457
|
+
// 而写入用 flag:'a' 追加 ⇒ 源 A 断流留下的半截文件会被续上源 B 的完整流。
|
|
458
|
+
// 致命处:sha256 只对**网络流**累积(hash.update)、**不回读文件** ⇒ 拼接体校验照样通过并落位,
|
|
459
|
+
// 直到加载期才失败。故每次尝试前必须清掉残留。
|
|
460
|
+
try { rmSync(dst, { force: true }) } catch (_) {}
|
|
416
461
|
const hash = createHash('sha256')
|
|
417
462
|
const reader = res.body.getReader()
|
|
418
463
|
const fd = writeFileSync // 占位避免未用告警;真正写入走手动缓冲
|
package/lib/shadow-host.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { mkdirSync, appendFileSync, writeFileSync, readFileSync, readdirSync, existsSync, statSync, rmSync } from 'node:fs'
|
|
12
12
|
import path from 'node:path'
|
|
13
|
+
import { resolveDshHomeForEnginePre } from './dsh-home.js'
|
|
13
14
|
import { homedir } from 'node:os'
|
|
14
15
|
import { createHash } from 'node:crypto'
|
|
15
16
|
import {
|
|
@@ -126,11 +127,8 @@ export function createShadowHost({ engine }) {
|
|
|
126
127
|
/** durable audit 目录与日期分片。 */
|
|
127
128
|
function auditDir() { return path.join(dshHome(), 'memory', 'retrieval-pre', 'audit') }
|
|
128
129
|
function dshHome() {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// M4-4 修复:必须拼接 .dsh(此前漏拼导致 audit 写到 <home>/memory 错误位置)
|
|
132
|
-
const base = engine.__homedirFn ? engine.__homedirFn() : (process.env.USERPROFILE || process.env.HOME || '')
|
|
133
|
-
return base ? path.join(base, '.dsh') : '.'
|
|
130
|
+
// ★#86-3:统一口径(M4-4 的「必须拼 .dsh」约定已由 resolveDshHomePre 保证)。
|
|
131
|
+
return resolveDshHomeForEnginePre(engine)
|
|
134
132
|
}
|
|
135
133
|
|
|
136
134
|
/** 串行 durable append(§15.4):engine 级链式;失败只计 audit-write-failed 不重试不污染 Session。 */
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M9-3 Skill 导出层 · 宿主侧(IO 落地)。
|
|
3
|
+
*
|
|
4
|
+
* 纯渲染在 `skill-export.js`;本文件只负责**写盘**与**项目标注解析**。
|
|
5
|
+
*
|
|
6
|
+
* ★ 用户 2026-09-19 拍板:
|
|
7
|
+
* - ⑪-2 **晋升为 `active` 后自动导出** ⇒ 由 host 在 `activate` 成功后调用 `exportSkillForPre()`。
|
|
8
|
+
* - ⑪-3 目录 = **用户级**(`<dshHome>/skills/`,DSH 四条发现路径之一,可跨项目迁移);
|
|
9
|
+
* 导出物**必须标注适用项目**。
|
|
10
|
+
*
|
|
11
|
+
* 设计纪律:
|
|
12
|
+
* ① **fail-soft 但可观察**:返回 `{ok:false, reason}`,不抛出、不静默;
|
|
13
|
+
* ② **不自动执行**任何导出程序(程序只是参考资料);
|
|
14
|
+
* ③ 写盘走**临时文件 + rename**(避免半截文件被 DSH 当成合法 SKILL.md);
|
|
15
|
+
* ④ 不覆盖**非本插件产出**的同名目录(有 `mem-skill-` 前缀 + 自检标记才接管)。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { mkdirSync, writeFileSync, renameSync, existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
19
|
+
import { join } from 'node:path'
|
|
20
|
+
import { renderSkillMarkdownPre, validateSkillMarkdownPre } from './skill-export.js'
|
|
21
|
+
|
|
22
|
+
/** 导出目录前缀(用于识别"这是本插件的产物",避免误覆盖用户手写技能)。 */
|
|
23
|
+
export const SKILL_EXPORT_PREFIX_V1 = 'mem-skill-'
|
|
24
|
+
/** 自检标记:出现在 SKILL.md 里即证明是本插件导出物。 */
|
|
25
|
+
export const SKILL_EXPORT_STAMP_V1 = 'mem-skill-export-v1'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 解析技能导出根目录。
|
|
29
|
+
* 优先级:opts.skillsRoot > <dshHome>/skills
|
|
30
|
+
* (`<dshHome>` 由调用方从 DSH 环境取得;本函数不猜路径。)
|
|
31
|
+
*/
|
|
32
|
+
export function resolveSkillsRootPre(opts = {}) {
|
|
33
|
+
if (opts.skillsRoot) return String(opts.skillsRoot)
|
|
34
|
+
const home = String(opts.dshHome || '').trim()
|
|
35
|
+
if (!home) return null
|
|
36
|
+
return join(home, 'skills')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 从项目根路径推出一个稳定的项目名(用于 ⑪-3 的「适用项目」标注)。 */
|
|
40
|
+
export function projectNameFromPre(projectPath) {
|
|
41
|
+
const p = String(projectPath || '').trim().replace(/[\\/]+$/, '')
|
|
42
|
+
if (!p) return ''
|
|
43
|
+
const base = p.split(/[\\/]/).filter(Boolean).pop() || ''
|
|
44
|
+
return base
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 收集可附带的程序文件(⑪-1):只收**明确的程序后缀**,不递归、不猜。 */
|
|
48
|
+
export function collectProgramsPre(projectPath, names) {
|
|
49
|
+
const out = []
|
|
50
|
+
if (!Array.isArray(names) || !names.length) return out
|
|
51
|
+
for (const n of names) {
|
|
52
|
+
const s = String(n || '').trim()
|
|
53
|
+
if (!s) continue
|
|
54
|
+
// 只认相对文件名,防目录穿越
|
|
55
|
+
if (s.includes('..') || /^[\\/]/.test(s) || /^[a-zA-Z]:/.test(s)) continue
|
|
56
|
+
out.push(s)
|
|
57
|
+
}
|
|
58
|
+
return out
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readIfExists(p) {
|
|
62
|
+
try { return existsSync(p) ? readFileSync(p, 'utf8') : null } catch (_) { return null }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 导出一个 procedure 为 `<skillsRoot>/<dirName>/SKILL.md`。
|
|
67
|
+
*
|
|
68
|
+
* @param {object} procedure procedure 记录(stage 应为 active)
|
|
69
|
+
* @param {object} opts
|
|
70
|
+
* @param {string} opts.skillsRoot 技能根目录(必填;见 resolveSkillsRootPre)
|
|
71
|
+
* @param {string} opts.projectPath 项目根绝对路径
|
|
72
|
+
* @param {string} opts.projectName 项目名(缺省由 projectPath 推导)
|
|
73
|
+
* @param {string[]} opts.programs 附带程序相对路径列表
|
|
74
|
+
* @param {string} opts.exportedAt ISO 时间
|
|
75
|
+
* @param {boolean} opts.force 为 true 时允许覆盖**非本插件**的同名目录
|
|
76
|
+
* @returns {{ok:boolean, reason?:string, dir?:string, file?:string, bytes?:number}}
|
|
77
|
+
*/
|
|
78
|
+
export function exportSkillForPre(procedure, opts = {}) {
|
|
79
|
+
try {
|
|
80
|
+
const skillsRoot = String(opts.skillsRoot || '')
|
|
81
|
+
if (!skillsRoot) return { ok: false, reason: 'no-skills-root' }
|
|
82
|
+
|
|
83
|
+
const projectPath = String(opts.projectPath || '')
|
|
84
|
+
const projectName = String(opts.projectName || '').trim() || projectNameFromPre(projectPath)
|
|
85
|
+
if (!projectName) return { ok: false, reason: 'no-project-name' }
|
|
86
|
+
|
|
87
|
+
const programs = collectProgramsPre(projectPath, opts.programs)
|
|
88
|
+
const r = renderSkillMarkdownPre(procedure, {
|
|
89
|
+
projectName,
|
|
90
|
+
projectPath,
|
|
91
|
+
programs,
|
|
92
|
+
exportedAt: opts.exportedAt || new Date().toISOString(),
|
|
93
|
+
})
|
|
94
|
+
if (!r.ok) return { ok: false, reason: r.reason }
|
|
95
|
+
|
|
96
|
+
// 自带标记 + 完整性自检(判据 = 用户 ⑪-1/⑪-3 的硬要求)
|
|
97
|
+
const content = r.content + '\n<!-- ' + SKILL_EXPORT_STAMP_V1 + ' -->\n'
|
|
98
|
+
const v = validateSkillMarkdownPre(content, { projectName, hasPrograms: programs.length > 0 })
|
|
99
|
+
if (!v.ok) return { ok: false, reason: 'notice-incomplete:' + v.problems.join('|') }
|
|
100
|
+
|
|
101
|
+
const dir = join(skillsRoot, r.dirName)
|
|
102
|
+
// 防误覆盖:目录已存在且**不是**本插件产物 ⇒ 拒绝(除非 force)
|
|
103
|
+
if (existsSync(dir) && !opts.force) {
|
|
104
|
+
const old = join(dir, 'SKILL.md')
|
|
105
|
+
const prev = readIfExists(old)
|
|
106
|
+
const isOurs = prev != null && prev.includes(SKILL_EXPORT_STAMP_V1)
|
|
107
|
+
const isOursByName = r.dirName.startsWith(SKILL_EXPORT_PREFIX_V1)
|
|
108
|
+
if (!isOurs && isOursByName) {
|
|
109
|
+
// 按名字是我们的,但内容不是 ⇒ 用户手写过,别动
|
|
110
|
+
if (prev != null) return { ok: false, reason: 'dir-occupied-by-foreign' }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
mkdirSync(dir, { recursive: true })
|
|
115
|
+
// 临时文件 + rename ⇒ 避免半截文件被 DSH 扫描到
|
|
116
|
+
const tmp = join(dir, '.SKILL.md.tmp-' + process.pid + '-' + Date.now())
|
|
117
|
+
const file = join(dir, 'SKILL.md')
|
|
118
|
+
writeFileSync(tmp, content, 'utf8')
|
|
119
|
+
renameSync(tmp, file)
|
|
120
|
+
|
|
121
|
+
return { ok: true, dir, file, bytes: Buffer.byteLength(content, 'utf8'), dirName: r.dirName }
|
|
122
|
+
} catch (e) {
|
|
123
|
+
return { ok: false, reason: 'export-failed:' + String((e && e.message) || e) }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** 列出已导出的技能(供诊断/GUI)。 */
|
|
128
|
+
export function listExportedSkillsPre(skillsRoot) {
|
|
129
|
+
const root = String(skillsRoot || '')
|
|
130
|
+
if (!root || !existsSync(root)) return []
|
|
131
|
+
const out = []
|
|
132
|
+
let entries = []
|
|
133
|
+
try { entries = readdirSync(root) } catch (_) { return [] }
|
|
134
|
+
for (const name of entries) {
|
|
135
|
+
if (!name.startsWith(SKILL_EXPORT_PREFIX_V1)) continue
|
|
136
|
+
const dir = join(root, name)
|
|
137
|
+
try { if (!statSync(dir).isDirectory()) continue } catch (_) { continue }
|
|
138
|
+
const prev = readIfExists(join(dir, 'SKILL.md'))
|
|
139
|
+
if (prev == null || !prev.includes(SKILL_EXPORT_STAMP_V1)) continue
|
|
140
|
+
const nameLine = /^name:\s*(.+)$/m.exec(prev)
|
|
141
|
+
const projLine = /适用项目\*\*:`([^`]+)`/.exec(prev)
|
|
142
|
+
out.push({
|
|
143
|
+
dirName: name,
|
|
144
|
+
dir,
|
|
145
|
+
name: nameLine ? nameLine[1].trim() : name,
|
|
146
|
+
project: projLine ? projLine[1] : '',
|
|
147
|
+
bytes: Buffer.byteLength(prev, 'utf8'),
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
return out
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export { renderSkillMarkdownPre, validateSkillMarkdownPre } from './skill-export.js'
|