@gestaltrun/dsh-im-runtime 0.1.0 → 0.1.6

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 (62) hide show
  1. package/README.i18n.yaml +35 -20
  2. package/README.md +142 -10
  3. package/README.zh.md +141 -9
  4. package/lib/index.js +140 -2
  5. package/lib/inline-image-DuX6vSEI.js +20 -0
  6. package/lib/read-receipts.js +413 -0
  7. package/lib/read-state-CnUZKCuY.js +1868 -0
  8. package/lib/runtime-Bm6iSEpv.js +10939 -0
  9. package/lib/tools.js +1 -1
  10. package/lib/types/account-placement.d.ts +22 -0
  11. package/lib/types/admission-annotations.d.ts +40 -0
  12. package/lib/types/agent-coordinator.d.ts +47 -2
  13. package/lib/types/agent-media.d.ts +46 -0
  14. package/lib/types/agent-prompt.d.ts +8 -2
  15. package/lib/types/checkpoint-recovery.d.ts +32 -0
  16. package/lib/types/checkpoint.d.ts +127 -0
  17. package/lib/types/consumer-chat-links.d.ts +59 -0
  18. package/lib/types/delivery-schema.d.ts +126 -2
  19. package/lib/types/delivery-sqlite.d.ts +206 -6
  20. package/lib/types/delivery-types.d.ts +97 -1
  21. package/lib/types/delivery.d.ts +29 -6
  22. package/lib/types/errors.d.ts +1 -1
  23. package/lib/types/evaluation-accounts.d.ts +86 -0
  24. package/lib/types/index.d.ts +34 -2
  25. package/lib/types/inline-image.d.ts +7 -0
  26. package/lib/types/input-drain.d.ts +73 -0
  27. package/lib/types/knowledge-image.d.ts +64 -0
  28. package/lib/types/media-lifetime.d.ts +22 -0
  29. package/lib/types/media.d.ts +32 -0
  30. package/lib/types/message-preview.d.ts +4 -0
  31. package/lib/types/message-upload.d.ts +55 -0
  32. package/lib/types/nas-host-lease.d.ts +33 -0
  33. package/lib/types/placement-slice.d.ts +30 -0
  34. package/lib/types/policies.d.ts +10 -0
  35. package/lib/types/preset-model.d.ts +50 -0
  36. package/lib/types/read-context.d.ts +27 -0
  37. package/lib/types/read-receipts.d.ts +10 -0
  38. package/lib/types/read-records.d.ts +30 -0
  39. package/lib/types/read-state.d.ts +18 -0
  40. package/lib/types/read-types.d.ts +28 -0
  41. package/lib/types/restart-recovery.d.ts +38 -0
  42. package/lib/types/runtime.d.ts +318 -10
  43. package/lib/types/schema.d.ts +13 -2
  44. package/lib/types/script-clock.d.ts +14 -0
  45. package/lib/types/sdk.d.ts +108 -0
  46. package/lib/types/service-types.d.ts +161 -4
  47. package/lib/types/session-events.d.ts +9 -9
  48. package/lib/types/simulation-batch.d.ts +40 -0
  49. package/lib/types/simulation-controller.d.ts +16 -3
  50. package/lib/types/simulation-operator.d.ts +19 -0
  51. package/lib/types/sqlite-connection.d.ts +27 -0
  52. package/lib/types/time.d.ts +13 -0
  53. package/lib/types/transport.d.ts +44 -4
  54. package/lib/types/transports.d.ts +7 -0
  55. package/lib/types/types.d.ts +22 -5
  56. package/lib/types/typing.d.ts +33 -0
  57. package/lib/types/upload-types.d.ts +121 -0
  58. package/lib/types.js +2 -1
  59. package/locale/en.json +1 -1
  60. package/locale/zh.json +1 -1
  61. package/package.json +67 -41
  62. package/lib/runtime-C0-S-_CC.js +0 -6758
package/README.zh.md CHANGED
@@ -7,17 +7,54 @@ kind: "package-reference"
7
7
 
8
8
  [English](README.md) | 中文
9
9
 
10
+ 运行时提供独立的 Host 注册表 `imConsumerChatLinks`,用于消费者对话链接。`resolve({ accountId, platform }, signal)` 先核对当前账号目录,再调用已注册提供方;缺少提供方返回 `unsupported`,同平台重复注册失败。注册归调用方的 Cordis fiber 所有,返回幂等 disposer,撤销时取消在途读取。身份查询失败保留为错误;注册表不持久化 URL,也不改变消息传输的依赖。
11
+
10
12
  ## 概要
11
13
 
12
14
  使用此包保存安全的钉钉或旺旺账号事实、将完整会话路由绑定到工作区,并保留消息投递状态。入站页面、历史导入、提供方游标、Session 提交证据、Agent 任务代次、出站结果、客户列表与处理暂停都有持久化查询收据。凭据保留在 Credentials 服务中。平台提供方负责登录、事件转换、发送和回执查询;此 runtime 负责将消息准入普通 Agent。
13
15
 
16
+ `snapshot()` 返回可见的正式账号和路由。它排除评测目标及换号时隐藏账号的目标;正式账号删除后保留的目标及其 revision 仍然可见。本机与未获租约的待命 Host 通过 `ensureEvaluationAccount(sellerId, workspaceId)` 取得断开连接、无凭据的旺旺身份及一条固定关闭路由。每次调用对齐路由工作区;`releaseEvaluationAccountRoute` 只移除所观察的路由代次。独立配置域 `gestaltrun_im_evaluation_accounts` 使用版本 1,拒绝凭据、连接意图、错误身份和开启路由。评测数据无效时以 `IM_EVALUATION_UNAVAILABLE` 停用评测,正式接待仍可启动。`isEvaluationAccount` 按已存储的评测身份分类,可校验 seller;名称和账号前缀不授予权限。正式账号操作不能修改这些记录,持有 NAS 租约的 Host 拒绝创建评测账号。`createFormalSimulationInstance` 在串行创建操作内检查所选正式路由;Host 评测保留 `createSimulationInstance`。详见[持久化确认](persistence-changes/2026-10-26-evaluation-accounts.md)。
17
+
14
18
  安装环境以运行时对等依赖提供 `@deepseek-ai/dsh-agent-preset-registry`。匹配的开发依赖提供类型声明和测试实现。
15
19
 
20
+ `cs-hosting` 预设在提交批次前读取可选的 `imPresetModelGate`。门闩未装配时准入失败,不使用成员自己的默认模型。门闩返回阻塞时,该批次保持未提交,等准入变化后再试。
21
+
22
+ 提供方游标写入和接待状态变化共用按账号的 `checkpointIntervalMs` 节流。接待状态综合产品托管意图、准入、模型可用性、收件连接和 Agent 在途处理;仅同步历史不会上报 `running`,仅有待处理记录不会上报 `draining`。上报失败时保留本地事实,下个间隔重报,并向 stderr 输出安全的账号级失败类别;写入抛错时,由 Host 日志独立回报安全诊断。抛出的异常和重试结果保留安全的 `diagnostic.code`、`diagnostic.status`、`diagnostic.reason` 和有界嵌套 cause;例如,上报被拒时可见 `NOT_FOUND` 和 HTTP 404。`checkpointFailureDiagnostic` 向传输方导出相同的纯机器元数据投影。类别与诊断均相同时抑制重复输出,任一变化或成功上报时分别输出新失败或恢复诊断。诊断不含消息正文、游标和凭据。租约代次被拒绝后停止上报,直到 `resumeCheckpoint` 开始新本地代次;旧代次的迟到结果不能停止新代次。
23
+
24
+ 检查点 `pendingCount` 包含尚未提交的输入、等待或正在发送的记录,以及状态尚未核实的当前 Agent 任务。已加载任务只有在 inbox 为空且 Session 成功刷写后确认以已完成轮次结束,才可视为安全。未加载任务、无法确认的完成证据、中断或失败轮次都保持非零。Session 完成和 inbox 变化会触发新报告;卸载在释放存储前等待检查点读取结束。旧版检查点生产端的计数不提供这些完成保证。
25
+
26
+ SDK lease 的可选方法 `isDirectConversation` 用于报告提供方详情能否确认原始不透明 cid 是单聊。能力缺失或结果未确认时,实时接收交由 saas 事件流处理。
27
+
28
+ IM transport 可在文字能力旁提供可选的 `sdk` Provider。SDK 状态属于进程;`withConnection` 仅为已观察到的在线代次接纳可信 Host 工作。已读、上传和媒体消费者负责接收方核验、持久化准备与提交授权。可选连接能力不增加 Session 事件。
29
+
30
+ `ImTransportSink.receiveRealtime` 接受已归一化的 SDK 消息,不读取或提交 Provider 游标。它在账号接收流程内检查账号身份、当前在线 SDK 代次、唯一的既有单聊买家和已启用路由。账号存在已启用的客服分配路由时等待 saas;新买家或映射有歧义时也等待 saas。SDK 会话 id 不用于新建 runtime 会话。持久写入失败继续抛出。两通道共用会话入库方法与准确的平台消息身份;同一买家的重复消息保留已存发送者信息,后到的昵称不再产生上传观察。现有已读同步器使用新入库消息,不再依赖 saas 先到;其记录与行为不变。
31
+
32
+ 运行时只观察已订阅旺旺单聊的输入状态。运行时核验本地接收方 UID,通过当前在线 SDK 代次解析已存储买家 UID,并比对 SDK 会话 ID。`typingTimeoutMs` 默认 5000,接受 1 至 2147483647 的整数。停止通知、买家消息、超时、SDK 离线和最后一个观察者离开都会移除状态。会话解析失败时,输入状态保持未确认,日志不包含载荷,也不自动重试。输入状态只保存在进程内,不改变消息历史、Session 事件、模型输入或持久化记录。
33
+
34
+ 必填的 `ImTransportSink.findInboundContent` 在监听账号内按准确的平台消息 id 查询,可限定会话。查询复用已有消息行与账号索引,只读取匹配的聚合。SDK 买家查询读取会话摘要的 `direct_peer_id`,旧的空会话可使用已持久保存的单聊对端;不新增索引,也不逐个解析所有会话聚合。必填的 `receiveRealtime` 与 `isNativeSender` 由同一个 runtime 拥有。查询失败继续抛给 transport,后者只跳过当前 SDK 消息,交由 saas 对账。
35
+
36
+ 文本、Markdown 或图片消息中的 PNG/JPEG/WebP/GIF 内联 data URL 在 Agent 提交前通过 Session 附件 Provider 准入。日志输入包含规范化后的持久图片引用与 `[image]` 标签,不包含 base64 文本。附件校验或存储失败会停止提交并保留待处理消息。历史读取使用同一简短标签,不重复图片字节。既有存储记录无需数据库迁移即可读取。
37
+
38
+ `sendManualMessage` 接受一个可选媒体文件;`sendMediaMessage` 向可信 Host 调用方提供同一 outbox 路径,自动发送必须提供冻结的路由绑定。选定字节私有暂存于 SQLite。上传元数据先提交,随后才启动唯一的消息尝试;每个准备步骤重新检查取消、账号可用性与 SDK 代次。其他进程或代次的已准备上传仅在显式调用时重新上传。人工归属、已知失败与未知结果跨重启保留;未知媒体不能重试,匹配的认证回执可将其确定为成功。`mediaMaxBytes` 默认限制每个解码文件为 10 MiB;SDK 或平台可施加更低限制。文件下载从存储的元数据取得新地址。参见[数据库迁移指南](../../docs/upgrade-guide/v0.2.1-alpha.3/im-media-delivery/guide.zh.md)。
39
+
40
+ 文件上传以已核实单聊买家最新的非空 `senderDisplayName` 作为 `ImSdkUpload.receiverNick`。该字段已有事件或商家目录提供的昵称;后续未命名消息不会抹除已有证据。其他买家的姓名与会话标题的 ID 回退值不参与取名。缺少昵称时在平台 prepare 前明确以 `IM_MEDIA_INVALID` 准备失败;图片不要求昵称。不新增昵称持久化记录。每个媒体意图先将已核实的单聊买家解析为 SDK 会话标识。解析失败是明确的准备失败,不进入消息提交通道。平台文件准备或提交结果不确定时保留未知状态,不重试。runtime 发布前通过私有预写标记,将未完成的文件准备恢复为未知状态,不依赖 SDK 或账号可用性,并修正此前被误判为失败的记录。attempt 与重试准入保留尚未确认的准备状态;明确的准备拒绝会清除标记,仍允许显式重试。私有准备记录保留该标识及代次;媒体回执必须与之匹配,不使用文字 transport 会话 id。缺少 SDK 标识的旧准备记录不能确认新的 SDK 回执。SQLite 布局仍为 3;待处理工作由当前 owner 准备,已提交且结果未知的工作绝不重发。
41
+
42
+ Runtime 卸载时停止接纳媒体发送与提供方页面、取消准备,并等待已接纳发送(含人工消息发布)、回执及消费者清理完成,随后关闭 SQLite 和账号存储。同一 owner 也先停止并等待绑定的消息上传 worker,再关闭存储,即使调用方仍然挂载。Host 与账号输入计数器保持可用,直到监听器、Agent 清理与媒体最终写入完成。已完成的 SDK 提交在卸载与重启后保留观察到的结果;卸载不会把成功变成未知结果。
43
+
44
+ 账号记录可选保存 `sdkConnectionIntent`(`online` 或 `kicked`)与 `sdkConnectionIntentRevision`;缺失表示不允许自动恢复。显式登录写入 `online` 及新的意图代次;显式登出删除两字段;被踢通过意图代次 CAS 写入 `kicked`。普通账号刷新保留该代次,账号交接切片保留两字段。`stopAccountForPlacement` 在最终导出前等待 SDK 主动登出及已接纳的踢线意图写入,不清除意图;停止失败阻断交接。成功账号导入或 checkpoint 恢复建立一次进程内激活通知:接待暂停释放时以 runtime 事件 revision 发布 `sdkRecoveryRequested`,重复释放启动暂停不产生新资格。托管恢复等待全部输入暂停释放并取得接待权后才登录。旧读取器忽略这些可选字段,写回时可能丢弃;此后须再次显式登录。见[持久化确认](persistence-changes/2026-10-22-sdk-connection-intent.md)。
45
+
46
+ ### Migration
47
+
48
+ 存量账号(包括生产测试沙箱 57)没有已保存的 SDK 在线意图,升级后必须显式登录一次。账号已连接、已有消息历史、托管路由已开启或云端恢复成功,都不表示商家已同意 SDK 登录。此次显式登录保存在线意图,供后续 Host 重启与运行位置交接恢复。
49
+
50
+ `ImSdkProvider` 实现方必须提供 `logout(accountId)`,并在 `login(accountId, signal)` 接受可选取消信号。登出等待已接纳的 SDK 工作和页面清理完成后返回 offline。取消先撤销登录准入,迟到结果不得发布 online。
51
+
16
52
  ## 目录
17
53
 
18
54
  - [使用此包](#use-this-package)
19
55
  - [理解实现](#understand-the-implementation)
20
56
  - [进一步阅读](#further-exploration)
57
+ - [买家已读回执](#buyer-read-receipts)
21
58
  - [模型体验](#model-experience)
22
59
  - [已知限制与后续工作](#known-limitations-and-deferred-work)
23
60
  - [开发说明](#dev-note)
@@ -29,13 +66,31 @@ kind: "package-reference"
29
66
 
30
67
  装载 Storage、一个 KV 后端、StorageDomain、Credentials、Session persistence、Workspace、AgentLoop、AgentPresets、默认 Agent 模型、此 runtime,以及至少一个 `ImTransport` 提供方。Agent 服务缺失时仍可使用配置和历史能力;自动准入只在这些服务所在的注入作用域中启用。
31
68
 
69
+ `renewAccount` 校验同一商家的凭据,写入新的自有凭据记录,仅在账号 revision 仍匹配时安装。并发断开或暂停优先,不改变正在使用的凭据。成功更新保留规则、历史和意图,先停止原监听器再启动新监听器,并且只删除被替换的 runtime 自有凭据记录。换号记录在 Host 私有可选字段中保留原连接意图;字段缺失的老账号继续可见。隐藏账号保持断开,拒绝普通重连和迟到的 Provider 回调,不公开活跃规则或模拟目标。显式校验后恢复时保留原 id、规则和消息范围;其他商家使用独立账号。
70
+
32
71
  ### 适用条件
33
72
 
34
73
  产品 Host 提供方与 IM BFF 共享 IM 配置权威时使用此包。需要 wire DTO 或只写账号接入请求类型的使用方应导入 `@gestaltrun/dsh-im-runtime/types`;Host 代码从根入口导入 `ImRuntimeService`、`ImRuntimeError` 和 `ImTransports`。
35
74
 
75
+ `pauseInput()` 返回进程内暂停所有权:新提供方页面在落盘前等待,新准入驱动保留待处理消息,新的 Host turn 在首个 step 前等待;已准入 turn 及其子 Agent 可完成。`drained(signal)` 等待已接收页面、准入驱动与 turn 所属的 activity 全部结束,超时不自动取消 turn。空闲 Agent 通过 maintenance 在 Inbox claim 前等待;排空不等待自己的 maintenance 预约,调用者应等待 `drained`,而非仍包含该预约的 `agent.whenIdle()`。调用者在取消和卸载时必须幂等调用 `resume()`;短暂停到期后继续持有暂停的升级调用者在取消、保留期到期或卸载时释放;最后一个暂停释放后继续接收并唤醒待处理会话。卸载拒绝等待页面,不推进它们的提供方游标。此暂停不写入 NAS,新 Host 按持久化游标恢复;[云端升级入口](../cloud-runtime/README.zh.md#cloud-upgrade)拥有时限、鉴权和 Session 最终刷写。
76
+
77
+ `pauseAccountInput(accountId)` 对一个账号及其拥有的子 Agent 使用相同的排空生命周期,其他账号与个人 Workspace 继续运行。Host 升级暂停和账号交接暂停不能并发持有同一 Agent;冲突抛出 `ImInputPauseError`(`IM_INPUT_PAUSE_CONFLICT`),调用者保留当前操作,待其结束后重试。页面接收等 Host 与账号都准入后才占用在途计数,等待期间不妨碍另一方排空。释放账号暂停会唤醒该账号待处理会话,不改变持久账号或路由代次。 账号计数器创建或 Agent 作用域就绪时即挂载观察,使之后暂停前已经结束的回复退出计数。暂停句柄可选的 `observeReplies(listener)` 立即报告并持续观察按 Session 去重的在途 turn 与回复准入预约数,不计维护 activity 或页面接收。没有进行中 turn 的 activity 仍阻止 `drained` 完成,即使回复计数已为零。返回的 disposer 停止观察,runtime 卸载清除全部观察者。没有该方法的旧 Provider 表示数量未知,不能当作零。
78
+
79
+ `stopAccountForPlacement(accountId)` 在账号成功排空后永久拒绝等待及后续提供方页面,确认监听停止并取消未发送的自动回复;Agent maintenance 暂停仍保留,停止结果未知时不能恢复接待。普通路由重配仍接收旧监听器已在途的页面,并按当前路由判定,不会永久关闭账号接收。
80
+
81
+ `placementStandby: true` 持有目标端的启动输入暂停,阻止监听启动。在此暂停期间通过 cloud-placement 导入 Session 历史,然后调用 `importAccountForPlacement(slice, targetWorkspaceId)`,并通过 `prepareResume` 准备每个恢复的 Agent。Host 只有在控制面授权和恢复准备完成后才能调用 `releasePlacementStartup()`,它只释放自己的启动暂停。此标志默认 false,不改变现有组合。交接所有者单独提供传输中加密的 Credentials;切片只包含凭据引用、账号与路由事实和投递状态,不包含 Agent 配置。wire 切片只能引用本账号规范的 IM 凭据记录,不能指向其他插件的记录。导出时将当前 runtime 拥有的续期键映射为目标规范引用,不改变源端记录;导出器核对账号 revision 后,按源端账号的实际凭据键读取。`exportAccountForPlacement` 只选取声明的来源内置 Workspace 中的任务代次;当前路由指向其他 Workspace 时拒绝,不传输它们的 Session。续签键的归属依据本账号已成功的续签操作收据核验,不依赖后续暂停、恢复或授权操作改变的账号 revision;其他账号的续签收据或凭据键仍会被拒绝。
82
+
83
+ `beginAccountPlacementImport(accountId, signal)` 在运行中的 Host 建立账号级接收待命,也适用于之前导出的账号。它只排空、停止该账号,并在导入 Session 前释放协调器拥有的 Agent 句柄;导入和准备接受此暂停,无需 Host 启动待命。取得新租约授权并完成准备后,`releaseAccountPlacement(accountId)` 只释放本次接收暂停;来源交接所有者还须释放先前的账号暂停。其他账号及 Host 维护暂停继续保留。Host 启动或维护暂停以 `IM_INPUT_PAUSE_CONFLICT` 拒绝此账号级入口;新建待命 Host 使用启动导入路径。开始暂停后若失败,保留停止或输入门禁,必须核验,不能激活接收。之后的调用会重试被拒绝的尝试,而不是返回原拒绝,并复用保留的输入暂停,释放后输入即可重新开放。
84
+
85
+ `recoverAccountPlacement(accountId, { checkpoint, sourceFence }, signal)` 在账号导入待命期间接收只有游标的云端 checkpoint。调用者在取得新租约前核验并持久保留已完成的迁出交接、对应云端持有者及本地自动接待持续隔离的证据。`beginAccountPlacementImport` 在 CAS 前停止监听;持有此暂停时,`validateAccountCheckpointRecovery(accountId, checkpoint, signal)` 要求本地已有非空流游标与 checkpoint 严格相等。不透明游标不一致时以 `IM_CHECKPOINT_CURSOR_CONFLICT` 拒绝,不回退游标或清除去重记录。恢复在 CAS 后重复核验;不对游标字符串或跨机器时间排序。checkpoint 存在待处理输入或结果未知的发送时,以 `IM_CHECKPOINT_BODIES_UNAVAILABLE` 拒绝;缺少本地已有非空游标的流时,以 `IM_CHECKPOINT_STREAM_MISSING` 拒绝。恢复抑制陈旧本地待处理输入,保留未知发送结果,并创建新的任务代次,不改变账号和路由 revision。旧本地 Session 文件保持不变,云端 Session 正文不可用;每个新 Session 都记录历史缺口提示。稳定的任务、Session 和提示标识使同一来源交接及 checkpoint epoch 的重试在重启后仍幂等。恢复继续保持接待暂停,直到交接所有者完成新租约授权并释放自己持有的暂停。
86
+
87
+ `prepareAccountPlacement(accountId, prepare, signal)` 在启动或账号导入暂停期间使用本端 preset 与模型门禁恢复账号的每个当前任务。恢复前核验任务属于该真实账号作用域,且已有持久化 Session 头的 id 与任务记录的 Session id 一致,且目录与 Workspace 一致;缺失会话时拒绝,不创建替代会话。回调接收同一个空闲 Agent 及已核验任务,准备完成后才开启接待。释放启动暂停后,`waitAccountPlacementReady` 等待当前监听重配及 Provider 就绪;监听失败会拒绝,不能报告追平成功。原账号暂停或断开意图保持不变。
88
+
89
+ 账号输入排空后,`stopAccountForPlacement` 停止该账号的确切监听器,中止仍在等待准入的页面,并取消尚未发送的自动回复,保留发送结果未知的记录。停止结果未确认时抛出 `IM_PLACEMENT_STOP_UNKNOWN`,保留停止门。开始此停止后,暂停不能恢复(`IM_PLACEMENT_ALREADY_STOPPED`);失败必须核验交接,不能自动恢复。目标账号导入在接待停止期间依次提交投递事实与配置;两者之间失败可能留下部分效果。交接 journal 负责 epoch 检查、回执和激活前核验。
90
+
36
91
  ### 最小配置
37
92
 
38
- `admissionBatchSize` 限制一次模型可见批次,默认值为 1000。群聊 `everyN` 不能超过该限制。`accountSetupTtlMs` 限制未确认提供方接入材料在 Host 内存中的保留时间,默认五分钟。StorageDomain 为账号配置选择持久后端,Credentials 实现选择密钥存储。会话投递使用 Host 自管的 sqlite 文件。
93
+ `admissionBatchSize` 限制一次模型可见批次,默认值为 1000。群聊 `everyN` 不能超过该限制。`accountSetupTtlMs` 限制未确认提供方接入材料在 Host 内存中的保留时间,默认五分钟。`checkpointIntervalMs` 是两次托管检查点上报的最小间隔,默认 10 秒,可在 `cordis.yml` 中修改。StorageDomain 为账号配置选择持久后端,Credentials 实现选择密钥存储。会话投递使用 Host 自管的 sqlite 文件。
39
94
 
40
95
  ```yaml
41
96
  - name: '@deepseek-ai/dsh-storage'
@@ -53,19 +108,32 @@ kind: "package-reference"
53
108
  admissionBatchSize: 1000
54
109
  accountSetupTtlMs: 300000
55
110
  deliverySqlitePath: /absolute/path/to/storage/gestaltrun_im_delivery.sqlite
111
+ checkpointIntervalMs: 10000
56
112
  ```
57
113
 
58
- `listAccountCandidates` 从已注册 transport 返回已安装的钉钉 profile 和旧版已准入旺旺商家。`startPlatformLogin`、`platformLoginStatus` 和 `cancelPlatformLogin` 委托可选的交互式 Provider 登录,不持久保存 attempt,也不暴露进程输出;Provider teardown 负责活跃 attempt。钉钉 setup 必须选择一个列表 Profile。新的旺旺 setup 可以直接携带使用时商家、主客服账号、HTTPS endpoint 与只写 AccessKey;runtime 把这些字段交给 Transport,并核对返回的商家身份及已验证客服身份。`discoverConversations` 为已配置账号返回提供方已知的会话标题及其稳定平台 id。`inspectConversation` 在列表页未包含该会话时按平台 id 解析标题。`previewAccountSetup` 要求 transport 校验只写输入,并返回安全身份、授权事实、过期时间与 Host 生成的 setup id,不创建账号,也不写入 Credentials。`confirmAccountSetup` 固定该身份,在同一持久操作收据中保存凭据记录与账号;相同 setup 与 operation id 会复用结果,即使 Host 重启也不会重复创建账号。签名操作成功但没有足够独立身份凭据时,Provider 可以返回 `unchecked`;账号会保存,但在刷新授权取得已验证事实之前不能配置路由。已配置的同一钉钉 profile 或旺旺商家会以 `IM_ACCOUNT_EXISTS` 拒绝。`deleteAccount` 比较已观察 revision,停止监听,并删除账号及其路由;会话历史保留。`cancelAccountSetup`、请求取消、setup 过期与 runtime dispose 都会释放未确认的内存材料;JavaScript 不保证物理擦除内存。暂停、断开、重连、刷新或确认接入响应不确定时,调用 `queryAccountOperation` 查询。成功的 `deleteAccount` 会删除账号聚合,该操作没有后续查询收据。`inspectAccount` 和 `refreshAccount` 报告提供方观察到的授权事实,不改变账号身份。连接意图、授权状态和监听状态仍是三个独立事实。
114
+ 使用独立的 `DSH_HOME` 和包含 IM bundle 的 profile 运行[监听重连测试夹具](tests/fixtures/listener-reconnect/overlay.yml)。该 overlay 会替换完整的运行时配置,并将投递 sqlite 文件保存在这个 home 下。将 overlay 复制到其他位置时,把 `./host.mjs` 改为夹具的绝对文件 URL。
115
+
116
+ `listAccountCandidates` 从已注册 transport 返回已安装的钉钉 profile 和旧版已准入旺旺商家。`startPlatformLogin`、`platformLoginStatus` 和 `cancelPlatformLogin` 委托可选的交互式 Provider 登录,不持久保存 attempt,也不暴露进程输出;Provider teardown 负责活跃 attempt。钉钉 setup 必须选择一个列表 Profile。新的旺旺 setup 可以直接携带使用时商家、主客服账号、HTTPS endpoint 与只写 AccessKey;runtime 把这些字段交给 Transport,并核对返回的商家身份及已验证客服身份。`discoverConversations` 为已配置账号返回提供方已知的会话标题及其稳定平台 id。`inspectConversation` 在列表页未包含该会话时按平台 id 解析标题。`previewAccountSetup` 要求 transport 校验只写输入,并返回安全身份、授权事实、过期时间与 Host 生成的 setup id,不创建账号,也不写入 Credentials。`confirmAccountSetup` 固定该身份,在同一持久操作收据中保存凭据记录与账号;相同 setup 与 operation id 会复用结果,即使 Host 重启也不会重复创建账号。`confirmAccountSetup.initialPaused: true` 可在发布前原子持久保存新账号的暂停状态;省略或 false 沿用默认未暂停行为。幂等指纹包含该选项,重放不能改变它。签名操作成功但没有足够独立身份凭据时,Provider 可以返回 `unchecked`;账号会保存,但在刷新授权取得已验证事实之前不能配置路由。已配置的同一钉钉 profile 或旺旺商家会以 `IM_ACCOUNT_EXISTS` 拒绝。`deleteAccount` 比较已观察 revision,停止监听,并删除账号及其路由;会话历史保留。`cancelAccountSetup`、请求取消、setup 过期与 runtime dispose 都会释放未确认的内存材料;JavaScript 不保证物理擦除内存。暂停、断开、重连、刷新或确认接入响应不确定时,调用 `queryAccountOperation` 查询。成功的 `deleteAccount` 会删除账号聚合,该操作没有后续查询收据。`inspectAccount` 和 `refreshAccount` 报告提供方观察到的授权事实,不改变账号身份。连接意图、授权状态和监听状态仍是三个独立事实。
59
117
 
60
118
  每条路由包含平台、账号、会话类型、`all` 或 `specific` 目标以及工作区归属。指定私聊目标可在稳定平台会话 ID 之外单独保留提供方对端标识。指定会话路由始终优先于全量路由,包括指定路由被停用时。私聊路由拒绝群触发设置;群聊路由必须至少配置 `mention`、正整数 `everyN` 或正整数 `fixedIntervalSeconds` 之一。`createRoute` 可记录可选的 `agentPreset` 与 `controller`。已记录的 preset 按原值使用;缺省时准入取 runtime 默认。空值会被拒绝,且声明 controller 必须同时记录 preset。已记录 preset 无法挂载时调用方必须失败,不得回退。产品 Provider 按 controller 身份注册到 `ctx.imRoutePolicies`。缺少 Provider 时拒绝准入和自动发送;未声明 controller 的普通路由跳过该注册表。
61
119
 
62
120
  `createRoute`、`saveRoute`、`rebindRoute` 和 `deleteRoute` 将结果与所属账号聚合记录一同保存。`saveRoute` 不能改变工作区归属;提交 `agentPreset` 与 `controller` 时会替换这两项。`rebindRoute` 与 `deleteRoute` 同时比较读取时的路由 revision 和工作区。传输层超时后,先调用 `queryRouteOperation`,再决定是否使用新的 operation id。模拟目标保存和移除按工作区提供相同的显式查询方式。`registerObservation` 让已连接账号在没有启用准入路由时仍接收历史;这些租约随注册方 Cordis fiber 生存和释放。
63
121
 
64
- `ingestInboundPage` 持久保存一个完整真实会话或已配置模拟会话页面,并按完整 scope 与平台消息标识去重。后到的重复消息只能把明确 mention 证据从缺失或 false 单调补为 true;已提交记录仍保持已提交,先前 Session 事件不回写。JSONL 导入只进入查询历史,不进入待处理 Agent 投递。崩溃后,`reconcileSession` 扫描 `SessionPersistence` 并确认匹配的稳定来源。投递记录和 Session 日志仍是独立持久写入,不声称存在跨日志事务。`listCustomers` 按账号分页可重建的会话摘要,真实与模拟行从不混排;字面子串搜索会转义 `%` 与 `_`。`ackCustomerRead` 以 max 语义推进 Host 拥有的读者水位。`setHandlingControl` 持久保存客户级自动处理暂停。客户索引表与投递 sqlite 共用同一文件;物理布局版本 2 从版本 1 显式迁移,磁盘上的更新版本会被拒绝。
122
+ `ingestInboundPage` 持久保存一个完整真实会话或已配置模拟会话页面,并按完整 scope 与平台消息标识去重。后到的重复消息只能把明确 mention 证据从缺失或 false 单调补为 true;已提交记录仍保持已提交,先前 Session 事件不回写。JSONL 导入只进入查询历史,不进入待处理 Agent 投递。崩溃后,`reconcileSession` 扫描 `SessionPersistence` 并确认匹配的稳定来源。投递记录和 Session 日志仍是独立持久写入,不声称存在跨日志事务。`listCustomers` 按账号分页可重建的会话摘要,真实与模拟行从不混排;字面子串搜索会转义 `%` 与 `_`。`projectConversations(project)` 为每个会话保留一个使用方值,在文档被解析或写入时从其游标、历史页与出站页派生;之后读取无需读取文档,由调用方释放该投影。`ackCustomerRead` 以 max 语义推进 Host 拥有的读者水位。`setHandlingControl` 持久保存客户级自动处理暂停。客户索引表与投递 sqlite 共用同一文件;物理布局版本 3 从版本 1 和 2 显式迁移,磁盘上的更新版本会被拒绝。
123
+
124
+ 客户列表的 `messagePreview` 对图片使用 `[image]`,对不支持内容使用 `[unsupported message type: <type>]`。文本或 Markdown 中的内嵌图片数据替换为 `[image]`,保留前后文字。原始消息内容不变。启动时不扫描或迁移既有摘要;该会话下次写入时更新预览。
125
+
126
+ `listCustomerSessions` 与 `listCustomers` 使用相同的分组、来源、搜索、排序和游标规则,但只返回客户键、类型、活动时间和最新 Session id,不查询未读数或处理控制。完整客户分页只计算返回分组的未读数,并在相同投递 changeSeq 和 reader 内复用。投递变化使数量失效;确认已读使对应 reader 的缓存失效。批量替换会清空缓存。该复用依赖投递连接是唯一写入者。
127
+
128
+ 会话的可选 `directRecipient` 字段扩展存储的 JSON 文档,不改变 `IM_DELIVERY_SCHEMA_VERSION`。新读取方接受缺少该字段的记录;旧读取方会丢弃未知字段,并保留原有的发送限制。旧版本重新打开并写入记录可能丢失对端身份。该字段独立于[持久化评审教程](../../docs/cookbook/reviewing-persistence-type-changes.zh.md)覆盖的 Session 声明持久化类型。
65
129
 
66
130
  提供方轮询游标由明确的平台账号与 stream 共同拥有。runtime 拥有的监听 sink 接收一个带稳定逐会话 operation id 的提供方页面。它先提交每个会话分组,再带全部页面收据提交提供方游标。如果进程在这些写入之间停止,或后续分组失败,重启后仍读取旧提供方游标,并通过持久去重安全重放各分组。监听器会收到启用路由计划,其中明确是否需要 mention 证据,以及可选的观察租约,使历史在没有准入时仍能流动。因此一个旺旺商家页面可以覆盖多个会话,钉钉也可组合全量群消息与 at-me 观察,而不会把提供方游标错误归给某个会话。
67
131
 
68
- `registerOutbound` 在任何平台调用前保存意图。`beginOutboundAttempt` 只授予一次尝试;调度结果未决时重启或重复 begin 会记录 `result-unknown`,调用者随后查询或确认,不盲目重试。自动意图冻结路由与账号 generation。`realScopeForSession` 只根据持久任务代次解析真实 Session,并返回冻结 scope、工作区、直接收件人、安全账号身份、账号及监听状态、最近会话同步时间,以及提供方已知的会话名称或群人数;未知名称和人数保持缺失。GUI 使用同一个稳定 request id 调用 `sendManualMessage`、`queryManualMessage` 与 `confirmManualMessage`;核验只调用提供方回执查询。`retryManualMessage` 要求新 request id 和同一 Session scope 中处于 `result-unknown` 的前序请求,并持久保存该关联。调用方可传可选的 `scopeId` 与 `accountId`;带 scope 的发送必须带 `accountId`。自动处理暂停时 DSH 人工发送仍可用;账号断连、未授权或监听不可用时拒绝新人工发送意图。已结算的人工发送会同时作为 `human-dsh` 入站保存,后续 Agent 准入可写入 `[operator from DSH]`,不必等待提供方回声。之后同一出站请求的提供方回声会替换占位的 `dsh-manual:<requestId>` 外部 id,不会再开第二条入站。自动意图继续受暂停和路由代次检查阻断。旺旺私聊收件人是本会话最后一个 external 发送者,查找范围包括已存储的全部历史消息。会话 id 不能充当收件人。只有本会话从未收到买家消息时才缺少收件人;此时自动发送和人工发送都以 `WANGWANG_DIRECT_RECIPIENT_UNAVAILABLE` 失败,且不调用 transport。已存收件人若不属于这些发送者,则忽略,直到观察到的发送者将其替换。
132
+ `executionForSession(sessionId)` 通过 `ImRuntimeService` 返回真实或模拟 IM Agent Session 最新保留的任务代次;没有记录时返回缺失。查询不创建 Agent,也不读取 Session 历史,产品可据此确保独立审计 Session 不属于 IM execution。
133
+
134
+ `sendManualMessage` 的文字请求可带 `expectedHandlingEpoch`。这类发送在准入及提供方向外提交前的最终检查中,都必须匹配同一持久暂停 epoch。恢复自动处理会使旧确认失效,包括提供方正在异步准备时。普通人工发送省略此限制。
135
+
136
+ `registerOutbound` 在任何平台调用前保存意图。`beginOutboundAttempt` 只授予一次尝试;调度结果未决时重启或重复 begin 会记录 `result-unknown`,调用者随后查询或确认,不盲目重试。自动意图冻结路由与账号 generation。`realScopeForSession` 只根据持久任务代次解析真实 Session,并返回冻结 scope、工作区、直接收件人、安全账号身份、账号及监听状态、最近会话同步时间,以及提供方已知的会话名称或群人数;未知名称和人数保持缺失。GUI 使用同一个稳定 request id 调用 `sendManualMessage`、`queryManualMessage` 与 `confirmManualMessage`。旺旺文字核验保留已存储结果,不查询提供方状态。其他平台的核验调用提供方回执查询。`retryManualMessage` 要求新 request id 和同一 Session scope 中处于 `result-unknown` 的前序请求,并持久保存该关联。调用方可传可选的 `scopeId` 与 `accountId`;带 scope 的发送必须带 `accountId`。自动处理暂停时 DSH 人工发送仍可用;账号断连、未授权或监听不可用时拒绝新人工发送意图。已结算的人工发送会同时作为 `human-dsh` 入站保存,后续 Agent 准入可写入 `[operator from DSH]`,不必等待提供方回声。之后同一出站请求的提供方回声会替换占位的 `dsh-manual:<requestId>` 外部 id,不会再开第二条入站。自动意图继续受暂停和路由代次检查阻断。旺旺私聊收件人优先使用已观察到的 external 发送者,其次使用会话上最新的平台对端身份。会话 id 不能充当收件人。入站页将平台提供的 `directRecipient` 持久保存,卖家消息也适用。两种身份都缺失时,自动发送和人工发送都以 `WANGWANG_DIRECT_RECIPIENT_UNAVAILABLE` 失败,且不调用 transport。旧的卖家先开口会话需等新事件补齐身份;发送时不补查平台。
69
137
 
70
138
  提供方把参与者和回显事实传给 `classifyInboundSender`。匹配已发送自动 outbox 时返回 `ai`,匹配已发送人工 outbox 时返回 `human-dsh`,明确的平台原生操作证据返回 `human-native`。若入站平台消息 id 已出现在已发送 outbox 上,即使提供方把该参与者标成外部,仍按该回声分类。该回声参与者或已配置账号的持久 actor id 之后再发的入站是 `human-native`,仍进入 Agent 准入。文本相等不会改变发送者归因。Agent 准入忽略 `ai` 行。mention 触发只使用随消息持久保存的提供方明确 mention 元数据。原生客户端人工行到达模型时为 `[operator from native client]`;DSH 人工行使用 `[operator from DSH]`。对端外部行使用 `[counterpart <name>]`,没有显示名时为 `[counterpart]`。角色标记是方括号前缀加空格,不是 `[label]: dest`,否则 GFM 会把它当成链接定义,轨迹页的 Markdown 预览就会是空的。这些标签保持平台中性,不把对端称为买家。
71
139
 
@@ -79,19 +147,44 @@ kind: "package-reference"
79
147
 
80
148
  `beginStopSimulation` 先持久保存 `stopping`,使后续输入和回复投递被拒绝,并且不等待工具调用方的当前 turn。`waitSimulationStopped` 是 GUI 的终态屏障。控制器只取消配对的实时活动与所选 scope 的 coordinator 工作,保留模拟用户 Session 和两份日志,并在静止后保存 `stopped`。GUI 与绑定的 `im_sim_stop` 无论调用顺序如何都会去重。已停止实例永不恢复;runtime 装载也不会只为显示运行中实例而恢复任一 Session。
81
149
 
82
- `@gestaltrun/dsh-im-runtime/tools` 入口在可信被测 Agent 上下文注册 `im_query_history`、`im_send_message` 和 scoped `im:takeover` 系统提示词 section。`im_send_message` 是该会话唯一出站路径;模型不得再把同一段回复写成 assistant 文本。模型参数不包含账号、会话、模拟实例、路由或工作区。符合条件的模拟用户 Agent 获得 `im_sim_create`;运行中配对另有准入组员发送、受管人工发送和 `im_sim_stop`。这些工具绑定实际调用 Session 与当前实例,不从模型参数接受这些身份。真实自动发送使用任务已记录的路由和观察到的旺旺对端;若该代次已过期,outbox 会在任何提供方调用前记录 `route-changed`。没有观察到对端时,旺旺发送在提供方调用前失败。模型可见的工具结果是 `Error: IM reply failed (WANGWANG_DIRECT_RECIPIENT_UNAVAILABLE <request id>): no counterpart sender has been observed in this Wangwang conversation, so nothing was sent. Wait for a counterpart message. Do not use the conversation id as the recipient.`
150
+ `@gestaltrun/dsh-im-runtime/tools` 入口在可信被测 Agent 上下文注册 `im_query_history`、`im_send_message` 和 scoped `im:takeover` 系统提示词 section。`im_send_message` 是该会话唯一出站路径;模型不得再把同一段回复写成 assistant 文本。模型参数不包含账号、会话、模拟实例、路由或工作区。符合条件的模拟用户 Agent 获得 `im_sim_create`;运行中配对另有准入组员发送、受管人工发送和 `im_sim_stop`。Host 驱动接管买家时拥有模拟用户工具面:接管期间该 Session 不再获得这些工具,自动回复投递保持暂停;释放后恢复。这些工具绑定实际调用 Session 与当前实例,不从模型参数接受这些身份。真实自动发送使用任务已记录的路由和可用的旺旺对端;若该代次已过期,outbox 会在任何提供方调用前记录 `route-changed`。既没有观察到对端、也没有保存平台对端身份时,旺旺发送在提供方调用前失败。模型可见的工具结果是 `Error: IM reply failed (WANGWANG_DIRECT_RECIPIENT_UNAVAILABLE <request id>): no counterpart sender or platform peer identity is stored for this Wangwang conversation, so nothing was sent. Wait for a new conversation event. Do not use the conversation id as the recipient.`
83
151
 
84
152
  -----
85
153
 
86
154
  <a id="understand-the-implementation"></a>
87
155
  ## 理解实现
88
156
 
157
+ `pauseAccountInput` 只关闭所选账号及其实际拥有的 Agent(含子 Agent)的新输入。有界排空等待后,交接所有者可调用 `cancelAccountActivityForPlacement`;它要求持有该账号暂停,只取消归属账号的运行任务,保留待接收 inbox,其他 Workspace 继续。随后由 `stopAccountForPlacement` 确认监听停止并取消未发送自动回复。已停止账号不能恢复该暂停,须通过托管恢复核验原交接。
158
+
159
+ `ImDeliverySqlite.exportAccountSlice(accountId)` 返回独立的真实会话记录、任务代次、平台游标回执、本机客户端身份、读水位和接待暂停事实,不包含模拟或派生索引。传输的 JSON 先经过 `parseImDeliveryAccountSlice`;只有写者和监听已停止、交接所有者已授权目标 epoch 时才能调用 `replaceAccountSlice`。替换在一个 SQLite 事务内完成,删除过时真实会话,保留其他账号和模拟,重建索引,并对读水位取最大值。任务和路由代次不变;交接所有者换算内置 Workspace id 并保存 epoch 回执。事务失败保留原切片;NAS 隔离后必须保持停止并核验。这些方法不授予托管权,也不启动接待。
160
+
89
161
  <details>
90
162
  <summary>实现细节——点击展开</summary>
91
163
 
92
- `gestaltrun_im_runtime` StorageDomain 为每个账号聚合、工作区模拟目标和双 Session 模拟实例保存一条记录,并继续使用 JSON 后端。会话投递、提供方游标、Agent 任务代次和客户索引表保存在 Host 自管的 sqlite 文件中,物理 schema 版本单调为 2。runtime 按 key 读取这些记录,并保留一小份已解析文档缓存;打开时不会载入全部会话。一次会话写入仍按 scope、游标或 execution 各存一个 JSON 文档,并在同一 sqlite 事务中更新客户索引。提供方游标只在引用的页面收据都存在后提交。任何操作都不声称在凭据、配置、投递、提供方游标、Agent 任务、模拟实例或 Session 记录之间提供原子事务。
164
+ `gestaltrun_im_runtime` StorageDomain 为每个账号聚合、工作区模拟目标及人工或双 Session 模拟实例保存一条记录,并继续使用 JSON 后端。会话投递、提供方游标、Agent 任务代次和客户索引表保存在 Host 自管的 sqlite 文件中,物理 schema 版本单调为 3。runtime 按 key 读取这些记录,并保留一小份已解析文档缓存;打开时不会载入全部会话。账号会话列表读取账号索引与客户摘要;SDK 消息关联保留从每次解析或写入的会话文档派生的标识,因此检查点、变更通知和 SDK 路径不会重复读取会话文档。一次会话写入仍按 scope、游标或 execution 各存一个 JSON 文档,并在同一 sqlite 事务中更新客户索引。提供方游标只在引用的页面收据都存在后提交。任何操作都不声称在凭据、配置、投递、提供方游标、Agent 任务、模拟实例或 Session 记录之间提供原子事务。
165
+
166
+ 上传观察仅为已有聚合 JSON 增加可选字段,不新增表、列或索引。媒体功能之前的 v2 数据库与含上传观察的 v2 数据库均升级到 v3,保留这些字段及读水位。托管旺旺消息的变更与不可变上传观察在同一 SQLite 事务中提交。`bindMessageUpload` 将观察转移到 `<deliverySqlitePath>.uploads.json`,同步队列文件后才清除待转移记录。收到确认的行退出队列;网络、响应校验和队列存储失败保留可恢复工作,不阻止平台收发。关闭时,worker 中止并等待请求结束。`uploadRetryMs` 控制重试间隔(默认 5000 毫秒,范围 100–600000);Host 日志显示积压数量和安全错误类别。迁移 Host 状态时,队列文件必须与投递数据库一起保留。
167
+
168
+ 队列在 `<deliverySqlitePath>.uploads.json.writer.sqlite` 持有独占事务,直到 `close()` 中止并等待请求结束。只有下一任 writer 才会在读取已提交队列前清理该队列以 UUID 命名的 `.pending` 崩溃遗留文件。rename 前失败会关闭自有描述符并尝试删除临时文件;清理失败保留操作错误为 `cause`、记录全部清理错误,并在 Host 日志报告安全错误码。有效队列与 runtime SQLite 日志均不会被删除。任何 Host 仍可能使用队列时,都必须保留 writer 锁文件。
169
+
170
+ 永久拒绝标记与原条目一起保存在同一个队列文件中。被拒条目在重启后保留。默认可见的诊断仅在摘要或其他待传条目的重试原因变化时报告数量,以及最多三个标识和原因码。这些条目不再自动提交,其他条目继续上传。网络失败仍可重试;无效或不完整的回执不会移除条目。
171
+
172
+ 需要放弃被拒观察时,先停止所属 Host,并备份队列文件。只选择 `rejections` 中已有的标识,从 `entries` 和 `rejections` 同时移除这些标识,保留其他全部字段和条目,并以原子替换方式保存。重启 Host 后核对剩余队列。保留 writer 锁文件和投递 SQLite 数据库。删除表示放弃本次上传,不撤销控制操作或服务端事实。修改后的队列校验失败时恢复备份。登录账号不匹配时,应换回匹配账号,不应删除事件。
173
+
174
+ 消息观察分别保留捕获时的平台发送账号和分配账号。重新登录后,出站修订仍使用原发送账号。已有消息没有捕获身份时,后续修订仍将全部身份字段保持为未知,不用后来的分配账号或配置账号回填。本地账号别名和会话标识不能代替平台发送账号。已成功执行的产品暂停、恢复和转交操作,在写入处理控制的同一 SQLite 事务中追加 `conversation-event` 观察。没有转交成功回执的安全暂停不报告转交事件。server 事件格式没有流量标记,模拟和测试流量的控制只保留在本地。上传 worker 沿用既有队列,在逐项核对 V2 回执后才移除条目。账号交接等待在途上传,并将文件队列中未确认的观察放入账号切片。重放观察不会执行控制操作。读取兼容性见[持久格式记录](persistence-changes/2026-10-24-upload-identity.md)。
93
175
 
94
- `ImTransports` 为每个平台保留一个存活的提供方,并随注册方 Cordis fiber 释放。只有账号连接意图为连接、未暂停、授权状态为 `ready`,并且存在启用路由或存活的观察租约时,runtime 才启动监听器;`unchecked`、`required` 或 `failed` 授权状态会使监听和手动发送保持停止。提供方能返回独立身份依据后,刷新 `unchecked` 账号即可启用接管。提供方 `listen` 只在监听器已建立并验证可用后返回,其返回的 `done` promise 报告后续正常结束或失败。主动停止会中止 signal、调用 `dispose` 并等待 `done`;启用路由计划变化时也按此顺序停止旧监听器,再启动替代实例。监听器终止失败不会触发无界重试。`ctx.imRuntime.subscribe` 和类型化 `imRuntime/changed` 事件发布持久变更及后续进程监听状态变化。snapshot revision 只在当前进程 generation 内排序;持久 operation id 与记录 revision 跨重启保留。
176
+ 上传保持旺旺主账号 id 和提供方消息 id;本地 scope id 和人工消息历史占位 id 不作为上游 id。runtime 修订的捕获时间同时写入 `sourceUpdatedAt` 和 `sourceOrder.kind=time`,重试沿用该时间和版本哈希。出站生命周期和归因修订各自保存为观察。真实出站消息缺少提供方 id 时持久等待,迟到回执提供 id 后补传;始终没有 id 的结果无法通过当前 server API 上传。已被精确回执认领为 `ai` 或 `human-dsh` 的回声修订保留 `outboundRequestId`,并把同一值写入 evidence 的 `requestId`;现有 server 上传消费者读取该字段。未被认领的 `human-native` 观察仍为 `native_human`,不写入 request id。模拟行使用带实例身份的合成 id 和 `traffic=simulation`;配置 `uploadTestRunId` 将真实提供方验收流量标记为 `test`。JSONL 历史导入不进入实时上传。
177
+
178
+ `ImTransports` 为每个平台保留一个存活的提供方,并随注册方 Cordis fiber 释放。只有账号连接意图为连接、未暂停、授权状态为 `ready`,并且存在启用路由或存活的观察租约时,runtime 才启动监听器;`unchecked`、`required` 或 `failed` 授权状态会使监听和手动发送保持停止。提供方能返回独立身份依据后,刷新 `unchecked` 账号即可启用接管。提供方 `listen` 只在监听器已建立并验证可用后返回,其返回的 `done` promise 报告后续正常结束或失败。主动停止会中止 signal、调用 `dispose` 并等待 `done`;启用路由计划变化时也按此顺序停止旧监听器,再启动替代实例。可恢复失败进入 `reconnecting`,按指数退避重试:`listenerReconnectInitialMs` 默认 1000,`listenerReconnectMaxMs` 默认 30000,`listenerReconnectMaxAttempts` 默认 3。`listenerReconnectStableMs` 默认 60000,接受 1–2147483647 的整数毫秒值(Node 定时器上限);这些字段均可在 `cordis.yml` 配置。间隔范围为 1–600000 毫秒,最大间隔不得小于初始间隔;重试上限范围为 0–20,零表示不自动重试。授权失败或重试用尽进入 `failed`,`lastError` 保留安全的平台错误码。进入 `running` 时保留预算;只有该监听连续运行满 `listenerReconnectStableMs`,或在运行期间成功持久提交一页提供方数据后才清零。启动阶段的首页、游标冲突和已停止监听代次的完成事件都不会清零。暂停、断开、移除 Provider 或卸载 runtime 都会取消重试及稳定运行定时器。新的有效 `reconnectAccount` 操作也能重启连接中账号的失败监听并重置预算;重放该操作不会再次重启。`ctx.imRuntime.subscribe` 和类型化 `imRuntime/changed` 事件发布持久变更及后续进程监听状态变化。snapshot revision 只在当前进程 generation 内排序;持久 operation id 与记录 revision 跨重启保留。
179
+
180
+ `createManualSimulationInstance({ routeId })` 冻结一条已启用的全量私聊路线,只创建其被测 Session。账号内的 `manual-sim-NNN` 编号超过所有已用编号,包括已停止和失败的实例。空对话立即以 `人工模拟NNN` 列出;买家首条消息保持客户 key 不变。被测回复留在对话历史,不驱动另一个 Agent。双 Session 的 `createSimulationInstance` 入口保留模拟用户 Session 行为。人工记录同时省略两个模拟用户字段;双 Session 记录同时保留。新版本读取两种记录,旧版本拒绝人工记录;不支持降级。[持久化确认记录](persistence-changes/2026-10-16-manual-simulation.md)保存生成的 schema 与兼容性检查。
181
+ ### 准入附注
182
+
183
+ `registerAdmissionAnnotator(provider)` 在普通准入与 steer 共用的组装点注册可选分析,返回取消并等待其工作的异步 disposer。提供者声明 id、版本、期限和处理范围。成功文本以 `[analysis <id>]` 放在入站原话之后;超时或失败时保留原话并记录状态。`inject: false` 仅分析,不增加模型文本或来源元数据;省略该字段则启用注入。runtime 在写入 Session 前冻结结果,恢复已准备批次时不重新分析。提供者必须在取消后结束,并在更新自有状态前丢弃晚到结果。
184
+
185
+ 客服托管通过同一个附注插槽补充暂停期间未处理的对话。公开的 `formatInbound(message, timeZone)` 渲染函数为产品附注提供发送人标签和收到时间。产品包拥有提供者、已校验的条数上限和截止时间,见[客服托管包](../cs-hosting/README.zh.md#paused-conversation-history);IM 准入使用通用 `[analysis <id>]` 说明。
186
+
187
+ 可选的 pending-batch 与来源字段保留旧批次身份,见[持久化确认](persistence-changes/2026-10-18-admission-annotations.md)。语义题目、校准、缓存和启用策略属于产品包,IM runtime 不负责消息分类。
95
188
 
96
189
  | 源文件 | 用途 |
97
190
  |---|---|
@@ -107,7 +200,7 @@ kind: "package-reference"
107
200
  | `src/route-resolve.ts` | 已存储路由的显式 Agent preset 解析 |
108
201
  | `src/transports.ts` | 可逆的平台提供方注册表 |
109
202
  | `src/agent-coordinator.ts` | 持久触发判断和普通 Agent 创建、恢复与 steer 生命周期 |
110
- | `src/simulation-controller.ts` | 持久双 Session 创建、隔离回复投递、恢复与终态停止 |
203
+ | `src/simulation-controller.ts` | 持久人工与双 Session 创建、隔离回复投递、恢复与终态停止 |
111
204
  | `src/simulation-tools.ts` | 绑定可信 Session 的模拟工具注册 |
112
205
  | `src/tools.ts` | scope 绑定的历史与出站 Agent 工具 |
113
206
 
@@ -125,10 +218,45 @@ kind: "package-reference"
125
218
 
126
219
  -----
127
220
 
221
+ <a id="restart-recovery"></a>
222
+ ## 重启恢复
223
+
224
+ Host 启动时只查询当前客服托管路由登记的会话索引及最新任务代次,不枚举 Session 目录。尾轮未结束时,通过公开 Agent 恢复接口补上 `interrupted` 结束事件和缺失工具结果;随后在同一 Session 提交普通跟进,提醒模型检查工具、持久子会话和后台任务,需要时继续并回复顾客。跟进正文由产品 prompt 持有,随 Inbox 与 `user/message` 事件落盘,可从 Session 日志重建模型输入。
225
+
226
+ 跟进的 source 关联中断轮的日志位置。同一次 Host 启动不会重复发;已落盘但未消费的跟进复用原消息身份唤醒。下一次 Host 只为新的中断登记新跟进。扫描期间其他消费者已恢复的 Session 仍按其 `interrupted` 证据核对;当前进程新开的轮次不作为崩溃尾轮。托管策略、当前路由代次、顾客暂停及模型准入在恢复前后仍然生效,条件恢复时重新核对。
227
+
228
+ 读取、Agent 恢复或跟进落盘失败会停止该会话的自动准入,并在默认 Host error 日志报告 `IM_RESTART_RECOVERY_FAILED`;检查 Session 存储和模型配置后重启 Host。公开的 Session 写入所有权占用会保留恢复任务,并通过 `IM_RESTART_OWNERSHIP_WAIT` 报告等待;会话发布或准入状态变化后重新核对,不夺取其他进程的写入权。不存在的 Session 留给未完成准入的正常恢复。持久子会话可通过已有 subagent 继续接口恢复;进程内后台任务不承诺跨重启存活。Schedule 的持久提醒仍按原 Session 的普通跟进路径送达。
229
+
230
+ <a id="buyer-read-receipts"></a>
231
+ ## 买家已读回执
232
+
233
+ 将 `@gestaltrun/dsh-im-runtime/read-receipts` 与 runtime 和可选旺旺 SDK 一同挂载,产品 bundle 已包含它。`pendingMessageLimit` 默认 1000,限制等待匹配持久化 OpenAPI 记录的 SDK 观测;超限观测保持未知,并产生安全诊断。本模块拥有独立的 `gestaltrun_im_reads` domain,discriminator 为 `gestaltrun.im-reads.v1`;SQLite 和已有账号记录无需迁移。
234
+
235
+ 入站已读上报要求消息为已持久化的实时单聊、买家身份已核验,并精确匹配 SDK messageId/UUID。先持久保存尝试标记,再调用 SDK;失败保持未确认,不重试。出站关联接受精确匹配的配置卖家 SDK 回显、成功的 SDK 文字或媒体回执,或消息 ID 已属于已确认 outbox 记录的已读推送。回执和直接推送关联按持久买家身份核验 SDK 会话;文字通道会话 ID 不是 SDK ID。旺旺文字现在返回 SDK 平台消息 id,已读和上传都使用这个 id。迟到的文字回执结算 request id 相同的 outbound,并把已经带有该平台消息 id 的实时 history 改回该 outbound 的 AI 或 DSH 人工发送者。其他 id 的行保留原有发送者证据。该行若已进入上传队列,修正后的发送者会再上传一次。这次上传保留 `outboundRequestId`,并把同一值写入 evidence 的 `requestId`。只有 SaaS 本地文字 ID、没有该平台 id 的历史记录保持未知,不能用正文相等或时间相近建立映射。人数汇总只有在接收范围已核验为唯一目标买家时才可确认买家已读;精确查询按目标买家 UID 和 cntaobao 域匹配状态 2。状态 0 确认未读;状态 1、缺字段、其他接收方和自己的读者通知均不能确认买家已读。每条消息、每个 SDK generation 至多执行一次关联查询和一次推送消歧查询,不轮询,也不自动重试失败的查询。
236
+
237
+ 已确认的已读在重启和 SDK 下线后保留,未读投影要求当前 generation 的证据。绑定的 Agent 在下一次获准 step 收到必读的 `im/message-read` Session 事实和一条 `im-read` notice context,不唤醒闲置 Agent。事实和 context 元数据可在重启后重建去重状态。已读记录只含关联证据,不含消息正文或凭据。关闭插件时先等待在途 SDK 操作和 context 消费者结束,再关闭 domain。
238
+
239
+ `injectSimulationOperator({ instanceId, requestId, text })` 只在被测 Agent 运行时接收私聊操作员输入。它持久化一条不带买家或操作员标签的普通 `user` 消息,不新增 IM 历史、不调用传输发送。返回的 Session/消息 id 表示持久化收件箱已接收,不表示回答完成。同一请求 id 与正文幂等;正文冲突或实例已停止时拒绝。`im_sim_operator_dm` 将此操作提供给交互式模拟侧,并绑定当前实例。计划中的人工动作直接调用 Host 接口;买家角色的工具由其所有者单独控制。
240
+
241
+ <a id="simulation-batches"></a>
242
+ ## 批量模拟与剧本时间
243
+
244
+ `ImSimulationBatch` 接收 Host runtime 与已校验的 `maxConcurrency` 限制(默认 2)。`run(jobs, signal)` 为每个请求创建会话对、执行回调,并等待停止收敛后按输入顺序返回结果。`stop()` 取消排队创建与活跃回调,等待清理结束。清理失败会取消剩余工作,并保留执行和清理两个错误。调用方继续拥有传入的买家 Session;回调必须在取消后结束。
245
+
246
+ `ImScriptClock(initialAt)` 校验 13 位 Unix 毫秒时间,并从该起点随真实耗时前进。`advance(at)` 跳到较晚的分组时间并重设耗时锚点;较早的分组保持当前时钟继续前进,无效时间抛错。通过 `registerSimulationClock` 注册后,同一来源用于两个 Session 的当前时间上下文、模拟消息的发生与收到时间,以及出站的创建与发送时间。在 Host 步骤之间推进。回复等待超时可以在被测 Agent 仍运行时结束 Host 步骤;之后的消息和工具调用使用推进后的剧本时间。`holdSimulationBuyer(instanceId)` 在 Host 角色驱动期间暂停自动买家投递并移除模拟操作员工具。`deliverSimulationBuyerReplies(instanceId)` 通过原生 IM 输入向已被持有且存活的买家投递待处理 AI 行和符合条件的 `human-dsh` 行。人工投递要求 Host 驱动且已确认转人工。返回值统计两种已持久接收的买家输入,包括 inbox 接收;零表示没有投递。AI 行标记为 submitted;人工行保持 received,并以买家 Session 输入去重。未被持有或已停止的实例会被拒绝。自动处理暂停期间持久接收的模拟输入立即被抑制,不等待 AI 回复。Host 在投递前准备状态,并在进入下一步前等待买家完成;实例停止后才释放 disposer,否则排队回复会恢复普通买家投递。
247
+
128
248
  <a id="model-experience"></a>
129
249
  ## 模型体验
130
250
 
131
- 每个准入批次通过目标 Agent preset 组装为一条带稳定标识的 user message。其 source 包含崩溃核对所需的完整持久证据。Agent 作用域的 `im:takeover` section 紧挨部署人格前缀之后,告诉模型它在这一个会话中以已配置账号发言。带标签的 user-role 行是 IM 记录。没有标签的用户消息是操作者直接对 Agent 说的话,总是在本 Session 内得到 assistant 文本回复。assistant 文本展示给操作者,从不发到 IM。入站行上的 counterpart 与 operator 标签是仅有的逐条身份。scope 绑定的 IM 工具只暴露已准入会话的历史与出站路径。模拟工具只对已配置的模拟用户工作区或已有绑定实例可见,其实例身份来自实时 Agent Session。
251
+ `timeZone` 是店铺的 IANA 时区(默认 `Asia/Shanghai`);无效时区使插件加载失败。每条入站记录(包括买家、操作员消息及 `im_query_history` 结果)在发送者标签前带持久化的收到时间 `[YYYY-MM-DD HH:mm]`。`occurredAt` 仍单独保存提供方的时间证据。
252
+
253
+ Host 可为创建中或运行中的实例调用 `registerSimulationClock(instanceId, source)`。`source.now()` 返回 Unix 毫秒,供该实例的两个 Session 使用;没有注册时,`currentTime(sessionId)` 读取系统时间。调用方在步骤结束之间推进时钟。注册独占且仅存于内存,由幂等 disposer、实例终止或 Host 释放移除;重启后需重新注册,时钟源异常直接传播,不回退到系统时间。
254
+
255
+ `im_send_message` 接受可选的有序附件。路径附件是 `{path, kind}`,`kind` 为 `image` 或 `file`。知识图片附件是 `{source:{type:'knowledge-image', referenceId}, kind:'image'}`,不能同时带 `path`。一次最多四张知识图片,且引用必须来自本 Session 已提交的知识工具结果和当前店铺。Host 自己读取字节;Agent 拿不到文件路径,托管只读文件策略也不放宽。路径通过 Agent 的文件系统与当前目录解析,也可使用 Session 附件展示的路径。路径图片按文件头识别 PNG、JPEG、GIF 或 BMP 的 MIME,不依赖路径扩展名。发送到 IM 的文件名在没有扩展名时补 `.png`、`.jpg`、`.gif` 或 `.bmp`;已有文件名保持不变。无法识别的格式(包括 WebP)会返回 `IM_MEDIA_INVALID` 与“请人工换一张 PNG、JPEG、GIF 或 BMP 图片”的提示,不提交剩余媒体或文字。文件附件保留名称与原始字节;知识图片引用保留已准备的名称与 MIME。开始发送前,每个来源都在 `mediaMaxBytes` 范围内读取,并保存为不可变文件附件。媒体按数组顺序经 `sendMediaMessage` 发送;只有所有媒体成功后,非空 `text` 才经同一个在线 IM SDK generation 发送。空文字表示只发附件。旺旺文字和媒体都要求真实单聊以及在线的 IM generation。IM 离线、被踢或结果不确定时,文字不会回退到 OpenAPI。媒体不可用时不会降级为只发文字。参见[旺旺文字发送指南](../../docs/upgrade-guide/v0.2.1-rc.2/im-sdk-text-send/guide.zh.md)。generation 变化会停止剩余媒体和文字。
256
+
257
+ 附件调用返回有序 `deliveries`,包含 request id、类型、状态及持久文件引用,明确保留部分成功。第一个失败或未知结果会停止后续消息。后续附件读取或文字步骤异常会返回 `IM_MEDIA_BATCH_INTERRUPTED`,保留已完成或结果未知的 `deliveries`,并在 `unsubmitted` 中只列出确认未提交的项;解决故障后仅对这些项发起新调用,不重发成功或未知项。批次执行期间取消使用 `IM_MEDIA_BATCH_CANCELLED`。`tools/result` 观察者仅在最终工具结果丢失 `deliveries` 时补充发送证据,包括 post-execute 期间取消。补充内容以纯文字进入 Agent 的持久化 next-step inbox:request id、类型、状态、错误码、文件名与字节数,以及未提交项,不含图片或文件块。正常成功、失败及未知结果均不增加上下文。标准 inbox 和 user-message 事件在内容到达模型前保存可重建的补充记录。同一 Session 工具调用重复执行会复用媒体与文字的 request id,未知结果不会再次提交。修正参数或主动发起新调用会有不同的 tool call id。标准 `tool/call` 与 `tool/result` 事件保存参数、结果及不可变文件引用,无需新增 Session 事件类型。Outbox 保存提供方生成的源文件引用,因此即使选定路径已不存在,重启后仍能读取原未知结果。仅文字的旺旺调用使用该 IM SDK generation。没有平台消息 id 的结果保持未知,不会再次发送。
258
+
259
+ 每个准入批次通过目标 Agent preset 组装为一条带稳定标识的 user message。其 source 包含崩溃核对所需的完整持久证据。Agent 作用域的 `im:takeover` section 紧挨部署人格前缀之后,告诉模型它在这一个会话中以已配置账号发言。带标签的 user-role 行是 IM 记录。没有标签的用户消息是操作者直接对 Agent 说的话,总是在本 Session 内得到 assistant 文本回复。assistant 文本展示给操作者,从不发到 IM。模型自行决定何时结束本轮;IM 提示词不要求空回复收尾。入站行上的 counterpart 与 operator 标签是仅有的逐条身份。scope 绑定的 IM 工具只暴露已准入会话的历史与出站路径。模拟工具只对已配置的模拟用户工作区或已有绑定实例可见,其实例身份来自实时 Agent Session。
132
260
 
133
261
  #### KV Cache 影响
134
262
 
@@ -145,6 +273,10 @@ kind: "package-reference"
145
273
 
146
274
  客户暂停通过 handling epoch 使自动输入和自动发送授权失效。恢复只准入新实时输入,先前排队的 AI 回复不能发送。待准入批次重载后保留原始 epoch。Forward journal 的 owner 用 `setAutomaticHandlingPausePending` 投影未落实的暂停;`isAutomaticHandlingBlocked` 将该阻断与持久 handling control 合并。阻断不伪造持久暂停或 epoch,并跨 Provider 卸载保留,直到 journal owner 协调好持久化与 acknowledgement。Pending 阻断准入、pre-step、出站注册与最终提交。自动发送的 Provider 准备阶段携带该 scope 的 epoch 撤销信号。Provider 在异步准备完成后、向外提交之前调用 `submission.authorizeSubmission()`;该检查还核验持久尝试与路由代次。客户暂停撤销尚未提交的准备,即使随后恢复也不复活旧授权。已经提交的消息保留真实的 Provider 结算,结算后由 Runtime 调用方释放授权。人工发送和 Provider 历史同步不受客户 AI 暂停影响。
147
275
 
276
+ 云端配置通过 `requireNasLease` 等待新代次准入,`deliveryJournalMode` 控制 WAL/DELETE,默认 WAL。`deliveryLockingMode` 可选 `NORMAL`(默认)或 `EXCLUSIVE`;后者要求从首次事务到连接关闭始终只有一个持有者。`deliveryCacheSizeKiB` 为 1–2147483647 的整数,省略时保留 SQLite 默认页缓存;该值是缓存预算,不会在启动时预读整个库。配置在打开文件前校验,并核对 PRAGMA 实际值。投递库遇到另一持有者的锁时,`deliveryBusyTimeoutMs`(默认 2000 毫秒)控制单条语句的等待时长;迁移的清理、每批提交和版本戳在 `deliveryMigrationRetryBudgetMs`(默认 30000 毫秒)预算内先回滚再退避重试,超限抛 `IM_DELIVERY_SQLITE_BUSY`,`user_version` 保持不变,下次打开重新迁移。详见[云端 NAS 存储](../cs-hosting/nas-storage.md)。
277
+
278
+ 同时配置 `requireNasLease: true`、`placementStandby: true` 与绝对路径 `standbyStorageRoot` 时,未持有 NAS 租约的 Host 在实例私有目录打开既有 IM schema 与投递 SQLite。真实账号准备、真实消息准入与真实发送均被拒绝。模拟使用不含平台凭据的断开连接账号。placement standby 只阻止真实 scope;显式 Host 或账号暂停仍阻止模拟。placement 所有者先等待评测与 CLI 工作结束,取得租约后调用 `reloadWithNasLease()` 关闭私有资源并重载正式存储。等待租约准入期间,私有 runtime 保持可读。保留的评测资源与 Session 日志不导入正式存储。
279
+
148
280
  <a id="dev-note"></a>
149
281
  ### 开发说明
150
282
 
package/lib/index.js CHANGED
@@ -1,2 +1,140 @@
1
- import { a as ImRoutePolicies, c as IM_DELIVERY_SCHEMA_VERSION, d as encodeImScopeId, f as imRuntimeDomainSpec, i as resolveRouteAgentPreset, l as ImDeliverySqlite, o as ImTransportError, p as ImRuntimeError, s as ImTransports, t as ImRuntime, u as encodeImProviderCursorId } from "./runtime-C0-S-_CC.js";
2
- export { IM_DELIVERY_SCHEMA_VERSION, ImDeliverySqlite, ImRoutePolicies, ImRuntime, ImRuntime as default, ImRuntimeError, ImTransportError, ImTransports, encodeImProviderCursorId, encodeImScopeId, imRuntimeDomainSpec, resolveRouteAgentPreset };
1
+ import { C as ImDeliverySqlite, D as resolveSqliteConnectionSpec, E as configureSqliteConnection, O as imRuntimeDomainSpec, S as IM_DELIVERY_SCHEMA_VERSION, T as parseImDeliveryAccountSlice, _ as resolveRouteAgentPreset, b as ImTransportListenerError, c as imageMime, d as ImPlacementStateError, f as parseImAccountPlacementSlice, g as formatImTime, h as checkpointFailureDiagnostic, l as resolveKnowledgeImage, m as ImInputPauseError, n as ImConsumerChatLinkError, o as bindKnowledgeImageResolver, p as ImCheckpointRecoveryError, r as ImConsumerChatLinks, s as hasKnowledgeImageResolver, t as ImRuntime, u as formatInbound, v as ImRoutePolicies, w as ImPlacementSliceError, x as ImTransports, y as ImTransportError } from "./runtime-Bm6iSEpv.js";
2
+ import { a as senderUid, c as encodeImScopeId, m as ImRuntimeError, r as buyerUid, s as encodeImProviderCursorId } from "./read-state-CnUZKCuY.js";
3
+ import { z } from "zod";
4
+ import { basename, dirname, resolve } from "node:path";
5
+ import { Service } from "@deepseek-ai/cordis";
6
+ import { existsSync, realpathSync } from "node:fs";
7
+ //#region src/nas-host-lease.ts
8
+ /** NAS 数据的 Host 租约接口;由产品控制面 Provider 实现,两个数据库共用同一实例。 */
9
+ /** 云端 Host 准入及同步写入隔离;租约失效后同一实例不得重新准入。 */
10
+ var NasHostLease = class extends Service {
11
+ /** @param ctx - Host 的 Cordis 上下文。 */
12
+ constructor(ctx) {
13
+ super(ctx, "nasHostLease");
14
+ }
15
+ databases = /* @__PURE__ */ new Set();
16
+ /** @param path - 数据库绝对路径。 @returns 关闭连接后释放占用;同一 Host 不能重复打开。 */
17
+ reserveDatabase(path) {
18
+ this.assertActive();
19
+ const absolute = resolve(path);
20
+ const key = existsSync(absolute) ? realpathSync(absolute) : joinParent(absolute);
21
+ if (this.databases.has(key)) throw new Error("同一云端 Host 不能重复打开 NAS 数据库。");
22
+ this.databases.add(key);
23
+ return () => {
24
+ this.databases.delete(key);
25
+ };
26
+ }
27
+ /** @returns 当前已准入租约的商家成员与云端沙箱,供客户数据写入构造租约证明;未准入、已失效或实现不提供时为 undefined。 */
28
+ holderIdentity() {}
29
+ };
30
+ function joinParent(path) {
31
+ return resolve(realpathSync(dirname(path)), basename(path));
32
+ }
33
+ //#endregion
34
+ //#region src/script-clock.ts
35
+ /** Host-owned clock. Advance between Host steps, including reply-wait timeouts; an unfinished Agent uses the advanced time for later events. */
36
+ var ImScriptClock = class {
37
+ realNow;
38
+ instant;
39
+ anchoredAt;
40
+ /** @param initialAt - Script origin in 13-digit Unix milliseconds. @param realNow - Source of elapsed real milliseconds. */
41
+ constructor(initialAt, realNow = Date.now) {
42
+ this.realNow = realNow;
43
+ this.instant = validInstant(initialAt);
44
+ this.anchoredAt = realNow();
45
+ }
46
+ /** @returns Current script instant in Unix milliseconds. */
47
+ now() {
48
+ return this.instant + (this.realNow() - this.anchoredAt);
49
+ }
50
+ /** @param at - Next segment instant; earlier instants keep the running clock, invalid instants throw. */
51
+ advance(at) {
52
+ validInstant(at);
53
+ if (at <= this.now()) return;
54
+ this.instant = at;
55
+ this.anchoredAt = this.realNow();
56
+ }
57
+ };
58
+ function validInstant(at) {
59
+ if (!Number.isSafeInteger(at) || at < 0xe8d4a51000 || at > 9999999999999) throw new RangeError("Script time must be 13-digit Unix milliseconds");
60
+ return at;
61
+ }
62
+ //#endregion
63
+ //#region src/simulation-batch.ts
64
+ /** Bounded Host execution of paired IM simulations with awaited terminal cleanup. */
65
+ /** Batch owner; caller-owned simulated-user Sessions remain available after instance cleanup. */
66
+ var ImSimulationBatch = class {
67
+ runtime;
68
+ concurrency;
69
+ active;
70
+ /** @param runtime - Host instance lifecycle authority. @param config - Validated concurrency limit. */
71
+ constructor(runtime, config = {}) {
72
+ this.runtime = runtime;
73
+ this.concurrency = z.number().int().min(1).max(100).parse(config.maxConcurrency ?? 2);
74
+ }
75
+ /**
76
+ * Run one batch, retaining input order and stopping every created instance before resolving.
77
+ * @param jobs - Distinct live simulated-user Sessions; callbacks must settle on cancellation.
78
+ * @param signal - Optional caller cancellation; absent means only stop() can cancel the batch.
79
+ * @returns One result per job; cleanup failure cancels remaining jobs and retains both failures. Concurrent run calls reject.
80
+ */
81
+ run(jobs, signal) {
82
+ if (this.active !== void 0) return Promise.reject(/* @__PURE__ */ new Error("A simulation batch is already running"));
83
+ const abort = new AbortController();
84
+ const effective = signal === void 0 ? abort.signal : AbortSignal.any([abort.signal, signal]);
85
+ const results = new Array(jobs.length);
86
+ let index = 0;
87
+ const worker = async () => {
88
+ for (;;) {
89
+ const current = index++;
90
+ const job = jobs[current];
91
+ if (job === void 0) return;
92
+ let instance;
93
+ try {
94
+ effective.throwIfAborted();
95
+ instance = await this.runtime.createSimulationInstance(job.request);
96
+ effective.throwIfAborted();
97
+ results[current] = {
98
+ status: "fulfilled",
99
+ value: await job.execute(instance, effective)
100
+ };
101
+ } catch (error) {
102
+ results[current] = {
103
+ status: "rejected",
104
+ error
105
+ };
106
+ } finally {
107
+ if (instance !== void 0) try {
108
+ await this.runtime.beginStopSimulation(instance.instanceId);
109
+ await this.runtime.waitSimulationStopped(instance.instanceId);
110
+ } catch (error) {
111
+ const prior = results[current];
112
+ results[current] = {
113
+ status: "rejected",
114
+ error: prior?.status === "rejected" ? new AggregateError([prior.error, error], "Simulation work and cleanup failed") : error
115
+ };
116
+ abort.abort(error);
117
+ }
118
+ }
119
+ }
120
+ };
121
+ const done = Promise.all(Array.from({ length: Math.min(this.concurrency, jobs.length) }, worker)).then(() => results);
122
+ const active = {
123
+ abort,
124
+ done
125
+ };
126
+ this.active = active;
127
+ return done.finally(() => {
128
+ if (this.active === active) this.active = void 0;
129
+ });
130
+ }
131
+ /** Cancel active work and queued creation, then await all callbacks and terminal cleanup. */
132
+ async stop() {
133
+ const active = this.active;
134
+ if (active === void 0) return;
135
+ active.abort.abort(/* @__PURE__ */ new Error("Simulation batch stopped"));
136
+ await active.done;
137
+ }
138
+ };
139
+ //#endregion
140
+ export { IM_DELIVERY_SCHEMA_VERSION, ImCheckpointRecoveryError, ImConsumerChatLinkError, ImConsumerChatLinks, ImDeliverySqlite, ImInputPauseError, ImPlacementSliceError, ImPlacementStateError, ImRoutePolicies, ImRuntime, ImRuntime as default, ImRuntimeError, ImScriptClock, ImSimulationBatch, ImTransportError, ImTransportListenerError, ImTransports, NasHostLease, bindKnowledgeImageResolver, buyerUid, checkpointFailureDiagnostic, configureSqliteConnection, encodeImProviderCursorId, encodeImScopeId, formatImTime, formatInbound, hasKnowledgeImageResolver, imRuntimeDomainSpec, imageMime, parseImAccountPlacementSlice, parseImDeliveryAccountSlice, resolveKnowledgeImage, resolveRouteAgentPreset, resolveSqliteConnectionSpec, senderUid };
@@ -0,0 +1,20 @@
1
+ //#region src/inline-image.ts
2
+ /** @param content - stored provider presentation, including older text-classified images. @returns an encoded raster image for attachment admission, or undefined for ordinary messages and placeholders. */
3
+ function inlineImImage(content) {
4
+ if (content.format !== "text" && content.format !== "markdown" && content.format !== "image") return void 0;
5
+ const match = /^data:(image\/(?:png|jpeg|webp|gif));base64,([A-Za-z0-9+/]+={0,2})$/u.exec(content.text.trim());
6
+ if (match === null) return void 0;
7
+ const mediaType = match[1];
8
+ if (mediaType !== "image/png" && mediaType !== "image/jpeg" && mediaType !== "image/webp" && mediaType !== "image/gif") return void 0;
9
+ return {
10
+ mediaType,
11
+ data: match[2],
12
+ name: `image.${mediaType === "image/jpeg" ? "jpg" : mediaType.slice(6)}`
13
+ };
14
+ }
15
+ /** @param content - stored message presentation. @returns text for text-only consumers, replacing inline raster bytes with an image label. */
16
+ function imMessageText(content) {
17
+ return inlineImImage(content) === void 0 ? content.text : "[image]";
18
+ }
19
+ //#endregion
20
+ export { inlineImImage as n, imMessageText as t };