dsh-connect 0.2.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/runner.js CHANGED
@@ -5,32 +5,27 @@
5
5
  * @module dsh-connect/runner
6
6
  */
7
7
  import { randomUUID } from "node:crypto";
8
- import { existsSync, statSync } from "node:fs";
9
- import { isAbsolute } from "node:path";
8
+ import { existsSync, mkdirSync, statSync } from "node:fs";
9
+ import { copyFile, readFile } from "node:fs/promises";
10
+ import { basename, isAbsolute, join } from "node:path";
10
11
  import { SessionId } from "@deepseek-ai/dsh-session";
11
12
  import { installModelSelection } from "@deepseek-ai/dsh-agent";
12
13
  import { createUserMessage, ReasoningEffortId } from "@deepseek-ai/dsh-llm";
13
14
  import { createAsyncQueue } from "./stream.js";
14
- import { HELP_TEXT, parseCommand } from "./commands.js";
15
+ import { helpText, parseCommand } from "./commands.js";
16
+ import { messages } from "./i18n.js";
15
17
  export function resolveConnectConfig(config) {
16
18
  return {
17
19
  agentPreset: config.agentPreset,
18
20
  workDir: config.workDir,
19
21
  workspaces: config.workspaces ?? [],
22
+ visionModel: config.visionModel,
23
+ language: config.language ?? "zh",
20
24
  allowUsers: config.allowUsers ?? [],
21
25
  allowChats: config.allowChats ?? [],
22
26
  stateDir: config.stateDir,
23
27
  };
24
28
  }
25
- const REASON_LABELS = {
26
- completed: "✅ 完成",
27
- aborted: "⏹️ 已中止",
28
- blocked: "🚫 阻塞",
29
- error: "❌ 出错",
30
- "max-tokens": "⚠️ 达到输出上限",
31
- interrupted: "⚠️ 被中断",
32
- unknown: "❓ 未知",
33
- };
34
29
  function mapReason(kind) {
35
30
  switch (kind) {
36
31
  case "completed":
@@ -88,15 +83,56 @@ function textOf(message) {
88
83
  .map((b) => b.text ?? "")
89
84
  .join("");
90
85
  }
91
- const MENU_TITLES = {
92
- root: "主菜单",
93
- workspace: "切换工作目录",
94
- chat: "切换对话",
95
- settings: "设置",
96
- model: "切换模型",
97
- reasoning: "推理强度",
98
- notify: "通知设置",
99
- };
86
+ /** Lightweight fold of `schedule/change` events (create / delete / dispatch). */
87
+ function foldReminders(events) {
88
+ const active = new Map();
89
+ for (const e of events) {
90
+ // `schedule/change` is a plugin-extended event type outside dsh-session's map.
91
+ const evt = e;
92
+ if (evt.type !== "schedule/change")
93
+ continue;
94
+ const d = evt.data;
95
+ if (d.version !== 1)
96
+ continue;
97
+ if (d.operation === "create" && d.schedule !== undefined) {
98
+ active.set(d.schedule.id, {
99
+ id: d.schedule.id,
100
+ kind: d.schedule.kind,
101
+ prompt: d.schedule.prompt,
102
+ scheduledAt: d.schedule.scheduledAt,
103
+ everySeconds: d.schedule.everySeconds,
104
+ });
105
+ }
106
+ else if (d.operation === "delete" && d.id !== undefined) {
107
+ active.delete(d.id);
108
+ }
109
+ else if (d.operation === "dispatch" && d.id !== undefined) {
110
+ const rec = active.get(d.id);
111
+ if (rec === undefined)
112
+ continue;
113
+ if (rec.kind === "every" && rec.everySeconds !== undefined && d.acceptedAt !== undefined) {
114
+ const next = Date.parse(d.acceptedAt) + rec.everySeconds * 1000;
115
+ active.set(d.id, { ...rec, scheduledAt: new Date(next).toISOString() });
116
+ }
117
+ else {
118
+ active.delete(d.id);
119
+ }
120
+ }
121
+ }
122
+ return [...active.values()];
123
+ }
124
+ /** Sniff an image's media type from its magic bytes (defaults to PNG). */
125
+ function detectImageMediaType(buf) {
126
+ if (buf.length >= 8 && buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47)
127
+ return "image/png";
128
+ if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff)
129
+ return "image/jpeg";
130
+ if (buf.length >= 12 && buf.toString("ascii", 0, 4) === "RIFF" && buf.toString("ascii", 8, 12) === "WEBP")
131
+ return "image/webp";
132
+ if (buf.length >= 6 && buf.toString("ascii", 0, 3) === "GIF")
133
+ return "image/gif";
134
+ return "image/png";
135
+ }
100
136
  export class AgentRunner {
101
137
  ctx;
102
138
  config;
@@ -113,6 +149,7 @@ export class AgentRunner {
113
149
  workDir;
114
150
  streamEnabled = true;
115
151
  summaryEnabled = true;
152
+ t;
116
153
  constructor(ctx, config, channel, chatKey, chatType, adapter, bindings) {
117
154
  this.ctx = ctx;
118
155
  this.config = config;
@@ -122,6 +159,51 @@ export class AgentRunner {
122
159
  this.adapter = adapter;
123
160
  this.bindings = bindings;
124
161
  this.workDir = config.workDir ?? this.resolveDefaultWorkDir();
162
+ this.t = messages(config.language);
163
+ }
164
+ menuTitle(menuId) {
165
+ switch (menuId) {
166
+ case "root":
167
+ return this.t.menuRoot;
168
+ case "workspace":
169
+ return this.t.menuWorkspace;
170
+ case "chat":
171
+ return this.t.menuChat;
172
+ case "settings":
173
+ return this.t.menuSettings;
174
+ case "model":
175
+ return this.t.menuModel;
176
+ case "reasoning":
177
+ return this.t.menuReasoning;
178
+ case "notify":
179
+ return this.t.menuNotify;
180
+ }
181
+ }
182
+ rootMenuSections() {
183
+ return [
184
+ { title: this.t.rootSectionWorkspace, ids: ["workspace"] },
185
+ { title: this.t.rootSectionChat, ids: ["chat", "history"] },
186
+ { title: this.t.rootSectionTask, ids: ["task", "goals", "schedule"] },
187
+ { title: this.t.rootSectionSystem, ids: ["status", "plugins", "compact", "settings"] },
188
+ ];
189
+ }
190
+ reasonLabel(reason) {
191
+ switch (reason) {
192
+ case "completed":
193
+ return this.t.reasonCompleted;
194
+ case "aborted":
195
+ return this.t.reasonAborted;
196
+ case "blocked":
197
+ return this.t.reasonBlocked;
198
+ case "error":
199
+ return this.t.reasonError;
200
+ case "max-tokens":
201
+ return this.t.reasonMaxTokens;
202
+ case "interrupted":
203
+ return this.t.reasonInterrupted;
204
+ default:
205
+ return this.t.reasonUnknown;
206
+ }
125
207
  }
126
208
  /** Prefer the user's first DSH workspace over the process cwd as default. */
127
209
  resolveDefaultWorkDir() {
@@ -155,7 +237,7 @@ export class AgentRunner {
155
237
  }
156
238
  else if (chunk.type === "reasoning-delta" && !turn.reasoning) {
157
239
  turn.reasoning = true;
158
- turn.chunks.push("🤔 深度思考中…\n");
240
+ turn.chunks.push(this.t.thinkingHint);
159
241
  }
160
242
  }
161
243
  }
@@ -194,7 +276,7 @@ export class AgentRunner {
194
276
  catch (error) {
195
277
  const detail = error instanceof Error ? error.message : String(error);
196
278
  await this.adapter
197
- .sendText(this.target(msg), `⚠️ 处理失败:${truncate(detail)}`)
279
+ .sendText(this.target(msg), this.t.processingFailed(truncate(detail)))
198
280
  .catch(() => undefined);
199
281
  }
200
282
  }
@@ -278,11 +360,12 @@ export class AgentRunner {
278
360
  }
279
361
  async driveAgent(agent, msg) {
280
362
  const firstSeq = agent.session.seq;
363
+ const userMessage = async () => {
364
+ const content = await this.buildUserContent(msg);
365
+ return createUserMessage({ content, source: { kind: "user" } });
366
+ };
281
367
  if (!this.streamEnabled) {
282
- agent.followup(createUserMessage({
283
- content: [{ type: "text", text: msg.text }],
284
- source: { kind: "user" },
285
- }));
368
+ agent.followup(await userMessage());
286
369
  await agent.whenIdle();
287
370
  await this.sessions.flush(agent.session);
288
371
  const outcome = summarizeTurn(agent.session.events, firstSeq);
@@ -294,10 +377,7 @@ export class AgentRunner {
294
377
  const chunks = createAsyncQueue();
295
378
  this.turn = { firstSeq, chunks, lastText: "", reasoning: false };
296
379
  const streamPromise = this.adapter.streamText(this.target(msg), chunks);
297
- agent.followup(createUserMessage({
298
- content: [{ type: "text", text: msg.text }],
299
- source: { kind: "user" },
300
- }));
380
+ agent.followup(await userMessage());
301
381
  await agent.whenIdle();
302
382
  await this.sessions.flush(agent.session);
303
383
  chunks.end();
@@ -307,6 +387,187 @@ export class AgentRunner {
307
387
  this.turn = undefined;
308
388
  return { ...outcome, text };
309
389
  }
390
+ /**
391
+ * Build the user content for one inbound message. When the message carries
392
+ * images, either pass them directly to the main model (if it supports
393
+ * vision) or run a vision sub-task with a capable model and inject its
394
+ * description as text — so a text-only main model never stalls on images.
395
+ */
396
+ async buildUserContent(msg) {
397
+ const content = [{ type: "text", text: msg.text }];
398
+ const rawImages = msg.images;
399
+ if (rawImages === undefined || rawImages.length === 0) {
400
+ if (msg.imageError !== undefined) {
401
+ content.push({ type: "text", text: this.t.imageDownloadFailed(msg.imageError) });
402
+ }
403
+ return content;
404
+ }
405
+ // Copy images into the agent's work directory so its tools can access them.
406
+ const paths = await this.stageImages(rawImages);
407
+ const locationText = paths.map((p) => `- ${p}`).join("\n");
408
+ const mainVision = await this.modelSupportsVision(this.agent?.options.provider, this.agent?.options.model);
409
+ if (mainVision) {
410
+ const imageBlocks = await this.imageBlocks(paths);
411
+ if (imageBlocks.length > 0) {
412
+ this.log(`connect: main model supports vision — passing ${imageBlocks.length} image block(s) directly`);
413
+ return [{ type: "text", text: msg.text }, ...imageBlocks];
414
+ }
415
+ this.log("connect: main model supports vision but image blocks failed to attach — falling back to paths");
416
+ return content;
417
+ }
418
+ const description = await this.describeImages(paths);
419
+ const note = this.t.imagesStaged(locationText, description);
420
+ content.push({ type: "text", text: note });
421
+ return content;
422
+ }
423
+ /** Copy images into `<workDir>/.dsh-connect-images` so the agent's tools can reach them. */
424
+ async stageImages(paths) {
425
+ const dir = join(this.workDir, ".dsh-connect-images");
426
+ try {
427
+ mkdirSync(dir, { recursive: true });
428
+ }
429
+ catch {
430
+ return [...paths];
431
+ }
432
+ const staged = [];
433
+ for (const p of paths) {
434
+ const target = join(dir, basename(p));
435
+ try {
436
+ await copyFile(p, target);
437
+ staged.push(target);
438
+ }
439
+ catch {
440
+ staged.push(p);
441
+ }
442
+ }
443
+ return staged;
444
+ }
445
+ async imageBlocks(paths) {
446
+ const attachments = this.ctx.get("attachments");
447
+ if (attachments?.saveImage === undefined)
448
+ return [];
449
+ const blocks = [];
450
+ for (const path of paths) {
451
+ try {
452
+ const buf = await readFile(path);
453
+ const mediaType = detectImageMediaType(buf);
454
+ const ref = await attachments.saveImage({ data: new Uint8Array(buf), mediaType });
455
+ blocks.push({ type: "image", attachment: ref });
456
+ }
457
+ catch {
458
+ // Skip unreadable / unsupported images.
459
+ }
460
+ }
461
+ return blocks;
462
+ }
463
+ async describeImages(paths) {
464
+ const vision = await this.findVisionModel();
465
+ if (vision === null) {
466
+ this.log("connect: describeImages skipped — no vision model available");
467
+ return "";
468
+ }
469
+ const attachments = this.ctx.get("attachments");
470
+ const llm = this.ctx.get("llm");
471
+ if (attachments?.saveImage === undefined || llm?.stream === undefined) {
472
+ this.log(`connect: describeImages skipped — attachments.saveImage=${attachments?.saveImage !== undefined}, llm.stream=${llm?.stream !== undefined}`);
473
+ return "";
474
+ }
475
+ const refs = [];
476
+ for (const path of paths) {
477
+ try {
478
+ const buf = await readFile(path);
479
+ refs.push(await attachments.saveImage({ data: new Uint8Array(buf), mediaType: detectImageMediaType(buf) }));
480
+ }
481
+ catch (error) {
482
+ this.log(`connect: saveImage failed for ${path}: ${String(error)}`);
483
+ }
484
+ }
485
+ if (refs.length === 0) {
486
+ this.log("connect: describeImages skipped — no image could be attached");
487
+ return "";
488
+ }
489
+ this.log(`connect: describing ${refs.length} image(s) with ${vision.provider}/${vision.model}`);
490
+ const messages = [
491
+ {
492
+ role: "user",
493
+ content: [
494
+ { type: "text", text: this.t.visionPrompt },
495
+ ...refs.map((ref) => ({ type: "image", attachment: ref })),
496
+ ],
497
+ },
498
+ ];
499
+ try {
500
+ let text = "";
501
+ const stream = llm.stream({ provider: vision.provider, model: vision.model, messages, maxTokens: 2048 });
502
+ for await (const chunk of stream) {
503
+ if (chunk.type === "text-delta" && typeof chunk.text === "string")
504
+ text += chunk.text;
505
+ }
506
+ this.log(`connect: vision description produced ${text.length} char(s)`);
507
+ return text.trim();
508
+ }
509
+ catch (error) {
510
+ this.log(`connect: vision description call failed: ${error instanceof Error ? error.message : String(error)}`);
511
+ return "";
512
+ }
513
+ }
514
+ async modelSupportsVision(provider, model) {
515
+ if (provider === undefined || model === undefined)
516
+ return false;
517
+ const llm = this.ctx.get("llm");
518
+ try {
519
+ const info = await llm?.resolveModelInfo?.(provider, model);
520
+ const supports = info?.inputModalities?.includes("image") ?? false;
521
+ this.log(`connect: main model vision check ${provider}/${model} -> ${supports} (inputModalities=${JSON.stringify(info?.inputModalities)})`);
522
+ return supports;
523
+ }
524
+ catch (error) {
525
+ this.log(`connect: main model vision check ${provider}/${model} failed: ${String(error)}`);
526
+ return false;
527
+ }
528
+ }
529
+ async findVisionModel() {
530
+ if (this.config.visionModel !== undefined) {
531
+ this.log(`connect: vision model configured: ${this.config.visionModel.provider}/${this.config.visionModel.model}`);
532
+ return this.config.visionModel;
533
+ }
534
+ const llm = this.ctx.get("llm");
535
+ if (llm?.listProviders === undefined || llm.resolveModelInfo === undefined) {
536
+ this.log("connect: vision auto-detection unavailable — llm service lacks listProviders/resolveModelInfo");
537
+ return null;
538
+ }
539
+ const providers = llm.listProviders() ?? [];
540
+ this.log(`connect: vision auto-detection scanning ${providers.length} provider(s): ${providers.map((p) => p.id).join(", ") || "(none)"}`);
541
+ for (const p of providers) {
542
+ let models = [];
543
+ try {
544
+ models = (await llm.listModels?.(p.id)) ?? [];
545
+ }
546
+ catch (error) {
547
+ this.log(`connect: listModels(${p.id}) failed: ${String(error)}`);
548
+ continue;
549
+ }
550
+ this.log(`connect: provider ${p.id} advertises ${models.length} model(s)`);
551
+ for (const m of models) {
552
+ try {
553
+ const info = await llm.resolveModelInfo(p.id, m.id);
554
+ this.log(`connect: ${p.id}/${m.id} inputModalities=${JSON.stringify(info.inputModalities)}`);
555
+ if (info.inputModalities?.includes("image")) {
556
+ this.log(`connect: vision model selected: ${p.id}/${m.id}`);
557
+ return { provider: p.id, model: m.id };
558
+ }
559
+ }
560
+ catch (error) {
561
+ this.log(`connect: resolveModelInfo(${p.id}/${m.id}) failed: ${String(error)}`);
562
+ }
563
+ }
564
+ }
565
+ this.log("connect: no image-capable model found via auto-detection");
566
+ return null;
567
+ }
568
+ log(message) {
569
+ this.ctx.get("logger")?.info?.(message);
570
+ }
310
571
  readTodos() {
311
572
  const agent = this.agent;
312
573
  if (agent === undefined)
@@ -326,13 +587,13 @@ export class AgentRunner {
326
587
  this.bindings.put({ ...binding, ownerKey: msg.senderKey, lastActiveAt: Date.now() });
327
588
  }
328
589
  async sendSummary(msg, outcome) {
329
- const lines = [`**任务结束**:${REASON_LABELS[outcome.reason]}`];
590
+ const lines = [this.t.taskEnded(this.reasonLabel(outcome.reason))];
330
591
  if (outcome.code !== undefined)
331
- lines.push(`错误码:\`${outcome.code}\``);
592
+ lines.push(this.t.errorCode(outcome.code));
332
593
  if (outcome.message !== undefined)
333
- lines.push(`原因:${truncate(outcome.message)}`);
594
+ lines.push(this.t.reasonMessage(truncate(outcome.message)));
334
595
  if (outcome.text !== "")
335
- lines.push(`已产出:${truncate(outcome.text, 300)}`);
596
+ lines.push(this.t.produced(truncate(outcome.text, 300)));
336
597
  const card = { markdown: lines.join("\n") };
337
598
  await this.adapter.sendCard(this.target(msg), card).catch(() => undefined);
338
599
  }
@@ -341,17 +602,17 @@ export class AgentRunner {
341
602
  switch (command.kind) {
342
603
  case "new": {
343
604
  await this.newChat(msg);
344
- await this.adapter.sendText(target, "已开启新会话。");
605
+ await this.adapter.sendText(target, this.t.newChatDone);
345
606
  break;
346
607
  }
347
608
  case "clear": {
348
609
  await this.clearChat(msg);
349
- await this.adapter.sendText(target, "会话已清空。");
610
+ await this.adapter.sendText(target, this.t.chatCleared);
350
611
  break;
351
612
  }
352
613
  case "stop": {
353
614
  this.agent?.cancel({ kind: "user" });
354
- await this.adapter.sendText(target, "已请求停止当前任务。");
615
+ await this.adapter.sendText(target, this.t.stopRequested);
355
616
  break;
356
617
  }
357
618
  case "status": {
@@ -369,22 +630,22 @@ export class AgentRunner {
369
630
  }
370
631
  const path = command.path;
371
632
  if (!isAbsolute(path)) {
372
- await this.adapter.sendText(target, "请输入绝对路径,例如 `/dir D:\\projects\\my-app`。");
633
+ await this.adapter.sendText(target, this.t.dirUsage);
373
634
  break;
374
635
  }
375
636
  try {
376
637
  if (!existsSync(path) || !statSync(path).isDirectory()) {
377
- await this.adapter.sendText(target, `目录不存在或不是文件夹:${path}`);
638
+ await this.adapter.sendText(target, this.t.dirNotExists(path));
378
639
  break;
379
640
  }
380
641
  }
381
642
  catch {
382
- await this.adapter.sendText(target, `无法访问目录:${path}`);
643
+ await this.adapter.sendText(target, this.t.dirUnreadable(path));
383
644
  break;
384
645
  }
385
646
  this.workDir = path;
386
647
  await this.newChat(msg);
387
- await this.adapter.sendText(target, `工作目录已切换为:\n${path}\n(已开启新会话)`);
648
+ await this.adapter.sendText(target, this.t.dirSwitched(path));
388
649
  break;
389
650
  }
390
651
  case "chat": {
@@ -405,7 +666,7 @@ export class AgentRunner {
405
666
  }
406
667
  case "workspace": {
407
668
  if (command.path === undefined) {
408
- await this.adapter.sendText(target, "用法:`/workspace <绝对路径>`,例如 `/workspace D:\\projects\\new-app`。");
669
+ await this.adapter.sendText(target, this.t.workspaceUsage);
409
670
  break;
410
671
  }
411
672
  await this.createWorkspace(command.path, target);
@@ -423,6 +684,10 @@ export class AgentRunner {
423
684
  await this.showGoals(target);
424
685
  break;
425
686
  }
687
+ case "schedule": {
688
+ await this.showSchedule(target);
689
+ break;
690
+ }
426
691
  case "model": {
427
692
  await this.openMenu(target, msg, "model", ["root"]);
428
693
  break;
@@ -432,7 +697,7 @@ export class AgentRunner {
432
697
  break;
433
698
  }
434
699
  case "help": {
435
- await this.adapter.sendText(target, HELP_TEXT);
700
+ await this.adapter.sendText(target, helpText(this.t));
436
701
  break;
437
702
  }
438
703
  default:
@@ -506,7 +771,7 @@ export class AgentRunner {
506
771
  seen.add(key);
507
772
  out.push({ path, title: title || path });
508
773
  };
509
- add(this.workDir, "当前目录");
774
+ add(this.workDir, this.t.currentDir);
510
775
  for (const w of registry?.list?.() ?? [])
511
776
  add(w.path, w.title);
512
777
  for (const p of this.config.workspaces)
@@ -516,14 +781,18 @@ export class AgentRunner {
516
781
  async openMenu(target, msg, menuId, stack = [], cardId) {
517
782
  const items = await this.menuItems(menuId);
518
783
  const options = items.map((i) => ({ id: i.id, label: i.label }));
519
- options.push({ id: "menu:exit", label: "❌ 退出" });
784
+ options.push({ id: "menu:exit", label: this.t.menuExit });
520
785
  if (stack.length > 0)
521
- options.push({ id: "menu:back", label: "🔙 返回" });
522
- const { choice, messageId } = await this.adapter.promptChoice(target, { title: MENU_TITLES[menuId], options }, cardId);
786
+ options.push({ id: "menu:back", label: this.t.menuBack });
787
+ const { choice, messageId } = await this.adapter.promptChoice(target, {
788
+ title: this.menuTitle(menuId),
789
+ options,
790
+ ...(menuId === "root" ? { sections: this.rootMenuSections(), footer: this.t.rootMenuFooter } : {}),
791
+ }, cardId);
523
792
  if (choice === undefined)
524
793
  return;
525
794
  if (choice === "menu:exit") {
526
- await this.adapter.closeMenu(messageId, "菜单已关闭。");
795
+ await this.adapter.closeMenu(messageId, this.t.menuClosed);
527
796
  return;
528
797
  }
529
798
  if (choice === "menu:back") {
@@ -544,15 +813,16 @@ export class AgentRunner {
544
813
  switch (menuId) {
545
814
  case "root":
546
815
  return [
547
- { id: "workspace", label: "📁 切换工作目录", onSelect: (t, m, cardId) => this.openMenu(t, m, "workspace", ["root"], cardId) },
548
- { id: "chat", label: "💬 切换对话", onSelect: (t, m, cardId) => this.openMenu(t, m, "chat", ["root"], cardId) },
549
- { id: "status", label: "📊 查看状态", leaf: true, onSelect: async (t) => { await this.showStatus(t); } },
550
- { id: "task", label: "📋 查看任务", leaf: true, onSelect: async (t) => { await this.showTasks(t); } },
551
- { id: "history", label: "🗒️ 历史消息", leaf: true, onSelect: async (t) => { await this.showHistory(t, 10); } },
552
- { id: "goals", label: "🎯 目标", leaf: true, onSelect: async (t) => { await this.showGoals(t); } },
553
- { id: "compact", label: "🗜️ 压缩上下文", leaf: true, onSelect: async (t) => { await this.compact(t); } },
554
- { id: "plugins", label: "🔌 查看插件", leaf: true, onSelect: async (t) => { await this.showPlugins(t); } },
555
- { id: "settings", label: "⚙️ 设置", onSelect: (t, m, cardId) => this.openMenu(t, m, "settings", ["root"], cardId) },
816
+ { id: "workspace", label: this.t.menuWorkspaceAction, onSelect: (t, m, cardId) => this.openMenu(t, m, "workspace", ["root"], cardId) },
817
+ { id: "chat", label: this.t.menuChatAction, onSelect: (t, m, cardId) => this.openMenu(t, m, "chat", ["root"], cardId) },
818
+ { id: "status", label: this.t.menuStatusAction, leaf: true, onSelect: async (t) => { await this.showStatus(t); } },
819
+ { id: "task", label: this.t.menuTaskAction, leaf: true, onSelect: async (t) => { await this.showTasks(t); } },
820
+ { id: "history", label: this.t.menuHistoryAction, leaf: true, onSelect: async (t) => { await this.showHistory(t, 10); } },
821
+ { id: "goals", label: this.t.menuGoalsAction, leaf: true, onSelect: async (t) => { await this.showGoals(t); } },
822
+ { id: "schedule", label: this.t.menuScheduleAction, leaf: true, onSelect: async (t) => { await this.showSchedule(t); } },
823
+ { id: "compact", label: this.t.menuCompactAction, leaf: true, onSelect: async (t) => { await this.compact(t); } },
824
+ { id: "plugins", label: this.t.menuPluginsAction, leaf: true, onSelect: async (t) => { await this.showPlugins(t); } },
825
+ { id: "settings", label: this.t.menuSettingsAction, onSelect: (t, m, cardId) => this.openMenu(t, m, "settings", ["root"], cardId) },
556
826
  ];
557
827
  case "workspace": {
558
828
  const workspaces = this.listWorkspaces();
@@ -580,7 +850,7 @@ export class AgentRunner {
580
850
  }));
581
851
  items.push({
582
852
  id: "action:new",
583
- label: "➕ 新建对话",
853
+ label: this.t.menuNewChat,
584
854
  leaf: true,
585
855
  onSelect: async (t, m) => {
586
856
  await this.newChat(m);
@@ -590,10 +860,10 @@ export class AgentRunner {
590
860
  }
591
861
  case "settings":
592
862
  return [
593
- { id: "model", label: "🤖 切换模型", onSelect: (t, m, cardId) => this.openMenu(t, m, "model", ["settings", "root"], cardId) },
594
- { id: "reasoning", label: "🧠 推理强度", onSelect: (t, m, cardId) => this.openMenu(t, m, "reasoning", ["settings", "root"], cardId) },
595
- { id: "notify", label: "🔔 通知设置", onSelect: (t, m, cardId) => this.openMenu(t, m, "notify", ["settings", "root"], cardId) },
596
- { id: "overview", label: "📄 配置总览", leaf: true, onSelect: async (t) => { await this.showSettings(t); } },
863
+ { id: "model", label: this.t.menuSettingsModel, onSelect: (t, m, cardId) => this.openMenu(t, m, "model", ["settings", "root"], cardId) },
864
+ { id: "reasoning", label: this.t.menuSettingsReasoning, onSelect: (t, m, cardId) => this.openMenu(t, m, "reasoning", ["settings", "root"], cardId) },
865
+ { id: "notify", label: this.t.menuSettingsNotify, onSelect: (t, m, cardId) => this.openMenu(t, m, "notify", ["settings", "root"], cardId) },
866
+ { id: "overview", label: this.t.menuSettingsOverview, leaf: true, onSelect: async (t) => { await this.showSettings(t); } },
597
867
  ];
598
868
  case "model":
599
869
  return await this.modelMenuItems();
@@ -603,7 +873,7 @@ export class AgentRunner {
603
873
  return [
604
874
  {
605
875
  id: "stream",
606
- label: `流式输出:${this.streamEnabled ? "开" : "关"}`,
876
+ label: this.t.streamLabel(this.streamEnabled),
607
877
  leaf: true,
608
878
  onSelect: async () => {
609
879
  this.streamEnabled = !this.streamEnabled;
@@ -611,7 +881,7 @@ export class AgentRunner {
611
881
  },
612
882
  {
613
883
  id: "summary",
614
- label: `结束摘要:${this.summaryEnabled ? "开" : "关"}`,
884
+ label: this.t.summaryLabel(this.summaryEnabled),
615
885
  leaf: true,
616
886
  onSelect: async () => {
617
887
  this.summaryEnabled = !this.summaryEnabled;
@@ -623,30 +893,40 @@ export class AgentRunner {
623
893
  async showStatus(target) {
624
894
  const agent = this.agent;
625
895
  if (agent === undefined) {
626
- await this.adapter.sendText(target, `状态:无活动会话\n工作目录:${this.workDir}`);
896
+ await this.adapter.sendText(target, this.t.statusNoSession(this.workDir));
627
897
  return;
628
898
  }
629
- const label = agent.status === "running" ? "🟢 运行中" : "⚪ 空闲";
899
+ const label = agent.status === "running" ? this.t.statusRunning : this.t.statusIdle;
630
900
  const model = `${agent.options.provider ?? "-"}/${agent.options.model ?? "-"}`;
631
- await this.adapter.sendText(target, [
632
- `状态:${label}`,
633
- `模型:${model}`,
634
- `工作目录:${this.workDir}`,
635
- `待处理消息:${this.queue.length}`,
636
- `会话:${agent.id}`,
637
- ].join("\n"));
901
+ const lines = [
902
+ this.t.statusField(label),
903
+ this.t.modelField(model),
904
+ this.t.workdirField(this.workDir),
905
+ this.t.queuedField(this.queue.length),
906
+ this.t.sessionField(agent.id),
907
+ ];
908
+ const tokenMeter = this.ctx.get("tokenMeter");
909
+ try {
910
+ const m = tokenMeter?.measure?.(agent.session);
911
+ if (m !== undefined)
912
+ lines.push(this.t.contextField(m.totalTokens, m.surfaceTokens));
913
+ }
914
+ catch {
915
+ // Token meter unavailable: skip.
916
+ }
917
+ await this.adapter.sendText(target, lines.join("\n"));
638
918
  }
639
919
  async showTasks(target) {
640
920
  const todos = this.readTodos();
641
921
  if (todos.length === 0) {
642
- await this.adapter.sendText(target, "当前没有任务清单。");
922
+ await this.adapter.sendText(target, this.t.noTodos);
643
923
  return;
644
924
  }
645
925
  const lines = todos.map((todo) => {
646
926
  const mark = todo.status === "completed" ? "✅" : todo.status === "in_progress" ? "🔄" : "⬜";
647
927
  return `${mark} ${todo.content}`;
648
928
  });
649
- await this.adapter.sendText(target, `当前任务(${todos.length} 项):\n${lines.join("\n")}`);
929
+ await this.adapter.sendText(target, this.t.currentTodos(todos.length, lines.join("\n")));
650
930
  }
651
931
  async showSettings(target) {
652
932
  const binding = this.bindings.get(this.channel, this.chatKey);
@@ -654,21 +934,21 @@ export class AgentRunner {
654
934
  const model = agent ? `${agent.options.provider ?? "-"}/${agent.options.model ?? "-"}` : "-";
655
935
  const sel = this.defaultSelection();
656
936
  const lines = [
657
- "⚙️ 设置:",
658
- `模型:${model}`,
659
- `推理强度:${sel.reasoningEffort ?? "默认"}`,
660
- `Agent 预设:${this.config.agentPreset ?? "默认"}`,
661
- `工作目录:${this.workDir}`,
662
- `工作区列表:${this.config.workspaces.length === 0 ? "(未配置)" : ""}`,
937
+ this.t.settingsHeader,
938
+ this.t.modelSetting(model),
939
+ this.t.reasoningSetting(sel.reasoningEffort ?? this.t.effortDefault),
940
+ this.t.agentPresetSetting(this.config.agentPreset ?? this.t.defaultLabel),
941
+ this.t.workdirSetting(this.workDir),
942
+ this.t.workspacesSetting + (this.config.workspaces.length === 0 ? this.t.notConfigured : ""),
663
943
  ];
664
944
  for (const w of this.config.workspaces)
665
945
  lines.push(` - ${w}`);
666
- lines.push(`对话数:${binding?.sessions.length ?? 0}`);
667
- lines.push(`流式输出:${this.streamEnabled ? "开" : "关"}`);
668
- lines.push(`结束摘要:${this.summaryEnabled ? "开" : "关"}`);
669
- lines.push(`发送者白名单:${this.config.allowUsers.length === 0 ? "全部放行" : `${this.config.allowUsers.length} 人`}`);
670
- lines.push(`会话白名单:${this.config.allowChats.length === 0 ? "全部放行" : `${this.config.allowChats.length} 个`}`);
671
- lines.push(`状态目录:${this.config.stateDir ?? ".dsh-connect"}`);
946
+ lines.push(this.t.chatCountField(binding?.sessions.length ?? 0));
947
+ lines.push(this.t.streamLabel(this.streamEnabled));
948
+ lines.push(this.t.summaryLabel(this.summaryEnabled));
949
+ lines.push(this.t.allowUsersField(this.config.allowUsers.length, this.config.allowUsers.length === 0));
950
+ lines.push(this.t.allowChatsField(this.config.allowChats.length, this.config.allowChats.length === 0));
951
+ lines.push(this.t.stateDirField(this.config.stateDir ?? ".dsh-connect"));
672
952
  await this.adapter.sendText(target, lines.join("\n"));
673
953
  }
674
954
  async modelMenuItems() {
@@ -683,7 +963,7 @@ export class AgentRunner {
683
963
  },
684
964
  }));
685
965
  if (items.length === 0) {
686
- items.push({ id: "model:none", label: "(未发现可用模型)", onSelect: async () => { } });
966
+ items.push({ id: "model:none", label: this.t.noModelsFound, onSelect: async () => { } });
687
967
  }
688
968
  return items;
689
969
  }
@@ -700,7 +980,7 @@ export class AgentRunner {
700
980
  // Skip providers whose catalog cannot be listed.
701
981
  }
702
982
  for (const m of models) {
703
- out.push({ provider: p.id, model: m.id, name: `${m.name || m.id}(${p.name || p.id})` });
983
+ out.push({ provider: p.id, model: m.id, name: this.t.modelName(m.name || m.id, p.name || p.id) });
704
984
  }
705
985
  }
706
986
  return out;
@@ -718,14 +998,14 @@ export class AgentRunner {
718
998
  reasoningMenuItems() {
719
999
  const current = this.defaultSelection();
720
1000
  const efforts = [
721
- { id: "low", name: "低 (low)" },
722
- { id: "medium", name: "中 (medium)" },
723
- { id: "high", name: "高 (high)" },
1001
+ { id: "low", name: this.t.effortLow },
1002
+ { id: "medium", name: this.t.effortMedium },
1003
+ { id: "high", name: this.t.effortHigh },
724
1004
  ];
725
1005
  const items = [
726
1006
  {
727
1007
  id: "effort:default",
728
- label: `${current.reasoningEffort === undefined ? "● " : ""}默认(跟随模型)`,
1008
+ label: `${current.reasoningEffort === undefined ? "● " : ""}${this.t.effortDefault}`,
729
1009
  leaf: true,
730
1010
  onSelect: async (t, m) => {
731
1011
  await this.setReasoning(undefined, m);
@@ -758,80 +1038,80 @@ export class AgentRunner {
758
1038
  const loader = this.ctx.get("loader");
759
1039
  const all = [...(loader?.entries?.() ?? [])];
760
1040
  if (all.length === 0) {
761
- await this.adapter.sendText(target, "未获取到插件清单。");
1041
+ await this.adapter.sendText(target, this.t.noPlugins);
762
1042
  return;
763
1043
  }
764
1044
  const shown = all.slice(0, 50);
765
1045
  const lines = shown.map((e) => {
766
- const status = e.disabled ? "⛔ 禁用" : "✅ 启用";
1046
+ const status = e.disabled ? this.t.pluginDisabled : this.t.pluginEnabled;
767
1047
  return `${status} ${e.id}${e.options.name ? ` (${e.options.name})` : ""}`;
768
1048
  });
769
1049
  if (all.length > shown.length)
770
- lines.push(`… 共 ${all.length} 个,仅显示前 ${shown.length} 个`);
771
- await this.adapter.sendText(target, `插件(${all.length}):\n${lines.join("\n")}`);
1050
+ lines.push(this.t.pluginsTruncated(all.length, shown.length));
1051
+ await this.adapter.sendText(target, this.t.pluginsCount(all.length) + lines.join("\n"));
772
1052
  }
773
1053
  async createWorkspace(path, target) {
774
1054
  if (!isAbsolute(path)) {
775
- await this.adapter.sendText(target, "请输入绝对路径,例如 `/workspace D:\\projects\\new-app`。");
1055
+ await this.adapter.sendText(target, this.t.workspaceUsage);
776
1056
  return;
777
1057
  }
778
1058
  try {
779
1059
  if (!existsSync(path) || !statSync(path).isDirectory()) {
780
- await this.adapter.sendText(target, `目录不存在或不是文件夹:${path}\n(请先创建该目录,再执行 /workspace)`);
1060
+ await this.adapter.sendText(target, this.t.dirNotExistsHint(path));
781
1061
  return;
782
1062
  }
783
1063
  }
784
1064
  catch {
785
- await this.adapter.sendText(target, `无法访问目录:${path}`);
1065
+ await this.adapter.sendText(target, this.t.dirUnreadable(path));
786
1066
  return;
787
1067
  }
788
1068
  const registry = this.ctx.get("workspaceRegistry");
789
1069
  if (registry?.create === undefined) {
790
- await this.adapter.sendText(target, "工作区服务不可用。");
1070
+ await this.adapter.sendText(target, this.t.workspaceServiceUnavailable);
791
1071
  return;
792
1072
  }
793
1073
  try {
794
1074
  const ws = await registry.create(path);
795
- await this.adapter.sendText(target, `工作区已创建:\n${ws.title} (${ws.path})`);
1075
+ await this.adapter.sendText(target, this.t.workspaceCreated(ws.title, ws.path));
796
1076
  }
797
1077
  catch (error) {
798
- await this.adapter.sendText(target, `创建失败:${error instanceof Error ? error.message : String(error)}`);
1078
+ await this.adapter.sendText(target, this.t.createFailed(error instanceof Error ? error.message : String(error)));
799
1079
  }
800
1080
  }
801
1081
  async compact(target) {
802
1082
  const agent = this.agent;
803
1083
  if (agent === undefined) {
804
- await this.adapter.sendText(target, "当前没有活动会话,无法压缩。");
1084
+ await this.adapter.sendText(target, this.t.noActiveSessionCompact);
805
1085
  return;
806
1086
  }
807
1087
  if (agent.status !== "idle") {
808
- await this.adapter.sendText(target, "当前会话正在运行,请稍后再试(或先 /stop)。");
1088
+ await this.adapter.sendText(target, this.t.sessionRunning);
809
1089
  return;
810
1090
  }
811
1091
  const presets = this.ctx.get("agentPresets");
812
1092
  const compaction = presets?.serviceFor?.(agent, "compaction");
813
1093
  if (compaction?.compactNow === undefined) {
814
- await this.adapter.sendText(target, "压缩服务不可用(当前预设未挂载压缩后端)。");
1094
+ await this.adapter.sendText(target, this.t.compactionUnavailable);
815
1095
  return;
816
1096
  }
817
1097
  try {
818
1098
  const result = await compaction.compactNow(agent, new AbortController().signal);
819
1099
  if (result === null) {
820
- await this.adapter.sendText(target, "没有可压缩的历史。");
1100
+ await this.adapter.sendText(target, this.t.nothingToCompact);
821
1101
  }
822
1102
  else {
823
- await this.adapter.sendText(target, "上下文已压缩。");
1103
+ await this.adapter.sendText(target, this.t.contextCompacted);
824
1104
  }
825
1105
  }
826
1106
  catch (error) {
827
1107
  const msg = error instanceof Error ? error.message : String(error);
828
- await this.adapter.sendText(target, `压缩失败:${truncate(msg)}`);
1108
+ await this.adapter.sendText(target, this.t.compactFailed(truncate(msg)));
829
1109
  }
830
1110
  }
831
1111
  async showHistory(target, limit) {
832
1112
  const agent = this.agent;
833
1113
  if (agent === undefined) {
834
- await this.adapter.sendText(target, "当前没有活动会话。");
1114
+ await this.adapter.sendText(target, this.t.noActiveSession);
835
1115
  return;
836
1116
  }
837
1117
  const events = agent.session.events;
@@ -850,51 +1130,81 @@ export class AgentRunner {
850
1130
  }
851
1131
  }
852
1132
  if (rows.length === 0) {
853
- await this.adapter.sendText(target, "会话里还没有消息。");
1133
+ await this.adapter.sendText(target, this.t.noMessagesYet);
854
1134
  return;
855
1135
  }
856
- await this.adapter.sendText(target, `最近 ${rows.length} 条消息:\n${rows.reverse().join("\n\n")}`);
1136
+ await this.adapter.sendText(target, this.t.recentMessages(rows.length, rows.reverse().join("\n\n")));
857
1137
  }
858
1138
  async showGoals(target) {
859
1139
  const agent = this.agent;
860
1140
  if (agent === undefined) {
861
- await this.adapter.sendText(target, "当前没有活动会话。");
1141
+ await this.adapter.sendText(target, this.t.noActiveSession);
862
1142
  return;
863
1143
  }
864
1144
  const goals = this.ctx.get("goals");
865
1145
  const view = goals?.get?.(agent);
866
1146
  if (view === undefined) {
867
- await this.adapter.sendText(target, "当前没有进行中的目标。");
1147
+ await this.adapter.sendText(target, this.t.noActiveGoals);
868
1148
  return;
869
1149
  }
870
- const phaseLabels = {
871
- active: "🟢 进行中",
872
- paused: "⏸️ 已暂停",
873
- blocked: "🚫 受阻",
874
- complete: "✅ 已完成",
875
- };
876
1150
  const lines = [
877
- `🎯 目标:${view.objective}`,
878
- `状态:${phaseLabels[view.phase] ?? view.phase}`,
879
- `轮次:${view.roundsStarted}/${view.maxGoalRounds}`,
880
- `自动续跑:${view.activation === "armed" ? "已武装" : "已解除"}`,
1151
+ this.t.goalObjective(view.objective),
1152
+ this.t.goalStatus(this.goalPhaseLabel(view.phase)),
1153
+ this.t.goalRounds(view.roundsStarted, view.maxGoalRounds),
1154
+ this.t.goalAutoRun(view.activation === "armed"),
881
1155
  ];
882
1156
  if (view.blockedReason !== undefined)
883
- lines.push(`受阻原因:${view.blockedReason.message}`);
1157
+ lines.push(this.t.goalBlockedReason(view.blockedReason.message));
884
1158
  await this.adapter.sendText(target, lines.join("\n"));
885
1159
  }
1160
+ goalPhaseLabel(phase) {
1161
+ switch (phase) {
1162
+ case "active":
1163
+ return this.t.goalPhaseActive;
1164
+ case "paused":
1165
+ return this.t.goalPhasePaused;
1166
+ case "blocked":
1167
+ return this.t.goalPhaseBlocked;
1168
+ case "complete":
1169
+ return this.t.goalPhaseComplete;
1170
+ default:
1171
+ return phase;
1172
+ }
1173
+ }
1174
+ async showSchedule(target) {
1175
+ const agent = this.agent;
1176
+ if (agent === undefined) {
1177
+ await this.adapter.sendText(target, this.t.noActiveSession);
1178
+ return;
1179
+ }
1180
+ const reminders = foldReminders(agent.session.events);
1181
+ if (reminders.length === 0) {
1182
+ await this.adapter.sendText(target, this.t.noReminders);
1183
+ return;
1184
+ }
1185
+ const now = Date.now();
1186
+ const locale = this.config.language === "en" ? "en-US" : "zh-CN";
1187
+ const lines = reminders.map((r) => {
1188
+ const due = Date.parse(r.scheduledAt) <= now;
1189
+ const state = due ? this.t.reminderDue : this.t.reminderClock;
1190
+ const kind = r.kind === "every" ? this.t.reminderEvery(Math.round((r.everySeconds ?? 0) / 60)) : this.t.reminderOnce;
1191
+ const when = new Date(Date.parse(r.scheduledAt)).toLocaleString(locale);
1192
+ return this.t.reminderLine(state, r.id, r.prompt, kind, when);
1193
+ });
1194
+ await this.adapter.sendText(target, this.t.remindersCount(reminders.length, lines.join("\n")));
1195
+ }
886
1196
  async showAllWorkspaces(target) {
887
1197
  const registry = this.ctx.get("workspaceRegistry");
888
1198
  const all = registry?.list?.() ?? [];
889
1199
  if (all.length === 0) {
890
- await this.adapter.sendText(target, "还没有工作区。可用 `/workspace <绝对路径>` 新建。");
1200
+ await this.adapter.sendText(target, this.t.noWorkspaces);
891
1201
  return;
892
1202
  }
893
1203
  const lines = all.map((w, i) => {
894
- const sess = w.sessionIds !== undefined ? ` · ${w.sessionIds.length} 会话` : "";
1204
+ const sess = w.sessionIds !== undefined ? this.t.workspaceSessions(w.sessionIds.length) : "";
895
1205
  return `${i + 1}. ${w.title}${w.title !== w.path ? ` (${w.path})` : ""}${sess}`;
896
1206
  });
897
- await this.adapter.sendText(target, `工作区(${all.length}):\n${lines.join("\n")}`);
1207
+ await this.adapter.sendText(target, this.t.workspacesCount(all.length, lines.join("\n")));
898
1208
  }
899
1209
  }
900
1210
  //# sourceMappingURL=runner.js.map