@sparkelf/dsh-plus 0.2.2-alpha.2.29 → 0.2.2-alpha.2.30
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/lib/apply.js +1 -1
- package/lib/bin.js +145 -217
- package/lib/types/apply.d.ts +1 -1
- package/lib/types/apply.js +117 -56
- package/lib/types/standalone-cli.js +2 -93
- package/lib/types/standalone-profile.d.ts +1 -8
- package/lib/types/standalone-profile.js +26 -179
- package/package.json +16 -1
- package/patches/ui-schedule-plaza-tab.patch +217 -0
|
@@ -9,9 +9,8 @@
|
|
|
9
9
|
* bundle's own list.
|
|
10
10
|
*/
|
|
11
11
|
import { spawnSync } from 'node:child_process';
|
|
12
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync,
|
|
12
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
13
13
|
import { createRequire } from 'node:module';
|
|
14
|
-
import { fileURLToPath } from 'node:url';
|
|
15
14
|
import { homedir } from 'node:os';
|
|
16
15
|
import { dirname, join, posix, resolve, win32 } from 'node:path';
|
|
17
16
|
import { parseDocument } from 'yaml';
|
|
@@ -99,12 +98,6 @@ function requireStringArray(value, label) {
|
|
|
99
98
|
throw new Error(label + ' must be a string array');
|
|
100
99
|
return value;
|
|
101
100
|
}
|
|
102
|
-
/** Read one non-empty string field. */
|
|
103
|
-
function requireString(value, label) {
|
|
104
|
-
if (typeof value !== 'string' || value.trim() === '')
|
|
105
|
-
throw new Error(label + ' must be a non-empty string');
|
|
106
|
-
return value;
|
|
107
|
-
}
|
|
108
101
|
/** Resolve the DSH home, honouring the environment override the launcher itself reads. */
|
|
109
102
|
export function resolveHome(env = process.env) {
|
|
110
103
|
const configured = env.DSH_HOME;
|
|
@@ -143,15 +136,12 @@ export function isWithin(candidate, parent, platform = process.platform) {
|
|
|
143
136
|
* @returns absolute path to the installed distribution.
|
|
144
137
|
*/
|
|
145
138
|
export function resolveDistributionDirectory(anchor) {
|
|
146
|
-
const ownDirectory = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
147
139
|
let resolved;
|
|
148
140
|
try {
|
|
149
141
|
resolved = dirname(createRequire(anchor).resolve('@sparkelf/dsh-plus/package.json'));
|
|
150
142
|
}
|
|
151
143
|
catch {
|
|
152
|
-
|
|
153
|
-
// tree has no copy to resolve. The running module's own package is the distribution.
|
|
154
|
-
return ownDirectory;
|
|
144
|
+
throw new Error('@sparkelf/dsh-plus is not installed in ' + dirname(anchor) + '; run this command from the directory that installed it');
|
|
155
145
|
}
|
|
156
146
|
// A resolution that leaves the consumer tree means the module answered from its own
|
|
157
147
|
// installation, which reports health for a distribution the consumer never installed.
|
|
@@ -167,14 +157,21 @@ export function resolveDistributionDirectory(anchor) {
|
|
|
167
157
|
break;
|
|
168
158
|
current = parent;
|
|
169
159
|
}
|
|
170
|
-
|
|
160
|
+
throw new Error('@sparkelf/dsh-plus resolved outside ' + resolve(dirname(anchor)) + ' (found ' + resolved + '); run this command from the directory that installed it');
|
|
171
161
|
}
|
|
162
|
+
/** The reviewed bundle order and pins the installed distribution declares. */
|
|
172
163
|
export function readDistributionProfile(distributionDirectory) {
|
|
173
164
|
const manifest = requireRecord(JSON.parse(readFileSync(join(distributionDirectory, 'package.json'), 'utf8')), 'Plus distribution manifest');
|
|
174
165
|
const plus = requireRecord(manifest.dshPlus, 'dshPlus');
|
|
175
166
|
const profile = requireRecord(plus.profile, 'dshPlus.profile');
|
|
176
|
-
const
|
|
167
|
+
const rawDependencies = requireRecord(profile.dependencies, 'dshPlus.profile.dependencies');
|
|
177
168
|
const rawAllowBuilds = requireRecord(profile.allowBuilds, 'dshPlus.profile.allowBuilds');
|
|
169
|
+
const dependencies = {};
|
|
170
|
+
for (const [name, spec] of Object.entries(rawDependencies)) {
|
|
171
|
+
if (typeof spec !== 'string' || spec === '')
|
|
172
|
+
throw new Error('dshPlus.profile.dependencies.' + name + ' must be a non-empty string');
|
|
173
|
+
dependencies[name] = spec;
|
|
174
|
+
}
|
|
178
175
|
const allowBuilds = {};
|
|
179
176
|
for (const [name, allowed] of Object.entries(rawAllowBuilds)) {
|
|
180
177
|
if (typeof allowed !== 'boolean')
|
|
@@ -193,16 +190,6 @@ export function readDistributionProfile(distributionDirectory) {
|
|
|
193
190
|
throw new Error('dshPlus.profile.overrides.' + name + ' must be a non-empty string');
|
|
194
191
|
overrides[name] = spec;
|
|
195
192
|
}
|
|
196
|
-
const editions = {};
|
|
197
|
-
const rawEditions = plus.editions === undefined ? {} : requireRecord(plus.editions, 'dshPlus.editions');
|
|
198
|
-
for (const [id, value] of Object.entries(rawEditions)) {
|
|
199
|
-
const edition = requireRecord(value, 'dshPlus.editions.' + id);
|
|
200
|
-
editions[id] = {
|
|
201
|
-
label: requireString(edition.label, 'dshPlus.editions.' + id + '.label'),
|
|
202
|
-
bundles: requireStringArray(edition.bundles, 'dshPlus.editions.' + id + '.bundles'),
|
|
203
|
-
packages: requireDependencyMap(requireRecord(edition.packages, 'dshPlus.editions.' + id + '.packages'), 'dshPlus.editions.' + id + '.packages'),
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
193
|
const compatibility = requireRecord(plus.compatibility, 'dshPlus.compatibility');
|
|
207
194
|
return {
|
|
208
195
|
name: String(manifest.name),
|
|
@@ -210,21 +197,10 @@ export function readDistributionProfile(distributionDirectory) {
|
|
|
210
197
|
dependencies,
|
|
211
198
|
allowBuilds,
|
|
212
199
|
overrides,
|
|
213
|
-
editions,
|
|
214
200
|
dshRange: String(compatibility.dsh),
|
|
215
201
|
version: String(manifest.version),
|
|
216
202
|
};
|
|
217
203
|
}
|
|
218
|
-
/** Read one package-name-to-spec map, rejecting empty entries. */
|
|
219
|
-
function requireDependencyMap(value, label) {
|
|
220
|
-
const dependencies = {};
|
|
221
|
-
for (const [name, spec] of Object.entries(value)) {
|
|
222
|
-
if (typeof spec !== 'string' || spec === '')
|
|
223
|
-
throw new Error(label + '.' + name + ' must be a non-empty string');
|
|
224
|
-
dependencies[name] = spec;
|
|
225
|
-
}
|
|
226
|
-
return dependencies;
|
|
227
|
-
}
|
|
228
204
|
/**
|
|
229
205
|
* Give the profile its own installed tree, built from the distribution's declarations.
|
|
230
206
|
*
|
|
@@ -246,12 +222,7 @@ function requireDependencyMap(value, label) {
|
|
|
246
222
|
export function installProfilePackages(paths, consumerDirectory) {
|
|
247
223
|
const consumerModules = join(consumerDirectory, 'node_modules');
|
|
248
224
|
if (!existsSync(consumerModules)) {
|
|
249
|
-
|
|
250
|
-
// beside it. The profile installs its own tree, so there is nothing to link from
|
|
251
|
-
// the caller; only the profile's packages matter.
|
|
252
|
-
installWithRegistryFallback(paths);
|
|
253
|
-
alignReplacedPackageNames(join(paths.profileDirectory, 'node_modules'));
|
|
254
|
-
return;
|
|
225
|
+
throw new Error('no node_modules in ' + consumerDirectory + '; run npm install there first');
|
|
255
226
|
}
|
|
256
227
|
const profileModules = join(paths.profileDirectory, 'node_modules');
|
|
257
228
|
if (existsSync(profileModules)) {
|
|
@@ -276,18 +247,10 @@ export function installProfilePackages(paths, consumerDirectory) {
|
|
|
276
247
|
function installWithRegistryFallback(paths) {
|
|
277
248
|
const registries = registryOrder();
|
|
278
249
|
for (const [index, registry] of registries.entries()) {
|
|
279
|
-
// A lockfile written against the previous registry keeps its resolved URLs, so the
|
|
280
|
-
// second attempt would fetch from the registry that just failed. Drop it and resolve
|
|
281
|
-
// against the registry being tried.
|
|
282
|
-
if (index > 0)
|
|
283
|
-
rmSync(join(paths.profileDirectory, 'pnpm-lock.yaml'), { force: true });
|
|
284
250
|
const result = spawnSync('pnpm', ['install', '--no-frozen-lockfile', '--registry', registry], {
|
|
285
251
|
cwd: paths.profileDirectory,
|
|
286
252
|
stdio: 'inherit',
|
|
287
253
|
shell: process.platform === 'win32',
|
|
288
|
-
// A registry that accepts the connection but never answers would otherwise hold the
|
|
289
|
-
// first start for pnpm's full retry budget before the fallback got a chance.
|
|
290
|
-
timeout: 5 * 60_000,
|
|
291
254
|
});
|
|
292
255
|
if (result.error !== undefined)
|
|
293
256
|
throw result.error;
|
|
@@ -350,12 +313,8 @@ const MAINLAND_REGISTRY = 'https://registry.npmmirror.com';
|
|
|
350
313
|
*/
|
|
351
314
|
export function registryOrder() {
|
|
352
315
|
const configured = process.env.DSH_PLUS_INSTALL_REGISTRY;
|
|
353
|
-
if (configured !== undefined && configured !== '')
|
|
354
|
-
|
|
355
|
-
// set to the mirror used to fall back to the same mirror and fail twice.
|
|
356
|
-
const fallback = configured === MAINLAND_REGISTRY ? OFFICIAL_REGISTRY : MAINLAND_REGISTRY;
|
|
357
|
-
return [configured, fallback];
|
|
358
|
-
}
|
|
316
|
+
if (configured !== undefined && configured !== '')
|
|
317
|
+
return [configured, MAINLAND_REGISTRY];
|
|
359
318
|
// Intl reports the system locale on every platform. The POSIX variables are empty on
|
|
360
319
|
// Windows, so reading them alone classified every Windows console as non-mainland and
|
|
361
320
|
// reached the origin first — measured in a consumer's log, which showed npmjs failing
|
|
@@ -472,113 +431,12 @@ export function resolvePaths(anchor, env = process.env) {
|
|
|
472
431
|
*
|
|
473
432
|
* @param paths - resolved standalone paths.
|
|
474
433
|
* @param consumerDirectory - directory whose `node_modules` holds the installed packages.
|
|
475
|
-
* @param edition - edition id whose package list to install, or undefined for the default.
|
|
476
434
|
* @returns whether this call created the manifest.
|
|
477
435
|
*/
|
|
478
|
-
export function ensureProfile(paths, consumerDirectory
|
|
436
|
+
export function ensureProfile(paths, consumerDirectory) {
|
|
479
437
|
const manifestPath = join(paths.profileDirectory, 'package.json');
|
|
480
438
|
const distribution = readDistributionProfile(paths.distributionDirectory);
|
|
481
|
-
const composition = edition === undefined ? undefined : distribution.editions[edition];
|
|
482
|
-
if (edition !== undefined && composition === undefined) {
|
|
483
|
-
const offered = Object.keys(distribution.editions);
|
|
484
|
-
throw new Error('unknown edition "' + edition + '"; this release offers '
|
|
485
|
-
+ (offered.length === 0 ? '(none)' : offered.join(', ')));
|
|
486
|
-
}
|
|
487
|
-
// An edition lists only third-party, fork, and self-developed plugins. The official
|
|
488
|
-
// runtime is the CLI's responsibility and must not be duplicated in that list.
|
|
489
|
-
for (const name of Object.keys(composition?.packages ?? {})) {
|
|
490
|
-
if (name.startsWith('@deepseek-ai/')) {
|
|
491
|
-
throw new Error('edition "' + String(edition) + '" lists the official package ' + name
|
|
492
|
-
+ '; editions contain only third-party or self-developed plugins');
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
const runtime = distribution.dshRange.replace(/^>=?/u, '');
|
|
496
|
-
const baseBundles = composition === undefined
|
|
497
|
-
? distribution.bundles
|
|
498
|
-
: distribution.bundles.filter(bundle => bundle.startsWith('@deepseek-ai/') || bundle === '@sparkelf/dsh-plus');
|
|
499
|
-
const basePackages = composition === undefined
|
|
500
|
-
? distribution.dependencies
|
|
501
|
-
: {
|
|
502
|
-
'@deepseek-ai/dsh': distribution.dshRange,
|
|
503
|
-
'@sparkelf/dsh-plus': distribution.version,
|
|
504
|
-
// `dsh-app-boot` declares its launcher as peer dependencies, and pnpm does not
|
|
505
|
-
// install a profile's transitive peers. Without these the boot loader resolves
|
|
506
|
-
// `@deepseek-ai/cordis-plugin-group` and fails before any plugin mounts.
|
|
507
|
-
'@deepseek-ai/cordis': '^4.0.6-alpha.1',
|
|
508
|
-
'@deepseek-ai/cordis-plugin-group': '^1.0.6-alpha.1',
|
|
509
|
-
'@deepseek-ai/cordis-plugin-include': '^1.0.11-alpha.1',
|
|
510
|
-
'@deepseek-ai/cordis-plugin-loader': '^1.0.7-alpha.1',
|
|
511
|
-
'@deepseek-ai/dsh-home-paths': runtime,
|
|
512
|
-
'@deepseek-ai/dsh-launch-environment': runtime,
|
|
513
|
-
// `dsh-session` / `dsh-tools` declare the rest of the runtime as peers; pnpm leaves
|
|
514
|
-
// them out of a profile install for the same reason as the launcher peers above.
|
|
515
|
-
'@deepseek-ai/dsh-agent': runtime,
|
|
516
|
-
'@deepseek-ai/dsh-llm': runtime,
|
|
517
|
-
'@deepseek-ai/dsh-scope': runtime,
|
|
518
|
-
'@deepseek-ai/dsh-session': runtime,
|
|
519
|
-
'@deepseek-ai/dsh-system-prompt': runtime,
|
|
520
|
-
'@deepseek-ai/dsh-user-approval': runtime,
|
|
521
|
-
// Capability packages that the official plugins import but declare only as peers.
|
|
522
|
-
// A profile install leaves them out and boot then fails inside an otherwise
|
|
523
|
-
// healthy plugin's import with ERR_MODULE_NOT_FOUND.
|
|
524
|
-
'@deepseek-ai/dsh-attachment': runtime,
|
|
525
|
-
'@deepseek-ai/dsh-compaction': runtime,
|
|
526
|
-
'@deepseek-ai/dsh-fs': runtime,
|
|
527
|
-
'@deepseek-ai/dsh-shell': runtime,
|
|
528
|
-
'@deepseek-ai/dsh-spill': runtime,
|
|
529
|
-
'@deepseek-ai/dsh-sandbox': runtime,
|
|
530
|
-
'@deepseek-ai/dsh-ptc-runtime': runtime,
|
|
531
|
-
'@deepseek-ai/dsh-llm-deepseek': runtime,
|
|
532
|
-
'@deepseek-ai/dsh-deepseek-account': runtime,
|
|
533
|
-
'@deepseek-ai/dsh-anonymous-user-id': runtime,
|
|
534
|
-
'@deepseek-ai/dsh-bash-local': runtime,
|
|
535
|
-
'@deepseek-ai/dsh-client-store': runtime,
|
|
536
|
-
'@deepseek-ai/dsh-jobs': runtime,
|
|
537
|
-
'@deepseek-ai/dsh-sdk-protocol': runtime,
|
|
538
|
-
'@deepseek-ai/dsh-session-persistence': runtime,
|
|
539
|
-
'@deepseek-ai/dsh-session-telemetry': runtime,
|
|
540
|
-
'@deepseek-ai/dsh-session-title-llm': runtime,
|
|
541
|
-
'@deepseek-ai/dsh-util-workspace-path': runtime,
|
|
542
|
-
'@deepseek-ai/dsh-workflow': runtime,
|
|
543
|
-
...Object.fromEntries(baseBundles
|
|
544
|
-
.filter(bundle => bundle.startsWith('@deepseek-ai/dsh-'))
|
|
545
|
-
.map(bundle => [bundle, runtime])),
|
|
546
|
-
};
|
|
547
|
-
const compositionPackages = composition === undefined
|
|
548
|
-
? basePackages
|
|
549
|
-
: { ...basePackages, ...composition.packages };
|
|
550
|
-
// Official cordis rows name the historical unscoped packages. The fork ships under
|
|
551
|
-
// `@sparkelf/*`, so the row would fail to resolve unless the profile also carries the
|
|
552
|
-
// historical name as an alias to the same version.
|
|
553
|
-
const rowAliases = {
|
|
554
|
-
'dsh-better-sidebar': '@sparkelf/dsh-better-sidebar',
|
|
555
|
-
'dsh-sql-workbench': '@sparkelf/dsh-sql-workbench',
|
|
556
|
-
};
|
|
557
|
-
for (const [alias, target] of Object.entries(rowAliases)) {
|
|
558
|
-
const spec = compositionPackages[target];
|
|
559
|
-
if (spec !== undefined && compositionPackages[alias] === undefined) {
|
|
560
|
-
compositionPackages[alias] = 'npm:' + target + '@' + spec;
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
const compositionBundles = composition === undefined
|
|
564
|
-
? baseBundles
|
|
565
|
-
: [...baseBundles, ...composition.bundles];
|
|
566
439
|
if (existsSync(manifestPath)) {
|
|
567
|
-
// A selected edition is an explicit install choice, so its packages and bundle order
|
|
568
|
-
// replace the profile's rather than being ignored. Other profile decisions stay.
|
|
569
|
-
if (composition !== undefined) {
|
|
570
|
-
const manifest = requireRecord(JSON.parse(readFileSync(manifestPath, 'utf8')), 'Plus profile manifest');
|
|
571
|
-
manifest.dependencies = { ...manifest.dependencies, ...compositionPackages };
|
|
572
|
-
manifest.dsh = {
|
|
573
|
-
...manifest.dsh,
|
|
574
|
-
profile: {
|
|
575
|
-
...manifest.dsh?.profile,
|
|
576
|
-
bundles: [...compositionBundles],
|
|
577
|
-
...(edition === undefined ? {} : { plusEdition: edition }),
|
|
578
|
-
},
|
|
579
|
-
};
|
|
580
|
-
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
581
|
-
}
|
|
582
440
|
// The workspace carries decisions the distribution owns — overrides and the build
|
|
583
441
|
// script allowlist — and a distribution release changes them. Rewriting on every
|
|
584
442
|
// start is what lets an upgraded installation receive the new values; a profile
|
|
@@ -592,12 +450,18 @@ export function ensureProfile(paths, consumerDirectory, edition) {
|
|
|
592
450
|
name: 'dsh-profile-' + STANDALONE_PROFILE,
|
|
593
451
|
private: true,
|
|
594
452
|
type: 'module',
|
|
595
|
-
dependencies:
|
|
453
|
+
dependencies: {
|
|
454
|
+
'@sparkelf/dsh-plus': distribution.version,
|
|
455
|
+
// The launcher's own tree supplies every service package the bundles mount. A
|
|
456
|
+
// profile that lists only the distribution's plugins installs a partial tree and
|
|
457
|
+
// fails at load with a module the launcher would have carried.
|
|
458
|
+
'@deepseek-ai/dsh': distribution.dshRange,
|
|
459
|
+
...distribution.dependencies,
|
|
460
|
+
},
|
|
596
461
|
dsh: {
|
|
597
462
|
profile: {
|
|
598
|
-
bundles:
|
|
463
|
+
bundles: distribution.bundles,
|
|
599
464
|
patchReload: 'live',
|
|
600
|
-
...(edition === undefined ? {} : { plusEdition: edition }),
|
|
601
465
|
},
|
|
602
466
|
},
|
|
603
467
|
};
|
|
@@ -642,7 +506,7 @@ function writeProfileOverrides(profileDirectory, overrides, allowBuilds, omitted
|
|
|
642
506
|
if (document.get('nodeLinker') === undefined)
|
|
643
507
|
document.set('nodeLinker', 'hoisted');
|
|
644
508
|
if (document.get('autoInstallPeers') === undefined)
|
|
645
|
-
document.set('autoInstallPeers',
|
|
509
|
+
document.set('autoInstallPeers', false);
|
|
646
510
|
writeFileSync(workspacePath, String(document));
|
|
647
511
|
}
|
|
648
512
|
/** Run git in one directory, returning undefined instead of throwing when asked to. */
|
|
@@ -676,8 +540,6 @@ export function applyProfileNpmPatches(distributionDirectory, profileDirectory)
|
|
|
676
540
|
const patchSet = readPatchSet(distributionDirectory, manifest);
|
|
677
541
|
const applied = [];
|
|
678
542
|
for (const entry of [...patchSet.entries, ...patchSet.thirdPartyPatches]) {
|
|
679
|
-
// Only npm targets reach an installed package; a source target needs the
|
|
680
|
-
// official checkout, which a standalone installation does not have.
|
|
681
543
|
if (entry.target.kind !== 'npm')
|
|
682
544
|
continue;
|
|
683
545
|
const targetName = entry.target.name;
|
|
@@ -698,22 +560,7 @@ function resolveInstalledPackage(from, packageName) {
|
|
|
698
560
|
manifestPath = requireFrom.resolve(packageName + '/package.json');
|
|
699
561
|
}
|
|
700
562
|
catch {
|
|
701
|
-
|
|
702
|
-
// up to the directory that holds the manifest.
|
|
703
|
-
let directory;
|
|
704
|
-
try {
|
|
705
|
-
directory = dirname(requireFrom.resolve(packageName));
|
|
706
|
-
}
|
|
707
|
-
catch {
|
|
708
|
-
throw new Error(packageName + ' is not installed under ' + from);
|
|
709
|
-
}
|
|
710
|
-
while (!existsSync(resolve(directory, 'package.json'))) {
|
|
711
|
-
const parent = dirname(directory);
|
|
712
|
-
if (parent === directory)
|
|
713
|
-
throw new Error(packageName + ' has no resolvable package manifest under ' + from);
|
|
714
|
-
directory = parent;
|
|
715
|
-
}
|
|
716
|
-
manifestPath = resolve(directory, 'package.json');
|
|
563
|
+
throw new Error(packageName + ' is not installed under ' + from);
|
|
717
564
|
}
|
|
718
565
|
const manifest = requireRecord(JSON.parse(readFileSync(manifestPath, 'utf8')), packageName + ' manifest');
|
|
719
566
|
return { name: packageName, version: String(manifest.version), directory: dirname(manifestPath), manifest };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sparkelf/dsh-plus",
|
|
3
3
|
"description": "DeepSeek Harness Plus official-base Web distribution profile and ranged compatibility patchset",
|
|
4
|
-
"version": "0.2.2-alpha.2.
|
|
4
|
+
"version": "0.2.2-alpha.2.30",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -514,6 +514,21 @@
|
|
|
514
514
|
"file": "./patches/ui-settings-compact-phone.patch",
|
|
515
515
|
"retireWhen": "官方 ui-settings-general 为窄屏提供手机布局后退役"
|
|
516
516
|
},
|
|
517
|
+
{
|
|
518
|
+
"id": "ui-schedule-plaza-tab",
|
|
519
|
+
"dsh": ">=0.2.2-alpha.2",
|
|
520
|
+
"target": {
|
|
521
|
+
"kind": "dsh-source",
|
|
522
|
+
"paths": [
|
|
523
|
+
"packages/client/ui-schedule/"
|
|
524
|
+
],
|
|
525
|
+
"packages": [
|
|
526
|
+
"@deepseek-ai/dsh-client-ui-schedule"
|
|
527
|
+
]
|
|
528
|
+
},
|
|
529
|
+
"file": "./patches/ui-schedule-plaza-tab.patch",
|
|
530
|
+
"retireWhen": "官方 ui-schedule 自动化任务页自行声明并渲染 schedule.manager.tabs 座位后退役"
|
|
531
|
+
},
|
|
517
532
|
{
|
|
518
533
|
"id": "web-runtime-compat",
|
|
519
534
|
"dsh": ">=0.2.2-alpha.2",
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
diff --git a/packages/client/ui-schedule/src/client/TaskManagerPage.module.css b/packages/client/ui-schedule/src/client/TaskManagerPage.module.css
|
|
2
|
+
index 7c4acf5a3d..ea9d29e2e3 100644
|
|
3
|
+
--- a/packages/client/ui-schedule/src/client/TaskManagerPage.module.css
|
|
4
|
+
+++ b/packages/client/ui-schedule/src/client/TaskManagerPage.module.css
|
|
5
|
+
@@ -149,6 +149,12 @@
|
|
6
|
+
neutral hairline, the 10px radius, and the mock's `--muted` leading icon. The
|
|
7
|
+
mock brightens the border on hover and takes the brand stroke on focus
|
|
8
|
+
rather than drawing a ring around the control. */
|
|
9
|
+
+/* The runtime extension tab keeps the page's centered column, so a contributor
|
|
10
|
+
+ body lines up with the search field and the task rows. */
|
|
11
|
+
+.plazaBody {
|
|
12
|
+
+ min-width: 0;
|
|
13
|
+
+}
|
|
14
|
+
+
|
|
15
|
+
.searchTools {
|
|
16
|
+
--task-search-height: 36px;
|
|
17
|
+
display: flex;
|
|
18
|
+
diff --git a/packages/client/ui-schedule/src/client/TaskManagerPage.tsx b/packages/client/ui-schedule/src/client/TaskManagerPage.tsx
|
|
19
|
+
index 3c330c9f26..f7e7b78312 100644
|
|
20
|
+
--- a/packages/client/ui-schedule/src/client/TaskManagerPage.tsx
|
|
21
|
+
+++ b/packages/client/ui-schedule/src/client/TaskManagerPage.tsx
|
|
22
|
+
@@ -5,7 +5,7 @@ import {
|
|
23
|
+
Button, IconClockOutlineRegular, IconCloseOutlineRegular, IconEllipsisOutlineRegular, IconPlusOutlineRegular,
|
|
24
|
+
IconSearchOutlineRegular, IconTrashOutlineRegular, Input, Menu, Modal, Tooltip,
|
|
25
|
+
} from '@deepseek-ai/dsh-client-ui-primitives'
|
|
26
|
+
-import type { HostObservable, InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
27
|
+
+import type { HostObservable, InjectFace, PropsLocale, PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
28
|
+
import type {} from '@deepseek-ai/dsh-client-ui-layout/client'
|
|
29
|
+
import type { ScheduleCatalogEntry, ScheduleId } from '@deepseek-ai/dsh-schedule/client'
|
|
30
|
+
import type { CatalogSnapshot } from './catalog-source.ts'
|
|
31
|
+
@@ -36,14 +36,26 @@ export interface TaskManagerInjected extends TaskDetailInjected {
|
|
32
|
+
* @returns resolution after the outcome is reported.
|
|
33
|
+
*/
|
|
34
|
+
readonly onClearEnded: () => Promise<void>
|
|
35
|
+
+ /**
|
|
36
|
+
+ * Whether a plugin currently contributes the page's runtime extension tab.
|
|
37
|
+
+ *
|
|
38
|
+
+ * The page reads this while rendering, so a contributor that registers after
|
|
39
|
+
+ * the page mounted still makes its tab appear on the next render; with no
|
|
40
|
+
+ * contributor the page keeps exactly its three status tabs.
|
|
41
|
+
+ * @returns true when the `schedule.manager.tabs` seat has an occupant.
|
|
42
|
+
+ */
|
|
43
|
+
+ readonly plazaAvailable?: () => boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Root-scoped task catalog props derived from the framework and injected actions. */
|
|
47
|
+
export type TaskManagerPageProps = PropsRuntime<'main'>
|
|
48
|
+
& InjectFace<TaskManagerInjected>
|
|
49
|
+
& PropsLocale<'schedule.manager'>
|
|
50
|
+
+ // Narrowed to the one seat this page declares; optional so a unit test can
|
|
51
|
+
+ // render the page without a slot host, as every other page-level test does.
|
|
52
|
+
+ & Partial<PropsRenderSlots<'schedule.manager.tabs'>>
|
|
53
|
+
|
|
54
|
+
-type StatusFilter = 'all' | ScheduleCatalogEntry['status']
|
|
55
|
+
+type StatusFilter = 'all' | ScheduleCatalogEntry['status'] | 'plaza'
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Render retained tasks with authoritative deletion and timing-only edits.
|
|
59
|
+
@@ -51,7 +63,7 @@ type StatusFilter = 'all' | ScheduleCatalogEntry['status']
|
|
60
|
+
* @returns the searchable task list beside the selected task's detail.
|
|
61
|
+
*/
|
|
62
|
+
export function TaskManagerPage(props: TaskManagerPageProps) {
|
|
63
|
+
- const { useCatalog, onNewTask, onClearEnded, onRetry, t } = props
|
|
64
|
+
+ const { useCatalog, onNewTask, onClearEnded, onRetry, plazaAvailable, renderSlot, t } = props
|
|
65
|
+
const catalog = useCatalog(snapshot => snapshot)
|
|
66
|
+
const { records, status } = catalog
|
|
67
|
+
const [search, setSearch] = useState('')
|
|
68
|
+
@@ -85,7 +97,7 @@ export function TaskManagerPage(props: TaskManagerPageProps) {
|
|
69
|
+
const rows = useMemo(() => {
|
|
70
|
+
const query = search.trim().toLowerCase()
|
|
71
|
+
return records.filter(record => (
|
|
72
|
+
- (statusFilter === 'all' || record.status === statusFilter)
|
|
73
|
+
+ (statusFilter === 'all' || (statusFilter !== 'plaza' && record.status === statusFilter))
|
|
74
|
+
&& (record.prompt.toLowerCase().includes(query)
|
|
75
|
+
|| record.sessionId.toLowerCase().includes(query)
|
|
76
|
+
|| taskName(record).toLowerCase().includes(query))
|
|
77
|
+
@@ -202,8 +214,24 @@ export function TaskManagerPage(props: TaskManagerPageProps) {
|
|
78
|
+
{t(value === 'all' ? 'statusFilter.all' : `status.${value}`)}
|
|
79
|
+
</button>
|
|
80
|
+
))}
|
|
81
|
+
+ {plazaAvailable?.() === true && (
|
|
82
|
+
+ <button
|
|
83
|
+
+ type="button"
|
|
84
|
+
+ className={clsx(css.filterTab, statusFilter === 'plaza' && css.filterTabActive)}
|
|
85
|
+
+ aria-pressed={statusFilter === 'plaza'}
|
|
86
|
+
+ data-testid="task-manager-plaza-tab"
|
|
87
|
+
+ onClick={() => { setStatusFilter('plaza') }}
|
|
88
|
+
+ >
|
|
89
|
+
+ {t('statusFilter.plaza')}
|
|
90
|
+
+ </button>
|
|
91
|
+
+ )}
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
+ {statusFilter === 'plaza' ? (
|
|
95
|
+
+ <div className={css.plazaBody} data-testid="task-manager-plaza-body">
|
|
96
|
+
+ {renderSlot?.('schedule.manager.tabs', {})}
|
|
97
|
+
+ </div>
|
|
98
|
+
+ ) : (<>
|
|
99
|
+
<div className={css.searchTools}>
|
|
100
|
+
<div className={css.searchField}>
|
|
101
|
+
<Input
|
|
102
|
+
@@ -299,6 +327,7 @@ export function TaskManagerPage(props: TaskManagerPageProps) {
|
|
103
|
+
})}
|
|
104
|
+
</ul>
|
|
105
|
+
</div>
|
|
106
|
+
+ </>)}
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
diff --git a/packages/client/ui-schedule/src/client/index.ts b/packages/client/ui-schedule/src/client/index.ts
|
|
111
|
+
index 7dcbc32ee3..cd072150e7 100644
|
|
112
|
+
--- a/packages/client/ui-schedule/src/client/index.ts
|
|
113
|
+
+++ b/packages/client/ui-schedule/src/client/index.ts
|
|
114
|
+
@@ -73,6 +73,21 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
115
|
+
/** Host task management page and task tab copy. */
|
|
116
|
+
'schedule.manager': TaskManagerKey
|
|
117
|
+
}
|
|
118
|
+
+ interface SlotMap {
|
|
119
|
+
+ /**
|
|
120
|
+
+ * One extension body contributed at runtime to the automation-task page.
|
|
121
|
+
+ *
|
|
122
|
+
+ * The page renders the single occupant as an additional status tab and
|
|
123
|
+
+ * supplies no owner props: a contributor owns its own data and needs no
|
|
124
|
+
+ * identity from the page. The seat belongs to no Session, so opening the
|
|
125
|
+
+ * page never mounts a contributor against an arbitrary conversation.
|
|
126
|
+
+ */
|
|
127
|
+
+ 'schedule.manager.tabs': {
|
|
128
|
+
+ kind: 'single'
|
|
129
|
+
+ scope: 'root'
|
|
130
|
+
+ owner: Record<string, never>
|
|
131
|
+
+ }
|
|
132
|
+
+ }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Required services for catalogs, ambient Session marks, the right Sidebar, Remote queries, original-Session navigation, and access. */
|
|
136
|
+
@@ -197,8 +212,13 @@ export function apply(ctx: ClientContext): void {
|
|
137
|
+
name: 'main',
|
|
138
|
+
key: PANEL_ID,
|
|
139
|
+
locale: MANAGER_NS,
|
|
140
|
+
+ // The automation page is the one first-level page that accepts a runtime
|
|
141
|
+
+ // extension tab. Declaring the seat here is what lets a plugin contribute
|
|
142
|
+
+ // a tab body without this package importing that plugin.
|
|
143
|
+
+ children: { 'schedule.manager.tabs': { kind: 'single', scope: 'root' } },
|
|
144
|
+
inject: (): TaskManagerInjected => ({
|
|
145
|
+
...detail,
|
|
146
|
+
+ plazaAvailable: () => ctx.slots.entriesOfSlot('schedule.manager.tabs').length > 0,
|
|
147
|
+
// The page has no creation form: a new reminder starts in a Session.
|
|
148
|
+
onNewTask: () => { ctx.uiWorkspace.startSession() },
|
|
149
|
+
onClearEnded: async () => {
|
|
150
|
+
diff --git a/packages/client/ui-schedule/src/client/task-manager-locales.ts b/packages/client/ui-schedule/src/client/task-manager-locales.ts
|
|
151
|
+
index fffde14568..145b05a825 100644
|
|
152
|
+
--- a/packages/client/ui-schedule/src/client/task-manager-locales.ts
|
|
153
|
+
+++ b/packages/client/ui-schedule/src/client/task-manager-locales.ts
|
|
154
|
+
@@ -20,6 +20,7 @@ export const en = {
|
|
155
|
+
'empty.action': 'New automation task',
|
|
156
|
+
'statusFilter.label': 'Task status',
|
|
157
|
+
'statusFilter.all': 'All',
|
|
158
|
+
+ 'statusFilter.plaza': 'Workflow plaza',
|
|
159
|
+
'status.active': 'Enabled',
|
|
160
|
+
'status.inactive': 'Inactive',
|
|
161
|
+
'detail.label': 'Task details',
|
|
162
|
+
@@ -176,6 +177,7 @@ export const zh: Record<TaskManagerKey, string> = {
|
|
163
|
+
'empty.action': '新建自动化任务',
|
|
164
|
+
'statusFilter.label': '任务状态',
|
|
165
|
+
'statusFilter.all': '全部',
|
|
166
|
+
+ 'statusFilter.plaza': '工作流广场',
|
|
167
|
+
'status.active': '已开启',
|
|
168
|
+
'status.inactive': '已结束',
|
|
169
|
+
'detail.label': '任务详情',
|
|
170
|
+
@@ -327,6 +329,7 @@ export const ja = {
|
|
171
|
+
'empty.action': '新規自動化タスク',
|
|
172
|
+
'statusFilter.label': 'タスクの状態',
|
|
173
|
+
'statusFilter.all': 'すべて',
|
|
174
|
+
+ 'statusFilter.plaza': 'ワークフロープラザ',
|
|
175
|
+
'status.active': '有効',
|
|
176
|
+
'status.inactive': '無効',
|
|
177
|
+
'detail.label': 'タスクの詳細',
|
|
178
|
+
@@ -476,6 +479,7 @@ export const es = {
|
|
179
|
+
'empty.action': 'Nueva tarea de automatización',
|
|
180
|
+
'statusFilter.label': 'Estado de la tarea',
|
|
181
|
+
'statusFilter.all': 'Todos',
|
|
182
|
+
+ 'statusFilter.plaza': 'Plaza de flujos',
|
|
183
|
+
'status.active': 'Habilitado',
|
|
184
|
+
'status.inactive': 'Inactivo',
|
|
185
|
+
'detail.label': 'Detalles de la tarea',
|
|
186
|
+
@@ -628,6 +632,7 @@ export const fr = {
|
|
187
|
+
'empty.action': "Nouvelle tâche d'automatisation",
|
|
188
|
+
'statusFilter.label': 'Statut de la tâche',
|
|
189
|
+
'statusFilter.all': 'Tout',
|
|
190
|
+
+ 'statusFilter.plaza': 'Place des workflows',
|
|
191
|
+
'status.active': 'Activé',
|
|
192
|
+
'status.inactive': 'Inactif',
|
|
193
|
+
'detail.label': 'Détails de la tâche',
|
|
194
|
+
@@ -780,6 +785,7 @@ export const de = {
|
|
195
|
+
'empty.action': 'Neue Automatisierungsaufgabe',
|
|
196
|
+
'statusFilter.label': 'Aufgabenstatus',
|
|
197
|
+
'statusFilter.all': 'Alle',
|
|
198
|
+
+ 'statusFilter.plaza': 'Workflow-Plaza',
|
|
199
|
+
'status.active': 'Aktiviert',
|
|
200
|
+
'status.inactive': 'Inaktiv',
|
|
201
|
+
'detail.label': 'Aufgabendetails',
|
|
202
|
+
@@ -932,6 +938,7 @@ export const ru = {
|
|
203
|
+
'empty.action': 'Новая задача автоматизации',
|
|
204
|
+
'statusFilter.label': 'Статус задачи',
|
|
205
|
+
'statusFilter.all': 'Все',
|
|
206
|
+
+ 'statusFilter.plaza': 'Витрина сценариев',
|
|
207
|
+
'status.active': 'Включено',
|
|
208
|
+
'status.inactive': 'Неактивный',
|
|
209
|
+
'detail.label': 'Подробности задачи',
|
|
210
|
+
@@ -1084,6 +1091,7 @@ export const pt = {
|
|
211
|
+
'empty.action': 'Nova tarefa de automação',
|
|
212
|
+
'statusFilter.label': 'Status da tarefa',
|
|
213
|
+
'statusFilter.all': 'Todos',
|
|
214
|
+
+ 'statusFilter.plaza': 'Praça de fluxos',
|
|
215
|
+
'status.active': 'Habilitado',
|
|
216
|
+
'status.inactive': 'Inativo',
|
|
217
|
+
'detail.label': 'Detalhes da tarefa',
|