@jingyi0605/codingns4dsh 0.1.0 → 0.1.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.
- package/README.en.md +306 -0
- package/README.md +190 -993
- package/data/build/dist/client/account-bar.d.ts +2 -2
- package/data/build/dist/client/account-bar.js +96 -39
- package/data/build/dist/client/bundle.js +13722 -3611
- package/data/build/dist/client/cli-catalog.js +20 -3
- package/data/build/dist/client/cli-slots.d.ts +5 -0
- package/data/build/dist/client/cli-slots.js +57 -7
- package/data/build/dist/client/debug/ui.d.ts +1 -1
- package/data/build/dist/client/debug/ui.js +62 -32
- package/data/build/dist/client/dsh-runtime-version.d.ts +9 -2
- package/data/build/dist/client/dsh-runtime-version.js +24 -2
- package/data/build/dist/client/features/cli-adapters.d.ts +1 -1
- package/data/build/dist/client/features/cli-adapters.js +19 -16
- package/data/build/dist/client/features/debug.js +2 -2
- package/data/build/dist/client/features/index.d.ts +2 -1
- package/data/build/dist/client/features/index.js +3 -1
- package/data/build/dist/client/features/lan-access-panel.d.ts +1 -1
- package/data/build/dist/client/features/lan-access-panel.js +8 -10
- package/data/build/dist/client/features/lan-access.js +1 -1
- package/data/build/dist/client/features/login-protection-panel.d.ts +1 -1
- package/data/build/dist/client/features/login-protection-panel.js +7 -9
- package/data/build/dist/client/features/reverse-proxy-panel.d.ts +1 -1
- package/data/build/dist/client/features/reverse-proxy-panel.js +99 -11
- package/data/build/dist/client/features/reverse-proxy.d.ts +1 -1
- package/data/build/dist/client/features/reverse-proxy.js +19 -18
- package/data/build/dist/client/features/terminal-enhancement-panel.d.ts +1 -1
- package/data/build/dist/client/features/terminal-enhancement-panel.js +62 -55
- package/data/build/dist/client/features/terminal-enhancement.d.ts +1 -1
- package/data/build/dist/client/features/terminal-enhancement.js +3 -3
- package/data/build/dist/client/features/types.d.ts +14 -3
- package/data/build/dist/client/features/workspace-session-enhancement-panel.d.ts +1 -1
- package/data/build/dist/client/features/workspace-session-enhancement-panel.js +31 -9
- package/data/build/dist/client/features/workspace-session-enhancement.js +72 -11
- package/data/build/dist/client/git-management.d.ts +16 -0
- package/data/build/dist/client/git-management.js +757 -0
- package/data/build/dist/client/index.d.ts +3 -2
- package/data/build/dist/client/index.js +152 -27
- package/data/build/dist/client/locale.js +91 -29
- package/data/build/dist/client/quick-phrase-layout.d.ts +49 -0
- package/data/build/dist/client/quick-phrase-layout.js +95 -0
- package/data/build/dist/client/quick-phrase-slot.d.ts +7 -0
- package/data/build/dist/client/quick-phrase-slot.js +370 -0
- package/data/build/dist/client/remote-web-context.js +1 -1
- package/data/build/dist/client/settings-bridge.d.ts +9 -6
- package/data/build/dist/client/settings-bridge.js +51 -27
- package/data/build/dist/client/settings-section.d.ts +3 -2
- package/data/build/dist/client/settings-section.js +33 -19
- package/data/build/dist/client/subscription-slot.d.ts +3 -0
- package/data/build/dist/client/subscription-slot.js +108 -16
- package/data/build/dist/client/terminal/model.d.ts +14 -2
- package/data/build/dist/client/terminal/model.js +109 -21
- package/data/build/dist/client/terminal/recovery.d.ts +50 -0
- package/data/build/dist/client/terminal/recovery.js +101 -0
- package/data/build/dist/client/terminal/ui.d.ts +2 -2
- package/data/build/dist/client/terminal/ui.js +49 -11
- package/data/build/dist/client/terminal/xterm-view.d.ts +2 -2
- package/data/build/dist/client/terminal/xterm-view.js +2 -1
- package/data/build/dist/client/theme.d.ts +6 -1
- package/data/build/dist/client/theme.js +32 -1
- package/data/build/dist/client/workspace-session-archive-dom.d.ts +12 -0
- package/data/build/dist/client/workspace-session-archive-dom.js +27 -5
- package/data/build/dist/client/workspace-session-visibility-dom.d.ts +30 -0
- package/data/build/dist/client/workspace-session-visibility-dom.js +604 -0
- package/data/build/dist/dsh-capabilities/client/config-forms-adapter.d.ts +50 -0
- package/data/build/dist/dsh-capabilities/client/config-forms-adapter.js +139 -0
- package/data/build/dist/dsh-capabilities/client/conversation-adapter.d.ts +7 -0
- package/data/build/dist/dsh-capabilities/client/conversation-adapter.js +6 -0
- package/data/build/dist/dsh-capabilities/client/locale-adapter.d.ts +9 -0
- package/data/build/dist/dsh-capabilities/client/locale-adapter.js +4 -0
- package/data/build/dist/dsh-capabilities/client/primitives-adapter.d.ts +10 -0
- package/data/build/dist/dsh-capabilities/client/primitives-adapter.js +14 -0
- package/data/build/dist/dsh-capabilities/client/settings-scope-adapter.d.ts +8 -0
- package/data/build/dist/dsh-capabilities/client/settings-scope-adapter.js +16 -0
- package/data/build/dist/dsh-capabilities/client/sidebar-adapter.d.ts +7 -0
- package/data/build/dist/dsh-capabilities/client/sidebar-adapter.js +5 -0
- package/data/build/dist/dsh-capabilities/client/theme-adapter.d.ts +6 -0
- package/data/build/dist/dsh-capabilities/client/theme-adapter.js +4 -0
- package/data/build/dist/dsh-capabilities/client/typert-adapter.d.ts +5 -0
- package/data/build/dist/dsh-capabilities/client/typert-adapter.js +4 -0
- package/data/build/dist/dsh-capabilities/host/config-forms-adapter.d.ts +19 -0
- package/data/build/dist/dsh-capabilities/host/config-forms-adapter.js +45 -0
- package/data/build/dist/dsh-capabilities/host/connection-rpc-adapter.d.ts +19 -0
- package/data/build/dist/dsh-capabilities/host/connection-rpc-adapter.js +13 -0
- package/data/build/dist/dsh-capabilities/host/settings-scope-adapter.d.ts +8 -0
- package/data/build/dist/dsh-capabilities/host/settings-scope-adapter.js +47 -0
- package/data/build/dist/dsh-capabilities/host/typert-adapter.d.ts +6 -0
- package/data/build/dist/dsh-capabilities/host/typert-adapter.js +4 -0
- package/data/build/dist/dsh-capabilities/index.d.ts +6 -0
- package/data/build/dist/dsh-capabilities/index.js +6 -0
- package/data/build/dist/dsh-capabilities/matrix.d.ts +15 -0
- package/data/build/dist/dsh-capabilities/matrix.js +32 -0
- package/data/build/dist/dsh-capabilities/registry.d.ts +22 -0
- package/data/build/dist/dsh-capabilities/registry.js +196 -0
- package/data/build/dist/dsh-capabilities/routes.d.ts +10 -0
- package/data/build/dist/dsh-capabilities/routes.js +34 -0
- package/data/build/dist/dsh-capabilities/settings-store.d.ts +29 -0
- package/data/build/dist/dsh-capabilities/settings-store.js +5 -0
- package/data/build/dist/dsh-capabilities/types.d.ts +50 -0
- package/data/build/dist/dsh-capabilities/types.js +23 -0
- package/data/build/dist/features/registry.d.ts +8 -2
- package/data/build/dist/features/registry.js +104 -1
- package/data/build/dist/host/auth-session.d.ts +8 -1
- package/data/build/dist/host/auth-session.js +28 -6
- package/data/build/dist/host/cli-adapters/claude-driver.d.ts +2 -0
- package/data/build/dist/host/cli-adapters/claude-driver.js +19 -4
- package/data/build/dist/host/cli-adapters/claude-model-options.d.ts +14 -0
- package/data/build/dist/host/cli-adapters/claude-model-options.js +157 -0
- package/data/build/dist/host/cli-adapters/codex-driver.d.ts +11 -0
- package/data/build/dist/host/cli-adapters/codex-driver.js +166 -14
- package/data/build/dist/host/cli-adapters/driver.d.ts +2 -0
- package/data/build/dist/host/cli-adapters/dsh-message-projector.d.ts +15 -0
- package/data/build/dist/host/cli-adapters/dsh-message-projector.js +120 -29
- package/data/build/dist/host/cli-adapters/dsh-tool-history.d.ts +4 -1
- package/data/build/dist/host/cli-adapters/dsh-tool-history.js +25 -7
- package/data/build/dist/host/cli-adapters/feature.js +74 -8
- package/data/build/dist/host/cli-adapters/gemini-driver.js +2 -2
- package/data/build/dist/host/cli-adapters/grok-driver.js +2 -2
- package/data/build/dist/host/cli-adapters/json-rpc-process.d.ts +4 -2
- package/data/build/dist/host/cli-adapters/json-rpc-process.js +8 -9
- package/data/build/dist/host/cli-adapters/kimi-driver.js +1 -1
- package/data/build/dist/host/cli-adapters/legacy-session-adapter.d.ts +17 -0
- package/data/build/dist/host/cli-adapters/legacy-session-adapter.js +88 -0
- package/data/build/dist/host/cli-adapters/legacy-session-settings.d.ts +24 -0
- package/data/build/dist/host/cli-adapters/legacy-session-settings.js +162 -0
- package/data/build/dist/host/cli-adapters/official-provider-subscription.d.ts +29 -0
- package/data/build/dist/host/cli-adapters/official-provider-subscription.js +361 -0
- package/data/build/dist/host/cli-adapters/opencode-driver.d.ts +2 -0
- package/data/build/dist/host/cli-adapters/opencode-driver.js +97 -12
- package/data/build/dist/host/cli-adapters/pi-driver.js +69 -23
- package/data/build/dist/host/cli-adapters/provider-registry.d.ts +33 -0
- package/data/build/dist/host/cli-adapters/provider-registry.js +83 -0
- package/data/build/dist/host/cli-adapters/provider-subscription.d.ts +25 -3
- package/data/build/dist/host/cli-adapters/provider-subscription.js +346 -30
- package/data/build/dist/host/cli-adapters/registry.d.ts +6 -0
- package/data/build/dist/host/cli-adapters/registry.js +170 -13
- package/data/build/dist/host/cli-adapters/rpc-driver-utils.js +10 -0
- package/data/build/dist/host/cli-adapters/session-store.d.ts +7 -0
- package/data/build/dist/host/cli-adapters/session-store.js +39 -1
- package/data/build/dist/host/cli-adapters/standard-stream-driver.d.ts +3 -0
- package/data/build/dist/host/cli-adapters/stream-normalizer.d.ts +3 -0
- package/data/build/dist/host/cli-adapters/stream-normalizer.js +22 -2
- package/data/build/dist/host/control-api-client.d.ts +3 -3
- package/data/build/dist/host/control-api-client.js +2 -1
- package/data/build/dist/host/credential-store.js +11 -1
- package/data/build/dist/host/dsh-device-runtime.d.ts +4 -0
- package/data/build/dist/host/dsh-device-runtime.js +20 -9
- package/data/build/dist/host/features/auth.js +8 -5
- package/data/build/dist/host/features/debug.js +1 -1
- package/data/build/dist/host/features/git-management.d.ts +5 -0
- package/data/build/dist/host/features/git-management.js +290 -0
- package/data/build/dist/host/features/host-status.js +1 -1
- package/data/build/dist/host/features/index.d.ts +1 -0
- package/data/build/dist/host/features/index.js +3 -0
- package/data/build/dist/host/features/terminal-process.js +1 -1
- package/data/build/dist/host/features/terminal-status.d.ts +3 -1
- package/data/build/dist/host/features/terminal-status.js +3 -1
- package/data/build/dist/host/index.d.ts +7 -3
- package/data/build/dist/host/index.js +76 -11
- package/data/build/dist/host/lan-access-dsh.js +18 -5
- package/data/build/dist/host/native-session-bridge.d.ts +23 -1
- package/data/build/dist/host/native-session-bridge.js +157 -10
- package/data/build/dist/host/rpc-table.d.ts +1 -1
- package/data/build/dist/host/rpc.js +60 -10
- package/data/build/dist/host/session-migration-repair.d.ts +19 -0
- package/data/build/dist/host/session-migration-repair.js +395 -0
- package/data/build/dist/host/settings.d.ts +8 -1
- package/data/build/dist/host/settings.js +94 -3
- package/data/build/dist/host/terminal/backends/tmux-backend.d.ts +1 -0
- package/data/build/dist/host/terminal/backends/tmux-backend.js +4 -2
- package/data/build/dist/host/terminal/controller-factory.d.ts +4 -0
- package/data/build/dist/host/terminal/controller-factory.js +26 -7
- package/data/build/dist/host/terminal/runtime-manager.d.ts +2 -1
- package/data/build/dist/host/terminal/runtime-manager.js +3 -0
- package/data/build/dist/host/terminal/startup.d.ts +2 -0
- package/data/build/dist/host/terminal/startup.js +2 -1
- package/data/build/dist/host/terminal/terminal-controller.d.ts +10 -3
- package/data/build/dist/host/terminal/terminal-controller.js +101 -15
- package/data/build/dist/host/terminal/terminal-process-service.js +3 -1
- package/data/build/dist/host/terminal/terminal-service.d.ts +2 -0
- package/data/build/dist/host/terminal/terminal-service.js +10 -0
- package/data/build/dist/index.d.ts +6 -2
- package/data/build/dist/index.js +8 -2
- package/data/build/dist/shared/contracts/cli-adapter.d.ts +14 -0
- package/data/build/dist/shared/contracts/config.d.ts +31 -1
- package/data/build/dist/shared/contracts/config.js +25 -0
- package/data/build/dist/shared/contracts/dsh-device.d.ts +13 -1
- package/data/build/dist/shared/contracts/errors.d.ts +1 -1
- package/data/build/dist/shared/contracts/feature.d.ts +6 -0
- package/data/build/dist/shared/contracts/git.d.ts +82 -0
- package/data/build/dist/shared/contracts/git.js +3 -0
- package/data/build/dist/shared/contracts/subscription.d.ts +46 -0
- package/data/build/dist/shared/contracts/terminal.d.ts +4 -1
- package/data/build/dist/shared/contracts/version.d.ts +2 -2
- package/data/build/dist/shared/contracts/version.js +4 -6
- package/data/build/dist/shared/debug.d.ts +14 -0
- package/data/build/dist/shared/debug.js +53 -0
- package/data/build/dist/shared/index.d.ts +7 -4
- package/data/build/dist/shared/index.js +2 -1
- package/data/build/dist/transport/debug.d.ts +1 -1
- package/data/build/dist/transport/debug.js +3 -30
- package/data/build/dist/transport/dsh-session.js +3 -3
- package/data/build/dist/typert.host.js +4 -2
- package/data/build/dist/typert.remote-client.d.ts +11 -0
- package/data/build/dist/typert.remote-client.js +7 -0
- package/dsh.bundle.patch +1 -1
- package/package.json +17 -5
- package/profile/README.md +4 -4
- package/profile/package.json +6 -4
- package/profile/scripts/check-dsh-install.mjs +2 -2
- package/profile/version.json +2 -2
- package/scripts/check-dsh-install.mjs +2 -2
- package/version.json +2 -2
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { createCapabilityProfile, } from './types.js';
|
|
2
|
+
export class DshCapabilityRegistryError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
capability;
|
|
5
|
+
constructor(code, message, capability = null, options) {
|
|
6
|
+
super(message, options);
|
|
7
|
+
this.name = 'DshCapabilityRegistryError';
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.capability = capability;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** 集中解析 DSH 能力路由;Profile 生成后不可变,避免运行期替换半初始化服务。 */
|
|
13
|
+
export class DshCapabilityRegistry {
|
|
14
|
+
dshVersion;
|
|
15
|
+
runtime;
|
|
16
|
+
routes = new Map();
|
|
17
|
+
profile;
|
|
18
|
+
constructor(dshVersion, runtime) {
|
|
19
|
+
this.dshVersion = dshVersion;
|
|
20
|
+
this.runtime = runtime;
|
|
21
|
+
}
|
|
22
|
+
register(route) {
|
|
23
|
+
validateRoute(route);
|
|
24
|
+
if (route.runtime !== this.runtime)
|
|
25
|
+
return;
|
|
26
|
+
const list = this.routes.get(route.capability) ?? [];
|
|
27
|
+
if (list.some((entry) => entry.id === route.id)) {
|
|
28
|
+
throw new DshCapabilityRegistryError('CAPABILITY_INVALID_ROUTE', `重复的能力路由: ${route.id}`, route.capability);
|
|
29
|
+
}
|
|
30
|
+
list.push(route);
|
|
31
|
+
this.routes.set(route.capability, list);
|
|
32
|
+
this.profile = undefined;
|
|
33
|
+
}
|
|
34
|
+
registerMany(routes) {
|
|
35
|
+
for (const route of routes)
|
|
36
|
+
this.register(route);
|
|
37
|
+
}
|
|
38
|
+
resolve(context) {
|
|
39
|
+
if (this.profile !== undefined)
|
|
40
|
+
return this.profile;
|
|
41
|
+
const resolutions = new Map();
|
|
42
|
+
const diagnostics = [];
|
|
43
|
+
for (const [capability, routes] of this.routes) {
|
|
44
|
+
const candidates = routes
|
|
45
|
+
.filter((route) => isVersionInRange(this.dshVersion, route.supportedDsh))
|
|
46
|
+
.sort((left, right) => routeRank(right) - routeRank(left));
|
|
47
|
+
if (candidates.length === 0) {
|
|
48
|
+
const reason = `DSH ${this.dshVersion} 没有能力 ${capability} 的适配路由`;
|
|
49
|
+
resolutions.set(capability, unavailable(capability, this.dshVersion, reason));
|
|
50
|
+
diagnostics.push({ code: 'CAPABILITY_VERSION_UNSUPPORTED', capability, dshVersion: this.dshVersion, message: reason });
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const topPriority = routeRank(candidates[0]);
|
|
54
|
+
const top = candidates.filter((route) => routeRank(route) === topPriority);
|
|
55
|
+
if (top.length > 1) {
|
|
56
|
+
throw new DshCapabilityRegistryError('CAPABILITY_ROUTE_CONFLICT', `能力 ${capability} 存在同优先级路由: ${top.map((route) => route.id).join(', ')}`, capability);
|
|
57
|
+
}
|
|
58
|
+
let selected;
|
|
59
|
+
for (const route of candidates) {
|
|
60
|
+
try {
|
|
61
|
+
if (route.detect(context)) {
|
|
62
|
+
selected = route;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
diagnostics.push({
|
|
68
|
+
code: 'CAPABILITY_DETECT_FAILED',
|
|
69
|
+
capability,
|
|
70
|
+
routeId: route.id,
|
|
71
|
+
dshVersion: this.dshVersion,
|
|
72
|
+
message: `${route.id} 探测失败: ${errorMessage(error)}`,
|
|
73
|
+
...(route.replacement === undefined ? {} : { replacement: route.replacement }),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (selected === undefined) {
|
|
78
|
+
const reason = `DSH ${this.dshVersion} 的能力 ${capability} 没有可用运行时实现`;
|
|
79
|
+
resolutions.set(capability, unavailable(capability, this.dshVersion, reason));
|
|
80
|
+
diagnostics.push({ code: 'CAPABILITY_UNAVAILABLE', capability, dshVersion: this.dshVersion, message: reason });
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const value = selected.create(context);
|
|
85
|
+
const resolution = {
|
|
86
|
+
capability,
|
|
87
|
+
routeId: selected.id,
|
|
88
|
+
dshVersion: this.dshVersion,
|
|
89
|
+
status: selected.status === 'deprecated' ? 'degraded' : 'ready',
|
|
90
|
+
value,
|
|
91
|
+
...(selected.replacement === undefined ? {} : { replacement: selected.replacement }),
|
|
92
|
+
};
|
|
93
|
+
resolutions.set(capability, resolution);
|
|
94
|
+
if (selected.status === 'deprecated') {
|
|
95
|
+
diagnostics.push({
|
|
96
|
+
code: 'CAPABILITY_DEPRECATED',
|
|
97
|
+
capability,
|
|
98
|
+
routeId: selected.id,
|
|
99
|
+
dshVersion: this.dshVersion,
|
|
100
|
+
message: `能力 ${capability} 使用已弃用路由 ${selected.id}`,
|
|
101
|
+
...(selected.replacement === undefined ? {} : { replacement: selected.replacement }),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
const reason = `${selected.id} 创建失败: ${errorMessage(error)}`;
|
|
107
|
+
resolutions.set(capability, unavailable(capability, this.dshVersion, reason));
|
|
108
|
+
diagnostics.push({ code: 'CAPABILITY_CREATE_FAILED', capability, routeId: selected.id, dshVersion: this.dshVersion, message: reason });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
this.profile = createCapabilityProfile(this.dshVersion, this.runtime, resolutions, diagnostics);
|
|
112
|
+
return this.profile;
|
|
113
|
+
}
|
|
114
|
+
getProfile(context) {
|
|
115
|
+
return this.resolve(context);
|
|
116
|
+
}
|
|
117
|
+
require(profile, capability) {
|
|
118
|
+
const resolution = profile.capabilities.get(capability);
|
|
119
|
+
if (resolution?.value === undefined || resolution.status === 'unavailable') {
|
|
120
|
+
throw new DshCapabilityRegistryError('CAPABILITY_UNAVAILABLE', resolution?.reason ?? `能力不可用: ${capability}`, capability);
|
|
121
|
+
}
|
|
122
|
+
return resolution.value;
|
|
123
|
+
}
|
|
124
|
+
explain(profile, capability) {
|
|
125
|
+
if (capability === undefined)
|
|
126
|
+
return profile.diagnostics;
|
|
127
|
+
return profile.diagnostics.filter((item) => item.capability === capability);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function validateRoute(route) {
|
|
131
|
+
if (route.id.trim() === '' || route.capability === undefined || route.supportedDsh.trim() === '') {
|
|
132
|
+
throw new DshCapabilityRegistryError('CAPABILITY_INVALID_ROUTE', '能力路由必须包含 id、capability 和 supportedDsh');
|
|
133
|
+
}
|
|
134
|
+
if (!Number.isFinite(route.priority))
|
|
135
|
+
throw new DshCapabilityRegistryError('CAPABILITY_INVALID_ROUTE', `能力路由优先级非法: ${route.id}`, route.capability);
|
|
136
|
+
if (typeof route.detect !== 'function' || typeof route.create !== 'function')
|
|
137
|
+
throw new DshCapabilityRegistryError('CAPABILITY_INVALID_ROUTE', `能力路由缺少探测或工厂: ${route.id}`, route.capability);
|
|
138
|
+
}
|
|
139
|
+
function routeRank(route) {
|
|
140
|
+
return route.priority + (route.status === 'supported' ? 1_000_000 : 0);
|
|
141
|
+
}
|
|
142
|
+
function unavailable(capability, dshVersion, reason) {
|
|
143
|
+
return { capability, dshVersion, status: 'unavailable', reason };
|
|
144
|
+
}
|
|
145
|
+
function isVersionInRange(version, range) {
|
|
146
|
+
const match = /^>=([^ ]+) <=([^ ]+)$/u.exec(range);
|
|
147
|
+
if (match === null)
|
|
148
|
+
return false;
|
|
149
|
+
const actual = parseVersion(version);
|
|
150
|
+
const minimum = parseVersion(match[1]);
|
|
151
|
+
const maximum = parseVersion(match[2]);
|
|
152
|
+
if (actual === undefined || minimum === undefined || maximum === undefined)
|
|
153
|
+
return false;
|
|
154
|
+
return compareVersions(actual, minimum) >= 0 && compareVersions(actual, maximum) <= 0;
|
|
155
|
+
}
|
|
156
|
+
function parseVersion(value) {
|
|
157
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/u.exec(value);
|
|
158
|
+
if (match === null)
|
|
159
|
+
return undefined;
|
|
160
|
+
return {
|
|
161
|
+
major: Number(match[1]),
|
|
162
|
+
minor: Number(match[2]),
|
|
163
|
+
patch: Number(match[3]),
|
|
164
|
+
pre: match[4] === undefined ? [] : match[4].split('.').map((part) => /^\d+$/u.test(part) ? Number(part) : part),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function compareVersions(left, right) {
|
|
168
|
+
for (const key of ['major', 'minor', 'patch']) {
|
|
169
|
+
if (left[key] !== right[key])
|
|
170
|
+
return left[key] - right[key];
|
|
171
|
+
}
|
|
172
|
+
if (left.pre.length === 0 && right.pre.length > 0)
|
|
173
|
+
return 1;
|
|
174
|
+
if (left.pre.length > 0 && right.pre.length === 0)
|
|
175
|
+
return -1;
|
|
176
|
+
for (let index = 0; index < Math.max(left.pre.length, right.pre.length); index += 1) {
|
|
177
|
+
const a = left.pre[index];
|
|
178
|
+
const b = right.pre[index];
|
|
179
|
+
if (a === undefined)
|
|
180
|
+
return -1;
|
|
181
|
+
if (b === undefined)
|
|
182
|
+
return 1;
|
|
183
|
+
if (a === b)
|
|
184
|
+
continue;
|
|
185
|
+
if (typeof a === 'number' && typeof b === 'string')
|
|
186
|
+
return -1;
|
|
187
|
+
if (typeof a === 'string' && typeof b === 'number')
|
|
188
|
+
return 1;
|
|
189
|
+
return a < b ? -1 : 1;
|
|
190
|
+
}
|
|
191
|
+
return 0;
|
|
192
|
+
}
|
|
193
|
+
function errorMessage(error) {
|
|
194
|
+
return error instanceof Error ? error.message : String(error);
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { DshCapabilityRegistry } from './registry.js';
|
|
2
|
+
import type { DshCapabilityRuntime } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* 注册 Codingns4DSH 当前实际消费的 DSH 服务能力。
|
|
5
|
+
*
|
|
6
|
+
* 探测只检查结构,不读取版本字符串;版本范围由 Registry 统一处理,避免
|
|
7
|
+
* 业务模块在运行时继续堆叠 if (version >= ...)。
|
|
8
|
+
*/
|
|
9
|
+
export declare function createDshCapabilityRegistry(dshVersion: string, runtime: DshCapabilityRuntime, context: unknown): DshCapabilityRegistry;
|
|
10
|
+
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { DshCapabilityRegistry } from './registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* 注册 Codingns4DSH 当前实际消费的 DSH 服务能力。
|
|
4
|
+
*
|
|
5
|
+
* 探测只检查结构,不读取版本字符串;版本范围由 Registry 统一处理,避免
|
|
6
|
+
* 业务模块在运行时继续堆叠 if (version >= ...)。
|
|
7
|
+
*/
|
|
8
|
+
export function createDshCapabilityRegistry(dshVersion, runtime, context) {
|
|
9
|
+
const registry = new DshCapabilityRegistry(dshVersion, runtime);
|
|
10
|
+
const value = context;
|
|
11
|
+
const rangeLegacy = '>=0.1.5-rc.3 <=0.1.6';
|
|
12
|
+
const rangeModern = '>=0.1.7-rc.2 <=0.1.7-rc.2';
|
|
13
|
+
const add = (route) => registry.register(route);
|
|
14
|
+
if (runtime === 'host') {
|
|
15
|
+
add({ id: 'settings-scope', capability: 'settings.store', supportedDsh: rangeLegacy, runtime, priority: 10, status: 'supported', introducedIn: '0.1.5-rc.3', detect: (ctx) => typeof ctx.settings?.register === 'function', create: (ctx) => ctx.settings });
|
|
16
|
+
add({ id: 'config-settings', capability: 'settings.store', supportedDsh: rangeModern, runtime, priority: 20, status: 'supported', introducedIn: '0.1.7-rc.2', detect: (ctx) => typeof ctx.settings?.describe === 'function' && typeof ctx.settings?.mutate === 'function', create: (ctx) => ctx.settings });
|
|
17
|
+
add({ id: 'connection-rpc', capability: 'connection.rpc', supportedDsh: rangeLegacy, runtime, priority: 10, status: 'supported', introducedIn: '0.1.5-rc.3', detect: (ctx) => ctx.connection !== undefined, create: (ctx) => ctx.connection });
|
|
18
|
+
add({ id: 'connection-rpc-peer-aware', capability: 'connection.rpc', supportedDsh: rangeModern, runtime, priority: 20, status: 'supported', introducedIn: '0.1.7-rc.2', detect: (ctx) => ctx.connection !== undefined, create: (ctx) => ctx.connection });
|
|
19
|
+
add({ id: 'connection-peer', capability: 'connection.peer', supportedDsh: rangeModern, runtime, priority: 20, status: 'supported', introducedIn: '0.1.7-rc.2', detect: (ctx) => ctx.connection?.peer !== undefined, create: (ctx) => ctx.connection.peer });
|
|
20
|
+
add({ id: 'remote-result', capability: 'typert.remote', supportedDsh: '>=0.1.5-rc.3 <=0.1.7-rc.2', runtime, priority: 10, status: 'supported', introducedIn: '0.1.5-rc.3', detect: (ctx) => ctx.remote !== undefined, create: (ctx) => ctx.remote });
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
add({ id: 'settings-scope', capability: 'settings.store', supportedDsh: rangeLegacy, runtime, priority: 10, status: 'supported', introducedIn: '0.1.5-rc.3', detect: (ctx) => typeof ctx.settingsScope?.bind === 'function', create: (ctx) => ctx.settingsScope });
|
|
24
|
+
add({ id: 'config-form', capability: 'settings.store', supportedDsh: rangeModern, runtime, priority: 20, status: 'supported', introducedIn: '0.1.7-rc.2', detect: (ctx) => typeof ctx.configForms?.get === 'function', create: (ctx) => ctx.configForms });
|
|
25
|
+
add({ id: 'icon-primitives', capability: 'ui.icon.plus', supportedDsh: '>=0.1.5-rc.3 <=0.1.7-rc.2', runtime, priority: 10, status: 'supported', introducedIn: '0.1.5-rc.3', detect: () => value.primitives !== undefined, create: () => value.primitives });
|
|
26
|
+
add({ id: 'locale-runtime', capability: 'locale.runtime', supportedDsh: '>=0.1.5-rc.3 <=0.1.7-rc.2', runtime, priority: 10, status: 'supported', introducedIn: '0.1.5-rc.3', detect: (ctx) => ctx.locale !== undefined, create: (ctx) => ctx.locale });
|
|
27
|
+
add({ id: 'theme-runtime', capability: 'theme.runtime', supportedDsh: '>=0.1.5-rc.3 <=0.1.7-rc.2', runtime, priority: 10, status: 'supported', introducedIn: '0.1.5-rc.3', detect: (ctx) => ctx.theme !== undefined, create: (ctx) => ctx.theme });
|
|
28
|
+
add({ id: 'conversation-events', capability: 'conversation.tool-call', supportedDsh: '>=0.1.5-rc.3 <=0.1.7-rc.2', runtime, priority: 10, status: 'supported', introducedIn: '0.1.5-rc.3', detect: (ctx) => ctx.uiConversation !== undefined, create: (ctx) => ctx.uiConversation });
|
|
29
|
+
add({ id: 'sidebar-right', capability: 'sidebar.right', supportedDsh: '>=0.1.5-rc.3 <=0.1.7-rc.2', runtime, priority: 10, status: 'supported', introducedIn: '0.1.5-rc.3', detect: (ctx) => ctx.sidebarRight !== undefined, create: (ctx) => ctx.sidebarRight });
|
|
30
|
+
add({ id: 'remote-result', capability: 'typert.remote', supportedDsh: '>=0.1.5-rc.3 <=0.1.7-rc.2', runtime, priority: 10, status: 'supported', introducedIn: '0.1.5-rc.3', detect: (ctx) => ctx.remote !== undefined, create: (ctx) => ctx.remote });
|
|
31
|
+
}
|
|
32
|
+
return registry;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codingns4DSH 内部设置存储契约。
|
|
3
|
+
*
|
|
4
|
+
* 业务模块只依赖这个最小接口,不直接依赖 DSH 的 SettingsScope 或未来的
|
|
5
|
+
* ConfigForm。不同 DSH 版本的读写、revision 和订阅语义都在边界适配器中收敛。
|
|
6
|
+
*/
|
|
7
|
+
export interface CodingNsSettingsSnapshot<T> {
|
|
8
|
+
readonly value: T | undefined;
|
|
9
|
+
readonly revision: number | undefined;
|
|
10
|
+
readonly writable: boolean;
|
|
11
|
+
readonly status: 'loading' | 'ready' | 'unavailable';
|
|
12
|
+
}
|
|
13
|
+
export interface CodingNsSettingsOperation {
|
|
14
|
+
readonly op: 'set' | 'unset';
|
|
15
|
+
readonly path: readonly string[];
|
|
16
|
+
readonly value?: unknown;
|
|
17
|
+
}
|
|
18
|
+
export interface CodingNsSettingsStore<T> {
|
|
19
|
+
getSnapshot(): CodingNsSettingsSnapshot<T>;
|
|
20
|
+
subscribe(listener: () => void): () => void;
|
|
21
|
+
mutate(operations: readonly CodingNsSettingsOperation[], expectedRevision?: number): Promise<boolean>;
|
|
22
|
+
set(field: string, value: unknown): Promise<boolean>;
|
|
23
|
+
unset(field: string): Promise<boolean>;
|
|
24
|
+
load?(): Promise<void>;
|
|
25
|
+
dispose?(): void | Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
/** 将 DSH 两代写入结果统一成内部成功布尔值。 */
|
|
28
|
+
export declare function accepted(write: Promise<void | boolean>): Promise<boolean>;
|
|
29
|
+
//# sourceMappingURL=settings-store.d.ts.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** DSH 宿主能力的稳定标识。业务模块只依赖这些标识,不直接依赖 DSH 版本。 */
|
|
2
|
+
export type DshCapabilityId = 'settings.store' | 'connection.rpc' | 'connection.peer' | 'ui.icon.plus' | 'ui.icon.chevron' | 'locale.runtime' | 'theme.runtime' | 'conversation.tool-call' | 'sidebar.right' | 'typert.remote';
|
|
3
|
+
export type DshCapabilityRuntime = 'host' | 'client';
|
|
4
|
+
export type DshCapabilityRouteStatus = 'supported' | 'deprecated';
|
|
5
|
+
export type DshCapabilityResolutionStatus = 'ready' | 'degraded' | 'unavailable';
|
|
6
|
+
export interface DshCapabilityDiagnostic {
|
|
7
|
+
readonly code: string;
|
|
8
|
+
readonly capability: DshCapabilityId;
|
|
9
|
+
readonly routeId?: string;
|
|
10
|
+
readonly dshVersion: string;
|
|
11
|
+
readonly message: string;
|
|
12
|
+
readonly replacement?: DshCapabilityId;
|
|
13
|
+
readonly featureNames?: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
export interface DshCapabilityRoute<T> {
|
|
16
|
+
readonly id: string;
|
|
17
|
+
readonly capability: DshCapabilityId;
|
|
18
|
+
readonly supportedDsh: string;
|
|
19
|
+
readonly runtime: DshCapabilityRuntime;
|
|
20
|
+
readonly priority: number;
|
|
21
|
+
readonly detect: (context: unknown) => boolean;
|
|
22
|
+
readonly create: (context: unknown) => T;
|
|
23
|
+
readonly status: DshCapabilityRouteStatus;
|
|
24
|
+
readonly introducedIn: string;
|
|
25
|
+
readonly removableAfter?: string;
|
|
26
|
+
readonly replacement?: DshCapabilityId;
|
|
27
|
+
}
|
|
28
|
+
export interface DshCapabilityResolution<T = unknown> {
|
|
29
|
+
readonly capability: DshCapabilityId;
|
|
30
|
+
readonly routeId?: string;
|
|
31
|
+
readonly dshVersion: string;
|
|
32
|
+
readonly status: DshCapabilityResolutionStatus;
|
|
33
|
+
readonly value?: T;
|
|
34
|
+
readonly reason?: string;
|
|
35
|
+
readonly replacement?: DshCapabilityId;
|
|
36
|
+
}
|
|
37
|
+
export interface DshCapabilityProfile {
|
|
38
|
+
readonly dshVersion: string;
|
|
39
|
+
readonly runtime: DshCapabilityRuntime;
|
|
40
|
+
readonly capabilities: ReadonlyMap<DshCapabilityId, DshCapabilityResolution>;
|
|
41
|
+
readonly diagnostics: readonly DshCapabilityDiagnostic[];
|
|
42
|
+
readonly frozenAt: number;
|
|
43
|
+
}
|
|
44
|
+
export interface FeatureCapabilityRequirement {
|
|
45
|
+
readonly capability: DshCapabilityId;
|
|
46
|
+
readonly required: boolean;
|
|
47
|
+
readonly fallback?: 'disable' | 'degrade' | 'error';
|
|
48
|
+
}
|
|
49
|
+
export declare function createCapabilityProfile(dshVersion: string, runtime: DshCapabilityRuntime, capabilities: ReadonlyMap<DshCapabilityId, DshCapabilityResolution>, diagnostics: readonly DshCapabilityDiagnostic[]): DshCapabilityProfile;
|
|
50
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function createCapabilityProfile(dshVersion, runtime, capabilities, diagnostics) {
|
|
2
|
+
return Object.freeze({
|
|
3
|
+
dshVersion,
|
|
4
|
+
runtime,
|
|
5
|
+
capabilities: freezeMap(capabilities),
|
|
6
|
+
diagnostics: Object.freeze([...diagnostics]),
|
|
7
|
+
frozenAt: Date.now(),
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
/** Map 的只读视图,避免 Object.freeze(Map) 仍可通过 set() 修改。 */
|
|
11
|
+
function freezeMap(source) {
|
|
12
|
+
const map = new Map(source);
|
|
13
|
+
const view = new Proxy(map, {
|
|
14
|
+
get(target, property, receiver) {
|
|
15
|
+
if (property === 'set' || property === 'delete' || property === 'clear')
|
|
16
|
+
return undefined;
|
|
17
|
+
const value = Reflect.get(target, property, target);
|
|
18
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
return Object.freeze(view);
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { FeatureDescriptor, FeatureDisposer, FeatureModule, FeatureResourceScope, FeatureState } from '../shared/contracts/feature.js';
|
|
2
|
-
|
|
2
|
+
import type { DshCapabilityDiagnostic, DshCapabilityProfile } from '../dsh-capabilities/types.js';
|
|
3
|
+
export type FeatureRegistryErrorCode = 'FEATURE_INVALID_DESCRIPTOR' | 'FEATURE_ALREADY_REGISTERED' | 'FEATURE_NOT_FOUND' | 'FEATURE_DEPENDENCY_MISSING' | 'FEATURE_DEPENDENCY_CYCLE' | 'FEATURE_START_FAILED' | 'FEATURE_DISPOSE_FAILED' | 'FEATURE_STATE_INVALID' | 'FEATURE_CAPABILITY_MISSING';
|
|
3
4
|
export declare class FeatureRegistryError extends Error {
|
|
4
5
|
readonly code: FeatureRegistryErrorCode;
|
|
5
6
|
readonly featureName: string | null;
|
|
@@ -12,6 +13,7 @@ export interface FeatureSnapshot {
|
|
|
12
13
|
runtime: FeatureDescriptor['runtime'];
|
|
13
14
|
dependencies: readonly string[];
|
|
14
15
|
reason: string | null;
|
|
16
|
+
capabilities: readonly DshCapabilityDiagnostic[];
|
|
15
17
|
}
|
|
16
18
|
/**
|
|
17
19
|
* 管理功能模块的依赖、状态和资源所有权。
|
|
@@ -35,10 +37,12 @@ export declare class FeatureResourceScopeImpl implements FeatureResourceScope {
|
|
|
35
37
|
*/
|
|
36
38
|
export declare class FeatureRegistry<S = unknown, M extends FeatureModule<S> = FeatureModule<S>> {
|
|
37
39
|
private readonly services;
|
|
40
|
+
private readonly capabilityProfile?;
|
|
38
41
|
private readonly records;
|
|
39
42
|
private readonly operations;
|
|
43
|
+
private reconcileOperation;
|
|
40
44
|
/** @param services - 每个模块在 start 时通过 context.services 取用的服务集合。 */
|
|
41
|
-
constructor(services: S);
|
|
45
|
+
constructor(services: S, capabilityProfile?: DshCapabilityProfile | undefined);
|
|
42
46
|
register(module: M): void;
|
|
43
47
|
registerMany(modules: readonly M[]): void;
|
|
44
48
|
has(name: string): boolean;
|
|
@@ -61,12 +65,14 @@ export declare class FeatureRegistry<S = unknown, M extends FeatureModule<S> = F
|
|
|
61
65
|
* 并发调用按模块串行队列排队,最终状态由最后一次调用决定。
|
|
62
66
|
*/
|
|
63
67
|
reconcile(enabledNames: readonly string[]): Promise<void>;
|
|
68
|
+
private reconcileInternal;
|
|
64
69
|
drain(name: string): Promise<void>;
|
|
65
70
|
dispose(name: string): Promise<void>;
|
|
66
71
|
disable(name: string): Promise<void>;
|
|
67
72
|
/** 把期望集合补齐为含依赖的闭包,避免依赖在停用阶段被误停。 */
|
|
68
73
|
private resolveDesired;
|
|
69
74
|
private startInternal;
|
|
75
|
+
private checkCapabilities;
|
|
70
76
|
private activeDependents;
|
|
71
77
|
private getRecord;
|
|
72
78
|
private enqueue;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { debugInfo } from '../shared/debug.js';
|
|
1
2
|
export class FeatureRegistryError extends Error {
|
|
2
3
|
code;
|
|
3
4
|
featureName;
|
|
@@ -57,11 +58,14 @@ export class FeatureResourceScopeImpl {
|
|
|
57
58
|
*/
|
|
58
59
|
export class FeatureRegistry {
|
|
59
60
|
services;
|
|
61
|
+
capabilityProfile;
|
|
60
62
|
records = new Map();
|
|
61
63
|
operations = new Map();
|
|
64
|
+
reconcileOperation = Promise.resolve();
|
|
62
65
|
/** @param services - 每个模块在 start 时通过 context.services 取用的服务集合。 */
|
|
63
|
-
constructor(services) {
|
|
66
|
+
constructor(services, capabilityProfile) {
|
|
64
67
|
this.services = services;
|
|
68
|
+
this.capabilityProfile = capabilityProfile;
|
|
65
69
|
}
|
|
66
70
|
register(module) {
|
|
67
71
|
validateDescriptor(module?.descriptor);
|
|
@@ -76,6 +80,13 @@ export class FeatureRegistry {
|
|
|
76
80
|
reason: null,
|
|
77
81
|
resources,
|
|
78
82
|
context: { descriptor: module.descriptor, resources, services: this.services },
|
|
83
|
+
capabilities: [],
|
|
84
|
+
});
|
|
85
|
+
traceFeature('registered', {
|
|
86
|
+
feature: name,
|
|
87
|
+
runtime: module.descriptor.runtime,
|
|
88
|
+
dependencies: module.descriptor.dependencies,
|
|
89
|
+
enabledByDefault: module.descriptor.enabledByDefault,
|
|
79
90
|
});
|
|
80
91
|
}
|
|
81
92
|
registerMany(modules) {
|
|
@@ -130,6 +141,7 @@ export class FeatureRegistry {
|
|
|
130
141
|
};
|
|
131
142
|
for (const name of this.records.keys())
|
|
132
143
|
visit(name);
|
|
144
|
+
traceFeature('validated', { features: [...this.records.keys()] });
|
|
133
145
|
}
|
|
134
146
|
async start(name) {
|
|
135
147
|
return this.enqueue(name, async () => {
|
|
@@ -145,8 +157,18 @@ export class FeatureRegistry {
|
|
|
145
157
|
* 并发调用按模块串行队列排队,最终状态由最后一次调用决定。
|
|
146
158
|
*/
|
|
147
159
|
async reconcile(enabledNames) {
|
|
160
|
+
const operation = this.reconcileOperation.then(() => this.reconcileInternal(enabledNames), () => this.reconcileInternal(enabledNames));
|
|
161
|
+
this.reconcileOperation = operation.catch(() => undefined);
|
|
162
|
+
return operation;
|
|
163
|
+
}
|
|
164
|
+
async reconcileInternal(enabledNames) {
|
|
148
165
|
this.validate();
|
|
149
166
|
const desired = this.resolveDesired(enabledNames);
|
|
167
|
+
traceFeature('reconcile', {
|
|
168
|
+
requested: enabledNames,
|
|
169
|
+
desired: [...desired],
|
|
170
|
+
states: this.list().map((item) => ({ name: item.name, state: item.state, reason: item.reason })),
|
|
171
|
+
});
|
|
150
172
|
for (const name of this.records.keys()) {
|
|
151
173
|
if (desired.has(name))
|
|
152
174
|
await this.start(name);
|
|
@@ -243,6 +265,7 @@ export class FeatureRegistry {
|
|
|
243
265
|
const record = this.getRecord(name);
|
|
244
266
|
if (record.state === 'enabled')
|
|
245
267
|
return;
|
|
268
|
+
traceFeature('start.begin', { feature: name, state: record.state, dependencies: record.module.descriptor.dependencies });
|
|
246
269
|
if (record.state === 'enabling') {
|
|
247
270
|
throw new FeatureRegistryError('FEATURE_STATE_INVALID', `Feature is already enabling: ${name}`, name);
|
|
248
271
|
}
|
|
@@ -255,6 +278,13 @@ export class FeatureRegistry {
|
|
|
255
278
|
starting.add(name);
|
|
256
279
|
for (const dependency of record.module.descriptor.dependencies) {
|
|
257
280
|
await this.enqueue(dependency, () => this.startInternal(dependency, starting));
|
|
281
|
+
const dependencyRecord = this.getRecord(dependency);
|
|
282
|
+
if (dependencyRecord.state !== 'enabled') {
|
|
283
|
+
record.state = 'disabled';
|
|
284
|
+
record.reason = `依赖模块 ${dependency} 未启用`;
|
|
285
|
+
traceFeature('start.blocked', { feature: name, dependency, dependencyState: dependencyRecord.state });
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
258
288
|
}
|
|
259
289
|
starting.delete(name);
|
|
260
290
|
if (record.resources.disposed) {
|
|
@@ -263,10 +293,33 @@ export class FeatureRegistry {
|
|
|
263
293
|
}
|
|
264
294
|
record.state = 'enabling';
|
|
265
295
|
record.reason = null;
|
|
296
|
+
const capabilityCheck = this.checkCapabilities(record);
|
|
297
|
+
record.capabilities = capabilityCheck.diagnostics;
|
|
298
|
+
traceFeature('start.capabilities', {
|
|
299
|
+
feature: name,
|
|
300
|
+
action: capabilityCheck.action,
|
|
301
|
+
reason: capabilityCheck.reason ?? null,
|
|
302
|
+
diagnostics: capabilityCheck.diagnostics,
|
|
303
|
+
});
|
|
304
|
+
if (capabilityCheck.action === 'disable') {
|
|
305
|
+
record.state = 'disabled';
|
|
306
|
+
record.reason = capabilityCheck.reason ?? null;
|
|
307
|
+
traceFeature('start.disabled', { feature: name, reason: record.reason });
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (capabilityCheck.action === 'error') {
|
|
311
|
+
record.state = 'failed';
|
|
312
|
+
record.reason = capabilityCheck.reason ?? '能力不可用';
|
|
313
|
+
traceFeature('start.failed', { feature: name, reason: record.reason });
|
|
314
|
+
throw new FeatureRegistryError('FEATURE_CAPABILITY_MISSING', record.reason, name);
|
|
315
|
+
}
|
|
316
|
+
record.context = { ...record.context, capabilityDiagnostics: record.capabilities };
|
|
266
317
|
try {
|
|
318
|
+
traceFeature('start.module', { feature: name });
|
|
267
319
|
const returned = await record.module.start(record.context);
|
|
268
320
|
addReturnedDisposer(record.resources, returned);
|
|
269
321
|
record.state = 'enabled';
|
|
322
|
+
traceFeature('start.enabled', { feature: name });
|
|
270
323
|
}
|
|
271
324
|
catch (error) {
|
|
272
325
|
try {
|
|
@@ -277,9 +330,33 @@ export class FeatureRegistry {
|
|
|
277
330
|
}
|
|
278
331
|
record.state = 'failed';
|
|
279
332
|
record.reason = errorMessage(error);
|
|
333
|
+
traceFeature('start.failed', { feature: name, reason: record.reason, error: serializeError(error) });
|
|
280
334
|
throw new FeatureRegistryError('FEATURE_START_FAILED', `Failed to start feature ${name}`, name, { cause: error });
|
|
281
335
|
}
|
|
282
336
|
}
|
|
337
|
+
checkCapabilities(record) {
|
|
338
|
+
const requirements = record.module.descriptor.requires ?? [];
|
|
339
|
+
if (requirements.length === 0 || this.capabilityProfile === undefined)
|
|
340
|
+
return { action: 'start', diagnostics: [] };
|
|
341
|
+
const diagnostics = [];
|
|
342
|
+
for (const requirement of requirements) {
|
|
343
|
+
const resolution = this.capabilityProfile.capabilities.get(requirement.capability);
|
|
344
|
+
if (resolution?.status !== 'unavailable') {
|
|
345
|
+
if (resolution?.status === 'degraded')
|
|
346
|
+
diagnostics.push(...this.capabilityProfile.diagnostics.filter((item) => item.capability === requirement.capability));
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const diagnostic = this.capabilityProfile.diagnostics.find((item) => item.capability === requirement.capability)
|
|
350
|
+
?? { code: 'CAPABILITY_UNAVAILABLE', capability: requirement.capability, dshVersion: this.capabilityProfile.dshVersion, message: resolution?.reason ?? `能力不可用: ${requirement.capability}` };
|
|
351
|
+
diagnostics.push(diagnostic);
|
|
352
|
+
const fallback = requirement.fallback ?? (requirement.required ? 'error' : 'disable');
|
|
353
|
+
if (requirement.required || fallback === 'error')
|
|
354
|
+
return { action: 'error', reason: `${record.module.descriptor.name} 缺少能力 ${requirement.capability}: ${diagnostic.message}`, diagnostics };
|
|
355
|
+
if (fallback === 'disable')
|
|
356
|
+
return { action: 'disable', reason: `能力 ${requirement.capability} 不可用,模块已禁用`, diagnostics };
|
|
357
|
+
}
|
|
358
|
+
return { action: 'start', diagnostics };
|
|
359
|
+
}
|
|
283
360
|
activeDependents(name) {
|
|
284
361
|
const result = [];
|
|
285
362
|
for (const record of this.records.values()) {
|
|
@@ -308,6 +385,15 @@ export class FeatureRegistry {
|
|
|
308
385
|
return next;
|
|
309
386
|
}
|
|
310
387
|
}
|
|
388
|
+
function traceFeature(event, details) {
|
|
389
|
+
debugInfo(`codingns4dsh: feature.${event}`, details);
|
|
390
|
+
}
|
|
391
|
+
function serializeError(error) {
|
|
392
|
+
if (error instanceof Error) {
|
|
393
|
+
return { name: error.name, message: error.message, ...(error.stack === undefined ? {} : { stack: error.stack }) };
|
|
394
|
+
}
|
|
395
|
+
return { name: typeof error, message: String(error) };
|
|
396
|
+
}
|
|
311
397
|
function validateDescriptor(descriptor) {
|
|
312
398
|
if (!descriptor || typeof descriptor.name !== 'string' || descriptor.name.trim() === '') {
|
|
313
399
|
throw new FeatureRegistryError('FEATURE_INVALID_DESCRIPTOR', 'Feature descriptor requires a non-empty name');
|
|
@@ -328,6 +414,22 @@ function validateDescriptor(descriptor) {
|
|
|
328
414
|
&& (typeof descriptor.minimumDshVersion !== 'string' || descriptor.minimumDshVersion.trim() === '')) {
|
|
329
415
|
throw new FeatureRegistryError('FEATURE_INVALID_DESCRIPTOR', `Feature ${descriptor.name} has invalid minimumDshVersion`, descriptor.name);
|
|
330
416
|
}
|
|
417
|
+
if (descriptor.requires !== undefined) {
|
|
418
|
+
if (!Array.isArray(descriptor.requires))
|
|
419
|
+
throw new FeatureRegistryError('FEATURE_INVALID_DESCRIPTOR', `Feature ${descriptor.name} has invalid requires`, descriptor.name);
|
|
420
|
+
const seen = new Set();
|
|
421
|
+
for (const requirement of descriptor.requires) {
|
|
422
|
+
if (requirement === undefined || typeof requirement.capability !== 'string' || requirement.capability.trim() === '' || typeof requirement.required !== 'boolean') {
|
|
423
|
+
throw new FeatureRegistryError('FEATURE_INVALID_DESCRIPTOR', `Feature ${descriptor.name} has invalid capability requirement`, descriptor.name);
|
|
424
|
+
}
|
|
425
|
+
if (seen.has(requirement.capability))
|
|
426
|
+
throw new FeatureRegistryError('FEATURE_INVALID_DESCRIPTOR', `Feature ${descriptor.name} has duplicate capability requirement`, descriptor.name);
|
|
427
|
+
seen.add(requirement.capability);
|
|
428
|
+
if (requirement.fallback !== undefined && !['disable', 'degrade', 'error'].includes(requirement.fallback)) {
|
|
429
|
+
throw new FeatureRegistryError('FEATURE_INVALID_DESCRIPTOR', `Feature ${descriptor.name} has invalid capability fallback`, descriptor.name);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
331
433
|
if (!Array.isArray(descriptor.dependencies) || descriptor.dependencies.some((dependency) => typeof dependency !== 'string' || dependency.trim() === '')) {
|
|
332
434
|
throw new FeatureRegistryError('FEATURE_INVALID_DESCRIPTOR', `Feature ${descriptor.name} has invalid dependencies`, descriptor.name);
|
|
333
435
|
}
|
|
@@ -374,6 +476,7 @@ function snapshot(record) {
|
|
|
374
476
|
runtime: record.module.descriptor.runtime,
|
|
375
477
|
dependencies: [...record.module.descriptor.dependencies],
|
|
376
478
|
reason: record.reason,
|
|
479
|
+
capabilities: [...record.capabilities],
|
|
377
480
|
};
|
|
378
481
|
}
|
|
379
482
|
function errorMessage(error) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AuthDeviceManagementSnapshotDto, CodingNsAuthLoginResult, CodingNsAuthSessionSnapshot, HostBindRequest, LoginByEmailRequest, TunnelBindingSummary } from '../shared/contracts/auth.js';
|
|
2
|
-
import type
|
|
2
|
+
import { type CodingNsControlApiClient } from './control-api-client.js';
|
|
3
3
|
import type { RelaySignalingTicketResponse } from '../shared/contracts/signaling.js';
|
|
4
4
|
import type { CodingNsCredentialStore } from './credential-store.js';
|
|
5
5
|
/**
|
|
@@ -13,6 +13,7 @@ export declare class CodingNsAuthSession {
|
|
|
13
13
|
private state;
|
|
14
14
|
private accessToken;
|
|
15
15
|
private credential;
|
|
16
|
+
private refreshPromise;
|
|
16
17
|
constructor(client: CodingNsControlApiClient, credentials: CodingNsCredentialStore, controlBaseUrl: string);
|
|
17
18
|
snapshot(): CodingNsAuthSessionSnapshot;
|
|
18
19
|
/**
|
|
@@ -22,10 +23,16 @@ export declare class CodingNsAuthSession {
|
|
|
22
23
|
getAccessToken(): string | null;
|
|
23
24
|
/** 仅供 Host 内部设备运行时调用,Client 不会拿到控制面客户端。 */
|
|
24
25
|
getControlClient(): CodingNsControlApiClient;
|
|
26
|
+
/**
|
|
27
|
+
* 执行需要 access token 的请求;服务端拒绝旧 token 时只续期一次并重试。
|
|
28
|
+
* refresh token 仍然只留在 Host,调用方无需复制认证恢复逻辑。
|
|
29
|
+
*/
|
|
30
|
+
withAccessToken<T>(operation: (accessToken: string) => Promise<T>): Promise<T>;
|
|
25
31
|
/** 使用保存的 refresh token 恢复会话;没有凭据时保持 logged_out。 */
|
|
26
32
|
restore(): Promise<CodingNsAuthSessionSnapshot>;
|
|
27
33
|
login(request: LoginByEmailRequest): Promise<CodingNsAuthLoginResult>;
|
|
28
34
|
refresh(): Promise<CodingNsAuthSessionSnapshot>;
|
|
35
|
+
private refreshInternal;
|
|
29
36
|
logout(): Promise<void>;
|
|
30
37
|
getDevices(): Promise<AuthDeviceManagementSnapshotDto>;
|
|
31
38
|
listHostBindings(): Promise<TunnelBindingSummary[]>;
|