lark-remote 0.4.1 → 0.4.3

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 (86) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.en.md +2 -2
  3. package/README.md +2 -2
  4. package/dist/bridge/approval-coordinator.d.ts +2 -0
  5. package/dist/bridge/approval-coordinator.js +16 -7
  6. package/dist/bridge/index.d.ts +22 -0
  7. package/dist/bridge/index.js +138 -56
  8. package/dist/card/approval-render.js +9 -8
  9. package/dist/card/card-budget.js +39 -26
  10. package/dist/card/run-renderer.js +9 -2
  11. package/dist/card/text-truncate.d.ts +9 -10
  12. package/dist/card/text-truncate.js +48 -43
  13. package/dist/cli.js +17 -0
  14. package/dist/config/codex-config.d.ts +3 -0
  15. package/dist/config/codex-config.js +4 -1
  16. package/dist/config/dir.js +6 -6
  17. package/dist/config/index.d.ts +7 -5
  18. package/dist/config/index.js +52 -13
  19. package/dist/config/kimi-config.d.ts +4 -1
  20. package/dist/config/kimi-config.js +5 -2
  21. package/dist/config/pi-config.d.ts +2 -7
  22. package/dist/config/pi-config.js +6 -6
  23. package/dist/config/wizard.js +8 -4
  24. package/dist/connector/index.d.ts +19 -0
  25. package/dist/connector/index.js +134 -23
  26. package/dist/inbound/placeholder.d.ts +3 -2
  27. package/dist/inbound/placeholder.js +17 -7
  28. package/dist/index.js +28 -6
  29. package/dist/instance-lock.d.ts +31 -15
  30. package/dist/instance-lock.js +64 -62
  31. package/dist/platform/agent-stopper.d.ts +29 -7
  32. package/dist/platform/agent-stopper.js +40 -8
  33. package/dist/platform/identity.d.ts +59 -3
  34. package/dist/platform/identity.js +105 -30
  35. package/dist/platform/path.d.ts +0 -7
  36. package/dist/platform/path.js +1 -51
  37. package/dist/platform/shell.d.ts +14 -0
  38. package/dist/platform/shell.js +35 -2
  39. package/dist/platform/sleep-blocker.d.ts +6 -0
  40. package/dist/platform/sleep-blocker.js +31 -5
  41. package/dist/platform/spawn.d.ts +1 -1
  42. package/dist/platform/spawn.js +1 -1
  43. package/dist/platform/terminator-posix.d.ts +1 -29
  44. package/dist/platform/terminator-posix.js +32 -30
  45. package/dist/platform/terminator-win32.js +4 -2
  46. package/dist/platform/terminator.d.ts +2 -2
  47. package/dist/platform/terminator.js +17 -2
  48. package/dist/platform/types.d.ts +3 -0
  49. package/dist/router/card-action-feedback.d.ts +17 -0
  50. package/dist/router/card-action-feedback.js +31 -0
  51. package/dist/router/card-helpers.d.ts +38 -2
  52. package/dist/router/card-helpers.js +79 -2
  53. package/dist/router/config/claude.js +8 -5
  54. package/dist/router/index.d.ts +41 -6
  55. package/dist/router/index.js +254 -84
  56. package/dist/runner/claude/runner.js +1 -1
  57. package/dist/runner/claude/session.d.ts +2 -6
  58. package/dist/runner/claude/session.js +7 -8
  59. package/dist/runner/codex/app-server/protocol-types.d.ts +4 -0
  60. package/dist/runner/codex/app-server/protocol-types.js +4 -0
  61. package/dist/runner/codex/app-server/runner.js +3 -0
  62. package/dist/runner/common/acp/base-acp-runner.js +7 -0
  63. package/dist/runner/common/connection-based-runner.d.ts +18 -0
  64. package/dist/runner/common/connection-based-runner.js +31 -0
  65. package/dist/runner/common/jsonrpc/client.js +6 -1
  66. package/dist/runner/common/jsonrpc/connection-manager.d.ts +20 -0
  67. package/dist/runner/common/jsonrpc/connection-manager.js +27 -1
  68. package/dist/runner/common/jsonrpc/transport.d.ts +29 -1
  69. package/dist/runner/common/jsonrpc/transport.js +79 -10
  70. package/dist/runner/common/spawning-runner.d.ts +49 -19
  71. package/dist/runner/common/spawning-runner.js +94 -54
  72. package/dist/runner/dsh/client.d.ts +1 -3
  73. package/dist/runner/dsh/client.js +0 -4
  74. package/dist/runner/dsh/types.d.ts +0 -36
  75. package/dist/runner/kimi/acp/runner.d.ts +18 -1
  76. package/dist/runner/kimi/acp/runner.js +100 -42
  77. package/dist/runner/kimi/acp/translator.d.ts +0 -1
  78. package/dist/runner/opencode/acp/translator.d.ts +0 -1
  79. package/dist/runner/pi/rpc/protocol-types.d.ts +4 -0
  80. package/dist/runner/pi/rpc/runner.js +3 -0
  81. package/dist/runner/resolve-agent-choices.js +0 -5
  82. package/package.json +2 -2
  83. package/dist/runner/common/process-stopper.d.ts +0 -7
  84. package/dist/runner/common/process-stopper.js +0 -8
  85. package/dist/runner/dsh/fake-dsh-server.d.ts +0 -60
  86. package/dist/runner/dsh/fake-dsh-server.js +0 -156
@@ -17,7 +17,8 @@
17
17
  * - AskUserQuestion flows through elicitation forms + a request_permission
18
18
  * question bridge (buildQuestionResponse)
19
19
  */
20
- import { spawnProcess, mergeProcessEnv, useDetachedProcessGroup } from '../../../platform/spawn.js';
20
+ import { mergeProcessEnv, useDetachedProcessGroup } from '../../../platform/spawn.js';
21
+ import { createShellBackend } from '../../../platform/shell.js';
21
22
  import { StringDecoder } from 'node:string_decoder';
22
23
  import { ConnectionManager, } from '../../common/jsonrpc/connection-manager.js';
23
24
  import { KimiAcpTranslator } from './translator.js';
@@ -26,7 +27,7 @@ import { KIMI_APPROVAL_KINDS, buildAcpPermissionOutcome, } from '../../common/ac
26
27
  import { mapAnswersByIndex } from '../../question-common.js';
27
28
  import { getLogger } from '../../../logger/index.js';
28
29
  import { BaseAcpRunner } from '../../common/acp/base-acp-runner.js';
29
- import { ProcessStopper } from '../../common/process-stopper.js';
30
+ import { createTerminator } from '../../../platform/terminator.js';
30
31
  /** §5.2: compaction 等待的默认沉默超时(10 分钟,独立于 turn idle 默认 30 分钟)。 */
31
32
  const COMPACT_IDLE_TIMEOUT_MS = 10 * 60_000;
32
33
  /** Poll interval for the wire.jsonl compaction record. */
@@ -134,6 +135,7 @@ function buildQuestionResponse(action, pending, response) {
134
135
  // =============================================================================
135
136
  export class KimiAcpRunner extends BaseAcpRunner {
136
137
  permissionMode;
138
+ thinkingEffort;
137
139
  compactIdleTimeoutMs;
138
140
  /**
139
141
  * §5.2 compaction baseline: wire.jsonl records sampled by compactBaseline()
@@ -155,7 +157,17 @@ export class KimiAcpRunner extends BaseAcpRunner {
155
157
  bashToolCallIds = new Set();
156
158
  /** 当前 run 的 session cwd(terminal/create 未给 cwd 时回退用)。 */
157
159
  activeCwd = null;
158
- processStopper = new ProcessStopper({ graceMs: 2_000 });
160
+ /**
161
+ * 终端子进程终止器(terminal/kill|release 与 dispose 清理用)。agent 必须是
162
+ * 'kimi':win32 上优雅段靠协议停止通道,查错 key 会静默退化成直接树杀。
163
+ */
164
+ terminator = createTerminator({ graceMs: 2_000, agent: 'kimi' });
165
+ /**
166
+ * shell seam:terminal/create 的可执行解析走这里(design.md §7.2)。kimi 的
167
+ * ACP 服务端固定要 `bash -c`,win32 上没有 Git Bash 时必须在 seam 里变成
168
+ * 明确错误,而不是一个语焉不详的 spawn ENOENT。
169
+ */
170
+ shell = createShellBackend();
159
171
  constructor(opts) {
160
172
  const managerOpts = {
161
173
  binary: opts.binary ?? 'kimi',
@@ -189,6 +201,7 @@ export class KimiAcpRunner extends BaseAcpRunner {
189
201
  turnIdleTimeoutMs: opts.turnIdleTimeoutMs,
190
202
  }, new ConnectionManager(managerOpts));
191
203
  this.model = opts.model;
204
+ this.thinkingEffort = opts.thinkingEffort;
192
205
  this.permissionMode = opts.permissionMode ?? 'manual';
193
206
  this.compactIdleTimeoutMs = opts.compactIdleTimeoutMs ?? COMPACT_IDLE_TIMEOUT_MS;
194
207
  }
@@ -226,6 +239,25 @@ export class KimiAcpRunner extends BaseAcpRunner {
226
239
  getLogger().warn(`[${this.logTag}] set_config_option model failed (non-fatal): ${err.message}`);
227
240
  }
228
241
  }
242
+ // A5: 下发配置的思考强度。configId 与取值不是猜的——kimi 真机(0.43.1)
243
+ // `session/new` 就通告 `{id:'thinking', category:'thought_level',
244
+ // options:[low|high|max]}`,set 成功后响应里 `currentValue` 变成新值;
245
+ // 未知 configId / 非法取值回 -32602(不静默接受),所以失败告警即可判定。
246
+ // 换模型后新模型可能不支持当前档 → 服务端拒绝、保持其默认档,卡片侧
247
+ // `thinkingEffortPatchForModel` 已按 supportEfforts 预挑兼容档。
248
+ if (this.thinkingEffort) {
249
+ try {
250
+ await client.request('session/set_config_option', {
251
+ sessionId,
252
+ configId: 'thinking',
253
+ value: this.thinkingEffort,
254
+ });
255
+ }
256
+ catch (err) {
257
+ getLogger().warn(`[${this.logTag}] set_config_option thinking failed (non-fatal): ` +
258
+ `${err.message}`);
259
+ }
260
+ }
229
261
  // Set permission mode (§5.1): EVERY fresh ACP session starts in
230
262
  // 'default' (= manual approvals, acp-server modes.ts DEFAULT_MODE_ID),
231
263
  // so the mode must be sent unconditionally — skipping yolo leaves the
@@ -266,10 +298,11 @@ export class KimiAcpRunner extends BaseAcpRunner {
266
298
  // Derived from the pre-fire snapshot captured by compactBaseline() —
267
299
  // re-reading here would race the prompt fire and misclassify new records.
268
300
  const baselineTerminalCount = this.compactBaselineRecords.filter(isCompactionTerminalLike).length;
269
- promptPromise.then(async (result) => {
301
+ let promptRejected = false;
302
+ // prompt 结算只供两样东西:成功 result 的翻译素材(stopReason/usage)与 §5.4
303
+ // 探针。终态判定不再挂在它后面(A7)。
304
+ const promptSettled = promptPromise.then((result) => {
270
305
  this.promptSettled = true;
271
- if (this.stopRequested)
272
- return; // already cancelled
273
306
  if (compactionReader) {
274
307
  // §5.4 可选防线(日志探针):prompt settle 后既无新 begin 也无新
275
308
  // terminal → compact 未被引擎接受(可能已在跑或被拒绝)。不解析
@@ -278,31 +311,50 @@ export class KimiAcpRunner extends BaseAcpRunner {
278
311
  if (totalNow === baselineTotalCount) {
279
312
  getLogger().warn(`[${this.logTag}] compact 未被引擎接受(无新 begin/terminal 记录,可能已在跑或被拒绝)`);
280
313
  }
281
- const outcome = await this.waitForCompactionTerminal(compactionReader, opts, baselineTerminalCount);
282
- if (this.forceFinish)
283
- return; // stopped while polling
284
- if (outcome === 'completed') {
285
- // 压缩完成:正常翻译 prompt 结果(subtype success)。
286
- const resultEvent = translator.handlePromptResponse(sessionId, result);
287
- this.pushEvents([resultEvent]);
288
- }
289
- else if (outcome === 'cancelled') {
290
- this.pushEvents([
291
- translator.produceErrorResult(sessionId, '压缩未完成:已被取消或压缩请求失败(可重试)'),
292
- ]);
293
- }
294
- else if (outcome === 'unknown') {
295
- this.pushEvents([
296
- translator.produceErrorResult(sessionId, `压缩状态未知:超过 ${formatIdleWindow(this.compactIdleTimeoutMs)} 未观察到完成/取消记录,压缩可能仍在后台进行`),
297
- ]);
298
- }
299
- // outcome === 'stopped':不推 result,由 consumeTurn 的 interrupted 兜底。
300
314
  }
301
- else {
302
- const resultEvent = translator.handlePromptResponse(sessionId, result);
303
- this.pushEvents([resultEvent]);
315
+ return result;
316
+ }, (err) => {
317
+ promptRejected = true;
318
+ this.handlePromptRejected(err, translator, sessionId, 'compact');
319
+ return undefined;
320
+ });
321
+ if (!compactionReader) {
322
+ // 读不到 compaction 记录(reader 未实现):沿用「prompt 结算即成功」的口径。
323
+ void promptSettled.then((result) => {
324
+ if (result)
325
+ this.pushEvents([translator.handlePromptResponse(sessionId, result)]);
326
+ });
327
+ return;
328
+ }
329
+ // A7:语义等待在 prompt 触发的那一刻武装。引擎静默超过 compactIdleTimeoutMs
330
+ // 就给出 compact 语义终态,不等 session/prompt 结算——否则挂住的 prompt 会把
331
+ // 用户拖到通用 turnIdleTimeout(默认 30min),拿到的还是一个语义泛化的 turn
332
+ // 超时,而不是「压缩状态未知 / 可重试」。
333
+ void this.waitForCompactionTerminal(compactionReader, opts, baselineTerminalCount).then(async (outcome) => {
334
+ // outcome === 'stopped':不推 result,由 consumeTurn 的 interrupted 兜底。
335
+ if (outcome === 'stopped' || this.forceFinish || this.stopRequested)
336
+ return;
337
+ if (outcome === 'cancelled') {
338
+ this.pushEvents([
339
+ translator.produceErrorResult(sessionId, '压缩未完成:已被取消或压缩请求失败(可重试)'),
340
+ ]);
341
+ return;
342
+ }
343
+ if (outcome === 'unknown') {
344
+ this.pushEvents([
345
+ translator.produceErrorResult(sessionId, `压缩状态未知:超过 ${formatIdleWindow(this.compactIdleTimeoutMs)} 未观察到完成/取消记录,压缩可能仍在后台进行`),
346
+ ]);
347
+ return;
304
348
  }
305
- }, (err) => this.handlePromptRejected(err, translator, sessionId, 'compact'));
349
+ // completed:成功 result 需要 prompt 的 stopReason/usage。§5.2 的正常时序里
350
+ // prompt 先于终态记录结算,这里通常是在等一个已完成的 promise。
351
+ const result = await promptSettled;
352
+ if (!result || promptRejected)
353
+ return; // handlePromptRejected 已产 error result
354
+ if (this.forceFinish || this.stopRequested)
355
+ return;
356
+ this.pushEvents([translator.handlePromptResponse(sessionId, result)]);
357
+ });
306
358
  }
307
359
  buildApprovalOutcome(action, pending, response) {
308
360
  return pending.kind === 'question'
@@ -501,17 +553,23 @@ export class KimiAcpRunner extends BaseAcpRunner {
501
553
  : undefined;
502
554
  const cwd = params.cwd ?? this.activeCwd ?? undefined;
503
555
  const outputLimit = params.outputByteLimit ?? TERMINAL_DEFAULT_OUTPUT_LIMIT;
504
- const proc = spawnProcess(params.command, params.args ?? [], {
556
+ // 走 shell seam(design.md §7.2):argv 逐 token 保留,只把「可执行文件在哪」
557
+ // 收口——win32 上 Git Bash 缺失时在这里抛 ShellUnavailableError,由
558
+ // handleTerminalRequest 转成明确的 Terminal request failed 回执,而不是一个
559
+ // 语焉不详的 spawn ENOENT。
560
+ const proc = this.shell.spawnArgv(params.command, params.args ?? [], {
505
561
  cwd,
506
- // env 覆盖大小写不敏感合并(win32 PATH/Path 双键防护,v2 §8.3)
507
- env: env ? mergeProcessEnv(process.env, env) : undefined,
508
- stdio: ['ignore', 'pipe', 'pipe'],
509
- // ProcessStopper 用负 PID 杀进程组(kill(-pgid));子进程必须是组长
510
- // 才能命中,否则 kill(-pid) 抛 ESRCH 被吞 → kill/release/清理全失效。
511
- // 与 JsonlRpcTransport / spawning-runner 同模式;win32 不 detached
512
- // (`.cmd` 垫片在 DETACHED_PROCESS 下丢 stdio),树杀由 taskkill /T 负责。
513
- detached: useDetachedProcessGroup(),
514
- windowsHide: true,
562
+ options: {
563
+ // env 覆盖大小写不敏感合并(win32 PATH/Path 双键防护,v2 §8.3)
564
+ env: env ? mergeProcessEnv(process.env, env) : undefined,
565
+ stdio: ['ignore', 'pipe', 'pipe'],
566
+ // Terminator 用负 PID 杀进程组(kill(-pgid));子进程必须是组长
567
+ // 才能命中,否则 kill(-pid) 抛 ESRCH 被吞 → kill/release/清理全失效。
568
+ // 与 JsonlRpcTransport / spawning-runner 同模式;win32 不 detached
569
+ // (`.cmd` 垫片在 DETACHED_PROCESS 下丢 stdio),树杀由 taskkill /T 负责。
570
+ detached: useDetachedProcessGroup(),
571
+ windowsHide: true,
572
+ },
515
573
  });
516
574
  // 每个流独立 decoder:多字节字符跨 chunk 拆分时由 decoder 保留不完整
517
575
  // 序列,避免按 chunk 单独 toString 产生 U+FFFD 替换符。
@@ -651,7 +709,7 @@ export class KimiAcpRunner extends BaseAcpRunner {
651
709
  this.currentClient?.respondError(id, RpcErrorCode.INVALID_PARAMS, `Unknown terminal: ${params.terminalId}`);
652
710
  return;
653
711
  }
654
- await this.processStopper.stop(handle.proc, { immediate: true });
712
+ await this.terminator.stop(handle.proc, { immediate: true });
655
713
  this.currentClient?.respond(id, {});
656
714
  }
657
715
  /** terminal/release: 从 map 删除;进程仍在则一并杀,回 {}。 */
@@ -663,7 +721,7 @@ export class KimiAcpRunner extends BaseAcpRunner {
663
721
  }
664
722
  this.terminals.delete(params.terminalId);
665
723
  // stop 对已退出进程是 no-op;仍在跑则立即 SIGTERM+SIGKILL。
666
- await this.processStopper.stop(handle.proc, { immediate: true });
724
+ await this.terminator.stop(handle.proc, { immediate: true });
667
725
  this.currentClient?.respond(id, {});
668
726
  }
669
727
  /** Kill every live terminal (dispose/connection-close 清理钩子)。 */
@@ -671,7 +729,7 @@ export class KimiAcpRunner extends BaseAcpRunner {
671
729
  const handles = [...this.terminals.values()];
672
730
  this.terminals.clear();
673
731
  for (const handle of handles) {
674
- await this.processStopper.stop(handle.proc, { immediate: true });
732
+ await this.terminator.stop(handle.proc, { immediate: true });
675
733
  }
676
734
  }
677
735
  // =========================================================================
@@ -27,7 +27,6 @@
27
27
  */
28
28
  import { type RequestPermissionParams, type AgentMessageChunkEvent, type AgentThoughtChunkEvent } from '../../common/acp/protocol-types.js';
29
29
  import { BaseAcpTranslator, type AcpTranslatorEvent } from '../../common/acp/base-translator.js';
30
- export type { AcpTurnStartedEvent, AcpLiveUsage, AcpUsageEvent, AcpTranslatorEvent, } from '../../common/acp/base-translator.js';
31
30
  export declare class KimiAcpTranslator extends BaseAcpTranslator {
32
31
  protected readonly logTag = "kimi-acp-translator";
33
32
  /** Accumulated text per tool-call or message (keyed by a logical item id). */
@@ -26,7 +26,6 @@
26
26
  */
27
27
  import type { RequestPermissionParams, AgentMessageChunkEvent, AgentThoughtChunkEvent, ToolCallUpdateEvent, UsageUpdateEvent } from '../../common/acp/protocol-types.js';
28
28
  import { BaseAcpTranslator, type AcpTranslatorEvent } from '../../common/acp/base-translator.js';
29
- export type { AcpTurnStartedEvent as OpencodeAcpTurnStartedEvent, AcpLiveUsage as OpencodeAcpLiveUsage, AcpUsageEvent as OpencodeAcpUsageEvent, AcpTranslatorEvent as OpencodeAcpTranslatorEvent, } from '../../common/acp/base-translator.js';
30
29
  export declare class OpencodeAcpTranslator extends BaseAcpTranslator {
31
30
  protected readonly logTag = "opencode-acp-translator";
32
31
  protected handleAgentMessageChunk(event: AgentMessageChunkEvent, _sessionId: string): AcpTranslatorEvent[];
@@ -32,6 +32,10 @@ export type PiRpcCommand = {
32
32
  } | {
33
33
  type: 'get_state';
34
34
  };
35
+ /**
36
+ * get_state 的完整返回体。桥只读 sessionId,其余字段照上游 schema 保留:当前没有
37
+ * consumer 是有意为之(静态扫描报的"零引用导出类型"即指此处),别据此删除。
38
+ */
35
39
  export interface PiRpcGetStateData {
36
40
  model?: unknown;
37
41
  sessionId: string;
@@ -53,6 +53,9 @@ export class PiRpcRunner extends ConnectionBasedRunner {
53
53
  idleTtlMs: opts.idleTtlMs,
54
54
  logTag: 'pi-rpc-connection-manager',
55
55
  });
56
+ // 协议停止通道(design §3.3):pi 的 cancel 是 `{type:'abort'}` 通知,已由
57
+ // cancelCurrentTurn 实现;登记后 win32 优雅停止才有通道可用(否则一路树杀)。
58
+ this.manager.stopper = ({ client }) => this.buildCooperativeStop(client);
56
59
  }
57
60
  get logTag() {
58
61
  return 'pi-rpc-runner';
@@ -20,11 +20,6 @@ export function resolveAgentChoices(config) {
20
20
  }
21
21
  // Clone to avoid mutation
22
22
  const resolved = structuredClone(config);
23
- // agents.codex 在 schema 中必填(带默认值),此处按旧语义补空对象;
24
- // 运行时只往 [agent] 槽位写字段,不依赖 codex 默认值。
25
- if (!resolved.agents) {
26
- resolved.agents = {};
27
- }
28
23
  const agents = resolved.agents;
29
24
  const target = (agents[agent] ??= {});
30
25
  const source = agentChoices;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lark-remote",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Feishu (Lark) private-chat bridge to local coding agents — Claude Code / Codex / opencode / pi / kimi, with live-streaming CardKit 2.0 cards",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -94,7 +94,7 @@
94
94
  "type": "module",
95
95
  "dependencies": {
96
96
  "@iarna/toml": "^2.2.5",
97
- "@larksuite/channel": "^0.3.0",
97
+ "@larksuite/channel": "^0.7.1",
98
98
  "axios": "^1.18.0",
99
99
  "cross-spawn": "^7.0.6",
100
100
  "form-data": "^4.0.6",
@@ -1,7 +0,0 @@
1
- /**
2
- * 已迁移到平台 seam:`src/platform/terminator-posix.ts`(design.md §3.2)。
3
- *
4
- * 本文件只保留 re-export,使既有调用方(bash runner、jsonrpc transport 等)
5
- * 与既有测试不受影响;M2 调用点迁移时逐个换成 `createTerminator()` 后删除。
6
- */
7
- export { ProcessStopper } from '../../platform/terminator-posix.js';
@@ -1,8 +0,0 @@
1
- /**
2
- * 已迁移到平台 seam:`src/platform/terminator-posix.ts`(design.md §3.2)。
3
- *
4
- * 本文件只保留 re-export,使既有调用方(bash runner、jsonrpc transport 等)
5
- * 与既有测试不受影响;M2 调用点迁移时逐个换成 `createTerminator()` 后删除。
6
- */
7
- export { ProcessStopper } from '../../platform/terminator-posix.js';
8
- //# sourceMappingURL=process-stopper.js.map
@@ -1,60 +0,0 @@
1
- /**
2
- * Fake DSH Web Host HTTP server for agent-dsh tests.
3
- *
4
- * Mimics the real DSH wire protocol (deepseek-harness apiproxy handler.ts):
5
- * - POST /api/<method>: body {type:'client-request',rpcId,method,payload},
6
- * response {type:'server-response',rpcId,result:{ok:true,value}|{ok:false,error}}.
7
- * - SSE GET /api/events.mux: frames `data: {type:'server-request',rpcId,
8
- * method:<frame.type>, payload:<frame>}\n\n`.
9
- *
10
- * The mux frame queue is consumed ACROSS connections (shared pointer): the
11
- * first connection drains a prefix then closes; a reconnect drains the rest —
12
- * which is exactly how a server that drops mid-turn is modelled. Unary
13
- * handlers and the history value are registered by the test.
14
- */
15
- import { type Server } from 'node:http';
16
- export interface FakeRpcResult {
17
- ok: true;
18
- value: unknown;
19
- }
20
- export interface FakeRpcError {
21
- ok: false;
22
- error: {
23
- code: string;
24
- message: string;
25
- details?: Record<string, unknown>;
26
- };
27
- }
28
- export type FakeUnaryHandler = (payload: unknown, rpcId: string) => FakeRpcResult | FakeRpcError;
29
- export interface DshServerRequestRecord {
30
- method: string;
31
- rpcId: string;
32
- payload: unknown;
33
- }
34
- export declare class FakeDshServer {
35
- readonly server: Server;
36
- private readonly unaryHandlers;
37
- private muxFrames;
38
- private historyValue;
39
- private port;
40
- private closedConnections;
41
- readonly requests: DshServerRequestRecord[];
42
- constructor();
43
- get baseUrl(): string;
44
- get connectionCount(): number;
45
- start(): Promise<void>;
46
- stop(): Promise<void>;
47
- register(method: string, handler: FakeUnaryHandler): void;
48
- setMuxFrames(frames: Array<object | {
49
- close: true;
50
- }>): void;
51
- setHistoryValue(value: unknown): void;
52
- /** Synchronous unary dispatch (no HTTP) — used by tests that inject the
53
- * reader's sync transport to avoid the spawnSync-in-process deadlock. */
54
- dispatchSync(method: string, payload: unknown, rpcId?: string): {
55
- type: string;
56
- rpcId: string;
57
- result: FakeRpcResult | FakeRpcError;
58
- };
59
- private handleMux;
60
- }
@@ -1,156 +0,0 @@
1
- /**
2
- * Fake DSH Web Host HTTP server for agent-dsh tests.
3
- *
4
- * Mimics the real DSH wire protocol (deepseek-harness apiproxy handler.ts):
5
- * - POST /api/<method>: body {type:'client-request',rpcId,method,payload},
6
- * response {type:'server-response',rpcId,result:{ok:true,value}|{ok:false,error}}.
7
- * - SSE GET /api/events.mux: frames `data: {type:'server-request',rpcId,
8
- * method:<frame.type>, payload:<frame>}\n\n`.
9
- *
10
- * The mux frame queue is consumed ACROSS connections (shared pointer): the
11
- * first connection drains a prefix then closes; a reconnect drains the rest —
12
- * which is exactly how a server that drops mid-turn is modelled. Unary
13
- * handlers and the history value are registered by the test.
14
- */
15
- import { createServer } from 'node:http';
16
- import { randomUUID } from 'node:crypto';
17
- export class FakeDshServer {
18
- server;
19
- unaryHandlers = new Map();
20
- muxFrames = [];
21
- historyValue = { events: [] };
22
- port = 0;
23
- closedConnections = 0;
24
- requests = [];
25
- constructor() {
26
- this.server = createServer((req, res) => {
27
- const url = req.url ?? '';
28
- const path = url.split('?')[0];
29
- if (req.method === 'GET' && path === '/api/events.mux') {
30
- this.closedConnections++;
31
- this.handleMux(res);
32
- return;
33
- }
34
- if (req.method !== 'POST' || !path.startsWith('/api/')) {
35
- res.writeHead(404);
36
- res.end('not found');
37
- return;
38
- }
39
- let raw = '';
40
- req.on('data', (c) => (raw += c));
41
- req.on('end', () => {
42
- let body = {};
43
- try {
44
- body = JSON.parse(raw);
45
- }
46
- catch {
47
- res.writeHead(400);
48
- res.end('bad request');
49
- return;
50
- }
51
- const method = path.slice('/api/'.length);
52
- if (body.type !== 'client-request' || !body.rpcId || body.method !== method) {
53
- res.writeHead(400);
54
- res.end('bad request');
55
- return;
56
- }
57
- const rpcId = body.rpcId;
58
- this.requests.push({ method, rpcId, payload: body.payload });
59
- const handler = this.unaryHandlers.get(method);
60
- let result;
61
- if (handler) {
62
- result = handler(body.payload, rpcId);
63
- }
64
- else if (method === 'session.history') {
65
- result = { ok: true, value: this.historyValue };
66
- }
67
- else if (method === 'session.list') {
68
- result = { ok: true, value: { items: [] } };
69
- }
70
- else {
71
- result = { ok: true, value: {} };
72
- }
73
- res.writeHead(200, { 'content-type': 'application/json' });
74
- res.end(JSON.stringify({ type: 'server-response', rpcId, result }));
75
- });
76
- });
77
- }
78
- get baseUrl() {
79
- return `http://127.0.0.1:${this.port}`;
80
- }
81
- get connectionCount() {
82
- return this.closedConnections;
83
- }
84
- async start() {
85
- await new Promise((resolve) => this.server.listen(0, '127.0.0.1', resolve));
86
- const addr = this.server.address();
87
- this.port = addr.port;
88
- }
89
- async stop() {
90
- await new Promise((resolve) => this.server.close(() => resolve()));
91
- }
92
- register(method, handler) {
93
- this.unaryHandlers.set(method, handler);
94
- }
95
- setMuxFrames(frames) {
96
- this.muxFrames = frames;
97
- }
98
- setHistoryValue(value) {
99
- this.historyValue = value;
100
- }
101
- /** Synchronous unary dispatch (no HTTP) — used by tests that inject the
102
- * reader's sync transport to avoid the spawnSync-in-process deadlock. */
103
- dispatchSync(method, payload, rpcId = `rpc-${randomUUID()}`) {
104
- this.requests.push({ method, rpcId, payload });
105
- const handler = this.unaryHandlers.get(method);
106
- let result;
107
- if (handler) {
108
- result = handler(payload, rpcId);
109
- }
110
- else if (method === 'session.history') {
111
- result = { ok: true, value: this.historyValue };
112
- }
113
- else if (method === 'session.list') {
114
- result = { ok: true, value: { items: [] } };
115
- }
116
- else {
117
- result = { ok: true, value: {} };
118
- }
119
- return { type: 'server-response', rpcId, result };
120
- }
121
- handleMux(res) {
122
- res.writeHead(200, {
123
- 'content-type': 'text/event-stream',
124
- 'cache-control': 'no-cache',
125
- connection: 'keep-alive',
126
- });
127
- // Drain the SHARED script (shift): the first connection consumes a prefix
128
- // then closes (a {close:true} marker or an exhausted queue); a reconnect
129
- // continues from where it left off — the natural model of a mid-turn drop.
130
- const timer = setInterval(() => {
131
- const item = this.muxFrames.shift();
132
- if (item === undefined) {
133
- clearInterval(timer);
134
- res.end();
135
- return;
136
- }
137
- if ('close' in item && item.close === true) {
138
- clearInterval(timer);
139
- res.end();
140
- return;
141
- }
142
- const frame = item;
143
- res.write(`data: ${JSON.stringify({
144
- type: 'server-request',
145
- rpcId: `mux-${randomUUID()}`,
146
- method: frame.type,
147
- payload: frame,
148
- })}\n\n`);
149
- }, 5);
150
- reqClose(res, timer);
151
- }
152
- }
153
- function reqClose(res, timer) {
154
- res.on('close', () => clearInterval(timer));
155
- }
156
- //# sourceMappingURL=fake-dsh-server.js.map