# 思源规则提醒编写教程 本教程介绍思源连接插件的规则提醒:把触发条件写进思源 callout,将相应正文送入模型上下文。第 1–10 节介绍 `reminder/v1` 工具成功事件,适用于“读 TypeScript 后提醒类型约束”“读某个项目后提醒项目约定”;第 11 节介绍 `reminder/v2` 自然请求与晚间休息提醒。 快速示例和插件配置见 [README](../README.md#思源-callout-规则提醒)。 ## 目录 - [1. 规则如何生效](#1-规则如何生效) - [2. 创建第一条 TypeScript 提醒](#2-创建第一条-typescript-提醒) - [3. 字段速查](#3-字段速查) - [4. 正则和路径的写法](#4-正则和路径的写法) - [5. 更多可复制示例](#5-更多可复制示例) - [6. 条件树:组合、否定和嵌套参数](#6-条件树组合否定和嵌套参数) - [7. 优先级与互斥组](#7-优先级与互斥组) - [8. 身份、去重和正文更新](#8-身份去重和正文更新) - [9. 分析器与异步判定器](#9-分析器与异步判定器) - [10. 验证与排错](#10-验证与排错) - [11. 自然请求与晚间休息提醒](#11-自然请求与晚间休息提醒) ## 1. 规则如何生效 一条提醒由三个部分组成: 1. **专属 callout**:标记为 `[!DSH-REMINDER]`,表示这个块属于提醒服务。 2. **一个 `dsh-rule` 代码块**:声明工具、参数和投递方式。 3. **callout 的其余正文**:作为提醒内容,可以使用普通 Markdown。 运行过程是:保存笔记 → 思源连接插件发现并校验规则 → 会话中工具成功执行 → 检查工具名和参数 → 在下一次自然模型请求前投递提醒。提醒以原生 `user/message` 写入会话;规则本身不会主动启动额外对话。 使用前需要: - 思源连接插件已配置并启用目标核心,当前会话选择了该核心,或者沿用默认核心。 - 提供公共 `reminderRules` 服务的 context-care `/requests` Host 行已运行。只有思源连接插件时,思源工具仍可使用,提醒需要此服务。 - 思源核心在线,规则所在笔记本已挂载。 规则可以放在该核心的任意文档,不用在插件配置中逐个登记文档或块 ID。核心选择决定会话接受哪个来源;规则身份也包含核心,所以不要把多个核心中复制的规则理解成同一条提醒。 内部规则读取和推送连接沿用核心配置。模型主动调用思源工具,仍执行其原有逐端点审批策略。 ## 2. 创建第一条 TypeScript 提醒 ### 2.1 保存完整 callout 在思源中粘贴或导入以下 Markdown,并让它形成一个原生 callout。复制内容时去掉教程示例外层的四反引号围栏,保留所有以 `>` 开头的行。 ````markdown > [!DSH-REMINDER] TypeScript 类型约定 > > ```dsh-rule > format: reminder/v1 > id: typescript-no-any > on: tool.succeeded > when: > tool: read > arguments: > file_path: > regex: '\.ts$' > flags: i > deliver: > model: > once: surface > ``` > > 读取 TypeScript 文件后,请保持明确的类型约束,禁止使用 `any`。仅在类型暂不明确时使用 `unknown`,再通过检查缩小类型范围。 ```` 这里的含义是:`read` 工具成功执行,并且它的 `file_path` 参数以 `.ts` 结尾时,投递正文。`flags: i` 忽略扩展名大小写,因此 `.d.ts` 和 `.TS` 也会匹配,`.ts.txt` 不匹配。 正文可以放在声明代码块之前或之后;两侧正文会按顺序合并。callout 第一行的标题和 `dsh-rule` 声明本身不属于提醒正文。建议把条件放前面,提醒内容放后面,方便维护。 ### 2.2 确认思源保存的是原生 callout 保存后的块需要同时满足: | 检查项 | 期望值 | | --- | --- | | 块类型 `type` | `callout` | | 块子类型 `subtype` | `DSH-REMINDER` | | 导出的 Markdown 第一行 | `[!DSH-REMINDER]` 标记(外层引用符号由思源导出) | | 声明围栏 | 正好一个语言名为 `dsh-rule` 的三反引号代码块 | 只看到文字 `[!DSH-REMINDER]`,或者把示例放进普通引用、普通代码块,都不足以注册规则。不同思源版本、粘贴方式和编辑器扩展可能影响自定义 callout 的转换;保存后若仍是普通引用,应通过思源的 callout 编辑方式或保留自定义 subtype 的导入方式建立该块。 需要检查时,可以在思源 SQL 查询界面运行以下只读查询: ```sql SELECT id, root_id, type, subtype, markdown FROM blocks WHERE type='callout' AND subtype='DSH-REMINDER' ORDER BY id LIMIT -1; ``` 查询中应能找到刚创建的块。它也用于区分“笔记还没保存成规则块”和“规则已识别但条件没匹配”。 ### 2.3 试一次实际读取 保存后,让当前会话通过 `read` 成功读取一份存在的 `.ts` 文件。下一次自然模型请求应收到正文。保持该提醒仍在可见上下文中,再读另一份 `.ts` 文件,应抑制重复投递。 `when.tool` 和参数名必须与当前工具定义一致。此示例针对名为 `read`、参数为 `file_path` 的工具;换了工具实现,需要按实际名称和参数调整。 ## 3. 字段速查 下面是**思源笔记中的 YAML 声明**,不是其它规则编辑器的通用格式。 | 字段 | 必填 | 含义与限制 | | --- | --- | --- | | `format` | 是 | 固定为 `reminder/v1`。 | | `id` | 否 | 块内规则 ID,默认 `reminder`,1–128 个字符。建议使用稳定、可读的名称。 | | `on` | 是 | 固定为 `tool.succeeded`,仅处理成功工具结果。 | | `when.tool` | 是 | 一个精确工具名,1–256 个字符;不支持把正则或数组直接放在此字段。 | | `when.arguments` | 否 | 顶层参数名到正则配置的映射;各项同时满足才匹配。 | | `when.arguments.<参数名>.regex` | 配置该参数时必填 | JavaScript 正则,1–4096 个字符。 | | `when.arguments.<参数名>.flags` | 否 | 默认空;常用 `i`,也支持 `m`、`s`、`u`。兼容接收 `g`,但匹配时会去掉它。 | | `when.condition` | 否 | 附加条件树,与工具名及参数正则同时满足。见[条件树](#6-条件树组合否定和嵌套参数)。 | | `priority` | 否 | 整数,默认 `0`,范围 −100000 到 100000;值越大越先判定。 | | `exclusiveGroup` | 否 | 1–256 个字符的互斥组名。同一轮同组最多选中一条。 | | `judge` | 否 | 已注册判定器的引用,见[插件扩展](#9-分析器与异步判定器)。 | | `deliver.model.once` | 是 | 固定为 `surface`,按当前可见上下文去重。 | `title` 由 callout 第一行产生,`text` 由 callout 正文产生,笔记 YAML 无需填写这两个字段。即使写入,思源来源也会用外层标题、正文覆盖它们。空正文会报错;正文最多 16384 个字符,标题最多 4096 个字符。 YAML 使用 1.2,缩进使用空格;重复键、aliases 和未知规则字段会被拒绝。一块中只能有一个 `dsh-rule` 声明;写多条规则时,分别创建多个 callout。 `cooldownMinutes`、`oncePerSurface`、`actions`、`schemaVersion` 等字段不属于这里的 `reminder/v1` 声明。按分钟冷却也不是当前格式的投递选项;这里采用 `once: surface`。 ## 4. 正则和路径的写法 ### 4.1 优先用 YAML 单引号 单引号内的反斜杠会保留给正则,通常最容易阅读: ```yaml regex: '\.ts$' ``` 等价的 YAML 双引号写法是: ```yaml regex: "\\.ts$" ``` 不要写成双引号包围的 `"\.ts$"`:YAML 会先处理转义,可能直接报错。正则写法不带 JavaScript 的首尾 `/`,修饰符单独放在 `flags`。 ### 4.2 常见模式 | 目标 | YAML 单引号中的 `regex` 值 | | --- | --- | | `.ts`,包含 `.d.ts` | `'\.ts$'` | | `.ts` 或 `.tsx` | `'\.tsx?$'` | | `.js`、`.mjs` 或 `.cjs` | `'\.(?:js|mjs|cjs)$'` | | Markdown | `'\.md$'` | | 路径中有完整的 `src` 目录段 | `'(^|[\\/])src[\\/]'` | | 指定项目里的 `.js`,兼容两种分隔符 | `'^D:[\\/]dev[\\/]my-project[\\/].*\.js$'` | `[\\/]` 匹配 Windows 反斜杠或正斜杠。路径按工具的实际参数原样匹配,不会自动替换盘符、展开相对路径或把分隔符统一成 `/`。如果工具传入相对路径,应另写适合相对路径的条件。 `^` 限定开头,`$` 限定结尾;没有它们时,正则可以匹配字符串的一部分。扩展名中的点应写为 `\.`,否则 `.` 会匹配任意字符。`flags: i` 会对整个模式忽略大小写,请按项目路径习惯选择。 ### 4.3 参数缺失与参数类型 `when.arguments` 检查的是同名顶层参数。缺少被检查的参数时,这条规则正常不匹配。正则适合字符串路径、URL 等文本;数字和布尔值用 `when.condition` 的比较节点,对象中的嵌套字段也用条件树。 ## 5. 更多可复制示例 以下完整示例与第一条规则一样,分别保存为原生 callout。 ### 5.1 读取某个项目的 JavaScript 后提醒 把 `my-project` 换成真实项目目录。 ````markdown > [!DSH-REMINDER] 项目 JavaScript 约定 > > ```dsh-rule > format: reminder/v1 > id: project-js-conventions > on: tool.succeeded > when: > tool: read > arguments: > file_path: > regex: '^D:[\\/]dev[\\/]my-project[\\/].*\.js$' > flags: i > deliver: > model: > once: surface > ``` > > 修改这个项目时沿用现有模块边界和命名方式,并为不直观的逻辑补充注释。 ```` ### 5.2 读取 Markdown 后提醒文档写法 ````markdown > [!DSH-REMINDER] Markdown 文档约定 > > ```dsh-rule > format: reminder/v1 > id: markdown-writing > on: tool.succeeded > when: > tool: read > arguments: > file_path: > regex: '\.md$' > flags: i > deliver: > model: > once: surface > ``` > > 编辑文档时保留有效链接,示例要能直接复制使用,并说明必要的前置条件。 ```` ### 5.3 访问指定站点后提醒 这是一个**待适配模板**:先从当前浏览器工具定义中取得真实工具名和 URL 参数名,再替换 `YOUR_NAVIGATE_TOOL`、`YOUR_URL_ARGUMENT`。如果 URL 位于对象内部,改用下一节的嵌套字段条件。 ````markdown > [!DSH-REMINDER] 文档站点阅读约定 > > ```dsh-rule > format: reminder/v1 > id: docs-site-reading > on: tool.succeeded > when: > tool: YOUR_NAVIGATE_TOOL > arguments: > YOUR_URL_ARGUMENT: > regex: '^https://docs\.example\.com(?:[/?#]|$)' > flags: i > deliver: > model: > once: surface > ``` > > 使用该站点资料时记录对应页面链接,并区分文档规定和自己的推断。 ```` 该模板匹配工具提交的 URL 参数,不会从结果中推断浏览器重定向后的最终地址。`docs.example.com` 是示例域名,也需要替换。 ## 6. 条件树:组合、否定和嵌套参数 `when.condition` 追加一个条件树。工具名检查、`when.arguments` 中的全部正则、这棵条件树之间是“并且”。 ### 6.1 读取 TS/TSX,但排除声明文件 ````markdown > [!DSH-REMINDER] TypeScript 实现文件约定 > > ```dsh-rule > format: reminder/v1 > id: typescript-implementation > on: tool.succeeded > when: > tool: read > arguments: > file_path: > regex: '\.tsx?$' > flags: i > condition: > kind: not > condition: > kind: regex > field: tool.arguments.file_path > pattern: '\.d\.ts$' > flags: i > deliver: > model: > once: surface > ``` > > 修改实现文件时保持明确的类型约束,新增行为后运行与改动相关的检查。 ```` 这里 `when.arguments` 已保证 `file_path` 存在;附加条件使用 `not` 排除 `.d.ts`。 ### 6.2 节点速查 | `kind` | 所需内容 | 含义 | | --- | --- | --- | | `all` | `conditions: [...]` | 子条件全部满足。 | | `any` | `conditions: [...]` | 至少一个子条件满足。 | | `not` | `condition: {...}` | 反转一个子条件。 | | `compare` | `field`、`op`,通常还有 `value` | 比较字段。 | | `regex` | `field`、`pattern`,可选 `flags` | 对文本字段执行正则。注意这里叫 `pattern`。 | | `keywords` | `field`、`values`,可选 `mode`、`caseSensitive`、`wordBoundary` | 检查文本关键词。 | | `always` | 无额外字段 | 始终成立。 | `all`、`any` 的 `conditions` 不能为空,每组最多 64 项;条件树有深度限制,提醒编译还会包一层 `all`,保持树结构简洁。条件树的正则修饰符为 `i`、`m`、`s`、`u`。在提醒中使用 `judge` 引用异步判定器;不要将引擎的根级 `detector` 节点塞进附加条件。 `compare.op` 支持:`eq`、`ne`、`gt`、`gte`、`lt`、`lte`、`contains`、`exists`。`exists` 无需 `value`;大小比较要求数值;`contains` 用于字符串或数组。关键词的 `mode` 支持 `ANY`、`ALL`、`NOT-ANY`、`NOT-ALL`,默认 `ANY`。 字段路径使用点号,如 `tool.arguments.body.path`。当前可读取 `tool.name`、`tool.arguments`、`tool.sessionId`、`tool.callId`、`tool.rootCallId`,以及 `facts` 和有界 `context`。当前事件不提供工具返回正文,不能把 `tool.result` 当成已存在字段。 ### 6.3 “src 或 lib”组合条件 下面是替换整个 `when` 的片段。它保留第一条规则的 `file_path` 正则,因此进入附加条件时路径已存在: ```yaml when: tool: read arguments: file_path: regex: '\.ts$' flags: i condition: kind: any conditions: - kind: regex field: tool.arguments.file_path pattern: '(^|[\\/])src[\\/]' - kind: regex field: tool.arguments.file_path pattern: '(^|[\\/])lib[\\/]' ``` 不要把第二个 `when` 追加到原 YAML 中;用这个完整的 `when` 替换原字段,保留其它顶层字段。 ### 6.4 可选嵌套参数先检查存在 以下也是替换 `when` 的片段。把工具名换为真实工具名;假设它有可选参数 `body.path`: ```yaml when: tool: YOUR_TOOL condition: kind: all conditions: - kind: compare field: tool.arguments.body.path op: exists - kind: regex field: tool.arguments.body.path pattern: '\.md$' flags: i ``` `all` 会按顺序短路。先检查存在,缺少字段时正常不匹配;直接对不存在的字段执行正则或比较可能产生判定诊断。 ## 7. 优先级与互斥组 没有互斥组时,同一个工具事件可以命中多条规则,各自按照身份去重。单纯提高 `priority` 不会排除其它规则。 有一条通用 TS 规则,同时还有某项目专用规则时,可以: 1. 给两条规则都增加 `exclusiveGroup: typescript-style`。 2. 通用规则设置 `priority: 0`,保留 `'\.ts$'`。 3. 项目规则设置 `priority: 100`,路径收紧为 `'^D:[\\/]dev[\\/]my-project[\\/].*\.ts$'`。 这两个字段都放在顶层,例如紧接 `on`: ```yaml priority: 100 exclusiveGroup: typescript-style ``` 项目内路径会先匹配项目规则,项目外可以继续匹配通用规则。优先级相同则按来源 ID、规则 ID 稳定排序;需要明确先后时,请填写不同优先级。 互斥组作用于同一轮判定,也可能跨来源,因此组名应表达共享意图。高优先级规则**正常不匹配**时,可以继续检查同组下一条;它若失败或超时,本轮同组后续规则会被阻止。同组赢家已在上下文中而被去重时,也不要期待低优先级规则自动作为补充提醒;需要同时投递的规则应使用不同组或不设组。 ## 8. 身份、去重和正文更新 ### 8.1 `surface` 是当前可见上下文 `deliver.model.once: surface` 表示:同一规则的提醒仍处于模型可见上下文时,不重复投递。 | 操作 | 结果 | | --- | --- | | 再读一份匹配文件、追加普通消息 | 已有提醒仍可见时,抑制重复。 | | 修改 callout 标题、正文或触发条件 | 更新来源;身份不变,已有可见提醒继续抑制重复。 | | 把块移动到同核心的其它文档 | 块 ID 不变,身份不变。 | | 修改 YAML `id`、复制成新块、复制到其它核心 | 身份改变,作为另一条规则处理。 | | fork 继承包含该提醒的上下文 | 继承的提醒仍参与去重。 | | 压缩移除该提醒后,又有匹配工具事件 | 可重新投递。仅等待或仅压缩不会主动触发。 | 思源来源身份包含核心、块 ID 和局部规则 ID。建议长期保持 `id` 不变;省略它也可以,此时局部 ID 为 `reminder`。改名或复制规则应出于规则管理需要,不要为了试验而反复制造新身份。 ### 8.2 保存后的动态刷新 连接建立、重连时会全量校准;平时通过 WebSocket 通知刷新相关文档。新增、编辑、删除规则,以及已知规则文档内的正文编辑,会进入刷新流程。断连时撤销来源,重连后恢复校准。 `reminders.refreshMs` 默认 30 秒,用于失败读取和超容量候选重试,空闲时不会每 30 秒扫描 SQL。更新遇到总量上限时,来源会保留上一批已接受规则;提高容量后会重试缓存候选。 ### 8.3 动态内容的两种含义 **改笔记正文**会更新后续可投递内容,但不会改写已经发送的提醒,也不会重置当前可见上下文的去重。 **按本次工具参数生成不同文本**需要插件扩展。当前提醒正文按原文投递,`{{tool.arguments.file_path}}`、`${...}` 或正则捕获组不会自动插值。分析器和判定器返回的 `facts` 供匹配和诊断使用,不会自动拼进正文。 需要实时生成正文时,由来源插件计算新的 `text`,通过 `registerSource` 返回句柄的 `replace` 提交规则快照,并维护递增 `revision`;同一身份仍遵守 `surface` 去重。若需要每次事件都发送不同消息,应设计对应的事件投递能力,不能仅靠替换同一条规则的文本实现。 ## 9. 分析器与异步判定器 普通路径规则只用前面的 YAML 即可。需要“识别文件语言”“按结构化项目事实判断”等能力时,可以由已安装插件注册分析器或判定器。 ### 9.1 分析器产生 `facts` 分析器模块导出 `analyze({ event, context })`,可返回 Promise,结果是 JSON 对象。例如: ```js export async function analyze({ event }) { const path = event.arguments.file_path; return { language: typeof path === 'string' && /\.tsx?$/i.test(path) ? 'ts' : 'other', }; } ``` 插件在注入 `reminderRules` 后,使用服务实例注册本地 ESM 模块: ```js const disposeAnalyzer = service.registerAnalyzer({ ref: 'typescript', revision: 1, module: new URL('./typescript-analysis.js', import.meta.url).href, }); // 插件卸载时等待 disposeAnalyzer() 完成。 ``` 这里 `service` 指已注入的 `reminderRules` 实例,模块需要由插件实际提供。默认导出名为 `analyze`,可用 `exportName` 指定其它具名导出。 每轮判定中,每个分析器执行一次,各自读取相同基础输入,结果汇总在 `facts.`。注册上例后,规则可以把 `when` 替换为: ```yaml when: tool: read condition: kind: all conditions: - kind: compare field: facts.typescript.language op: exists - kind: compare field: facts.typescript.language op: eq value: ts ``` 没有注册该分析器,或分析失败没有产出对应事实时,这个条件正常不匹配。笔记中的 `ref` 需要与插件注册名称一致;分析器版本由插件注册管理。 ### 9.2 异步判定器返回是否命中 当基本条件满足后,还可以调用异步判定器。笔记中增加以下顶层片段: ```yaml judge: ref: project-policy revision: 1 params: project: my-project timeoutMs: 100 ``` 插件提供一个默认名为 `judge` 的导出函数,调用参数为 `{ event, params }`,返回 JSON `{ ok: boolean, facts?: ... }`,也可以返回 Promise。通过下列方式注册: ```js const disposeJudge = service.registerDetector({ ref: 'project-policy', revision: 1, module: new URL('./project-policy.js', import.meta.url).href, }); // 插件卸载时等待 disposeJudge() 完成。 ``` `ref` 与 `revision` 都必须精确对应已注册判定器。`params` 是可选 JSON 值,`timeoutMs` 是可选的 1–60000 毫秒整数;它只能进一步收紧服务的单条执行预算,不能放大整体预算。 默认单条预算为 250 毫秒、整轮预算为 1000 毫秒。规则正则、分析器和异步判定在可终止的真实 Worker 中执行。传入上下文是有界采样:默认最近 8 条消息、最多 32768 个文本字符,`context.truncated` 表示是否截断;不要假定它是完整会话或任意历史检索结果。 笔记保存条件和引用;模块代码由插件交付,不能在 `dsh-rule` 中放 JavaScript 来注册执行。Worker 是可终止执行单元,插件模块仍需遵守宿主能力边界。 ## 10. 验证与排错 ### 10.1 建议的验证顺序 1. **识别**:确认思源保存了 `callout` / `DSH-REMINDER` 块,且只有一个 `dsh-rule` 围栏。 2. **注册**:在“会话规则”查看思源来源与规则;“提醒容量”可查看总注册数、上限和最近拒绝来源。 3. **正例**:执行一次成功且参数匹配的工具调用,检查下一次自然模型请求的提醒正文。 4. **反例**:换用不匹配的参数,确认不投递。用尚未可见过该规则提醒的会话或上下文测试,避免去重掩盖匹配结果。 5. **去重**:保持提醒可见,再执行匹配调用,检查不重复投递。 6. **修改**:改正文保存并确认来源刷新。已有提醒不会被改写;新身份或提醒已移除后的新事件,才适合观察更新后的投递。 提醒用于给模型提供约定。涉及代码行为的要求,仍应通过代码审查、类型检查或相关测试验证。 ### 10.2 常见问题 | 现象 | 检查方向 | | --- | --- | | 查询不到规则块 | 是否只有普通引用或代码块;笔记本是否挂载;自定义 subtype 是否保存正确。 | | 有块,规则列表却没有它 | `reminderRules` 服务是否运行;当前会话核心是否对应;查看插件日志里的块 ID 和解析诊断。 | | 提示“必须有且只有一个代码块” | 一个 callout 中只保留一个 `dsh-rule`,检查语言名大小写和三反引号闭合。 | | YAML 或字段报错 | 检查缩进、重复键、双引号转义、`format`,以及是否误用了其它规则系统字段。 | | 规则已注册但不触发 | 工具是否成功;工具名是否精确一致;参数名、实际路径分隔符、正则和附加条件是否匹配。 | | 更新正文后仍看见旧提醒 | 已发送消息保持原文;同一身份的可见提醒继续去重。另查来源是否被容量上限拒绝更新。 | | 多条规则同时提醒 | `priority` 只决定顺序;需要择一时设置相同 `exclusiveGroup`。 | | 同组备用规则没有执行 | 高优先级规则可能已命中,或失败、超时阻止了本轮同组后续判定。 | | 插件扩展规则不触发 | 检查分析器事实是否存在,判定器 `ref` / `revision`、模块导出和执行预算是否对应。 | | 断线后不再提醒 | 来源离线会撤销;恢复连接后应全量校准,检查核心连接与插件日志。 | | 新增规则因容量被拒 | 在“提醒容量”调整上限,0 表示不限;保存后等待下一次候选重试。 | 单个坏块会产生自己的诊断,其他有效规则继续注册。排错时先沿“块识别 → 来源注册 → 事件匹配 → 上下文去重”检查,可以更快找到具体断点。 ## 11. 自然请求与晚间休息提醒 `reminder/v2` 使用 `on: request.before`,在下一次自然模型请求前检查条件,纯聊天也适用。使用前,思源插件与 context-care 提醒服务都需要支持 v2;原有工具规则继续使用 v1。下面的示例在运行 DSH 的本机时间 23:00–次日 06:00,且最近 5 分钟有真人输入时,每个会话每晚最多提醒一次。 ````markdown > [!DSH-REMINDER] 晚间休息 > > ```dsh-rule > format: reminder/v2 > id: evening-rest > on: request.before > when: > timeWindow: > start: '23:00' > end: '06:00' > timeZone: local > humanActivity: > withinSeconds: 300 > deliver: > model: > once: window > ``` > > 现在已进入本机晚间休息时段,近期有真人输入,人类伙伴很可能还在参与对话。晚睡可能影响健康,请结合当前任务判断是否紧急:没有紧急任务时,温和建议伙伴早点睡;有紧急任务时尊重当前安排,不催促、不强制中断工作。尊重伙伴的选择。 > > 如果伙伴表示“今晚不再提醒”,请调用 reminder_snooze,暂停当前会话的窗口提醒,窗口结束后自动恢复。 ```` ### 11.1 时间与人在场条件 | 字段 | 含义 | | --- | --- | | `when.timeWindow.start` / `end` | `HH:mm`,开始包含、结束不包含。开始大于结束时跨午夜,两者不能相同。 | | `when.timeWindow.timeZone` | 当前支持 `local`,默认也是 `local`,表示 Host 本机时区,不取浏览器时区。 | | `when.humanActivity.withinSeconds` | 1–86400 的整数,距最近真人输入的最大秒数;示例的 300 秒可以修改。 | | `when.condition` | 可选条件树,沿用第 6 节的写法;请求上下文与分析器事实分别在 `context`、`facts` 下。 | | `deliver.model.once` | `window` 按时间窗口持久去重,要求声明 `timeWindow`;`surface` 沿用可见上下文去重。 | v2 的 `when` 不接受 `tool` 和 `arguments`。时间条件、人在场条件及附加条件同时满足才匹配;省略某个可选条件表示不限制它。`id`、`priority`、`exclusiveGroup`、`judge` 和正文规则沿用前文。 人在场依据来自宿主持久记录中的真人来源标记和首次入队时间。消息排队、请求重试及历史再次进入上下文都不会刷新这个时间。插件通知、定时续跑、来源未知的消息和 fork 继承历史不算当前会话的新真人活动;当前实现也不把审批操作作为在场证据。只有近期输入是一个在场线索,不代表已经确认伙伴一直看着屏幕。 分析器及判定器还可读取 `event.clock.at`(Unix 毫秒)、`event.clock.timeZone`(Host 时区)以及 `event.humanActivity`(`{ at, evidence: 'human-message' }` 或 `null`)。这些时间与在场条件由运行时先行检查,分析器无法用返回事实覆盖它们。投递前会再次检查条件,过期候选不会延迟送到白天。 ### 11.2 按夜去重与今晚暂缓 跨午夜窗口归属开始日期,例如 10 月 14 日 23:00 与 10 月 15 日 01:00 属于同一晚。窗口去重以当前会话、来源和规则身份为范围;不同会话分别计算。保持规则身份时,正文编辑、上下文压缩、候选清理与重启都不会让同一晚再提醒;下一晚可以重新提醒,即使上一晚正文仍在上下文中。 对伙伴自然表达的“今晚不再提醒”,模型可调用 `reminder_snooze`,参数为 `{}` 时暂停当前会话所有正在生效的窗口提醒。只暂停指定规则时,同时提供 `sourceId` 与 `ruleId`,可从会话规则目录或提醒详情取得。暂缓结果写入持久状态,到窗口结束自动恢复;未进入窗口时,工具会报告当前没有可暂停的提醒。 规则只负责把建议送给模型。紧急程度由模型结合对话判断,规则不做关键词式紧急判定,也不创建定时对话或强制停止任务。验证时可把窗口临时改为涵盖当前本机时间,发送一条真人消息,随后检查提醒、去重和暂缓结果,再恢复计划中的晚间时间。 ## 12. 指定接收会话与共享次数 以下扩展需要 context-care 0.11.0 与思源插件 0.6.0。原有 v1/v2 声明继续可用。在规则顶层声明 `scope`,可限制接收会话、预设,或同时限制两者: ```yaml scope: sessionIds: [session-a, session-b] agentPresets: [writing] ``` 请把示例值替换为实际 ID。每个列表内部满足任意一个即可;同时写两种列表时,接收者必须同时符合会话与预设条件。省略某个维度表示不限,空列表会被拒绝。预设身份来自 Host 保存的会话头;规则来源自身的范围、思源核心选择和规则范围也必须同时满足。 `once` 管当前会话去重,`shared` 管同一来源、同一规则在多个会话间的总额度。例如让符合范围的会话合计只提醒两次: ```yaml deliver: model: once: surface shared: reset: lifetime lifetime: project-launch-1 limit: 2 ``` `limit: 1` 就是多个会话合计一次。`lifetime` 是明确命名的活动周期;正文编辑、规则 revision 更新、上下文压缩和 Host 重启都保留已消费次数。开始新活动时更换 `lifetime`。删除再创建思源块会改变规则身份,因此应编辑原块来保留同一规则的消费记录。 晚间规则可把第 11 节的 `deliver` 换成下面一段,让多个会话每晚共享一次: ```yaml deliver: model: once: window shared: reset: window limit: 1 ``` 窗口共享要求规则含 `when.timeWindow`,适用于 v2 和 v3。跨午夜属于开始日期的窗口,下一窗口恢复额度。检测命中不消费次数;服务在交付前持久预留额度,再保存会话候选,提交后记为消费。持久失败可释放未提交预留,已经消费的额度不会被迟到的回滚释放。共享范围是同一个 Host 的提醒服务与存储领域;分别启动的 Host 进程不共享原子额度。 ## 13. 普通文本与可读推理触发 `reminder/v3` 在输出过程中检查供应商实际提供的普通文本或可读 reasoning 文本。下面的规则命中后,在下一次自然请求前提醒: ````markdown > [!DSH-REMINDER] 核对完成证据 > > ```dsh-rule > format: reminder/v3 > id: check-completion > on: assistant.output > when: > blockTypes: [text, reasoning] > pattern: > kind: literal > text: 已全部完成 > deliver: > model: > once: surface > ``` > > 请核对任务的验收条件和实际结果,再报告完成情况。 ```` `blockTypes` 可选 `text`、`reasoning`,至少填写一种。供应商未暴露的推理内容无法匹配。匹配以单个输出块为单位,分片会累积;不同块、不同类型、重试 attempt 之间不会拼接成一个命中。 三种 `pattern` 写法如下,匹配均区分大小写;正则可用 `flags: i` 忽略大小写: ```yaml # 完整字面片段 pattern: kind: literal text: 已全部完成 # 同一块中出现任意关键词;mode: all 则要求全部出现 pattern: kind: keywords values: [已完成, 全部通过] mode: any # 正则表达式 pattern: kind: regex regex: '已经?全部完成' flags: u ``` 普通命中只保存待交付提醒,当前输出正常继续,也不会主动唤醒空闲会话。若规则确实需要中止当前输出并立即续接,在 `deliver.model` 下显式加入: ```yaml interrupt: abort-resume ``` 服务会先完成范围、额度和持久化检查,再取消当前 attempt 并通过 Host 收件箱用同一条提醒续接。每次真人轮次默认最多中止一次;`/requests` 行的 `reminders.maxInterruptsPerTurn: 0` 可关闭中止,此时命中保留到下一自然请求。规则处理与模型流并行,命中被处理时该 attempt 可能已自然结束,这种情况也留到下一请求。 输出检测受容量限制:默认每块使用最近 32768 字符的滑动窗口、每 attempt 最多 64 块,待处理队列最多 32 帧或 4 MiB。窗口向前移动时继续检测;块数或队列超限时记录诊断并停止该 attempt 的后续检测,模型输出继续。模式所需跨度应小于保留窗口。`scope`、`shared` 和时间窗口的写法与第 12 节相同。 ## 14. 只在工具超时后提醒 v1 的 `tool.failed` 必须指定结构化错误码。Host 的工具超时策略将自己的超时标记为 `TOOL_TIMEOUT`;下面只在 `pwsh` 被该策略判定为超时后提醒: ````markdown > [!DSH-REMINDER] 工具超时后核对 > > ```dsh-rule > format: reminder/v1 > id: shell-timeout > on: tool.failed > when: > tool: pwsh > error: > code: TOOL_TIMEOUT > deliver: > model: > once: surface > ``` > > 这次工具调用已超时。请核对工具结果和已产生的副作用,再决定是否重试。 ```` `when.tool` 使用真实工具名,也可加 `when.arguments` 正则缩小参数范围。工具成功、普通失败、主动取消以及文本中单独出现“timeout”或 `TOOL_TIMEOUT` 都不会触发本例。提醒在工具结算后的下一次请求前交付,沿用表层去重与共享额度。 这个错误码属于 Host 工具超时策略。工具自己返回的业务超时、网络超时,或 shell 转为后台任务,只有最终失败结果确实携带匹配错误码时才命中;它们不一定是 `TOOL_TIMEOUT`。针对其他结构化错误可声明相应的 `when.error.code`。成功规则继续用 `tool.succeeded`,不能同时声明 `when.error`。