add-coder 0.3.33 → 0.3.35

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.
Files changed (98) hide show
  1. package/dist/index.js +21 -3
  2. package/package.json +3 -3
  3. package/templates/.add-coder-src-hash.json +95 -41
  4. package/templates/adapters/claude/hooks/doc-format-guard.mjs +172 -84
  5. package/templates/adapters/claude/hooks/post-tool-use.mjs +59 -1
  6. package/templates/adapters/claude/hooks/prompt-submit.mjs +72 -0
  7. package/templates/adapters/claude/hooks/session-start.mjs +65 -1
  8. package/templates/adapters/codex/hooks/doc-format-guard.mjs +172 -84
  9. package/templates/adapters/codex/hooks/post-tool-use.mjs +59 -1
  10. package/templates/adapters/codex/hooks/prompt-submit.mjs +72 -0
  11. package/templates/adapters/codex/hooks/session-start.mjs +65 -1
  12. package/templates/adapters/qoder/hooks/doc-format-guard.mjs +172 -84
  13. package/templates/adapters/qoder/hooks/post-tool-use.mjs +59 -1
  14. package/templates/adapters/qoder/hooks/prompt-submit.mjs +72 -0
  15. package/templates/adapters/qoder/hooks/session-start.mjs +67 -1
  16. package/templates/adapters/trae/hooks/doc-format-guard.mjs +172 -84
  17. package/templates/adapters/trae/hooks/post-tool-use.mjs +59 -1
  18. package/templates/adapters/trae/hooks/prompt-submit.mjs +72 -0
  19. package/templates/adapters/trae/hooks/session-start.mjs +65 -1
  20. package/templates/adapters/vscode/hooks/doc-format-guard.mjs +172 -84
  21. package/templates/adapters/vscode/hooks/post-tool-use.mjs +59 -1
  22. package/templates/adapters/vscode/hooks/prompt-submit.mjs +72 -0
  23. package/templates/adapters/vscode/hooks/session-start.mjs +65 -1
  24. package/templates/core/governance/doc-format-guard.ts +29 -112
  25. package/templates/core/governance/post-tool-router.ts +33 -1
  26. package/templates/core/governance/prompt-router.ts +47 -0
  27. package/templates/core/governance/session-start-guard.ts +48 -1
  28. package/templates/core/prisma/add.prisma +203 -0
  29. package/templates/core/scripts/db-ensure.sh +61 -2
  30. package/templates/core/scripts/mcp-server/shared/db-types.ts +119 -0
  31. package/templates/core/scripts/mcp-server/shared/hitl-create-policy.ts +27 -0
  32. package/templates/core/scripts/mcp-server/shared/hitl-proposal-content.ts +110 -0
  33. package/templates/core/scripts/mcp-server/shared/hitl-widget-instance.ts +85 -0
  34. package/templates/core/scripts/mcp-server/shared/memory/calibration/batch-fit.ts +250 -0
  35. package/templates/core/scripts/mcp-server/shared/memory/calibration/feedback-stats.ts +101 -0
  36. package/templates/core/scripts/mcp-server/shared/memory/calibration/unit-state.ts +224 -0
  37. package/templates/core/scripts/mcp-server/shared/memory/domain/conflicts.ts +59 -0
  38. package/templates/core/scripts/mcp-server/shared/memory/domain/dedup.ts +45 -0
  39. package/templates/core/scripts/mcp-server/shared/memory/domain/errors.ts +33 -0
  40. package/templates/core/scripts/mcp-server/shared/memory/domain/handoff-digest.ts +92 -0
  41. package/templates/core/scripts/mcp-server/shared/memory/domain/metric-candidate.ts +79 -0
  42. package/templates/core/scripts/mcp-server/shared/memory/domain/scope.ts +94 -0
  43. package/templates/core/scripts/mcp-server/shared/memory/domain/secrets.ts +50 -0
  44. package/templates/core/scripts/mcp-server/shared/memory/domain/state-machine.ts +90 -0
  45. package/templates/core/scripts/mcp-server/shared/memory/embedding/index.ts +117 -0
  46. package/templates/core/scripts/mcp-server/shared/memory/embedding/local-onnx.ts +105 -0
  47. package/templates/core/scripts/mcp-server/shared/memory/embedding/openai-compatible.ts +87 -0
  48. package/templates/core/scripts/mcp-server/shared/memory/jobs/consolidation.ts +226 -0
  49. package/templates/core/scripts/mcp-server/shared/memory/jobs/evidence-collector.ts +153 -0
  50. package/templates/core/scripts/mcp-server/shared/memory/jobs/snapshot.ts +114 -0
  51. package/templates/core/scripts/mcp-server/shared/memory/metrics/gate-recall.ts +134 -0
  52. package/templates/core/scripts/mcp-server/shared/memory/metrics/gate-writer.ts +217 -0
  53. package/templates/core/scripts/mcp-server/shared/memory/metrics/stage-words.ts +69 -0
  54. package/templates/core/scripts/mcp-server/shared/memory/retrieval/context-builder.ts +89 -0
  55. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/pg.ts +139 -0
  56. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite-fts5.sql +29 -0
  57. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fts/sqlite.ts +106 -0
  58. package/templates/core/scripts/mcp-server/shared/memory/retrieval/fusion.ts +43 -0
  59. package/templates/core/scripts/mcp-server/shared/memory/retrieval/pipeline.ts +285 -0
  60. package/templates/core/scripts/mcp-server/shared/memory/retrieval/query-terms.ts +31 -0
  61. package/templates/core/scripts/mcp-server/shared/memory/retrieval/recall-writer.ts +87 -0
  62. package/templates/core/scripts/mcp-server/shared/memory/retrieval/reranker.ts +116 -0
  63. package/templates/core/scripts/mcp-server/shared/memory/retrieval/types.ts +52 -0
  64. package/templates/core/scripts/mcp-server/shared/memory/retrieval/vector/pgvector.ts +143 -0
  65. package/templates/core/scripts/mcp-server/shared/memory/retrieval/vector/sqlite-vec.ts +118 -0
  66. package/templates/core/scripts/mcp-server/shared/memory/switches.ts +39 -0
  67. package/templates/core/scripts/mcp-server/shared/review-files.ts +22 -0
  68. package/templates/core/scripts/mcp-server/shared/runtime-freshness.ts +235 -0
  69. package/templates/core/scripts/mcp-server/tools/gateway/check_dps.ts +41 -3
  70. package/templates/core/scripts/mcp-server/tools/gateway/check_rahs.ts +40 -1
  71. package/templates/core/scripts/mcp-server/tools/gateway/check_spec_sync.ts +2 -2
  72. package/templates/core/scripts/mcp-server/tools/hitl.ts +108 -42
  73. package/templates/core/scripts/mcp-server/tools/index.ts +7 -1
  74. package/templates/core/scripts/mcp-server/tools/memory-compat.ts +258 -0
  75. package/templates/core/scripts/mcp-server/tools/memory.ts +654 -0
  76. package/templates/core/scripts/mcp-server/tools/plan.ts +8 -3
  77. package/templates/core/scripts/mcp-server/tools/review.ts +10 -7
  78. package/templates/core/scripts/mcp-server.ts +36 -0
  79. package/templates/core/templates/checklist-template.md +13 -0
  80. package/templates/core/templates/review-implementation-template.md +24 -0
  81. package/templates/core/templates/review-template.md +16 -0
  82. package/templates/core/validation/index.ts +136 -0
  83. package/templates/core/validation/policy.ts +91 -0
  84. package/templates/core/validation/registry.ts +61 -0
  85. package/templates/core/validation/schema-validator.ts +277 -0
  86. package/templates/core/validation/validators/add-route.ts +32 -0
  87. package/templates/core/validation/validators/checklist.ts +48 -0
  88. package/templates/core/validation/validators/handoff.ts +46 -0
  89. package/templates/core/validation/validators/hitl.ts +22 -0
  90. package/templates/core/validation/validators/index.ts +52 -0
  91. package/templates/core/validation/validators/plan.ts +20 -0
  92. package/templates/core/validation/validators/report.ts +16 -0
  93. package/templates/core/validation/validators/review.ts +30 -0
  94. package/templates/core/validation/validators/spec.ts +25 -0
  95. package/templates/core/validation/validators/tasks.ts +40 -0
  96. package/templates/core/validation/validators/types.ts +32 -0
  97. package/templates/core/vocabulary/add-governance-vocabulary.md +18 -0
  98. package/templates/core/scripts/mcp-server/shared/dps-spec-ref.ts +0 -17
@@ -0,0 +1,94 @@
1
+ /*
2
+ * Memory Scope 规则(Plan §7.2)
3
+ *
4
+ * 优先级:symbol > path > module > branch > repository > organization
5
+ * ORGANIZATION 首版禁用(写入拒绝,§17-7);PLAN/SPEC 只在对应流程或显式历史查询中参与。
6
+ */
7
+ import { MemoryError } from "./errors.js"
8
+
9
+ export type MemoryScopeType =
10
+ | "ORGANIZATION" | "REPOSITORY" | "BRANCH" | "MODULE"
11
+ | "PATH" | "SYMBOL" | "PLAN" | "SPEC"
12
+
13
+ export interface Scope {
14
+ type: MemoryScopeType
15
+ value: string
16
+ }
17
+
18
+ /** 召回时的上下文坐标(由调用方从运行时环境组装) */
19
+ export interface ScopeContext {
20
+ repository: string
21
+ branch?: string
22
+ module?: string
23
+ paths?: string[]
24
+ symbols?: string[]
25
+ planKeyword?: string
26
+ specRef?: string
27
+ /** 显式历史查询时放行 PLAN/SPEC scope(默认 false) */
28
+ includePlanScope?: boolean
29
+ }
30
+
31
+ const RANK: Record<MemoryScopeType, number> = {
32
+ SYMBOL: 7,
33
+ PATH: 6,
34
+ MODULE: 5,
35
+ BRANCH: 4,
36
+ REPOSITORY: 3,
37
+ PLAN: 2,
38
+ SPEC: 2,
39
+ ORGANIZATION: 1,
40
+ }
41
+
42
+ export function scopeRank(t: MemoryScopeType): number {
43
+ return RANK[t]
44
+ }
45
+
46
+ /** 写入守卫:ORGANIZATION 首版禁用 */
47
+ export function assertScopeWritable(scope: Scope): void {
48
+ if (scope.type === "ORGANIZATION") {
49
+ throw new MemoryError("ERR_ORG_SCOPE_DISABLED")
50
+ }
51
+ }
52
+
53
+ /** 判定一条 Memory 的 scope 在当前上下文是否有效(召回过滤用) */
54
+ export function scopeApplies(memScope: Scope, ctx: ScopeContext): boolean {
55
+ switch (memScope.type) {
56
+ case "ORGANIZATION":
57
+ return false // 首版禁用
58
+ case "REPOSITORY":
59
+ return memScope.value === ctx.repository
60
+ case "BRANCH":
61
+ return !!ctx.branch && ctx.branch === memScope.value
62
+ case "MODULE":
63
+ // module 语义:当前任一路径以该模块目录为前缀
64
+ return !!ctx.module && ctx.module === memScope.value ||
65
+ (ctx.paths ?? []).some((p) => p === memScope.value || p.startsWith(memScope.value + "/"))
66
+ case "PATH":
67
+ return (ctx.paths ?? []).some(
68
+ (p) => p === memScope.value || p.startsWith(memScope.value.replace(/\/?$/, "/")),
69
+ )
70
+ case "SYMBOL":
71
+ return (ctx.symbols ?? []).includes(memScope.value)
72
+ case "PLAN":
73
+ return !!ctx.includePlanScope && !!ctx.planKeyword && ctx.planKeyword === memScope.value
74
+ case "SPEC":
75
+ return !!ctx.includePlanScope && !!ctx.specRef && ctx.specRef === memScope.value
76
+ }
77
+ }
78
+
79
+ /**
80
+ * supersede 兼容性(Plan §4.4):新旧 Memory 的 repository/scope 必须兼容。
81
+ * 兼容 = 同类型同值;或 PATH/MODULE 类型间存在前缀覆盖;或一方为 REPOSITORY(全库覆盖)。
82
+ */
83
+ export function scopesCompatible(a: Scope, b: Scope): boolean {
84
+ if (a.type === b.type && a.value === b.value) return true
85
+ if (a.type === "REPOSITORY" || b.type === "REPOSITORY") {
86
+ return a.type === "REPOSITORY" && b.type === "REPOSITORY" ? a.value === b.value : true
87
+ }
88
+ const pathLike = (t: MemoryScopeType) => t === "PATH" || t === "MODULE"
89
+ if (pathLike(a.type) && pathLike(b.type)) {
90
+ return a.value.startsWith(b.value.replace(/\/?$/, "/")) ||
91
+ b.value.startsWith(a.value.replace(/\/?$/, "/"))
92
+ }
93
+ return false
94
+ }
@@ -0,0 +1,50 @@
1
+ /*
2
+ * 密钥/凭证扫描(Plan §11:Evidence 摘录在写入前执行秘密与敏感信息检测)
3
+ * 命中即拒写(ERR_SECRET_DETECTED)。宁可误报,不可漏报。
4
+ */
5
+ import { MemoryError } from "./errors.js"
6
+
7
+ export interface SecretHit {
8
+ pattern: string
9
+ index: number
10
+ preview: string
11
+ }
12
+
13
+ interface SecretPattern {
14
+ name: string
15
+ re: RegExp
16
+ }
17
+
18
+ const PATTERNS: SecretPattern[] = [
19
+ { name: "private-key-block", re: /-----BEGIN (?:RSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/ },
20
+ { name: "aws-access-key", re: /\bAKIA[0-9A-Z]{16}\b/ },
21
+ { name: "github-token", re: /\b(?:ghp|gho|ghu|ghs|ghr|github_pat)_[A-Za-z0-9_]{20,}\b/ },
22
+ { name: "openai-style-key", re: /\bsk-[A-Za-z0-9_-]{20,}\b/ },
23
+ { name: "slack-token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/ },
24
+ { name: "bearer-token", re: /\bBearer\s+[A-Za-z0-9._~+/=-]{20,}\b/i },
25
+ { name: "conn-string-password", re: /(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis):\/\/[^:\s/]+:[^@\s]+@/i },
26
+ { name: "generic-secret-assign", re: /\b(?:password|passwd|secret|api[_-]?key|access[_-]?token)\b\s*[:=]\s*["'][^"'\s]{8,}["']/i },
27
+ { name: "jwt", re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/ },
28
+ ]
29
+
30
+ export function scanSecrets(text: string): SecretHit[] {
31
+ const hits: SecretHit[] = []
32
+ for (const p of PATTERNS) {
33
+ const m = p.re.exec(text)
34
+ if (m) {
35
+ hits.push({
36
+ pattern: p.name,
37
+ index: m.index,
38
+ preview: text.slice(Math.max(0, m.index - 10), m.index + 20).replace(/\s+/g, " ").slice(0, 40),
39
+ })
40
+ }
41
+ }
42
+ return hits
43
+ }
44
+
45
+ export function assertNoSecrets(text: string): void {
46
+ const hits = scanSecrets(text)
47
+ if (hits.length > 0) {
48
+ throw new MemoryError("ERR_SECRET_DETECTED", hits.map((h) => h.pattern).join(","))
49
+ }
50
+ }
@@ -0,0 +1,90 @@
1
+ /*
2
+ * Memory 生命周期状态机(Plan §5)
3
+ *
4
+ * CANDIDATE → PENDING → ACTIVE → STALE → SUPERSEDED → ARCHIVED
5
+ * │ │ │ └──────────────→ ARCHIVED
6
+ * └──────────┴─────────┴──────────────────────→ REJECTED
7
+ *
8
+ * 首版默认只允许人工或显式治理调用激活(approve 的强制校验在此表达)。
9
+ */
10
+ import { MemoryError } from "./errors.js"
11
+
12
+ export type MemoryStatus =
13
+ | "CANDIDATE" | "PENDING" | "ACTIVE" | "STALE"
14
+ | "SUPERSEDED" | "REJECTED" | "ARCHIVED"
15
+
16
+ export type MemoryAction =
17
+ | "propose" | "submit_review" | "approve" | "reject"
18
+ | "mark_stale" | "supersede" | "archive" | "restore"
19
+
20
+ interface Transition {
21
+ from: MemoryStatus[]
22
+ to: MemoryStatus
23
+ }
24
+
25
+ /** 迁移表:propose 是创建语义(无 from),其余均为状态到状态 */
26
+ export const TRANSITIONS: Record<MemoryAction, Transition> = {
27
+ propose: { from: [], to: "CANDIDATE" },
28
+ submit_review: { from: ["CANDIDATE"], to: "PENDING" },
29
+ approve: { from: ["PENDING", "CANDIDATE"], to: "ACTIVE" },
30
+ reject: { from: ["CANDIDATE", "PENDING"], to: "REJECTED" },
31
+ mark_stale: { from: ["ACTIVE"], to: "STALE" },
32
+ supersede: { from: ["ACTIVE", "STALE"], to: "SUPERSEDED" },
33
+ archive: { from: ["ACTIVE", "STALE", "SUPERSEDED"], to: "ARCHIVED" },
34
+ restore: { from: ["ARCHIVED", "REJECTED"], to: "CANDIDATE" },
35
+ }
36
+
37
+ /** approve / supersede 的不变量校验输入(DB 查询结果由调用方传入,领域层保持纯函数) */
38
+ export interface TransitionContext {
39
+ evidenceCount?: number
40
+ approvedBy?: string | null
41
+ approvedAt?: Date | null
42
+ /** supersede 专用:新记忆 id 与 repository/scope 兼容性判定结果 */
43
+ supersededById?: string | null
44
+ supersessionCompatible?: boolean
45
+ }
46
+
47
+ export interface TransitionResult {
48
+ from: MemoryStatus
49
+ to: MemoryStatus
50
+ action: MemoryAction
51
+ }
52
+
53
+ /** 判定迁移是否合法;非法时抛 MemoryError(ERR_ILLEGAL_TRANSITION / ERR_EVIDENCE_REQUIRED / ...) */
54
+ export function assertTransition(
55
+ current: MemoryStatus,
56
+ action: MemoryAction,
57
+ ctx: TransitionContext = {},
58
+ ): TransitionResult {
59
+ const t = TRANSITIONS[action]
60
+ if (action === "propose") {
61
+ // 创建语义:不与现有状态冲突(由调用方决定新建或合并)
62
+ return { from: current, to: t.to, action }
63
+ }
64
+ if (!t.from.includes(current)) {
65
+ throw new MemoryError("ERR_ILLEGAL_TRANSITION", `${current} --${action}--> ${t.to} 不允许(允许起点: ${t.from.join("/")})`)
66
+ }
67
+ if (action === "approve") {
68
+ if (!ctx.evidenceCount || ctx.evidenceCount < 1) {
69
+ throw new MemoryError("ERR_EVIDENCE_REQUIRED")
70
+ }
71
+ if (!ctx.approvedBy) {
72
+ throw new MemoryError("ERR_APPROVAL_REQUIRED", "缺少 approvedBy")
73
+ }
74
+ }
75
+ if (action === "supersede") {
76
+ if (!ctx.supersededById) {
77
+ throw new MemoryError("ERR_SUPERSESSION_INVALID", "缺少 supersededById")
78
+ }
79
+ if (ctx.supersessionCompatible === false) {
80
+ throw new MemoryError("ERR_SUPERSESSION_INVALID", "新旧 Memory 的 repository/scope 不兼容")
81
+ }
82
+ }
83
+ return { from: current, to: t.to, action }
84
+ }
85
+
86
+ /** 默认召回允许的状态集合(Plan §7.1:默认排除 CANDIDATE/PENDING/REJECTED/ARCHIVED/SUPERSEDED/过期项) */
87
+ export const DEFAULT_RECALL_STATUSES: readonly MemoryStatus[] = ["ACTIVE"]
88
+
89
+ /** 显式诊断模式额外放行 STALE(带警告) */
90
+ export const DIAGNOSTIC_RECALL_STATUSES: readonly MemoryStatus[] = ["ACTIVE", "STALE"]
@@ -0,0 +1,117 @@
1
+ /*
2
+ * Embedding 抽象与降级(Plan §8.1/§8.4,Spec §9)
3
+ *
4
+ * 提供者:none(默认,显式不可用)/ local-onnx / openai-compatible(Phase 5 轮 3)。
5
+ * Vector adapter:pgvector / sqlite-vec(retrieval/vector/),能力检测失败即降级。
6
+ * 契约:记忆系统任何环节不得因 embedding 缺失而阻塞 Gate(FTS-only 合法降级)。
7
+ */
8
+ import { MemoryError } from "../domain/errors.js"
9
+ import type { ComponentHealth, RankedId, RecallFilter } from "../retrieval/types.js"
10
+
11
+ export interface EmbeddingProvider {
12
+ readonly id: string // "none" | "local-onnx" | "openai-compatible" | "custom"
13
+ readonly dimension: number
14
+ embed(texts: string[]): Promise<number[][]>
15
+ health(): Promise<ComponentHealth>
16
+ }
17
+
18
+ export interface VectorSearchAdapter {
19
+ search(vector: number[], filter: RecallFilter, limit: number): Promise<RankedId[]>
20
+ upsert(memoryId: string, vector: number[], model: string): Promise<void>
21
+ health(): Promise<ComponentHealth>
22
+ }
23
+
24
+ /** 首版唯一 provider:显式不可用。调用方据此走 FTS-only 并标注 degradedMode */
25
+ export function createNoneEmbeddingProvider(): EmbeddingProvider {
26
+ return {
27
+ id: "none",
28
+ dimension: 0,
29
+ embed: (): Promise<number[][]> => Promise.reject(new MemoryError("ERR_EMBEDDING_DISABLED")),
30
+ health: (): Promise<ComponentHealth> =>
31
+ Promise.resolve({ component: "embedding", status: "disabled", detail: "EmbeddingProvider=none(首版定案,FTS-only)" }),
32
+ }
33
+ }
34
+
35
+ /** Vector adapter capability 存根:health 报告 unavailable,search/upsert 拒绝 */
36
+ export function createUnavailableVectorAdapter(): VectorSearchAdapter {
37
+ return {
38
+ search: (): Promise<RankedId[]> =>
39
+ Promise.reject(new MemoryError("ERR_EMBEDDING_DISABLED", "VectorSearchAdapter 未配置(Phase 5 接入)")),
40
+ upsert: (): Promise<void> =>
41
+ Promise.reject(new MemoryError("ERR_EMBEDDING_DISABLED", "VectorSearchAdapter 未配置(Phase 5 接入)")),
42
+ health: (): Promise<ComponentHealth> =>
43
+ Promise.resolve({ component: "vector-search", status: "unavailable", detail: "向量索引未启用(Phase 5 接入 pgvector/sqlite-vec)" }),
44
+ }
45
+ }
46
+
47
+ /**
48
+ * 维度真源校验(Review R5 回流):provider 上报维度必须与 DB 侧声明维度一致,否则拒写。
49
+ * declaredDim 来自 AddMemory.embeddingDim(写入路径的真源)。
50
+ */
51
+ export function assertEmbeddingDimension(providerDim: number, declaredDim: number): void {
52
+ if (declaredDim <= 0) return // 未声明维度:首写,由 provider 值落库
53
+ if (providerDim !== declaredDim) {
54
+ throw new MemoryError(
55
+ "ERR_DIMENSION_MISMATCH",
56
+ `嵌入维度不一致:provider=${providerDim} vs 声明=${declaredDim}(拒绝写入,避免脏向量)`,
57
+ )
58
+ }
59
+ }
60
+
61
+ export type EmbeddingMode = "none" | "local-onnx" | "openai-compatible"
62
+
63
+ export interface EmbeddingEnvConfig {
64
+ mode?: string
65
+ model?: string
66
+ dimension?: number
67
+ baseUrl?: string
68
+ apiKey?: string
69
+ cacheDir?: string
70
+ remoteHost?: string
71
+ }
72
+
73
+ /** 从环境变量解析提供者配置(纯函数,便于测试与文档化) */
74
+ export function parseEmbeddingEnv(env: NodeJS.ProcessEnv = process.env): EmbeddingEnvConfig {
75
+ const mode = (env.ADD_MEMORY_EMBEDDING ?? "none").toLowerCase()
76
+ const dim = Number(env.ADD_MEMORY_EMBEDDING_DIM ?? "")
77
+ return {
78
+ mode,
79
+ model: env.ADD_MEMORY_EMBEDDING_MODEL,
80
+ dimension: Number.isFinite(dim) && dim > 0 ? Math.floor(dim) : undefined,
81
+ baseUrl: env.ADD_MEMORY_EMBEDDING_BASE_URL,
82
+ apiKey: env.ADD_MEMORY_EMBEDDING_API_KEY,
83
+ cacheDir: env.ADD_MEMORY_EMBEDDING_CACHE_DIR,
84
+ remoteHost: env.ADD_MEMORY_EMBEDDING_REMOTE_HOST,
85
+ }
86
+ }
87
+
88
+ /**
89
+ * 按配置装配提供者(默认 none → 行为与首版一致)。
90
+ * local-onnx / openai-compatible 走动态 import,未配置的路径不加载任何重依赖。
91
+ */
92
+ export async function createEmbeddingProviderFromConfig(
93
+ config: EmbeddingEnvConfig = parseEmbeddingEnv(),
94
+ ): Promise<EmbeddingProvider> {
95
+ const mode = (config.mode ?? "none") as EmbeddingMode
96
+ if (mode === "local-onnx") {
97
+ const { createLocalOnnxEmbeddingProvider } = await import("./local-onnx.js")
98
+ return createLocalOnnxEmbeddingProvider({
99
+ model: config.model,
100
+ cacheDir: config.cacheDir,
101
+ remoteHost: config.remoteHost,
102
+ })
103
+ }
104
+ if (mode === "openai-compatible") {
105
+ if (!config.baseUrl) {
106
+ throw new MemoryError("ERR_EMBEDDING_DISABLED", "openai-compatible 需要 ADD_MEMORY_EMBEDDING_BASE_URL")
107
+ }
108
+ const { createOpenAiCompatibleEmbeddingProvider } = await import("./openai-compatible.js")
109
+ return createOpenAiCompatibleEmbeddingProvider({
110
+ baseUrl: config.baseUrl,
111
+ model: config.model ?? "text-embedding-3-small",
112
+ apiKey: config.apiKey,
113
+ dimension: config.dimension,
114
+ })
115
+ }
116
+ return createNoneEmbeddingProvider()
117
+ }
@@ -0,0 +1,105 @@
1
+ /*
2
+ * 本地 ONNX 嵌入提供者(Plan §3.4 轮 3 Task 3.1 / Spec §6 §VectorCapability)
3
+ *
4
+ * 关键约束:
5
+ * 1. **可选依赖**:@huggingface/transformers 与模型权重都可能在目标机器缺失——加载失败
6
+ * 必须收敛为 ERR_EMBEDDING_DISABLED(调用方据此走 fts-only),绝不抛裸异常阻塞 Gate;
7
+ * 2. **维度以模型元数据为真源**:dimension 由首次推理的实际输出长度决定(不硬编码),
8
+ * 与 DB 侧 AddMemory.embeddingDim 不一致时由调用方拒绝写入(Review R5 回流);
9
+ * 3. 惰性加载:不在模块顶层 import/下载模型,首次 embed 时才初始化。
10
+ */
11
+ import { MemoryError } from "../domain/errors.js"
12
+ import type { ComponentHealth } from "../retrieval/types.js"
13
+ import type { EmbeddingProvider } from "./index.js"
14
+
15
+ /** 默认模型:中文小模型(真源维度 512,与 Plan R5「维度真源」一致) */
16
+ export const DEFAULT_LOCAL_ONNX_MODEL = "Xenova/bge-small-zh-v1.5"
17
+
18
+ export interface LocalOnnxOptions {
19
+ model?: string
20
+ /** 显式缓存目录;缺省复用 HF_HUB_CACHE → HF_HOME/hub → ~/.cache/huggingface/hub */
21
+ cacheDir?: string
22
+ /** 镜像站(内网/受限网络) */
23
+ remoteHost?: string
24
+ /** 测试注入:直接给定 pipeline 工厂 */
25
+ loadPipeline?: (model: string) => Promise<(texts: string[]) => Promise<number[][]>>
26
+ }
27
+
28
+ export interface LocalOnnxProvider extends EmbeddingProvider {
29
+ /** 首次推理后可用;未初始化时为 0(调用方以 assertEmbeddingDimension 校验) */
30
+ readonly resolvedDimension: number
31
+ }
32
+
33
+ export function createLocalOnnxEmbeddingProvider(opts: LocalOnnxOptions = {}): LocalOnnxProvider {
34
+ const model = opts.model ?? DEFAULT_LOCAL_ONNX_MODEL
35
+ let embedFn: ((texts: string[]) => Promise<number[][]>) | null = null
36
+ let dimension = 0
37
+
38
+ async function ensureLoaded(): Promise<(texts: string[]) => Promise<number[][]>> {
39
+ if (embedFn) return embedFn
40
+ try {
41
+ if (opts.loadPipeline) {
42
+ embedFn = await opts.loadPipeline(model)
43
+ } else {
44
+ const { pipeline, env } = await import("@huggingface/transformers")
45
+ if (opts.cacheDir) env.cacheDir = opts.cacheDir
46
+ if (opts.remoteHost) env.remoteHost = opts.remoteHost
47
+ const extractor = await pipeline("feature-extraction", model)
48
+ embedFn = async (texts: string[]) => {
49
+ const result = await extractor(texts, { pooling: "mean", normalize: true })
50
+ const list = result.tolist() as number[][]
51
+ return list.length === texts.length ? list : [list as unknown as number[]]
52
+ }
53
+ }
54
+ return embedFn
55
+ } catch (error) {
56
+ throw new MemoryError(
57
+ "ERR_EMBEDDING_DISABLED",
58
+ `本地 ONNX 模型不可用(model=${model}):${error instanceof Error ? error.message : String(error)}`,
59
+ )
60
+ }
61
+ }
62
+
63
+ return {
64
+ id: "local-onnx",
65
+ get dimension(): number {
66
+ return dimension
67
+ },
68
+ get resolvedDimension(): number {
69
+ return dimension
70
+ },
71
+ async embed(texts: string[]): Promise<number[][]> {
72
+ if (texts.length === 0) return []
73
+ const fn = await ensureLoaded()
74
+ let vectors: number[][]
75
+ try {
76
+ vectors = await fn(texts)
77
+ } catch (error) {
78
+ throw new MemoryError(
79
+ "ERR_EMBEDDING_DISABLED",
80
+ `本地 ONNX 推理失败(model=${model}):${error instanceof Error ? error.message : String(error)}`,
81
+ )
82
+ }
83
+ const dim = vectors[0]?.length ?? 0
84
+ if (dim === 0) {
85
+ throw new MemoryError("ERR_EMBEDDING_DISABLED", `本地 ONNX 返回空向量(model=${model})`)
86
+ }
87
+ // 维度真源:以模型实际输出为准;同一 provider 内必须自洽
88
+ if (dimension !== 0 && dimension !== dim) {
89
+ throw new MemoryError("ERR_DIMENSION_MISMATCH", `provider 维度漂移:${dimension} → ${dim}`)
90
+ }
91
+ dimension = dim
92
+ return vectors
93
+ },
94
+ health(): Promise<ComponentHealth> {
95
+ if (embedFn && dimension > 0) {
96
+ return Promise.resolve({ component: "embedding", status: "ok", detail: `local-onnx model=${model} dim=${dimension}` })
97
+ }
98
+ return Promise.resolve({
99
+ component: "embedding",
100
+ status: "degraded",
101
+ detail: `local-onnx 未初始化(model=${model});首次 embed 时加载,失败则降级 fts-only`,
102
+ })
103
+ },
104
+ }
105
+ }
@@ -0,0 +1,87 @@
1
+ /*
2
+ * OpenAI 兼容嵌入提供者(Plan §3.4 轮 3 Task 3.1)
3
+ *
4
+ * 适用于任意 /v1/embeddings 兼容端点(本地 vLLM / 自建网关 / 官方 API)。
5
+ * 契约同 local-onnx:维度以首次响应为准(配置项 dimension 仅作期望值校验),
6
+ * 任何失败收敛为 ERR_EMBEDDING_DISABLED,不阻塞 Gate。
7
+ */
8
+ import { MemoryError } from "../domain/errors.js"
9
+ import type { ComponentHealth } from "../retrieval/types.js"
10
+ import type { EmbeddingProvider } from "./index.js"
11
+
12
+ export interface OpenAiCompatibleOptions {
13
+ baseUrl: string
14
+ model: string
15
+ apiKey?: string
16
+ /** 期望维度(可选):与首次响应不一致时报 ERR_DIMENSION_MISMATCH */
17
+ dimension?: number
18
+ timeoutMs?: number
19
+ /** 测试注入 */
20
+ fetchImpl?: typeof fetch
21
+ }
22
+
23
+ export function createOpenAiCompatibleEmbeddingProvider(
24
+ opts: OpenAiCompatibleOptions,
25
+ ): EmbeddingProvider {
26
+ const doFetch = opts.fetchImpl ?? fetch
27
+ const endpoint = `${opts.baseUrl.replace(/\/+$/, "")}/embeddings`
28
+ let dimension = opts.dimension ?? 0
29
+ let lastError: string | null = null
30
+
31
+ return {
32
+ id: "openai-compatible",
33
+ get dimension(): number {
34
+ return dimension
35
+ },
36
+ async embed(texts: string[]): Promise<number[][]> {
37
+ if (texts.length === 0) return []
38
+ const controller = new AbortController()
39
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 15_000)
40
+ let payload: { data?: { embedding?: number[] }[] }
41
+ try {
42
+ const res = await doFetch(endpoint, {
43
+ method: "POST",
44
+ headers: {
45
+ "content-type": "application/json",
46
+ ...(opts.apiKey ? { authorization: `Bearer ${opts.apiKey}` } : {}),
47
+ },
48
+ body: JSON.stringify({ model: opts.model, input: texts }),
49
+ signal: controller.signal,
50
+ })
51
+ if (!res.ok) {
52
+ throw new Error(`HTTP ${res.status}`)
53
+ }
54
+ payload = (await res.json()) as { data?: { embedding?: number[] }[] }
55
+ } catch (error) {
56
+ lastError = error instanceof Error ? error.message : String(error)
57
+ throw new MemoryError("ERR_EMBEDDING_DISABLED", `兼容端点调用失败(${endpoint}):${lastError}`)
58
+ } finally {
59
+ clearTimeout(timer)
60
+ }
61
+
62
+ const vectors = (payload.data ?? []).map((d) => d.embedding ?? []).filter((v) => v.length > 0)
63
+ if (vectors.length === 0) {
64
+ throw new MemoryError("ERR_EMBEDDING_DISABLED", `兼容端点未返回向量(model=${opts.model})`)
65
+ }
66
+ const dim = vectors[0].length
67
+ if (dimension !== 0 && dimension !== dim) {
68
+ throw new MemoryError("ERR_DIMENSION_MISMATCH", `期望维度 ${dimension},实收 ${dim}`)
69
+ }
70
+ dimension = dim
71
+ lastError = null
72
+ return vectors
73
+ },
74
+ health(): Promise<ComponentHealth> {
75
+ if (dimension > 0) {
76
+ return Promise.resolve({ component: "embedding", status: "ok", detail: `openai-compatible model=${opts.model} dim=${dimension}` })
77
+ }
78
+ return Promise.resolve({
79
+ component: "embedding",
80
+ status: lastError ? "unavailable" : "degraded",
81
+ detail: lastError
82
+ ? `兼容端点不可用:${lastError}`
83
+ : `兼容端点未调用(model=${opts.model},endpoint=${endpoint})`,
84
+ })
85
+ },
86
+ }
87
+ }