dsh-bot-team 1.1.0 → 1.3.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 +194 -0
- package/README.md +206 -156
- package/client.js +3706 -746
- package/cordis.patch.yml +2 -2
- package/host/agents.js +469 -289
- package/host/background-interactions.js +213 -0
- package/host/conversations.js +45 -0
- package/host/discussion-flow.js +179 -0
- package/host/discussion-runner.js +491 -0
- package/host/domain.js +551 -69
- package/host/engine.js +621 -166
- package/host/execution.js +192 -121
- package/host/native-activity.js +225 -0
- package/host/native-session.js +144 -0
- package/host/pdf.js +43 -6
- package/host/prompt.js +253 -9
- package/host/routes.js +31 -1
- package/host/service.js +751 -127
- package/host/session-export.js +22 -0
- package/host/session-visibility.js +147 -0
- package/host/storage-layout.js +109 -0
- package/host/tools.js +314 -85
- package/host/update-check.js +351 -0
- package/host/util.js +3 -2
- package/host/workspace.js +36 -2
- package/index.js +1 -1
- package/package.json +14 -6
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/** Read-only npm update detection. No install operations or recurring work. */
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import semver from 'semver';
|
|
4
|
+
|
|
5
|
+
export const UPDATE_REGISTRY_URL = 'https://registry.npmjs.org/dsh-bot-team';
|
|
6
|
+
export const RELEASE_NOTES_URL = 'https://github.com/shper/dsh-bot-team/blob/main/CHANGELOG.md';
|
|
7
|
+
export const RELEASE_NOTES_SOURCE_URL = 'https://raw.githubusercontent.com/shper/dsh-bot-team/main/CHANGELOG.md';
|
|
8
|
+
const NPM_PACKAGE_URL = 'https://www.npmjs.com/package/dsh-bot-team';
|
|
9
|
+
const SEMVER_OPTIONS = { includePrerelease: true };
|
|
10
|
+
|
|
11
|
+
const isRecord = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
12
|
+
const own = (value, key) => Object.hasOwn(value, key);
|
|
13
|
+
const validVersion = value => typeof value === 'string' && semver.valid(value) ? value.trim() : null;
|
|
14
|
+
const versionUrl = version => version ? NPM_PACKAGE_URL + '/v/' + encodeURIComponent(version) : NPM_PACKAGE_URL;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* DSH app-boot documents this getter as the single version used for admission.
|
|
18
|
+
* Its implementation reads the running app-boot package's own manifest. Do not
|
|
19
|
+
* infer a runtime from this plugin's engines range or a workspace installation.
|
|
20
|
+
* The runtime resolver supplies this host module; bare Node/embedders may not.
|
|
21
|
+
*/
|
|
22
|
+
export async function resolveDshRuntimeVersion(load = () => import('@deepseek-ai/dsh-app-boot')) {
|
|
23
|
+
try {
|
|
24
|
+
const host = await load();
|
|
25
|
+
return validVersion(await host.getDshRuntimeVersion());
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function acceptsRange(runtimeVersion, range) {
|
|
32
|
+
return typeof range === 'string' && range.trim() !== '' &&
|
|
33
|
+
semver.validRange(range, SEMVER_OPTIONS) !== null && semver.satisfies(runtimeVersion, range, SEMVER_OPTIONS);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Match DSH admission semantics, including prerelease runtimes and peer fallback. */
|
|
37
|
+
function compatibilityOf(manifest, runtimeVersion) {
|
|
38
|
+
if (!runtimeVersion) return 'unknown';
|
|
39
|
+
if (own(manifest, 'engines')) {
|
|
40
|
+
if (!isRecord(manifest.engines)) return 'incompatible';
|
|
41
|
+
if (own(manifest.engines, 'dsh')) {
|
|
42
|
+
return acceptsRange(runtimeVersion, manifest.engines.dsh) ? 'compatible' : 'incompatible';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (own(manifest, 'peerDependencies')) {
|
|
46
|
+
if (!isRecord(manifest.peerDependencies)) return 'incompatible';
|
|
47
|
+
for (const [name, range] of Object.entries(manifest.peerDependencies)) {
|
|
48
|
+
if (typeof range !== 'string') return 'incompatible';
|
|
49
|
+
if (name !== '@deepseek-ai/dsh' && !name.startsWith('@deepseek-ai/dsh-')) continue;
|
|
50
|
+
const effective = ['workspace:^', 'workspace:~', 'workspace:*'].includes(range) ? runtimeVersion : range;
|
|
51
|
+
if (!acceptsRange(runtimeVersion, effective)) return 'incompatible';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return 'compatible';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
class UpdateCheckError extends Error {}
|
|
58
|
+
|
|
59
|
+
function manifestOf(versions, version) {
|
|
60
|
+
const manifest = own(versions, version) ? versions[version] : null;
|
|
61
|
+
if (!isRecord(manifest) || manifest.version !== version) return null;
|
|
62
|
+
if (own(manifest, 'name') && manifest.name !== 'dsh-bot-team') return null;
|
|
63
|
+
return manifest;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function selectVersions(metadata, currentVersion, runtimeVersion) {
|
|
67
|
+
if (!validVersion(currentVersion)) throw new UpdateCheckError('Current plugin version is not valid SemVer.');
|
|
68
|
+
if (!isRecord(metadata) || !isRecord(metadata['dist-tags']) || !isRecord(metadata.versions)) {
|
|
69
|
+
throw new UpdateCheckError('npm returned invalid package metadata.');
|
|
70
|
+
}
|
|
71
|
+
const latestVersion = validVersion(metadata['dist-tags'].latest);
|
|
72
|
+
if (!latestVersion || semver.prerelease(latestVersion) !== null) {
|
|
73
|
+
throw new UpdateCheckError('npm latest does not identify a stable version.');
|
|
74
|
+
}
|
|
75
|
+
const latest = manifestOf(metadata.versions, latestVersion);
|
|
76
|
+
if (!latest) throw new UpdateCheckError('npm latest version metadata is missing or invalid.');
|
|
77
|
+
|
|
78
|
+
let availableVersion = null;
|
|
79
|
+
// npm latest is the stable release ceiling: a newer next/beta tag, even if
|
|
80
|
+
// its version string is stable, is never automatically recommended.
|
|
81
|
+
for (const version of Object.keys(metadata.versions)) {
|
|
82
|
+
if (!validVersion(version) || semver.prerelease(version) !== null ||
|
|
83
|
+
!semver.gt(version, currentVersion) || semver.gt(version, latestVersion)) continue;
|
|
84
|
+
const manifest = manifestOf(metadata.versions, version);
|
|
85
|
+
if (!manifest || compatibilityOf(manifest, runtimeVersion) !== 'compatible') continue;
|
|
86
|
+
if (!availableVersion || semver.gt(version, availableVersion)) availableVersion = version;
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
runtimeVersion,
|
|
90
|
+
latestVersion,
|
|
91
|
+
availableVersion,
|
|
92
|
+
hasUpdate: availableVersion !== null,
|
|
93
|
+
// These two fields describe latest, even when a lower version is safe.
|
|
94
|
+
compatibility: compatibilityOf(latest, runtimeVersion),
|
|
95
|
+
requiredDsh: typeof latest.engines?.dsh === 'string' ? latest.engines.dsh : null,
|
|
96
|
+
releaseNotesUrl: RELEASE_NOTES_URL,
|
|
97
|
+
updateUrl: versionUrl(availableVersion || latestVersion),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function cancelBody(body, reason) {
|
|
102
|
+
try { void body?.cancel(reason).catch(() => {}); } catch { /* already closed/locked */ }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function readResponseText(response, signal, maxBytes) {
|
|
106
|
+
if (signal.aborted) {
|
|
107
|
+
cancelBody(response.body, signal.reason);
|
|
108
|
+
throw signal.reason;
|
|
109
|
+
}
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
cancelBody(response.body);
|
|
112
|
+
throw new UpdateCheckError('npm registry returned HTTP ' + response.status + '.');
|
|
113
|
+
}
|
|
114
|
+
const contentLength = Number(response.headers?.get('content-length'));
|
|
115
|
+
if (Number.isFinite(contentLength) && contentLength > maxBytes) {
|
|
116
|
+
cancelBody(response.body);
|
|
117
|
+
throw new UpdateCheckError('npm package metadata exceeds the size limit.');
|
|
118
|
+
}
|
|
119
|
+
if (typeof response.body?.getReader !== 'function') {
|
|
120
|
+
cancelBody(response.body);
|
|
121
|
+
throw new UpdateCheckError('npm returned an unreadable response body.');
|
|
122
|
+
}
|
|
123
|
+
const reader = response.body.getReader();
|
|
124
|
+
const cancel = () => cancelBody(reader, signal.reason);
|
|
125
|
+
signal.addEventListener('abort', cancel, { once: true });
|
|
126
|
+
const chunks = [];
|
|
127
|
+
let size = 0;
|
|
128
|
+
try {
|
|
129
|
+
while (true) {
|
|
130
|
+
const { done, value } = await reader.read();
|
|
131
|
+
if (signal.aborted) throw signal.reason;
|
|
132
|
+
if (done) break;
|
|
133
|
+
size += value.byteLength;
|
|
134
|
+
if (size > maxBytes) throw new UpdateCheckError('npm package metadata exceeds the size limit.');
|
|
135
|
+
chunks.push(value);
|
|
136
|
+
}
|
|
137
|
+
const bytes = new Uint8Array(size);
|
|
138
|
+
let offset = 0;
|
|
139
|
+
for (const chunk of chunks) {
|
|
140
|
+
bytes.set(chunk, offset);
|
|
141
|
+
offset += chunk.byteLength;
|
|
142
|
+
}
|
|
143
|
+
return new TextDecoder().decode(bytes);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
cancelBody(reader, error);
|
|
146
|
+
throw error;
|
|
147
|
+
} finally {
|
|
148
|
+
signal.removeEventListener('abort', cancel);
|
|
149
|
+
reader.releaseLock();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Exact version headings only; Unreleased and headings inside code are ignored. */
|
|
154
|
+
function extractReleaseNotes(markdown, version) {
|
|
155
|
+
const lines = markdown.split(/\r?\n/);
|
|
156
|
+
let start = -1;
|
|
157
|
+
let fence = null;
|
|
158
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
159
|
+
const line = lines[index];
|
|
160
|
+
const marker = /^ {0,3}(`{3,}|~{3,})/.exec(line)?.[1];
|
|
161
|
+
if (marker) {
|
|
162
|
+
if (!fence) fence = marker;
|
|
163
|
+
else if (marker[0] === fence[0] && marker.length >= fence.length) fence = null;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (fence) continue;
|
|
167
|
+
const heading = /^##[ \t]+(.+)$/.exec(line)?.[1];
|
|
168
|
+
if (start !== -1 && /^#{1,2}[ \t]+/.test(line)) return lines.slice(start, index).join('\n').trim() || null;
|
|
169
|
+
if (!heading) continue;
|
|
170
|
+
const closingBracket = heading.indexOf(']');
|
|
171
|
+
const token = heading.startsWith('[')
|
|
172
|
+
? (closingBracket > 1 ? heading.slice(1, closingBracket) : null)
|
|
173
|
+
: heading.split(/[ \t]/)[0];
|
|
174
|
+
if (token === version) start = index + 1;
|
|
175
|
+
}
|
|
176
|
+
return start === -1 ? null : lines.slice(start).join('\n').trim() || null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* One checker per service. fetch/getRuntimeVersion/now are injectable so tests
|
|
181
|
+
* exercise the real startup path without network requests or environment flags.
|
|
182
|
+
* GET reads snapshot(); check({ force: true }) bypasses only the success cache,
|
|
183
|
+
* always joining an existing request. Errors keep the last successful fields.
|
|
184
|
+
*/
|
|
185
|
+
export class UpdateChecker {
|
|
186
|
+
constructor({ currentVersion, fetch: fetchImpl = globalThis.fetch,
|
|
187
|
+
getRuntimeVersion = resolveDshRuntimeVersion, now = Date.now,
|
|
188
|
+
timeoutMs = 10000, maxBytes = 2 * 1024 * 1024, cacheTtlMs = 5 * 60 * 1000,
|
|
189
|
+
notesTimeoutMs = 2000, notesMaxBytes = 128 * 1024,
|
|
190
|
+
onChange = () => {} } = {}) {
|
|
191
|
+
this.fetch = fetchImpl;
|
|
192
|
+
this.getRuntimeVersion = getRuntimeVersion;
|
|
193
|
+
this.now = now;
|
|
194
|
+
this.timeoutMs = timeoutMs;
|
|
195
|
+
this.maxBytes = maxBytes;
|
|
196
|
+
this.cacheTtlMs = cacheTtlMs;
|
|
197
|
+
this.notesTimeoutMs = notesTimeoutMs;
|
|
198
|
+
this.notesMaxBytes = notesMaxBytes;
|
|
199
|
+
this.onChange = onChange;
|
|
200
|
+
this.inFlight = null;
|
|
201
|
+
this.controller = null;
|
|
202
|
+
this.disposed = false;
|
|
203
|
+
this.lastSuccessAt = null;
|
|
204
|
+
this.state = {
|
|
205
|
+
instanceId: randomUUID(),
|
|
206
|
+
revision: 0,
|
|
207
|
+
status: 'idle',
|
|
208
|
+
currentVersion,
|
|
209
|
+
runtimeVersion: null,
|
|
210
|
+
latestVersion: null,
|
|
211
|
+
availableVersion: null,
|
|
212
|
+
hasUpdate: false,
|
|
213
|
+
compatibility: null,
|
|
214
|
+
requiredDsh: null,
|
|
215
|
+
checkedAt: null,
|
|
216
|
+
error: null,
|
|
217
|
+
releaseNotes: null,
|
|
218
|
+
releaseNotesVersion: null,
|
|
219
|
+
releaseNotesUrl: RELEASE_NOTES_URL,
|
|
220
|
+
updateUrl: versionUrl(validVersion(currentVersion)),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
snapshot() { return { ...this.state }; }
|
|
225
|
+
|
|
226
|
+
publish(fields) {
|
|
227
|
+
this.state = { ...this.state, ...fields, revision: this.state.revision + 1 };
|
|
228
|
+
try { this.onChange(this.snapshot()); } catch { /* observers cannot fail a check */ }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async request(signal, onMetadata) {
|
|
232
|
+
signal.throwIfAborted();
|
|
233
|
+
const [metadata, rawRuntimeVersion] = await Promise.all([
|
|
234
|
+
Promise.resolve().then(async () => {
|
|
235
|
+
const response = await this.fetch(UPDATE_REGISTRY_URL, {
|
|
236
|
+
method: 'GET',
|
|
237
|
+
headers: { accept: 'application/vnd.npm.install-v1+json' },
|
|
238
|
+
redirect: 'error',
|
|
239
|
+
cache: 'no-store',
|
|
240
|
+
signal,
|
|
241
|
+
});
|
|
242
|
+
const text = await readResponseText(response, signal, this.maxBytes);
|
|
243
|
+
try { return JSON.parse(text); }
|
|
244
|
+
catch { throw new UpdateCheckError('npm returned invalid JSON.'); }
|
|
245
|
+
}),
|
|
246
|
+
Promise.resolve().then(() => this.getRuntimeVersion()).catch(() => null),
|
|
247
|
+
]);
|
|
248
|
+
const result = {
|
|
249
|
+
...selectVersions(metadata, this.state.currentVersion, validVersion(rawRuntimeVersion)),
|
|
250
|
+
releaseNotes: null, releaseNotesVersion: null,
|
|
251
|
+
};
|
|
252
|
+
onMetadata(result);
|
|
253
|
+
const notesVersion = result.availableVersion ||
|
|
254
|
+
(semver.gt(result.latestVersion, this.state.currentVersion) ? result.latestVersion : null);
|
|
255
|
+
if (notesVersion) {
|
|
256
|
+
const releaseNotes = await this.readReleaseNotes(notesVersion, signal);
|
|
257
|
+
if (releaseNotes) return { ...result, releaseNotes, releaseNotesVersion: notesVersion };
|
|
258
|
+
}
|
|
259
|
+
return result;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async readReleaseNotes(version, signal) {
|
|
263
|
+
const controller = new AbortController();
|
|
264
|
+
const cancel = () => controller.abort(signal.reason);
|
|
265
|
+
if (signal.aborted) return null;
|
|
266
|
+
signal.addEventListener('abort', cancel, { once: true });
|
|
267
|
+
const timer = setTimeout(() => controller.abort(), this.notesTimeoutMs);
|
|
268
|
+
let onAbort;
|
|
269
|
+
const cancelled = new Promise((_, reject) => {
|
|
270
|
+
onAbort = () => reject(controller.signal.reason);
|
|
271
|
+
controller.signal.addEventListener('abort', onAbort, { once: true });
|
|
272
|
+
});
|
|
273
|
+
try {
|
|
274
|
+
const text = await Promise.race([
|
|
275
|
+
Promise.resolve().then(async () => {
|
|
276
|
+
controller.signal.throwIfAborted();
|
|
277
|
+
const response = await this.fetch(RELEASE_NOTES_SOURCE_URL, {
|
|
278
|
+
method: 'GET', headers: { accept: 'text/plain' }, redirect: 'error',
|
|
279
|
+
cache: 'no-store', signal: controller.signal,
|
|
280
|
+
});
|
|
281
|
+
return readResponseText(response, controller.signal, this.notesMaxBytes);
|
|
282
|
+
}),
|
|
283
|
+
cancelled,
|
|
284
|
+
]);
|
|
285
|
+
return extractReleaseNotes(text, version);
|
|
286
|
+
} catch {
|
|
287
|
+
return null;
|
|
288
|
+
} finally {
|
|
289
|
+
clearTimeout(timer);
|
|
290
|
+
signal.removeEventListener('abort', cancel);
|
|
291
|
+
controller.signal.removeEventListener('abort', onAbort);
|
|
292
|
+
controller.abort();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
check({ force = false } = {}) {
|
|
297
|
+
if (this.disposed) return Promise.resolve(this.snapshot());
|
|
298
|
+
if (this.inFlight) return this.inFlight;
|
|
299
|
+
const age = this.lastSuccessAt === null ? Infinity : this.now() - this.lastSuccessAt;
|
|
300
|
+
if (!force && this.state.status === 'ready' && age >= 0 && age < this.cacheTtlMs) {
|
|
301
|
+
return Promise.resolve(this.snapshot());
|
|
302
|
+
}
|
|
303
|
+
const previous = this.snapshot();
|
|
304
|
+
const controller = new AbortController();
|
|
305
|
+
this.controller = controller;
|
|
306
|
+
let onAbort;
|
|
307
|
+
const cancelled = new Promise((_, reject) => {
|
|
308
|
+
onAbort = () => reject(controller.signal.reason);
|
|
309
|
+
controller.signal.addEventListener('abort', onAbort, { once: true });
|
|
310
|
+
});
|
|
311
|
+
const timer = setTimeout(() => controller.abort(new UpdateCheckError('Update check timed out.')), this.timeoutMs);
|
|
312
|
+
// The race also bounds injected/custom fetch implementations which ignore
|
|
313
|
+
// AbortSignal. Late completions never publish a stale result after dispose.
|
|
314
|
+
let metadataResult = null;
|
|
315
|
+
const ready = result => {
|
|
316
|
+
this.lastSuccessAt = this.now();
|
|
317
|
+
this.publish({ ...result, status: 'ready', checkedAt: new Date(this.lastSuccessAt).toISOString(), error: null });
|
|
318
|
+
};
|
|
319
|
+
this.inFlight = Promise.race([Promise.resolve().then(() => this.request(controller.signal, result => { metadataResult = result; })), cancelled])
|
|
320
|
+
.then(result => {
|
|
321
|
+
if (!this.disposed) ready(result);
|
|
322
|
+
})
|
|
323
|
+
.catch(error => {
|
|
324
|
+
if (!this.disposed) {
|
|
325
|
+
// A notes timeout must not discard a successful npm version lookup.
|
|
326
|
+
if (metadataResult) ready(metadataResult);
|
|
327
|
+
else this.publish({ status: 'error', error: error instanceof UpdateCheckError
|
|
328
|
+
? error.message : 'Unable to check the npm registry.' });
|
|
329
|
+
}
|
|
330
|
+
})
|
|
331
|
+
.finally(() => {
|
|
332
|
+
clearTimeout(timer);
|
|
333
|
+
controller.signal.removeEventListener('abort', onAbort);
|
|
334
|
+
controller.abort();
|
|
335
|
+
this.controller = null;
|
|
336
|
+
this.inFlight = null;
|
|
337
|
+
if (this.disposed && this.state.status === 'checking') {
|
|
338
|
+
this.state = { ...previous, revision: this.state.revision + 1 };
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
.then(() => this.snapshot());
|
|
342
|
+
this.publish({ status: 'checking', error: null });
|
|
343
|
+
return this.inFlight;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
dispose() {
|
|
347
|
+
if (this.disposed) return;
|
|
348
|
+
this.disposed = true;
|
|
349
|
+
this.controller?.abort(new UpdateCheckError('Update check cancelled.'));
|
|
350
|
+
}
|
|
351
|
+
}
|
package/host/util.js
CHANGED
|
@@ -27,14 +27,15 @@ export function resolveConfig(raw = {}) {
|
|
|
27
27
|
migrateLegacyLayout: raw.migrateLegacyLayout !== false,
|
|
28
28
|
maxParallelTurns: positiveInt(raw.maxParallelTurns, 3, 1, 8),
|
|
29
29
|
maxContextMessages: positiveInt(raw.maxContextMessages, 40, 5, 200),
|
|
30
|
-
|
|
30
|
+
// Native sessions may wait for approval, children, or a continuing goal.
|
|
31
|
+
// Zero follows the native lifecycle instead of imposing a five-minute cap.
|
|
32
|
+
turnTimeoutMs: raw.turnTimeoutMs === 0 || raw.turnTimeoutMs === undefined ? 0 : positiveInt(raw.turnTimeoutMs, 0, 5000, 3600000),
|
|
31
33
|
maxMessageChars: positiveInt(raw.maxMessageChars, 40000, 1000, 200000),
|
|
32
34
|
maxGroups: positiveInt(raw.maxGroups, 50, 1, 500),
|
|
33
35
|
enableModelTools: raw.enableModelTools !== false,
|
|
34
36
|
botSandbox: raw.botSandbox !== false,
|
|
35
37
|
botApprovalPolicy: raw.botApprovalPolicy === 'never' ? 'never' : 'ask',
|
|
36
38
|
autoDispatchReady: raw.autoDispatchReady === true,
|
|
37
|
-
maxTurnsPerHour: positiveInt(raw.maxTurnsPerHour, 120, 1, 10000),
|
|
38
39
|
dailyTokenBudget: positiveInt(raw.dailyTokenBudget, 5000000, 1000, 100000000),
|
|
39
40
|
toolWaitTimeoutMs: positiveInt(raw.toolWaitTimeoutMs, 180000, 10000, 900000),
|
|
40
41
|
maxMembersPerGroup: positiveInt(raw.maxMembersPerGroup, 12, 1, 50),
|
package/host/workspace.js
CHANGED
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import os from 'node:os';
|
|
20
20
|
import path from 'node:path';
|
|
21
|
-
import {
|
|
21
|
+
import { constants } from 'node:fs';
|
|
22
|
+
import { access, mkdir, readdir, realpath, rename, rmdir, stat, unlink } from 'node:fs/promises';
|
|
22
23
|
|
|
23
24
|
export function dshHomeDir() {
|
|
24
25
|
return process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
|
|
@@ -60,6 +61,39 @@ export function sessionCwdMatches(headerCwd, workspaceCwd) {
|
|
|
60
61
|
&& path.resolve(headerCwd) === path.resolve(workspaceCwd);
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
/** Normalize a user-selected host path without creating any project directories. */
|
|
65
|
+
export function normalizeWorkspacePath(value) {
|
|
66
|
+
if (typeof value !== 'string' || value.length > 4096 || value.includes('\0')) {
|
|
67
|
+
throw Object.assign(new Error('workspaceCwd 必须是有效的目录路径'), { code: 'invalid-input' });
|
|
68
|
+
}
|
|
69
|
+
let target = value.trim();
|
|
70
|
+
if (!target) return '';
|
|
71
|
+
if (target === '~') target = os.homedir();
|
|
72
|
+
else if (target.startsWith('~/')) target = path.join(os.homedir(), target.slice(2));
|
|
73
|
+
if (!path.isAbsolute(target)) {
|
|
74
|
+
throw Object.assign(new Error('workspaceCwd 必须是绝对路径(可使用 ~/)'), { code: 'invalid-input' });
|
|
75
|
+
}
|
|
76
|
+
return path.resolve(target);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Resolve symlinks and verify an existing, readable/searchable local directory. */
|
|
80
|
+
export async function validateProjectWorkspace(value) {
|
|
81
|
+
const target = normalizeWorkspacePath(value);
|
|
82
|
+
if (!target) throw Object.assign(new Error('项目目录不能为空'), { code: 'invalid-input' });
|
|
83
|
+
try {
|
|
84
|
+
const canonical = await realpath(target);
|
|
85
|
+
if (!(await stat(canonical)).isDirectory()) {
|
|
86
|
+
throw new Error('请选择文件夹,而不是文件');
|
|
87
|
+
}
|
|
88
|
+
await access(canonical, constants.R_OK | constants.X_OK);
|
|
89
|
+
return canonical;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
const reason = error?.code === 'ENOENT' ? '目录不存在' : error?.code === 'EACCES' || error?.code === 'EPERM'
|
|
92
|
+
? '目录不可访问' : String(error?.message || error);
|
|
93
|
+
throw Object.assign(new Error('无法引入项目目录:' + reason), { code: 'invalid-input' });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
63
97
|
export async function pathExists(target) {
|
|
64
98
|
try {
|
|
65
99
|
await stat(target);
|
|
@@ -167,7 +201,7 @@ export async function migrateLegacyLayout({ repo, config, logger }) {
|
|
|
167
201
|
const legacyWorkspace = current ? isInside(current, legacyRoot) : false;
|
|
168
202
|
const previousDirectWorkspace = current ? isPreviousDirectWorkspace(config, group, current) : false;
|
|
169
203
|
if (current && !legacyWorkspace && !previousDirectWorkspace) {
|
|
170
|
-
|
|
204
|
+
// A user-owned project may have moved or become unavailable. Never recreate it.
|
|
171
205
|
continue;
|
|
172
206
|
}
|
|
173
207
|
if (current && current !== target) await moveWorkspace(current, target, logger, summary);
|
package/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { resolveConfig } from './host/util.js';
|
|
|
11
11
|
// Package is published as dsh-bot-team; the Cordis plugin id stays bot-team,
|
|
12
12
|
// matching the built-in pattern (dsh-tool-bash → name "tool-bash").
|
|
13
13
|
export const name = 'bot-team';
|
|
14
|
-
export const inject = ['agents', 'storageDomain', 'connection', 'tools', 'agentPresets'];
|
|
14
|
+
export const inject = ['agents', 'storageDomain', 'connection', 'tools', 'agentPresets', 'sessionQuery', 'workspaceController'];
|
|
15
15
|
|
|
16
16
|
export function apply(ctx, rawConfig = {}) {
|
|
17
17
|
const config = resolveConfig(rawConfig && typeof rawConfig === 'object' ? rawConfig : {});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-bot-team",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Bot Team: single chats and multi-agent group chat for DeepSeek Harness. Every bot gets a direct chat; create groups/topics, organize them into collapsible custom folders with drag ordering, add full-capability bot agents (files, search, web, workspace, commands, skills) with their own model and BOT.md persona/working rules, attach files or pick skills with + or / in the composer, @mention them to assign tasks, review each other's work, and run multi-round discussions and tasks. The UI follows DSH zh/en, and group timelines export to Markdown, HTML or a paginated PDF with selectable text.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"locale/*.json",
|
|
19
19
|
"icon.svg",
|
|
20
20
|
"LICENSE",
|
|
21
|
-
"README.md"
|
|
21
|
+
"README.md",
|
|
22
|
+
"CHANGELOG.md"
|
|
22
23
|
],
|
|
23
24
|
"dsh": {
|
|
24
25
|
"bundle": {
|
|
@@ -29,7 +30,8 @@
|
|
|
29
30
|
"immediately": true,
|
|
30
31
|
"inject": [
|
|
31
32
|
"@deepseek-ai/dsh-client-ui-layout",
|
|
32
|
-
"@deepseek-ai/dsh-client-ui-sidebar"
|
|
33
|
+
"@deepseek-ai/dsh-client-ui-sidebar",
|
|
34
|
+
"@deepseek-ai/dsh-client-ui-workspace"
|
|
33
35
|
]
|
|
34
36
|
}
|
|
35
37
|
},
|
|
@@ -52,8 +54,7 @@
|
|
|
52
54
|
"homepage": "https://github.com/shper/dsh-bot-team#readme",
|
|
53
55
|
"repository": {
|
|
54
56
|
"type": "git",
|
|
55
|
-
"url": "git+https://github.com/shper/dsh-bot-team.git"
|
|
56
|
-
"directory": "bot-team"
|
|
57
|
+
"url": "git+https://github.com/shper/dsh-bot-team.git"
|
|
57
58
|
},
|
|
58
59
|
"bugs": {
|
|
59
60
|
"url": "https://github.com/shper/dsh-bot-team/issues"
|
|
@@ -69,6 +70,13 @@
|
|
|
69
70
|
"prepublishOnly": "npm run check && npm test"
|
|
70
71
|
},
|
|
71
72
|
"dependencies": {
|
|
72
|
-
"fontkit": "^2.0.4"
|
|
73
|
+
"fontkit": "^2.0.4",
|
|
74
|
+
"semver": "^7.8.5"
|
|
75
|
+
},
|
|
76
|
+
"peerDependencies": {
|
|
77
|
+
"@deepseek-ai/dsh-app-boot": "*"
|
|
78
|
+
},
|
|
79
|
+
"peerDependenciesMeta": {
|
|
80
|
+
"@deepseek-ai/dsh-app-boot": { "optional": true }
|
|
73
81
|
}
|
|
74
82
|
}
|