@jingyi0605/codingns4dsh 0.1.1 → 0.1.7-rc.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 +4 -4
- package/README.md +14 -5
- package/data/build/dist/client/account-bar.js +57 -3
- package/data/build/dist/client/bundle.js +36245 -267
- package/data/build/dist/client/cli-slots.js +60 -18
- package/data/build/dist/client/dsh-h5-bootstrap.d.ts +2 -0
- package/data/build/dist/client/dsh-h5-bootstrap.js +79 -13
- package/data/build/dist/client/features/file-management-panel.d.ts +5 -0
- package/data/build/dist/client/features/file-management-panel.js +48 -0
- package/data/build/dist/client/features/file-management.d.ts +4 -0
- package/data/build/dist/client/features/file-management.js +50 -0
- package/data/build/dist/client/features/index.d.ts +10 -1
- package/data/build/dist/client/features/index.js +12 -1
- package/data/build/dist/client/features/login-protection-panel.js +1 -1
- package/data/build/dist/client/features/login-protection-session.d.ts +2 -0
- package/data/build/dist/client/features/login-protection-session.js +18 -0
- package/data/build/dist/client/features/peer-host.d.ts +4 -0
- package/data/build/dist/client/features/peer-host.js +89 -0
- package/data/build/dist/client/features/reverse-proxy-panel.js +23 -7
- package/data/build/dist/client/features/reverse-proxy.d.ts +1 -2
- package/data/build/dist/client/features/reverse-proxy.js +75 -5
- package/data/build/dist/client/features/types.d.ts +9 -0
- package/data/build/dist/client/features/workspace-session-enhancement-panel.js +10 -0
- package/data/build/dist/client/features/workspace-session-enhancement.js +18 -1
- package/data/build/dist/client/file-management-dom.d.ts +12 -0
- package/data/build/dist/client/file-management-dom.js +527 -0
- package/data/build/dist/client/git-management.js +24 -5
- package/data/build/dist/client/host-navigation.d.ts +29 -0
- package/data/build/dist/client/host-navigation.js +29 -0
- package/data/build/dist/client/host-router.d.ts +3 -0
- package/data/build/dist/client/host-router.js +2 -0
- package/data/build/dist/client/index.d.ts +6 -1
- package/data/build/dist/client/index.js +30 -10
- package/data/build/dist/client/locale.js +36 -7
- package/data/build/dist/client/peer-host-connection-button.d.ts +13 -0
- package/data/build/dist/client/peer-host-connection-button.js +69 -0
- package/data/build/dist/client/peer-host-management-api.d.ts +40 -0
- package/data/build/dist/client/peer-host-management-api.js +33 -0
- package/data/build/dist/client/peer-host-management-panel.d.ts +13 -0
- package/data/build/dist/client/peer-host-management-panel.js +181 -0
- package/data/build/dist/client/peer-host-native-session-ui.d.ts +60 -0
- package/data/build/dist/client/peer-host-native-session-ui.js +372 -0
- package/data/build/dist/client/peer-host-scoped-client.d.ts +67 -0
- package/data/build/dist/client/peer-host-scoped-client.js +316 -0
- package/data/build/dist/client/peer-host-session-controller.d.ts +30 -0
- package/data/build/dist/client/peer-host-session-controller.js +95 -0
- package/data/build/dist/client/quick-phrase-slot.js +14 -0
- package/data/build/dist/client/remote-web-context.js +26 -4
- package/data/build/dist/client/session-changed-files-view.d.ts +15 -0
- package/data/build/dist/client/session-changed-files-view.js +337 -0
- package/data/build/dist/client/settings-bridge.d.ts +14 -4
- package/data/build/dist/client/settings-bridge.js +59 -12
- package/data/build/dist/client/settings-section.js +17 -4
- package/data/build/dist/client/subscription-slot.js +21 -8
- package/data/build/dist/client/workspace-session-rightbar-dom.d.ts +28 -0
- package/data/build/dist/client/workspace-session-rightbar-dom.js +291 -0
- package/data/build/dist/dsh-capabilities/client/config-forms-adapter.js +25 -33
- package/data/build/dist/dsh-capabilities/client/settings-scope-adapter.d.ts +7 -1
- package/data/build/dist/dsh-capabilities/client/settings-scope-adapter.js +34 -11
- package/data/build/dist/dsh-capabilities/matrix.js +9 -0
- package/data/build/dist/dsh-capabilities/routes.js +35 -0
- package/data/build/dist/dsh-capabilities/settings-store.d.ts +12 -0
- package/data/build/dist/dsh-capabilities/settings-store.js +34 -0
- package/data/build/dist/dsh-capabilities/types.d.ts +1 -1
- package/data/build/dist/features/host-router.d.ts +26 -0
- package/data/build/dist/features/host-router.js +110 -0
- package/data/build/dist/features/registry.js +3 -0
- package/data/build/dist/host/cli-adapters/codex-driver.d.ts +13 -1
- package/data/build/dist/host/cli-adapters/codex-driver.js +528 -72
- package/data/build/dist/host/cli-adapters/command-code-driver.d.ts +21 -2
- package/data/build/dist/host/cli-adapters/command-code-driver.js +408 -105
- package/data/build/dist/host/cli-adapters/driver.d.ts +2 -0
- package/data/build/dist/host/cli-adapters/dsh-message-projector.d.ts +2 -0
- package/data/build/dist/host/cli-adapters/dsh-message-projector.js +47 -28
- package/data/build/dist/host/cli-adapters/dsh-tool-history.js +37 -0
- package/data/build/dist/host/cli-adapters/feature.js +94 -10
- package/data/build/dist/host/cli-adapters/json-rpc-process.d.ts +6 -0
- package/data/build/dist/host/cli-adapters/json-rpc-process.js +14 -3
- package/data/build/dist/host/cli-adapters/kimi-driver.js +3 -3
- package/data/build/dist/host/cli-adapters/model-catalog.d.ts +6 -0
- package/data/build/dist/host/cli-adapters/model-catalog.js +8 -0
- package/data/build/dist/host/cli-adapters/opencode-driver.js +14 -2
- package/data/build/dist/host/cli-adapters/pi-driver.js +2 -2
- package/data/build/dist/host/cli-adapters/process-utils.d.ts +8 -0
- package/data/build/dist/host/cli-adapters/process-utils.js +48 -0
- package/data/build/dist/host/cli-adapters/registry.d.ts +6 -1
- package/data/build/dist/host/cli-adapters/registry.js +41 -7
- package/data/build/dist/host/cli-adapters/rpc-driver-utils.js +23 -4
- package/data/build/dist/host/cli-adapters/standard-stream-driver.d.ts +3 -0
- package/data/build/dist/host/cli-adapters/standard-stream-driver.js +36 -15
- package/data/build/dist/host/dsh-device-runtime.d.ts +2 -0
- package/data/build/dist/host/dsh-device-runtime.js +55 -29
- package/data/build/dist/host/features/auth.js +10 -2
- package/data/build/dist/host/features/file-management.d.ts +5 -0
- package/data/build/dist/host/features/file-management.js +267 -0
- package/data/build/dist/host/features/git-management.js +24 -2
- package/data/build/dist/host/features/index.d.ts +2 -0
- package/data/build/dist/host/features/index.js +6 -0
- package/data/build/dist/host/features/peer-host.d.ts +22 -0
- package/data/build/dist/host/features/peer-host.js +322 -0
- package/data/build/dist/host/features/types.d.ts +4 -0
- package/data/build/dist/host/host-router.d.ts +3 -0
- package/data/build/dist/host/host-router.js +2 -0
- package/data/build/dist/host/index-injection.d.ts +17 -0
- package/data/build/dist/host/index-injection.js +24 -0
- package/data/build/dist/host/index.d.ts +4 -1
- package/data/build/dist/host/index.js +41 -6
- package/data/build/dist/host/lan-access-dsh.d.ts +34 -1
- package/data/build/dist/host/lan-access-dsh.js +145 -26
- package/data/build/dist/host/modules/peer-host/host-api-proxy-service.d.ts +52 -0
- package/data/build/dist/host/modules/peer-host/host-api-proxy-service.js +166 -0
- package/data/build/dist/host/modules/peer-host/host-ws-connector.d.ts +31 -0
- package/data/build/dist/host/modules/peer-host/host-ws-connector.js +108 -0
- package/data/build/dist/host/modules/peer-host/host-ws-proxy-service.d.ts +26 -0
- package/data/build/dist/host/modules/peer-host/host-ws-proxy-service.js +159 -0
- package/data/build/dist/host/modules/peer-host/peer-host-aggregate-service.d.ts +50 -0
- package/data/build/dist/host/modules/peer-host/peer-host-aggregate-service.js +97 -0
- package/data/build/dist/host/modules/peer-host/peer-host-diagnostics.d.ts +19 -0
- package/data/build/dist/host/modules/peer-host/peer-host-diagnostics.js +54 -0
- package/data/build/dist/host/modules/peer-host/peer-host-handshake.d.ts +44 -0
- package/data/build/dist/host/modules/peer-host/peer-host-handshake.js +133 -0
- package/data/build/dist/host/modules/peer-host/peer-host-relay.d.ts +65 -0
- package/data/build/dist/host/modules/peer-host/peer-host-relay.js +196 -0
- package/data/build/dist/host/modules/peer-host/peer-host-session.d.ts +42 -0
- package/data/build/dist/host/modules/peer-host/peer-host-session.js +152 -0
- package/data/build/dist/host/modules/peer-host/peer-host-store.d.ts +92 -0
- package/data/build/dist/host/modules/peer-host/peer-host-store.js +323 -0
- package/data/build/dist/host/modules/peer-host/peer-host-ws-gateway.d.ts +34 -0
- package/data/build/dist/host/modules/peer-host/peer-host-ws-gateway.js +156 -0
- package/data/build/dist/host/native-session-bridge.d.ts +18 -2
- package/data/build/dist/host/native-session-bridge.js +232 -8
- package/data/build/dist/host/remote-web-runtime.js +26 -1
- package/data/build/dist/host/rpc.js +8 -3
- package/data/build/dist/host/session-changes.d.ts +5 -0
- package/data/build/dist/host/session-changes.js +163 -0
- package/data/build/dist/host/settings.js +6 -0
- package/data/build/dist/index.d.ts +1 -1
- package/data/build/dist/index.js +1 -1
- package/data/build/dist/shared/contracts/cli-adapter.d.ts +23 -2
- package/data/build/dist/shared/contracts/config.d.ts +15 -0
- package/data/build/dist/shared/contracts/config.js +10 -0
- package/data/build/dist/shared/contracts/dsh-device.d.ts +2 -0
- package/data/build/dist/shared/contracts/errors.d.ts +14 -0
- package/data/build/dist/shared/contracts/errors.js +15 -0
- package/data/build/dist/shared/contracts/feature.d.ts +2 -0
- package/data/build/dist/shared/contracts/file-management.d.ts +6 -0
- package/data/build/dist/shared/contracts/file-management.js +3 -0
- package/data/build/dist/shared/contracts/peer-host.d.ts +116 -15
- package/data/build/dist/shared/contracts/peer-host.js +19 -0
- package/data/build/dist/shared/contracts/version.d.ts +1 -1
- package/data/build/dist/shared/contracts/version.js +1 -1
- package/data/build/dist/shared/index.d.ts +4 -3
- package/data/build/dist/shared/index.js +2 -2
- package/data/build/dist/transport/debug.d.ts +5 -0
- package/data/build/dist/transport/debug.js +23 -1
- package/data/build/dist/transport/dsh-transport.d.ts +2 -0
- package/data/build/dist/transport/dsh-transport.js +11 -0
- package/data/build/dist/transport/webrtc-client.d.ts +2 -0
- package/data/build/dist/transport/webrtc-client.js +2 -1
- package/data/build/dist/transport/webrtc-host.d.ts +2 -0
- package/data/build/dist/transport/webrtc-host.js +6 -3
- package/dsh.bundle.patch +10 -0
- package/package.json +23 -11
- package/profile/README.md +2 -2
- package/profile/package.json +2 -2
- package/profile/version.json +1 -1
- package/version.json +1 -1
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
import { CodingNsDshToolHistoryProjector } from './dsh-tool-history.js';
|
|
2
2
|
import { CodingNsAgentEventNormalizer, } from './stream-normalizer.js';
|
|
3
|
+
/**
|
|
4
|
+
* 把驱动的 Provider 口径 usage 映射成 DSH `TokenUsage` 的互斥桶口径。
|
|
5
|
+
*
|
|
6
|
+
* DSH 约定 `inputTokens` 只含未缓存输入,计费输入 = inputTokens + 缓存读写,而
|
|
7
|
+
* 驱动层已经用 `usageChunk` 按各 Provider 的字段语义折算出 `uncachedInputTokens`
|
|
8
|
+
* (Codex、Command Code 的 inputTokens 含缓存命中,Anthropic 风格的 input 不含)。
|
|
9
|
+
* 这里必须以该字段为准;直接把驱动事件里的 `inputTokens` 当成未缓存输入写进 DSH,
|
|
10
|
+
* 会让 token-meter 把缓存读取重复计入分母:缓存命中率被腰斩,上下文占用翻倍。
|
|
11
|
+
*/
|
|
12
|
+
function toDshTokenUsage(event) {
|
|
13
|
+
// 没有缓存分桶时 `inputTokens` 本身就是全部输入,也就是未缓存输入。
|
|
14
|
+
const inputTokens = event.uncachedInputTokens ?? event.inputTokens;
|
|
15
|
+
return {
|
|
16
|
+
inputTokens: Math.max(0, inputTokens),
|
|
17
|
+
outputTokens: event.outputTokens,
|
|
18
|
+
...(event.cacheReadTokens === undefined ? {} : { cacheReadTokens: event.cacheReadTokens }),
|
|
19
|
+
...(event.cacheWriteTokens === undefined ? {} : { cacheWriteTokens: event.cacheWriteTokens }),
|
|
20
|
+
...(event.totalTokens === undefined ? {} : { totalTokens: event.totalTokens }),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
3
23
|
/**
|
|
4
24
|
* 所有外部 Agent 共用的 DSH 消息投影器。
|
|
5
25
|
*
|
|
@@ -16,6 +36,8 @@ export class CodingNsDshMessageProjector {
|
|
|
16
36
|
reasoningText = '';
|
|
17
37
|
textText = '';
|
|
18
38
|
finished = false;
|
|
39
|
+
/** 只要收到正文、工具、usage 或交互事件,就说明 Provider 确实产出了结果。 */
|
|
40
|
+
receivedProviderEvent = false;
|
|
19
41
|
constructor(options) {
|
|
20
42
|
this.options = options;
|
|
21
43
|
this.toolHistory = new CodingNsDshToolHistoryProjector(options.nativeSessions, options.sessionId, options.adapterId);
|
|
@@ -26,6 +48,9 @@ export class CodingNsDshMessageProjector {
|
|
|
26
48
|
async push(event) {
|
|
27
49
|
if (this.finished)
|
|
28
50
|
return [];
|
|
51
|
+
if (event.type !== 'session-binding' && event.type !== 'finish') {
|
|
52
|
+
this.receivedProviderEvent = true;
|
|
53
|
+
}
|
|
29
54
|
if (event.type === 'usage')
|
|
30
55
|
this.recordUsageSample(event);
|
|
31
56
|
const projected = [];
|
|
@@ -40,6 +65,9 @@ export class CodingNsDshMessageProjector {
|
|
|
40
65
|
async complete(reason = 'stop') {
|
|
41
66
|
if (this.finished)
|
|
42
67
|
return [];
|
|
68
|
+
if (reason === 'stop' && !this.receivedProviderEvent) {
|
|
69
|
+
return this.fail('CODINGNS_PROVIDER_EMPTY_RESPONSE: Provider 未返回任何有效事件。');
|
|
70
|
+
}
|
|
43
71
|
const projected = await this.flushUsage();
|
|
44
72
|
projected.push(...await this.project({ type: 'finish', reason }));
|
|
45
73
|
return projected;
|
|
@@ -68,13 +96,9 @@ export class CodingNsDshMessageProjector {
|
|
|
68
96
|
* 提前向 LLM 流发送第二个 usage,只写入 assistant/attempt 供上下文计量投影使用。
|
|
69
97
|
*/
|
|
70
98
|
recordUsageSample(event) {
|
|
99
|
+
// assistant/attempt 同样是 DSH 会话记录,usage 必须按 DSH 的互斥桶口径落盘。
|
|
71
100
|
const usage = {
|
|
72
|
-
|
|
73
|
-
outputTokens: event.outputTokens,
|
|
74
|
-
...(event.cacheReadTokens === undefined ? {} : { cacheReadTokens: event.cacheReadTokens }),
|
|
75
|
-
...(event.cacheWriteTokens === undefined ? {} : { cacheWriteTokens: event.cacheWriteTokens }),
|
|
76
|
-
...(event.uncachedInputTokens === undefined ? {} : { uncachedInputTokens: event.uncachedInputTokens }),
|
|
77
|
-
...(event.totalTokens === undefined ? {} : { totalTokens: event.totalTokens }),
|
|
101
|
+
...toDshTokenUsage(event),
|
|
78
102
|
...(event.cacheHitRate === undefined ? {} : { cacheHitRate: event.cacheHitRate }),
|
|
79
103
|
...(event.contextWindow === undefined ? {} : { contextWindow: event.contextWindow }),
|
|
80
104
|
...(event.contextTokens === undefined ? {} : { contextTokens: event.contextTokens }),
|
|
@@ -85,6 +109,7 @@ export class CodingNsDshMessageProjector {
|
|
|
85
109
|
provider: this.options.adapterId,
|
|
86
110
|
model: this.options.modelId ?? this.options.adapterId,
|
|
87
111
|
contextWindow: event.contextWindow,
|
|
112
|
+
confirmed: true,
|
|
88
113
|
});
|
|
89
114
|
}
|
|
90
115
|
this.options.nativeSessions?.appendUsageSample?.(this.options.sessionId, usage);
|
|
@@ -102,13 +127,9 @@ export class CodingNsDshMessageProjector {
|
|
|
102
127
|
case 'message-boundary':
|
|
103
128
|
return this.closeMessageBlock(event.channel);
|
|
104
129
|
case 'step-boundary':
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
// 识别,因此写入一个 Markdown 引用定义作为不可见的回复边界。
|
|
109
|
-
// 必须从新行开始;DSH UI 会忽略 definition 节点,而不是把 HTML 当注释解析。
|
|
110
|
-
// 该内容不会显示给用户,也不会改变工具的原生 step 坐标。
|
|
111
|
-
return this.appendDelta('text', '\n\n[//]: # (codingns-step-boundary)');
|
|
130
|
+
// step 边界由 DSH 原生 turn/step 事件表达,不再伪造 Markdown 文本。
|
|
131
|
+
// 伪造的空文本会被 Chat 渲染成额外的空白块,导致相邻 step 间距异常。
|
|
132
|
+
return [];
|
|
112
133
|
case 'tool-event':
|
|
113
134
|
return this.externalToolChunk(this.toolHistory.observe(event));
|
|
114
135
|
case 'permission-request':
|
|
@@ -118,23 +139,16 @@ export class CodingNsDshMessageProjector {
|
|
|
118
139
|
await this.requestQuestions(event);
|
|
119
140
|
return [];
|
|
120
141
|
case 'usage':
|
|
121
|
-
return [{
|
|
122
|
-
type: 'usage',
|
|
123
|
-
usage: {
|
|
124
|
-
inputTokens: event.inputTokens,
|
|
125
|
-
outputTokens: event.outputTokens,
|
|
126
|
-
...(event.cacheReadTokens === undefined ? {} : { cacheReadTokens: event.cacheReadTokens }),
|
|
127
|
-
...(event.cacheWriteTokens === undefined ? {} : { cacheWriteTokens: event.cacheWriteTokens }),
|
|
128
|
-
...(event.uncachedInputTokens === undefined ? {} : { uncachedInputTokens: event.uncachedInputTokens }),
|
|
129
|
-
...(event.totalTokens === undefined ? {} : { totalTokens: event.totalTokens }),
|
|
130
|
-
...(event.cacheHitRate === undefined ? {} : { cacheHitRate: event.cacheHitRate }),
|
|
131
|
-
...(event.contextWindow === undefined ? {} : { contextWindow: event.contextWindow }),
|
|
132
|
-
...(event.contextTokens === undefined ? {} : { contextTokens: event.contextTokens }),
|
|
133
|
-
...(event.contextUsageRatio === undefined ? {} : { contextUsageRatio: event.contextUsageRatio }),
|
|
134
|
-
},
|
|
135
|
-
}];
|
|
142
|
+
return [{ type: 'usage', usage: toDshTokenUsage(event) }];
|
|
136
143
|
case 'session-binding':
|
|
137
144
|
return [];
|
|
145
|
+
case 'context-compaction':
|
|
146
|
+
this.options.nativeSessions?.appendCompactionEvent?.(this.options.sessionId, {
|
|
147
|
+
...event,
|
|
148
|
+
provider: event.provider ?? this.options.adapterId,
|
|
149
|
+
model: event.model ?? this.options.modelId ?? this.options.adapterId,
|
|
150
|
+
});
|
|
151
|
+
return [];
|
|
138
152
|
case 'finish':
|
|
139
153
|
const closed = [
|
|
140
154
|
...this.closeMessageBlock('reasoning'),
|
|
@@ -144,6 +158,8 @@ export class CodingNsDshMessageProjector {
|
|
|
144
158
|
this.toolHistory.finalize(event.reason, failureMessage);
|
|
145
159
|
closed.push({ type: 'finish', reason: toDshFinishReason(event.reason, failureMessage) });
|
|
146
160
|
return closed;
|
|
161
|
+
default:
|
|
162
|
+
return assertNeverNormalizedEvent(event);
|
|
147
163
|
}
|
|
148
164
|
}
|
|
149
165
|
appendDelta(channel, text) {
|
|
@@ -236,6 +252,9 @@ export class CodingNsDshMessageProjector {
|
|
|
236
252
|
await responder(response);
|
|
237
253
|
}
|
|
238
254
|
}
|
|
255
|
+
function assertNeverNormalizedEvent(value) {
|
|
256
|
+
throw new Error(`未支持的 DSH 事件类型: ${String(value.type ?? 'unknown')}`);
|
|
257
|
+
}
|
|
239
258
|
function toDshFinishReason(reason, failureMessage) {
|
|
240
259
|
if (reason === 'cancel') {
|
|
241
260
|
return { kind: 'aborted', failure: { message: failureMessage ?? '外部 Agent 执行已取消', code: 'ABORTED' } };
|
|
@@ -316,8 +316,25 @@ function toolResultMeta(name, argumentsJson) {
|
|
|
316
316
|
if (name !== 'edit' && name !== 'write')
|
|
317
317
|
return {};
|
|
318
318
|
const args = parseRecord(argumentsJson);
|
|
319
|
+
if (name === 'edit' && Array.isArray(args?.changes)) {
|
|
320
|
+
const diffs = args.changes.flatMap((value) => {
|
|
321
|
+
if (!isRecord(value))
|
|
322
|
+
return [];
|
|
323
|
+
const path = stringValue(value.file_path ?? value.path);
|
|
324
|
+
const diff = stringValue(value.diff ?? value.patch);
|
|
325
|
+
if (path === null || diff === null)
|
|
326
|
+
return [];
|
|
327
|
+
return [unifiedDiffMeta(path, diff, stringValue(value.kind))];
|
|
328
|
+
});
|
|
329
|
+
return diffs.length > 0 ? { meta: { diffs } } : {};
|
|
330
|
+
}
|
|
319
331
|
const path = stringValue(args?.file_path);
|
|
320
332
|
const newText = stringValue(name === 'edit' ? args?.new_string : args?.content);
|
|
333
|
+
if (name === 'edit' && path !== null) {
|
|
334
|
+
const diff = stringValue(args?.diff ?? args?.patch);
|
|
335
|
+
if (diff !== null)
|
|
336
|
+
return { meta: { diffs: [unifiedDiffMeta(path, diff, stringValue(args?.kind))] } };
|
|
337
|
+
}
|
|
321
338
|
if (path === null || newText === null)
|
|
322
339
|
return {};
|
|
323
340
|
const oldText = name === 'edit' ? stringValue(args?.old_string) : null;
|
|
@@ -327,6 +344,26 @@ function toolResultMeta(name, argumentsJson) {
|
|
|
327
344
|
},
|
|
328
345
|
};
|
|
329
346
|
}
|
|
347
|
+
function unifiedDiffMeta(path, diff, kind) {
|
|
348
|
+
const oldLines = [];
|
|
349
|
+
const newLines = [];
|
|
350
|
+
for (const line of diff.split('\n')) {
|
|
351
|
+
if (line.startsWith('--- ') || line.startsWith('+++ ') || line.startsWith('@@'))
|
|
352
|
+
continue;
|
|
353
|
+
if (line.startsWith('-'))
|
|
354
|
+
oldLines.push(line.slice(1));
|
|
355
|
+
else if (line.startsWith('+'))
|
|
356
|
+
newLines.push(line.slice(1));
|
|
357
|
+
else if (line.startsWith(' ')) {
|
|
358
|
+
const context = line.slice(1);
|
|
359
|
+
oldLines.push(context);
|
|
360
|
+
newLines.push(context);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const oldText = kind === 'add' || kind === 'create' ? null : oldLines.join('\n');
|
|
364
|
+
const newText = kind === 'delete' || kind === 'remove' ? '' : newLines.join('\n');
|
|
365
|
+
return { path, oldText, newText };
|
|
366
|
+
}
|
|
330
367
|
/** Command Code 等 Provider 会把文本块包成 JSON;原生结果只展示真正文本。 */
|
|
331
368
|
function normalizeToolOutput(value) {
|
|
332
369
|
if (value === '')
|
|
@@ -109,24 +109,42 @@ export function createCliAdaptersFeature(options = {}) {
|
|
|
109
109
|
const dispose = events.on('llm/stream', async function* (options, next) {
|
|
110
110
|
const value = asRecord(options);
|
|
111
111
|
const sessionId = typeof value?.sessionId === 'string' ? value.sessionId : '';
|
|
112
|
-
|
|
112
|
+
if (value?.purpose === 'session-title' || value?.purpose === 'compaction') {
|
|
113
|
+
yield* next();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const storedConfig = sessionId ? registry.getSession(sessionId) : { adapterId: 'dsh' };
|
|
117
|
+
// DSH 原生请求仍会携带当前模型提供方。旧会话在重载后可能暂时
|
|
118
|
+
// 被恢复成 dsh,但 provider 已明确指向外部 Agent;继续旁路会让
|
|
119
|
+
// 第二轮直接落入 DSH 空流,并丢失外部驱动的 usage/context 修正。
|
|
120
|
+
const dshSelection = readDshSelection(value);
|
|
121
|
+
const messages = Array.isArray(value?.messages) ? value.messages.filter(isMessage) : [];
|
|
122
|
+
const selectedExternalAdapter = selectExternalAdapter(dshSelection.providerId, inferMessageAdapter(messages), registry);
|
|
123
|
+
const config = storedConfig.adapterId === 'dsh' && selectedExternalAdapter !== undefined
|
|
124
|
+
? {
|
|
125
|
+
adapterId: selectedExternalAdapter,
|
|
126
|
+
...(dshSelection.modelId === undefined ? {} : { modelId: dshSelection.modelId }),
|
|
127
|
+
...(dshSelection.effortId === undefined ? {} : { effortId: dshSelection.effortId }),
|
|
128
|
+
}
|
|
129
|
+
: storedConfig;
|
|
130
|
+
// DSH 首轮请求可能只通过 provider 临时选择外部 Agent,第二轮请求通常不再携带
|
|
131
|
+
// provider。必须在路由决定后立即持久化绑定,否则下一轮会在进入 Registry 前退回 dsh。
|
|
132
|
+
if (sessionId !== '' && storedConfig.adapterId === 'dsh' && selectedExternalAdapter !== undefined) {
|
|
133
|
+
registry.setSession(sessionId, config);
|
|
134
|
+
}
|
|
113
135
|
if (config.adapterId === 'dsh') {
|
|
114
|
-
const selection =
|
|
136
|
+
const selection = dshSelection;
|
|
115
137
|
if (sessionId !== '' && (selection.modelId !== undefined || selection.effortId !== undefined)) {
|
|
116
138
|
registry.setSession(sessionId, { adapterId: 'dsh', ...selection });
|
|
117
139
|
}
|
|
118
|
-
yield* next
|
|
140
|
+
yield* guardDshNativeStream(next);
|
|
119
141
|
return;
|
|
120
142
|
}
|
|
121
|
-
if (value?.purpose === 'session-title' || value?.purpose === 'compaction') {
|
|
122
|
-
yield* next();
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
const messages = Array.isArray(value?.messages) ? value.messages.filter(isMessage) : [];
|
|
126
143
|
const cwd = resolveSessionCwd(context.services.nativeSessions, sessionId, value);
|
|
127
144
|
// 只有 Provider 驱动明确维护了稳定的 turn 分段,才把工具边界映射为 DSH step。
|
|
128
|
-
//
|
|
129
|
-
// token-meter
|
|
145
|
+
// Command Code、Codex 会在下一个 assistant 消息处结束当前 step;未声明分段
|
|
146
|
+
// 支持的驱动(如 OpenCode)仍把整轮保持在一个 step,避免 token-meter 在下一
|
|
147
|
+
// 条 usage 到达前失去投影。
|
|
130
148
|
const input = {
|
|
131
149
|
sessionId,
|
|
132
150
|
messages,
|
|
@@ -199,6 +217,48 @@ export function createCliAdaptersFeature(options = {}) {
|
|
|
199
217
|
},
|
|
200
218
|
};
|
|
201
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* DSH 原生 Provider 可能只返回一个 finish(stop)。不能把这个空回合当作成功,
|
|
222
|
+
* 否则 Session 会落下一条空 assistant/message,用户只能看到“一秒结束”。
|
|
223
|
+
*/
|
|
224
|
+
async function* guardDshNativeStream(next) {
|
|
225
|
+
let meaningful = false;
|
|
226
|
+
const terminal = [];
|
|
227
|
+
for await (const chunk of next()) {
|
|
228
|
+
if (isDshFinishChunk(chunk)) {
|
|
229
|
+
terminal.push(chunk);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
meaningful ||= isMeaningfulDshChunk(chunk);
|
|
233
|
+
yield chunk;
|
|
234
|
+
}
|
|
235
|
+
if (meaningful) {
|
|
236
|
+
for (const chunk of terminal)
|
|
237
|
+
yield chunk;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const message = 'CODINGNS_PROVIDER_EMPTY_RESPONSE: DSH Provider 未返回任何有效事件。';
|
|
241
|
+
yield { type: 'block-start', index: 1, blockType: 'text' };
|
|
242
|
+
yield { type: 'text-delta', index: 1, text: message };
|
|
243
|
+
yield { type: 'block-end', index: 1, block: { type: 'text', text: message } };
|
|
244
|
+
yield { type: 'finish', reason: { kind: 'error', failure: { message, code: 'PROVIDER_ERROR' } } };
|
|
245
|
+
}
|
|
246
|
+
function isDshFinishChunk(value) {
|
|
247
|
+
const record = asRecord(value);
|
|
248
|
+
return record?.type === 'finish';
|
|
249
|
+
}
|
|
250
|
+
function isMeaningfulDshChunk(value) {
|
|
251
|
+
const record = asRecord(value);
|
|
252
|
+
if (record === null)
|
|
253
|
+
return false;
|
|
254
|
+
if (record.type === 'text-delta' || record.type === 'reasoning-delta' || record.type === 'usage' || record.type === 'tool-call' || record.type === 'tool-result')
|
|
255
|
+
return true;
|
|
256
|
+
if (record.type === 'block-end') {
|
|
257
|
+
const block = asRecord(record.block);
|
|
258
|
+
return typeof block?.text === 'string' && block.text !== '';
|
|
259
|
+
}
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
202
262
|
function readAdapterId(value) {
|
|
203
263
|
const record = asRecord(value);
|
|
204
264
|
if (typeof record?.adapterId !== 'string' || record.adapterId.trim() === '')
|
|
@@ -280,15 +340,39 @@ function readModelSelectionCandidates(value) {
|
|
|
280
340
|
const result = [];
|
|
281
341
|
for (const candidate of [value, asRecord(value?.request), asRecord(value?.config), asRecord(value?.header), asRecord(asRecord(value?.record)?.rows)]) {
|
|
282
342
|
const selection = asRecord(candidate?.modelSelection);
|
|
343
|
+
if (selection !== null)
|
|
344
|
+
result.push(selection);
|
|
283
345
|
const lastUsed = asRecord(selection?.lastUsed);
|
|
284
346
|
const next = asRecord(selection?.next);
|
|
347
|
+
const pending = asRecord(selection?.pending);
|
|
285
348
|
if (lastUsed !== null)
|
|
286
349
|
result.push(lastUsed);
|
|
287
350
|
if (next !== null)
|
|
288
351
|
result.push(next);
|
|
352
|
+
if (pending !== null)
|
|
353
|
+
result.push(pending);
|
|
289
354
|
}
|
|
290
355
|
return result;
|
|
291
356
|
}
|
|
357
|
+
function selectExternalAdapter(selectedProvider, messageAdapter, registry) {
|
|
358
|
+
for (const candidate of [selectedProvider, messageAdapter]) {
|
|
359
|
+
if (candidate !== undefined && candidate !== 'dsh' && registry.isEnabled(candidate))
|
|
360
|
+
return candidate;
|
|
361
|
+
}
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
/** 从当前会话历史恢复外部路由,覆盖 DSH 第二轮省略 provider 的请求形态。 */
|
|
365
|
+
function inferMessageAdapter(messages) {
|
|
366
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
367
|
+
const source = asRecord(messages[index].source);
|
|
368
|
+
if (source?.kind !== 'model' || source.plugin !== 'codingns4dsh')
|
|
369
|
+
continue;
|
|
370
|
+
const provider = source.provider;
|
|
371
|
+
if (typeof provider === 'string' && provider.trim() !== '')
|
|
372
|
+
return provider.trim();
|
|
373
|
+
}
|
|
374
|
+
return undefined;
|
|
375
|
+
}
|
|
292
376
|
async function setAdapterEnabled(settings, registry, value) {
|
|
293
377
|
const record = asRecord(value);
|
|
294
378
|
const adapterId = readAdapterId(record);
|
|
@@ -13,6 +13,12 @@ export interface JsonRpcMessage {
|
|
|
13
13
|
};
|
|
14
14
|
readonly [key: string]: unknown;
|
|
15
15
|
}
|
|
16
|
+
/** 保留 JSON-RPC 错误的稳定字段,供适配器识别可恢复错误。 */
|
|
17
|
+
export declare class JsonRpcRequestError extends Error {
|
|
18
|
+
readonly code: number | undefined;
|
|
19
|
+
readonly data: unknown;
|
|
20
|
+
constructor(code: number | undefined, data: unknown);
|
|
21
|
+
}
|
|
16
22
|
export interface JsonRpcProcessOptions {
|
|
17
23
|
readonly command: string;
|
|
18
24
|
readonly args?: readonly string[];
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import readline from 'node:readline';
|
|
3
|
-
import { terminateChildProcess, WINDOWS } from './process-utils.js';
|
|
3
|
+
import { commandEnvironment, terminateChildProcess, WINDOWS } from './process-utils.js';
|
|
4
|
+
/** 保留 JSON-RPC 错误的稳定字段,供适配器识别可恢复错误。 */
|
|
5
|
+
export class JsonRpcRequestError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
data;
|
|
8
|
+
constructor(code, data) {
|
|
9
|
+
super('JSON-RPC 请求失败');
|
|
10
|
+
this.name = 'JsonRpcRequestError';
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.data = data;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
4
15
|
/**
|
|
5
16
|
* 统一管理 stdio JSON-RPC 子进程:请求编号、逐行解码、取消和退出清理都在这里完成。
|
|
6
17
|
* 凭据不会被记录,也不会进入错误文本。
|
|
@@ -132,7 +143,7 @@ export class JsonRpcProcess {
|
|
|
132
143
|
return;
|
|
133
144
|
const child = this.runSpawn(this.options.command, this.options.args ?? [], {
|
|
134
145
|
cwd: this.options.cwd,
|
|
135
|
-
env: { ...
|
|
146
|
+
env: { ...commandEnvironment(this.options.command), ...(this.options.env ?? {}) },
|
|
136
147
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
137
148
|
windowsHide: true,
|
|
138
149
|
shell: WINDOWS,
|
|
@@ -193,7 +204,7 @@ export class JsonRpcProcess {
|
|
|
193
204
|
continue;
|
|
194
205
|
// Provider 错误可能带命令行、路径或凭据片段,只向上层暴露稳定错误,不回传原文。
|
|
195
206
|
if (isRecord(value.error))
|
|
196
|
-
pending.reject(new
|
|
207
|
+
pending.reject(new JsonRpcRequestError(typeof value.error.code === 'number' ? value.error.code : undefined, value.error.data));
|
|
197
208
|
else
|
|
198
209
|
pending.resolve(value.result);
|
|
199
210
|
continue;
|
|
@@ -7,7 +7,7 @@ import { probeStoredSession, readFirstJsonRecord, resolveSessionDirectory } from
|
|
|
7
7
|
import { firstToolText, isToolRecord, normalizeToolStatus, serializeToolValue } from './tool-observation.js';
|
|
8
8
|
import { isQuestionEvent, readAgentQuestions } from './interaction-events.js';
|
|
9
9
|
import { usageChunk } from './rpc-driver-utils.js';
|
|
10
|
-
import { terminateChildProcess } from './process-utils.js';
|
|
10
|
+
import { commandEnvironment, terminateChildProcess } from './process-utils.js';
|
|
11
11
|
/** Kimi 的 wire 协议优先,旧版 CLI 不支持时自动回退 stream-json。 */
|
|
12
12
|
export class KimiCliDriver extends StandardStreamDriver {
|
|
13
13
|
legacySyntax = false;
|
|
@@ -113,7 +113,7 @@ export class KimiCliDriver extends StandardStreamDriver {
|
|
|
113
113
|
if (input.modelId && !isProviderDefaultModel(input.modelId))
|
|
114
114
|
args.push('--model', input.modelId);
|
|
115
115
|
const child = this.runSpawn(command, args, {
|
|
116
|
-
cwd: input.cwd ?? process.cwd(), env:
|
|
116
|
+
cwd: input.cwd ?? process.cwd(), env: commandEnvironment(command), stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true, shell: process.platform === 'win32',
|
|
117
117
|
});
|
|
118
118
|
const stdin = child.stdin;
|
|
119
119
|
const interaction = {
|
|
@@ -223,7 +223,7 @@ export class KimiCliDriver extends StandardStreamDriver {
|
|
|
223
223
|
}
|
|
224
224
|
detectSyntax(command) {
|
|
225
225
|
try {
|
|
226
|
-
const result = this.runSpawnSync(command, ['--help'], { encoding: 'utf8', timeout: 5_000, windowsHide: true, shell: process.platform === 'win32' });
|
|
226
|
+
const result = this.runSpawnSync(command, ['--help'], { encoding: 'utf8', timeout: 5_000, windowsHide: true, shell: process.platform === 'win32', env: commandEnvironment(command) });
|
|
227
227
|
const output = `${result.stdout ?? ''}\n${result.stderr ?? ''}`.toLowerCase();
|
|
228
228
|
this.legacySyntax = /(?:^|\s)wire(?:\s|$)/u.test(output) && !output.includes('--wire');
|
|
229
229
|
}
|
|
@@ -14,6 +14,12 @@ export declare const CLAUDE_CATALOG: CodingNsCliModelCatalog;
|
|
|
14
14
|
export declare const KIMI_CATALOG: CodingNsCliModelCatalog;
|
|
15
15
|
export declare const GEMINI_CATALOG: CodingNsCliModelCatalog;
|
|
16
16
|
export declare const CODEX_CATALOG: CodingNsCliModelCatalog;
|
|
17
|
+
/**
|
|
18
|
+
* Codex app-server 在 request/context 中会先沿用 1M 的通用默认值,
|
|
19
|
+
* 但 gpt-5.6-sol 的真实 tokenUsage.contextWindow 是 258400。
|
|
20
|
+
* 这个提示只用于首个 usage 到达前稳定 DSH 的分母;真实 usage 仍然优先。
|
|
21
|
+
*/
|
|
22
|
+
export declare function knownCodexContextWindow(modelId: string | undefined): number | undefined;
|
|
17
23
|
export declare const GROK_CATALOG: CodingNsCliModelCatalog;
|
|
18
24
|
export declare const PI_CATALOG: CodingNsCliModelCatalog;
|
|
19
25
|
/** 把 CLI 帮助解析到的模型补上已知档位,未知模型保持空数组。 */
|
|
@@ -64,6 +64,14 @@ export const CODEX_CATALOG = staticCatalog('codex', 'Codex', [
|
|
|
64
64
|
{ id: 'gpt-5.4', efforts: ['low', 'medium', 'high', 'xhigh'] },
|
|
65
65
|
{ id: 'gpt-5.3-codex', efforts: ['low', 'medium', 'high', 'xhigh'] },
|
|
66
66
|
]);
|
|
67
|
+
/**
|
|
68
|
+
* Codex app-server 在 request/context 中会先沿用 1M 的通用默认值,
|
|
69
|
+
* 但 gpt-5.6-sol 的真实 tokenUsage.contextWindow 是 258400。
|
|
70
|
+
* 这个提示只用于首个 usage 到达前稳定 DSH 的分母;真实 usage 仍然优先。
|
|
71
|
+
*/
|
|
72
|
+
export function knownCodexContextWindow(modelId) {
|
|
73
|
+
return modelId?.trim().toLowerCase() === 'gpt-5.6-sol' ? 258400 : undefined;
|
|
74
|
+
}
|
|
67
75
|
export const GROK_CATALOG = staticCatalog('grok', 'Grok', [
|
|
68
76
|
{ id: 'provider-default', name: '跟随 Grok 默认模型', efforts: ['low', 'medium', 'high', 'xhigh'] },
|
|
69
77
|
{ id: 'grok-4.6', efforts: ['low', 'medium', 'high', 'xhigh'] },
|
|
@@ -4,7 +4,7 @@ import { isProviderDefaultModel } from './model-catalog.js';
|
|
|
4
4
|
import { firstToolText, normalizeToolStatus, serializeToolValue } from './tool-observation.js';
|
|
5
5
|
import { isQuestionEvent, questionAnswersList, readAgentQuestions } from './interaction-events.js';
|
|
6
6
|
import { usageChunk } from './rpc-driver-utils.js';
|
|
7
|
-
import { terminateChildProcess } from './process-utils.js';
|
|
7
|
+
import { commandEnvironment, resolveCommandPath, terminateChildProcess } from './process-utils.js';
|
|
8
8
|
const WINDOWS = process.platform === 'win32';
|
|
9
9
|
const DEFAULT_BINARIES = WINDOWS ? ['opencode.exe', 'opencode'] : ['opencode'];
|
|
10
10
|
const DEFAULT_URLS = ['http://127.0.0.1:4096'];
|
|
@@ -380,7 +380,7 @@ export class OpenCodeDriver {
|
|
|
380
380
|
try {
|
|
381
381
|
child = this.runSpawn(command, [...this.serverArgs, '--port', String(port)], {
|
|
382
382
|
cwd,
|
|
383
|
-
env:
|
|
383
|
+
env: commandEnvironment(command),
|
|
384
384
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
385
385
|
windowsHide: true,
|
|
386
386
|
shell: WINDOWS,
|
|
@@ -441,6 +441,18 @@ export class OpenCodeDriver {
|
|
|
441
441
|
}
|
|
442
442
|
}
|
|
443
443
|
catch { /* PATH 中没有命令 */ }
|
|
444
|
+
const resolved = resolveCommandPath(command, this.runSpawnSync);
|
|
445
|
+
if (resolved === null)
|
|
446
|
+
continue;
|
|
447
|
+
try {
|
|
448
|
+
const result = this.runSpawnSync(resolved, ['--version'], { encoding: 'utf8', timeout: 3_000, windowsHide: true, shell: WINDOWS, env: commandEnvironment(resolved) });
|
|
449
|
+
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`;
|
|
450
|
+
if (result.status === 0) {
|
|
451
|
+
this.cachedBinary = resolved;
|
|
452
|
+
return { command: resolved, version: output.match(/\d+\.\d+\.\d+/u)?.[0] ?? null };
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
catch { /* 登录 Shell 找到的命令也可能已失效 */ }
|
|
444
456
|
}
|
|
445
457
|
return null;
|
|
446
458
|
}
|
|
@@ -6,7 +6,7 @@ import { detectBinary, emptyCatalog, isRecord, textValue, usageChunk } from './r
|
|
|
6
6
|
import { PI_CATALOG, isProviderDefaultModel } from './model-catalog.js';
|
|
7
7
|
import { probeStoredSession, readFirstJsonRecord } from './session-probe.js';
|
|
8
8
|
import { firstToolText, isToolRecord, normalizeToolStatus, serializeToolValue } from './tool-observation.js';
|
|
9
|
-
import { WINDOWS } from './process-utils.js';
|
|
9
|
+
import { commandEnvironment, WINDOWS } from './process-utils.js';
|
|
10
10
|
/** Pi Agent 的 --mode rpc 适配器。Pi 的长期 RPC 细节被限制在本文件和标准进程层内。 */
|
|
11
11
|
export class PiAgentDriver {
|
|
12
12
|
descriptor = { id: 'pi', name: 'Pi Agent', protocol: 'json-rpc', capabilities: ['models', 'stream', 'resume', 'interrupt', 'tool-events', 'reasoning', 'usage', 'steer'] };
|
|
@@ -51,7 +51,7 @@ export class PiAgentDriver {
|
|
|
51
51
|
rpc.dispose();
|
|
52
52
|
}
|
|
53
53
|
try {
|
|
54
|
-
const result = this.runSpawnSync(detection.command, ['--list-models'], { encoding: 'utf8', timeout: 12_000, windowsHide: true, shell: WINDOWS });
|
|
54
|
+
const result = this.runSpawnSync(detection.command, ['--list-models'], { encoding: 'utf8', timeout: 12_000, windowsHide: true, shell: WINDOWS, env: commandEnvironment(detection.command) });
|
|
55
55
|
const catalog = parsePiCliCatalog(`${result.stdout ?? ''}\n${result.stderr ?? ''}`);
|
|
56
56
|
if (catalog.groups.length > 0)
|
|
57
57
|
return catalog;
|
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
1
2
|
interface KillableChild {
|
|
2
3
|
readonly pid?: number | null;
|
|
3
4
|
kill(signal?: string): boolean;
|
|
4
5
|
}
|
|
5
6
|
/** Windows 的 npm CLI 通常是 .cmd 包装器,必须经由 shell 才能启动。 */
|
|
6
7
|
export declare const WINDOWS: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* 桌面应用通常不是从登录终端启动,继承到的 PATH 只有系统目录。
|
|
10
|
+
* 先尝试当前进程的 PATH,失败后再从用户登录 Shell 解析命令位置。
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveCommandPath(command: string, run?: typeof spawnSync): string | null;
|
|
13
|
+
/** 把解析出的 CLI 所在目录补到子进程 PATH,确保 npm shim 的 node shebang 可用。 */
|
|
14
|
+
export declare function commandEnvironment(command: string): Record<string, string | undefined>;
|
|
7
15
|
/**
|
|
8
16
|
* 结束 CLI 进程及其子进程树。
|
|
9
17
|
*
|
|
@@ -1,6 +1,54 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
2
3
|
/** Windows 的 npm CLI 通常是 .cmd 包装器,必须经由 shell 才能启动。 */
|
|
3
4
|
export const WINDOWS = process.platform === 'win32';
|
|
5
|
+
let loginShellPath;
|
|
6
|
+
/**
|
|
7
|
+
* 桌面应用通常不是从登录终端启动,继承到的 PATH 只有系统目录。
|
|
8
|
+
* 先尝试当前进程的 PATH,失败后再从用户登录 Shell 解析命令位置。
|
|
9
|
+
*/
|
|
10
|
+
export function resolveCommandPath(command, run = spawnSync) {
|
|
11
|
+
if (isAbsoluteCommand(command))
|
|
12
|
+
return command;
|
|
13
|
+
try {
|
|
14
|
+
const result = WINDOWS
|
|
15
|
+
? run('where.exe', [command], { encoding: 'utf8', timeout: 3_000, windowsHide: true })
|
|
16
|
+
: run(process.env.SHELL || '/bin/sh', ['-ilc', 'command -v "$1"; printf "\\n__CODINGNS_PATH__%s\\n" "$PATH"', 'codingns4dsh-command-lookup', command], { encoding: 'utf8', timeout: 3_000, windowsHide: true });
|
|
17
|
+
if (result.status !== 0)
|
|
18
|
+
return null;
|
|
19
|
+
const output = `${result.stdout ?? ''}\n${result.stderr ?? ''}`;
|
|
20
|
+
const pathMarker = output.match(/(?:^|\n)__CODINGNS_PATH__(.*?)(?:\n|$)/u)?.[1]?.trim();
|
|
21
|
+
if (pathMarker)
|
|
22
|
+
loginShellPath = pathMarker;
|
|
23
|
+
const lines = output
|
|
24
|
+
.split(/\r?\n/u)
|
|
25
|
+
.map((line) => line.trim())
|
|
26
|
+
.filter((line) => line.length > 0);
|
|
27
|
+
for (const line of lines.reverse()) {
|
|
28
|
+
if (isAbsoluteCommand(line))
|
|
29
|
+
return line;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// 登录 Shell 不可用时仍按未安装处理。
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
/** 把解析出的 CLI 所在目录补到子进程 PATH,确保 npm shim 的 node shebang 可用。 */
|
|
38
|
+
export function commandEnvironment(command) {
|
|
39
|
+
const currentPath = loginShellPath ?? process.env.PATH ?? '';
|
|
40
|
+
if (!isAbsoluteCommand(command))
|
|
41
|
+
return { ...process.env };
|
|
42
|
+
const directory = dirname(command);
|
|
43
|
+
const separator = WINDOWS ? ';' : ':';
|
|
44
|
+
const pathEntries = currentPath.split(separator).filter((entry) => entry.length > 0);
|
|
45
|
+
if (!pathEntries.includes(directory))
|
|
46
|
+
pathEntries.unshift(directory);
|
|
47
|
+
return { ...process.env, PATH: pathEntries.join(separator) };
|
|
48
|
+
}
|
|
49
|
+
function isAbsoluteCommand(value) {
|
|
50
|
+
return WINDOWS ? /^[A-Za-z]:[\\/]/u.test(value) || value.startsWith('\\\\') : value.startsWith('/');
|
|
51
|
+
}
|
|
4
52
|
/**
|
|
5
53
|
* 结束 CLI 进程及其子进程树。
|
|
6
54
|
*
|
|
@@ -28,7 +28,12 @@ export declare class CodingNsCliAdapterRegistry {
|
|
|
28
28
|
private readonly modelTimers;
|
|
29
29
|
private readonly modelGenerations;
|
|
30
30
|
private readonly requestedModelCatalogs;
|
|
31
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* 已切到下一个 DSH step、但仍在继续产出 Provider 流的运行。
|
|
33
|
+
*
|
|
34
|
+
* 通用切分由 Registry 持有活迭代器;驱动自行分段的适配器(Codex、Command Code)
|
|
35
|
+
* 把进程和段状态留在驱动内,这里只登记续段意图,用 null 迭代器区分两者。
|
|
36
|
+
*/
|
|
32
37
|
private readonly segmentedTurns;
|
|
33
38
|
private cacheGeneration;
|
|
34
39
|
private disposed;
|