dsh-connect 0.1.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 ADDED
@@ -0,0 +1,705 @@
1
+ /**
2
+ * Per-chat agent driver: serializes inbound messages, creates/resumes the bound
3
+ * DSH agent (with preset composition + model selection), and bridges the live
4
+ * `session/event` stream into the adapter's streaming reply.
5
+ * @module dsh-connect/runner
6
+ */
7
+ import { randomUUID } from "node:crypto";
8
+ import { existsSync, statSync } from "node:fs";
9
+ import { isAbsolute } from "node:path";
10
+ import { SessionId } from "@deepseek-ai/dsh-session";
11
+ import { installModelSelection } from "@deepseek-ai/dsh-agent";
12
+ import { createUserMessage, ReasoningEffortId } from "@deepseek-ai/dsh-llm";
13
+ import { createAsyncQueue } from "./stream.js";
14
+ import { HELP_TEXT, parseCommand } from "./commands.js";
15
+ export function resolveConnectConfig(config) {
16
+ return {
17
+ agentPreset: config.agentPreset,
18
+ workDir: config.workDir,
19
+ workspaces: config.workspaces ?? [],
20
+ allowUsers: config.allowUsers ?? [],
21
+ allowChats: config.allowChats ?? [],
22
+ stateDir: config.stateDir,
23
+ };
24
+ }
25
+ const REASON_LABELS = {
26
+ completed: "✅ 完成",
27
+ aborted: "⏹️ 已中止",
28
+ blocked: "🚫 阻塞",
29
+ error: "❌ 出错",
30
+ "max-tokens": "⚠️ 达到输出上限",
31
+ interrupted: "⚠️ 被中断",
32
+ unknown: "❓ 未知",
33
+ };
34
+ function mapReason(kind) {
35
+ switch (kind) {
36
+ case "completed":
37
+ case "aborted":
38
+ case "blocked":
39
+ case "error":
40
+ case "max-tokens":
41
+ case "interrupted":
42
+ return kind;
43
+ default:
44
+ return "unknown";
45
+ }
46
+ }
47
+ export function summarizeTurn(events, firstSeq) {
48
+ let started = false;
49
+ let text = "";
50
+ let reason = "unknown";
51
+ let code;
52
+ let message;
53
+ for (const event of events) {
54
+ if (event.seq < firstSeq)
55
+ continue;
56
+ if (event.type === "turn/start") {
57
+ started = true;
58
+ continue;
59
+ }
60
+ if (!started)
61
+ continue;
62
+ if (event.type === "assistant/message") {
63
+ const joined = event.data.message.content
64
+ .filter((block) => block.type === "text")
65
+ .map((block) => block.text)
66
+ .join("");
67
+ if (joined !== "")
68
+ text = joined;
69
+ }
70
+ if (event.type === "turn/end") {
71
+ reason = mapReason(event.data.reason.kind);
72
+ if (event.data.reason.kind === "error") {
73
+ code = event.data.reason.error.code;
74
+ message = event.data.reason.error.message;
75
+ }
76
+ }
77
+ }
78
+ return { reason, text, ...(code === undefined ? {} : { code }), ...(message === undefined ? {} : { message }) };
79
+ }
80
+ function truncate(text, max = 500) {
81
+ return text.length <= max ? text : `${text.slice(0, max)}…`;
82
+ }
83
+ const MENU_TITLES = {
84
+ root: "主菜单",
85
+ workspace: "切换工作目录",
86
+ chat: "切换对话",
87
+ settings: "设置",
88
+ model: "切换模型",
89
+ reasoning: "推理强度",
90
+ notify: "通知设置",
91
+ };
92
+ export class AgentRunner {
93
+ ctx;
94
+ config;
95
+ channel;
96
+ chatKey;
97
+ chatType;
98
+ adapter;
99
+ bindings;
100
+ queue = [];
101
+ running = false;
102
+ agent;
103
+ handle;
104
+ turn;
105
+ workDir;
106
+ streamEnabled = true;
107
+ summaryEnabled = true;
108
+ constructor(ctx, config, channel, chatKey, chatType, adapter, bindings) {
109
+ this.ctx = ctx;
110
+ this.config = config;
111
+ this.channel = channel;
112
+ this.chatKey = chatKey;
113
+ this.chatType = chatType;
114
+ this.adapter = adapter;
115
+ this.bindings = bindings;
116
+ this.workDir = config.workDir ?? this.resolveDefaultWorkDir();
117
+ }
118
+ /** Prefer the user's first DSH workspace over the process cwd as default. */
119
+ resolveDefaultWorkDir() {
120
+ const registry = this.ctx.get("workspaceRegistry");
121
+ const first = registry?.list?.()[0];
122
+ return first?.path ?? process.cwd();
123
+ }
124
+ enqueue(msg) {
125
+ const command = parseCommand(msg.text);
126
+ if (command.kind !== "message") {
127
+ void this.handleCommand(command, msg);
128
+ return;
129
+ }
130
+ this.queue.push(msg);
131
+ void this.drain();
132
+ }
133
+ /** Route one live session event to the active turn's chunk queue. */
134
+ onSessionEvent(session, event) {
135
+ const turn = this.turn;
136
+ if (turn === undefined)
137
+ return;
138
+ if (this.agent === undefined || session.id !== this.agent.id)
139
+ return;
140
+ if (event.seq < turn.firstSeq)
141
+ return;
142
+ if (event.type === "assistant/chunk") {
143
+ const chunk = event.data.chunk;
144
+ if (chunk.type === "text-delta") {
145
+ turn.chunks.push(chunk.text);
146
+ turn.lastText += chunk.text;
147
+ }
148
+ else if (chunk.type === "reasoning-delta" && !turn.reasoning) {
149
+ turn.reasoning = true;
150
+ turn.chunks.push("🤔 深度思考中…\n");
151
+ }
152
+ }
153
+ }
154
+ target(msg) {
155
+ return {
156
+ chatKey: this.chatKey,
157
+ chatType: this.chatType,
158
+ ...(msg.replyRef === undefined ? {} : { replyRef: msg.replyRef }),
159
+ };
160
+ }
161
+ async drain() {
162
+ if (this.running)
163
+ return;
164
+ this.running = true;
165
+ try {
166
+ while (this.queue.length > 0) {
167
+ const msg = this.queue.shift();
168
+ if (msg === undefined)
169
+ break;
170
+ await this.runTurn(msg);
171
+ }
172
+ }
173
+ finally {
174
+ this.running = false;
175
+ }
176
+ }
177
+ async runTurn(msg) {
178
+ try {
179
+ const agent = await this.ensureAgent(msg);
180
+ const outcome = await this.driveAgent(agent, msg);
181
+ this.touchBinding(msg);
182
+ if (outcome.reason !== "completed" && this.summaryEnabled) {
183
+ await this.sendSummary(msg, outcome);
184
+ }
185
+ }
186
+ catch (error) {
187
+ const detail = error instanceof Error ? error.message : String(error);
188
+ await this.adapter
189
+ .sendText(this.target(msg), `⚠️ 处理失败:${truncate(detail)}`)
190
+ .catch(() => undefined);
191
+ }
192
+ }
193
+ async ensureAgent(msg) {
194
+ await this.ctx.get("loader")?.await?.();
195
+ const binding = this.bindings.get(this.channel, this.chatKey);
196
+ const live = binding === undefined ? undefined : this.agents.get(SessionId(binding.sessionId));
197
+ if (live !== undefined) {
198
+ this.agent = live;
199
+ return live;
200
+ }
201
+ const selection = this.defaultSelection();
202
+ const composed = await this.composeSetup(selection);
203
+ if (binding !== undefined) {
204
+ try {
205
+ const handle = await this.agents.resume({
206
+ resumeSessionId: SessionId(binding.sessionId),
207
+ agentOptions: { provider: selection.provider, model: selection.model },
208
+ setup: composed.setup,
209
+ });
210
+ this.handle = handle;
211
+ this.agent = handle.agent;
212
+ this.watchAgent(handle.agent);
213
+ return handle.agent;
214
+ }
215
+ catch (error) {
216
+ this.ctx.get("logger")?.warn?.(`connect: resume of ${binding.sessionId} failed, creating fresh session: ${String(error)}`);
217
+ }
218
+ }
219
+ const sessionId = SessionId(`connect-${randomUUID()}`);
220
+ const handle = await this.agents.create({
221
+ sessionId,
222
+ meta: {
223
+ cwd: this.workDir,
224
+ ...(composed.agentPreset === undefined ? {} : { agentPreset: composed.agentPreset }),
225
+ },
226
+ agentOptions: { provider: selection.provider, model: selection.model },
227
+ setup: composed.setup,
228
+ });
229
+ this.handle = handle;
230
+ this.agent = handle.agent;
231
+ this.watchAgent(handle.agent);
232
+ this.recordSession(String(sessionId), truncate(msg.text, 40), msg.senderKey);
233
+ return handle.agent;
234
+ }
235
+ // Service lookups must go through ctx.get(): property access (ctx.agents) is
236
+ // fiber-scoped and rejected from the async Feishu callback with "without inject".
237
+ get agents() {
238
+ return this.ctx.get("agents");
239
+ }
240
+ get sessions() {
241
+ return this.ctx.get("sessions");
242
+ }
243
+ defaultSelection() {
244
+ const service = this.ctx.get("agentDefaultModel");
245
+ return service?.currentSelection?.() ?? { provider: "", model: "" };
246
+ }
247
+ async composeSetup(selection) {
248
+ const presets = this.ctx.get("agentPresets");
249
+ if (presets === undefined) {
250
+ return {
251
+ setup: (agentCtx) => {
252
+ installModelSelection(agentCtx, { current: selection, assembled: undefined });
253
+ },
254
+ };
255
+ }
256
+ const resolved = await presets.resolve(this.config.agentPreset);
257
+ return {
258
+ agentPreset: resolved.id,
259
+ setup: async (agentCtx) => {
260
+ installModelSelection(agentCtx, { current: selection, assembled: undefined });
261
+ await presets.mount(agentCtx, resolved.id);
262
+ },
263
+ };
264
+ }
265
+ watchAgent(agent) {
266
+ // Scope-filtered: receives only this agent's session events.
267
+ agent.ctx.on("session/event", (session, event) => {
268
+ this.onSessionEvent(session, event);
269
+ });
270
+ }
271
+ async driveAgent(agent, msg) {
272
+ const firstSeq = agent.session.seq;
273
+ if (!this.streamEnabled) {
274
+ agent.followup(createUserMessage({
275
+ content: [{ type: "text", text: msg.text }],
276
+ source: { kind: "user" },
277
+ }));
278
+ await agent.whenIdle();
279
+ await this.sessions.flush(agent.session);
280
+ const outcome = summarizeTurn(agent.session.events, firstSeq);
281
+ if (outcome.text !== "") {
282
+ await this.adapter.sendText(this.target(msg), outcome.text).catch(() => undefined);
283
+ }
284
+ return outcome;
285
+ }
286
+ const chunks = createAsyncQueue();
287
+ this.turn = { firstSeq, chunks, lastText: "", reasoning: false };
288
+ const streamPromise = this.adapter.streamText(this.target(msg), chunks);
289
+ agent.followup(createUserMessage({
290
+ content: [{ type: "text", text: msg.text }],
291
+ source: { kind: "user" },
292
+ }));
293
+ await agent.whenIdle();
294
+ await this.sessions.flush(agent.session);
295
+ chunks.end();
296
+ await streamPromise;
297
+ const outcome = summarizeTurn(agent.session.events, firstSeq);
298
+ const text = outcome.text !== "" ? outcome.text : this.turn.lastText;
299
+ this.turn = undefined;
300
+ return { ...outcome, text };
301
+ }
302
+ readTodos() {
303
+ const agent = this.agent;
304
+ if (agent === undefined)
305
+ return [];
306
+ const events = agent.session.events;
307
+ for (let i = events.length - 1; i >= 0; i--) {
308
+ const event = events[i];
309
+ if (event.type === "todo/write")
310
+ return [...event.data.todos];
311
+ }
312
+ return [];
313
+ }
314
+ touchBinding(msg) {
315
+ const binding = this.bindings.get(this.channel, this.chatKey);
316
+ if (binding === undefined)
317
+ return;
318
+ this.bindings.put({ ...binding, ownerKey: msg.senderKey, lastActiveAt: Date.now() });
319
+ }
320
+ async sendSummary(msg, outcome) {
321
+ const lines = [`**任务结束**:${REASON_LABELS[outcome.reason]}`];
322
+ if (outcome.code !== undefined)
323
+ lines.push(`错误码:\`${outcome.code}\``);
324
+ if (outcome.message !== undefined)
325
+ lines.push(`原因:${truncate(outcome.message)}`);
326
+ if (outcome.text !== "")
327
+ lines.push(`已产出:${truncate(outcome.text, 300)}`);
328
+ const card = { markdown: lines.join("\n") };
329
+ await this.adapter.sendCard(this.target(msg), card).catch(() => undefined);
330
+ }
331
+ async handleCommand(command, msg) {
332
+ const target = this.target(msg);
333
+ switch (command.kind) {
334
+ case "new": {
335
+ await this.newChat(msg);
336
+ await this.adapter.sendText(target, "已开启新会话。");
337
+ break;
338
+ }
339
+ case "clear": {
340
+ await this.clearChat(msg);
341
+ await this.adapter.sendText(target, "会话已清空。");
342
+ break;
343
+ }
344
+ case "stop": {
345
+ this.agent?.cancel({ kind: "user" });
346
+ await this.adapter.sendText(target, "已请求停止当前任务。");
347
+ break;
348
+ }
349
+ case "status": {
350
+ await this.showStatus(target);
351
+ break;
352
+ }
353
+ case "task": {
354
+ await this.showTasks(target);
355
+ break;
356
+ }
357
+ case "dir": {
358
+ if (command.path === undefined) {
359
+ await this.openMenu(target, msg, "workspace", ["root"]);
360
+ break;
361
+ }
362
+ const path = command.path;
363
+ if (!isAbsolute(path)) {
364
+ await this.adapter.sendText(target, "请输入绝对路径,例如 `/dir D:\\projects\\my-app`。");
365
+ break;
366
+ }
367
+ try {
368
+ if (!existsSync(path) || !statSync(path).isDirectory()) {
369
+ await this.adapter.sendText(target, `目录不存在或不是文件夹:${path}`);
370
+ break;
371
+ }
372
+ }
373
+ catch {
374
+ await this.adapter.sendText(target, `无法访问目录:${path}`);
375
+ break;
376
+ }
377
+ this.workDir = path;
378
+ await this.newChat(msg);
379
+ await this.adapter.sendText(target, `工作目录已切换为:\n${path}\n(已开启新会话)`);
380
+ break;
381
+ }
382
+ case "chat": {
383
+ await this.openMenu(target, msg, "chat", ["root"]);
384
+ break;
385
+ }
386
+ case "menu": {
387
+ await this.openMenu(target, msg, "root");
388
+ break;
389
+ }
390
+ case "settings": {
391
+ await this.openMenu(target, msg, "settings", ["root"]);
392
+ break;
393
+ }
394
+ case "help": {
395
+ await this.adapter.sendText(target, HELP_TEXT);
396
+ break;
397
+ }
398
+ default:
399
+ break;
400
+ }
401
+ }
402
+ async disposeAgent() {
403
+ const agent = this.agent;
404
+ const handle = this.handle;
405
+ this.agent = undefined;
406
+ this.handle = undefined;
407
+ this.turn = undefined;
408
+ if (agent !== undefined) {
409
+ agent.cancel({ kind: "disposed" });
410
+ await agent.whenIdle().catch(() => undefined);
411
+ }
412
+ await handle?.dispose().catch(() => undefined);
413
+ }
414
+ recordSession(sessionId, title, ownerKey) {
415
+ const binding = this.bindings.get(this.channel, this.chatKey);
416
+ const others = (binding?.sessions ?? []).filter((s) => s.sessionId !== sessionId);
417
+ const sessions = [
418
+ { sessionId, title, createdAt: Date.now(), lastActiveAt: Date.now(), workDir: this.workDir },
419
+ ...others,
420
+ ].slice(0, 50);
421
+ this.bindings.put({
422
+ channel: this.channel,
423
+ chatKey: this.chatKey,
424
+ chatType: this.chatType,
425
+ sessionId,
426
+ ownerKey,
427
+ createdAt: binding?.createdAt ?? Date.now(),
428
+ lastActiveAt: Date.now(),
429
+ sessions,
430
+ });
431
+ }
432
+ async newChat(msg) {
433
+ await this.disposeAgent();
434
+ const binding = this.bindings.get(this.channel, this.chatKey);
435
+ if (binding !== undefined) {
436
+ this.bindings.put({ ...binding, sessionId: "", ownerKey: msg.senderKey, lastActiveAt: Date.now() });
437
+ }
438
+ }
439
+ async clearChat(msg) {
440
+ const binding = this.bindings.get(this.channel, this.chatKey);
441
+ const current = binding?.sessionId;
442
+ await this.disposeAgent();
443
+ if (binding !== undefined && current !== undefined && current !== "") {
444
+ const sessions = binding.sessions.filter((s) => s.sessionId !== current);
445
+ this.bindings.put({ ...binding, sessionId: "", sessions, ownerKey: msg.senderKey, lastActiveAt: Date.now() });
446
+ }
447
+ }
448
+ async switchTo(sessionId, ownerKey) {
449
+ const binding = this.bindings.get(this.channel, this.chatKey);
450
+ if (binding === undefined)
451
+ return;
452
+ const sessions = binding.sessions.map((s) => (s.sessionId === sessionId ? { ...s, lastActiveAt: Date.now() } : s));
453
+ this.bindings.put({ ...binding, sessionId, sessions, ownerKey, lastActiveAt: Date.now() });
454
+ await this.disposeAgent();
455
+ }
456
+ listWorkspaces() {
457
+ const registry = this.ctx.get("workspaceRegistry");
458
+ const out = [];
459
+ const seen = new Set();
460
+ const add = (path, title) => {
461
+ if (path === "")
462
+ return;
463
+ const key = path.replace(/[\\/]+$/, "").toLowerCase();
464
+ if (seen.has(key))
465
+ return;
466
+ seen.add(key);
467
+ out.push({ path, title: title || path });
468
+ };
469
+ add(this.workDir, "当前目录");
470
+ for (const w of registry?.list?.() ?? [])
471
+ add(w.path, w.title);
472
+ for (const p of this.config.workspaces)
473
+ add(p, p);
474
+ return out;
475
+ }
476
+ async openMenu(target, msg, menuId, stack = []) {
477
+ const items = await this.menuItems(menuId);
478
+ const options = items.map((i) => ({ id: i.id, label: i.label }));
479
+ if (stack.length > 0)
480
+ options.push({ id: "menu:back", label: "🔙 返回" });
481
+ const choice = await this.adapter.promptChoice(target, { title: MENU_TITLES[menuId], options });
482
+ if (choice === undefined)
483
+ return;
484
+ if (choice === "menu:back") {
485
+ const parent = stack[stack.length - 1];
486
+ await this.openMenu(target, msg, parent, stack.slice(0, -1));
487
+ return;
488
+ }
489
+ const item = items.find((i) => i.id === choice);
490
+ if (item === undefined)
491
+ return;
492
+ await item.onSelect(target, msg);
493
+ }
494
+ async menuItems(menuId) {
495
+ switch (menuId) {
496
+ case "root":
497
+ return [
498
+ { id: "workspace", label: "📁 切换工作目录", onSelect: (t, m) => this.openMenu(t, m, "workspace", ["root"]) },
499
+ { id: "chat", label: "💬 切换对话", onSelect: (t, m) => this.openMenu(t, m, "chat", ["root"]) },
500
+ { id: "status", label: "📊 查看状态", onSelect: (t) => this.showStatus(t) },
501
+ { id: "task", label: "📋 查看任务", onSelect: (t) => this.showTasks(t) },
502
+ { id: "settings", label: "⚙️ 设置", onSelect: (t, m) => this.openMenu(t, m, "settings", ["root"]) },
503
+ ];
504
+ case "workspace": {
505
+ const workspaces = this.listWorkspaces();
506
+ return workspaces.map((w) => ({
507
+ id: `dir:${w.path}`,
508
+ label: `${w.path === this.workDir ? "● " : ""}${w.title}${w.title !== w.path ? ` (${w.path})` : ""}`,
509
+ onSelect: async (t, m) => {
510
+ this.workDir = w.path;
511
+ await this.newChat(m);
512
+ await this.adapter.sendText(t, `工作目录已切换为:\n${w.path}`);
513
+ },
514
+ }));
515
+ }
516
+ case "chat": {
517
+ const binding = this.bindings.get(this.channel, this.chatKey);
518
+ const sessions = binding?.sessions ?? [];
519
+ const active = binding?.sessionId;
520
+ const items = sessions.map((s) => ({
521
+ id: `session:${s.sessionId}`,
522
+ label: `${s.sessionId === active ? "●" : "○"} ${s.title || s.sessionId}`,
523
+ onSelect: async (t, m) => {
524
+ await this.switchTo(s.sessionId, m.senderKey);
525
+ await this.adapter.sendText(t, `已切换到对话:${s.title || s.sessionId}`);
526
+ },
527
+ }));
528
+ items.push({
529
+ id: "action:new",
530
+ label: "➕ 新建对话",
531
+ onSelect: async (t, m) => {
532
+ await this.newChat(m);
533
+ await this.adapter.sendText(t, "已开启新对话。");
534
+ },
535
+ });
536
+ return items;
537
+ }
538
+ case "settings":
539
+ return [
540
+ { id: "model", label: "🤖 切换模型", onSelect: (t, m) => this.openMenu(t, m, "model", ["settings", "root"]) },
541
+ { id: "reasoning", label: "🧠 推理强度", onSelect: (t, m) => this.openMenu(t, m, "reasoning", ["settings", "root"]) },
542
+ { id: "notify", label: "🔔 通知设置", onSelect: (t, m) => this.openMenu(t, m, "notify", ["settings", "root"]) },
543
+ { id: "overview", label: "📄 配置总览", onSelect: (t) => this.showSettings(t) },
544
+ ];
545
+ case "model":
546
+ return await this.modelMenuItems();
547
+ case "reasoning":
548
+ return this.reasoningMenuItems();
549
+ case "notify":
550
+ return [
551
+ {
552
+ id: "stream",
553
+ label: `流式输出:${this.streamEnabled ? "开" : "关"}`,
554
+ onSelect: async (t) => {
555
+ this.streamEnabled = !this.streamEnabled;
556
+ await this.adapter.sendText(t, `流式输出已${this.streamEnabled ? "开启" : "关闭"}。`);
557
+ },
558
+ },
559
+ {
560
+ id: "summary",
561
+ label: `结束摘要:${this.summaryEnabled ? "开" : "关"}`,
562
+ onSelect: async (t) => {
563
+ this.summaryEnabled = !this.summaryEnabled;
564
+ await this.adapter.sendText(t, `结束摘要已${this.summaryEnabled ? "开启" : "关闭"}。`);
565
+ },
566
+ },
567
+ ];
568
+ }
569
+ }
570
+ async showStatus(target) {
571
+ const agent = this.agent;
572
+ if (agent === undefined) {
573
+ await this.adapter.sendText(target, `状态:无活动会话\n工作目录:${this.workDir}`);
574
+ return;
575
+ }
576
+ const label = agent.status === "running" ? "🟢 运行中" : "⚪ 空闲";
577
+ const model = `${agent.options.provider ?? "-"}/${agent.options.model ?? "-"}`;
578
+ await this.adapter.sendText(target, [
579
+ `状态:${label}`,
580
+ `模型:${model}`,
581
+ `工作目录:${this.workDir}`,
582
+ `待处理消息:${this.queue.length}`,
583
+ `会话:${agent.id}`,
584
+ ].join("\n"));
585
+ }
586
+ async showTasks(target) {
587
+ const todos = this.readTodos();
588
+ if (todos.length === 0) {
589
+ await this.adapter.sendText(target, "当前没有任务清单。");
590
+ return;
591
+ }
592
+ const lines = todos.map((todo) => {
593
+ const mark = todo.status === "completed" ? "✅" : todo.status === "in_progress" ? "🔄" : "⬜";
594
+ return `${mark} ${todo.content}`;
595
+ });
596
+ await this.adapter.sendText(target, `当前任务(${todos.length} 项):\n${lines.join("\n")}`);
597
+ }
598
+ async showSettings(target) {
599
+ const binding = this.bindings.get(this.channel, this.chatKey);
600
+ const agent = this.agent;
601
+ const model = agent ? `${agent.options.provider ?? "-"}/${agent.options.model ?? "-"}` : "-";
602
+ const sel = this.defaultSelection();
603
+ const lines = [
604
+ "⚙️ 设置:",
605
+ `模型:${model}`,
606
+ `推理强度:${sel.reasoningEffort ?? "默认"}`,
607
+ `Agent 预设:${this.config.agentPreset ?? "默认"}`,
608
+ `工作目录:${this.workDir}`,
609
+ `工作区列表:${this.config.workspaces.length === 0 ? "(未配置)" : ""}`,
610
+ ];
611
+ for (const w of this.config.workspaces)
612
+ lines.push(` - ${w}`);
613
+ lines.push(`对话数:${binding?.sessions.length ?? 0}`);
614
+ lines.push(`流式输出:${this.streamEnabled ? "开" : "关"}`);
615
+ lines.push(`结束摘要:${this.summaryEnabled ? "开" : "关"}`);
616
+ lines.push(`发送者白名单:${this.config.allowUsers.length === 0 ? "全部放行" : `${this.config.allowUsers.length} 人`}`);
617
+ lines.push(`会话白名单:${this.config.allowChats.length === 0 ? "全部放行" : `${this.config.allowChats.length} 个`}`);
618
+ lines.push(`状态目录:${this.config.stateDir ?? ".dsh-connect"}`);
619
+ await this.adapter.sendText(target, lines.join("\n"));
620
+ }
621
+ async modelMenuItems() {
622
+ const choices = await this.listModelChoices();
623
+ const current = this.defaultSelection();
624
+ const items = choices.map((c) => ({
625
+ id: `model:${c.provider}:${c.model}`,
626
+ label: `${c.provider === current.provider && c.model === current.model ? "● " : ""}${c.name}`,
627
+ onSelect: async (t, m) => {
628
+ await this.setModel(c.provider, c.model, m);
629
+ await this.adapter.sendText(t, `模型已切换为:${c.name}`);
630
+ },
631
+ }));
632
+ if (items.length === 0) {
633
+ items.push({ id: "model:none", label: "(未发现可用模型)", onSelect: async () => { } });
634
+ }
635
+ return items;
636
+ }
637
+ async listModelChoices() {
638
+ const llm = this.ctx.get("llm");
639
+ const providers = llm?.listProviders?.() ?? [];
640
+ const out = [];
641
+ for (const p of providers) {
642
+ let models = [];
643
+ try {
644
+ models = (await llm?.listModels?.(p.id)) ?? [];
645
+ }
646
+ catch {
647
+ // Skip providers whose catalog cannot be listed.
648
+ }
649
+ for (const m of models) {
650
+ out.push({ provider: p.id, model: m.id, name: `${m.name || m.id}(${p.name || p.id})` });
651
+ }
652
+ }
653
+ return out;
654
+ }
655
+ async setModel(provider, model, msg) {
656
+ const svc = this.ctx.get("agentDefaultModel");
657
+ const cur = this.defaultSelection();
658
+ await svc?.saveSelection?.({
659
+ provider,
660
+ model,
661
+ ...(cur.reasoningEffort === undefined ? {} : { reasoningEffort: cur.reasoningEffort }),
662
+ });
663
+ await this.newChat(msg);
664
+ }
665
+ reasoningMenuItems() {
666
+ const current = this.defaultSelection();
667
+ const efforts = [
668
+ { id: "low", name: "低 (low)" },
669
+ { id: "medium", name: "中 (medium)" },
670
+ { id: "high", name: "高 (high)" },
671
+ ];
672
+ const items = [
673
+ {
674
+ id: "effort:default",
675
+ label: `${current.reasoningEffort === undefined ? "● " : ""}默认(跟随模型)`,
676
+ onSelect: async (t, m) => {
677
+ await this.setReasoning(undefined, m);
678
+ await this.adapter.sendText(t, "推理强度已设为默认。");
679
+ },
680
+ },
681
+ ];
682
+ for (const e of efforts) {
683
+ items.push({
684
+ id: `effort:${e.id}`,
685
+ label: `${e.id === current.reasoningEffort ? "● " : ""}${e.name}`,
686
+ onSelect: async (t, m) => {
687
+ await this.setReasoning(e.id, m);
688
+ await this.adapter.sendText(t, `推理强度已切换为:${e.name}`);
689
+ },
690
+ });
691
+ }
692
+ return items;
693
+ }
694
+ async setReasoning(effort, msg) {
695
+ const svc = this.ctx.get("agentDefaultModel");
696
+ const cur = this.defaultSelection();
697
+ await svc?.saveSelection?.({
698
+ provider: cur.provider,
699
+ model: cur.model,
700
+ ...(effort === undefined ? {} : { reasoningEffort: ReasoningEffortId(effort) }),
701
+ });
702
+ await this.newChat(msg);
703
+ }
704
+ }
705
+ //# sourceMappingURL=runner.js.map