@xmanrui/dsh-im 0.11.0 → 0.13.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/README.en.md +71 -37
- package/README.md +73 -39
- package/lib/client.js +2 -2
- package/lib/index.js +134 -124
- package/package.json +31 -2
- package/plugin-src/client/i18n.js +1 -1
- package/plugin-src/client/index.js +1 -1
- package/plugin-src/host/channels/dingtalk/production.mjs +7 -0
- package/plugin-src/host/channels/feishu/production.mjs +7 -0
- package/plugin-src/host/channels/qq/production.mjs +7 -0
- package/plugin-src/host/channels/shared/production.mjs +7 -0
- package/plugin-src/host/channels/slack/production.mjs +7 -0
- package/plugin-src/host/channels/wecom/production.mjs +7 -0
- package/plugin-src/host/channels/weixin/production.mjs +7 -0
- package/plugin-src/host/channels/whatsapp/production.mjs +7 -0
- package/plugin-src/host/harness-session-coordinator.mjs +134 -0
- package/scripts/verify-package.mjs +34 -0
- package/src/channels/dingtalk/dingtalk-bridge.mjs +82 -0
- package/src/channels/discord/discord-api.mjs +1 -1
- package/src/channels/feishu/bridge.mjs +82 -0
- package/src/channels/qq/qq-bridge.mjs +85 -0
- package/src/channels/shared/bot-workspace-store.mjs +45 -1
- package/src/channels/shared/control-command.mjs +84 -0
- package/src/channels/shared/harness-approval.mjs +4 -0
- package/src/channels/shared/harness-client.mjs +336 -29
- package/src/channels/shared/model-command.mjs +352 -0
- package/src/channels/shared/session-binding-lock.mjs +40 -0
- package/src/channels/shared/text-harness-bridge.mjs +82 -0
- package/src/channels/shared/workspace-session.mjs +21 -9
- package/src/channels/wecom/wecom-bridge.mjs +76 -0
- package/src/channels/weixin/weixin-api.mjs +1 -1
- package/src/channels/weixin/weixin-bridge.mjs +82 -0
|
@@ -18,7 +18,15 @@ import {
|
|
|
18
18
|
} from '../shared/harness-question.mjs';
|
|
19
19
|
import { HarnessApprovalQueue } from '../shared/harness-approval.mjs';
|
|
20
20
|
import { runCompactCommand } from '../shared/compact-command.mjs';
|
|
21
|
+
import {
|
|
22
|
+
isControlCommand,
|
|
23
|
+
runControlCommand,
|
|
24
|
+
} from '../shared/control-command.mjs';
|
|
21
25
|
import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
|
|
26
|
+
import {
|
|
27
|
+
isModelCommand,
|
|
28
|
+
runModelCommand,
|
|
29
|
+
} from '../shared/model-command.mjs';
|
|
22
30
|
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
23
31
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
24
32
|
|
|
@@ -35,6 +43,11 @@ const HELP_TEXT = [
|
|
|
35
43
|
'/workspacelist 列出工作区绝对路径',
|
|
36
44
|
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
|
37
45
|
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
|
46
|
+
'/models 按序号列出所有可用模型',
|
|
47
|
+
'/model [序号或完整模型ID] 查看或切换当前会话模型',
|
|
48
|
+
'示例:先发 /models,再发 /model 2',
|
|
49
|
+
'/stop 停止当前任务',
|
|
50
|
+
'/steer 补充指令 纠偏当前任务',
|
|
38
51
|
'/status 检查连接状态',
|
|
39
52
|
'/help 显示本帮助',
|
|
40
53
|
].join('\n');
|
|
@@ -77,6 +90,7 @@ export class FeishuHarnessBridge {
|
|
|
77
90
|
#resolvedQuestionReplies = new Map();
|
|
78
91
|
#acceptedMessageIds = new Set();
|
|
79
92
|
#interactionTasks = new Set();
|
|
93
|
+
#commandTasks = new Set();
|
|
80
94
|
#approvals;
|
|
81
95
|
#status;
|
|
82
96
|
#allowedSenderOpenIds;
|
|
@@ -141,6 +155,37 @@ export class FeishuHarnessBridge {
|
|
|
141
155
|
|
|
142
156
|
this.#acceptedMessageIds.add(messageId);
|
|
143
157
|
const processingReaction = this.#addReaction(messageId, 'OnIt');
|
|
158
|
+
const commandMessage = extractInboundMessage(event, this.#client);
|
|
159
|
+
const commandText = nonEmptyString(commandMessage.content) ?? '';
|
|
160
|
+
const commandRunner = isControlCommand(commandText)
|
|
161
|
+
? runControlCommand
|
|
162
|
+
: (isModelCommand(commandText) ? runModelCommand : null);
|
|
163
|
+
const addressed = event?.message?.chat_type === 'p2p'
|
|
164
|
+
|| (Array.isArray(event?.message?.mentions) && event.message.mentions.length > 0);
|
|
165
|
+
if (commandRunner && addressed) {
|
|
166
|
+
const processing = this.#processFastCommand(
|
|
167
|
+
event,
|
|
168
|
+
messageId,
|
|
169
|
+
key,
|
|
170
|
+
commandMessage,
|
|
171
|
+
commandRunner,
|
|
172
|
+
);
|
|
173
|
+
let current;
|
|
174
|
+
current = processing
|
|
175
|
+
.then(() => this.#finishReaction(messageId, processingReaction, 'DONE'))
|
|
176
|
+
.catch((error) => this.#handleMessageFailure(
|
|
177
|
+
event,
|
|
178
|
+
messageId,
|
|
179
|
+
processingReaction,
|
|
180
|
+
error,
|
|
181
|
+
))
|
|
182
|
+
.finally(() => {
|
|
183
|
+
this.#acceptedMessageIds.delete(messageId);
|
|
184
|
+
this.#commandTasks.delete(current);
|
|
185
|
+
});
|
|
186
|
+
this.#commandTasks.add(current);
|
|
187
|
+
return current;
|
|
188
|
+
}
|
|
144
189
|
if (this.#isResolvedQuestionReply(event, key)) {
|
|
145
190
|
const current = Promise.resolve()
|
|
146
191
|
.then(() => this.#discardResolvedInteractionReply(event, messageId))
|
|
@@ -276,6 +321,10 @@ export class FeishuHarnessBridge {
|
|
|
276
321
|
}
|
|
277
322
|
|
|
278
323
|
async #handleMessageFailure(event, messageId, processingReaction, error) {
|
|
324
|
+
if (error?.code === 'turn-stopped') {
|
|
325
|
+
await this.#removeProcessingReaction(messageId, processingReaction);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
279
328
|
if (this.#signal?.aborted) {
|
|
280
329
|
await this.#removeProcessingReaction(messageId, processingReaction);
|
|
281
330
|
return;
|
|
@@ -297,9 +346,41 @@ export class FeishuHarnessBridge {
|
|
|
297
346
|
pending.queue ? [pending.queue] : []
|
|
298
347
|
)),
|
|
299
348
|
...this.#interactionTasks,
|
|
349
|
+
...this.#commandTasks,
|
|
300
350
|
]);
|
|
301
351
|
}
|
|
302
352
|
|
|
353
|
+
async #processFastCommand(event, messageId, key, message, runner) {
|
|
354
|
+
this.#signal?.throwIfAborted();
|
|
355
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
356
|
+
await this.#state.markSeen(messageId);
|
|
357
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
358
|
+
this.#status.messagesReceived += 1;
|
|
359
|
+
const result = await runner(
|
|
360
|
+
nonEmptyString(message.content) ?? '',
|
|
361
|
+
this.#harness,
|
|
362
|
+
this.#state,
|
|
363
|
+
key,
|
|
364
|
+
{
|
|
365
|
+
signal: this.#signal,
|
|
366
|
+
hasImages: hasInboundImages(message),
|
|
367
|
+
pendingInteraction: this.#pendingInteractions.has(key)
|
|
368
|
+
|| this.#approvals.hasPending(key),
|
|
369
|
+
control: { owner: this, key },
|
|
370
|
+
},
|
|
371
|
+
);
|
|
372
|
+
if (result?.stopped) {
|
|
373
|
+
await Promise.allSettled([
|
|
374
|
+
this.#cancelPendingInteraction(key),
|
|
375
|
+
this.#approvals.closeRoute(key),
|
|
376
|
+
]);
|
|
377
|
+
}
|
|
378
|
+
for (const reply of result?.messages ?? [result?.message]) {
|
|
379
|
+
if (reply) await this.#send(event.message.chat_id, reply);
|
|
380
|
+
}
|
|
381
|
+
this.#status.lastError = null;
|
|
382
|
+
}
|
|
383
|
+
|
|
303
384
|
async #handle(event, key, { alreadyRecorded = false } = {}) {
|
|
304
385
|
this.#signal?.throwIfAborted();
|
|
305
386
|
const messageId = event.message.message_id;
|
|
@@ -372,6 +453,7 @@ export class FeishuHarnessBridge {
|
|
|
372
453
|
return {
|
|
373
454
|
timeoutMs: this.#replyTimeoutMs,
|
|
374
455
|
signal: this.#signal,
|
|
456
|
+
control: { owner: this, key },
|
|
375
457
|
onInteraction: (interaction) => this.#handleInteraction(interaction, {
|
|
376
458
|
key,
|
|
377
459
|
actor: senderOpenId(event),
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
2
2
|
import { runCompactCommand } from '../shared/compact-command.mjs';
|
|
3
|
+
import {
|
|
4
|
+
isControlCommand,
|
|
5
|
+
runControlCommand,
|
|
6
|
+
} from '../shared/control-command.mjs';
|
|
3
7
|
import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
|
|
4
8
|
import {
|
|
5
9
|
harnessAnswerForQuestion,
|
|
@@ -7,6 +11,10 @@ import {
|
|
|
7
11
|
validHarnessQuestion,
|
|
8
12
|
} from '../shared/harness-question.mjs';
|
|
9
13
|
import { HarnessApprovalQueue } from '../shared/harness-approval.mjs';
|
|
14
|
+
import {
|
|
15
|
+
isModelCommand,
|
|
16
|
+
runModelCommand,
|
|
17
|
+
} from '../shared/model-command.mjs';
|
|
10
18
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
11
19
|
import {
|
|
12
20
|
fetchImageBuffer,
|
|
@@ -38,6 +46,11 @@ const HELP_TEXT = [
|
|
|
38
46
|
'/workspacelist 列出工作区绝对路径',
|
|
39
47
|
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
|
40
48
|
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
|
49
|
+
'/models 按序号列出所有可用模型',
|
|
50
|
+
'/model [序号或完整模型ID] 查看或切换当前会话模型',
|
|
51
|
+
'示例:先发 /models,再发 /model 2',
|
|
52
|
+
'/stop 停止当前任务',
|
|
53
|
+
'/steer 补充指令 纠偏当前任务',
|
|
41
54
|
'/status 检查连接状态',
|
|
42
55
|
'/help 显示本帮助',
|
|
43
56
|
].join('\n');
|
|
@@ -134,6 +147,7 @@ export class QqHarnessBridge {
|
|
|
134
147
|
#interactionKeys = new Map();
|
|
135
148
|
#acceptedMessageIds = new Set();
|
|
136
149
|
#approvalTasks = new Set();
|
|
150
|
+
#commandTasks = new Set();
|
|
137
151
|
#approvals;
|
|
138
152
|
|
|
139
153
|
constructor({
|
|
@@ -184,6 +198,34 @@ export class QqHarnessBridge {
|
|
|
184
198
|
rememberConnectionTestTarget(this.#state, message.replyTarget);
|
|
185
199
|
}
|
|
186
200
|
const pending = this.#pendingInteractions.get(key);
|
|
201
|
+
const commandText = safeText(message);
|
|
202
|
+
const commandRunner = isControlCommand(commandText)
|
|
203
|
+
? runControlCommand
|
|
204
|
+
: (isModelCommand(commandText) ? runModelCommand : null);
|
|
205
|
+
const allowed = this.#ownerUserOpenid === '*' || sender === this.#ownerUserOpenid;
|
|
206
|
+
const addressed = message.kind !== 'group'
|
|
207
|
+
|| message.rawEventType === 'GROUP_AT_MESSAGE_CREATE';
|
|
208
|
+
if (commandRunner && allowed && addressed) {
|
|
209
|
+
let task;
|
|
210
|
+
task = this.#processFastCommand(
|
|
211
|
+
message,
|
|
212
|
+
messageId,
|
|
213
|
+
key,
|
|
214
|
+
commandText,
|
|
215
|
+
commandRunner,
|
|
216
|
+
).catch((error) => {
|
|
217
|
+
if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
|
|
218
|
+
this.#status.lastError = error?.message ?? String(error);
|
|
219
|
+
this.#logger.error?.('[dsh-im:qq] failed to process a command:', error);
|
|
220
|
+
return this.#bot.sendText(message.replyTarget, '消息处理失败,请稍后重试。')
|
|
221
|
+
.catch(() => undefined);
|
|
222
|
+
}).finally(() => {
|
|
223
|
+
this.#acceptedMessageIds.delete(messageId);
|
|
224
|
+
this.#commandTasks.delete(task);
|
|
225
|
+
});
|
|
226
|
+
this.#commandTasks.add(task);
|
|
227
|
+
return task;
|
|
228
|
+
}
|
|
187
229
|
const approval = this.#approvals.claimReply({
|
|
188
230
|
key,
|
|
189
231
|
actor: sender,
|
|
@@ -262,9 +304,35 @@ export class QqHarnessBridge {
|
|
|
262
304
|
pending.queue ? [pending.queue] : []
|
|
263
305
|
)),
|
|
264
306
|
...this.#approvalTasks,
|
|
307
|
+
...this.#commandTasks,
|
|
265
308
|
]);
|
|
266
309
|
}
|
|
267
310
|
|
|
311
|
+
async #processFastCommand(message, messageId, key, text, runner) {
|
|
312
|
+
this.#signal?.throwIfAborted();
|
|
313
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
314
|
+
await this.#state.markSeen(messageId);
|
|
315
|
+
this.#status.messagesReceived += 1;
|
|
316
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
317
|
+
const result = await runner(text, this.#harness, this.#state, key, {
|
|
318
|
+
signal: this.#signal,
|
|
319
|
+
hasImages: hasQqImageAttachments(message),
|
|
320
|
+
pendingInteraction: this.#pendingInteractions.has(key)
|
|
321
|
+
|| this.#approvals.hasPending(key),
|
|
322
|
+
control: { owner: this, key },
|
|
323
|
+
});
|
|
324
|
+
if (result?.stopped) {
|
|
325
|
+
await Promise.allSettled([
|
|
326
|
+
this.#cancelPendingInteraction(key),
|
|
327
|
+
this.#approvals.closeRoute(key),
|
|
328
|
+
]);
|
|
329
|
+
}
|
|
330
|
+
for (const reply of result?.messages ?? [result?.message]) {
|
|
331
|
+
if (reply) await this.#bot.sendText(message.replyTarget, reply);
|
|
332
|
+
}
|
|
333
|
+
this.#status.lastError = null;
|
|
334
|
+
}
|
|
335
|
+
|
|
268
336
|
async #process(message, key, { alreadyRecorded = false } = {}) {
|
|
269
337
|
if (this.#signal?.aborted) return;
|
|
270
338
|
const messageId = nonEmptyString(message?.messageId);
|
|
@@ -360,6 +428,7 @@ export class QqHarnessBridge {
|
|
|
360
428
|
askOptions: {
|
|
361
429
|
timeoutMs: this.#replyTimeoutMs,
|
|
362
430
|
signal: this.#signal,
|
|
431
|
+
control: { owner: this, key },
|
|
363
432
|
onUpdate: stream ? async (update) => {
|
|
364
433
|
const progress = update.type === 'text'
|
|
365
434
|
? update.text
|
|
@@ -399,6 +468,22 @@ export class QqHarnessBridge {
|
|
|
399
468
|
this.#status.lastReplyAt = new Date().toISOString();
|
|
400
469
|
this.#status.lastError = null;
|
|
401
470
|
} catch (error) {
|
|
471
|
+
if (error?.code === 'turn-stopped') {
|
|
472
|
+
if (stream) {
|
|
473
|
+
try {
|
|
474
|
+
await stream.cancel?.();
|
|
475
|
+
} catch (streamError) {
|
|
476
|
+
this.#logger.warn?.('[dsh-im:qq] unable to cancel a stopped QQ stream:', streamError);
|
|
477
|
+
}
|
|
478
|
+
try {
|
|
479
|
+
await this.#bot.sendText(target, '已停止。');
|
|
480
|
+
} catch (sendError) {
|
|
481
|
+
this.#logger.warn?.('[dsh-im:qq] unable to announce a stopped QQ turn:', sendError);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
await this.#state.markSeen(messageId);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
402
487
|
stream?.cancel?.();
|
|
403
488
|
if (this.#signal?.aborted) return;
|
|
404
489
|
this.#status.lastError = error?.message ?? String(error);
|
|
@@ -511,7 +511,9 @@ export function createBotWorkspaceScope(harness, { botId, workspaces, state }) {
|
|
|
511
511
|
}
|
|
512
512
|
};
|
|
513
513
|
}
|
|
514
|
-
if ((property === 'listWorkspaces'
|
|
514
|
+
if ((property === 'listWorkspaces'
|
|
515
|
+
|| property === 'listWorkspaceSessions'
|
|
516
|
+
|| property === 'listModels')
|
|
515
517
|
&& typeof target[property] === 'function') {
|
|
516
518
|
return async (...args) => {
|
|
517
519
|
if (!isCurrentScope()) {
|
|
@@ -627,6 +629,30 @@ export function createBotWorkspaceScope(harness, { botId, workspaces, state }) {
|
|
|
627
629
|
sessionGenerations.delete(sessionId);
|
|
628
630
|
const isCurrentSession = () => isCurrentScope()
|
|
629
631
|
&& generation === workspaces.generationFor(botId);
|
|
632
|
+
const invokeCurrentSession = async (method, args, action) => {
|
|
633
|
+
if (!isCurrentSession()) {
|
|
634
|
+
throw workspaceSessionStale(
|
|
635
|
+
`The bot workspace changed before this ${action} started.`,
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
const result = await target[method](sessionId, ...args);
|
|
639
|
+
if (!isCurrentSession()) {
|
|
640
|
+
throw workspaceSessionStale(
|
|
641
|
+
`The bot workspace changed while this ${action} was running.`,
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
return result;
|
|
645
|
+
};
|
|
646
|
+
const invokeStartedSessionMutation = async (method, args, action) => {
|
|
647
|
+
if (!isCurrentSession()) {
|
|
648
|
+
throw workspaceSessionStale(
|
|
649
|
+
`The bot workspace changed before this ${action} started.`,
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
// Once an irreversible control mutation has started, preserve its
|
|
653
|
+
// actual outcome even if a workspace switch commits concurrently.
|
|
654
|
+
return target[method](sessionId, ...args);
|
|
655
|
+
};
|
|
630
656
|
return Object.freeze({
|
|
631
657
|
sessionId,
|
|
632
658
|
async sessionExists(...args) {
|
|
@@ -634,6 +660,24 @@ export function createBotWorkspaceScope(harness, { botId, workspaces, state }) {
|
|
|
634
660
|
const exists = await target.sessionExists(sessionId, ...args);
|
|
635
661
|
return isCurrentSession() && exists;
|
|
636
662
|
},
|
|
663
|
+
models(...args) {
|
|
664
|
+
return invokeCurrentSession('getSessionModels', args, 'model listing');
|
|
665
|
+
},
|
|
666
|
+
selectModel(...args) {
|
|
667
|
+
return invokeCurrentSession('selectSessionModel', args, 'model selection');
|
|
668
|
+
},
|
|
669
|
+
isRunning(...args) {
|
|
670
|
+
return invokeCurrentSession('isSessionRunning', args, 'run-state check');
|
|
671
|
+
},
|
|
672
|
+
hasActiveTurn(...args) {
|
|
673
|
+
return invokeCurrentSession('hasActiveTurn', args, 'turn ownership check');
|
|
674
|
+
},
|
|
675
|
+
stopActiveTurn(...args) {
|
|
676
|
+
return invokeStartedSessionMutation('stopActiveTurn', args, 'turn stop');
|
|
677
|
+
},
|
|
678
|
+
steerActiveTurn(...args) {
|
|
679
|
+
return invokeStartedSessionMutation('steerActiveTurn', args, 'turn steering');
|
|
680
|
+
},
|
|
637
681
|
ask(...args) {
|
|
638
682
|
if (!isCurrentSession()) {
|
|
639
683
|
throw workspaceSessionStale(
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const CONTROL_COMMAND = /^\/(?:stop|steer)(?=$|\s)/iu;
|
|
2
|
+
const STOP_COMMAND = /^\/stop(?=$|\s)/iu;
|
|
3
|
+
const STOP_USAGE = '用法:/stop(不带参数)';
|
|
4
|
+
const STEER_USAGE = '用法:/steer <补充指令>';
|
|
5
|
+
const TEXT_ONLY = '控制命令仅支持纯文字,请移除图片后重试。';
|
|
6
|
+
|
|
7
|
+
function commandResult(message, extra = {}) {
|
|
8
|
+
return { message, ...extra };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function requestOptions(signal) {
|
|
12
|
+
return signal ? { signal } : {};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function boundSession(harness, state, key) {
|
|
16
|
+
if (typeof state?.sessionFor !== 'function') return null;
|
|
17
|
+
const sessionId = state.sessionFor(key);
|
|
18
|
+
if (typeof sessionId !== 'string' || !sessionId) return null;
|
|
19
|
+
if (typeof harness?.workspaceSession !== 'function') {
|
|
20
|
+
throw new TypeError('Harness does not support workspace sessions');
|
|
21
|
+
}
|
|
22
|
+
const session = harness.workspaceSession(sessionId);
|
|
23
|
+
if (!session || typeof session !== 'object') {
|
|
24
|
+
throw new TypeError('Harness returned an invalid workspace session');
|
|
25
|
+
}
|
|
26
|
+
return session;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isControlCommand(text) {
|
|
30
|
+
return typeof text === 'string' && CONTROL_COMMAND.test(text.trim());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function runControlCommand(text, harness, state, key, {
|
|
34
|
+
signal,
|
|
35
|
+
hasImages = false,
|
|
36
|
+
pendingInteraction = false,
|
|
37
|
+
control,
|
|
38
|
+
} = {}) {
|
|
39
|
+
if (!isControlCommand(text)) return null;
|
|
40
|
+
const command = text.trim();
|
|
41
|
+
const stop = STOP_COMMAND.test(command);
|
|
42
|
+
|
|
43
|
+
if (hasImages) return commandResult(TEXT_ONLY);
|
|
44
|
+
|
|
45
|
+
if (stop) {
|
|
46
|
+
if (!/^\/stop$/iu.test(command)) return commandResult(STOP_USAGE);
|
|
47
|
+
const session = boundSession(harness, state, key);
|
|
48
|
+
if (!session) return commandResult('当前聊天没有正在运行的任务。');
|
|
49
|
+
if (typeof session.stopActiveTurn !== 'function') {
|
|
50
|
+
throw new TypeError('Harness session does not support stopping active turns');
|
|
51
|
+
}
|
|
52
|
+
const stopped = await session.stopActiveTurn(control, requestOptions(signal));
|
|
53
|
+
return stopped
|
|
54
|
+
? commandResult('已请求停止当前任务。', { stopped: true })
|
|
55
|
+
: commandResult('当前聊天没有正在运行的任务。');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const match = /^\/steer(?:\s+([\s\S]*))?$/iu.exec(command);
|
|
59
|
+
const instruction = match?.[1]?.trim() ?? '';
|
|
60
|
+
if (!instruction) return commandResult(STEER_USAGE);
|
|
61
|
+
if (pendingInteraction) {
|
|
62
|
+
return commandResult([
|
|
63
|
+
'当前任务正在等待你的回答或审批。',
|
|
64
|
+
'',
|
|
65
|
+
'请先处理当前请求,或者发送 /stop 停止任务。',
|
|
66
|
+
].join('\n'));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const session = boundSession(harness, state, key);
|
|
70
|
+
if (!session) {
|
|
71
|
+
return commandResult('当前聊天没有正在运行的任务,请直接发送普通消息。');
|
|
72
|
+
}
|
|
73
|
+
if (typeof session.steerActiveTurn !== 'function') {
|
|
74
|
+
throw new TypeError('Harness session does not support steering active turns');
|
|
75
|
+
}
|
|
76
|
+
const steered = await session.steerActiveTurn(
|
|
77
|
+
instruction,
|
|
78
|
+
control,
|
|
79
|
+
requestOptions(signal),
|
|
80
|
+
);
|
|
81
|
+
return steered
|
|
82
|
+
? commandResult('已提交补充指令,Agent 会在下一步读取。')
|
|
83
|
+
: commandResult('当前聊天没有正在运行的任务,请直接发送普通消息。');
|
|
84
|
+
}
|