add-coder 0.3.39 → 0.3.40
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.en.md +16 -3
- package/README.md +16 -3
- package/dist/index.js +421 -70
- package/package.json +3 -2
- package/templates/.add-coder-src-hash.json +65 -53
- package/templates/adapters/claude/hooks/post-tool-use.mjs +1 -1
- package/templates/adapters/claude/hooks/prompt-submit.mjs +1 -1
- package/templates/adapters/claude/hooks/session-start.mjs +25 -5
- package/templates/adapters/claude/hooks/stop-check.mjs +2 -2
- package/templates/adapters/codex/hooks/post-tool-use.mjs +1 -1
- package/templates/adapters/codex/hooks/prompt-submit.mjs +1 -1
- package/templates/adapters/codex/hooks/session-start.mjs +25 -5
- package/templates/adapters/codex/hooks/stop-check.mjs +2 -2
- package/templates/adapters/qoder/hooks/post-tool-use.mjs +1 -1
- package/templates/adapters/qoder/hooks/prompt-submit.mjs +1 -1
- package/templates/adapters/qoder/hooks/session-start.mjs +25 -5
- package/templates/adapters/qoder/hooks/stop-check.mjs +2 -2
- package/templates/adapters/trae/hooks/post-tool-use.mjs +1 -1
- package/templates/adapters/trae/hooks/prompt-submit.mjs +1 -1
- package/templates/adapters/trae/hooks/session-start.mjs +25 -5
- package/templates/adapters/trae/hooks/stop-check.mjs +2 -2
- package/templates/adapters/vscode/hooks/post-tool-use.mjs +1 -1
- package/templates/adapters/vscode/hooks/prompt-submit.mjs +1 -1
- package/templates/adapters/vscode/hooks/session-start.mjs +25 -5
- package/templates/adapters/vscode/hooks/stop-check.mjs +2 -2
- package/templates/core/AGENTS.md +46 -0
- package/templates/core/docs/ADD-governance-claude-code.md +52 -0
- package/templates/core/docs/ADD-governance-codex.md +62 -0
- package/templates/core/docs/ADD-governance-qoder-cn.md +30 -0
- package/templates/core/docs/ADD-governance-trae.md +42 -0
- package/templates/core/docs/ADD-governance-vscode-copilot.md +88 -0
- package/templates/core/governance/rules.ts +2 -2
- package/templates/core/governance/session-start-guard.ts +28 -4
- package/templates/core/prisma/add.prisma +4 -0
- package/templates/core/scripts/mcp-server/resources/hitl-approval-widget.ts +4 -2
- package/templates/core/scripts/mcp-server/resources/index.ts +2 -0
- package/templates/core/scripts/mcp-server/resources/widget-probe.ts +53 -0
- package/templates/core/scripts/mcp-server/shared/db-types.ts +5 -1
- package/templates/core/scripts/mcp-server/shared/dev-operation.ts +97 -0
- package/templates/core/scripts/mcp-server/shared/hitl-ui.ts +46 -1
- package/templates/core/scripts/mcp-server/shared/memory/jobs/snapshot.ts +9 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/cjk-segmenter.ts +140 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/cjk-tokenize.ts +62 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/pg.ts +68 -16
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite-fts5.sql +3 -3
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite.ts +23 -6
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts-fingerprint.ts +116 -0
- package/templates/core/scripts/mcp-server/shared/memory/retrieval/query-terms.ts +9 -25
- package/templates/core/scripts/mcp-server/shared/memory/switches.ts +4 -2
- package/templates/core/scripts/mcp-server/shared/plan-lifecycle.ts +16 -3
- package/templates/core/scripts/mcp-server/shared/server-capabilities.ts +65 -0
- package/templates/core/scripts/mcp-server/tools/audit.ts +16 -34
- package/templates/core/scripts/mcp-server/tools/hitl.ts +18 -8
- package/templates/core/scripts/mcp-server/tools/index.ts +7 -2
- package/templates/core/scripts/mcp-server/tools/memory.ts +235 -1
- package/templates/core/scripts/mcp-server/tools/plan-lifecycle.ts +219 -0
- package/templates/core/scripts/mcp-server/tools/plan.ts +2 -0
- package/templates/core/scripts/mcp-server/tools/widget-probe.ts +53 -0
- package/templates/core/scripts/mcp-server.ts +5 -1
- package/templates/core/scripts/memory/backfill-search-text.ts +132 -0
- package/templates/core/scripts/memory/memory-jobs.ts +124 -0
- package/templates/core/scripts/plan-close.ts +85 -0
- package/templates/core/skills/add-paradigm/SKILL.md +19 -4
- package/templates/core/skills/session-init/SKILL.md +27 -0
- package/templates/core/templates/add-route-template-heavyweight.md +2 -0
- package/templates/core/templates/add-route-template.md +4 -0
- package/templates/core/templates/handoff-multi-round-template.md +2 -0
- package/templates/core/templates/simple-plan-template.md +4 -0
- package/templates/core/vocabulary/add-governance-vocabulary.md +2 -0
|
@@ -93,6 +93,94 @@ VS Code Copilot hooks 产物位于 **项目根目录** 的 `.vscode/hooks/`(14
|
|
|
93
93
|
|
|
94
94
|
---
|
|
95
95
|
|
|
96
|
+
## 工具可见性:VirtualTools 折叠(VS Code 用户必配项)
|
|
97
|
+
|
|
98
|
+
> **一句话**:Copilot Chat 会在**所有 MCP 服务器的工具总数**越过阈值时把部分工具"折叠"成代理,未激活就调用会稳定误报 `currently disabled by the user`。必配项一劳永逸关掉折叠;降级流程用于尚未改设置的存量会话。
|
|
99
|
+
> **证据来源**:[GitHub Issue #21](https://github.com/xiaomingming92/add-coder/issues/21)(含源码级根因定位与请求级工具快照对比)。
|
|
100
|
+
|
|
101
|
+
### VS Code 用户必配项:关闭虚拟工具折叠
|
|
102
|
+
|
|
103
|
+
把下面这行加进 **用户级 `settings.json`** 或 **工作区级 `.vscode/settings.json`**(二选一即可):
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
"github.copilot.chat.virtualTools.threshold": 0
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
已有 `settings.json` 的话,直接粘这一行进大括号内即可;从零建文件的完整形态:
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"github.copilot.chat.virtualTools.threshold": 0
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**设置语义**:
|
|
118
|
+
|
|
119
|
+
| 项 | 值 |
|
|
120
|
+
|---|---|
|
|
121
|
+
| 归属 | GitHub Copilot Chat 扩展注册的实验设置(不在 VS Code core 设置里) |
|
|
122
|
+
| 类型 / 取值域 | `number`,`0`–`128` |
|
|
123
|
+
| 默认值 | `128` |
|
|
124
|
+
| 设为 `0` 的含义 | 阈值 = ∞ ⇒ **虚拟工具折叠整体禁用** ⇒ 全部工具始终直连 |
|
|
125
|
+
|
|
126
|
+
**触发条件(务必看清口径)**:折叠的判定基准是 **所有 MCP 服务器的工具总数 ≥ `threshold/2`**,默认即 **≥ 64 个工具**——**不是**单个服务器的工具数。
|
|
127
|
+
|
|
128
|
+
- add-coder 自身注册 **47** 个工具;
|
|
129
|
+
- 叠加常见扩展(Pylance 19 个、Java Debug、Python 等)后**必然越线**;
|
|
130
|
+
- 因此任何 VS Code + Copilot Chat 用户都会遇到,与是否只装了 add-coder 无关。
|
|
131
|
+
|
|
132
|
+
**生效方式**:改完设置后 **需重载 VS Code 窗口**(`Ctrl/Cmd+Shift+P` → `Developer: Reload Window`,或关闭重开)。只重开会话、或 reload 扩展都不会生效。
|
|
133
|
+
|
|
134
|
+
**自查**:在 VS Code 项目下执行
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
npx add-coder status
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
未配置时会输出缺键告警与修复指引(只报告,不写你的设置文件)。
|
|
141
|
+
|
|
142
|
+
### 工具假禁用的降级流程
|
|
143
|
+
|
|
144
|
+
**症状**(未配置必配项时):
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
ERROR: Tool mcp_<server>_<name> is currently disabled by the user, and cannot be called.
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
> ⚠️ **这是误报**:你从未在 UI 里禁用任何工具。真实原因是该工具被折叠进虚拟工具代理,而代理尚未被激活。
|
|
151
|
+
|
|
152
|
+
**处置三步**:
|
|
153
|
+
|
|
154
|
+
1. 在**当前会话的工具列表**里找描述含 `Contains the tools:` 字样的 `activate_fallback_*` 代理(形如 `activate_fallback_mcp_add-dev-tools_get_memory_1`);
|
|
155
|
+
2. 调用该代理 —— 返回文本会以 `Tools activated: ...` 列出它携带的成员工具;
|
|
156
|
+
3. 重试原先失败的目标工具(本会话内有效)。
|
|
157
|
+
|
|
158
|
+
> ⚠️ **代理名会变,勿缓存**:代理名由「折叠段首个工具名 + 树编号」拼成,会随会话槽位重算而**重构**(实测同一环境从 `get_memory` 组变为 `get_hook_events` 组,旧名字随即失效)。**每次都以当前工具列表为准**,不要把代理名写进脚本、提示词或笔记里复用。
|
|
159
|
+
|
|
160
|
+
**受影响的治理工具族**(折叠按字母序裁掉后 N-1 个,治理工具恰好集中在字母序后半段):
|
|
161
|
+
|
|
162
|
+
| 工具族 | 具体工具 | 断链后果 |
|
|
163
|
+
|---|---|---|
|
|
164
|
+
| Plan 追踪 | `plan_track` / `plan_status` / `plan_sync` | 进度不落库、`tasks.md` 勾选无法同步 |
|
|
165
|
+
| Review 追踪 | `review_track` / `review_status` / `review_sync` | P0/P1 缺陷无法入库与回写 |
|
|
166
|
+
| 状态与人机审核 | `status_hitl` / `update_hitl` / `render_hitl_approval` | **审批无法落库** → TONGYI 哨兵不生成 → Plan/Review 写入被 PreToolUse 阻断 |
|
|
167
|
+
| 审计链 | `record_dev_operation` / `query_audit_logs` | **ADD-7 开发操作审计断链**、稀疏推理恢复失效 |
|
|
168
|
+
| 上下文与记忆 | `get_project_context` / `get_memory` | 会话恢复与记忆召回不可用 |
|
|
169
|
+
| 关键字面 | `plan_*` / `review_*` / `status_*` / `update_*` | 上述族名的共同前缀,可用于快速判断"是不是折叠误伤" |
|
|
170
|
+
|
|
171
|
+
**降级流程与必配项的关系**:降级流程是**临时手段**(仅本会话有效,下次开窗又要重新激活);必配项是**根治**(一次配置,全部工具始终直连)。生产/日常使用请配置必配项。
|
|
172
|
+
|
|
173
|
+
### 附录:为什么这是宿主问题(可据此向上游反馈)
|
|
174
|
+
|
|
175
|
+
本问题**不是 add-coder 的实现缺陷**:折叠由 Copilot Chat 扩展自身的虚拟工具分组机制(源码内 `VirtualToolGrouper`,可通过 bundle 中 `Contains the tools:` 字样定位)触发;同为受害者的还有 **Pylance**(19 个工具中 14 个被折叠)、**Java Debug**、**Python** 等官方或知名扩展 —— 属宿主通用行为。
|
|
176
|
+
|
|
177
|
+
可向上游反馈两点:
|
|
178
|
+
|
|
179
|
+
1. **错误文案具误导性**:`disabled by the user` 会让用户去 UI 里找一个根本不存在的"禁用开关",而真实原因是虚拟工具折叠未激活;建议文案区分"用户禁用"与"折叠未激活"两种状态。
|
|
180
|
+
2. **折叠机制会命中 MCP 服务器的治理关键工具**:折叠按字母序裁剪,审批 / 审计类工具(`update_hitl` / `record_dev_operation` / `query_audit_logs` 等)恰好落在被裁区间,对大工具集 MCP 服务器造成可用性影响;建议折叠策略至少保证关键工具直连,或提供显式开关。
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
96
184
|
## 自定义 Hook 源切换
|
|
97
185
|
|
|
98
186
|
VS Code Copilot 的 `.vscode/hooks/*.mjs` 为 VS Code 原生 node 产物(14 入口)。项目同时产出 `.claude/` 目录(含 Claude Code 完整 hook 体系 + settings.json),两套体系可切换。
|
|
@@ -529,7 +529,7 @@ export const context = {
|
|
|
529
529
|
{
|
|
530
530
|
"id": "has_add_dev_unclosed",
|
|
531
531
|
"consumed": true,
|
|
532
|
-
"text": "[ADD Stop] ⚠️ 代码已完成但验收未闭环:\n{{info}}\n\n请依次执行(不要等下次会话):\n ① Write devlog →
|
|
532
|
+
"text": "[ADD Stop] ⚠️ 代码已完成但验收未闭环:\n{{info}}\n\n请依次执行(不要等下次会话):\n ① Write devlog → 调用 `record_dev_operation`(MCP)落库审计;并把本轮验收记录(日期 / Plan / 轮次 / 改了什么 / 验收结果 / 遗留项 / 架构回看)写进 handoff 的验收段落——**不新建 devlog 文件**\n ② Edit handoff → 更新 §验证标准 全部 [x] + 补充审计 ID(真实 cuid)\n ★ 同步: checklist 有新 cuid → handoff ADD-7 表必须对应新增行\n ★ Step 0 准入: handoff + add-route + Specs 三元组缺一不可,缺则回退 Step 0.5\n ③ Read docs/ → 回看架构文档确认一致性\n ④ Edit add-route → 勾选对应 Step [x]\n\n以上全部完成后 Agent 才能停止。\n\n下次恢复: 读 handoff → query_audit_logs({ planKeyword })(devlog 即 DevOperation 记录,无独立文件)\n"
|
|
533
533
|
},
|
|
534
534
|
{
|
|
535
535
|
"id": "has_add_dev_closed",
|
|
@@ -541,7 +541,7 @@ export const context = {
|
|
|
541
541
|
{
|
|
542
542
|
"adapter": "qoder",
|
|
543
543
|
"id": "has_add_dev_unclosed",
|
|
544
|
-
"text": "[ADD Stop] ⚠️ 代码已完成但验收未闭环:\n{{info}}\n\n请依次执行(不要等下次会话):\n ① Write devlog →
|
|
544
|
+
"text": "[ADD Stop] ⚠️ 代码已完成但验收未闭环:\n{{info}}\n\n请依次执行(不要等下次会话):\n ① Write devlog → 调用 `record_dev_operation`(MCP)落库审计;并把本轮验收记录(日期 / Plan / 轮次 / 改了什么 / 验收结果 / 遗留项 / 架构回看)写进 handoff 的验收段落——**不新建 devlog 文件**\n ② Edit handoff → 更新 §验证标准 全部 [x] + 补充审计 ID(真实 cuid)\n ★ 同步: checklist 有新 cuid → handoff ADD-7 表必须对应新增行\n ★ Step 0 准入: handoff + add-route + Specs 三元组缺一不可,缺则回退 Step 0.5\n ③ Read docs/ → 回看架构文档确认一致性\n ④ Edit add-route → 勾选对应 Step [x]\n\n以上全部完成后 Agent 才能停止。\n\n下次恢复: 读 handoff → query_audit_logs({ planKeyword })(devlog 即 DevOperation 记录,无独立文件)\n"
|
|
545
545
|
}
|
|
546
546
|
],
|
|
547
547
|
"templates": {
|
|
@@ -108,19 +108,43 @@ export class SessionStartGuard {
|
|
|
108
108
|
* shadow → 仅提示快照存在(召回可执行并落审计,但不注入上下文)
|
|
109
109
|
* inject → 读预计算快照注入(新鲜度 ≤7 天,token 预算截断,带来源边界标签)
|
|
110
110
|
* 同步 Hook ≤200ms 约束:只读文件,任何异常 fail-open 静默跳过。
|
|
111
|
+
*
|
|
112
|
+
* [2026-09-21 接线修复] 三态必须**可区分**,且"未接线/已过期"不再静默:
|
|
113
|
+
* 原实现 `if (!existsSync(file)) return` 会让"没跑 job"与"没有已治理记忆"、
|
|
114
|
+
* 以及"档位没开"三种情况在会话里完全无痕,用户与模型都无从判断记忆是否接上。
|
|
111
115
|
*/
|
|
112
116
|
protected emitMemoryL1(): void {
|
|
113
117
|
try {
|
|
114
118
|
const mode = recallMode()
|
|
115
119
|
if (mode === "off" || !this.magicDir) return
|
|
116
120
|
const file = join(this.projectDir, this.magicDir, MEMORY_DIR_NAME, L1_SNAPSHOT_FILE)
|
|
117
|
-
|
|
121
|
+
// ① 未接线:文件不存在(缺 job 入口 / 首次安装 / 从未刷新)
|
|
122
|
+
if (!existsSync(file)) {
|
|
123
|
+
process.stdout.write(
|
|
124
|
+
`[Memory] ⚠️ L1 快照未接线(${file} 不存在)——当前档位 ${mode}。` +
|
|
125
|
+
`生成快照:执行 \`npx add-coder sync\`、调用 MCP 工具 refresh_memory_snapshots、或运行 ` +
|
|
126
|
+
`\`${this.magicDir}/scripts/memory/memory-jobs.ts refresh-l1\`;` +
|
|
127
|
+
`注入开关:ADD_MEMORY_RECALL_MODE=inject(默认)。详见 ${this.magicDir}/docs/ADD-governance-codex.md\n`,
|
|
128
|
+
)
|
|
129
|
+
return
|
|
130
|
+
}
|
|
118
131
|
if (mode === "shadow") {
|
|
119
|
-
process.stdout.write(
|
|
132
|
+
process.stdout.write(
|
|
133
|
+
`[Memory] 当前档位 shadow:召回可执行并落审计,但**不注入**上下文(快照已生成: ${file})。` +
|
|
134
|
+
`需要注入时去掉 ADD_MEMORY_RECALL_MODE 或设为 inject(默认);也可直接调用 recall_memory 显式召回。\n`,
|
|
135
|
+
)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
// ② 已接线但过期:inject 下不注入,但必须说明原因与刷新入口
|
|
139
|
+
const ageMs = Date.now() - statSync(file).mtimeMs
|
|
140
|
+
if (ageMs > L1_SNAPSHOT_TTL_MS) {
|
|
141
|
+
const days = Math.floor(ageMs / 86400000)
|
|
142
|
+
process.stdout.write(
|
|
143
|
+
`[Memory] ⚠️ L1 快照已过期(${days} 天 > TTL 7 天,未注入):${file}` +
|
|
144
|
+
`——刷新:调用 refresh_memory_snapshots 或运行 \`${this.magicDir}/scripts/memory/memory-jobs.ts refresh-l1\`\n`,
|
|
145
|
+
)
|
|
120
146
|
return
|
|
121
147
|
}
|
|
122
|
-
// inject:过期快照不注入
|
|
123
|
-
if (Date.now() - statSync(file).mtimeMs > L1_SNAPSHOT_TTL_MS) return
|
|
124
148
|
const text = readFileSync(file, "utf-8")
|
|
125
149
|
const budget = memoryMaxTokens()
|
|
126
150
|
if (estimateTokens(text) <= budget) {
|
|
@@ -78,6 +78,7 @@ enum PlanLifecycleStatus {
|
|
|
78
78
|
BLOCKED
|
|
79
79
|
REJECTED
|
|
80
80
|
CLOSED
|
|
81
|
+
REOPENED
|
|
81
82
|
ABANDONED
|
|
82
83
|
}
|
|
83
84
|
|
|
@@ -295,6 +296,9 @@ model AddMemory {
|
|
|
295
296
|
topic String
|
|
296
297
|
content String
|
|
297
298
|
summary String?
|
|
299
|
+
/// 检索展开文本(写入期产出:CJK bigram / jieba 词级 token 串,两侧共用同一 tokenization 契约)
|
|
300
|
+
/// 主通道索引 AddMemory_searchText_tsv_idx 建立在该列的 tsvector 表达式上(Plan Task 2.3)
|
|
301
|
+
searchText String @default("")
|
|
298
302
|
|
|
299
303
|
scopeType MemoryScopeType
|
|
300
304
|
scopeValue String
|
|
@@ -5,13 +5,15 @@ import { PROJECT_ROOT, MAGIC_DIR } from "../shared/fs.js"
|
|
|
5
5
|
import {
|
|
6
6
|
HITL_APPROVAL_WIDGET_FILE,
|
|
7
7
|
HITL_APPROVAL_WIDGET_MIME,
|
|
8
|
-
|
|
8
|
+
getHitlApprovalWidgetUri,
|
|
9
9
|
} from "../shared/hitl-ui.js"
|
|
10
10
|
|
|
11
11
|
export function registerHitlApprovalWidgetResource(server: McpServer) {
|
|
12
|
+
// [2026-09-21 修复] 与工具 `_meta.ui.resourceUri` 共用同一函数(基名 + 内容哈希),保证两处严格一致
|
|
13
|
+
const WIDGET_URI = getHitlApprovalWidgetUri()
|
|
12
14
|
server.registerResource(
|
|
13
15
|
"hitl-approval-widget",
|
|
14
|
-
|
|
16
|
+
WIDGET_URI,
|
|
15
17
|
{
|
|
16
18
|
title: "ADD HITL Approval",
|
|
17
19
|
description: "逐项审核 ADD HITL 提案的 core 标准 widget",
|
|
@@ -4,6 +4,7 @@ import { registerRoundTaskResources } from "./round-task.js"
|
|
|
4
4
|
import { registerVersionResource } from "./add-coder-version.js"
|
|
5
5
|
import { registerHookEventResources } from "./hook-events-report.js"
|
|
6
6
|
import { registerHitlApprovalWidgetResource } from "./hitl-approval-widget.js"
|
|
7
|
+
import { registerWidgetProbeResource } from "./widget-probe.js"
|
|
7
8
|
|
|
8
9
|
export function registerAllResources(server: McpServer) {
|
|
9
10
|
registerAddStateResources(server)
|
|
@@ -11,4 +12,5 @@ export function registerAllResources(server: McpServer) {
|
|
|
11
12
|
registerVersionResource(server)
|
|
12
13
|
registerHookEventResources(server) // 2 resources: hook-events/{daily,weekly}
|
|
13
14
|
registerHitlApprovalWidgetResource(server)
|
|
15
|
+
registerWidgetProbeResource(server) // 1 resource: 最小 MCP Apps 渲染探针(诊断用)
|
|
14
16
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/server"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 最小 MCP Apps 渲染探针(诊断用,2026-09-21 引入)。
|
|
5
|
+
*
|
|
6
|
+
* 背景:Codex 桌面端对 HITL 审批 widget 报 "This app couldn't be loaded",而按 MCP 协议直连 server 的探针
|
|
7
|
+
* 证明 `resources/read` 能取到 widget(15128 字符、`text/html;profile=mcp-app`)——失败在宿主渲染阶段。
|
|
8
|
+
* 为把「宿主加载不了任何 app」与「我们 widget 自身有问题」分开,注册本资源:
|
|
9
|
+
* · 无任何外部资源引用(无 script src / link / @import)
|
|
10
|
+
* · 内联脚本只有 10 行、全程 try/catch,**不查任何业务 DOM id**,不外抛异常
|
|
11
|
+
* · 页面文案会随加载阶段变化,观察文案即可定位:HTML 已加载(脚本未执行)/ 内联脚本已执行 / 脚本异常
|
|
12
|
+
* 配套工具 `probe_widget_render` 携带 `_meta.ui.resourceUri` 指向本资源,调用即触发宿主渲染。
|
|
13
|
+
*/
|
|
14
|
+
export const WIDGET_PROBE_URI = "ui://add-coder/widget-probe-minimal"
|
|
15
|
+
export const WIDGET_PROBE_MIME = "text/html;profile=mcp-app"
|
|
16
|
+
|
|
17
|
+
export const WIDGET_PROBE_HTML = [
|
|
18
|
+
"<!DOCTYPE html>",
|
|
19
|
+
'<html lang="zh-CN">',
|
|
20
|
+
"<head>",
|
|
21
|
+
'<meta charset="utf-8">',
|
|
22
|
+
"<title>ADD widget probe</title>",
|
|
23
|
+
"<style>body{font:14px/1.6 system-ui,sans-serif;margin:0;padding:12px}</style>",
|
|
24
|
+
"</head>",
|
|
25
|
+
"<body>",
|
|
26
|
+
'<div id="probe-msg">ADD widget probe: HTML 已加载(内联脚本未执行 ⇒ 疑似 CSP 拦截)</div>',
|
|
27
|
+
"<script>",
|
|
28
|
+
"try {",
|
|
29
|
+
' var m = document.getElementById("probe-msg");',
|
|
30
|
+
' m.textContent = "ADD widget probe: 内联脚本已执行";',
|
|
31
|
+
' window.parent.postMessage({ jsonrpc: "2.0", id: 1, method: "ui/initialize", params: { protocolVersion: "2026-07-28" } }, "*");',
|
|
32
|
+
"} catch (e) {",
|
|
33
|
+
' document.getElementById("probe-msg").textContent = "ADD widget probe: 脚本异常 " + e;',
|
|
34
|
+
"}",
|
|
35
|
+
"</script>",
|
|
36
|
+
"</body>",
|
|
37
|
+
"</html>",
|
|
38
|
+
].join("\n")
|
|
39
|
+
|
|
40
|
+
export function registerWidgetProbeResource(server: McpServer) {
|
|
41
|
+
server.registerResource(
|
|
42
|
+
"widget-probe-minimal",
|
|
43
|
+
WIDGET_PROBE_URI,
|
|
44
|
+
{
|
|
45
|
+
title: "ADD Widget Render Probe",
|
|
46
|
+
description: "最小 MCP Apps 渲染探针:区分「宿主加载不了任何 app」与「业务 widget 自身有问题」",
|
|
47
|
+
mimeType: WIDGET_PROBE_MIME,
|
|
48
|
+
},
|
|
49
|
+
(uri) => ({
|
|
50
|
+
contents: [{ uri: uri.href, mimeType: WIDGET_PROBE_MIME, text: WIDGET_PROBE_HTML }],
|
|
51
|
+
}),
|
|
52
|
+
)
|
|
53
|
+
}
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* 契约:schema 须与 prisma/add.prisma 模型对齐(字段名/可空性)。
|
|
16
16
|
*/
|
|
17
17
|
import * as z from "zod/v4"
|
|
18
|
+
// lifecycle 值的**唯一真源**(zod):本文件不再手写字面量列表(2026-09-21 决策)
|
|
19
|
+
import { PlanLifecycleStatusSchema } from "./plan-lifecycle.js"
|
|
18
20
|
|
|
19
21
|
// ===== 行 schema(对齐 prisma/add.prisma;looseObject 前瞻兼容) =====
|
|
20
22
|
|
|
@@ -23,7 +25,7 @@ export const PlanRowSchema = z.looseObject({
|
|
|
23
25
|
projectKey: z.string(),
|
|
24
26
|
adapterKey: z.string(),
|
|
25
27
|
planName: z.string(),
|
|
26
|
-
lifecycle:
|
|
28
|
+
lifecycle: PlanLifecycleStatusSchema,
|
|
27
29
|
revision: z.number(),
|
|
28
30
|
planPath: z.string(),
|
|
29
31
|
planKeyword: z.string().nullable(),
|
|
@@ -164,6 +166,8 @@ export const AddMemoryRowSchema = z.looseObject({
|
|
|
164
166
|
status: MemoryStatusSchema,
|
|
165
167
|
topic: z.string(),
|
|
166
168
|
content: z.string(),
|
|
169
|
+
/** 检索展开文本(写入期产出:jieba 词级 / bigram token 串);主通道索引建立在该列 tsvector 表达式上 */
|
|
170
|
+
searchText: z.string().default(""),
|
|
167
171
|
summary: z.string().nullable(),
|
|
168
172
|
scopeType: MemoryScopeTypeSchema,
|
|
169
173
|
scopeValue: z.string(),
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* dev-operation.ts — DevOperation(ADD-7 开发操作审计)写入的**唯一实现**
|
|
3
|
+
*
|
|
4
|
+
* 背景(2026-09-21 决策):审计写入此前散成两份——`tools/audit.ts::record_dev_operation` 一份、
|
|
5
|
+
* 临时脚本 `scripts/plan-close.ts` 又手写一份(自己解析 AddUser + 直写)⇒ 典型的"同一能力两处实现"漂移。
|
|
6
|
+
* 现抽为本文件:**record_dev_operation / plan_update / 脚本** 三方共用,形状与幂等键语义完全一致。
|
|
7
|
+
*
|
|
8
|
+
* 幂等:`operationKey` 未提供时按 `{projectKey, producerAdapterKey, action, targetType, targetId,
|
|
9
|
+
* planKeyword, beforeState, afterState, reason}` 的规范化 JSON 取 sha256(与既有行为逐字一致,
|
|
10
|
+
* 不把 toolName 纳入 hash —— 换调用方不该产生新审计行)。落库按唯一键
|
|
11
|
+
* `projectKey_producerAdapterKey_toolName_operationKey` upsert(重复调用吸收)。
|
|
12
|
+
*/
|
|
13
|
+
import { createHash } from "node:crypto"
|
|
14
|
+
import type { RuntimeContextKey } from "./runtime-context.js"
|
|
15
|
+
|
|
16
|
+
export interface DevOperationWriterDatabase {
|
|
17
|
+
devOperation: {
|
|
18
|
+
upsert(args: Record<string, unknown>): Promise<DevOperationWrittenRow>
|
|
19
|
+
}
|
|
20
|
+
addUser: {
|
|
21
|
+
findUnique(args: Record<string, unknown>): Promise<{ id: string } | null>
|
|
22
|
+
create(args: Record<string, unknown>): Promise<{ id: string }>
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface DevOperationWrittenRow {
|
|
27
|
+
id: string
|
|
28
|
+
operationKey: string
|
|
29
|
+
createdAt: Date
|
|
30
|
+
beforeState: unknown
|
|
31
|
+
afterState: unknown
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface WriteDevOperationInput {
|
|
35
|
+
context: RuntimeContextKey
|
|
36
|
+
action: string
|
|
37
|
+
targetType: string
|
|
38
|
+
targetId?: string
|
|
39
|
+
planKeyword?: string
|
|
40
|
+
beforeState: Record<string, unknown> | unknown[]
|
|
41
|
+
afterState: Record<string, unknown> | unknown[]
|
|
42
|
+
reason?: string | null
|
|
43
|
+
operationKey?: string
|
|
44
|
+
/** 审计来源工具/脚本名;默认 record_dev_operation */
|
|
45
|
+
toolName?: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 审计用户解析:先 ai-assistant,缺失则建;再退回库内首个用户;都没有则显式报错(不静默) */
|
|
49
|
+
async function resolveAuditUserId(database: DevOperationWriterDatabase): Promise<string> {
|
|
50
|
+
const existing = await database.addUser.findUnique({ where: { username: "ai-assistant" }, select: { id: true } })
|
|
51
|
+
if (existing) return existing.id
|
|
52
|
+
const created = await database.addUser.create({
|
|
53
|
+
data: { id: "ai-assistant", username: "ai-assistant", email: "ai-assistant@internal" },
|
|
54
|
+
})
|
|
55
|
+
return created.id
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function writeDevOperation(
|
|
59
|
+
database: DevOperationWriterDatabase,
|
|
60
|
+
input: WriteDevOperationInput,
|
|
61
|
+
): Promise<DevOperationWrittenRow> {
|
|
62
|
+
const toolName = input.toolName ?? "record_dev_operation"
|
|
63
|
+
const userId = await resolveAuditUserId(database)
|
|
64
|
+
const operationPayload = {
|
|
65
|
+
projectKey: input.context.projectKey,
|
|
66
|
+
producerAdapterKey: input.context.adapterKey,
|
|
67
|
+
action: input.action,
|
|
68
|
+
targetType: input.targetType,
|
|
69
|
+
targetId: input.targetId || "unknown",
|
|
70
|
+
planKeyword: input.planKeyword || "unknown",
|
|
71
|
+
beforeState: input.beforeState,
|
|
72
|
+
afterState: input.afterState,
|
|
73
|
+
reason: input.reason ?? null,
|
|
74
|
+
}
|
|
75
|
+
const operationKey =
|
|
76
|
+
(input.operationKey ?? "").trim() ||
|
|
77
|
+
createHash("sha256").update(JSON.stringify(operationPayload)).digest("hex")
|
|
78
|
+
|
|
79
|
+
return database.devOperation.upsert({
|
|
80
|
+
where: {
|
|
81
|
+
projectKey_producerAdapterKey_toolName_operationKey: {
|
|
82
|
+
projectKey: input.context.projectKey,
|
|
83
|
+
producerAdapterKey: input.context.adapterKey,
|
|
84
|
+
toolName,
|
|
85
|
+
operationKey,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
create: {
|
|
89
|
+
...operationPayload,
|
|
90
|
+
userId,
|
|
91
|
+
contextId: input.context.contextId,
|
|
92
|
+
toolName,
|
|
93
|
+
operationKey,
|
|
94
|
+
},
|
|
95
|
+
update: {},
|
|
96
|
+
})
|
|
97
|
+
}
|
|
@@ -1,3 +1,48 @@
|
|
|
1
|
-
|
|
1
|
+
import { createHash } from "node:crypto"
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
import { PROJECT_ROOT, MAGIC_DIR } from "./env.js"
|
|
5
|
+
|
|
2
6
|
export const HITL_APPROVAL_WIDGET_MIME = "text/html;profile=mcp-app"
|
|
3
7
|
export const HITL_APPROVAL_WIDGET_FILE = "hitl-approval-widget.html"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 资源 URI **基名**(不含内容哈希)。
|
|
11
|
+
*
|
|
12
|
+
* [2026-09-21 修复] 宿主把 resource URI 当**缓存键**(官方 MCP Apps 规范:
|
|
13
|
+
* "Treat the resource URI as a cache key. When you make a breaking change to the HTML,
|
|
14
|
+
* JavaScript, or CSS, publish a new URI and update every tool that references it.")。
|
|
15
|
+
* 2026-09-18 改过 widget HTML 但 URI 未变,宿主命中旧缓存 ⇒ 实测 "This app couldn't be loaded"。
|
|
16
|
+
* 现改为**机制化**:最终 URI = 基名 + widget 内容短哈希(`getHitlApprovalWidgetUri()`),
|
|
17
|
+
* 改 HTML/JS/CSS 即自动换 URI,不再依赖人记得手动 bump。
|
|
18
|
+
*/
|
|
19
|
+
export const HITL_APPROVAL_WIDGET_URI_BASE = "ui://add-coder/hitl-approval"
|
|
20
|
+
|
|
21
|
+
let cachedUri: string | null = null
|
|
22
|
+
|
|
23
|
+
/** widget 内容短哈希(sha256 前 8 位);文件缺失/不可读返回 ""(调用方回退静态基名) */
|
|
24
|
+
export function computeWidgetContentHash(): string {
|
|
25
|
+
try {
|
|
26
|
+
const p = join(PROJECT_ROOT, MAGIC_DIR, "templates", HITL_APPROVAL_WIDGET_FILE)
|
|
27
|
+
if (!existsSync(p)) return ""
|
|
28
|
+
return createHash("sha256").update(readFileSync(p)).digest("hex").slice(0, 8)
|
|
29
|
+
} catch {
|
|
30
|
+
return ""
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 资源注册与工具 `_meta.ui.resourceUri` **共用**的 URI(进程内 memoize,保证两处完全一致)。
|
|
36
|
+
* 同一进程内若替换了 widget 文件,需重启 MCP server 才会换 URI(宿主本就要求重启后重读元数据)。
|
|
37
|
+
*/
|
|
38
|
+
export function getHitlApprovalWidgetUri(): string {
|
|
39
|
+
if (cachedUri) return cachedUri
|
|
40
|
+
const hash = computeWidgetContentHash()
|
|
41
|
+
cachedUri = hash ? `${HITL_APPROVAL_WIDGET_URI_BASE}-${hash}` : HITL_APPROVAL_WIDGET_URI_BASE
|
|
42
|
+
return cachedUri
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 测试专用:清空 memo,使同一进程内可重新计算(生产路径不需要) */
|
|
46
|
+
export function resetHitlApprovalWidgetUriCache(): void {
|
|
47
|
+
cachedUri = null
|
|
48
|
+
}
|
|
@@ -36,6 +36,15 @@ export function renderSnapshotMarkdown(level: "L1" | "L2", items: RecalledMemory
|
|
|
36
36
|
`[Memory ${level} · 来源: AddMemory 治理库 · 生成于 ${generatedAt.toISOString()}]`,
|
|
37
37
|
`<agent-memory source="add-memory" trust="governed">`,
|
|
38
38
|
]
|
|
39
|
+
// 空态显式标记(2026-09-21 接线 Plan Task 1.1.4):
|
|
40
|
+
// 零条快照也必须可区分于"未接线"——否则文件不存在既可能是没跑 job、也可能是没有已治理记忆,自检无法判定。
|
|
41
|
+
if (items.length === 0) {
|
|
42
|
+
lines.push(
|
|
43
|
+
level === "L1"
|
|
44
|
+
? "- 本仓库暂无已治理记忆(ACTIVE=0):快照已接线,但尚无通过治理的记忆可注入"
|
|
45
|
+
: "- 本次查询未命中已治理记忆(ACTIVE=0)",
|
|
46
|
+
)
|
|
47
|
+
}
|
|
39
48
|
for (const it of items) {
|
|
40
49
|
lines.push(
|
|
41
50
|
`- [${it.kind}] ${it.topic}(置信 ${it.confidence.toFixed(2)})`,
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* CJK 分词器(主通道 + 兜底)— Plan add-coder-memory-cjk-bigram-baseline 轮 4(人类裁决:完整上 jieba)
|
|
3
|
+
*
|
|
4
|
+
* 路线(与 farm-agent `src/lib/memory/cjk-segmenter.ts` 的 2026-09-18 人类裁定对齐):
|
|
5
|
+
* ① **jieba 为主**:`@node-rs/jieba`(native,optionalDependency)+ 可选用户词典;
|
|
6
|
+
* ② **bigram 兜底**:`cjk-tokenize.ts`(纯函数、零依赖)——jieba 不可用时自动降级,**不是错误**。
|
|
7
|
+
*
|
|
8
|
+
* 为什么"主 + 兜底"而不是二选一:
|
|
9
|
+
* · jieba 给的是**词级 token**(IDF/BM25 才有判别力、索引不膨胀),但依赖原生二进制与词典版本;
|
|
10
|
+
* · bigram 是**地板**(1-2 字查询可索引、OOV 不惧、天然读写同源),但 token 数≈字符数、判别力弱;
|
|
11
|
+
* · add-coder 的 memory 代码**模板分发到六端用户项目**,native 依赖在 pnpm 严格布局 / 非主流平台可能根本装不上
|
|
12
|
+
* ⇒ 必须有一条"装不上也能跑"的路径,且**必须显式告知当前用的是哪条**(降级明示原则)。
|
|
13
|
+
*
|
|
14
|
+
* 契约:**写入侧与查询侧必须调用同一函数**(读写同源);返回 `method` 供召回结果透出,
|
|
15
|
+
* `method === "bigram"` 即视为降级,须能被 `get_memory_health` / recall 审计观察到。
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
18
|
+
import { createRequire } from "node:module"
|
|
19
|
+
import { join } from "node:path"
|
|
20
|
+
import { MAGIC_DIR, PROJECT_ROOT } from "../../env.js"
|
|
21
|
+
import { expandForQuery, tokenize } from "./cjk-tokenize.js"
|
|
22
|
+
|
|
23
|
+
export type SegmentMethod = "jieba" | "bigram"
|
|
24
|
+
|
|
25
|
+
export interface SegmentResult {
|
|
26
|
+
/** token 数组(与写入侧同一实现产出) */
|
|
27
|
+
terms: string[]
|
|
28
|
+
/** 实际使用的分词器:`jieba` = 主通道;`bigram` = 兜底(降级) */
|
|
29
|
+
method: SegmentMethod
|
|
30
|
+
/** 用户词典是否已加载(仅 jieba 路径有意义) */
|
|
31
|
+
userDict?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 用户词典(可选注入):每行一条术语,或 `术语 词频`(jieba 用户词典格式) */
|
|
35
|
+
export const USER_DICT_PATH = ["data", "jieba", "userdict.txt"] as const
|
|
36
|
+
|
|
37
|
+
interface JiebaLike {
|
|
38
|
+
cut(text: string, hmm?: boolean): string[]
|
|
39
|
+
cutForSearch(text: string, hmm?: boolean): string[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let jieba: JiebaLike | null | undefined
|
|
43
|
+
let userDictLoaded = false
|
|
44
|
+
let loadFailureReason: string | null = null
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 构建实例:`@node-rs/jieba` v2 是 **class 形式** —— `Jieba.withDict(dict)`,
|
|
48
|
+
* 内置词典在子路径 `@node-rs/jieba/dict`(`Uint8Array`);v2 未暴露 `addWord`,
|
|
49
|
+
* 故用户词典走「内置词典文本 + 追加用户行 → 合并 bytes」再 `withDict`。
|
|
50
|
+
*/
|
|
51
|
+
function buildJieba(req: NodeRequire): JiebaLike {
|
|
52
|
+
const { Jieba } = req("@node-rs/jieba") as { Jieba: { withDict: (dict: Uint8Array) => JiebaLike } }
|
|
53
|
+
const { dict } = req("@node-rs/jieba/dict") as { dict: Uint8Array }
|
|
54
|
+
|
|
55
|
+
const userPath = join(PROJECT_ROOT, MAGIC_DIR, ...USER_DICT_PATH)
|
|
56
|
+
if (existsSync(userPath)) {
|
|
57
|
+
try {
|
|
58
|
+
const userLines = readFileSync(userPath, "utf-8")
|
|
59
|
+
.split("\n")
|
|
60
|
+
.map((l) => l.trim())
|
|
61
|
+
.filter((l) => l.length > 0 && !l.startsWith("#"))
|
|
62
|
+
if (userLines.length > 0) {
|
|
63
|
+
const baseDictPath = req.resolve("@node-rs/jieba/dict.txt") as string
|
|
64
|
+
const merged = Buffer.concat([
|
|
65
|
+
readFileSync(baseDictPath),
|
|
66
|
+
Buffer.from("\n" + userLines.join("\n"), "utf8"),
|
|
67
|
+
])
|
|
68
|
+
userDictLoaded = true
|
|
69
|
+
return Jieba.withDict(new Uint8Array(merged))
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
// 用户词典损坏 → 退回内置词典,但留下原因(禁止静默)
|
|
73
|
+
loadFailureReason = `userdict 读取失败(已退回内置词典): ${error instanceof Error ? error.message : String(error)}`
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return Jieba.withDict(dict)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function loadJieba(): JiebaLike | null {
|
|
80
|
+
if (jieba !== undefined) return jieba
|
|
81
|
+
try {
|
|
82
|
+
// 同步加载语义:@node-rs/jieba 导出同步 `cut`;用 createRequire 避免顶层 await 影响调用方。
|
|
83
|
+
// 注意:本模块是 ESM,**不能**用裸 `require`(运行时 ReferenceError 会被下面的 catch 吞掉 →
|
|
84
|
+
// 即使 jieba 装好了也会永远走兜底),必须用 node:module 的 createRequire。
|
|
85
|
+
const req = createRequire(import.meta.url)
|
|
86
|
+
jieba = buildJieba(req)
|
|
87
|
+
} catch (error) {
|
|
88
|
+
// 装不上(未声明 / 平台不匹配 / pnpm 严格布局不可达)→ 兜底,不抛错
|
|
89
|
+
jieba = null
|
|
90
|
+
loadFailureReason = error instanceof Error ? error.message : String(error)
|
|
91
|
+
}
|
|
92
|
+
return jieba
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** jieba 不可用时的原因(诊断用;`get_memory_health` / 召回审计可透出) */
|
|
96
|
+
export function jiebaUnavailableReason(): string | null {
|
|
97
|
+
return loadFailureReason
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 统一分词入口(**写入侧与查询侧都必须调它**)。
|
|
102
|
+
* jieba 可用 → 词级 token;不可用或切分为空 → bigram 兜底。
|
|
103
|
+
*/
|
|
104
|
+
export function segmentForMatch(text: string): SegmentResult {
|
|
105
|
+
const instance = loadJieba()
|
|
106
|
+
if (instance) {
|
|
107
|
+
try {
|
|
108
|
+
const terms = instance
|
|
109
|
+
// 检索场景用 `cutForSearch`:除整词外还产出子词(召回优先)
|
|
110
|
+
.cutForSearch(text, true)
|
|
111
|
+
.map((t) => t.trim().toLowerCase())
|
|
112
|
+
// 噪声过滤(2026-09-21 实测修正):
|
|
113
|
+
// · 必须含**字母/数字/CJK**——jieba 的 cutForSearch 会把 ":" "(" 这类标点也切出来,
|
|
114
|
+
// 而 bigram 契约只匹配 CJK 连续段,两者若不一致就会让索引里塞满无意义 token;
|
|
115
|
+
// · 单字符拉丁(如 "地块A" 切出的 "a")噪声大,丢弃;CJK 单字保留(与 bigram 契约一致)。
|
|
116
|
+
.filter((t) => /[\p{L}\p{N}]/u.test(t) && !(t.length === 1 && /^[\x00-\x7f]$/.test(t)))
|
|
117
|
+
if (terms.length > 0) {
|
|
118
|
+
return { terms: [...new Set(terms)], method: "jieba", userDict: userDictLoaded }
|
|
119
|
+
}
|
|
120
|
+
loadFailureReason = "jieba 切分返回空结果,已走 bigram 兜底"
|
|
121
|
+
} catch (error) {
|
|
122
|
+
// 切分异常 → 落到兜底,但记录原因(禁止静默降级)
|
|
123
|
+
loadFailureReason = `jieba 切分异常(已走 bigram 兜底): ${error instanceof Error ? error.message : String(error)}`
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { terms: tokenize(text), method: "bigram" }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 文档侧:展开为空格分隔 token 串(写入期落 `searchText`),并回报所用分词器 */
|
|
130
|
+
export function expandForIndexWithMethod(text: string): { text: string; method: SegmentMethod } {
|
|
131
|
+
const { terms, method } = segmentForMatch(text)
|
|
132
|
+
return { text: terms.join(" "), method }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** 查询侧:token 数组(与 `expandForIndexWithMethod` 同一实现) */
|
|
136
|
+
export function expandForQueryWithMethod(query: string): SegmentResult {
|
|
137
|
+
return segmentForMatch(query)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export { expandForQuery, tokenize }
|