dsh-plugin-tool-management 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +131 -0
- package/README.md +185 -238
- package/README_EN.md +185 -304
- package/cordis.patch.yml +9 -55
- package/docs/images/1/345/234/272/346/231/257.png +0 -0
- package/docs/images/1/345/234/272/346/231/257_en.png +0 -0
- package/docs/images/2MCP.png +0 -0
- package/docs/images/2MCP_en.png +0 -0
- package/docs/images/3/346/212/200/350/203/275.png +0 -0
- package/docs/images/3/346/212/200/350/203/275_en.png +0 -0
- package/docs/images/4/345/255/220/346/231/272/350/203/275/344/275/223.png +0 -0
- package/docs/images/4/345/255/220/346/231/272/350/203/275/344/275/223_en.png +0 -0
- package/docs/images/5/346/217/220/347/244/272/350/257/215.png +0 -0
- package/docs/images/5/346/217/220/347/244/272/350/257/215_en.png +0 -0
- package/docs/images/6/350/256/260/345/277/206.png +0 -0
- package/docs/images/6/350/256/260/345/277/206_en.png +0 -0
- package/docs/images/7/344/274/232/350/257/235.png +0 -0
- package/docs/images/7/344/274/232/350/257/235_en.png +0 -0
- package/docs/images/8/345/205/274/345/256/271.png +0 -0
- package/docs/images/8/345/205/274/345/256/271_en.png +0 -0
- package/docs/update.md +55 -0
- package/lib/agents-md/preset-id.js +49 -0
- package/lib/agents-md/service.js +180 -57
- package/lib/client.js +4710 -3560
- package/lib/compat/preset-reach.js +425 -0
- package/lib/compat/probe.js +665 -0
- package/lib/history/bridge.js +293 -0
- package/lib/history/workspace.js +498 -52
- package/lib/http-fence.js +94 -0
- package/lib/hub.js +160 -2
- package/lib/index.js +759 -157
- package/lib/mcp/override-blocks.js +195 -0
- package/lib/rules/provider.js +3 -3
- package/lib/rules/service.js +342 -31
- package/lib/scene-prompt-sync.js +112 -0
- package/lib/skills/core.js +123 -35
- package/lib/skills/service.js +9 -1
- package/lib/subagents/service.js +197 -23
- package/lib/subagents/tools.js +8 -2
- package/package.json +6 -3
- package/screenshots.json +10 -9
- package/docs/Changelog.md +0 -517
- package/docs/images/MCP.png +0 -0
- package/docs/images//344/274/232/350/257/235.png +0 -0
- package/docs/images//345/234/272/346/231/257.png +0 -0
- package/docs/images//345/255/220/346/231/272/350/203/275/344/275/223.png +0 -0
- package/docs/images//346/212/200/350/203/275.png +0 -0
- package/docs/images//346/217/220/347/244/272/350/257/215.png +0 -0
- package/docs/images//350/256/260/345/277/206.png +0 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { symbols } from "@deepseek-ai/cordis";
|
|
2
|
+
import { workspaceDomainSpec } from "@deepseek-ai/dsh-workspace";
|
|
3
|
+
import { assessHost, routeFor, refusalsFor, CapabilityRefusalError } from "../compat/probe.js";
|
|
4
|
+
export { CapabilityRefusalError };
|
|
5
|
+
// No Service subclass, no ctx.provide(), no second storage-domain owner.
|
|
6
|
+
// The official API lacks unarchive and a cache deletion barrier, so this file
|
|
7
|
+
// adapts the gaps. WHICH parts of the host it may touch is decided by
|
|
8
|
+
// ../compat/probe.ts from live, observable facts (module identity, member
|
|
9
|
+
// presence, shapes, read-only behaviour), never from comparing source text:
|
|
10
|
+
// text equality only ever proved "same release", and it turned every harmless
|
|
11
|
+
// upstream refactor into a total feature outage.
|
|
12
|
+
const cacheGuards = new WeakMap();
|
|
13
|
+
const raw = (value) => value?.[symbols.original] ?? value;
|
|
14
|
+
const sameLifecycle = (a, b) => a && b && a.createdAt === b.createdAt && (a.cwd ?? null) === (b.cwd ?? null);
|
|
15
|
+
/**
|
|
16
|
+
* Install the write barrier around the host's projection cache when (and only
|
|
17
|
+
* when) the host cannot safely delete a cache row on its own.
|
|
18
|
+
*
|
|
19
|
+
* Guarded on capability, not on implementation text: the cache is wrapped only
|
|
20
|
+
* when the write path and a deletable table are both present, and a host that
|
|
21
|
+
* ships its own `delete`/`whenIdle` barrier never gets wrapped at all.
|
|
22
|
+
*
|
|
23
|
+
* @param cache - host projection cache instance.
|
|
24
|
+
* @returns the barrier handle.
|
|
25
|
+
* @throws {CapabilityRefusalError} when the cache cannot support a safe delete.
|
|
26
|
+
*/
|
|
27
|
+
function acquireCacheGuard(cache) {
|
|
28
|
+
let guard = cacheGuards.get(cache);
|
|
29
|
+
if (guard) {
|
|
30
|
+
guard.users += 1;
|
|
31
|
+
return guard;
|
|
32
|
+
}
|
|
33
|
+
const missing = ["put", "write", "requireTable"].filter((name) => typeof cache[name] !== "function");
|
|
34
|
+
if (missing.length > 0) {
|
|
35
|
+
throw new CapabilityRefusalError("delete", refusalsFor({ findings: [{ id: "projection.write", label: "投影缓存写入路径", kind: "write", owner: "projectionCache", fallback: "disable-destructive", state: "missing-member", detail: `宿主投影缓存缺少 ${missing.join(", ")}`, missing }] }, ["projection.write"]));
|
|
36
|
+
}
|
|
37
|
+
const table = cache.requireTable();
|
|
38
|
+
if (typeof table?.delete !== "function") {
|
|
39
|
+
throw new CapabilityRefusalError("delete", [{
|
|
40
|
+
id: "projection.table-delete",
|
|
41
|
+
label: "投影缓存行删除",
|
|
42
|
+
detail: "宿主投影缓存存储不支持安全删除(table.delete 缺失)",
|
|
43
|
+
recovery: "请更新本插件到与本机 DSH 匹配的版本。",
|
|
44
|
+
}]);
|
|
45
|
+
}
|
|
46
|
+
const descriptors = Object.fromEntries(["put", "write"].map((name) => [name, Object.getOwnPropertyDescriptor(cache, name)]));
|
|
47
|
+
const originals = { put: cache.put, write: cache.write };
|
|
48
|
+
const blocked = new Map();
|
|
49
|
+
const pending = new Set();
|
|
50
|
+
const queues = new Map();
|
|
51
|
+
const isBlocked = (id, identity) => blocked.has(id) && (blocked.get(id) === null || sameLifecycle(blocked.get(id), identity));
|
|
52
|
+
function track(task) {
|
|
53
|
+
pending.add(task);
|
|
54
|
+
task.then(() => pending.delete(task), () => pending.delete(task));
|
|
55
|
+
return task;
|
|
56
|
+
}
|
|
57
|
+
function serial(id, fn) {
|
|
58
|
+
const task = (queues.get(id) ?? Promise.resolve()).then(fn);
|
|
59
|
+
const settled = task.then(() => { }, () => { });
|
|
60
|
+
queues.set(id, settled);
|
|
61
|
+
settled.then(() => { if (queues.get(id) === settled)
|
|
62
|
+
queues.delete(id); });
|
|
63
|
+
return track(task);
|
|
64
|
+
}
|
|
65
|
+
const wrappers = {
|
|
66
|
+
put(id, identity, rows) {
|
|
67
|
+
// Preserve the upstream snapshot-at-call boundary even while queued.
|
|
68
|
+
if (isBlocked(id, identity))
|
|
69
|
+
return Promise.resolve();
|
|
70
|
+
const expected = { ...identity };
|
|
71
|
+
let detached;
|
|
72
|
+
try {
|
|
73
|
+
detached = structuredClone(rows);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
return Promise.reject(error);
|
|
77
|
+
}
|
|
78
|
+
// Serialize puts with deletion; never erase a new lifecycle reusing id.
|
|
79
|
+
return serial(id, async () => {
|
|
80
|
+
if (isBlocked(id, expected))
|
|
81
|
+
return;
|
|
82
|
+
await originals.put.call(cache, id, expected, detached);
|
|
83
|
+
if (isBlocked(id, expected)) {
|
|
84
|
+
const stored = table.get(id);
|
|
85
|
+
if (stored && sameLifecycle(stored.identity, expected))
|
|
86
|
+
await table.delete(id);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
write(session) {
|
|
91
|
+
// Track the flush await before put as well as put itself. Otherwise a
|
|
92
|
+
// delayed write could outlive disposal and bypass the restored method.
|
|
93
|
+
try {
|
|
94
|
+
return track(Promise.resolve(originals.write.call(cache, session)));
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
return Promise.reject(error);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
cache.put = wrappers.put;
|
|
102
|
+
cache.write = wrappers.write;
|
|
103
|
+
guard = {
|
|
104
|
+
users: 1,
|
|
105
|
+
begin(id, header) { blocked.set(id, header ?? null); },
|
|
106
|
+
clearTombstone(id) { blocked.delete(id); },
|
|
107
|
+
async whenIdle() { while (pending.size)
|
|
108
|
+
await Promise.allSettled([...pending]); },
|
|
109
|
+
async delete(id) {
|
|
110
|
+
await this.whenIdle();
|
|
111
|
+
return serial(id, async () => {
|
|
112
|
+
const stored = table.get(id);
|
|
113
|
+
const identity = blocked.get(id);
|
|
114
|
+
if (!stored || identity == null || sameLifecycle(stored.identity, identity))
|
|
115
|
+
await table.delete(id);
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
async release() {
|
|
119
|
+
if (--this.users > 0)
|
|
120
|
+
return;
|
|
121
|
+
await this.whenIdle();
|
|
122
|
+
if (this.users > 0)
|
|
123
|
+
return;
|
|
124
|
+
let restored = true;
|
|
125
|
+
for (const name of ["put", "write"]) {
|
|
126
|
+
// Never overwrite a later third-party wrapper.
|
|
127
|
+
if (cache[name] !== wrappers[name]) {
|
|
128
|
+
restored = false;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (descriptors[name])
|
|
132
|
+
Object.defineProperty(cache, name, descriptors[name]);
|
|
133
|
+
else
|
|
134
|
+
delete cache[name];
|
|
135
|
+
}
|
|
136
|
+
if (restored) {
|
|
137
|
+
blocked.clear();
|
|
138
|
+
cacheGuards.delete(cache);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
cacheGuards.set(cache, guard);
|
|
143
|
+
return guard;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Live-session members the delete sequence needs, split by branch.
|
|
147
|
+
*
|
|
148
|
+
* `liveEntryFor`/`detachEntered` are private in the official declarations
|
|
149
|
+
* (dsh-session lib/types/index.d.ts), so they are consumed through the same
|
|
150
|
+
* capability gate as everything else. The point of checking BEFORE any
|
|
151
|
+
* destructive step is unchanged: otherwise the failure lands mid-sequence
|
|
152
|
+
* (after flush, before ledger cleanup) and leaves "transcript gone, archive
|
|
153
|
+
* flag still set" behind.
|
|
154
|
+
*
|
|
155
|
+
* @param sessions - ctx.sessions; the cold branch tolerates it being absent.
|
|
156
|
+
* @param hasLiveSession - whether this session is currently live.
|
|
157
|
+
* @returns the capability ids that must be `ok`, in evaluation order.
|
|
158
|
+
*/
|
|
159
|
+
export function requiredSessionCapabilities(hasLiveSession) {
|
|
160
|
+
return hasLiveSession
|
|
161
|
+
? ["sessions.detach-live"]
|
|
162
|
+
: ["sessions.cold-announce"];
|
|
163
|
+
}
|
|
164
|
+
export function createHistoryBridge(ctx, suppliedRegistry, onArchive) {
|
|
165
|
+
// Cordis returns a fresh traceable method proxy on access. Compare and adapt
|
|
166
|
+
// the original object, not proxies; calls retain the service owner's context.
|
|
167
|
+
const registry = raw(suppliedRegistry);
|
|
168
|
+
const cache = raw(ctx.get("sessionProjectionCache"));
|
|
169
|
+
let guard;
|
|
170
|
+
let stopObserving;
|
|
171
|
+
let assessment;
|
|
172
|
+
let assessmentAt = 0;
|
|
173
|
+
/** Re-probe at most every 5s: probing is cheap but not free on hot paths. */
|
|
174
|
+
const ASSESS_TTL_MS = 5000;
|
|
175
|
+
/**
|
|
176
|
+
* The current host assessment. Refreshed lazily so a host that gains or
|
|
177
|
+
* loses a service is noticed without a plugin reload.
|
|
178
|
+
* @param force - re-probe even when a cached value is still fresh.
|
|
179
|
+
*/
|
|
180
|
+
function hostAssessment(force = false) {
|
|
181
|
+
const now = Date.now();
|
|
182
|
+
if (force || assessment === undefined || now - assessmentAt > ASSESS_TTL_MS) {
|
|
183
|
+
try {
|
|
184
|
+
assessment = assessHost(ctx);
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
// A probe that throws is itself a finding: report it rather than
|
|
188
|
+
// letting it escape into a delete sequence.
|
|
189
|
+
ctx.logger?.warn?.(`host capability probe failed: ${String(error)}`);
|
|
190
|
+
assessment = undefined;
|
|
191
|
+
}
|
|
192
|
+
assessmentAt = now;
|
|
193
|
+
}
|
|
194
|
+
return assessment;
|
|
195
|
+
}
|
|
196
|
+
/** Capability ids not `ok` from the given list. */
|
|
197
|
+
function unavailable(ids) {
|
|
198
|
+
const current = hostAssessment();
|
|
199
|
+
if (current === undefined)
|
|
200
|
+
return ids.map((id) => ({ id, label: id, detail: "宿主能力探测不可用", recovery: "请查看插件日志 dsh-plugin-tool-management.log" }));
|
|
201
|
+
return refusalsFor(current, ids);
|
|
202
|
+
}
|
|
203
|
+
/** Throw when any listed capability is not usable; nothing is written before this runs. */
|
|
204
|
+
function requireCapabilities(operation, ids) {
|
|
205
|
+
const refusals = unavailable(ids);
|
|
206
|
+
if (refusals.length > 0)
|
|
207
|
+
throw new CapabilityRefusalError(operation, refusals);
|
|
208
|
+
}
|
|
209
|
+
const method = (name, ...args) => {
|
|
210
|
+
if (typeof registry[name] !== "function")
|
|
211
|
+
throw new Error(`宿主缺少归档兼容接口 ${name},操作已停止`);
|
|
212
|
+
return registry[name](...args);
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* The adapter route needs the registry's read/write/delegate members. This is
|
|
216
|
+
* a presence-and-shape check on the live object — the previous implementation
|
|
217
|
+
* compared these methods' source text against this plugin's own copy of the
|
|
218
|
+
* official prototype, which failed on any upstream refactor and, worse,
|
|
219
|
+
* depended on the plugin loading a second copy of the package.
|
|
220
|
+
*/
|
|
221
|
+
function checkWorkspace(operation = "archive") {
|
|
222
|
+
const current = hostAssessment();
|
|
223
|
+
if (current !== undefined) {
|
|
224
|
+
const decision = routeFor(current, operation);
|
|
225
|
+
if (decision.via === "none")
|
|
226
|
+
throw new CapabilityRefusalError(operation, decision.refusals);
|
|
227
|
+
return decision;
|
|
228
|
+
}
|
|
229
|
+
requireCapabilities(operation, ["workspace.read-state", "workspace.enqueue"]);
|
|
230
|
+
return { via: "adapter", refusals: [] };
|
|
231
|
+
}
|
|
232
|
+
/** Structural check of the workspace index shape. */
|
|
233
|
+
function checkIndexShape() {
|
|
234
|
+
for (const name of ["headers", "sessionPaths", "invalidSessionPaths", "entities"]) {
|
|
235
|
+
if (!(registry[name] instanceof Map))
|
|
236
|
+
throw new Error(`工作区索引 ${name} 不兼容,未修改数据`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
registry,
|
|
241
|
+
/** Latest assessment, for the compatibility surface and the HTTP status op. */
|
|
242
|
+
capabilities(force = false) { return hostAssessment(force); },
|
|
243
|
+
/** Capability ids an operation still needs, without throwing. */
|
|
244
|
+
refusalsFor(operation, ids) { return unavailable(ids); }, observe() {
|
|
245
|
+
if (stopObserving)
|
|
246
|
+
return;
|
|
247
|
+
let previous = new Set(registry.archivedSessionIds);
|
|
248
|
+
// Official durable domain notifications also cover archive operations
|
|
249
|
+
// from the official sidebar or an external archive manager. No monkey
|
|
250
|
+
// patch of archiveSession and no competing writer of its ledger.
|
|
251
|
+
stopObserving = ctx.on("domain/changed", (change) => {
|
|
252
|
+
if (change?.domain !== workspaceDomainSpec.name || change.table !== "" || !Array.isArray(change.value?.archivedSessionIds))
|
|
253
|
+
return;
|
|
254
|
+
const next = new Set(change.value.archivedSessionIds);
|
|
255
|
+
const at = Date.now();
|
|
256
|
+
for (const id of next)
|
|
257
|
+
if (!previous.has(id))
|
|
258
|
+
void onArchive(id, at).catch((error) => ctx.logger.warn(String(error)));
|
|
259
|
+
for (const id of previous)
|
|
260
|
+
if (!next.has(id))
|
|
261
|
+
void onArchive(id, null).catch((error) => ctx.logger.warn(String(error)));
|
|
262
|
+
previous = next;
|
|
263
|
+
});
|
|
264
|
+
},
|
|
265
|
+
checkWorkspace,
|
|
266
|
+
checkIndexShape,
|
|
267
|
+
enqueue(fn) { checkWorkspace("archive"); checkIndexShape(); return registry.enqueueOperation(fn); },
|
|
268
|
+
state: () => method("requireState"),
|
|
269
|
+
table: () => method("requireTable"),
|
|
270
|
+
setState: (state) => method("setState", state),
|
|
271
|
+
readHeader: (id) => method("readSessionHeader", id),
|
|
272
|
+
/**
|
|
273
|
+
* Install the cache delete barrier for one session, before anything is
|
|
274
|
+
* flushed or detached. Prefers a host-native barrier when it exists.
|
|
275
|
+
*/
|
|
276
|
+
async beginDelete(id, header) {
|
|
277
|
+
checkWorkspace("delete");
|
|
278
|
+
if (!cache)
|
|
279
|
+
throw new CapabilityRefusalError("delete", [{
|
|
280
|
+
id: "projection.write",
|
|
281
|
+
label: "投影缓存写入路径",
|
|
282
|
+
detail: "宿主未提供 sessionProjectionCache",
|
|
283
|
+
recovery: "请确认 DSH 版本并更新本插件到匹配版本。",
|
|
284
|
+
}]);
|
|
285
|
+
if (typeof cache.delete === "function" && typeof cache.whenIdle === "function")
|
|
286
|
+
return;
|
|
287
|
+
guard ??= acquireCacheGuard(cache);
|
|
288
|
+
guard.begin(id, header);
|
|
289
|
+
},
|
|
290
|
+
cache() { return guard ?? cache; },
|
|
291
|
+
async dispose() { stopObserving?.(); await guard?.release(); },
|
|
292
|
+
};
|
|
293
|
+
}
|