@jingyi0605/codingns4dsh 0.2.1-rc.2 → 0.2.1-rc.4

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 (210) hide show
  1. package/README.en.md +6 -4
  2. package/README.md +6 -6
  3. package/data/build/dist/bootstrap/dsh-connection-adapter.d.ts +1 -1
  4. package/data/build/dist/bootstrap/index.d.ts +1 -1
  5. package/data/build/dist/client/bundle.js +5224 -290
  6. package/data/build/dist/client/bundle.js.map +1 -1
  7. package/data/build/dist/client/client.magpie-model-tag.js +155 -5
  8. package/data/build/dist/client/client.magpie-model-tag.js.map +1 -1
  9. package/data/build/dist/client/client.native-workspace-store.js +30 -4
  10. package/data/build/dist/client/client.native-workspace-store.js.map +1 -1
  11. package/data/build/dist/client/client.transport.js +747 -48
  12. package/data/build/dist/client/client.transport.js.map +1 -1
  13. package/data/build/dist/client/desktop-assistant.js +953 -185
  14. package/data/build/dist/client/feature-requirement.d.ts +43 -0
  15. package/data/build/dist/client/feature-requirement.d.ts.map +1 -0
  16. package/data/build/dist/client/feature-requirement.js +105 -0
  17. package/data/build/dist/client/feature-requirement.js.map +1 -0
  18. package/data/build/dist/client/features/index.d.ts +1 -0
  19. package/data/build/dist/client/features/index.d.ts.map +1 -1
  20. package/data/build/dist/client/features/index.js +3 -0
  21. package/data/build/dist/client/features/index.js.map +1 -1
  22. package/data/build/dist/client/features/magpie-management-controls.d.ts +46 -0
  23. package/data/build/dist/client/features/magpie-management-controls.d.ts.map +1 -0
  24. package/data/build/dist/client/features/magpie-management-controls.js +794 -0
  25. package/data/build/dist/client/features/magpie-management-controls.js.map +1 -0
  26. package/data/build/dist/client/features/magpie-management-page.d.ts +129 -0
  27. package/data/build/dist/client/features/magpie-management-page.d.ts.map +1 -0
  28. package/data/build/dist/client/features/magpie-management-page.js +1279 -0
  29. package/data/build/dist/client/features/magpie-management-page.js.map +1 -0
  30. package/data/build/dist/client/features/magpie-management.d.ts +7 -0
  31. package/data/build/dist/client/features/magpie-management.d.ts.map +1 -0
  32. package/data/build/dist/client/features/magpie-management.js +154 -0
  33. package/data/build/dist/client/features/magpie-management.js.map +1 -0
  34. package/data/build/dist/client/features/magpie-mark.d.ts +10 -0
  35. package/data/build/dist/client/features/magpie-mark.d.ts.map +1 -0
  36. package/data/build/dist/client/features/magpie-mark.js +21 -0
  37. package/data/build/dist/client/features/magpie-mark.js.map +1 -0
  38. package/data/build/dist/client/features/magpie-provider-logo.d.ts +11 -0
  39. package/data/build/dist/client/features/magpie-provider-logo.d.ts.map +1 -0
  40. package/data/build/dist/client/features/magpie-provider-logo.js +35 -0
  41. package/data/build/dist/client/features/magpie-provider-logo.js.map +1 -0
  42. package/data/build/dist/client/features/subscription-quota-groups.d.ts +193 -0
  43. package/data/build/dist/client/features/subscription-quota-groups.d.ts.map +1 -0
  44. package/data/build/dist/client/features/subscription-quota-groups.js +548 -0
  45. package/data/build/dist/client/features/subscription-quota-groups.js.map +1 -0
  46. package/data/build/dist/client/git-management.d.ts.map +1 -1
  47. package/data/build/dist/client/git-management.js +11 -9
  48. package/data/build/dist/client/git-management.js.map +1 -1
  49. package/data/build/dist/client/git-panel-styles.js +1 -0
  50. package/data/build/dist/client/git-panel-styles.js.map +1 -1
  51. package/data/build/dist/client/index.d.ts +1 -1
  52. package/data/build/dist/client/index.d.ts.map +1 -1
  53. package/data/build/dist/client/index.js +1 -1
  54. package/data/build/dist/client/index.js.map +1 -1
  55. package/data/build/dist/client/locale.d.ts.map +1 -1
  56. package/data/build/dist/client/locale.js +7 -0
  57. package/data/build/dist/client/locale.js.map +1 -1
  58. package/data/build/dist/client/locales/magpie.d.ts +4 -0
  59. package/data/build/dist/client/locales/magpie.d.ts.map +1 -0
  60. package/data/build/dist/client/locales/magpie.js +644 -0
  61. package/data/build/dist/client/locales/magpie.js.map +1 -0
  62. package/data/build/dist/client/locales/usage.d.ts.map +1 -1
  63. package/data/build/dist/client/locales/usage.js +52 -0
  64. package/data/build/dist/client/locales/usage.js.map +1 -1
  65. package/data/build/dist/client/magpie-model-tag.d.ts +9 -1
  66. package/data/build/dist/client/magpie-model-tag.d.ts.map +1 -1
  67. package/data/build/dist/client/magpie-model-tag.js +15 -3
  68. package/data/build/dist/client/magpie-model-tag.js.map +1 -1
  69. package/data/build/dist/client/provider-icons.d.ts +8 -0
  70. package/data/build/dist/client/provider-icons.d.ts.map +1 -1
  71. package/data/build/dist/client/provider-icons.js +122 -2
  72. package/data/build/dist/client/provider-icons.js.map +1 -1
  73. package/data/build/dist/client/settings-bridge.d.ts +1 -0
  74. package/data/build/dist/client/settings-bridge.d.ts.map +1 -1
  75. package/data/build/dist/client/settings-bridge.js +29 -10
  76. package/data/build/dist/client/settings-bridge.js.map +1 -1
  77. package/data/build/dist/client/settings-section.d.ts.map +1 -1
  78. package/data/build/dist/client/settings-section.js +21 -3
  79. package/data/build/dist/client/settings-section.js.map +1 -1
  80. package/data/build/dist/client/style.css +224 -221
  81. package/data/build/dist/client/subscription-slot.d.ts +22 -1
  82. package/data/build/dist/client/subscription-slot.d.ts.map +1 -1
  83. package/data/build/dist/client/subscription-slot.js +374 -25
  84. package/data/build/dist/client/subscription-slot.js.map +1 -1
  85. package/data/build/dist/dsh-capabilities/client/global-panel-adapter.d.ts +27 -0
  86. package/data/build/dist/dsh-capabilities/client/global-panel-adapter.d.ts.map +1 -0
  87. package/data/build/dist/dsh-capabilities/client/global-panel-adapter.js +109 -0
  88. package/data/build/dist/dsh-capabilities/client/global-panel-adapter.js.map +1 -0
  89. package/data/build/dist/dsh-capabilities/host/session-title-adapter.d.ts +2 -1
  90. package/data/build/dist/dsh-capabilities/host/session-title-adapter.d.ts.map +1 -1
  91. package/data/build/dist/dsh-capabilities/host/session-title-adapter.js +32 -2
  92. package/data/build/dist/dsh-capabilities/host/session-title-adapter.js.map +1 -1
  93. package/data/build/dist/dsh-capabilities/matrix.d.ts.map +1 -1
  94. package/data/build/dist/dsh-capabilities/matrix.js +12 -11
  95. package/data/build/dist/dsh-capabilities/matrix.js.map +1 -1
  96. package/data/build/dist/dsh-capabilities/routes.d.ts.map +1 -1
  97. package/data/build/dist/dsh-capabilities/routes.js +12 -9
  98. package/data/build/dist/dsh-capabilities/routes.js.map +1 -1
  99. package/data/build/dist/dsh-capabilities/types.d.ts +1 -1
  100. package/data/build/dist/dsh-capabilities/types.d.ts.map +1 -1
  101. package/data/build/dist/dsh-capabilities/types.js.map +1 -1
  102. package/data/build/dist/host/cli-adapters/acp-cli-driver.d.ts +27 -0
  103. package/data/build/dist/host/cli-adapters/acp-cli-driver.d.ts.map +1 -1
  104. package/data/build/dist/host/cli-adapters/acp-cli-driver.js +88 -13
  105. package/data/build/dist/host/cli-adapters/acp-cli-driver.js.map +1 -1
  106. package/data/build/dist/host/cli-adapters/codex-driver.d.ts +10 -0
  107. package/data/build/dist/host/cli-adapters/codex-driver.d.ts.map +1 -1
  108. package/data/build/dist/host/cli-adapters/codex-driver.js +32 -1
  109. package/data/build/dist/host/cli-adapters/codex-driver.js.map +1 -1
  110. package/data/build/dist/host/cli-adapters/command-code-acp-loader.d.ts +6 -5
  111. package/data/build/dist/host/cli-adapters/command-code-acp-loader.d.ts.map +1 -1
  112. package/data/build/dist/host/cli-adapters/command-code-acp-loader.js +72 -12
  113. package/data/build/dist/host/cli-adapters/command-code-acp-loader.js.map +1 -1
  114. package/data/build/dist/host/cli-adapters/command-code-driver.d.ts.map +1 -1
  115. package/data/build/dist/host/cli-adapters/command-code-driver.js +26 -1
  116. package/data/build/dist/host/cli-adapters/command-code-driver.js.map +1 -1
  117. package/data/build/dist/host/cli-adapters/dsh-message-projector.d.ts.map +1 -1
  118. package/data/build/dist/host/cli-adapters/dsh-message-projector.js +15 -2
  119. package/data/build/dist/host/cli-adapters/dsh-message-projector.js.map +1 -1
  120. package/data/build/dist/host/cli-adapters/feature.d.ts +13 -0
  121. package/data/build/dist/host/cli-adapters/feature.d.ts.map +1 -1
  122. package/data/build/dist/host/cli-adapters/feature.js +438 -6
  123. package/data/build/dist/host/cli-adapters/feature.js.map +1 -1
  124. package/data/build/dist/host/cli-adapters/grok-driver.js +5 -0
  125. package/data/build/dist/host/cli-adapters/grok-driver.js.map +1 -1
  126. package/data/build/dist/host/cli-adapters/magpie-detection.d.ts +72 -0
  127. package/data/build/dist/host/cli-adapters/magpie-detection.d.ts.map +1 -0
  128. package/data/build/dist/host/cli-adapters/magpie-detection.js +312 -0
  129. package/data/build/dist/host/cli-adapters/magpie-detection.js.map +1 -0
  130. package/data/build/dist/host/cli-adapters/magpie-management.d.ts +50 -0
  131. package/data/build/dist/host/cli-adapters/magpie-management.d.ts.map +1 -0
  132. package/data/build/dist/host/cli-adapters/magpie-management.js +1343 -0
  133. package/data/build/dist/host/cli-adapters/magpie-management.js.map +1 -0
  134. package/data/build/dist/host/cli-adapters/magpie-update.d.ts +59 -0
  135. package/data/build/dist/host/cli-adapters/magpie-update.d.ts.map +1 -0
  136. package/data/build/dist/host/cli-adapters/magpie-update.js +249 -0
  137. package/data/build/dist/host/cli-adapters/magpie-update.js.map +1 -0
  138. package/data/build/dist/host/cli-adapters/magpie-usage.d.ts +70 -8
  139. package/data/build/dist/host/cli-adapters/magpie-usage.d.ts.map +1 -1
  140. package/data/build/dist/host/cli-adapters/magpie-usage.js +1075 -125
  141. package/data/build/dist/host/cli-adapters/magpie-usage.js.map +1 -1
  142. package/data/build/dist/host/cli-adapters/magpie-web.d.ts +69 -0
  143. package/data/build/dist/host/cli-adapters/magpie-web.d.ts.map +1 -0
  144. package/data/build/dist/host/cli-adapters/magpie-web.js +485 -0
  145. package/data/build/dist/host/cli-adapters/magpie-web.js.map +1 -0
  146. package/data/build/dist/host/cli-adapters/process-utils.d.ts +2 -0
  147. package/data/build/dist/host/cli-adapters/process-utils.d.ts.map +1 -1
  148. package/data/build/dist/host/cli-adapters/process-utils.js +8 -0
  149. package/data/build/dist/host/cli-adapters/process-utils.js.map +1 -1
  150. package/data/build/dist/host/cli-adapters/session-performance-projection.d.ts +6 -0
  151. package/data/build/dist/host/cli-adapters/session-performance-projection.d.ts.map +1 -1
  152. package/data/build/dist/host/cli-adapters/session-performance-projection.js +25 -0
  153. package/data/build/dist/host/cli-adapters/session-performance-projection.js.map +1 -1
  154. package/data/build/dist/host/dsh-gateway-feature.d.ts.map +1 -1
  155. package/data/build/dist/host/dsh-gateway-feature.js +26 -3
  156. package/data/build/dist/host/dsh-gateway-feature.js.map +1 -1
  157. package/data/build/dist/host/features/host-status.d.ts +25 -0
  158. package/data/build/dist/host/features/host-status.d.ts.map +1 -1
  159. package/data/build/dist/host/features/host-status.js +60 -6
  160. package/data/build/dist/host/features/host-status.js.map +1 -1
  161. package/data/build/dist/host/features/lan-access-dsh.d.ts.map +1 -1
  162. package/data/build/dist/host/features/lan-access-dsh.js +61 -10
  163. package/data/build/dist/host/features/lan-access-dsh.js.map +1 -1
  164. package/data/build/dist/host/features/session-translation-protection.d.ts.map +1 -1
  165. package/data/build/dist/host/features/session-translation-protection.js +2 -1
  166. package/data/build/dist/host/features/session-translation-protection.js.map +1 -1
  167. package/data/build/dist/host/lan-access-dsh.d.ts.map +1 -1
  168. package/data/build/dist/host/lan-access-dsh.js +6 -2
  169. package/data/build/dist/host/lan-access-dsh.js.map +1 -1
  170. package/data/build/dist/host/modules/pwa/pwa-session-notifications.js +2 -1
  171. package/data/build/dist/host/modules/pwa/pwa-session-notifications.js.map +1 -1
  172. package/data/build/dist/host/rpc.d.ts +15 -2
  173. package/data/build/dist/host/rpc.d.ts.map +1 -1
  174. package/data/build/dist/host/rpc.js +47 -6
  175. package/data/build/dist/host/rpc.js.map +1 -1
  176. package/data/build/dist/host/settings.d.ts.map +1 -1
  177. package/data/build/dist/host/settings.js +5 -0
  178. package/data/build/dist/host/settings.js.map +1 -1
  179. package/data/build/dist/shared/contracts/config.d.ts +18 -0
  180. package/data/build/dist/shared/contracts/config.d.ts.map +1 -1
  181. package/data/build/dist/shared/contracts/config.js +8 -0
  182. package/data/build/dist/shared/contracts/config.js.map +1 -1
  183. package/data/build/dist/shared/contracts/errors.d.ts +2 -2
  184. package/data/build/dist/shared/contracts/feature.d.ts +7 -0
  185. package/data/build/dist/shared/contracts/feature.d.ts.map +1 -1
  186. package/data/build/dist/shared/contracts/magpie-management.d.ts +414 -0
  187. package/data/build/dist/shared/contracts/magpie-management.d.ts.map +1 -0
  188. package/data/build/dist/shared/contracts/magpie-management.js +2 -0
  189. package/data/build/dist/shared/contracts/magpie-management.js.map +1 -0
  190. package/data/build/dist/shared/contracts/subscription.d.ts +77 -1
  191. package/data/build/dist/shared/contracts/subscription.d.ts.map +1 -1
  192. package/data/build/dist/shared/contracts/subscription.js.map +1 -1
  193. package/data/build/dist/shared/contracts/version.d.ts +4 -4
  194. package/data/build/dist/shared/contracts/version.js +3 -3
  195. package/data/build/dist/shared/index.d.ts +3 -1
  196. package/data/build/dist/shared/index.d.ts.map +1 -1
  197. package/data/build/dist/shared/index.js +2 -1
  198. package/data/build/dist/shared/index.js.map +1 -1
  199. package/data/build/dist/shared/magpie-provider-id.d.ts +12 -0
  200. package/data/build/dist/shared/magpie-provider-id.d.ts.map +1 -0
  201. package/data/build/dist/shared/magpie-provider-id.js +16 -0
  202. package/data/build/dist/shared/magpie-provider-id.js.map +1 -0
  203. package/data/build/dist/shared/session-title.d.ts.map +1 -1
  204. package/data/build/dist/shared/session-title.js +10 -4
  205. package/data/build/dist/shared/session-title.js.map +1 -1
  206. package/package.json +29 -29
  207. package/profile/README.md +2 -2
  208. package/profile/package.json +3 -3
  209. package/profile/version.json +3 -3
  210. package/version.json +3 -3
@@ -21,9 +21,15 @@ import { CodingNsCliSessionStore } from './session-store.js';
21
21
  import { CodingNsDshMessageProjector } from './dsh-message-projector.js';
22
22
  import { CommandCodeSubscriptionService } from './command-code-subscription.js';
23
23
  import { ProviderSubscriptionService } from './provider-subscription.js';
24
- import { MagpieUsageService, magpieAgentForAdapter } from './magpie-usage.js';
24
+ import { attachMagpieSessionNames, MagpieUsageService, magpieAgentForAdapter } from './magpie-usage.js';
25
+ import { readDshProviderSource } from './dsh-config.js';
26
+ import { MagpieManagementService } from './magpie-management.js';
27
+ import { MagpieWebService } from './magpie-web.js';
28
+ import { MagpieUpdateService } from './magpie-update.js';
29
+ import { MagpieInstallDetectionService, normalizeCustomCommand } from './magpie-detection.js';
25
30
  import { createMagpieSessionRouting } from './magpie-session-routing.js';
26
31
  import { normalizeSubagentBridgeSettings, normalizeSubscriptionUsageSettings } from '../../shared/contracts/config.js';
32
+ import { CodingNsRpcError } from '../rpc-table.js';
27
33
  import { setAdapterRegistry } from './registry-holder.js';
28
34
  import { createDshVirtualProviderRegistration } from './dsh-virtual-providers.js';
29
35
  import { dispatchBridgeSubagent } from '../cli-bridge/dispatch.js';
@@ -49,6 +55,7 @@ import { sessionPromptScope } from '../../dsh-capabilities/host/session-prompt-a
49
55
  import { createSessionPromptTranslation } from '../features/session-prompt-translation.js';
50
56
  import { codexPerformanceProjection } from './codex-performance-projection.js';
51
57
  import { grokContextProjection, grokPerformanceProjection } from './grok-performance-projection.js';
58
+ import { countSessionTurnSteps } from './session-performance-projection.js';
52
59
  import { fromCliProviderId, toCliProviderId } from '../../shared/cli-provider-route.js';
53
60
  import { DshAgentTeamAdapter } from '../../dsh-capabilities/host/agent-team-adapter.js';
54
61
  import { CodingNsTeamDispatcher } from '../cli-bridge/team-dispatch.js';
@@ -80,8 +87,55 @@ export function createCliAdaptersFeature(options = {}) {
80
87
  const timeoutMs = normalizeSubscriptionUsageSettings(settings?.subscriptionUsage).timeoutSecs * 1000;
81
88
  return new ProviderSubscriptionService({ commandCode: new CommandCodeSubscriptionService({ timeoutMs }), timeoutMs });
82
89
  };
83
- let subscriptions = buildSubscriptions(context.services.settings?.get());
84
- const magpieUsage = options.magpieUsage ?? new MagpieUsageService({ lifetimeSignal: lifetime.signal });
90
+ let subscriptions = options.subscriptions ?? buildSubscriptions(context.services.settings?.get());
91
+ let magpieDetection = options.magpieDetection ?? new MagpieInstallDetectionService(options.magpieDetectionOptions);
92
+ // 最近一次可用的 Magpie 入口;探测会随用户指定的路径变化而重建,Web 服务总是读最新值。
93
+ let magpieCommand = null;
94
+ let magpieVersion = null;
95
+ const magpieWeb = options.magpieWeb ?? new MagpieWebService({ startupCommand: () => magpieCommand });
96
+ const magpieUsage = options.magpieUsage ?? new MagpieUsageService({ lifetimeSignal: lifetime.signal, webApi: magpieWeb });
97
+ const magpieManagement = options.magpieManagement ?? new MagpieManagementService({ webApi: magpieWeb });
98
+ const magpieUpdates = options.magpieUpdates ?? new MagpieUpdateService({ webApi: magpieWeb, version: () => magpieVersion });
99
+ let magpieWebDesired = false;
100
+ let magpieWebSync = Promise.resolve();
101
+ const scheduleMagpieWebSync = (enabled) => {
102
+ magpieWebDesired = enabled;
103
+ if (!enabled) {
104
+ magpieUpdates.stopBackgroundChecks();
105
+ void magpieWeb.stop().catch(() => undefined);
106
+ }
107
+ const next = magpieWebSync.then(async () => {
108
+ if (magpieWebDesired !== enabled)
109
+ return;
110
+ if (!enabled) {
111
+ await magpieWeb.stop();
112
+ return;
113
+ }
114
+ try {
115
+ await magpieWeb.start();
116
+ magpieUpdates.startBackgroundChecks();
117
+ }
118
+ catch (error) {
119
+ console.warn('codingns4dsh: Magpie Web startup failed; the next Web API request will retry', error);
120
+ }
121
+ });
122
+ magpieWebSync = next.catch(() => undefined);
123
+ };
124
+ // 设置里可能保留了“已启用”,但本机没有可用 Magpie(换机、卸载或安装损坏)。
125
+ // 这种情况下不拉起 Magpie Web,等用户安装后重新检测再启用。
126
+ let magpieInstalled = await magpieDetection.detect().then((detection) => {
127
+ const usable = MagpieInstallDetectionService.isUsable(detection);
128
+ magpieCommand = usable ? detection.command : null;
129
+ magpieVersion = usable ? detection.version : null;
130
+ return usable;
131
+ }, () => false);
132
+ scheduleMagpieWebSync(context.services.settings?.get().modules.magpieManagement === true && magpieInstalled);
133
+ context.resources.add(async () => {
134
+ magpieWebDesired = false;
135
+ await magpieWeb.stop();
136
+ await magpieWebSync;
137
+ await magpieWeb.dispose();
138
+ });
85
139
  let subscriptionSettings = JSON.stringify(context.services.settings?.get().subscriptionUsage ?? null);
86
140
  // 注入的 registry 自带 SessionStore;只有自建时才有本地 store 变量。
87
141
  const sessionStore = options.registry?.sessionRecords
@@ -334,11 +388,17 @@ export function createCliAdaptersFeature(options = {}) {
334
388
  const subscription = readSubscriptionRequest(payload);
335
389
  const requestSignal = signal ?? lifetime.signal;
336
390
  const session = subscription.sessionId === undefined ? undefined : registry.getSession(subscription.sessionId);
337
- if (session !== undefined && session.adapterId === subscription.adapterId) {
391
+ if (context.services.settings?.get().modules.magpieManagement === true
392
+ && session !== undefined && session.adapterId === subscription.adapterId) {
338
393
  const magpie = await magpieUsage.read(session, requestSignal, subscription.includeRoute, subscription.sessionId);
339
394
  requestSignal.throwIfAborted();
340
- if (magpie !== null)
341
- return { providerUsage: null, magpie };
395
+ const snapshot = magpie ?? (subscription.sessionId === undefined
396
+ ? null
397
+ : await pendingMagpieUsage(magpieUsage, nativeSessions, subscription.sessionId, session, requestSignal));
398
+ if (snapshot !== null) {
399
+ const sessionName = subscription.sessionId === undefined ? undefined : readDshSessionDisplayName(subscription.sessionId, sessionStore, nativeSessions);
400
+ return { providerUsage: null, magpie: sessionName === undefined ? snapshot : { ...snapshot, sessionName } };
401
+ }
342
402
  }
343
403
  requestSignal.throwIfAborted();
344
404
  const providerUsage = await subscriptions.read(subscription.adapterId, subscription.providerId, subscription.modelId);
@@ -365,6 +425,67 @@ export function createCliAdaptersFeature(options = {}) {
365
425
  default: throw new Error(`未知 CLI RPC: cli/${action}`);
366
426
  }
367
427
  }));
428
+ context.resources.add(context.services.rpc.register('magpie', async (action, payload, rpcContext) => {
429
+ const rpcSignal = asRecord(rpcContext)?.signal;
430
+ const signal = isAbortSignal(rpcSignal) ? rpcSignal : lifetime.signal;
431
+ // 安装探测本身就是启用前的判断依据,因此不要求模块已启用。
432
+ if (action === 'management/detection') {
433
+ const request = readMagpieDetectionRequest(payload);
434
+ const force = readMagpieDetectionForce(payload);
435
+ // A custom path is request-scoped; it must not poison later automatic detection.
436
+ const customCommand = normalizeCustomCommand(request.commandPath);
437
+ const detectionService = customCommand === null
438
+ ? magpieDetection
439
+ : new MagpieInstallDetectionService({ ...options.magpieDetectionOptions, customCommand });
440
+ const detection = await detectionService.detect(customCommand === null ? {} : { commandPath: customCommand }, { force: force || customCommand !== null });
441
+ magpieCommand = MagpieInstallDetectionService.isUsable(detection) ? detection.command : null;
442
+ magpieVersion = MagpieInstallDetectionService.isUsable(detection) ? detection.version : null;
443
+ return detection;
444
+ }
445
+ if (context.services.settings?.get().modules.magpieManagement !== true) {
446
+ throw new CodingNsRpcError('CODINGNS_MAGPIE_DISABLED', 'Magpie management is disabled');
447
+ }
448
+ switch (action) {
449
+ case 'usage/dashboard': {
450
+ const dashboard = await magpieUsage.readDashboard(readMagpieDashboardPeriod(payload), signal);
451
+ return attachMagpieSessionNames(dashboard, await magpieSessionNameBindings(sessionStore, nativeSessions, signal));
452
+ }
453
+ case 'usage/dashboard/stream': {
454
+ const input = asRecord(payload);
455
+ const period = readMagpieDashboardPeriod(payload);
456
+ const force = input?.force === true;
457
+ return streamMagpieDashboard(magpieUsage, period, signal, force, sessionStore, nativeSessions);
458
+ }
459
+ case 'usage/requests': {
460
+ const request = readMagpieRequestOptions(payload);
461
+ const page = await magpieUsage.readRequestPage(request, signal);
462
+ if (page === null || request.sessionId === undefined)
463
+ return page;
464
+ return {
465
+ ...page,
466
+ sessionMetrics: readMagpieSessionMetrics(sessionStore, nativeSessions, request.sessionId, request.sessionIds),
467
+ };
468
+ }
469
+ case 'management/snapshot':
470
+ return magpieManagement.readSnapshot(signal);
471
+ case 'management/provider/update':
472
+ return magpieManagement.updateProvider(payload, signal);
473
+ case 'management/group/update':
474
+ return magpieManagement.updateGroup(payload, signal);
475
+ case 'management/account/update':
476
+ return magpieManagement.updateAccount(payload, signal);
477
+ case 'update/status':
478
+ return magpieUpdates.readStatus(signal);
479
+ case 'update/notes':
480
+ return magpieUpdates.readNotes(signal);
481
+ case 'update/check':
482
+ return magpieUpdates.checkForUpdate(signal);
483
+ case 'update/install':
484
+ return magpieUpdates.installUpdate(payload, signal);
485
+ default:
486
+ throw new CodingNsRpcError('CODINGNS_RPC_NOT_FOUND', `Unknown Magpie RPC: ${action}`);
487
+ }
488
+ }));
368
489
  // 外部 CLI 子代理托管:开启时启动本机回环桥接,把外部 CLI 的子代理调用
369
490
  // 转投成 DSH 原生可续子会话;关闭或模块停用时收回端口并清空驱动句柄。
370
491
  let bridgeServer;
@@ -435,6 +556,17 @@ export function createCliAdaptersFeature(options = {}) {
435
556
  subscriptions = buildSubscriptions(next);
436
557
  }
437
558
  syncSubagentBridgeSettings(next.subagentBridge);
559
+ const magpieWanted = next.modules.magpieManagement === true;
560
+ // 只在“需要启动”时重新探测;停用时立即回收,退出探测的旧结果不覆盖新状态。
561
+ void (magpieWanted && !magpieInstalled
562
+ ? magpieDetection.detect({}, { force: true }).then(detection => MagpieInstallDetectionService.isUsable(detection), () => false)
563
+ : Promise.resolve(magpieInstalled)).then((installed) => {
564
+ magpieInstalled = installed;
565
+ if (next.modules.magpieManagement !== true)
566
+ scheduleMagpieWebSync(false);
567
+ else if (installed)
568
+ scheduleMagpieWebSync(true);
569
+ });
438
570
  }));
439
571
  }
440
572
  const events = context.services.events;
@@ -684,6 +816,7 @@ export function createCliAdaptersFeature(options = {}) {
684
816
  // Close native interaction cards before unloading their provider transports.
685
817
  lifetime.abort();
686
818
  updates.dispose();
819
+ magpieUpdates.dispose();
687
820
  await management.dispose();
688
821
  await registry.dispose();
689
822
  await sessionStore.flush();
@@ -968,12 +1101,25 @@ function normalizeDshModelGroup(value) {
968
1101
  /**
969
1102
  * DSH 原生 Provider 可能只返回一个 finish(stop)。不能把这个空回合当作成功,
970
1103
  * 否则 Session 会落下一条空 assistant/message,用户只能看到“一秒结束”。
1104
+ *
1105
+ * error finish 携带 Provider 的真实失败原因(状态码、上游错误消息、错误码),
1106
+ * 必须原样透传:替换成统一文案会把 400/429/凭证失效等不同故障折叠成同一句
1107
+ * “未返回任何有效事件”,既无法排查也不可分类告警。统一兜底文案只用于一种
1108
+ * 情形——流以 stop(或无 finish)干净结束且没有任何有效事件,此时没有任何
1109
+ * 原始错误可透传。
971
1110
  */
972
1111
  async function* guardDshNativeStream(next) {
973
1112
  let meaningful = false;
974
1113
  const terminal = [];
975
1114
  for await (const chunk of next()) {
976
1115
  if (isDshFinishChunk(chunk)) {
1116
+ // error finish 本身就是“有效事件”:立即透传,并标记 meaningful,
1117
+ // 让此前缓冲的非 error finish(正常情况下不会出现)也在收尾时放出。
1118
+ if (isDshErrorFinishChunk(chunk)) {
1119
+ meaningful = true;
1120
+ yield chunk;
1121
+ continue;
1122
+ }
977
1123
  terminal.push(chunk);
978
1124
  continue;
979
1125
  }
@@ -1002,6 +1148,11 @@ function isDshFinishChunk(value) {
1002
1148
  const record = asRecord(value);
1003
1149
  return record?.type === 'finish';
1004
1150
  }
1151
+ /** error finish(reason.kind === 'error')携带 Provider 的原始失败,是必须透传的有效事件。 */
1152
+ function isDshErrorFinishChunk(value) {
1153
+ const record = asRecord(value);
1154
+ return record?.type === 'finish' && asRecord(record.reason)?.kind === 'error';
1155
+ }
1005
1156
  function isMeaningfulDshChunk(value) {
1006
1157
  const record = asRecord(value);
1007
1158
  if (record === null)
@@ -1386,6 +1537,105 @@ function nativePreStepContext(translation, registry, sessionId, nativeSessions)
1386
1537
  translationEnabled: session.translationEnabled ?? translation.nativeEnabled === true,
1387
1538
  };
1388
1539
  }
1540
+ function readOptionalText(value) {
1541
+ return typeof value === 'string' ? value.trim() || undefined : undefined;
1542
+ }
1543
+ /** 原生 Provider 选中的模型,用于在 Magpie 会话身份可读之前判断该模型是否由 Magpie 提供。 */
1544
+ function readNativeModelSelection(nativeSessions, sessionId) {
1545
+ if (nativeSessions === undefined)
1546
+ return undefined;
1547
+ let session;
1548
+ try {
1549
+ session = nativeSessions.get(sessionId);
1550
+ }
1551
+ catch {
1552
+ return undefined;
1553
+ }
1554
+ const root = asRecord(session);
1555
+ const selection = asRecord(root?.modelSelection) ?? asRecord(asRecord(asRecord(root?.record)?.rows)?.modelSelection);
1556
+ const value = asRecord(selection?.val) ?? selection;
1557
+ // 待用的 pending/next 优先;lastUsed 可能仍描述上一个模型。
1558
+ for (const candidate of [asRecord(value?.pending), asRecord(value?.next), asRecord(value?.lastUsed)]) {
1559
+ const modelId = readOptionalText(candidate?.model);
1560
+ if (modelId === undefined)
1561
+ continue;
1562
+ const providerId = readOptionalText(candidate?.provider);
1563
+ return providerId === undefined ? { modelId } : { providerId, modelId };
1564
+ }
1565
+ return undefined;
1566
+ }
1567
+ /** Magpie 网关只监听本机回环;Provider 指向这里时才算 Magpie 自己的路由。 */
1568
+ const MAGPIE_GATEWAY_HOSTS = new Set(['127.0.0.1', 'localhost', '[::1]', '::1']);
1569
+ function isMagpieGateway(baseUrl) {
1570
+ try {
1571
+ const target = new URL(baseUrl);
1572
+ // 端口由 `magpie web` 绑定(默认 3425),配置里改端口也应视为同一网关,
1573
+ // 因此这里只要求回环地址,不再硬编码端口。
1574
+ return MAGPIE_GATEWAY_HOSTS.has(target.hostname);
1575
+ }
1576
+ catch {
1577
+ return false;
1578
+ }
1579
+ }
1580
+ /**
1581
+ * Provider ID 本身可能是任意用户命名,因此先看 DSH 配置里的 baseURL;
1582
+ * 配置读不到时只接受与 `readDshDefaultProvider()` 相同的 Magpie 名称约定。
1583
+ */
1584
+ function isMagpieGatewayProvider(providerId) {
1585
+ const source = readDshProviderSource(providerId);
1586
+ if (source !== null)
1587
+ return isMagpieGateway(source.baseUrl);
1588
+ return /^magpie(?:[-_.].*)?$/iu.test(providerId);
1589
+ }
1590
+ /**
1591
+ * 读到用量之后仍然要判断「这个模型是不是 Magpie 的」。
1592
+ *
1593
+ * `magpieUsage.read()` 只有在会话身份(Provider 会话 ID 或 DSH 会话 ID)已经出现在
1594
+ * Magpie 账本里时才返回快照。新会话的首个请求可能还没被 Magpie Web 记账,或者两边的
1595
+ * 会话 ID 在滚动升级期间并不相等,这时返回 null 会让 Host 直接回退到官方余量。
1596
+ * 只有确认模型由 Magpie 提供时才下发占位快照,避免官方数字抢占首屏。
1597
+ */
1598
+ async function pendingMagpieUsage(magpie, nativeSessions, sessionId, session, signal) {
1599
+ const agent = magpieAgentForAdapter(session.adapterId);
1600
+ const modelId = session.modelId?.trim();
1601
+ if (agent === null || modelId === undefined || modelId === '')
1602
+ return null;
1603
+ try {
1604
+ const selectedProviderId = session.providerId?.trim();
1605
+ const providerId = selectedProviderId === undefined || selectedProviderId === ''
1606
+ ? readNativeModelSelection(nativeSessions, sessionId)?.providerId
1607
+ : selectedProviderId;
1608
+ if (providerId === undefined || !isMagpieGatewayProvider(providerId))
1609
+ return null;
1610
+ const confirmed = await magpie.readModelMetadata(session.adapterId, modelId, signal);
1611
+ signal.throwIfAborted();
1612
+ return confirmed === null ? null : pendingMagpieSnapshot(agent, modelId);
1613
+ }
1614
+ catch {
1615
+ // 目录不可读、超时或请求被取消:保持原有回退语义,不伪造 Magpie 用量。
1616
+ return null;
1617
+ }
1618
+ }
1619
+ /** Magpie 已经确认提供该模型、但会话身份还读不到时下发的占位快照。 */
1620
+ function pendingMagpieSnapshot(agent, modelId) {
1621
+ const capturedAt = new Date().toISOString();
1622
+ return {
1623
+ agent,
1624
+ modelId,
1625
+ capturedAt,
1626
+ usageStatus: 'unavailable',
1627
+ aggregate: null,
1628
+ sessionId: null,
1629
+ session: null,
1630
+ routeStatus: 'not-requested',
1631
+ route: null,
1632
+ upstreamProvider: null,
1633
+ upstreamProviders: [],
1634
+ // 客户端据此在 3 秒内带路由补读,而不是停留在官方余额上。
1635
+ quotaStatus: 'provider-unavailable',
1636
+ quotas: [],
1637
+ };
1638
+ }
1389
1639
  /** 读取原生会话 header;测试替身或会话尚未装载时返回 undefined。 */
1390
1640
  function readNativeSessionHeader(nativeSessions, sessionId) {
1391
1641
  if (nativeSessions === undefined)
@@ -1881,6 +2131,188 @@ function findCwdInValue(value, depth) {
1881
2131
  }
1882
2132
  return undefined;
1883
2133
  }
2134
+ function readMagpieDashboardPeriod(payload) {
2135
+ const value = asRecord(payload)?.period;
2136
+ if (value === 'today' || value === 'week' || value === 'month' || value === 'all')
2137
+ return value;
2138
+ throw new TypeError('Magpie usage period must be today, week, month, or all');
2139
+ }
2140
+ async function* streamMagpieDashboard(service, period, signal, force, sessionStore, nativeSessions) {
2141
+ const names = magpieSessionNameBindings(sessionStore, nativeSessions, signal).catch(() => []);
2142
+ let latest;
2143
+ for await (const chunk of service.readDashboardStream(period, signal, force)) {
2144
+ latest = chunk.dashboard;
2145
+ yield chunk;
2146
+ }
2147
+ if (latest !== undefined) {
2148
+ const named = attachMagpieSessionNames(latest, await names);
2149
+ if (named !== latest)
2150
+ yield { part: 'sessions', dashboard: named, stale: false };
2151
+ }
2152
+ }
2153
+ /** 探测载荷只接受可选入口路径;其余字段一律忽略,避免把参数带进命令行。 */
2154
+ function readMagpieDetectionRequest(payload) {
2155
+ const record = asRecord(payload);
2156
+ if (record === null || typeof record.commandPath !== 'string')
2157
+ return {};
2158
+ const commandPath = record.commandPath.trim();
2159
+ return commandPath === '' ? {} : { commandPath };
2160
+ }
2161
+ /** `force` 只在用户主动重新检测时出现;缺省复用短期探测结果。 */
2162
+ function readMagpieDetectionForce(payload) {
2163
+ return asRecord(payload)?.force === true;
2164
+ }
2165
+ function readMagpieRequestOptions(payload) {
2166
+ const record = asRecord(payload);
2167
+ if (record === null)
2168
+ throw new TypeError('Magpie request page must be an object');
2169
+ const period = readMagpieDashboardPeriod(record);
2170
+ const offset = record.offset === undefined ? 0 : record.offset;
2171
+ const limit = record.limit === undefined ? 50 : record.limit;
2172
+ if (typeof offset !== 'number' || !Number.isSafeInteger(offset) || offset < 0 || offset > 1_000_000) {
2173
+ throw new TypeError('Magpie request offset is out of bounds');
2174
+ }
2175
+ if (typeof limit !== 'number' || !Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
2176
+ throw new TypeError('Magpie request limit must be between 1 and 100');
2177
+ }
2178
+ const sessionId = readOptionalMagpieIdentity(record.sessionId, 512);
2179
+ const sessionIds = record.sessionIds === undefined
2180
+ ? undefined
2181
+ : readOptionalMagpieIdentityList(record.sessionIds, 16, 512);
2182
+ const sessionAgent = readOptionalMagpieIdentity(record.sessionAgent, 64);
2183
+ if ((sessionId === undefined) !== (sessionAgent === undefined)) {
2184
+ throw new TypeError('Magpie session details require both sessionId and sessionAgent');
2185
+ }
2186
+ if (sessionIds !== undefined && sessionId === undefined) {
2187
+ throw new TypeError('Magpie session IDs require a primary session ID');
2188
+ }
2189
+ if (sessionAgent !== undefined && !/^[a-z0-9][a-z0-9-]*$/u.test(sessionAgent)) {
2190
+ throw new TypeError('Magpie session Agent is invalid');
2191
+ }
2192
+ return {
2193
+ period,
2194
+ offset,
2195
+ limit,
2196
+ ...(sessionId === undefined ? {} : { sessionId }),
2197
+ ...(sessionIds === undefined ? {} : { sessionIds }),
2198
+ ...(sessionAgent === undefined ? {} : { sessionAgent }),
2199
+ };
2200
+ }
2201
+ function readMagpieSessionMetrics(sessionStore, nativeSessions, sessionId, sessionIds) {
2202
+ if (nativeSessions === undefined)
2203
+ return null;
2204
+ const identities = new Set([sessionId, ...(sessionIds ?? [])]);
2205
+ const bindings = sessionStore.list({ includeArchived: true }).filter((record) => identities.has(record.dshSessionId) || (record.providerSessionId !== undefined && identities.has(record.providerSessionId)));
2206
+ const direct = bindings.find((record) => record.dshSessionId === sessionId);
2207
+ const boundSessionIds = [...new Set(bindings.map((record) => record.dshSessionId))];
2208
+ if (direct === undefined && boundSessionIds.length > 1)
2209
+ return null;
2210
+ const dshSessionId = direct?.dshSessionId ?? boundSessionIds[0] ?? sessionId;
2211
+ let session;
2212
+ try {
2213
+ session = nativeSessions.get(dshSessionId);
2214
+ }
2215
+ catch {
2216
+ return null;
2217
+ }
2218
+ const snapshotEvents = asRecord(session)?.snapshotEvents;
2219
+ if (typeof snapshotEvents !== 'function')
2220
+ return null;
2221
+ try {
2222
+ const events = snapshotEvents.call(session);
2223
+ return Array.isArray(events) ? countSessionTurnSteps(events) : null;
2224
+ }
2225
+ catch {
2226
+ return null;
2227
+ }
2228
+ }
2229
+ function readOptionalMagpieIdentity(value, maxLength) {
2230
+ if (value === undefined)
2231
+ return undefined;
2232
+ if (typeof value !== 'string')
2233
+ throw new TypeError('Magpie session identity must be text');
2234
+ const text = value.trim();
2235
+ if (text === '' || text.length > maxLength || /[\u0000-\u001f\u007f]/u.test(text)) {
2236
+ throw new TypeError('Magpie session identity is invalid');
2237
+ }
2238
+ return text;
2239
+ }
2240
+ function readOptionalMagpieIdentityList(value, maxItems, maxLength) {
2241
+ if (!Array.isArray(value) || value.length > maxItems)
2242
+ throw new TypeError('Magpie session IDs are invalid');
2243
+ const values = value.map((item) => readOptionalMagpieIdentity(item, maxLength));
2244
+ if (values.some((item) => item === undefined))
2245
+ throw new TypeError('Magpie session IDs are invalid');
2246
+ return [...new Set(values)];
2247
+ }
2248
+ async function magpieSessionNameBindings(sessionStore, nativeSessions, signal) {
2249
+ const records = sessionStore.list({ includeArchived: true });
2250
+ const bindings = [];
2251
+ const knownSessionIds = new Set();
2252
+ let nativeValues = nativeSessions?.list() ?? [];
2253
+ if (nativeSessions?.listRemote !== undefined) {
2254
+ try {
2255
+ nativeValues = await nativeSessions.listRemote(signal);
2256
+ }
2257
+ catch {
2258
+ signal?.throwIfAborted();
2259
+ }
2260
+ }
2261
+ const nativeNames = new Map();
2262
+ for (const value of nativeValues) {
2263
+ const sessionId = nativeSessionId(value);
2264
+ const name = readNativeSessionDisplayName(value);
2265
+ if (sessionId !== undefined && name !== undefined)
2266
+ nativeNames.set(sessionId, name);
2267
+ }
2268
+ for (const record of records) {
2269
+ knownSessionIds.add(record.dshSessionId);
2270
+ const name = nativeNames.get(record.dshSessionId) ?? readDshSessionDisplayName(record.dshSessionId, sessionStore, nativeSessions);
2271
+ bindings.push({
2272
+ dshSessionId: record.dshSessionId,
2273
+ adapterId: record.adapterId,
2274
+ ...(record.providerSessionId === undefined ? {} : { providerSessionId: record.providerSessionId }),
2275
+ ...(name === undefined ? {} : { name }),
2276
+ });
2277
+ }
2278
+ // Native DSH sessions do not always have a CodingNS session-index record. They
2279
+ // still use the DSH session ID as Magpie's identity, so include their title
2280
+ // only when no external adapter binding claims the same DSH session.
2281
+ for (const value of nativeValues) {
2282
+ const sessionId = nativeSessionId(value);
2283
+ if (sessionId === undefined || knownSessionIds.has(sessionId))
2284
+ continue;
2285
+ const name = readNativeSessionDisplayName(value);
2286
+ bindings.push({ dshSessionId: sessionId, adapterId: 'dsh', ...(name === undefined ? {} : { name }) });
2287
+ }
2288
+ return bindings;
2289
+ }
2290
+ function readDshSessionDisplayName(sessionId, sessionStore, nativeSessions) {
2291
+ return readNativeSessionDisplayName(nativeSessions?.get(sessionId)) ?? cleanDisplayName(sessionStore.get(sessionId)?.title);
2292
+ }
2293
+ function readNativeSessionDisplayName(value) {
2294
+ const root = asRecord(value);
2295
+ if (root === null)
2296
+ return undefined;
2297
+ const projection = asRecord(root.projections) ?? asRecord(asRecord(root.record)?.projections);
2298
+ const values = asRecord(projection?.values);
2299
+ const record = asRecord(root.record);
2300
+ const rows = asRecord(record?.rows);
2301
+ const rowProjection = asRecord(rows?.projections);
2302
+ const rowValues = asRecord(rowProjection?.values);
2303
+ for (const candidate of [values?.title, rowValues?.title, root.title, root.name, root.displayName, record?.title, record?.name]) {
2304
+ const title = cleanDisplayName(candidate);
2305
+ if (title !== undefined)
2306
+ return title;
2307
+ }
2308
+ return undefined;
2309
+ }
2310
+ function cleanDisplayName(value) {
2311
+ if (typeof value !== 'string')
2312
+ return undefined;
2313
+ const name = value.trim().replace(/[\u0000-\u001f\u007f]/gu, '').slice(0, 240);
2314
+ return name === '' ? undefined : name;
2315
+ }
1884
2316
  function asRecord(value) { return typeof value === 'object' && value !== null && !Array.isArray(value) ? value : null; }
1885
2317
  function isRecord(value) { return asRecord(value) !== null; }
1886
2318
  function nativeSessionId(value) {