@jingyi0605/codingns4dsh 0.1.2 → 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.
Files changed (171) hide show
  1. package/README.en.md +4 -4
  2. package/README.md +14 -5
  3. package/data/build/dist/client/account-bar.js +57 -3
  4. package/data/build/dist/client/bundle.js +36246 -268
  5. package/data/build/dist/client/cli-slots.js +60 -18
  6. package/data/build/dist/client/dsh-h5-bootstrap.d.ts +2 -0
  7. package/data/build/dist/client/dsh-h5-bootstrap.js +79 -13
  8. package/data/build/dist/client/features/file-management-panel.d.ts +5 -0
  9. package/data/build/dist/client/features/file-management-panel.js +48 -0
  10. package/data/build/dist/client/features/file-management.d.ts +4 -0
  11. package/data/build/dist/client/features/file-management.js +50 -0
  12. package/data/build/dist/client/features/index.d.ts +10 -1
  13. package/data/build/dist/client/features/index.js +12 -1
  14. package/data/build/dist/client/features/login-protection-panel.js +1 -1
  15. package/data/build/dist/client/features/login-protection-session.d.ts +2 -0
  16. package/data/build/dist/client/features/login-protection-session.js +18 -0
  17. package/data/build/dist/client/features/peer-host.d.ts +4 -0
  18. package/data/build/dist/client/features/peer-host.js +89 -0
  19. package/data/build/dist/client/features/reverse-proxy-panel.js +23 -7
  20. package/data/build/dist/client/features/reverse-proxy.d.ts +1 -2
  21. package/data/build/dist/client/features/reverse-proxy.js +75 -5
  22. package/data/build/dist/client/features/types.d.ts +9 -0
  23. package/data/build/dist/client/features/workspace-session-enhancement-panel.js +10 -0
  24. package/data/build/dist/client/features/workspace-session-enhancement.js +18 -1
  25. package/data/build/dist/client/file-management-dom.d.ts +12 -0
  26. package/data/build/dist/client/file-management-dom.js +527 -0
  27. package/data/build/dist/client/git-management.js +24 -5
  28. package/data/build/dist/client/host-navigation.d.ts +29 -0
  29. package/data/build/dist/client/host-navigation.js +29 -0
  30. package/data/build/dist/client/host-router.d.ts +3 -0
  31. package/data/build/dist/client/host-router.js +2 -0
  32. package/data/build/dist/client/index.d.ts +6 -1
  33. package/data/build/dist/client/index.js +30 -10
  34. package/data/build/dist/client/locale.js +36 -7
  35. package/data/build/dist/client/peer-host-connection-button.d.ts +13 -0
  36. package/data/build/dist/client/peer-host-connection-button.js +69 -0
  37. package/data/build/dist/client/peer-host-management-api.d.ts +40 -0
  38. package/data/build/dist/client/peer-host-management-api.js +33 -0
  39. package/data/build/dist/client/peer-host-management-panel.d.ts +13 -0
  40. package/data/build/dist/client/peer-host-management-panel.js +181 -0
  41. package/data/build/dist/client/peer-host-native-session-ui.d.ts +60 -0
  42. package/data/build/dist/client/peer-host-native-session-ui.js +372 -0
  43. package/data/build/dist/client/peer-host-scoped-client.d.ts +67 -0
  44. package/data/build/dist/client/peer-host-scoped-client.js +316 -0
  45. package/data/build/dist/client/peer-host-session-controller.d.ts +30 -0
  46. package/data/build/dist/client/peer-host-session-controller.js +95 -0
  47. package/data/build/dist/client/quick-phrase-slot.js +14 -0
  48. package/data/build/dist/client/remote-web-context.js +26 -4
  49. package/data/build/dist/client/session-changed-files-view.d.ts +15 -0
  50. package/data/build/dist/client/session-changed-files-view.js +337 -0
  51. package/data/build/dist/client/settings-bridge.d.ts +14 -4
  52. package/data/build/dist/client/settings-bridge.js +59 -12
  53. package/data/build/dist/client/settings-section.js +17 -4
  54. package/data/build/dist/client/subscription-slot.js +21 -8
  55. package/data/build/dist/client/workspace-session-rightbar-dom.d.ts +28 -0
  56. package/data/build/dist/client/workspace-session-rightbar-dom.js +291 -0
  57. package/data/build/dist/dsh-capabilities/client/config-forms-adapter.js +25 -33
  58. package/data/build/dist/dsh-capabilities/client/settings-scope-adapter.d.ts +7 -1
  59. package/data/build/dist/dsh-capabilities/client/settings-scope-adapter.js +34 -11
  60. package/data/build/dist/dsh-capabilities/matrix.js +9 -0
  61. package/data/build/dist/dsh-capabilities/routes.js +35 -0
  62. package/data/build/dist/dsh-capabilities/settings-store.d.ts +12 -0
  63. package/data/build/dist/dsh-capabilities/settings-store.js +34 -0
  64. package/data/build/dist/dsh-capabilities/types.d.ts +1 -1
  65. package/data/build/dist/features/host-router.d.ts +26 -0
  66. package/data/build/dist/features/host-router.js +110 -0
  67. package/data/build/dist/features/registry.js +3 -0
  68. package/data/build/dist/host/cli-adapters/codex-driver.d.ts +13 -1
  69. package/data/build/dist/host/cli-adapters/codex-driver.js +528 -72
  70. package/data/build/dist/host/cli-adapters/command-code-driver.d.ts +21 -2
  71. package/data/build/dist/host/cli-adapters/command-code-driver.js +408 -105
  72. package/data/build/dist/host/cli-adapters/driver.d.ts +2 -0
  73. package/data/build/dist/host/cli-adapters/dsh-message-projector.d.ts +2 -0
  74. package/data/build/dist/host/cli-adapters/dsh-message-projector.js +47 -28
  75. package/data/build/dist/host/cli-adapters/dsh-tool-history.js +37 -0
  76. package/data/build/dist/host/cli-adapters/feature.js +94 -10
  77. package/data/build/dist/host/cli-adapters/json-rpc-process.d.ts +6 -0
  78. package/data/build/dist/host/cli-adapters/json-rpc-process.js +14 -3
  79. package/data/build/dist/host/cli-adapters/kimi-driver.js +3 -3
  80. package/data/build/dist/host/cli-adapters/model-catalog.d.ts +6 -0
  81. package/data/build/dist/host/cli-adapters/model-catalog.js +8 -0
  82. package/data/build/dist/host/cli-adapters/opencode-driver.js +14 -2
  83. package/data/build/dist/host/cli-adapters/pi-driver.js +2 -2
  84. package/data/build/dist/host/cli-adapters/process-utils.d.ts +8 -0
  85. package/data/build/dist/host/cli-adapters/process-utils.js +48 -0
  86. package/data/build/dist/host/cli-adapters/registry.d.ts +6 -1
  87. package/data/build/dist/host/cli-adapters/registry.js +41 -7
  88. package/data/build/dist/host/cli-adapters/rpc-driver-utils.js +23 -4
  89. package/data/build/dist/host/cli-adapters/standard-stream-driver.d.ts +3 -0
  90. package/data/build/dist/host/cli-adapters/standard-stream-driver.js +36 -15
  91. package/data/build/dist/host/dsh-device-runtime.d.ts +2 -0
  92. package/data/build/dist/host/dsh-device-runtime.js +55 -29
  93. package/data/build/dist/host/dsh-runtime-version.d.ts +3 -2
  94. package/data/build/dist/host/dsh-runtime-version.js +71 -2
  95. package/data/build/dist/host/features/auth.js +10 -2
  96. package/data/build/dist/host/features/file-management.d.ts +5 -0
  97. package/data/build/dist/host/features/file-management.js +267 -0
  98. package/data/build/dist/host/features/git-management.js +24 -2
  99. package/data/build/dist/host/features/index.d.ts +2 -0
  100. package/data/build/dist/host/features/index.js +6 -0
  101. package/data/build/dist/host/features/peer-host.d.ts +22 -0
  102. package/data/build/dist/host/features/peer-host.js +322 -0
  103. package/data/build/dist/host/features/types.d.ts +4 -0
  104. package/data/build/dist/host/host-router.d.ts +3 -0
  105. package/data/build/dist/host/host-router.js +2 -0
  106. package/data/build/dist/host/index-injection.d.ts +17 -0
  107. package/data/build/dist/host/index-injection.js +24 -0
  108. package/data/build/dist/host/index.d.ts +4 -1
  109. package/data/build/dist/host/index.js +41 -7
  110. package/data/build/dist/host/lan-access-dsh.d.ts +34 -1
  111. package/data/build/dist/host/lan-access-dsh.js +145 -26
  112. package/data/build/dist/host/modules/peer-host/host-api-proxy-service.d.ts +52 -0
  113. package/data/build/dist/host/modules/peer-host/host-api-proxy-service.js +166 -0
  114. package/data/build/dist/host/modules/peer-host/host-ws-connector.d.ts +31 -0
  115. package/data/build/dist/host/modules/peer-host/host-ws-connector.js +108 -0
  116. package/data/build/dist/host/modules/peer-host/host-ws-proxy-service.d.ts +26 -0
  117. package/data/build/dist/host/modules/peer-host/host-ws-proxy-service.js +159 -0
  118. package/data/build/dist/host/modules/peer-host/peer-host-aggregate-service.d.ts +50 -0
  119. package/data/build/dist/host/modules/peer-host/peer-host-aggregate-service.js +97 -0
  120. package/data/build/dist/host/modules/peer-host/peer-host-diagnostics.d.ts +19 -0
  121. package/data/build/dist/host/modules/peer-host/peer-host-diagnostics.js +54 -0
  122. package/data/build/dist/host/modules/peer-host/peer-host-handshake.d.ts +44 -0
  123. package/data/build/dist/host/modules/peer-host/peer-host-handshake.js +133 -0
  124. package/data/build/dist/host/modules/peer-host/peer-host-relay.d.ts +65 -0
  125. package/data/build/dist/host/modules/peer-host/peer-host-relay.js +196 -0
  126. package/data/build/dist/host/modules/peer-host/peer-host-session.d.ts +42 -0
  127. package/data/build/dist/host/modules/peer-host/peer-host-session.js +152 -0
  128. package/data/build/dist/host/modules/peer-host/peer-host-store.d.ts +92 -0
  129. package/data/build/dist/host/modules/peer-host/peer-host-store.js +323 -0
  130. package/data/build/dist/host/modules/peer-host/peer-host-ws-gateway.d.ts +34 -0
  131. package/data/build/dist/host/modules/peer-host/peer-host-ws-gateway.js +156 -0
  132. package/data/build/dist/host/native-session-bridge.d.ts +18 -2
  133. package/data/build/dist/host/native-session-bridge.js +232 -8
  134. package/data/build/dist/host/remote-web-runtime.js +26 -1
  135. package/data/build/dist/host/rpc.js +8 -3
  136. package/data/build/dist/host/session-changes.d.ts +5 -0
  137. package/data/build/dist/host/session-changes.js +163 -0
  138. package/data/build/dist/host/settings.js +6 -0
  139. package/data/build/dist/index.d.ts +1 -1
  140. package/data/build/dist/index.js +1 -1
  141. package/data/build/dist/shared/contracts/cli-adapter.d.ts +23 -2
  142. package/data/build/dist/shared/contracts/config.d.ts +15 -0
  143. package/data/build/dist/shared/contracts/config.js +10 -0
  144. package/data/build/dist/shared/contracts/dsh-device.d.ts +2 -0
  145. package/data/build/dist/shared/contracts/errors.d.ts +14 -0
  146. package/data/build/dist/shared/contracts/errors.js +15 -0
  147. package/data/build/dist/shared/contracts/feature.d.ts +2 -0
  148. package/data/build/dist/shared/contracts/file-management.d.ts +6 -0
  149. package/data/build/dist/shared/contracts/file-management.js +3 -0
  150. package/data/build/dist/shared/contracts/peer-host.d.ts +116 -15
  151. package/data/build/dist/shared/contracts/peer-host.js +19 -0
  152. package/data/build/dist/shared/contracts/version.d.ts +1 -1
  153. package/data/build/dist/shared/contracts/version.js +1 -1
  154. package/data/build/dist/shared/index.d.ts +4 -3
  155. package/data/build/dist/shared/index.js +2 -2
  156. package/data/build/dist/transport/debug.d.ts +5 -0
  157. package/data/build/dist/transport/debug.js +23 -1
  158. package/data/build/dist/transport/dsh-transport.d.ts +2 -0
  159. package/data/build/dist/transport/dsh-transport.js +11 -0
  160. package/data/build/dist/transport/webrtc-client.d.ts +2 -0
  161. package/data/build/dist/transport/webrtc-client.js +2 -1
  162. package/data/build/dist/transport/webrtc-host.d.ts +2 -0
  163. package/data/build/dist/transport/webrtc-host.js +6 -3
  164. package/data/build/dist/typert.host.d.ts +1 -1
  165. package/data/build/dist/typert.host.js +2 -1
  166. package/dsh.bundle.patch +10 -0
  167. package/package.json +19 -11
  168. package/profile/README.md +2 -2
  169. package/profile/package.json +2 -2
  170. package/profile/version.json +1 -1
  171. 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
- inputTokens: event.inputTokens,
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
- // DSH 0.1.7 的 Chat 分组器会把同一 Turn 中没有可见 assistant
106
- // 回复的连续工具节点合并到一个 process group。Codex 的工具已经
107
- // 在外部进程完成,分段之间没有 DSH 原生 assistant 文本可供分组器
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
- const config = sessionId ? registry.getSession(sessionId) : { adapterId: 'dsh' };
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 = readDshSelection(value);
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
- // OpenCode、Command Code 会在同一轮连续产出工具和 usage,强行切 step 会让
129
- // token-meter 在下一条 usage 到达前失去投影,表现为上下文组件反复卸载。
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: { ...process.env, ...(this.options.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 Error('JSON-RPC 请求失败'));
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: { ...process.env }, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true, shell: process.platform === 'win32',
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: { ...process.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
- /** 已切到下一个 DSH step、但仍在继续产出 Provider 流的驱动迭代器。 */
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;