dsh-chat-tide 0.0.0-stage → 0.2.2

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 (111) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/CONTRIBUTING.md +60 -0
  3. package/LICENSE +21 -0
  4. package/README.en.md +68 -0
  5. package/README.md +67 -2
  6. package/RELEASE.md +55 -0
  7. package/assets/icon.png +0 -0
  8. package/cordis.patch.yml +35 -0
  9. package/lib/ambient.js +44 -0
  10. package/lib/api.js +199 -0
  11. package/lib/attention.js +191 -0
  12. package/lib/budget.js +37 -0
  13. package/lib/circuit.js +42 -0
  14. package/lib/client.js +543 -0
  15. package/lib/config.js +139 -0
  16. package/lib/conflict.js +44 -0
  17. package/lib/const.js +24 -0
  18. package/lib/context.js +232 -0
  19. package/lib/db.js +325 -0
  20. package/lib/dedupe.js +29 -0
  21. package/lib/egress.js +84 -0
  22. package/lib/embed-child.mjs +41 -0
  23. package/lib/embed-worker.mjs +35 -0
  24. package/lib/embed.js +309 -0
  25. package/lib/events.js +82 -0
  26. package/lib/exemplar.js +48 -0
  27. package/lib/extract.js +183 -0
  28. package/lib/facts.js +311 -0
  29. package/lib/fts.js +97 -0
  30. package/lib/images.js +57 -0
  31. package/lib/index.js +1074 -0
  32. package/lib/inject.js +107 -0
  33. package/lib/janitor.js +137 -0
  34. package/lib/llmroute.js +43 -0
  35. package/lib/merge.js +54 -0
  36. package/lib/people.js +55 -0
  37. package/lib/prefilter.js +74 -0
  38. package/lib/qqtext.js +42 -0
  39. package/lib/repair.js +119 -0
  40. package/lib/route-decision.js +303 -0
  41. package/lib/route.js +42 -0
  42. package/lib/schema.js +134 -0
  43. package/lib/scope.js +121 -0
  44. package/lib/scrub.js +23 -0
  45. package/lib/sessions.js +69 -0
  46. package/lib/settings.js +135 -0
  47. package/lib/silence.js +92 -0
  48. package/lib/status-page.js +263 -0
  49. package/lib/topics.js +72 -0
  50. package/lib/vec.js +63 -0
  51. package/package.json +92 -4
  52. package/presets/README.md +38 -0
  53. package/presets/attention/example.json +10 -0
  54. package/presets/config/profiles.json +32 -0
  55. package/presets/private.example.json +18 -0
  56. package/presets/rules/generic.json +79 -0
  57. package/presets/rules.schema.json +32 -0
  58. package/probes/01-sqlite-vec.mjs +70 -0
  59. package/probes/02-fts-bigram.mjs +68 -0
  60. package/probes/03-embedding.mjs +73 -0
  61. package/probes/04-storage.mjs +118 -0
  62. package/probes/05-facts.mjs +113 -0
  63. package/probes/06-events-egress.mjs +72 -0
  64. package/probes/07-extract.mjs +66 -0
  65. package/probes/08-context-janitor.mjs +80 -0
  66. package/probes/09-route-silence.mjs +75 -0
  67. package/probes/10-vector-pipeline.mjs +77 -0
  68. package/probes/11-llm-fallback.mjs +28 -0
  69. package/probes/12-exemplar-budget-merge.mjs +76 -0
  70. package/probes/13-circuit.mjs +45 -0
  71. package/probes/14-embed-worker.mjs +41 -0
  72. package/probes/15-backfill.mjs +57 -0
  73. package/probes/16-scope-preset.mjs +36 -0
  74. package/probes/17-prefilter.mjs +33 -0
  75. package/probes/18-qqtext.mjs +33 -0
  76. package/probes/19-repair.mjs +47 -0
  77. package/probes/20-context-message.mjs +49 -0
  78. package/probes/21-status-page.mjs +75 -0
  79. package/probes/22-retention.mjs +45 -0
  80. package/probes/23-ambient-probability.mjs +60 -0
  81. package/probes/24-inject-delta.mjs +50 -0
  82. package/probes/25-route-decision.mjs +122 -0
  83. package/probes/26-attention.mjs +52 -0
  84. package/probes/27-people-alias.mjs +42 -0
  85. package/probes/28-wake-gates.mjs +88 -0
  86. package/probes/29-scope-modes.mjs +53 -0
  87. package/probes/30-attention-vector.mjs +84 -0
  88. package/probes/31-group-speakers.mjs +58 -0
  89. package/probes/32-image-cache.mjs +54 -0
  90. package/probes/33-topic-growth.mjs +60 -0
  91. package/probes/34-integrity.mjs +80 -0
  92. package/probes/35-privacy.mjs +52 -0
  93. package/probes/36-inject-scopes.mjs +62 -0
  94. package/probes/37-inject-e2e.mjs +92 -0
  95. package/probes/38-review-fixes.mjs +74 -0
  96. package/probes/39-image-only.mjs +31 -0
  97. package/probes/40-scope-follow.mjs +40 -0
  98. package/probes/41-attention-label.mjs +35 -0
  99. package/scripts/attention-sim.mjs +54 -0
  100. package/scripts/check-dead-config.mjs +27 -0
  101. package/scripts/check-docs.mjs +71 -0
  102. package/scripts/check-naming.mjs +48 -0
  103. package/scripts/curate-report.mjs +228 -0
  104. package/scripts/doctor.mjs +110 -0
  105. package/scripts/export-private-rules.mjs +35 -0
  106. package/scripts/make-icon.mjs +114 -0
  107. package/scripts/metrics.mjs +94 -0
  108. package/scripts/pack.mjs +36 -0
  109. package/scripts/recall-check.mjs +56 -0
  110. package/scripts/run-checks.mjs +45 -0
  111. package/scripts/seed.mjs +82 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,64 @@
1
+ # Changelog
2
+
3
+ ## 0.2.2
4
+
5
+ ### Fixed
6
+
7
+ - The session picker rendered once per settings group instead of once, because
8
+ its condition was global. It now renders only in the scope group.
9
+
10
+ ### Changed
11
+
12
+ - Scope is the first section on the settings page, before the switches.
13
+
14
+ ## 0.2.1
15
+
16
+ ### Fixed
17
+
18
+ - The published package now includes CONTRIBUTING.md and RELEASE.md, so the
19
+ advertised `npm test` runs after install instead of failing on missing docs.
20
+ - Probe 28 skips its wake assertion when sqlite-vec's platform package is
21
+ absent, instead of reporting a failure for an environment it cannot control.
22
+
23
+ ### Changed
24
+
25
+ - README states the installation weight honestly: about 300MB of dependencies
26
+ (onnxruntime and friends) before the model itself is downloaded.
27
+
28
+ ## 0.2.0
29
+
30
+ ### Added
31
+
32
+ - Scope is selectable on the plugin page: follow dsh-qqbot (default), a specific
33
+ workspace, specific sessions picked from a list, or off.
34
+ - `/tide/api/sessions` lists recent sessions (title, workspace, time) for that picker.
35
+ - Attention labels are normalised on write: leading subjects and instruction
36
+ clauses are stripped, descriptions are kept, nothing is truncated.
37
+ - `scripts/attention-doctor.mjs` reports labels needing rework, missing vectors,
38
+ orphan vectors and duplicate labels; `--fix` applies the repairs.
39
+ - CI runs every probe plus the naming, dead-config and docs checks on Node 22 and 24.
40
+
41
+ ### Changed
42
+
43
+ - Waking only screens group chats; private and local sessions pass through.
44
+ - Routing scores the current human message alone instead of every block in the
45
+ step payload, which used to drag one-word messages across the threshold.
46
+ - Attention thresholds recalibrated from measurements: 0.48 hit, 0.03 gap.
47
+ - Extraction is told who is speaking, so a group member's words are no longer
48
+ written as the owner's facts.
49
+ - The embedding model stays bge-small-zh; bge-base measured 0.01 AUC better at
50
+ three times the latency.
51
+
52
+ ### Fixed
53
+
54
+ - Image-only messages are rejected instead of merely logged, and QQ's `[图片]`
55
+ placeholder no longer counts as text.
56
+ - `routing.uncertainPolicy` now decides the action instead of only naming it.
57
+ - Re-extraction records the source scope instead of leaving facts as `unknown`.
58
+ - The status route registers through `ctx.inject(['webServer'])`, so a restart or
59
+ a web-service rebuild no longer leaves it 404.
60
+ - The package ships `probes/`, so the advertised `npm test` works after install.
61
+
62
+ ## 0.1.1
63
+
64
+ First repository snapshot.
@@ -0,0 +1,60 @@
1
+ # Contributing
2
+
3
+ Thanks for looking. Tide is small on purpose — please keep it that way.
4
+
5
+ ## Shape of the codebase
6
+
7
+ - `lib/` — the engine. Host half (`index.js`) plus the browser half (`client.js`) that renders the
8
+ plugin's configuration page. `db.js` owns the schema; every migration is append-only.
9
+ `const.js` is the single source for the row id, route, data directory, log names and style prefix.
10
+ - `presets/` — content, not code: rules, attention seeds, recommended configs. Generic presets ship;
11
+ `presets/private.json` never does.
12
+ - `probes/` — numbered regression suites. Each one builds its own throwaway database and asserts
13
+ behaviour. They are the test suite; there is no separate framework.
14
+ - `scripts/` — operator tooling: packaging, seeding, health, metrics and consistency checks.
15
+
16
+ ## Before opening a pull request
17
+
18
+ ```bash
19
+ npm install
20
+ npm test # 等价于 node scripts/run-checks.mjs --skip-model
21
+ # 想连模型探针一起跑:node scripts/run-checks.mjs
22
+ ```
23
+
24
+ All suites and the three consistency checks must pass. `check-docs` fails when the README drifts
25
+ from the code; `check-dead-config` fails when a declared configuration key is never read.
26
+
27
+ If you change the schema, add a migration to `lib/db.js` and a probe that covers it — do not edit
28
+ an existing migration, since someone's database may already be past it.
29
+
30
+ ## Behaviour rules for contributions
31
+
32
+ - Memory is data, never instructions. Anything injected into a prompt must be fenced as data.
33
+ - Prefer deciding with local signals (FTS, vectors) before spending a model call.
34
+ - Every deletion is soft, auditable and explainable. Never hard-delete user content.
35
+ - Anything that leaves the machine must be visible in the configuration. No silent network calls.
36
+ - No personal identifiers in code, tests, docs, or presets. Use placeholders and environment
37
+ variables (`CM_OWNER`, `CM_PEER`).
38
+
39
+ ## Commit style
40
+
41
+ One change per commit, imperative summary, body explains the why rather than restating the diff.
42
+
43
+ ## Local install
44
+
45
+ Data lives in `~/.dsh/tide/memory.db`; the read-only page is `/tide` on the host port.
46
+
47
+ When linking this repository into a profile for development, add `dsh-chat-tide` to the profile's `dsh.profile.bundles`. Without that entry the package is installed but never loaded, and the plugin page shows nothing.
48
+
49
+ ## Tooling
50
+
51
+ | Command | Purpose |
52
+ |---|---|
53
+ | `npm test` | every probe plus the three consistency checks |
54
+ | `node scripts/doctor.mjs` | read-only health check of an existing database |
55
+ | `node scripts/metrics.mjs` | data, routing, injection and recent episodes snapshot |
56
+ | `node scripts/curate-report.mjs` | memory quality: duplicates, fragments, expiry, scope violations |
57
+ | `node scripts/check-naming.mjs` | route, directory, log and style consistency |
58
+ | `node scripts/check-dead-config.mjs` | every declared configuration key is actually read |
59
+ | `node scripts/check-docs.mjs` | the README matches the code |
60
+ | `node scripts/seed.mjs` | import presets (idempotent) |
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 FakerKonzo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.en.md ADDED
@@ -0,0 +1,68 @@
1
+ # Tide — chat memory with selective waking
2
+
3
+ > A chat-memory plugin for DSH: conversations settle into facts and episodes, and the agent speaks only when it should.
4
+ > 中文:[README.md](./README.md)
5
+
6
+ ## The problem
7
+
8
+ Chat agents usually fail in one of two ways: they forget, or they answer everything.
9
+
10
+ Tide lets the agent remember what people said and speak only when it should — group chatter is not answered line by line, but when a topic it cares about comes up, it joins in. What it remembers can be browsed, edited, and deleted with a record.
11
+
12
+ ## Compatibility
13
+
14
+ | Item | Requirement |
15
+ |---|---|
16
+ | DSH | `>=0.2.2-rc.1 <0.3.0`, tested on 0.2.2-rc.1 and 0.2.2-rc.3 |
17
+ | Node | `>=22.5` (uses the built-in `node:sqlite`) |
18
+ | QQ adapter | Optional, tested with `@tencent-connect/dsh-qqbot@0.5.5`; without it memory still works for local sessions, group waking just never fires |
19
+
20
+ Everything else is installed with the package. The first run downloads a local embedding model of about 25–30MB.
21
+
22
+ ## How it works
23
+
24
+ 1. **Remember first**: incoming chat is recorded; nothing answers anyone at this stage.
25
+ 2. **Then decide**: for a group message it checks whether it is being addressed, then whether the topic is something it cares about. If so it speaks; otherwise it rolls a small probability and the model decides whether to join.
26
+ 3. **Speak with memory**: only when it does speak are the relevant rules, facts and recent episodes brought into the turn. The rest of the time they stay out of the context.
27
+ 4. **Settle afterwards**: at the end of a turn, facts, episodes and interests worth keeping are extracted.
28
+ 5. **Clean up over time**: duplicates merge, stale entries retire, one-off claims get corrected.
29
+
30
+ Memory is kept per person: in a group, what each member said stays under their name. What was said in a private chat does not appear in groups by default.
31
+
32
+ ## Install
33
+
34
+ Package `dsh-chat-tide`, Loader row id `tide`.
35
+
36
+ 1. Install `dsh-chat-tide` with the DSH plugin manager (or `link:` a local checkout while developing).
37
+ 2. Add `dsh-chat-tide` to `dsh.profile.bundles` in the profile's `package.json`. Skipping this installs the package without loading it.
38
+ 3. Open the plugin page and confirm which conversations it should manage under "Scope".
39
+ 4. On first run keep the waking route off, confirm ingestion and injection work, then enable routing.
40
+
41
+ Data lives in `~/.dsh/tide/memory.db`.
42
+
43
+ ## Configuration
44
+
45
+ Most switches and numbers live on the plugin page; changes apply within five seconds without a restart. The ones people touch most:
46
+
47
+ | Key | Default | Meaning |
48
+ |---|---|---|
49
+ | `routing.attentionThreshold` | 0.48 | Attention hit threshold; higher is pickier |
50
+ | `routing.attentionMargin` | 0.03 | How far ahead of the runner-up a hit must be |
51
+ | `routing.ambientReplyProbability` | 0.1 | Chance of a tentative reply outside attention |
52
+ | `routing.l2Threshold` | 0.45 | How close a single fact must be to count as relevant |
53
+ | `routing.maxWakesPerDay` | 20 | Daily cap on spontaneous replies per conversation |
54
+ | `routing.minWakeIntervalMinutes` | 10 | Minimum gap between two wake-ups |
55
+ | `scope.mode` | follow | Which conversations it manages: follow dsh-qqbot / a workspace / specific sessions / off |
56
+ | `context.unifySessions` | false | Whether private and group chats share one memory |
57
+ | `context.groupExcludePrivate` | true | Whether groups are shielded from private sources |
58
+
59
+ ## Known limits
60
+
61
+ - The default exploration chance is a starting guess; tune it for your group
62
+ - Interests grow from real conversations, so an empty memory stays silent
63
+ - An image-only message never wakes it; when an image carries text, whether to look is the model's call
64
+ - The embedding model downloads on first use, so cold start is slow
65
+
66
+ ## License
67
+
68
+ MIT — see [LICENSE](./LICENSE).
package/README.md CHANGED
@@ -1,3 +1,68 @@
1
- # Temporary Holding Version
1
+ # Tide — 聊天记忆与选择性接茬
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > 给 DSH 的聊天记忆插件:把对话沉淀成事实与经历,在合适的时候才开口。
4
+ > English: [README.en.md](./README.en.md)
5
+
6
+ ## 它解决什么
7
+
8
+ 聊天机器人常见两种坏状态:记不住,或者什么都接。
9
+
10
+ Tide 让机器人记住你们聊过的事,并且只在真正该说话的时候开口——群里的闲聊不会句句都接,但聊到它在意的话题,它会主动搭话。记住的东西可以翻、可以改,删掉也有记录。
11
+
12
+ ## 兼容与依赖
13
+
14
+ | 项 | 要求 |
15
+ |---|---|
16
+ | DSH | `>=0.2.2-rc.1 <0.3.0`,在 0.2.2-rc.1 与 0.2.2-rc.3 上实机跑过 |
17
+ | Node | `>=22.5`(用到内置 `node:sqlite`) |
18
+ | QQ 适配器 | 可选,实测 `@tencent-connect/dsh-qqbot@0.5.5`;没有它也能记本地会话,只是群聊接茬不会触发 |
19
+
20
+ 其余依赖随包安装,不需要手动处理。首次使用会下载一个约 25–30MB 的本地向量模型。
21
+
22
+ ## 它怎么工作
23
+
24
+ 1. **先记住**:聊天内容先落成原始记录,这时候还不会回复任何人。
25
+ 2. **再判断**:群消息来了,先看是不是在找它,再看这件事是不是它在意的。在意就直接接;不在意就按一个小概率试探,是否开口由模型自己决定。
26
+ 3. **带着记忆说话**:真要开口时,才把相关的规矩、事实和近期经历带进这一轮。平时不占上下文。
27
+ 4. **说完再沉淀**:一轮结束,从对话里提炼值得留下的事实、经历和关注点。
28
+ 5. **定期清理**:重复的合并、过期的退休、只在当时成立的改掉。
29
+
30
+ 记忆按人分开:群里谁说的记在谁名下,不会混成一个人的事。私聊里说的默认不会出现在群里。
31
+
32
+ ## 安装
33
+
34
+ 包名 `dsh-chat-tide`,插件行 id 是 `tide`。
35
+
36
+ 1. 在 DSH 插件管理器里安装 `dsh-chat-tide`(开发时也可以 `link:` 本地目录)。
37
+ 2. 把 `dsh-chat-tide` 加进 profile 的 `package.json` → `dsh.profile.bundles`。漏了会「装了但不加载」。
38
+ 3. 打开插件详情页,在「作用域」里确认它该管哪些会话。
39
+ 4. 首次只开入站镜像与注入,确认没问题再开路由。
40
+
41
+ 数据默认存在 `~/.dsh/tide/memory.db`。
42
+
43
+ ## 配置
44
+
45
+ 大部分开关和数值在插件详情页里调,改完最多 5 秒生效,不用重启。常用的几项:
46
+
47
+ | 键 | 默认 | 含义 |
48
+ |---|---|---|
49
+ | `routing.attentionThreshold` | 0.48 | 关注点命中阈值,越高越挑 |
50
+ | `routing.attentionMargin` | 0.03 | 命中还要跟第二名拉开多少 |
51
+ | `routing.ambientReplyProbability` | 0.1 | 不在关注点内时,试探接话的概率 |
52
+ | `routing.l2Threshold` | 0.45 | 单看事实够不够近就能判相关 |
53
+ | `routing.maxWakesPerDay` | 20 | 每个会话每天最多主动开口几次 |
54
+ | `routing.minWakeIntervalMinutes` | 10 | 两次开口至少隔多久 |
55
+ | `scope.mode` | follow | 哪些会话归它管:跟随 dsh-qqbot / 指定工作区 / 指定会话 / 关闭 |
56
+ | `context.unifySessions` | false | 私聊和群聊是否共用一份记忆 |
57
+ | `context.groupExcludePrivate` | true | 群里是否屏蔽私聊里说过的事 |
58
+
59
+ ## 已知边界
60
+
61
+ - 试探概率的默认值只是起点,得按自己的群调
62
+ - 关注点靠真实对话长出来,空库时它不会主动说话
63
+ - 纯图片消息不会唤醒它;图片里带文字时,看不看由模型自己判断
64
+ - 第一次用要下载向量模型,会慢一点
65
+
66
+ ## 许可
67
+
68
+ MIT,见 [LICENSE](./LICENSE)。
package/RELEASE.md ADDED
@@ -0,0 +1,55 @@
1
+ # Release checklist
2
+
3
+ Run through this before tagging a public release. Nothing here touches a live deployment.
4
+
5
+ ## 1. Code and checks
6
+
7
+ - [ ] `for f in probes/*.mjs; do node "$f"; done` — all suites pass (probe 14 prints `SKIP` when
8
+ the embedding model is not cached; set `MODEL_CACHE_DIR` to reuse an existing download)
9
+ - [ ] `node scripts/check-naming.mjs` — route, directory, log names and style prefix agree
10
+ - [ ] `node scripts/check-dead-config.mjs` — every declared configuration key is actually read
11
+ - [ ] `node scripts/check-docs.mjs` — the README matches the code
12
+ - [ ] `node scripts/doctor.mjs` against a copy of a real database — no `fail`
13
+ - [ ] CI is green on both Node majors for the release commit
14
+ - [ ] `node --check` every file under `lib/` (the exporter does this too)
15
+
16
+ ## 2. Sanitisation
17
+
18
+ - [ ] No real openids, account ids, host paths, or `appSecret` anywhere in the publishable tree
19
+ - [ ] `presets/private.json` is absent from the export (gitignored, removed by the exporter)
20
+ - [ ] `cordis.patch.yml` uses placeholders only
21
+ - [ ] `node scripts/export-repo.mjs` prints `敏感标识检查: 通过`
22
+
23
+ ## 3. Package metadata
24
+
25
+ - [ ] `name` is `dsh-chat-tide`, `version` follows semver (bump when behaviour changes)
26
+ - [ ] `license` is MIT and `LICENSE` is present
27
+ - [ ] `icon` points to an existing file; `files` lists `lib`, `assets`, `presets`, docs
28
+ - [ ] `dsh.engines.dsh` matches the Harness versions actually tested
29
+
30
+ ## 4. Documentation claims
31
+
32
+ - [ ] Every feature described in the README exists and is reachable from configuration
33
+ - [ ] Known limits are stated, not hidden (README "Known limits" section)
34
+ - [ ] Anything that leaves the machine is documented (model calls, optional model download)
35
+ - [ ] No links to files that are not shipped
36
+
37
+ ## 5. Repository
38
+
39
+ - [ ] `node scripts/export-repo.mjs <target>` produces the tree
40
+ - [ ] `git init`, first commit, remote added, pushed
41
+ - [ ] Tag the release (`vX.Y.Z`)
42
+ - [ ] `CONTRIBUTING.md` present; issue templates optional
43
+
44
+ ## 6. Optional npm publish
45
+
46
+ - [ ] `npm pack --dry-run` shows only intended files (no `presets/private.json`, no `.db`)
47
+ - [ ] Publish, then install from the registry into a clean profile
48
+ - [ ] **Add `dsh-chat-tide` to that profile's `dsh.profile.bundles`**, otherwise the package
49
+ installs but never loads
50
+
51
+ ## 7. After release
52
+
53
+ - [ ] Install from the published artifact, run one real conversation, confirm the config page and
54
+ `/tide` respond
55
+ - [ ] Record the schema version shipped; note the migration path for existing databases
Binary file
@@ -0,0 +1,35 @@
1
+ # Tide bundle patch — copy and edit before installing.
2
+ # The row id and the package name must match: <package>#<row id> keys the plugin's config page.
3
+ - insert:
4
+ - id: tide
5
+ name: 'dsh-chat-tide'
6
+ config:
7
+ enabled: true
8
+ scope:
9
+ # off | all | qq | allowlist | workspace | preset | sessions
10
+ mode: 'allowlist'
11
+ peers:
12
+ - '<your-peer-openid-1>'
13
+ - '<your-peer-openid-2>'
14
+ embedding:
15
+ enabled: true
16
+ model: 'Xenova/bge-small-zh-v1.5'
17
+ dtype: 'q8'
18
+ cacheDir: ''
19
+ remoteHost: 'https://hf-mirror.com'
20
+ features:
21
+ ingest: true
22
+ writePipeline: true
23
+ egressGate: false
24
+ contextInject: true
25
+ routing: true
26
+ tools: true
27
+ writeTools: false
28
+ extract:
29
+ provider: '<your-llm-provider>'
30
+ model: '<your-llm-model>'
31
+ minChars: 6
32
+ context:
33
+ enabled: true
34
+ maxChars: 1200
35
+ eventLimit: 5
package/lib/ambient.js ADDED
@@ -0,0 +1,44 @@
1
+ // 接茬概率:只在「灰区兜底判相关 且 L2 初筛命中」之后掷骰子。
2
+ // 未命中 L2 的绝不经手;direct / command 永不经过这里。
3
+
4
+ /**
5
+ * L2 初筛:拿当前消息去比对已有 active 事实,判断是否有主题重叠。
6
+ * @param {number[]} queryVec 归一化后的查询向量
7
+ * @param {Array<{factId:string, vec:Float32Array}>} factVectors 归一化事实向量
8
+ * @returns {{hit:boolean, score:number, factId:string|null}}
9
+ */
10
+ export function screenAgainstL2(queryVec, factVectors, threshold = 0.45) {
11
+ if (!queryVec || !factVectors?.length) return { hit: false, score: 0, factId: null };
12
+ let best = 0;
13
+ let bestId = null;
14
+ for (const f of factVectors) {
15
+ let dot = 0;
16
+ const n = Math.min(queryVec.length, f.vec.length);
17
+ for (let i = 0; i < n; i++) dot += queryVec[i] * f.vec[i];
18
+ if (dot > best) {
19
+ best = dot;
20
+ bestId = f.factId;
21
+ }
22
+ }
23
+ return { hit: best >= threshold, score: best, factId: bestId };
24
+ }
25
+
26
+ /**
27
+ * L2 初筛通过后掷骰子。返回是否应该接茬,以及原因(写审计用)。
28
+ * 只对 ambient 生效;direct / command 直接放行。
29
+ */
30
+ export function shouldReplyToAmbient({
31
+ label,
32
+ l2 = { hit: false, score: 0 },
33
+ probability = 0.1,
34
+ random = Math.random,
35
+ }) {
36
+ if (label !== 'ambient' && label !== 'relevant_ambient' && label !== 'uncertain') {
37
+ return { reply: true, reason: 'not-ambient' };
38
+ }
39
+ if (probability <= 0) return { reply: false, reason: 'probability-off' };
40
+ if (!l2.hit) return { reply: false, reason: 'no-l2-overlap', score: l2.score };
41
+ if (probability >= 1) return { reply: true, reason: 'probability-always', score: l2.score };
42
+ const hit = random() < probability;
43
+ return { reply: hit, reason: hit ? 'probability-hit' : 'probability-miss', score: l2.score };
44
+ }
package/lib/api.js ADDED
@@ -0,0 +1,199 @@
1
+ // 插件设置页的 JSON API(只认本机):给客户端插件读/写设置与概览
2
+ import { effectiveView, setOverride, clearOverride } from './settings.js';
3
+ import { GROUPS } from './schema.js';
4
+ import { createStatusHandler } from './status-page.js';
5
+ import { listRecentSessions } from './sessions.js';
6
+ import { ROUTE, API_BASE, apiUrl, dataDir, LOG_CLIENT_LOAD } from './const.js';
7
+
8
+ const json = (res, status, body) => {
9
+ res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
10
+ res.end(JSON.stringify(body));
11
+ };
12
+
13
+ function count(db, sql, ...args) {
14
+ try {
15
+ return db.prepare(sql).get(...args)?.c ?? 0;
16
+ } catch {
17
+ return 0;
18
+ }
19
+ }
20
+
21
+ export function createApiHandler(db, { cfg = {} } = {}) {
22
+ return async (req, res) => {
23
+ const host = String(req.headers?.host ?? '');
24
+ const name = host.split(':')[0].toLowerCase();
25
+ if (!(name === '127.0.0.1' || name === 'localhost' || name === '[::1]' || name === '::1')) {
26
+ return json(res, 403, { ok: false, error: 'forbidden' });
27
+ }
28
+ const url = new URL(req.url ?? '/', 'http://localhost');
29
+
30
+ // 客户端加载自检:浏览器端 apply 跑过就会打一条,用来确认前端那一半有没有起来
31
+ if (req.method === 'POST' && url.pathname === apiUrl('/client-loaded')) {
32
+ const chunks = [];
33
+ for await (const chunk of req) chunks.push(chunk);
34
+ let body = {};
35
+ try {
36
+ body = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
37
+ } catch {
38
+ /* 空体也可以 */
39
+ }
40
+ try {
41
+ const { appendFileSync, mkdirSync } = await import('node:fs');
42
+ const { join } = await import('node:path');
43
+ const dir = dataDir();
44
+ mkdirSync(dir, { recursive: true });
45
+ appendFileSync(
46
+ join(dir, LOG_CLIENT_LOAD),
47
+ `${new Date().toISOString()}\t${String(body.key ?? '')}\t${String(body.id ?? '')}\t${String(body.version ?? '')}\n`,
48
+ );
49
+ } catch {
50
+ /* 记不下来不影响响应 */
51
+ }
52
+ return json(res, 200, { ok: true });
53
+ }
54
+
55
+ // 最近会话(供作用域选择用;只读标题、工作区与时间)
56
+ if (req.method === 'GET' && url.pathname === apiUrl('/sessions')) {
57
+ const limit = Math.min(Number(url.searchParams.get('limit') ?? 60), 200);
58
+ const sessions = listRecentSessions(limit);
59
+ return json(res, 200, { ok: true, rows: sessions });
60
+ }
61
+
62
+ // 概览
63
+ if (req.method === 'GET' && url.pathname === apiUrl('/overview')) {
64
+ return json(res, 200, {
65
+ ok: true,
66
+ schema: (() => {
67
+ try {
68
+ return db.prepare('PRAGMA user_version').get()?.user_version ?? 0;
69
+ } catch {
70
+ return 0;
71
+ }
72
+ })(),
73
+ facts: count(db, "SELECT count(*) c FROM fact WHERE status='active'"),
74
+ rules: count(db, "SELECT count(*) c FROM fact WHERE status='active' AND subtype='rule'"),
75
+ events: count(db, "SELECT count(*) c FROM event WHERE status='active' AND event_type='observation'"),
76
+ attention: count(db, "SELECT count(*) c FROM attention WHERE status='active'"),
77
+ pendingVectors: count(db, 'SELECT count(*) c FROM fact_version WHERE embedding_pending=1'),
78
+ });
79
+ }
80
+
81
+ // 读设置(含分组清单:页面按它渲染,不硬编码键名)
82
+ if (req.method === 'GET' && url.pathname === apiUrl('/settings')) {
83
+ return json(res, 200, { ok: true, groups: GROUPS, fields: effectiveView(db, cfg) });
84
+ }
85
+
86
+ // 写设置:{ values: { key: value } } 或 { clear: [key] }
87
+ if (req.method === 'POST' && url.pathname === apiUrl('/settings')) {
88
+ const chunks = [];
89
+ for await (const chunk of req) chunks.push(chunk);
90
+ let body;
91
+ try {
92
+ body = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
93
+ } catch {
94
+ return json(res, 400, { ok: false, error: 'invalid-json' });
95
+ }
96
+ const applied = [];
97
+ for (const [k, v] of Object.entries(body.values ?? {})) {
98
+ const r = setOverride(db, k, v);
99
+ if (!r.ok) return json(res, 400, { ok: false, error: `${k}:${r.error}` });
100
+ applied.push({ key: k, value: r.value });
101
+ }
102
+ for (const k of body.clear ?? []) {
103
+ clearOverride(db, String(k));
104
+ applied.push({ key: String(k), cleared: true });
105
+ }
106
+ return json(res, 200, { ok: true, applied });
107
+ }
108
+
109
+ // 事实列表(可按关键词过滤)
110
+ if (req.method === 'GET' && url.pathname === apiUrl('/facts')) {
111
+ const limit = Math.min(Number(url.searchParams.get('limit') ?? 30), 200);
112
+ const q = (url.searchParams.get('q') ?? '').trim();
113
+ const rows = q
114
+ ? db
115
+ .prepare(
116
+ `SELECT f.canonical_key AS key, f.subtype, f.scope, f.provenance_tier AS tier,
117
+ v.value, f.updated_at AS updatedAt
118
+ FROM fact f JOIN fact_version v ON v.fact_id=f.fact_id AND v.version=f.current_version
119
+ WHERE f.status='active' AND (f.canonical_key LIKE ? OR v.value LIKE ?)
120
+ ORDER BY f.updated_at DESC LIMIT ?`,
121
+ )
122
+ .all(`%${q}%`, `%${q}%`, limit)
123
+ : db
124
+ .prepare(
125
+ `SELECT f.canonical_key AS key, f.subtype, f.scope, f.provenance_tier AS tier,
126
+ v.value, f.updated_at AS updatedAt
127
+ FROM fact f JOIN fact_version v ON v.fact_id=f.fact_id AND v.version=f.current_version
128
+ WHERE f.status='active' ORDER BY f.updated_at DESC LIMIT ?`,
129
+ )
130
+ .all(limit);
131
+ return json(res, 200, { ok: true, rows });
132
+ }
133
+
134
+ // 经历列表
135
+ if (req.method === 'GET' && url.pathname === apiUrl('/events')) {
136
+ const limit = Math.min(Number(url.searchParams.get('limit') ?? 30), 200);
137
+ const rows = db
138
+ .prepare(
139
+ `SELECT content, speaker_name AS speaker, scope, occurred_at AS occurredAt
140
+ FROM event WHERE status='active' AND event_type='observation'
141
+ ORDER BY occurred_at DESC LIMIT ?`,
142
+ )
143
+ .all(limit);
144
+ return json(res, 200, { ok: true, rows });
145
+ }
146
+
147
+ // 关注点列表
148
+ if (req.method === 'GET' && url.pathname === apiUrl('/attention')) {
149
+ const rows = db
150
+ .prepare(
151
+ `SELECT attention_id AS id, label, note, weight, hits, last_hit_at AS lastHitAt
152
+ FROM attention WHERE status='active' ORDER BY weight DESC, created_at DESC LIMIT 200`,
153
+ )
154
+ .all();
155
+ return json(res, 200, { ok: true, rows });
156
+ }
157
+
158
+ // 退休一个关注点
159
+ if (req.method === 'POST' && url.pathname === apiUrl('/attention/retire')) {
160
+ const chunks = [];
161
+ for await (const chunk of req) chunks.push(chunk);
162
+ let body = {};
163
+ try {
164
+ body = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
165
+ } catch {
166
+ return json(res, 400, { ok: false, error: 'invalid-json' });
167
+ }
168
+ const id = String(body.id ?? '');
169
+ if (!id) return json(res, 400, { ok: false, error: 'missing-id' });
170
+ db.prepare("UPDATE attention SET status='retired' WHERE attention_id=?").run(id);
171
+ try {
172
+ db.prepare('DELETE FROM attention_fts WHERE attention_id=?').run(id);
173
+ db.prepare('DELETE FROM vec_attention WHERE rowid = (SELECT rowid FROM attention WHERE attention_id=?)').run(id);
174
+ } catch {
175
+ /* 向量表缺失忽略 */
176
+ }
177
+ return json(res, 200, { ok: true, retired: id });
178
+ }
179
+
180
+ return json(res, 404, { ok: false, error: 'not-found' });
181
+ };
182
+ }
183
+
184
+ /** 单一路由:/tide 与 /tide/api/* 共用一个前缀处理器。 */
185
+ export function createPluginHandler(db, { dbFile, cfg } = {}) {
186
+ const api = createApiHandler(db, { cfg });
187
+ const page = createStatusHandler(db, { dbFile, cfg });
188
+ return async (req, res) => {
189
+ const url = new URL(req.url ?? '/', 'http://localhost');
190
+ if (url.pathname === API_BASE || url.pathname.startsWith(API_BASE + '/')) {
191
+ return api(req, res);
192
+ }
193
+ if (url.pathname === ROUTE || url.pathname === ROUTE + '/') {
194
+ return page(req, res);
195
+ }
196
+ res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
197
+ res.end('not found');
198
+ };
199
+ }