dsh-expert-group 0.1.1

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 (45) hide show
  1. package/LICENSE +38 -0
  2. package/README.md +355 -0
  3. package/THIRD_PARTY_NOTICES.md +35 -0
  4. package/assets/notification-whoosh.wav +0 -0
  5. package/bin/prompt-inspector.mjs +63 -0
  6. package/client-bundle.js +14891 -0
  7. package/client-expert-group-model.js +426 -0
  8. package/client-expert-group-sidebar.js +5401 -0
  9. package/client-freshness.js +364 -0
  10. package/client-model-config.js +2155 -0
  11. package/client-notifications.js +38 -0
  12. package/client.js +6487 -0
  13. package/cordis.patch.yml +10 -0
  14. package/fanout-host.js +1507 -0
  15. package/fanout.js +2981 -0
  16. package/host/composition.js +155 -0
  17. package/host/expert-group-runtime.js +822 -0
  18. package/host/expert-group-snapshot.js +450 -0
  19. package/host/fanout-runtime.js +822 -0
  20. package/host/handoff.js +80 -0
  21. package/host/lineage.js +274 -0
  22. package/host/notifications.js +189 -0
  23. package/host/persistence-health.js +389 -0
  24. package/host/recovery.js +242 -0
  25. package/host/rpc-transport.js +573 -0
  26. package/host/settings-runtime.js +168 -0
  27. package/host/settings-source.js +265 -0
  28. package/host/storage-binding.js +94 -0
  29. package/host/system-toast.js +916 -0
  30. package/image-routing.js +263 -0
  31. package/index.js +9327 -0
  32. package/install.ps1 +219 -0
  33. package/orchestrator.js +239 -0
  34. package/package.json +142 -0
  35. package/prompt-contracts.js +148 -0
  36. package/prompt-inspector.js +584 -0
  37. package/prompt-sidecar.js +610 -0
  38. package/resource-budget.js +540 -0
  39. package/role-registry.js +286 -0
  40. package/rpc-host.js +310 -0
  41. package/scripts/assemble-client.mjs +605 -0
  42. package/scripts/build.mjs +124 -0
  43. package/shared.js +4003 -0
  44. package/storage.js +190 -0
  45. package/uninstall.ps1 +47 -0
package/storage.js ADDED
@@ -0,0 +1,190 @@
1
+ /**
2
+ * dsh-expert-group 的 storage domain 纯辅助函数。
3
+ * 运行数据没有 settings fallback;这里仅定义 domain 快照、binding 与 child LRU 协议。
4
+ */
5
+
6
+ export const CHILD_LRU_LIMIT = 300;
7
+
8
+ /**
9
+ * 首次 storageDomain 注入门闩:注入回调未执行前,写入只能等待;
10
+ * 回调明确报告缺失服务或初始化失败时,等待者收到同一个失败结果。
11
+ */
12
+ export function createInitializationGate() {
13
+ let settled = false;
14
+ let resolveGate;
15
+ let rejectGate;
16
+ const promise = new Promise((resolve, reject) => {
17
+ resolveGate = resolve;
18
+ rejectGate = reject;
19
+ });
20
+ return {
21
+ promise,
22
+ resolve(value) {
23
+ if (settled) return false;
24
+ settled = true;
25
+ resolveGate(value);
26
+ return true;
27
+ },
28
+ reject(error) {
29
+ if (settled) return false;
30
+ settled = true;
31
+ rejectGate(error);
32
+ return true;
33
+ },
34
+ get settled() { return settled; },
35
+ };
36
+ }
37
+
38
+ function finiteSequence(value) {
39
+ return Number.isInteger(value) && value >= 0 ? value : undefined;
40
+ }
41
+
42
+ function recordSequence(value) {
43
+ return value !== null && typeof value === "object" && !Array.isArray(value)
44
+ ? finiteSequence(value.lruSequence)
45
+ : undefined;
46
+ }
47
+
48
+ function withSequence(value, sequence) {
49
+ return value !== null && typeof value === "object" && !Array.isArray(value)
50
+ ? { ...value, lruSequence: sequence }
51
+ : value;
52
+ }
53
+
54
+ function compareEntries(left, right) {
55
+ const leftSequence = recordSequence(left[1]);
56
+ const rightSequence = recordSequence(right[1]);
57
+ if (leftSequence !== undefined && rightSequence !== undefined && leftSequence !== rightSequence) return leftSequence - rightSequence;
58
+ if (leftSequence === undefined && rightSequence !== undefined) return -1;
59
+ if (leftSequence !== undefined && rightSequence === undefined) return 1;
60
+ const leftKey = String(left[0]);
61
+ const rightKey = String(right[0]);
62
+ return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
63
+ }
64
+
65
+ /**
66
+ * 把缺失或重复序号的历史记录按 key 稳定补齐,分配值严格避开已有序号,
67
+ * 完全不依赖 JSON/SQLite entries 顺序。
68
+ */
69
+ export function normalizeLruEntries(entries) {
70
+ const unique = new Map(entries ?? []);
71
+ const sorted = [...unique.entries()].sort(compareEntries);
72
+ const occupied = new Set();
73
+ const unresolved = [];
74
+ for (const [key, value] of sorted) {
75
+ const sequence = recordSequence(value);
76
+ if (sequence === undefined || occupied.has(sequence)) unresolved.push([key, value]);
77
+ else occupied.add(sequence);
78
+ }
79
+ let nextSequence = occupied.size === 0 ? 0 : Math.max(...occupied) + 1;
80
+ const assigned = new Map();
81
+ unresolved.sort(([leftKey], [rightKey]) => {
82
+ const left = String(leftKey);
83
+ const right = String(rightKey);
84
+ return left < right ? -1 : left > right ? 1 : 0;
85
+ });
86
+ for (const [key, value] of unresolved) {
87
+ while (occupied.has(nextSequence)) nextSequence += 1;
88
+ assigned.set(key, nextSequence);
89
+ occupied.add(nextSequence);
90
+ nextSequence += 1;
91
+ }
92
+ return sorted.map(([key, value]) => [
93
+ key,
94
+ assigned.has(key) ? withSequence(value, assigned.get(key)) : value,
95
+ ]).sort(compareEntries);
96
+ }
97
+
98
+ /** 追加最新 child,维护单调序号,超限从最旧序号裁剪。 */
99
+ export function appendLruEntry(entries, key, value, limit = CHILD_LRU_LIMIT) {
100
+ if (limit <= 0) return [];
101
+ const current = normalizeLruEntries([...entries ?? []].filter(([entryKey]) => entryKey !== key));
102
+ const maxSequence = current.reduce((max, [, entryValue]) => Math.max(max, recordSequence(entryValue) ?? -1), -1);
103
+ return [...current, [key, withSequence(value, maxSequence + 1)]]
104
+ .sort(compareEntries)
105
+ .slice(-limit);
106
+ }
107
+
108
+ /** 返回 domain 表读出的公共 source 形状,不读取 settings。 */
109
+ export function readDomainSnapshot(sessionEntries, childEntries) {
110
+ return {
111
+ sessionOverrides: Object.fromEntries(sessionEntries ?? []),
112
+ childModels: Object.fromEntries(childEntries ?? []),
113
+ };
114
+ }
115
+
116
+ /** 计算先写最新记录、再删除裁剪项的计划。 */
117
+ export function planLruPersistence(entries, key, value, limit = CHILD_LRU_LIMIT) {
118
+ const current = [...entries ?? []];
119
+ const next = appendLruEntry(current, key, value, limit);
120
+ const keep = new Set(next.map(([entryKey]) => entryKey));
121
+ return {
122
+ record: next.find(([entryKey]) => entryKey === key)?.[1] ?? value,
123
+ trimKeys: current.map(([entryKey]) => entryKey).filter((entryKey) => !keep.has(entryKey)),
124
+ entries: next,
125
+ };
126
+ }
127
+
128
+ /** domain 普通记录写入;generation 失效时不再触碰旧 handle。 */
129
+ export async function persistDomainEntry({ table, key, value, isCurrent }) {
130
+ if (typeof isCurrent === "function" && !isCurrent()) return { written: false, detached: true };
131
+ await table.put(key, value);
132
+ if (typeof isCurrent === "function" && !isCurrent()) return { written: true, detached: true };
133
+ return { written: true, detached: false };
134
+ }
135
+
136
+ /**
137
+ * child durable put 成功前不删除任何旧记录;裁剪删除失败时保留新记录,下一次写入可重试。
138
+ */
139
+ export async function persistLruEntry({ table, key, value, limit = CHILD_LRU_LIMIT, isCurrent }) {
140
+ if (typeof isCurrent === "function" && !isCurrent()) return { record: value, trimmed: false, detached: true, deleted: [] };
141
+ const plan = planLruPersistence(table.entries(), key, value, limit);
142
+ await table.put(key, plan.record);
143
+ const deleted = [];
144
+ if (typeof isCurrent === "function" && !isCurrent()) return { record: plan.record, trimmed: false, detached: true, deleted };
145
+ for (const trimKey of plan.trimKeys) {
146
+ try {
147
+ await table.delete(trimKey);
148
+ deleted.push(trimKey);
149
+ } catch (error) {
150
+ return { record: plan.record, trimmed: false, deleted, error };
151
+ }
152
+ if (typeof isCurrent === "function" && !isCurrent()) return { record: plan.record, trimmed: false, detached: true, deleted };
153
+ }
154
+ return { record: plan.record, trimmed: true, deleted };
155
+ }
156
+
157
+ /**
158
+ * storageDomain 重载控制器。旧 binding 失效后立即脱离 current;迟到 Promise 不能安装到新 generation。
159
+ */
160
+ export function createBindingController() {
161
+ let generation = 0;
162
+ let active;
163
+ const invalidate = (binding) => {
164
+ if (binding === undefined || binding.status === "fallback") return false;
165
+ binding.status = "fallback";
166
+ binding.onInvalidate?.();
167
+ if (active === binding) active = undefined;
168
+ return true;
169
+ };
170
+ return {
171
+ begin() {
172
+ if (active !== undefined) invalidate(active);
173
+ const binding = { generation: ++generation, status: "opening" };
174
+ active = binding;
175
+ return binding;
176
+ },
177
+ install(binding, value = {}) {
178
+ if (active !== binding || binding.status === "fallback") return false;
179
+ Object.assign(binding, value, { status: "ready" });
180
+ return true;
181
+ },
182
+ invalidate,
183
+ isCurrent(binding) {
184
+ return active === binding && binding.status !== "fallback";
185
+ },
186
+ current() {
187
+ return active;
188
+ },
189
+ };
190
+ }
package/uninstall.ps1 ADDED
@@ -0,0 +1,47 @@
1
+ # 从用户的 web profile 卸载 dsh-expert-group,并清理同名安装副本。
2
+ # 用法:powershell -ExecutionPolicy Bypass -File .\uninstall.ps1
3
+ $ErrorActionPreference = "Stop"
4
+
5
+ Write-Host "卸载 dsh-expert-group..." -ForegroundColor Cyan
6
+
7
+ $dsh = Get-Command dsh -ErrorAction SilentlyContinue
8
+ if ($null -eq $dsh) {
9
+ throw "未找到 dsh 命令。请先安装 @deepseek-ai/dsh 并确保其 bin 目录在 PATH 中。"
10
+ }
11
+
12
+ $dshHome = if ($env:DSH_HOME) { $env:DSH_HOME } else { Join-Path $env:USERPROFILE ".dsh" }
13
+ $profileDir = Join-Path $dshHome "profiles\web"
14
+ $profileManifest = Join-Path $profileDir "package.json"
15
+ # -Encoding UTF8:WinPS 5.1 的 Get-Content 默认按 ANSI 读取,会破坏 UTF-8 manifest 的
16
+ # 中文字段并让 ConvertFrom-Json 失败;显式声明使 WinPS 5.1 与 pwsh 7 行为一致。
17
+ $manifest = if (Test-Path -LiteralPath $profileManifest) { Get-Content -LiteralPath $profileManifest -Raw -Encoding UTF8 | ConvertFrom-Json } else { $null }
18
+
19
+ # 卸载当前包名(与 package.json 的 name 一致)。
20
+ foreach ($pkg in @("dsh-expert-group")) {
21
+ $installed = $false
22
+ if ($null -ne $manifest -and $null -ne $manifest.dependencies) {
23
+ $installed = @($manifest.dependencies.PSObject.Properties | Where-Object { $_.Name -eq $pkg }).Count -gt 0
24
+ }
25
+ if ($installed) {
26
+ & dsh plugin --profile web remove $pkg
27
+ if ($LASTEXITCODE -ne 0) {
28
+ throw "dsh plugin remove $pkg 失败,退出码 $LASTEXITCODE"
29
+ }
30
+ }
31
+ }
32
+
33
+ # 清理 install.ps1 复制出的安装副本(全局位置与旧的 profile 内 packages 位置都处理)。
34
+ # 与安装白名单语义对齐:install.ps1 现在只复制 package.json files 声明的发布面,安装副本
35
+ # 自身就是完整的「整包」目录,因此卸载的逆操作仍然是整目录删除,不改成按白名单逐项删除——
36
+ # 逐项删除只认同名单,会把旧版整目录复制遗留在用户目录里的 test/、.scratch/、tgz 等
37
+ # 残留永久留下,反而破坏卸载的收尾保证。
38
+ foreach ($pkg in @("dsh-expert-group")) {
39
+ foreach ($target in @((Join-Path $dshHome "profiles\packages\$pkg"), (Join-Path $profileDir "packages\$pkg"))) {
40
+ if (Test-Path -LiteralPath $target) {
41
+ Remove-Item -LiteralPath $target -Recurse -Force
42
+ Write-Host "已清理安装副本:$target" -ForegroundColor DarkGray
43
+ }
44
+ }
45
+ }
46
+
47
+ Write-Host "完成。重启 DSH Web 后生效。" -ForegroundColor Green