@jingyi0605/codingns4dsh 0.1.7-rc.3 → 0.2.0-beta.1

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 (89) hide show
  1. package/LICENSE +695 -0
  2. package/README.en.md +23 -9
  3. package/README.md +23 -9
  4. package/data/build/dist/bootstrap/dsh-connection-adapter.d.ts +1 -1
  5. package/data/build/dist/bootstrap/index.d.ts +1 -1
  6. package/data/build/dist/client/bundle.js +1200 -170
  7. package/data/build/dist/client/context-breakdown-dom.d.ts +53 -0
  8. package/data/build/dist/client/context-breakdown-dom.js +171 -0
  9. package/data/build/dist/client/dsh-h5-bootstrap.js +7 -1
  10. package/data/build/dist/client/features/cli-adapters.js +11 -0
  11. package/data/build/dist/client/features/index.d.ts +1 -0
  12. package/data/build/dist/client/features/index.js +1 -0
  13. package/data/build/dist/client/features/subscription-usage-panel.d.ts +15 -0
  14. package/data/build/dist/client/features/subscription-usage-panel.js +117 -0
  15. package/data/build/dist/client/features/workspace-session-enhancement-panel.js +22 -5
  16. package/data/build/dist/client/features/workspace-session-enhancement.js +17 -2
  17. package/data/build/dist/client/git-management.js +80 -10
  18. package/data/build/dist/client/index.d.ts +1 -0
  19. package/data/build/dist/client/index.js +16 -49
  20. package/data/build/dist/client/locale.js +20 -2
  21. package/data/build/dist/client/remote-web-context.js +26 -2
  22. package/data/build/dist/client/settings-bridge.d.ts +20 -5
  23. package/data/build/dist/client/settings-bridge.js +30 -7
  24. package/data/build/dist/client/subscription-slot.d.ts +3 -1
  25. package/data/build/dist/client/subscription-slot.js +31 -6
  26. package/data/build/dist/client/terminal/ui.js +1 -1
  27. package/data/build/dist/client/terminal/xterm-view.js +1 -1
  28. package/data/build/dist/dsh-capabilities/client/config-forms-adapter.d.ts +10 -0
  29. package/data/build/dist/dsh-capabilities/client/config-forms-adapter.js +22 -0
  30. package/data/build/dist/dsh-capabilities/client/primitives-adapter.d.ts +5 -2
  31. package/data/build/dist/dsh-capabilities/client/primitives-adapter.js +3 -2
  32. package/data/build/dist/dsh-capabilities/client/settings-scope-adapter.d.ts +2 -2
  33. package/data/build/dist/dsh-capabilities/host/config-forms-adapter.d.ts +22 -1
  34. package/data/build/dist/dsh-capabilities/host/config-forms-adapter.js +29 -3
  35. package/data/build/dist/dsh-capabilities/host/settings-scope-adapter.d.ts +2 -2
  36. package/data/build/dist/dsh-capabilities/matrix.js +31 -1
  37. package/data/build/dist/dsh-capabilities/routes.d.ts +8 -1
  38. package/data/build/dist/dsh-capabilities/routes.js +173 -5
  39. package/data/build/dist/dsh-capabilities/types.d.ts +1 -1
  40. package/data/build/dist/features/registry.d.ts +7 -0
  41. package/data/build/dist/features/registry.js +20 -2
  42. package/data/build/dist/host/cli-adapters/feature.js +24 -2
  43. package/data/build/dist/host/cli-adapters/native-team-proxy.d.ts +36 -0
  44. package/data/build/dist/host/cli-adapters/native-team-proxy.js +108 -0
  45. package/data/build/dist/host/cli-adapters/provider-subscription.d.ts +35 -0
  46. package/data/build/dist/host/cli-adapters/provider-subscription.js +405 -5
  47. package/data/build/dist/host/cli-adapters/registry.d.ts +9 -3
  48. package/data/build/dist/host/cli-adapters/registry.js +30 -0
  49. package/data/build/dist/host/cli-adapters/session-store.d.ts +8 -2
  50. package/data/build/dist/host/cli-adapters/session-store.js +19 -0
  51. package/data/build/dist/host/features/git-management.js +41 -18
  52. package/data/build/dist/host/features/types.d.ts +6 -3
  53. package/data/build/dist/host/index-injection.d.ts +12 -5
  54. package/data/build/dist/host/index-injection.js +32 -11
  55. package/data/build/dist/host/index.js +15 -4
  56. package/data/build/dist/host/lan-access-dsh.d.ts +2 -2
  57. package/data/build/dist/host/native-session-bridge.d.ts +4 -0
  58. package/data/build/dist/host/native-session-bridge.js +51 -9
  59. package/data/build/dist/host/relay-tunnel-runtime.js +10 -1
  60. package/data/build/dist/host/remote-web-runtime.js +40 -17
  61. package/data/build/dist/host/rpc.d.ts +24 -5
  62. package/data/build/dist/host/rpc.js +38 -9
  63. package/data/build/dist/host/session-migration-repair.js +14 -0
  64. package/data/build/dist/host/settings.d.ts +2 -2
  65. package/data/build/dist/host/settings.js +11 -1
  66. package/data/build/dist/host/terminal/startup.d.ts +2 -2
  67. package/data/build/dist/shared/contracts/cli-adapter.d.ts +22 -1
  68. package/data/build/dist/shared/contracts/config.d.ts +21 -0
  69. package/data/build/dist/shared/contracts/config.js +17 -0
  70. package/data/build/dist/shared/contracts/errors.d.ts +2 -2
  71. package/data/build/dist/shared/contracts/subscription.d.ts +7 -0
  72. package/data/build/dist/shared/contracts/subscription.js +14 -1
  73. package/data/build/dist/shared/contracts/version.d.ts +4 -4
  74. package/data/build/dist/shared/contracts/version.js +3 -3
  75. package/data/build/dist/shared/index.d.ts +3 -2
  76. package/data/build/dist/shared/index.js +2 -1
  77. package/data/build/dist/transport/carrier.d.ts +11 -1
  78. package/data/build/dist/transport/carrier.js +77 -26
  79. package/data/build/dist/transport/debug.d.ts +5 -0
  80. package/data/build/dist/transport/debug.js +17 -2
  81. package/data/build/dist/transport/dsh-session.d.ts +18 -1
  82. package/data/build/dist/transport/dsh-session.js +48 -13
  83. package/data/build/dist/transport/index.d.ts +1 -1
  84. package/data/build/dist/transport/index.js +1 -1
  85. package/package.json +23 -19
  86. package/profile/README.md +2 -2
  87. package/profile/package.json +4 -3
  88. package/profile/version.json +3 -3
  89. package/version.json +3 -3
@@ -204,19 +204,20 @@ async function readHistory(_workspaceId, root, rawLimit, rawOffset) {
204
204
  const offset = Math.max(0, Math.min(1_000_000, Number.isSafeInteger(rawOffset) ? Number(rawOffset) : 0));
205
205
  let result;
206
206
  try {
207
- result = await runGit(root, ['log', `--skip=${String(offset)}`, `--max-count=${String(limit)}`, '--format=%H%x1f%an%x1f%aI%x1f%s%x1f%b%x1e']);
207
+ result = await runGit(root, ['log', `--skip=${String(offset)}`, `--max-count=${String(limit)}`, '--decorate=short', '--format=%H%x1f%an%x1f%aI%x1f%s%x1f%b%x1f%D%x1e']);
208
208
  }
209
209
  catch (error) {
210
210
  if (isEmptyRepositoryError(error))
211
211
  return { items: [], cursor: String(offset), nextCursor: null, totalCount: 0 };
212
212
  throw error;
213
213
  }
214
+ const refByShortName = new Map((await readRefRecords(root)).map((record) => [record.shortName, record]));
214
215
  const items = [];
215
216
  for (const record of result.stdout.split('\x1e')) {
216
217
  const fields = record.trim().split('\x1f');
217
218
  if (fields.length < 5 || !fields[0])
218
219
  continue;
219
- items.push({ commitHash: fields[0], authorName: fields[1] ?? '', authoredAt: fields[2] ?? '', subject: fields[3] ?? '', body: fields[4] ?? '', refs: [] });
220
+ items.push({ commitHash: fields[0], authorName: fields[1] ?? '', authoredAt: fields[2] ?? '', subject: fields[3] ?? '', body: fields[4] ?? '', refs: parseHistoryRefs(fields[5] ?? '', refByShortName) });
220
221
  }
221
222
  let total = 0;
222
223
  try {
@@ -228,24 +229,46 @@ async function readHistory(_workspaceId, root, rawLimit, rawOffset) {
228
229
  }
229
230
  return { items, cursor: String(offset), nextCursor: offset + items.length < total ? String(offset + items.length) : null, totalCount: total };
230
231
  }
231
- async function readBranches(root) {
232
- const currentBranch = (await runGit(root, ['branch', '--show-current'])).stdout.trim() || 'HEAD';
233
- // for-each-ref 不展开 %xNN;使用 Git 原生支持的 NUL 字段分隔符,避免分支名被污染。
234
- const result = await runGit(root, ['for-each-ref', '--format=%(refname)%00%(refname:short)%00%(HEAD)%00%(upstream:short)', 'refs/heads', 'refs/remotes']);
235
- const local = [];
236
- const remote = [];
237
- const fields = result.stdout.replace(/\r?\n$/u, '').split('\0');
238
- for (let index = 0; index + 3 < fields.length; index += 4) {
239
- const fullName = fields[index] ?? '';
240
- const name = fields[index + 1] ?? '';
241
- const head = fields[index + 2] ?? '';
242
- const upstream = fields[index + 3] ?? '';
243
- if (!name)
232
+ /** 读取本地与远程分支的 ref 表;记录之间换行分隔,字段使用 Git 原生支持的 NUL 分隔符(for-each-ref 不展开 %xNN)。 */
233
+ async function readRefRecords(root) {
234
+ const result = await runGit(root, ['for-each-ref', '--format=%(refname)%00%(refname:short)%00%(objectname)%00%(upstream:short)%00%(HEAD)', 'refs/heads', 'refs/remotes']);
235
+ const records = [];
236
+ for (const line of result.stdout.split(/\r?\n/u)) {
237
+ if (line === '')
238
+ continue;
239
+ const [fullName = '', shortName = '', commitHash = '', upstream = '', head = ''] = line.split('\0');
240
+ if (shortName === '')
241
+ continue;
242
+ const remote = fullName.startsWith('refs/remotes/');
243
+ records.push({ fullName, shortName, commitHash, upstream: upstream.trim() || null, current: head.trim() === '*', kind: remote ? 'remote' : 'local', remoteName: remote && shortName.includes('/') ? shortName.split('/')[0] ?? null : null });
244
+ }
245
+ return records;
246
+ }
247
+ /** 解析 git log 的 %D 装饰字段:标签不显示,HEAD -> 分支 记为 head,其余通过 ref 表补全类型。 */
248
+ function parseHistoryRefs(decoration, refByShortName) {
249
+ const refs = [];
250
+ for (const rawToken of decoration.split(',')) {
251
+ const token = rawToken.trim();
252
+ if (token === '' || token.startsWith('tag: '))
244
253
  continue;
245
- const item = { name, current: head === '*', upstream: upstream || null, remote: fullName.startsWith('refs/remotes/') };
246
- (item.remote ? remote : local).push(item);
254
+ if (token.startsWith('HEAD -> ')) {
255
+ const name = token.slice('HEAD -> '.length).trim();
256
+ if (name !== '')
257
+ refs.push({ name, kind: 'head', remoteName: null });
258
+ continue;
259
+ }
260
+ const record = refByShortName.get(token);
261
+ if (record === undefined || record.shortName.endsWith('/HEAD'))
262
+ continue;
263
+ refs.push({ name: record.shortName, kind: record.kind, remoteName: record.remoteName });
247
264
  }
248
- return { currentBranch, local, remote };
265
+ return refs;
266
+ }
267
+ async function readBranches(root) {
268
+ const currentBranch = (await runGit(root, ['branch', '--show-current'])).stdout.trim() || 'HEAD';
269
+ const records = await readRefRecords(root);
270
+ const toItem = (record) => ({ name: record.shortName, current: record.current, upstream: record.upstream, remote: record.kind === 'remote' });
271
+ return { currentBranch, local: records.filter((record) => record.kind === 'local').map(toItem), remote: records.filter((record) => record.kind === 'remote').map(toItem) };
249
272
  }
250
273
  async function switchBranch(root, branchName, create) {
251
274
  if (!/^[A-Za-z0-9._/-]+$/u.test(branchName) || branchName.startsWith('-'))
@@ -1,9 +1,10 @@
1
1
  import type { CodingNsRpcTable } from '../rpc-table.js';
2
- import type { SettingsProvider, SettingsScope } from '@deepseek-ai/dsh-settings';
3
2
  import type { CodingNsSettings } from '../../shared/contracts/config.js';
3
+ import type { DshHostSettingsProvider, DshHostSettingsScope } from '../../dsh-capabilities/host/config-forms-adapter.js';
4
4
  import type { CodingNsNativeSessionBridge } from '../native-session-bridge.js';
5
5
  import type { TerminalProcessService } from '../terminal/terminal-process-service.js';
6
6
  import type { DebugWorkspaceService } from '../debug.js';
7
+ import type { CodingNsNativeTeamProxy } from '../cli-adapters/native-team-proxy.js';
7
8
  export interface CodingNsHostEvents {
8
9
  on(name: string, listener: (...args: any[]) => any): unknown;
9
10
  }
@@ -18,9 +19,9 @@ export interface CodingNsHostServices {
18
19
  /** 当前 DSH 宿主的真实版本;由 Host 入口启动门禁解析并向功能模块传递。 */
19
20
  readonly dshVersion?: string;
20
21
  /** 持久化设置;测试或嵌入式调用未提供时,局域网映射仍可手动启动。 */
21
- readonly settings?: SettingsScope<CodingNsSettings>;
22
+ readonly settings?: DshHostSettingsScope<CodingNsSettings>;
22
23
  /** Host 设置提供器,供远程设置 RPC 做版本校验和持久化写入。 */
23
- readonly settingsProvider?: SettingsProvider;
24
+ readonly settingsProvider?: DshHostSettingsProvider;
24
25
  /** 当前 DSH Web 服务实际监听端口,用于自动定位本机 DSH。 */
25
26
  readonly dshWebPort?: number;
26
27
  /** DSH 官方生成的一次性 Web 认证 URL;只留在 Host 内部完成 Cookie 交换。 */
@@ -29,6 +30,8 @@ export interface CodingNsHostServices {
29
30
  readonly events?: CodingNsHostEvents;
30
31
  /** DSH 原生会话桥接;不可用时为 undefined,插件不因此阻断启动。 */
31
32
  readonly nativeSessions?: CodingNsNativeSessionBridge;
33
+ /** DSH 0.2 原生 Agent Team Proxy;未安装实验 Team 包时为空。 */
34
+ readonly nativeTeam?: CodingNsNativeTeamProxy;
32
35
  /** 终端启动项和 PTY 进程服务;只由 Host RPC 使用。 */
33
36
  readonly terminalProcesses?: TerminalProcessService;
34
37
  /** Workspace 级调试服务;只使用 Host 解析出的根目录。 */
@@ -6,12 +6,19 @@ export interface DshIndexInjectionEntry {
6
6
  readonly [key: string]: unknown;
7
7
  }
8
8
  /**
9
- * 在启动页声明 Host 所有权,同时保留 Desktop 已经提供的完整 Transport。
9
+ * 在启动页声明 Host 所有权,让 ui-settings 对非回环页面(局域网直连、中继)
10
+ * 保持 host 模式,同时保留 Desktop 已经提供的完整 Transport。
10
11
  *
11
- * 这里只使用 DSH WebServer 支持的 JSON global 行,不在首页安装全局 setter。
12
- * 页面可能由 Desktop Shell 在更晚的阶段注入自己的 Transport;因此找不到
13
- * 已有 Transport 时必须保持不变,不能追加同名全局,否则会覆盖 Desktop 的
14
- * `streamBaseUrl`,让 `/api/remote.mux` 退回 `ws://app`。
12
+ * 页面级 `__DSH_TRANSPORT__` 同时被 Desktop 壳、中继桥和这里写入,因此注入只做
13
+ * 合并写、且只追加脚本行(kind: "script"):
14
+ *
15
+ * - 不追加同名全局行。全局行由 DSH 在文档里直接赋值,一旦排在 Desktop 的行之后
16
+ * 就会整体覆盖 Transport,丢掉 `streamBaseUrl`,让 `/api/remote.mux` 退回
17
+ * `ws://app` 并卡住插件页。
18
+ * - 脚本行在客户端启动前执行:已有 Transport 时只补 `ownsHost`,没有 Transport
19
+ * 且页面也不是 Desktop(`dshDesktopBoot` 未定义,Desktop 自己声明所有权)时
20
+ * 才创建 `{ ownsHost: true }`。
21
+ * - 未知形状(例如 Desktop 托管的字符串标记)保持原样,不被覆盖。
15
22
  */
16
23
  export declare function injectDshWebTransportOwnership(table: unknown[]): void;
17
24
  //# sourceMappingURL=index-injection.d.ts.map
@@ -1,20 +1,41 @@
1
1
  /**
2
- * 在启动页声明 Host 所有权,同时保留 Desktop 已经提供的完整 Transport。
2
+ * 在启动页声明 Host 所有权,让 ui-settings 对非回环页面(局域网直连、中继)
3
+ * 保持 host 模式,同时保留 Desktop 已经提供的完整 Transport。
3
4
  *
4
- * 这里只使用 DSH WebServer 支持的 JSON global 行,不在首页安装全局 setter。
5
- * 页面可能由 Desktop Shell 在更晚的阶段注入自己的 Transport;因此找不到
6
- * 已有 Transport 时必须保持不变,不能追加同名全局,否则会覆盖 Desktop 的
7
- * `streamBaseUrl`,让 `/api/remote.mux` 退回 `ws://app`。
5
+ * 页面级 `__DSH_TRANSPORT__` 同时被 Desktop 壳、中继桥和这里写入,因此注入只做
6
+ * 合并写、且只追加脚本行(kind: "script"):
7
+ *
8
+ * - 不追加同名全局行。全局行由 DSH 在文档里直接赋值,一旦排在 Desktop 的行之后
9
+ * 就会整体覆盖 Transport,丢掉 `streamBaseUrl`,让 `/api/remote.mux` 退回
10
+ * `ws://app` 并卡住插件页。
11
+ * - 脚本行在客户端启动前执行:已有 Transport 时只补 `ownsHost`,没有 Transport
12
+ * 且页面也不是 Desktop(`dshDesktopBoot` 未定义,Desktop 自己声明所有权)时
13
+ * 才创建 `{ ownsHost: true }`。
14
+ * - 未知形状(例如 Desktop 托管的字符串标记)保持原样,不被覆盖。
8
15
  */
9
16
  export function injectDshWebTransportOwnership(table) {
10
17
  const index = table.findIndex((entry) => isTransportInjection(entry));
11
- if (index < 0)
12
- return;
13
- const entry = table[index];
14
- if (!isRecord(entry) || !isRecord(entry.value))
15
- return;
16
- table[index] = { ...entry, value: { ...entry.value, ownsHost: true } };
18
+ if (index >= 0) {
19
+ const entry = table[index];
20
+ if (!isRecord(entry) || !isRecord(entry.value))
21
+ return;
22
+ table[index] = { ...entry, value: { ...entry.value, ownsHost: true } };
23
+ }
24
+ table.push({ kind: 'script', placement: 'head', text: TRANSPORT_OWNERSHIP_SCRIPT });
17
25
  }
26
+ /**
27
+ * 在客户端读取 Transport 之前补齐 Host 所有权。
28
+ *
29
+ * 只做合并写和“无 Transport 时创建”,不覆盖任何已有字段;`dshDesktopBoot`
30
+ * 存在时交给 Desktop 壳自己的 Transport(它已经带 `ownsHost`)。
31
+ */
32
+ const TRANSPORT_OWNERSHIP_SCRIPT = [
33
+ '(function(){',
34
+ 'var t=globalThis.__DSH_TRANSPORT__;',
35
+ 'if(t===void 0||t===null){if(globalThis.dshDesktopBoot===void 0)globalThis.__DSH_TRANSPORT__={ownsHost:true};return}',
36
+ 'if(typeof t==="object")t.ownsHost=true;',
37
+ '})()',
38
+ ].join('');
18
39
  function isTransportInjection(value) {
19
40
  return isRecord(value) && value.name === '__DSH_TRANSPORT__';
20
41
  }
@@ -12,6 +12,7 @@ import { createDshCapabilityRegistry } from '../dsh-capabilities/index.js';
12
12
  import { debugInfo, debugWarn } from '../shared/debug.js';
13
13
  import { repairLegacySessionLogs } from './session-migration-repair.js';
14
14
  import { injectDshWebTransportOwnership } from './index-injection.js';
15
+ import { DshNativeTeamProxy } from './cli-adapters/native-team-proxy.js';
15
16
  export function apply(ctx) {
16
17
  if (ctx === undefined)
17
18
  return;
@@ -53,9 +54,10 @@ export function apply(ctx) {
53
54
  debugInfo('codingns4dsh: legacy session repair finished', report);
54
55
  });
55
56
  ctx.inject(['settings', 'connection', 'webServer'], async (hostCtx) => {
57
+ const settingsContext = hostCtx;
56
58
  debugInfo('codingns4dsh: host inject ready', {
57
59
  hasConnection: hostCtx.connection !== undefined,
58
- hasSettings: hostCtx.settings !== undefined,
60
+ hasSettings: settingsContext.settings !== undefined,
59
61
  hasWebServer: hostCtx.webServer !== undefined,
60
62
  });
61
63
  const webServerPort = hostCtx.webServer.port;
@@ -77,12 +79,12 @@ export function apply(ctx) {
77
79
  });
78
80
  debugInfo('codingns4dsh: host index injection registration ready');
79
81
  debugInfo('codingns4dsh: host settings registration begin');
80
- const settings = registerCodingNsSettings(hostCtx);
82
+ const settings = registerCodingNsSettings(settingsContext);
81
83
  debugInfo('codingns4dsh: host settings registered');
82
84
  const workspaceRoots = new Map();
83
85
  // controller 必须在功能模块和浏览器 Client 开始消费状态前完成装配。
84
86
  // 工厂在本次启动只读取一次开关,设置 watcher 不会热切同名 service。
85
- const terminal = await installTerminalController(hostCtx, settings, hostCtx.settings, {
87
+ const terminal = await installTerminalController(hostCtx, settings, settingsContext.settings, {
86
88
  resolveWorkspaceRoot: (workspaceId) => workspaceRoots.get(workspaceId) ?? resolveWorkspaceRoot(hostCtx, workspaceId),
87
89
  registerWorkspaceRoot: (workspaceId, cwd) => workspaceRoots.set(workspaceId, cwd),
88
90
  });
@@ -91,11 +93,12 @@ export function apply(ctx) {
91
93
  rpc: new CodingNsRpcTable(),
92
94
  dshVersion,
93
95
  settings,
94
- settingsProvider: hostCtx.settings,
96
+ settingsProvider: settingsContext.settings,
95
97
  dshWebPort: webServerPort,
96
98
  dshWebAuthenticatedUrl: hostCtx.connection.authenticatedUrl(`http://127.0.0.1:${String(webServerPort)}`),
97
99
  events: { on: hostCtx.on.bind(hostCtx) },
98
100
  nativeSessions: createCodingNsNativeSessionBridge(hostCtx, dshVersion),
101
+ nativeTeam: new DshNativeTeamProxy(readOptionalService(hostCtx, 'agentTeams'), readOptionalService(hostCtx, 'agents')),
99
102
  terminalProcesses: terminal.processService,
100
103
  resolveWorkspaceRoot: (workspaceId) => workspaceRoots.get(workspaceId) ?? resolveWorkspaceRoot(hostCtx, workspaceId),
101
104
  listWorkspaceRoots: () => [...workspaceRoots.values(), ...readWorkspaceRoots(hostCtx)],
@@ -158,6 +161,14 @@ export function apply(ctx) {
158
161
  }, 'codingns4dsh: 功能模块启停同步');
159
162
  });
160
163
  }
164
+ function readOptionalService(ctx, name) {
165
+ try {
166
+ return ctx.get(name);
167
+ }
168
+ catch {
169
+ return undefined;
170
+ }
171
+ }
161
172
  function isRecord(value) {
162
173
  return typeof value === 'object' && value !== null;
163
174
  }
@@ -1,7 +1,7 @@
1
1
  import { Transform, type TransformCallback } from 'node:stream';
2
- import type { SettingsScope } from '@deepseek-ai/dsh-settings';
3
2
  import type { CodingNsSettings, LanAccessDshLoginSettings, LoginProtectionScopes } from '../shared/contracts/config.js';
4
3
  import type { LanAccessDshConfig, LanAccessDshLoginConfig, LanAccessDshSnapshot } from '../shared/contracts/lan-access-dsh.js';
4
+ import type { DshHostSettingsScope } from '../dsh-capabilities/host/config-forms-adapter.js';
5
5
  export interface LanAccessDshStream {
6
6
  pipe(destination: LanAccessDshStream): LanAccessDshStream;
7
7
  destroy(error?: Error): void;
@@ -112,5 +112,5 @@ export declare class LanAccessDshError extends Error {
112
112
  constructor(code: 'LAN_ACCESS_DSH_INVALID' | 'DSH_PORT_NOT_FOUND' | 'DSH_PORT_AMBIGUOUS', message: string);
113
113
  }
114
114
  export declare function normalizeLanAccessDshConfig(value: Partial<LanAccessDshConfig>, allowedListenHosts?: readonly string[]): LanAccessDshConfig;
115
- export declare function createLanAccessDshRpcHandler(proxy: LanAccessDshProxy, settings?: SettingsScope<CodingNsSettings>, loginStore?: LanAccessDshLoginStore): (action: string, payload: unknown) => unknown | Promise<unknown>;
115
+ export declare function createLanAccessDshRpcHandler(proxy: LanAccessDshProxy, settings?: DshHostSettingsScope<CodingNsSettings>, loginStore?: LanAccessDshLoginStore): (action: string, payload: unknown) => unknown | Promise<unknown>;
116
116
  //# sourceMappingURL=lan-access-dsh.d.ts.map
@@ -12,6 +12,8 @@ export interface CodingNsNativeSessionStore {
12
12
  list(): readonly unknown[];
13
13
  flush?(session: unknown): Promise<boolean> | Promise<void> | boolean | void;
14
14
  }
15
+ /** 插件可安全写入的 DSH Session generation;未来 generation 必须先有专用 adapter。 */
16
+ export type CodingNsNativeSessionFormat = 3 | 4 | 'unknown' | 'unsupported';
15
17
  /** 公共 CLI 投影层写入 DSH 的工具调用,不包含任何 Provider 私有字段。 */
16
18
  export interface CodingNsNativeToolCall {
17
19
  readonly callId: string;
@@ -123,6 +125,8 @@ export interface CodingNsNativeSessionBridge {
123
125
  readonly workspaceController?: CodingNsNativeWorkspaceController;
124
126
  /** 获取已经进入 DSH 原生 SessionStore 的会话。 */
125
127
  get(sessionId: string): unknown | undefined;
128
+ /** 读取 Session header generation,用于阻止未来格式被旧投影器误写。 */
129
+ formatVersion?(sessionId: string): CodingNsNativeSessionFormat;
126
130
  /** 获取当前 Host 已装载的原生会话;失败时返回空数组。 */
127
131
  list(): readonly unknown[];
128
132
  /** 调用 DSH 原生 session/list;无 Controller 时回退到本地 SessionStore。 */
@@ -23,11 +23,24 @@ export function createCodingNsNativeSessionBridge(ctx, dshVersion) {
23
23
  // 下一步的工具写进旧 step;step/start 到达后由工具投影器重试。
24
24
  const pendingStepTransitions = new Set();
25
25
  let injectedStepSequence = 0;
26
- const modernInjectedSource = isModernDshVersion(dshVersion);
26
+ const modernProducerSource = isModernDshVersion(dshVersion);
27
+ // 当前会话的 generation 比运行时版本号更直接:v4 只接受生产者自持的
28
+ // source.kind,旧的 plugin 包装会被准入拒绝;v3 仍由 dsh-compaction 的
29
+ // plugin 判定识别。会话头缺少版本时(精简 Host 或旧夹具)才退回版本号推断。
30
+ const usesProducerOwnedSource = (session) => {
31
+ const format = sessionFormat(session);
32
+ if (format === 4)
33
+ return true;
34
+ if (format === 3)
35
+ return false;
36
+ return modernProducerSource;
37
+ };
27
38
  const appendNativeToolCall = (sessionId, call) => {
28
39
  if (on !== undefined && pendingStepTransitions.has(sessionId))
29
40
  return null;
30
41
  const session = appendableSession(store?.get(sessionId));
42
+ if (sessionFormat(session) === 'unsupported')
43
+ return null;
31
44
  const position = session === null ? null : activeStep(session);
32
45
  if (session === null || position === null)
33
46
  return null;
@@ -66,7 +79,7 @@ export function createCodingNsNativeSessionBridge(ctx, dshVersion) {
66
79
  };
67
80
  const appendNativeToolResult = (handle, result) => {
68
81
  const session = appendableSession(store?.get(handle.sessionId));
69
- if (session === null)
82
+ if (session === null || sessionFormat(session) === 'unsupported')
70
83
  return false;
71
84
  const error = result.error?.trim();
72
85
  session.append('tool/result', {
@@ -92,7 +105,7 @@ export function createCodingNsNativeSessionBridge(ctx, dshVersion) {
92
105
  };
93
106
  const appendNativeRequestContext = (sessionId, context) => {
94
107
  const session = appendableSession(store?.get(sessionId));
95
- if (session === null || context.provider.trim() === '' || context.model.trim() === '')
108
+ if (session === null || sessionFormat(session) === 'unsupported' || context.provider.trim() === '' || context.model.trim() === '')
96
109
  return false;
97
110
  try {
98
111
  // request/context 是 token-meter 的容量来源。相同路由重复追加会让
@@ -174,7 +187,7 @@ export function createCodingNsNativeSessionBridge(ctx, dshVersion) {
174
187
  const appendNativeUsageSample = (sessionId, usage) => {
175
188
  const session = appendableSession(store?.get(sessionId));
176
189
  const position = session === null ? null : activeStep(session);
177
- if (session === null || position === null)
190
+ if (session === null || sessionFormat(session) === 'unsupported' || position === null)
178
191
  return false;
179
192
  try {
180
193
  const previousContext = latestRequestContext(session);
@@ -209,7 +222,7 @@ export function createCodingNsNativeSessionBridge(ctx, dshVersion) {
209
222
  };
210
223
  const appendNativeCompactionEvent = (sessionId, event) => {
211
224
  const session = appendableSession(store?.get(sessionId));
212
- if (session === null)
225
+ if (session === null || sessionFormat(session) === 'unsupported')
213
226
  return false;
214
227
  try {
215
228
  if (event.phase === 'start') {
@@ -245,13 +258,13 @@ export function createCodingNsNativeSessionBridge(ctx, dshVersion) {
245
258
  model: event.model?.trim() || 'external-agent',
246
259
  });
247
260
  // DSH 前端的 compaction 节点由紧随 summary 的 checkpoint 替换事件生成。
248
- // DSH 0.1.7 起会话格式只接受生产者自持的 source.kind,旧的 plugin 包装
249
- // 会让整轮持久化失败;0.1.6 及更早仍由 dsh-compaction 的 plugin 标记识别。
261
+ // v4 的检查点必须携带同一次 compaction/start 的 compactionId,v3 继续
262
+ // 使用该版本 dsh-compaction 识别的 plugin 标记。
250
263
  session.append('user/message', {
251
264
  id: `codingns-compaction-${state.compactionId}`,
252
265
  role: 'user',
253
266
  content: [{ type: 'text', text: summary }],
254
- source: modernInjectedSource
267
+ source: usesProducerOwnedSource(session)
255
268
  ? { kind: 'compact-checkpoint', compactionId: state.compactionId }
256
269
  : { kind: 'plugin', plugin: 'compact' },
257
270
  }, {
@@ -290,7 +303,7 @@ export function createCodingNsNativeSessionBridge(ctx, dshVersion) {
290
303
  id: `codingns-external-step-${injectedStepSequence}-${randomUUID()}`,
291
304
  role: 'user',
292
305
  content: [{ type: 'text', text: boundedSummary }],
293
- source: modernInjectedSource
306
+ source: usesProducerOwnedSource(appendableSession(store?.get(sessionId)))
294
307
  ? { kind: 'model-selection', form: 'notice', summary: boundedSummary }
295
308
  : { kind: 'plugin', plugin: 'codingns4dsh', form: 'notice', summary: boundedSummary },
296
309
  });
@@ -316,6 +329,9 @@ export function createCodingNsNativeSessionBridge(ctx, dshVersion) {
316
329
  get(sessionId) {
317
330
  return store?.get(sessionId);
318
331
  },
332
+ formatVersion(sessionId) {
333
+ return sessionFormat(appendableSession(store?.get(sessionId)));
334
+ },
319
335
  list() {
320
336
  try {
321
337
  return store?.list() ?? [];
@@ -491,6 +507,32 @@ function appendableSession(value) {
491
507
  return null;
492
508
  return value;
493
509
  }
510
+ /**
511
+ * Session V4 的 header 可能直接暴露 version,也可能包在 data 中;旧测试夹具
512
+ * 没有 header 时返回 unknown,继续保持 0.1.x 的兼容行为。对未来 generation
513
+ * 直接停止插件写入,避免把 V4 envelope 当作未知事件追加后破坏 projection。
514
+ */
515
+ function sessionFormat(session) {
516
+ if (session === null)
517
+ return 'unknown';
518
+ let events;
519
+ try {
520
+ events = session.snapshotEvents();
521
+ }
522
+ catch {
523
+ return 'unknown';
524
+ }
525
+ const header = events[0];
526
+ if (!isRecord(header))
527
+ return 'unknown';
528
+ const data = isRecord(header.data) ? header.data : undefined;
529
+ const version = finiteInteger(header.version) ?? (data === undefined ? null : finiteInteger(data.version));
530
+ if (version === null)
531
+ return 'unknown';
532
+ if (version === 3 || version === 4)
533
+ return version;
534
+ return version > 4 ? 'unsupported' : 'unknown';
535
+ }
494
536
  function isNativeStepStart(value) {
495
537
  return isRecord(value) && value.type === 'step/start';
496
538
  }
@@ -11,6 +11,7 @@ import { RTCCertificate, RTCDtlsTransport, RTCPeerConnection } from 'werift';
11
11
  import { acceptWebRtcHost, createHostSignalingTicketRequest, } from '../transport/webrtc-host.js';
12
12
  import { DshGateway } from '../transport/dsh-gateway.js';
13
13
  import { createDshTransportDebugLogger } from '../transport/debug.js';
14
+ import { DATA_CHANNEL_MAX_MESSAGE_BYTES } from '../transport/carrier.js';
14
15
  /** Relay Tunnel 规定的 DataChannel label,不允许使用插件自定义值。 */
15
16
  export const CODINGNS_TUNNEL_DATA_CHANNEL_LABEL = 'codingns-tunnel';
16
17
  /** 文件存储适用于 Host 桌面进程;先写临时文件再 rename,避免半截证书。 */
@@ -227,7 +228,15 @@ function isSignalingOpen(socket) {
227
228
  export function createWeriftPeerConnectionFactory(identity) {
228
229
  return ({ iceServers, iceTransportPolicy }) => {
229
230
  const certificate = identity ? new RTCCertificate(identity.privateKeyPem, identity.certPem, identity.signatureHash) : undefined;
230
- const peer = new RTCPeerConnection({ iceServers, iceTransportPolicy, ...(certificate ? { certificates: [certificate] } : {}) });
231
+ // 浏览器按对端 SDP 宣告的 max-message-size 限制 RTCDataChannel.send()。werift 默认
232
+ // 只宣告 64 KiB,而 Carrier 的整分片是 64 KiB 正文加 20 字节头,浏览器一旦发送
233
+ // 超过 64 KiB 的消息就会抛错,中继下的原生设置与附件因此失败。
234
+ const peer = new RTCPeerConnection({
235
+ iceServers,
236
+ iceTransportPolicy,
237
+ maxMessageSize: DATA_CHANNEL_MAX_MESSAGE_BYTES,
238
+ ...(certificate ? { certificates: [certificate] } : {}),
239
+ });
231
240
  const adapted = {
232
241
  get connectionState() { return peer.connectionState; },
233
242
  get iceConnectionState() { return peer.iceConnectionState; },
@@ -128,8 +128,16 @@ export function createRemoteWebRuntimeFeature(options) {
128
128
  }
129
129
  }
130
130
  catch (error) {
131
- debug.log('web.request.error', { operation, streamId: context.envelope.streamId, error: error instanceof Error ? error.message : String(error) });
132
- context.send('stream.error', { errorCode: error instanceof RemoteWebRuntimeError ? error.code : 'WEB_RUNTIME_FAILED', detail: error instanceof Error ? error.message : 'DSH Web Runtime 失败', retryable: false });
131
+ // 稳定错误码 + 请求路径是定位中继设置页空白这类问题的最小信息;
132
+ // 报文正文与凭据仍然不进日志。
133
+ const errorCode = error instanceof RemoteWebRuntimeError ? error.code : 'WEB_RUNTIME_FAILED';
134
+ debug.log('web.request.error', {
135
+ operation,
136
+ streamId: context.envelope.streamId,
137
+ errorCode,
138
+ ...(isRecord(request) && typeof request.path === 'string' ? { path: request.path } : {}),
139
+ });
140
+ context.send('stream.error', { errorCode, detail: error instanceof Error ? error.message : 'DSH Web Runtime 失败', retryable: false });
133
141
  }
134
142
  finally {
135
143
  if (operation !== 'web.ws.open') {
@@ -350,19 +358,30 @@ export function createLocalDshWebRuntimeProvider(options) {
350
358
  },
351
359
  async getPluginBundle(session, _pluginId, path) { return this.getAsset(session, path); },
352
360
  async request(session, input) {
353
- ensureSession(sessions, session);
354
- const path = normalizePath(input.path);
355
- if (!allowed.some((prefix) => path === prefix || path.startsWith(prefix)))
356
- throw new Error('DSH Web 请求路径不在白名单内');
357
- const init = { method: input.method ?? 'GET' };
358
- if (input.headers !== undefined)
359
- init.headers = Object.fromEntries(input.headers);
360
- if (input.body !== undefined)
361
- init.body = normalizeConnectionRpcBody(path, input.method ?? 'GET', input.body);
362
- await ensureAuthenticated();
363
- const response = await fetchLocal(path, init);
364
- debug.log('web.provider.request.response', { sessionId: session.sessionId, path, method: input.method ?? 'GET', status: response.status });
365
- return { status: response.status, headers: [...response.headers.entries()], body: await response.text() };
361
+ const method = input.method ?? 'GET';
362
+ let path = input.path;
363
+ try {
364
+ ensureSession(sessions, session);
365
+ path = normalizePath(input.path);
366
+ if (!allowed.some((prefix) => path === prefix || path.startsWith(prefix)))
367
+ throw new RemoteWebRuntimeError('WEB_PATH_NOT_ALLOWED', 'DSH Web 请求路径不在白名单内');
368
+ const init = { method };
369
+ if (input.headers !== undefined)
370
+ init.headers = Object.fromEntries(input.headers);
371
+ if (input.body !== undefined)
372
+ init.body = normalizeConnectionRpcBody(path, method, input.body);
373
+ await ensureAuthenticated();
374
+ const response = await fetchLocal(path, init);
375
+ debug.log('web.provider.request.response', { sessionId: session.sessionId, path, method, status: response.status });
376
+ return { status: response.status, headers: [...response.headers.entries()], body: await response.text() };
377
+ }
378
+ catch (error) {
379
+ // 失败原因只以稳定错误码进入日志:路径和状态足够定位是哪类请求,
380
+ // 具体报文仍然不进日志。
381
+ const errorCode = error instanceof RemoteWebRuntimeError ? error.code : 'WEB_REQUEST_FAILED';
382
+ debug.log('web.provider.request.error', { sessionId: safeSessionId(session), path, method, errorCode });
383
+ throw error instanceof RemoteWebRuntimeError ? error : new RemoteWebRuntimeError(errorCode, error instanceof Error ? error.message : String(error));
384
+ }
366
385
  },
367
386
  async openWebSocket(session, path) {
368
387
  ensureSession(sessions, session);
@@ -527,11 +546,15 @@ async function readAsset(response) {
527
546
  }
528
547
  function ensureSession(sessions, session) {
529
548
  if (sessions.get(session.sessionId) !== session)
530
- throw new Error('DSH Web Session 已失效');
549
+ throw new RemoteWebRuntimeError('WEB_SESSION_NOT_FOUND', 'DSH Web Session 已失效');
550
+ }
551
+ /** 会话可能已经不在 map 里(例如 Host 重启或旧 iframe 的收尾请求),日志取不到 id 也要能记录。 */
552
+ function safeSessionId(session) {
553
+ return session === undefined ? undefined : session.sessionId;
531
554
  }
532
555
  function normalizePath(value) {
533
556
  if (!value.startsWith('/') || value.includes('\\') || value.split('/').some((part) => part === '..'))
534
- throw new Error('DSH Web 路径无效');
557
+ throw new RemoteWebRuntimeError('WEB_PATH_INVALID', 'DSH Web 路径无效');
535
558
  return value;
536
559
  }
537
560
  function toBytes(value) {
@@ -1,7 +1,25 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
- import type { ConnectionRpcHandler } from '@deepseek-ai/dsh-client-connection';
3
- import type { SettingsProvider } from '@deepseek-ai/dsh-settings';
4
2
  import { type CodingNsRpcHandler, type CodingNsRpcTable } from './rpc-table.js';
3
+ import type { DshHostSettingsProvider } from '../dsh-capabilities/host/config-forms-adapter.js';
4
+ /** 0.2 Connection handler 的 Peer 参数;旧版 handler 仍可通过可选参数调用。 */
5
+ /** CodingNS 自有的 Connection RPC 结果契约,避免绑定 DSH 具体导出名称。 */
6
+ type CodingNsConnectionRpcResult<T = unknown> = {
7
+ readonly ok: true;
8
+ readonly value: T;
9
+ readonly attachments?: readonly CodingNsConnectionRpcAttachment[];
10
+ } | {
11
+ readonly ok: false;
12
+ readonly error: {
13
+ readonly code: string;
14
+ readonly message: string;
15
+ readonly details: object;
16
+ };
17
+ };
18
+ interface CodingNsConnectionRpcAttachment {
19
+ readonly path: readonly (string | number)[];
20
+ readonly bytes: Uint8Array;
21
+ }
22
+ type CodingNsConnectionRpcHandler = (endpoint: string, payload: unknown, signal: AbortSignal, peer?: unknown) => Promise<CodingNsConnectionRpcResult<unknown>>;
5
23
  /**
6
24
  * 创建 Codingns4DSH Host RPC 主处理器。
7
25
  *
@@ -10,9 +28,10 @@ import { type CodingNsRpcHandler, type CodingNsRpcTable } from './rpc-table.js';
10
28
  * 之间的唯一边界:密码只在一次 RPC 请求中经过 Host,refresh token 只进入 Host
11
29
  * 凭据存储。
12
30
  */
13
- export declare function createCodingNsRpcHandler(table: CodingNsRpcTable): ConnectionRpcHandler;
31
+ export declare function createCodingNsRpcHandler(table: CodingNsRpcTable): CodingNsConnectionRpcHandler;
14
32
  /** 在当前 Connection 上挂载 Codingns4DSH RPC 主处理器;注销由调用方的 effect 负责。 */
15
- export declare function registerCodingNsRpc(ctx: Context, table: CodingNsRpcTable, settingsProvider?: SettingsProvider): void;
33
+ export declare function registerCodingNsRpc(ctx: Context, table: CodingNsRpcTable, settingsProvider?: DshHostSettingsProvider): void;
16
34
  /** 创建远程设置处理器;只允许 Codingns4DSH 自己的 namespace 和路径编辑。 */
17
- export declare function createCodingNsSettingsRpcHandler(provider: SettingsProvider): CodingNsRpcHandler;
35
+ export declare function createCodingNsSettingsRpcHandler(provider: DshHostSettingsProvider): CodingNsRpcHandler;
36
+ export {};
18
37
  //# sourceMappingURL=rpc.d.ts.map