@h-ai/ai 0.1.0-alpha.67 → 0.1.0-alpha.68

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 CHANGED
@@ -24,8 +24,28 @@ AI 能力模块,提供统一的 `ai` 服务对象,覆盖 LLM 对话、文生
24
24
  - `ai.audio`:语音识别(ASR)与语音合成(TTS),支持完整与流式调用,覆盖 OpenAI / MiMo / Qwen / 豆包平台。
25
25
  - `ai.image`:文生图,覆盖 OpenAI GPT Image、Google Gemini Image / Nano Banana、Qwen-Image 2.0/3.0、Seedream 4.x/5.x 与 Pollinations 免费额度模型。
26
26
  - `ai.a2a`:Agent-to-Agent 请求处理与远端调用。
27
- - `@h-ai/ai/client`:前端轻量客户端(配合 API 服务)。
28
- - `AIStoreProvider`:统一存储抽象;无数据库时默认使用进程内临时 Store,已初始化 reldb + vecdb 时自动使用持久化 DB Provider。
27
+ - `@h-ai/ai/client`:独立浏览器 Audio WebSocket 客户端。
28
+
29
+ `@h-ai/ai/client` 仅提供 `createAudioClient({ url, getTicket })` 与 Audio 客户端类型。HTTP 使用 `@h-ai/api-client` 的 typed 领域方法;默认聊天补全非流式,流与 A2A 使用应用自行绑定的 contract。旧字符串路径客户端和重复 SSE helper 已删除,浏览器不导出 Node 单例 `ai`。
30
+
31
+ ```ts
32
+ import { apiClient } from '@h-ai/api-client'
33
+
34
+ const ready = await apiClient.init({ baseUrl: 'https://api.example.com/api/v1' })
35
+ if (!ready.success)
36
+ throw new Error(ready.error.message)
37
+ try {
38
+ const response = await apiClient.ai.chats.createCompletion({ messages: [{ role: 'user', content: '你好' }] })
39
+ if (!response.success)
40
+ throw new Error(response.error.message)
41
+ const reply = response.data.choices[0]?.message.content
42
+ }
43
+ finally {
44
+ await apiClient.close()
45
+ }
46
+ ```
47
+
48
+ - `AIStoreProvider`:统一存储抽象;生产要求 `stateScope: 'shared'`,内置实现须先初始化 PostgreSQL/MySQL 与网络 vecdb。开发/显式测试无数据库时使用临时 Store。
29
49
 
30
50
  更完整的方法清单、错误码与长示例见 [REFERENCE.md](./REFERENCE.md)。
31
51
 
@@ -33,7 +53,7 @@ AI 能力模块,提供统一的 `ai` 服务对象,覆盖 LLM 对话、文生
33
53
 
34
54
  ### 快速开始
35
55
 
36
- #### LLM-only(零数据库依赖)
56
+ #### 开发 LLM-only(零数据库依赖)
37
57
 
38
58
  ```ts
39
59
  import { ai } from '@h-ai/ai'
@@ -54,7 +74,7 @@ const result = await ai.llm.chat({
54
74
  await ai.close()
55
75
  ```
56
76
 
57
- 未初始化 reldb/vecdb 时,AI 使用进程内临时 Store 保存会话等运行时状态;`ai.close()`、进程退出或多实例切换后数据不会保留。需要 Memory、Context、Persona 或会话跨重启持久化时,使用下面的 DB Provider。
77
+ 开发/显式测试未初始化 reldb/vecdb 时,AI 使用进程内临时 Store;关闭、退出或切换实例后不保留。`NODE_ENV` 或 `HAI_CORE_ENV` 为 production 时,临时 Store、SQLite 或本地向量后端会使初始化失败。生产必须先初始化共享 RelDB 与网络 vecdb,或传入满足同等原子契约的共享 Provider。
58
78
 
59
79
  #### 持久化 DB Provider(reldb + vecdb)
60
80
 
@@ -63,31 +83,42 @@ import { ai } from '@h-ai/ai'
63
83
  import { reldb } from '@h-ai/reldb'
64
84
  import { vecdb } from '@h-ai/vecdb'
65
85
 
66
- await reldb.init({ type: 'sqlite', database: './ai.db' })
67
- await vecdb.init({ type: 'lancedb', path: './ai-vec.db' })
68
-
69
- const init = await ai.init({
70
- llm: {
71
- model: 'gpt-4o-mini',
72
- apiKey: process.env.HAI_AI_LLM_APIKEY,
73
- },
74
- })
75
- if (!init.success) {
76
- // 按 init.error.code 处理配置/依赖错误
77
- }
78
-
79
- const result = await ai.llm.chat({
80
- messages: [{ role: 'user', content: '你好!' }],
81
- })
82
- if (result.success) {
86
+ // 开发/测试使用本地后端;生产改为共享 PostgreSQL/MySQL 与网络 vecdb。
87
+ const failures: unknown[] = []
88
+ try {
89
+ const database = await reldb.init({ type: 'sqlite', database: './ai.db' })
90
+ if (!database.success)
91
+ throw new Error(database.error.message)
92
+ const vectors = await vecdb.init({ type: 'lancedb', path: './ai-vec.db' })
93
+ if (!vectors.success)
94
+ throw new Error(vectors.error.message)
95
+ const initialized = await ai.init({
96
+ llm: { model: 'gpt-4o-mini', apiKey: process.env.HAI_AI_LLM_APIKEY },
97
+ })
98
+ if (!initialized.success)
99
+ throw new Error(initialized.error.message)
100
+ const result = await ai.llm.chat({ messages: [{ role: 'user', content: 'Hello!' }] })
101
+ if (!result.success)
102
+ throw new Error(result.error.message)
83
103
  const reply = result.data.choices[0]?.message.content ?? ''
84
104
  }
85
-
86
- await ai.close()
87
- await vecdb.close()
88
- await reldb.close()
105
+ catch (error) {
106
+ failures.push(error)
107
+ }
108
+ finally {
109
+ await ai.close()
110
+ for (const service of [vecdb, reldb]) {
111
+ const closed = await service.close()
112
+ if (!closed.success)
113
+ failures.push(new Error(closed.error.message))
114
+ }
115
+ }
116
+ if (failures.length)
117
+ throw new AggregateError(failures, 'AI operation or cleanup failed')
89
118
  ```
90
119
 
120
+ MySQL 首次初始化允许多个节点并发建索引:仅接受 mysql2 明确的 ER_DUP_KEYNAME/1061,并重新核对列及顺序、非唯一完整列、升序可见 BTREE 和无表达式;已有同名错误结构仍失败。账户需要建表和 INDEX 权限,元数据读取、权限或其他 DDL 错误使 ai.init 返回 CONFIGURATION_ERROR。返回前等待全部已启动 DDL 结束;MySQL DDL 不回滚,失败保留已创建结构,修复权限/结构后再重试。逐步检查初始化结果,失败不接收业务请求,并逆序关闭 AI、vecdb、reldb。
121
+
91
122
  #### 自定义 StoreProvider
92
123
 
93
124
  ```ts
@@ -110,7 +141,9 @@ await ai.close()
110
141
  - 生命周期为 `await ai.init(config, options?)` / `await ai.close()`;关闭会等待自定义 `AIStoreProvider.close()`。
111
142
  - 领域方法返回 `HaiResult<T>` 或 `Promise<HaiResult<T>>`;业务失败通过 `result.success === false` 和 `result.error.code` 表达。流式 `AsyncIterable`、客户端传输和第三方回调在建立或迭代期间可能抛异常。
112
143
  - `ai.tools` 与 `ai.stream` 是纯函数子系统,无需初始化即可使用。
113
- - 未初始化 reldb/vecdb 时默认使用进程内临时 Store;两者均已初始化时自动使用 DB Provider。自定义 Provider 可隐藏其他存储后端。
144
+ - 开发/测试缺少 reldb/vecdb 时使用临时 Store;两者均已初始化时自动使用 DB Provider。生产校验真实数据库与向量后端共享能力,自定义 Provider 必须如实声明 `stateScope`。
145
+
146
+ 公共扩展通过 AIStoreProvider 注入 Store;内置 LLM Provider/工厂依赖与 Context 内部依赖不属于公开 API。模型接入使用配置,历史查询、AskOptions 和 ChatRecord 保持公开。
114
147
 
115
148
  ### API 概览
116
149
 
@@ -144,6 +177,8 @@ const temp = await ai.llm.chat({
144
177
  })
145
178
  ```
146
179
 
180
+ SSE 文本解码遵循 [WHATWG 事件流规则](https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation):LF/CRLF/CR 跨片、多行 data、仅去冒号后一个空格、继承有效 id/retry;空或缺省 event 按 message,EOF 不补空行派发残余事件。encodeSSE 规范化 data 换行,并对 id/event 换行、id 的 NUL 或无效 retry 显式抛错。Anthropic/MiMo 共用内部 UTF-8 流读取;消费方退出、取消、协议结束和失败均取消正文并释放 reader,不自动重连或重放业务。Anthropic timeout 覆盖正文,MiMo 保留每个 HTTP 请求的独立 timeout。
181
+
147
182
  #### 工具调用
148
183
 
149
184
  ```ts
@@ -180,7 +215,13 @@ const chat = await ai.llm.chat({ messages, tools: registry.getDefinitions() })
180
215
 
181
216
  `ai.a2a.getAgentCard()` 返回完整 A2A 0.3 Agent Card,含 `protocolVersion`、MIME 和 streaming 能力;Kit 默认发现路径为 `/.well-known/agent-card.json`。`handleRequest()` 始终返回 JSON-RPC/SSE 处理结果,未初始化返回 JSON-RPC 错误。可信 HTTP 认证上下文的 `agentId` 映射为 executor 的 `context.user.userName`。当前支持 JSON-RPC/SSE,不声明 push notifications、gRPC 或 REST 支持;业务资源授权仍由应用负责。
182
217
 
183
- A2A 的任务表和消息表在 `ai.init()` 阶段初始化。任务按可信 `agentId` 隔离读取(含获取、取消和继续消息);匿名请求共享匿名作用域,需隔离的部署必须开启认证。`listMessages` 是服务端管理接口,支持 `callerId`、`contextId`、`since` 过滤,不能直接向未授权客户端暴露。旧版任务记录没有可靠主体信息,升级后不会自动推断归属或删除数据,需由应用审核后迁移 `object_id` 为 `user:<agentId>`(匿名为 `anonymous`)。旧消息缺少主体/上下文字段时不会匹配相应过滤条件。
218
+ A2A 的 journal、context 归属与消息表在 `ai.init()` 阶段初始化。任务按可信 `agentId` 隔离(含获取、取消和续接);匿名请求共享匿名主体,生产应在可信传输层认证。`listMessages` 是服务端管理接口,支持 `callerId`、`contextId`、`since`,不能向未授权客户端暴露。
219
+
220
+ Task 与事件使用同一共享 journal,不再装配独立的旧 TaskStore。push notification 配置和 authenticated extended card RPC 返回 SDK 标准“不支持/未配置”错误,不执行任务;executor 相关类型保留公开导出,内部 handler/Store 装配类型不属于公共契约。
221
+
222
+ 入站消息必须持久化后才进入 executor,并在等待后重新校验共享执行代际与期限;agent 消息在同一有界事件队列内等待日志成功后才提交 journal。日志失败会产生 failed/interrupted Task,不会发布虚假的成功结果;同一消息重试不重新执行。日志与 journal 不是跨记录事务,已有日志不等于成功交付,最终以 Task 的 `requiresEffectReconciliation` 标记核对可能已发生的业务效果。消息查询失败返回 `A2A_LIST_MESSAGES_FAILED`(984),不暴露原始 SQL 或连接详情。
223
+
224
+ Task 与事件一次 CAS 持久化,任意节点可订阅;相同 message ID/内容重试仅返回同一轮结果,改变内容或重复旧轮消息失败。跨节点取消由真实执行 owner 调用 executor.cancelTask,取消未确认期间同一 Task 不接受新消息,避免迟到取消作用于新执行;input-required 可在节点重启后续接。DB 租约过期、绝对期限到达或执行器只返回 working 时标记 failed/interrupted,并设置 `metadata.haiExecution.requiresEffectReconciliation=true`;旧节点恢复不能写入,不自动重放未知外部效果。`a2a.execution` 默认 `leaseMs: 30000`、`pollIntervalMs: 250`、`maxExecutionMs: 300000`。单消息 1 MiB、journal 8 MiB、单轮 2048 事件、Task 256 回执、32 个 reference tasks,超限明确失败。外部副作用须使用稳定业务幂等键并核对未知结果;详细部署/验证边界见[共享状态能力表](../../.agents/instructions/distributed-state.md)。
184
225
 
185
226
  #### MCP Server
186
227
 
@@ -202,6 +243,16 @@ await server.connect(new StreamableHTTPServerTransport({ sessionIdGenerator: ()
202
243
 
203
244
  #### Memory / RAG / Knowledge
204
245
 
246
+ Knowledge 每次导入生成唯一 `revision` 和分块 ID,先完整准备分块、向量及实体,再持久化暂存清单、写入向量,最后通过共享 Store CAS 一次发布正文、实体和元数据。相同文档并发重建只允许一个写入者;失败时保留完整旧版,缩短文档不会留下可见旧尾块。检索只接受当前发布版本,并在发布穿插时重试,持续冲突返回错误供调用方重试。向量候选可能按需扩大,清理积压会增加查询成本。
247
+
248
+ `ingest` 成功结果包含 `revision` 与 `cleanupPending`:后者为 true 表示新版本已经发布,但旧向量删除仍待补偿。调用 `repair({ collection, offset, limit })` 分页重试;`setup` 自动检查前 100 份清单,生产环境应定期扫描全部页。暂存期限为存储端时钟的 5 分钟,超时写入者不能发布;故障暂存的永久墓碑用于反复删除迟到的外部写入,不能随日志保留期删除。SQL 与 vecdb 不组成跨库事务,恢复依赖持久清单和持续补偿。`removeDocument` / `deleteCollection` 先撤销可见性,清理失败返回错误;集合删除保留空向量集合及代际墓碑以支持补偿,重新 setup 不显示上一代内容。
249
+
250
+ 默认 DB Store 传播集合查询/创建及向量写入失败。LanceDB 使用持久主键原子 merge;Qdrant 写入/删除等待应用完成,计数使用精确计数。多节点必须共享关系存储和服务端向量后端;自定义 `KnowledgeStore` 须实现相同的版本发布与补偿契约。
251
+
252
+ 使用 LanceDB 时,集合维度与 metric 随 Arrow schema 持久化;连接 metric 只影响新集合。重启后复用同一持久目录,embedding 维数改变时重建匹配集合;多个节点各自的本地路径不会自动共享,分布式 Knowledge 推荐服务端向量后端。
253
+
254
+ Knowledge 的 `retrieve/ask` 及默认 DB Store 会将 `filter` 传给向量 Provider,过滤校验/查询失败会向上传播;Knowledge 返回 `KNOWLEDGE_RETRIEVE_FAILED`(ask 包装为 `RAG_FAILED`),不会伪装成空结果。LanceDB 的过滤只支持顶层键的 string/有限 number/boolean/null AND 精确匹配,例如 `{ tenant: 'acme', active: true }`;null 不匹配缺失字段,不支持对象/数组/undefined。条件在向量 topK 前执行;其它后端的 null/复杂值语义须单独核对。LanceDB 元数据改用原生 JSON 字段,仍可保存嵌套 JSON;旧字符串元数据集合须重建后才能写入或过滤。
255
+
205
256
  ```ts
206
257
  const enriched = await ai.memory.injectMemories(messages, { objectId: 'user-001', topK: 5 })
207
258
  if (!enriched.success) {
@@ -221,10 +272,19 @@ const rag = await ai.rag.query('核心架构是什么?', { sources: ['docs'],
221
272
 
222
273
  const setup = await ai.knowledge.setup()
223
274
  if (setup.success) {
224
- await ai.knowledge.ingest({ documentId: 'doc-001', content: markdownText, title: '产品手册' })
275
+ const ingested = await ai.knowledge.ingest({ documentId: 'doc-001', content: markdownText, title: '产品手册' })
276
+ if (!ingested.success)
277
+ return ingested
278
+ if (ingested.data.cleanupPending) {
279
+ const repaired = await ai.knowledge.repair({ offset: 0, limit: 100 })
280
+ if (!repaired.success)
281
+ return repaired
282
+ }
225
283
  }
226
284
  ```
227
285
 
286
+ Knowledge 分块直接遵循 datapipe 的单位与硬上限:word 为词,其他为 Unicode code point,保留清洗后文本的分隔符;AI 默认 chunkOptions 仍为 markdown/maxSize=1500/overlap=200,覆盖较小 maxSize 时须同时保证继承的 overlap 小于它。分块配置/表达式失败返回 KNOWLEDGE_INGEST_FAILED,embedding 与版本发布均不会执行,不退回整篇文本绕过预算。
287
+
228
288
  #### 语音(Audio)
229
289
 
230
290
  先在 `ai.init()` 中注册语音模型并映射默认识别/合成模型。凭据默认回退到平台环境变量;只有 LLM 与语音模型确认使用同一凭据时,才显式启用 `inheritLlmApiKey`:
@@ -332,6 +392,8 @@ return playable.data
332
392
 
333
393
  浏览器 / 移动端通过 `@h-ai/serv` 暴露的统一语音 WebSocket 入口访问,`@h-ai/ai/client` 提供与 Node 端一致的 `audio.*` API(传输细节内部隐藏)。浏览器客户端严格区分正常结束、取消(`AUDIO_CANCELLED`)与异常断连(`AUDIO_CONNECTION_FAILED`):取消或在 `end` 前断连会抛出对应领域错误码,`synthesize` 不会把未完成的部分音频当作成功结果返回。
334
394
 
395
+ IndexTTS 的 Node HTTP 请求使用同版本 Undici fetch/Agent 与 multipart 类型,避免混用 Node 内置 fetch 的调度器协议;模型 `timeout` 覆盖响应头与正文等待。主动取消返回 `AUDIO_CANCELLED`,超时返回 `AUDIO_TIMEOUT`,成功、失败和取消均关闭独立连接;按段流式降级仍在迭代时抛出领域错误。
396
+
335
397
  > 自托管模型服务(faster-whisper / IndexTTS / Qwen3-4B 的 CPU/GPU Docker 镜像、权重下载与离线打包)见 [`models/`](./models/README.md)。镜像与权重下载优先使用 ModelScope,自动回退 HuggingFace 镜像。
336
398
 
337
399
  #### 文生图(Image)
@@ -364,8 +426,20 @@ if (result.success) {
364
426
 
365
427
  `referenceImages` 可省略;公共层只接受图片字节与 MIME,multipart、`inlineData`、Data URL 等差异由 Provider 转换。凭据可在模型条目传入,或使用 `HAI_AI_IMAGE_<PROVIDER>_APIKEY` / 厂商环境变量。Qwen 与 Seedream 返回的临时 URL 会在 Provider 内立即下载,不会泄漏到公共返回值。完整接口差异与模型说明见 [REFERENCE.md](./REFERENCE.md#image)。
366
428
 
429
+ 图片下载只允许无凭证、默认 443 端口的公网 HTTPS,每次 socket DNS 解析校验全部地址并直接使用该批地址;阻止 IPv4/IPv6 私网、回环、链路本地、映射/转换及保留地址,重定向最多 3 次且每跳重新校验。下载使用独立连接,不传递厂商认证头或应用 Cookie。`baseUrl` 是可信运维配置,厂商 API 请求拒绝重定向。
430
+
431
+ `image.maxImageBytes` 默认 20971520(20 MiB,最大 100 MiB),限制单张输出和参考图;`image.maxImages` 默认 4(最大 16),限制输出与参考图数量,临时 URL 串行下载。实际读取(含解压后)超过上限立即取消;JSON 响应最多 `max(64 KiB, 2 × maxImageBytes)`,错误正文最多 64 KiB。模型 `timeout` 默认 120000ms,覆盖厂商请求、重定向和全部下载;`signal` 可取消整个过程。输出仅支持 PNG/JPEG/GIF/WebP 的 MIME 与魔数匹配,拒绝 SVG;魔数校验不等于完整图片解码或尺寸核验。拒绝地址、超限、超时分别返回 `IMAGE_DOWNLOAD_BLOCKED`、`IMAGE_TOO_LARGE`、`IMAGE_TIMEOUT`,调用方须先检查 success。
432
+
367
433
  #### Context 管理器
368
434
 
435
+ 自动记忆提取在本节点后台运行;`pendingMemoryTasks=0` 不代表全部成功。`flush()` 等待本批任务,异常和失败 HaiResult 都返回 `MEMORY_EXTRACT_FAILED`(含失败数量),同轮并发观察者共享结果。返回即确认该批报告,后续只观察新任务,不自动重放可能部分完成的提取。`save()`、`reset({ waitForMemoryTasks: true })`、`consolidate()` 遇到该失败立即返回,不继续后续状态或记忆写入。进程崩溃后的外部效果仍须人工核对。
436
+
437
+ `scope: { objectId, sessionId }` 必填。相同 scope 的所有节点共用一条版本记录,原子保存消息、摘要、生成 owner、轮次及待提交状态;`addMessage`、每个对外流式 delta 和最终提交即时持久化,`save()` 只刷新共享快照、等待本节点记忆任务并同步会话目录。`getMessages/getTurns/getSummaries/getTokenUsage` 返回最近一次成功读写的独立快照;跨节点读取前调用 `await manager.reload()` 或 `restoreManager()`。
438
+
439
+ 并发限制按共享 scope 生效;`reject` 立即返回忙,`queue` 可取消地轮询(不承诺 FIFO)。生成租约为存储端时钟 30 秒,每 10 秒续租,最终写入同时校验版本、owner 和期限。失联生成过期后由下一次操作标为 interrupted,保留生成记录,不自动重放 LLM、工具或记忆提取。manual 的 pending 不自动过期,可在另一节点/重启后按同一 turnId 提交;相同终态与文本重复提交幂等,不同内容拒绝。外部工具/LLM/后台记忆不与上下文跨系统原子提交,进程失联后须核对未知副作用,不能据此承诺外部恰好一次。`reset` 更换共享纪元;`removeSession` 清空正文并保留永久墓碑,同一 scope 不复用,旧节点不能重建。
440
+
441
+ 自定义 `AIRelStore` 必须提供 `readVersioned`、`compareAndSwap` 和存储端 `currentTime`;普通 save/saveMany 也要更换不可复用版本。CAS 的 `expectedVersion: undefined` 仅创建,`notAfter` 在最终写入以存储时钟严格检查,冲突/截止返回 undefined,存储故障抛错。默认 PostgreSQL/MySQL/SQLite 实现均执行这些条件;SQLite 仅供开发/测试,生产使用 PostgreSQL/MySQL。新 schema 含 version 列,不提供旧表自动迁移。
442
+
369
443
  ```ts
370
444
  const manager = ai.context.createManager({
371
445
  scope: { objectId: 'user-001', sessionId: 'sess-001' },
@@ -375,13 +449,15 @@ const manager = ai.context.createManager({
375
449
  })
376
450
  if (manager.success) {
377
451
  const reply = await manager.data.chat('你好')
378
- await manager.data.save()
452
+ const saved = await manager.data.save()
453
+ if (!saved.success)
454
+ return saved
379
455
  // reset 生命周期完整:终止活动生成、清空消息/摘要/轮次,默认保留系统提示词
380
456
  await manager.data.reset() // 可传 { preserveSystemPrompt, cancelActiveTurn, waitForMemoryTasks }
381
457
  }
382
458
  ```
383
459
 
384
- 同一管理器默认实行**单活动生成**,避免「上一轮 AI 尚未退出,下一轮 user 消息先写入」导致的消息乱序;`reset()` 现为异步并会终止活动生成、释放并发屏障、清空轮次并默认重新写入 Persona/System Prompt。
460
+ 同一 scope 默认实行**跨节点单活动生成**,避免「上一轮 AI 尚未退出,下一轮 user 消息先写入」导致的消息乱序;`reset()` 现为异步并会终止活动生成、释放并发屏障、清空轮次并默认重新写入 Persona/System Prompt。
385
461
 
386
462
  ##### 真实对话状态(Conversation Commit Layer)
387
463
 
@@ -392,12 +468,15 @@ if (manager.success) {
392
468
  `chatStream` 在**调用上游模型前**就登记轮次并产出 `turn_started`(事件序列 `turn_started → delta* → done`,中途取消时 `turn_started → delta* → cancelled`)。因此即使生成到一半被 `AbortSignal` 取消,也能拿到 `turnId` 与已生成文本,用真实内容提交:
393
469
 
394
470
  ```ts
395
- const m = ai.context.createManager({ turnCommit: 'manual' /* ... */ }).data
471
+ const created = ai.context.createManager({ scope: { objectId: 'user-001', sessionId: 'sess-001' }, turnCommit: 'manual' })
472
+ if (!created.success)
473
+ return created
474
+ const m = created.data
396
475
  const controller = new AbortController()
397
476
 
398
477
  for await (const ev of m.chatStream('请展开讲讲', { signal: controller.signal })) {
399
478
  if (ev.type === 'turn_started') {
400
- m.markTurnSpeaking(ev.turnId) // 可选:标记进入播放
479
+ await m.markTurnSpeaking(ev.turnId) // 可选:标记进入播放
401
480
  }
402
481
  else if (ev.type === 'delta') {
403
482
  feedTts(ev.text) // 边生成边合成播放
@@ -450,6 +529,7 @@ await ai.persona.save({
450
529
  // 组合出可直接喂给 ContextManager 的系统提示词(systemPrompt + traits)
451
530
  const composed = await ai.persona.compose('xiaoq')
452
531
  const manager = ai.context.createManager({
532
+ scope: { objectId: 'user-001', sessionId: 'persona-session' },
453
533
  systemPrompt: composed.data,
454
534
  memory: { enable: true, enableExtract: true, scope: { personaId: 'xiaoq' } },
455
535
  })
@@ -457,6 +537,8 @@ const manager = ai.context.createManager({
457
537
 
458
538
  `ai.persona` 提供 `save` / `get` / `update` / `remove` / `list` / `compose`;角色档案全局共享(不按 `objectId` 隔离)。
459
539
 
540
+ ### 配置
541
+
460
542
  ```yaml
461
543
  llm:
462
544
  apiKey: ''
@@ -512,9 +594,17 @@ memory:
512
594
  ```
513
595
 
514
596
  - **`native`(默认,推荐)**:HAI 原生引擎,复用同一套 vecdb(向量库)、reldb(关系库)、LLM 与 Embedding。`extract` 采用 **Mem0 式批量合并**——一次 LLM 调用对整批抽取事实与相关既有记忆做 ADD / UPDATE / DELETE / NONE 决策,实现增量更新、跨条去重与矛盾删除,并支持 `category` 主题标签。`maxEntriesPerObject`、`maxEntriesGlobal`、`recencyDecay`、`embeddingEnabled`、`writebackRelatedTopK` 均作用于此后端;淘汰按 `objectId` 分区触发,不会因某一主体写入过多而淘汰其他主体的记忆。native 后端的 `scope` 过滤:候选集已被 `objectId` 索引收窄(≤ `maxEntriesPerObject`),PostgreSQL 上还会把 scope 下推为 `data @> '{"scope":...}'::jsonb` 包含查询并命中 JSONB **GIN 索引**(SQLite / MySQL 退回内存匹配,结果一致)。
515
- - **`mem0`(真·mem0ai/oss)**:直接使用 `mem0ai/oss` 的 `Memory` 引擎(嵌入式,无云服务)。LLM / Embedder 从 `llm` 配置提取;`qdrant` / `pgvector` 可复用底层 vecdb。无法映射 `lancedb` / `chroma` 等后端时默认 fail-fast,只有显式设置 `memory.allowEphemeralFallback: true` 才使用 mem0 in-memory,避免重启后静默丢失记忆。历史记录默认禁用。
597
+ - **`mem0`(真·mem0ai/oss)**:直接使用 `mem0ai/oss` 的 `Memory` 引擎(嵌入式,无云服务)。LLM / Embedder 从 `llm` 配置提取;`qdrant` 可复用底层 vecdb。当前适配仅映射带完整连接参数的 `qdrant`;生产环境(`NODE_ENV` 或 `HAI_CORE_ENV` 为 `production`)即使设置 `memory.allowEphemeralFallback: true` 也拒绝实际内存回退。临时回退仅限开发或明确的 `HAI_E2E=1` / `HAI_E2E_MOCK=1`;缺少后端同样不能在生产启动。历史记录默认禁用。
598
+
599
+ 两个 Provider 对外 `ai.memory.*` API 完全一致(`extract` / `recall` / `injectMemories` / `add` / `update` / `get` / `remove` / `list` / `listPage` / `clear`),均支持 `objectId`(主体隔离)与 `scope`(业务作用域 key-value 过滤,如 `{ topicId, personaId }`)。`recall` / `list` / `listPage` / `clear` 均按 `scope` 严格过滤,`clear` 在传入 `types` / `scope` 时仅删除同时匹配项(避免误删)。mem0 后端的 `extract` 在框架层用统一提取器完成分类与打分(honor `types` / `model` / `minImportance` / `systemPrompt`)后以 `infer:false` 写入,保留 `hai_type` / `hai_importance`;`recall` 同样支持 `types` 过滤与 `recencyWeight` 时间衰减——二者行为与 native 一致。Mem0 的 `update` 使用 SDK 原地修改文本与元数据,保持 `id` 和创建时间。SDK 查询使用 `filters.user_id`,写入和整主体删除使用 `userId`。SDK 无列表游标:`list` / `listPage` / 条件 `clear` 最多支持每主体 1000 条原始候选(含过期条目),多取一条探测完整性;超限返回 `MEMORY_LIST_INCOMPLETE`(906),不会伪造 total 或执行部分条件删除。候选完整时按 UTC 日期剔除过期项,再按类型/scope 过滤和分页;仅 `objectId` 的清理使用 SDK `deleteAll`,不受列表上限限制。公共空条件清理被拒绝;跨主体全清仅限受信管理员显式 `admin.clearAll({ confirm: true })`。
600
+
601
+ Mem0 3.3.1 的 Qdrant 适配仍依赖 `QdrantClient.search`;项目将 `@qdrant/js-client-rest` 精确锁定为受测的 `1.18.0`,AI 的可选 peer 也声明此版本。启动会检查该公开方法,缺失时明确配置失败;不要覆盖为已移除此方法的 1.19。
516
602
 
517
- 两个 Provider 对外 `ai.memory.*` API 完全一致(`extract` / `recall` / `injectMemories` / `add` / `update` / `get` / `remove` / `list` / `listPage` / `clear`),均支持 `objectId`(主体隔离)与 `scope`(业务作用域 key-value 过滤,如 `{ topicId, personaId }`)。`recall` / `list` / `listPage` / `clear` 均按 `scope` 严格过滤,`clear` 在传入 `types` / `scope` 时仅删除同时匹配项(避免误删)。mem0 后端的 `extract` 在框架层用统一提取器完成分类与打分(honor `types` / `model` / `minImportance` / `systemPrompt`)后以 `infer:false` 写入,保留 `hai_type` / `hai_importance`;`recall` 同样支持 `types` 过滤与 `recencyWeight` 时间衰减——二者行为与 native 一致。一个差异:mem0 后端在 `update` 涉及 type/importance/metadata 时会重建记忆并重新分配 `id`(native 后端保持 id 稳定)。
603
+ 适配器通过 SDK 公开的 `client` 配置注入独享客户端;Mem0 的 Qdrant 写入与删除均使用 `wait: true`,等待服务端应用完成后才继续写后读或分批清理。
604
+
605
+ Mem0 的 pgvector 组合在所有环境拒绝启动:当前 SDK 将 PostgreSQL 连接保存在私有对象中,未提供公开关闭入口,框架无法履行资源清理契约。需要 pgvector 时选择 `memory.provider: native`;该原生后端保持支持。`disableHistory: true` 使用 SDK 无资源的历史占位实现,不创建历史 SQLite 文件。工厂通过公开的一条有界只读查询等待实际后端 ready 后才发布成功。
606
+
607
+ `add` / `extract` 的 SDK 回执不含完整业务字段,框架写后读取权威记录再返回;并发提取等待全部在途写入结束。若写入或写后读失败,`error.ext.requiresEffectReconciliation=true`,`memoryIds` 保存已知写入 ID(回执丢失时可能为空)。应按主体核对实际记忆后决定恢复,不能盲目重做 add/extract;框架不自动重放。
518
608
 
519
609
  **候选池与 scope 漏召回**:`scope` 过滤在内存中完成,若先按 `topK` 截断再过滤,同一主体下相关度较高的其它主题/角色记忆会把目标 scope 的记忆挤出候选池,导致「明明有却召回 0 条」。为此 `recall` / `injectMemories` 先取回 `topK × candidateMultiplier`(默认 5)条候选,过滤后再截取 `topK`。scope 隔离越细(如按 `topicId` + `personaId`),可将 `candidateMultiplier` 调大:
520
610
 
@@ -560,8 +650,8 @@ if (!result.success) {
560
650
  - `hai:ai:600-701`:Retrieval/RAG。
561
651
  - `hai:ai:800-805`:Knowledge。
562
652
  - `hai:ai:050-059`:Audio。
563
- - `hai:ai:060-065`:Image。
564
- - `hai:ai:900-905`:Memory。
653
+ - `hai:ai:060-068`:Image,含下载地址拒绝、资源超限与总超时。
654
+ - `hai:ai:900-906`:Memory。
565
655
  - `hai:ai:980-984`:A2A。
566
656
 
567
657
  ## 开发测试
@@ -573,6 +663,8 @@ pnpm --filter @h-ai/ai test
573
663
  pnpm --filter @h-ai/ai build
574
664
  ```
575
665
 
666
+ 源码 JSDoc 的公开示例从实际包入口导入,内部工厂示例标记“模块内部上下文”,分别按真实声明编译。根目录执行 `pnpm check:jsdoc --package ai` 核验覆盖与示例类型;普通业务方法检查 HaiResult,流式与浏览器 AudioClient 保留显式抛错契约。
667
+
576
668
  ## 许可证
577
669
 
578
670
  Apache-2.0