@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
|
@@ -5,7 +5,7 @@ import { join } from 'node:path';
|
|
|
5
5
|
import readline from 'node:readline';
|
|
6
6
|
import { firstToolText, serializeToolValue } from './tool-observation.js';
|
|
7
7
|
import { usageChunk } from './rpc-driver-utils.js';
|
|
8
|
-
import { terminateChildProcess } from './process-utils.js';
|
|
8
|
+
import { commandEnvironment, resolveCommandPath, terminateChildProcess } from './process-utils.js';
|
|
9
9
|
const WINDOWS = process.platform === 'win32';
|
|
10
10
|
const COMMAND_CODE_BINARIES = WINDOWS
|
|
11
11
|
? ['command-code', 'commandcode', 'cmdc']
|
|
@@ -91,8 +91,12 @@ const CATALOG_EFFORTS = new Map([
|
|
|
91
91
|
['xai/grok-4.7', ['low', 'medium', 'high', 'xhigh']],
|
|
92
92
|
]);
|
|
93
93
|
/**
|
|
94
|
-
* Command Code
|
|
95
|
-
*
|
|
94
|
+
* Command Code 驱动:沿用 `--session + -p + --output-format json` 的 NDJSON 事件流。
|
|
95
|
+
*
|
|
96
|
+
* 与 Codex 驱动保持同一套消息优化:正文/推理增量携带 assistant 消息身份,工具完成后
|
|
97
|
+
* 的下一条 assistant 消息之前结束当前 DSH step,因此一个 Provider 运行会被切成多个
|
|
98
|
+
* step,而不是把整轮正文堆积到最后一条结算消息里。驱动只输出公共事件契约,工具历史、
|
|
99
|
+
* usage 和原生组件映射全部交给公共消息投影层。
|
|
96
100
|
*/
|
|
97
101
|
export class CommandCodeDriver {
|
|
98
102
|
descriptor = {
|
|
@@ -101,12 +105,16 @@ export class CommandCodeDriver {
|
|
|
101
105
|
protocol: 'command',
|
|
102
106
|
capabilities: ['models', 'stream', 'resume', 'interrupt', 'tool-events', 'reasoning', 'usage'],
|
|
103
107
|
};
|
|
108
|
+
/** 驱动自己维护 Provider turn 边界,Host 可以把工具边界映射为 DSH step。 */
|
|
109
|
+
supportsSegmentedTurns = true;
|
|
104
110
|
homeDirectory;
|
|
105
111
|
binaries;
|
|
106
112
|
runSpawnSync;
|
|
107
113
|
runSpawn;
|
|
108
114
|
cachedBinary = null;
|
|
115
|
+
cachedEnvironment;
|
|
109
116
|
processes = new Set();
|
|
117
|
+
turns = new Map();
|
|
110
118
|
constructor(options = {}) {
|
|
111
119
|
this.homeDirectory = options.homeDirectory ?? join(homedir(), '.commandcode');
|
|
112
120
|
this.binaries = options.binaries ?? COMMAND_CODE_BINARIES;
|
|
@@ -115,28 +123,47 @@ export class CommandCodeDriver {
|
|
|
115
123
|
}
|
|
116
124
|
async detect() {
|
|
117
125
|
for (const command of this.binaries) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
126
|
+
const direct = this.detectCommand(command);
|
|
127
|
+
if (direct !== null)
|
|
128
|
+
return direct;
|
|
129
|
+
if (!this.lookupAfterDetectionFailure)
|
|
130
|
+
continue;
|
|
131
|
+
const resolved = resolveCommandPath(command, this.runSpawnSync);
|
|
132
|
+
if (resolved === null)
|
|
133
|
+
continue;
|
|
134
|
+
const fallback = this.detectCommand(resolved);
|
|
135
|
+
if (fallback !== null)
|
|
136
|
+
return fallback;
|
|
130
137
|
}
|
|
131
138
|
return { installed: false, version: null, command: null };
|
|
132
139
|
}
|
|
140
|
+
lookupAfterDetectionFailure = false;
|
|
141
|
+
detectCommand(command) {
|
|
142
|
+
this.lookupAfterDetectionFailure = false;
|
|
143
|
+
try {
|
|
144
|
+
const result = this.runSpawnSync(command, ['--version'], { encoding: 'utf8', timeout: 3_000, windowsHide: true, shell: WINDOWS, env: commandEnvironment(command) });
|
|
145
|
+
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`.trim();
|
|
146
|
+
const version = output.match(/\d+\.\d+\.\d+/u)?.[0] ?? null;
|
|
147
|
+
if (result.status === 0 && version !== null) {
|
|
148
|
+
this.cachedBinary = command;
|
|
149
|
+
this.cachedEnvironment = commandEnvironment(command);
|
|
150
|
+
return { installed: true, version, command };
|
|
151
|
+
}
|
|
152
|
+
this.lookupAfterDetectionFailure = result.status === null;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// PATH 中不存在候选命令属于正常的未安装状态。
|
|
156
|
+
this.lookupAfterDetectionFailure = true;
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
133
160
|
async listModels() {
|
|
134
161
|
const detection = await this.detect();
|
|
135
162
|
if (!detection.installed || detection.command === null)
|
|
136
163
|
return emptyCatalog();
|
|
137
164
|
let stdout = '';
|
|
138
165
|
try {
|
|
139
|
-
const result = this.runSpawnSync(detection.command, ['--list-models'], { encoding: 'utf8', timeout: 12_000, windowsHide: true, shell: WINDOWS });
|
|
166
|
+
const result = this.runSpawnSync(detection.command, ['--list-models'], { encoding: 'utf8', timeout: 12_000, windowsHide: true, shell: WINDOWS, ...(this.cachedEnvironment === undefined ? {} : { env: this.cachedEnvironment }) });
|
|
140
167
|
stdout = result.stdout ?? '';
|
|
141
168
|
}
|
|
142
169
|
catch {
|
|
@@ -177,6 +204,52 @@ export class CommandCodeDriver {
|
|
|
177
204
|
const binary = this.cachedBinary ?? (await this.detect()).command;
|
|
178
205
|
if (binary === null)
|
|
179
206
|
throw new Error('Command Code 未安装');
|
|
207
|
+
const segmented = input.splitToolSteps === true;
|
|
208
|
+
const turn = segmented ? this.acquireTurn(input, binary) : this.startTurn(input, binary);
|
|
209
|
+
let suspended = false;
|
|
210
|
+
const onAbort = () => { turn.aborted = true; this.disposeTurn(turn); };
|
|
211
|
+
input.signal?.addEventListener('abort', onAbort, { once: true });
|
|
212
|
+
if (input.signal?.aborted)
|
|
213
|
+
onAbort();
|
|
214
|
+
try {
|
|
215
|
+
yield* this.consumeTurn(turn, input, segmented, () => { suspended = true; });
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
input.signal?.removeEventListener('abort', onAbort);
|
|
219
|
+
// 只有驱动自己为下一个 DSH step 挂起时才保留进程;正常结束、取消和调用方
|
|
220
|
+
// 提前关闭流都必须回收 CLI 进程与临时 transcript。
|
|
221
|
+
if (!suspended)
|
|
222
|
+
this.disposeTurn(turn);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
dispose() {
|
|
226
|
+
for (const turn of [...this.turns.values()])
|
|
227
|
+
this.disposeTurn(turn);
|
|
228
|
+
this.turns.clear();
|
|
229
|
+
for (const child of this.processes)
|
|
230
|
+
terminateChildProcess(child);
|
|
231
|
+
this.processes.clear();
|
|
232
|
+
this.cachedBinary = null;
|
|
233
|
+
}
|
|
234
|
+
/** 丢弃等待下一个 DSH step 的 `-p` 运行,避免旧进程继续占用新一轮输出。 */
|
|
235
|
+
discardSegmentedTurn(sessionId) {
|
|
236
|
+
const turn = this.turns.get(sessionId);
|
|
237
|
+
if (turn !== undefined)
|
|
238
|
+
this.disposeTurn(turn);
|
|
239
|
+
}
|
|
240
|
+
/** 只有 Host 显式声明续段时才复用常驻进程;新的用户回合必须重新启动。 */
|
|
241
|
+
acquireTurn(input, binary) {
|
|
242
|
+
const existing = this.turns.get(input.sessionId);
|
|
243
|
+
if (existing !== undefined) {
|
|
244
|
+
if (input.resumeSegmentedTurn === true && !existing.disposed && !existing.finished)
|
|
245
|
+
return existing;
|
|
246
|
+
this.disposeTurn(existing);
|
|
247
|
+
}
|
|
248
|
+
const turn = this.startTurn(input, binary);
|
|
249
|
+
this.turns.set(input.sessionId, turn);
|
|
250
|
+
return turn;
|
|
251
|
+
}
|
|
252
|
+
startTurn(input, binary) {
|
|
180
253
|
const transcriptPath = join(tmpdir(), `codingns4dsh-cc-${safeId(input.sessionId)}.jsonl`);
|
|
181
254
|
writeTranscript(transcriptPath, input);
|
|
182
255
|
const args = ['--session', transcriptPath, '-p', input.prompt, '--output-format', 'json', '--tools-all', '--yolo'];
|
|
@@ -184,15 +257,39 @@ export class CommandCodeDriver {
|
|
|
184
257
|
args.push('-m', input.modelId);
|
|
185
258
|
if (input.effortId && input.effortId !== 'default' && input.effortId !== 'Default')
|
|
186
259
|
args.push('--effort', input.effortId);
|
|
187
|
-
const child = this.runSpawn(binary, args, { cwd: input.cwd ?? process.cwd(), env:
|
|
260
|
+
const child = this.runSpawn(binary, args, { cwd: input.cwd ?? process.cwd(), env: this.cachedEnvironment ?? commandEnvironment(binary), stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, shell: WINDOWS });
|
|
188
261
|
this.processes.add(child);
|
|
189
|
-
let finished = false;
|
|
190
|
-
const onAbort = () => { terminateChildProcess(child); };
|
|
191
|
-
input.signal?.addEventListener('abort', onAbort, { once: true });
|
|
192
262
|
// 必须消费 stderr,错误内容不能回传给 DSH,避免泄露命令参数或文件片段。
|
|
193
|
-
child.stderr
|
|
263
|
+
child.stderr?.on('data', () => undefined);
|
|
264
|
+
const turn = {
|
|
265
|
+
sessionId: input.sessionId,
|
|
266
|
+
child,
|
|
267
|
+
transcriptPath,
|
|
268
|
+
queue: createEventQueue(),
|
|
269
|
+
currentMessageId: undefined,
|
|
270
|
+
sawCompletedTool: false,
|
|
271
|
+
pendingChunk: undefined,
|
|
272
|
+
terminal: false,
|
|
273
|
+
finished: false,
|
|
274
|
+
aborted: false,
|
|
275
|
+
failure: null,
|
|
276
|
+
disposed: false,
|
|
277
|
+
};
|
|
278
|
+
// 未监听 error 的 ChildProcess 会把 spawn 失败升级成宿主进程异常。
|
|
279
|
+
if (typeof child.on === 'function') {
|
|
280
|
+
child.on('error', (error) => {
|
|
281
|
+
turn.failure ??= error;
|
|
282
|
+
turn.queue.close();
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
void this.pumpTurn(turn);
|
|
286
|
+
return turn;
|
|
287
|
+
}
|
|
288
|
+
/** 常驻读取 stdout:NDJSON 转成公共事件,进程结束后关闭队列。 */
|
|
289
|
+
async pumpTurn(turn) {
|
|
290
|
+
const state = createStreamState(() => turn.aborted);
|
|
194
291
|
try {
|
|
195
|
-
const lines = readline.createInterface({ input: child.stdout });
|
|
292
|
+
const lines = readline.createInterface({ input: turn.child.stdout });
|
|
196
293
|
try {
|
|
197
294
|
for await (const line of lines) {
|
|
198
295
|
if (!line.trim())
|
|
@@ -201,62 +298,234 @@ export class CommandCodeDriver {
|
|
|
201
298
|
if (item === null)
|
|
202
299
|
continue;
|
|
203
300
|
const event = item.type === 'event' && isRecord(item.event) ? item.event : item;
|
|
204
|
-
const
|
|
205
|
-
const chunks = commandCodeEventChunks(event, input.signal?.aborted ?? false);
|
|
206
|
-
for (const chunk of chunks) {
|
|
301
|
+
for (const chunk of commandCodeEventChunks(event, state)) {
|
|
207
302
|
if (chunk.type === 'finish')
|
|
208
|
-
|
|
209
|
-
|
|
303
|
+
turn.terminal = true;
|
|
304
|
+
turn.queue.push(chunk);
|
|
210
305
|
}
|
|
211
|
-
if (eventType === 'result' && !finished)
|
|
212
|
-
finished = true;
|
|
213
306
|
}
|
|
214
307
|
}
|
|
215
308
|
finally {
|
|
216
309
|
lines.close();
|
|
217
310
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
else
|
|
222
|
-
throw new Error('Command Code 执行失败');
|
|
223
|
-
}
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
turn.failure ??= error instanceof Error ? error : new Error(String(error));
|
|
224
314
|
}
|
|
225
315
|
finally {
|
|
226
|
-
|
|
227
|
-
this.processes.delete(child);
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
316
|
+
turn.queue.close();
|
|
317
|
+
this.processes.delete(turn.child);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async *consumeTurn(turn, input, segmented, suspend) {
|
|
321
|
+
while (true) {
|
|
322
|
+
let chunk;
|
|
323
|
+
if (turn.pendingChunk !== undefined) {
|
|
324
|
+
chunk = turn.pendingChunk;
|
|
325
|
+
turn.pendingChunk = undefined;
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
const next = await turn.queue.next();
|
|
329
|
+
if (next.done) {
|
|
330
|
+
if (turn.failure !== null)
|
|
331
|
+
throw turn.failure;
|
|
332
|
+
if (turn.terminal)
|
|
333
|
+
return;
|
|
334
|
+
if (turn.aborted || input.signal?.aborted === true) {
|
|
335
|
+
yield { type: 'finish', reason: 'cancel' };
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
throw new Error('Command Code 执行失败');
|
|
339
|
+
}
|
|
340
|
+
chunk = next.value;
|
|
341
|
+
}
|
|
342
|
+
// 一个 `-p` 运行会在同一进程里连续跑多个 agent turn。把新 assistant 消息的
|
|
343
|
+
// 首个正文留给下一次 llm/stream,当前流只返回边界,确保 DSH 先创建新 step。
|
|
344
|
+
if (segmented
|
|
345
|
+
&& (chunk.type === 'text-delta' || chunk.type === 'reasoning-delta')
|
|
346
|
+
&& chunk.messageId !== undefined) {
|
|
347
|
+
const previousMessageId = turn.currentMessageId;
|
|
348
|
+
if (previousMessageId !== undefined
|
|
349
|
+
&& previousMessageId !== chunk.messageId
|
|
350
|
+
&& turn.sawCompletedTool) {
|
|
351
|
+
turn.pendingChunk = chunk;
|
|
352
|
+
turn.currentMessageId = chunk.messageId;
|
|
353
|
+
turn.sawCompletedTool = false;
|
|
354
|
+
suspend();
|
|
355
|
+
yield { type: 'step-boundary' };
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
turn.currentMessageId = chunk.messageId;
|
|
359
|
+
}
|
|
360
|
+
if (chunk.type === 'tool-event' && (chunk.status === 'completed' || chunk.status === 'failed')) {
|
|
361
|
+
turn.sawCompletedTool = true;
|
|
231
362
|
}
|
|
232
|
-
|
|
363
|
+
if (chunk.type === 'finish')
|
|
364
|
+
turn.finished = true;
|
|
365
|
+
yield chunk;
|
|
233
366
|
}
|
|
234
367
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
368
|
+
disposeTurn(turn) {
|
|
369
|
+
if (turn.disposed)
|
|
370
|
+
return;
|
|
371
|
+
turn.disposed = true;
|
|
372
|
+
turn.queue.close();
|
|
373
|
+
if (this.turns.get(turn.sessionId) === turn)
|
|
374
|
+
this.turns.delete(turn.sessionId);
|
|
375
|
+
this.processes.delete(turn.child);
|
|
376
|
+
terminateChildProcess(turn.child);
|
|
377
|
+
try {
|
|
378
|
+
rmSync(turn.transcriptPath, { force: true });
|
|
379
|
+
}
|
|
380
|
+
catch { /* 临时文件清理尽力而为 */ }
|
|
240
381
|
}
|
|
241
382
|
}
|
|
242
|
-
function
|
|
383
|
+
function createStreamState(aborted) {
|
|
384
|
+
return {
|
|
385
|
+
messageSequence: 0,
|
|
386
|
+
messageId: null,
|
|
387
|
+
emittedText: '',
|
|
388
|
+
emittedReasoning: '',
|
|
389
|
+
sawText: false,
|
|
390
|
+
perRequestUsageSeen: false,
|
|
391
|
+
turnUsageSeen: false,
|
|
392
|
+
sessionId: null,
|
|
393
|
+
aborted,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
function createEventQueue() {
|
|
397
|
+
const values = [];
|
|
398
|
+
const waiters = [];
|
|
399
|
+
let closed = false;
|
|
400
|
+
const next = () => {
|
|
401
|
+
const value = values.shift();
|
|
402
|
+
if (value !== undefined)
|
|
403
|
+
return Promise.resolve({ done: false, value });
|
|
404
|
+
if (closed)
|
|
405
|
+
return Promise.resolve({ done: true, value: undefined });
|
|
406
|
+
return new Promise((resolve) => waiters.push(resolve));
|
|
407
|
+
};
|
|
408
|
+
return {
|
|
409
|
+
next,
|
|
410
|
+
push(event) {
|
|
411
|
+
if (closed)
|
|
412
|
+
return;
|
|
413
|
+
const waiter = waiters.shift();
|
|
414
|
+
if (waiter !== undefined)
|
|
415
|
+
waiter({ done: false, value: event });
|
|
416
|
+
else
|
|
417
|
+
values.push(event);
|
|
418
|
+
},
|
|
419
|
+
close() {
|
|
420
|
+
if (closed)
|
|
421
|
+
return;
|
|
422
|
+
closed = true;
|
|
423
|
+
while (waiters.length > 0)
|
|
424
|
+
waiters.shift()?.({ done: true, value: undefined });
|
|
425
|
+
},
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Command Code NDJSON → 公共 Agent 事件。
|
|
430
|
+
*
|
|
431
|
+
* 关键约定(对照本机 1.66.0 真实事件流):
|
|
432
|
+
* - `turn_start` / `message_start` 标识新的 assistant 消息,正文和推理增量都带上它;
|
|
433
|
+
* - `tool_*` 使用 `toolCallId` 作为稳定调用 ID,同一调用的 queued/running/completed
|
|
434
|
+
* 只能投影成一个 DSH 工具节点;
|
|
435
|
+
* - `model_request_end` 的 usage 是当前请求的上下文占用;`run_end`/最终 `result`
|
|
436
|
+
* 的 usage 是整个运行的累计值,只在完全没有单请求 usage 时兜底。
|
|
437
|
+
*/
|
|
438
|
+
function commandCodeEventChunks(event, state) {
|
|
243
439
|
const chunks = [];
|
|
244
440
|
const type = textValue(event.type).trim().toLowerCase();
|
|
245
|
-
const sessionId =
|
|
246
|
-
if (sessionId)
|
|
441
|
+
const sessionId = readSessionId(event);
|
|
442
|
+
if (sessionId !== null && sessionId !== state.sessionId) {
|
|
443
|
+
state.sessionId = sessionId;
|
|
247
444
|
chunks.push({ type: 'session-binding', providerSessionId: sessionId });
|
|
248
|
-
if (type === 'thinking_delta' || type === 'thinking-delta') {
|
|
249
|
-
const text = textValue(event.delta ?? event.thinking ?? event.content);
|
|
250
|
-
if (text)
|
|
251
|
-
chunks.push({ type: 'reasoning-delta', text });
|
|
252
|
-
}
|
|
253
|
-
else if (type === 'text_delta' || type === 'text-delta') {
|
|
254
|
-
const text = textValue(event.delta ?? event.text ?? event.content);
|
|
255
|
-
if (text)
|
|
256
|
-
chunks.push({ type: 'text-delta', text });
|
|
257
445
|
}
|
|
258
|
-
|
|
259
|
-
|
|
446
|
+
switch (type) {
|
|
447
|
+
case 'turn_start':
|
|
448
|
+
case 'turn-start':
|
|
449
|
+
beginMessage(state);
|
|
450
|
+
break;
|
|
451
|
+
case 'message_start':
|
|
452
|
+
case 'message-start':
|
|
453
|
+
// 正常一轮只有一个模型请求;没有 turn_start 的旧版本由这里补消息身份。
|
|
454
|
+
if (state.messageId === null)
|
|
455
|
+
beginMessage(state);
|
|
456
|
+
break;
|
|
457
|
+
case 'text_delta':
|
|
458
|
+
case 'text-delta': {
|
|
459
|
+
const text = textValue(event.delta ?? event.text ?? event.content);
|
|
460
|
+
if (text) {
|
|
461
|
+
ensureMessage(state);
|
|
462
|
+
state.emittedText += text;
|
|
463
|
+
state.sawText = true;
|
|
464
|
+
chunks.push(textDelta(text, state));
|
|
465
|
+
}
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
case 'thinking_delta':
|
|
469
|
+
case 'thinking-delta': {
|
|
470
|
+
const text = textValue(event.delta ?? event.thinking ?? event.content);
|
|
471
|
+
if (text) {
|
|
472
|
+
ensureMessage(state);
|
|
473
|
+
state.emittedReasoning += text;
|
|
474
|
+
chunks.push(reasoningDelta(text, state));
|
|
475
|
+
}
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
case 'message':
|
|
479
|
+
case 'message_update':
|
|
480
|
+
case 'message-update':
|
|
481
|
+
case 'message_end':
|
|
482
|
+
case 'message-end':
|
|
483
|
+
// 只补齐增量流没有覆盖到的正文,避免累计快照被重复追加。
|
|
484
|
+
appendContentFallback(chunks, event.content ?? recordValue(event.message)?.content, state);
|
|
485
|
+
break;
|
|
486
|
+
case 'model_request_end':
|
|
487
|
+
case 'model-request-end': {
|
|
488
|
+
const usage = usageChunk(recordValue(event.usage));
|
|
489
|
+
if (usage !== null) {
|
|
490
|
+
state.perRequestUsageSeen = true;
|
|
491
|
+
state.turnUsageSeen = true;
|
|
492
|
+
chunks.push(usage);
|
|
493
|
+
}
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
case 'turn_end':
|
|
497
|
+
case 'turn-end': {
|
|
498
|
+
// 旧版本可能只在 turn_end 带 usage;与 model_request_end 去重,不重复结算。
|
|
499
|
+
if (!state.turnUsageSeen) {
|
|
500
|
+
const usage = usageChunk(recordValue(event.usage));
|
|
501
|
+
if (usage !== null) {
|
|
502
|
+
state.perRequestUsageSeen = true;
|
|
503
|
+
state.turnUsageSeen = true;
|
|
504
|
+
chunks.push(usage);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
case 'result': {
|
|
510
|
+
if (!state.perRequestUsageSeen) {
|
|
511
|
+
const usage = usageChunk(recordValue(event.usage));
|
|
512
|
+
if (usage !== null)
|
|
513
|
+
chunks.push(usage);
|
|
514
|
+
}
|
|
515
|
+
if (!state.sawText) {
|
|
516
|
+
const finalText = textValue(event.finalText ?? recordValue(event.result)?.finalText ?? (typeof event.result === 'string' ? event.result : undefined));
|
|
517
|
+
if (finalText) {
|
|
518
|
+
ensureMessage(state);
|
|
519
|
+
state.sawText = true;
|
|
520
|
+
state.emittedText += finalText;
|
|
521
|
+
chunks.push(textDelta(finalText, state));
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
chunks.push({ type: 'finish', reason: state.aborted() ? 'cancel' : resultReason(event) });
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
default:
|
|
528
|
+
break;
|
|
260
529
|
}
|
|
261
530
|
if (isToolStart(type)) {
|
|
262
531
|
const tool = readToolChunk(event, type === 'tool_queued' || type === 'tool_started' ? 'started' : 'running');
|
|
@@ -264,62 +533,82 @@ function commandCodeEventChunks(event, cancelled) {
|
|
|
264
533
|
chunks.push(tool);
|
|
265
534
|
}
|
|
266
535
|
else if (isToolResult(type)) {
|
|
267
|
-
const
|
|
268
|
-
|
|
536
|
+
const failed = type.includes('error') || type.includes('fail') || type.includes('denied') || type.includes('blocked');
|
|
537
|
+
const tool = readToolChunk(event, failed ? 'failed' : 'completed');
|
|
538
|
+
if (tool !== null) {
|
|
269
539
|
chunks.push(tool);
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
chunks.push(usageEvent);
|
|
276
|
-
if (type === 'result') {
|
|
277
|
-
const finalText = textValue(event.finalText ?? resultRecord?.finalText ?? (typeof event.result === 'string' ? event.result : undefined) ?? event.output ?? event.text);
|
|
278
|
-
if (finalText)
|
|
279
|
-
chunks.push({ type: 'text-snapshot', text: finalText });
|
|
280
|
-
chunks.push({ type: 'finish', reason: cancelled ? 'cancel' : 'stop' });
|
|
540
|
+
// 工具完成后的正文属于下一条 assistant 消息。即使 CLI 没有发送
|
|
541
|
+
// turn_start/message_start,也要让公共投影层看到消息身份切换。
|
|
542
|
+
if (tool.status === 'completed' || tool.status === 'failed')
|
|
543
|
+
resetMessageIdentity(state);
|
|
544
|
+
}
|
|
281
545
|
}
|
|
282
546
|
return chunks;
|
|
283
547
|
}
|
|
284
|
-
function
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
548
|
+
function beginMessage(state) {
|
|
549
|
+
state.messageSequence += 1;
|
|
550
|
+
state.messageId = `command-code-message-${state.messageSequence}`;
|
|
551
|
+
state.emittedText = '';
|
|
552
|
+
state.emittedReasoning = '';
|
|
553
|
+
state.turnUsageSeen = false;
|
|
554
|
+
}
|
|
555
|
+
/** 丢弃当前消息身份;下一条正文增量会懒加载新的身份。 */
|
|
556
|
+
function resetMessageIdentity(state) {
|
|
557
|
+
state.messageId = null;
|
|
558
|
+
state.emittedText = '';
|
|
559
|
+
state.emittedReasoning = '';
|
|
560
|
+
}
|
|
561
|
+
function ensureMessage(state) {
|
|
562
|
+
if (state.messageId === null)
|
|
563
|
+
beginMessage(state);
|
|
564
|
+
}
|
|
565
|
+
function textDelta(text, state) {
|
|
566
|
+
return { type: 'text-delta', text, ...(state.messageId === null ? {} : { messageId: state.messageId }) };
|
|
567
|
+
}
|
|
568
|
+
function reasoningDelta(text, state) {
|
|
569
|
+
return { type: 'reasoning-delta', text, ...(state.messageId === null ? {} : { messageId: state.messageId }) };
|
|
570
|
+
}
|
|
571
|
+
/** 用完整的 assistant content 补齐缺失的正文/推理增量(只发送尚未发送的尾部)。 */
|
|
572
|
+
function appendContentFallback(chunks, content, state) {
|
|
573
|
+
if (!Array.isArray(content))
|
|
305
574
|
return;
|
|
575
|
+
let text = '';
|
|
576
|
+
let reasoning = '';
|
|
577
|
+
for (const block of content) {
|
|
578
|
+
const value = recordValue(block);
|
|
579
|
+
if (value === null)
|
|
580
|
+
continue;
|
|
581
|
+
const blockType = textValue(value.type).toLowerCase();
|
|
582
|
+
const isReasoning = blockType.includes('thinking') || blockType.includes('reasoning')
|
|
583
|
+
|| typeof value.thinking === 'string' || typeof value.reasoning === 'string';
|
|
584
|
+
if (isReasoning)
|
|
585
|
+
reasoning += textValue(value.thinking ?? value.reasoning ?? value.text ?? value.content);
|
|
586
|
+
else
|
|
587
|
+
text += textValue(value.text ?? value.content);
|
|
588
|
+
}
|
|
589
|
+
if (text.length > state.emittedText.length) {
|
|
590
|
+
const delta = text.slice(state.emittedText.length);
|
|
591
|
+
ensureMessage(state);
|
|
592
|
+
state.emittedText = text;
|
|
593
|
+
state.sawText = true;
|
|
594
|
+
chunks.push(textDelta(delta, state));
|
|
595
|
+
}
|
|
596
|
+
if (reasoning.length > state.emittedReasoning.length) {
|
|
597
|
+
const delta = reasoning.slice(state.emittedReasoning.length);
|
|
598
|
+
ensureMessage(state);
|
|
599
|
+
state.emittedReasoning = reasoning;
|
|
600
|
+
chunks.push(reasoningDelta(delta, state));
|
|
306
601
|
}
|
|
307
|
-
const reasoning = textValue(payload.thinking ?? payload.reasoning);
|
|
308
|
-
if (reasoning)
|
|
309
|
-
chunks.push({ type: 'reasoning-snapshot', text: reasoning });
|
|
310
|
-
const text = textValue(payload.text ?? payload.content ?? event.text ?? event.content);
|
|
311
|
-
if (text)
|
|
312
|
-
chunks.push({ type: 'text-snapshot', text });
|
|
313
602
|
}
|
|
314
603
|
function readToolChunk(event, status) {
|
|
315
|
-
const callId =
|
|
604
|
+
const callId = firstToolText(event.callId, event.call_id, event.toolCallId, event.tool_call_id, event.toolUseId, event.tool_use_id, event.id);
|
|
316
605
|
const fn = recordValue(event.function);
|
|
317
|
-
const toolName = textValue(event.name ?? event.toolName ?? event.tool ?? fn?.name) || 'tool';
|
|
318
|
-
const error = textValue(event.error ?? event.reason);
|
|
606
|
+
const toolName = textValue(event.name ?? event.toolName ?? event.tool_name ?? event.tool ?? fn?.name) || 'tool';
|
|
607
|
+
const error = textValue(event.error ?? event.reason ?? event.message);
|
|
319
608
|
const output = textValue(event.output ?? event.result ?? event.content);
|
|
320
609
|
const input = event.input ?? fn?.arguments ?? event.arguments;
|
|
321
610
|
const agentId = firstToolText(event.agentId, event.agent_id);
|
|
322
|
-
const detail = serializeToolValue(event.detail ?? event.metadata);
|
|
611
|
+
const detail = serializeToolValue(event.detail ?? event.metadata ?? event.description);
|
|
323
612
|
if (!callId && !toolName)
|
|
324
613
|
return null;
|
|
325
614
|
return {
|
|
@@ -339,7 +628,21 @@ function isToolStart(type) {
|
|
|
339
628
|
return ['tool_queued', 'tool_started', 'tool_running', 'tool_use', 'tool_call', 'function_call'].includes(type);
|
|
340
629
|
}
|
|
341
630
|
function isToolResult(type) {
|
|
342
|
-
return ['tool_completed', 'tool_result', 'tool_return', 'tool_failed', 'tool_error', 'tool_denied', 'function_result'].includes(type);
|
|
631
|
+
return ['tool_completed', 'tool_result', 'tool_return', 'tool_failed', 'tool_error', 'tool_denied', 'tool_hook_blocked', 'function_result'].includes(type);
|
|
632
|
+
}
|
|
633
|
+
function resultReason(event) {
|
|
634
|
+
const resultRecord = recordValue(event.result);
|
|
635
|
+
const stopReason = textValue(event.stopReason ?? resultRecord?.stopReason).toLowerCase();
|
|
636
|
+
const subtype = textValue(event.subtype).toLowerCase();
|
|
637
|
+
if (event.error !== undefined || subtype === 'error' || stopReason.includes('error') || stopReason.includes('fail'))
|
|
638
|
+
return 'error';
|
|
639
|
+
if (stopReason.includes('interrupt') || stopReason.includes('cancel') || stopReason === 'aborted')
|
|
640
|
+
return 'cancel';
|
|
641
|
+
return 'stop';
|
|
642
|
+
}
|
|
643
|
+
function readSessionId(event) {
|
|
644
|
+
const value = textValue(event.sessionId ?? event.session_id ?? recordValue(event.session)?.id ?? recordValue(event.result)?.sessionId).trim();
|
|
645
|
+
return value === '' ? null : value;
|
|
343
646
|
}
|
|
344
647
|
function textValue(value) {
|
|
345
648
|
return typeof value === 'string' ? value : value === undefined || value === null ? '' : structuredText(value);
|
|
@@ -29,6 +29,8 @@ export interface CodingNsCliDriver {
|
|
|
29
29
|
readonly descriptor: Omit<CodingNsCliAdapterDescriptor, 'installed' | 'enabled' | 'version' | 'command'>;
|
|
30
30
|
/** 驱动是否已经自行维护 Provider turn 的 step 边界。 */
|
|
31
31
|
readonly supportsSegmentedTurns?: boolean;
|
|
32
|
+
/** 丢弃等待下一个 DSH step 的 Provider 运行;只有自行分段的驱动需要实现。 */
|
|
33
|
+
discardSegmentedTurn?(sessionId: string): void;
|
|
32
34
|
detect(): Promise<Pick<CodingNsCliAdapterDescriptor, 'installed' | 'version' | 'command'>>;
|
|
33
35
|
listModels(): Promise<CodingNsCliModelCatalog>;
|
|
34
36
|
probeSession?(input: CodingNsCliSessionProbeInput): Promise<CodingNsCliSessionProbeResult>;
|
|
@@ -27,6 +27,8 @@ export declare class CodingNsDshMessageProjector {
|
|
|
27
27
|
private reasoningText;
|
|
28
28
|
private textText;
|
|
29
29
|
private finished;
|
|
30
|
+
/** 只要收到正文、工具、usage 或交互事件,就说明 Provider 确实产出了结果。 */
|
|
31
|
+
private receivedProviderEvent;
|
|
30
32
|
constructor(options: CodingNsDshMessageProjectorOptions);
|
|
31
33
|
get isFinished(): boolean;
|
|
32
34
|
push(event: CodingNsAgentEvent): Promise<readonly CodingNsDshStreamChunk[]>;
|