@xmanrui/dsh-im 4.23.0 → 4.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +7 -2
- package/README.md +7 -2
- package/lib/client.js +498 -347
- package/lib/index.js +284 -283
- package/package.json +11 -2
- package/plugin-src/client/channel-card-meta.js +3 -9
- package/plugin-src/client/channels/dingtalk/index.js +9 -8
- package/plugin-src/client/channels/feishu/index.js +21 -17
- package/plugin-src/client/channels/qq/index.js +9 -8
- package/plugin-src/client/channels/shared/collapsible-account.js +49 -26
- package/plugin-src/client/channels/shared/token-api.js +3 -0
- package/plugin-src/client/channels/shared/token-channel.js +9 -8
- package/plugin-src/client/channels/telegram/index.js +3 -0
- package/plugin-src/client/channels/telegram/styles.js +12 -0
- package/plugin-src/client/channels/telegram/thinking-traces.js +25 -0
- package/plugin-src/client/channels/wecom/index.js +9 -8
- package/plugin-src/client/channels/wecom-app/index.js +9 -8
- package/plugin-src/client/channels/weixin/index.js +9 -8
- package/plugin-src/client/channels/whatsapp/index.js +9 -8
- package/plugin-src/client/i18n.js +13 -1
- package/plugin-src/client/index.js +10 -2
- package/plugin-src/client/styles.js +12 -8
- package/plugin-src/client/update-panel.js +18 -8
- package/plugin-src/host/channels/shared/rpc.mjs +9 -0
- package/plugin-src/host/channels/shared/thinking-traces-rpc.mjs +11 -0
- package/plugin-src/host/modern-harness-api.mjs +7 -2
- package/plugin-src/host/update-service.mjs +19 -14
- package/scripts/verify-package.mjs +11 -5
- package/src/channels/email/email-runtime.mjs +9 -2
- package/src/channels/email/transports/agent-mail.mjs +14 -3
- package/src/channels/feishu/bridge.mjs +122 -46
- package/src/channels/feishu/feishu-channel.mjs +35 -0
- package/src/channels/feishu/live-cot.mjs +260 -0
- package/src/channels/feishu/slash-command-registry.mjs +17 -0
- package/src/channels/feishu/step-push-mode.mjs +10 -4
- package/src/channels/shared/harness-client.mjs +224 -39
- package/src/channels/shared/text-harness-bridge.mjs +60 -14
- package/src/channels/shared/workspace-session.mjs +7 -1
- package/src/channels/telegram/config-store.mjs +4 -1
- package/src/channels/telegram/telegram-controller.mjs +13 -1
- package/src/channels/telegram/telegram-runtime.mjs +198 -1
|
@@ -115,6 +115,7 @@ import {
|
|
|
115
115
|
FEISHU_STEP_PUSH_MODES,
|
|
116
116
|
normalizeFeishuStepPushMode,
|
|
117
117
|
} from './step-push-mode.mjs';
|
|
118
|
+
import { FeishuLiveCot } from './live-cot.mjs';
|
|
118
119
|
|
|
119
120
|
// Lazily evaluated: t() must run after setImHostLanguage, not at import time.
|
|
120
121
|
const INTERACTION_RESOLVED_TEXT = () => t('这个问题已在其他客户端处理,无需再次回答。');
|
|
@@ -123,6 +124,7 @@ const RESOLVED_REPLY_TTL_MS = 30 * 60_000;
|
|
|
123
124
|
const ROOT_CANDIDATE_TTL_MS = 30 * 60_000;
|
|
124
125
|
/** Bound main-feed roots awaiting a topic so silent drops cannot grow them. */
|
|
125
126
|
const MAX_ROOT_CANDIDATES = 512;
|
|
127
|
+
const MAX_TOPIC_ANCHORS = 4_000;
|
|
126
128
|
|
|
127
129
|
const MENU_COMMAND = /^\/m(?:enu)?$/i;
|
|
128
130
|
const REPAIR_COMMAND_PREFIX = /^\/repair(?:\s|$)/i;
|
|
@@ -607,7 +609,7 @@ export class FeishuHarnessBridge {
|
|
|
607
609
|
#groupTopicReply = false;
|
|
608
610
|
/** When true, streaming turns push tool calls and interim notes as discrete messages. */
|
|
609
611
|
#stepPush = false;
|
|
610
|
-
/** Step push presentation:
|
|
612
|
+
/** Step push presentation: discrete posts, a CardKit card, or native live CoT. */
|
|
611
613
|
#stepPushMode = FEISHU_STEP_PUSH_MODES.POST;
|
|
612
614
|
/** Per-conversation step push state: key → { lastSentAt, count, breakerLogged }. */
|
|
613
615
|
#stepPushSendState = new Map();
|
|
@@ -623,6 +625,8 @@ export class FeishuHarnessBridge {
|
|
|
623
625
|
#anchorTopicReply = new Map();
|
|
624
626
|
/** Main-feed roots this bot intends to auto-open as topics, awaiting thread_id. */
|
|
625
627
|
#rootCandidates = new Map();
|
|
628
|
+
/** Last successfully injected topic root per conversation and AI session. */
|
|
629
|
+
#topicAnchors = new Map();
|
|
626
630
|
#repair;
|
|
627
631
|
#repairAttempt = null;
|
|
628
632
|
#repairMonitorVersion = 0;
|
|
@@ -4874,6 +4878,72 @@ export class FeishuHarnessBridge {
|
|
|
4874
4878
|
};
|
|
4875
4879
|
}
|
|
4876
4880
|
|
|
4881
|
+
async #preparePrompt(event, key, message) {
|
|
4882
|
+
const snapshot = this.#acceptedMessageIds.get(event.message.message_id);
|
|
4883
|
+
const build = async (input) => {
|
|
4884
|
+
const rich = hasInboundImages(input) || hasReplyReference(input);
|
|
4885
|
+
const original = rich
|
|
4886
|
+
? await promptContentForInboundMessage(input, { signal: this.#signal, deferImages: true })
|
|
4887
|
+
: input.content;
|
|
4888
|
+
const enhanced = enhanceContextContent(original, snapshot, () => ({
|
|
4889
|
+
channel: 'feishu',
|
|
4890
|
+
senderId: senderOpenId(event),
|
|
4891
|
+
chatId: event.message.chat_id,
|
|
4892
|
+
threadId: event.message.thread_id,
|
|
4893
|
+
}));
|
|
4894
|
+
return {
|
|
4895
|
+
content: rich || enhanced !== original ? enhanced : undefined,
|
|
4896
|
+
contextEnhanced: enhanced !== original,
|
|
4897
|
+
sourceGuidance: snapshot?.config?.guidance,
|
|
4898
|
+
};
|
|
4899
|
+
};
|
|
4900
|
+
const rootId = nonEmptyString(event.message.root_id);
|
|
4901
|
+
const topicAnchor = event.message.chat_type === 'group'
|
|
4902
|
+
&& nonEmptyString(event.message.thread_id)
|
|
4903
|
+
&& rootId && message.replyTo?.messageId === rootId
|
|
4904
|
+
// A reference-only message must not become an empty prompt.
|
|
4905
|
+
&& (nonEmptyString(message.content) || hasInboundImages(message) || hasInboundFiles(message));
|
|
4906
|
+
if (!topicAnchor) return build(message);
|
|
4907
|
+
|
|
4908
|
+
const withoutAnchor = { ...message };
|
|
4909
|
+
delete withoutAnchor.replyTo;
|
|
4910
|
+
const base = await build(withoutAnchor);
|
|
4911
|
+
let preparedSessionId;
|
|
4912
|
+
let anchorLoaded = false;
|
|
4913
|
+
return {
|
|
4914
|
+
...base,
|
|
4915
|
+
prepareContent: async ({ sessionId }) => {
|
|
4916
|
+
preparedSessionId = sessionId;
|
|
4917
|
+
anchorLoaded = false;
|
|
4918
|
+
const previous = this.#topicAnchors.get(key);
|
|
4919
|
+
if (previous?.sessionId === sessionId && previous.rootMessageId === rootId) {
|
|
4920
|
+
return base.content ?? message.content;
|
|
4921
|
+
}
|
|
4922
|
+
const prepared = await build({
|
|
4923
|
+
...message,
|
|
4924
|
+
replyTo: {
|
|
4925
|
+
...message.replyTo,
|
|
4926
|
+
load: async (options) => {
|
|
4927
|
+
const reference = await message.replyTo.load(options);
|
|
4928
|
+
anchorLoaded = !reference?.unavailableReason && Boolean(
|
|
4929
|
+
nonEmptyString(reference?.content) || reference?.attachments?.length,
|
|
4930
|
+
);
|
|
4931
|
+
return reference;
|
|
4932
|
+
},
|
|
4933
|
+
},
|
|
4934
|
+
});
|
|
4935
|
+
return prepared.content;
|
|
4936
|
+
},
|
|
4937
|
+
onAskComplete: (sessionId) => {
|
|
4938
|
+
if (!anchorLoaded || sessionId !== preparedSessionId) return;
|
|
4939
|
+
if (!this.#topicAnchors.has(key) && this.#topicAnchors.size >= MAX_TOPIC_ANCHORS) {
|
|
4940
|
+
this.#topicAnchors.delete(this.#topicAnchors.keys().next().value);
|
|
4941
|
+
}
|
|
4942
|
+
this.#topicAnchors.set(key, { sessionId, rootMessageId: rootId });
|
|
4943
|
+
},
|
|
4944
|
+
};
|
|
4945
|
+
}
|
|
4946
|
+
|
|
4877
4947
|
/**
|
|
4878
4948
|
* 分步直推:`#answerWithStream` 流式分支在开关开启时的完整替代路径。
|
|
4879
4949
|
* 过程(工具调用、助手中间说明)与最终答案均以富文本 post 逐条直推
|
|
@@ -4885,30 +4955,18 @@ export class FeishuHarnessBridge {
|
|
|
4885
4955
|
const messageId = event.message.message_id;
|
|
4886
4956
|
const text = message.content;
|
|
4887
4957
|
let askCompleted = false;
|
|
4888
|
-
const markAskComplete = () => {
|
|
4958
|
+
const markAskComplete = (sessionId) => {
|
|
4889
4959
|
if (askCompleted) return;
|
|
4890
4960
|
askCompleted = true;
|
|
4961
|
+
prompt.onAskComplete?.(sessionId);
|
|
4891
4962
|
this.#endImTurn(key);
|
|
4892
4963
|
onAskComplete?.();
|
|
4893
4964
|
};
|
|
4894
4965
|
// 与流式分支一致的提示内容构造:图片与回复引用展开为富提示内容,已接受
|
|
4895
4966
|
// 的上下文增强按原样重放。直推分流发生在 `#answerWithStream` 构造之前,
|
|
4896
4967
|
// 这里就是本回合唯一一次构造(无重复的 prompt 往返)。
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
: undefined;
|
|
4900
|
-
const snapshot = this.#acceptedMessageIds.get(messageId);
|
|
4901
|
-
let contextEnhanced = false;
|
|
4902
|
-
if (snapshot) {
|
|
4903
|
-
const originalContent = content ?? text;
|
|
4904
|
-
content = enhanceContextContent(originalContent, snapshot, () => ({
|
|
4905
|
-
channel: 'feishu',
|
|
4906
|
-
senderId: senderOpenId(event),
|
|
4907
|
-
chatId: event.message.chat_id,
|
|
4908
|
-
threadId: event.message.thread_id,
|
|
4909
|
-
}));
|
|
4910
|
-
contextEnhanced = content !== originalContent;
|
|
4911
|
-
}
|
|
4968
|
+
const prompt = await this.#preparePrompt(event, key, message);
|
|
4969
|
+
const { content, contextEnhanced } = prompt;
|
|
4912
4970
|
// Every turn reopens the per-conversation circuit breaker.
|
|
4913
4971
|
const priorState = this.#stepPushSendState.get(key);
|
|
4914
4972
|
if (this.#stepPushSendState.size >= MAX_STEP_PUSH_SEND_STATES && !priorState) {
|
|
@@ -4931,6 +4989,19 @@ export class FeishuHarnessBridge {
|
|
|
4931
4989
|
// 流式卡片模式:每轮一张过程卡(原地 patch),过程与最终答案都进卡;
|
|
4932
4990
|
// post 模式维持逐条直推。先预建卡片状态,纯问答回合也能在收尾时开卡。
|
|
4933
4991
|
const streamingCard = this.#stepPushMode === FEISHU_STEP_PUSH_MODES.STREAMING_CARD;
|
|
4992
|
+
const liveCot = this.#stepPushMode === FEISHU_STEP_PUSH_MODES.LIVE_COT
|
|
4993
|
+
&& typeof this.#channel?.createCot === 'function'
|
|
4994
|
+
&& typeof this.#channel?.writeCotEvents === 'function';
|
|
4995
|
+
const cot = liveCot
|
|
4996
|
+
? new FeishuLiveCot(this.#channel, chatId, {
|
|
4997
|
+
replyTo: messageId,
|
|
4998
|
+
hidden: false,
|
|
4999
|
+
onFailure: (error) => this.#logger.warn?.(
|
|
5000
|
+
'[dsh-feishu] native live process failed; final answer will continue:',
|
|
5001
|
+
error?.message ?? String(error),
|
|
5002
|
+
),
|
|
5003
|
+
})
|
|
5004
|
+
: null;
|
|
4934
5005
|
if (streamingCard) {
|
|
4935
5006
|
if (this.#stepCards.has(key)) {
|
|
4936
5007
|
// 上一轮异常退出留下的「运行中」卡片先收尾,避免与本轮混淆。
|
|
@@ -4983,7 +5054,9 @@ export class FeishuHarnessBridge {
|
|
|
4983
5054
|
messageId,
|
|
4984
5055
|
);
|
|
4985
5056
|
};
|
|
4986
|
-
const watchdog = streamingCard
|
|
5057
|
+
const watchdog = streamingCard || liveCot
|
|
5058
|
+
? null
|
|
5059
|
+
: this.#startThinkingStatusWatchdog(key, chatId, messageId);
|
|
4987
5060
|
let completed;
|
|
4988
5061
|
try {
|
|
4989
5062
|
completed = await askInWorkspaceSession({
|
|
@@ -4994,13 +5067,14 @@ export class FeishuHarnessBridge {
|
|
|
4994
5067
|
text,
|
|
4995
5068
|
content,
|
|
4996
5069
|
titleText: event.batchSubmission?.title,
|
|
4997
|
-
sourceGuidance:
|
|
5070
|
+
sourceGuidance: prompt.sourceGuidance,
|
|
5071
|
+
prepareContent: prompt.prepareContent,
|
|
4998
5072
|
contextEnhanced,
|
|
4999
5073
|
createOptions: { signal: this.#signal },
|
|
5000
5074
|
existsOptions: { signal: this.#signal },
|
|
5001
5075
|
askOptions: {
|
|
5002
5076
|
...baseAskOptions,
|
|
5003
|
-
progressMode: 'all',
|
|
5077
|
+
progressMode: liveCot ? 'live' : 'all',
|
|
5004
5078
|
// 提问/审批卡弹出前先定格当前过程卡,答案随之流到交互消息之后
|
|
5005
5079
|
// 的新卡上(与主流式路径的 rotate 语义一致)。
|
|
5006
5080
|
...(streamingCard ? {
|
|
@@ -5012,6 +5086,15 @@ export class FeishuHarnessBridge {
|
|
|
5012
5086
|
},
|
|
5013
5087
|
} : {}),
|
|
5014
5088
|
onUpdate: async (update) => {
|
|
5089
|
+
if (liveCot) {
|
|
5090
|
+
await cot.handle(update);
|
|
5091
|
+
if (update.type === 'assistant-message') {
|
|
5092
|
+
pendingStep = update;
|
|
5093
|
+
} else if (update.type === 'tool') {
|
|
5094
|
+
pendingStep = null;
|
|
5095
|
+
}
|
|
5096
|
+
return;
|
|
5097
|
+
}
|
|
5015
5098
|
if (update.type === 'assistant-message') {
|
|
5016
5099
|
if (pendingStep && Number(update.step) > Number(pendingStep.step)) {
|
|
5017
5100
|
await flushPendingStep();
|
|
@@ -5054,6 +5137,7 @@ export class FeishuHarnessBridge {
|
|
|
5054
5137
|
},
|
|
5055
5138
|
});
|
|
5056
5139
|
} catch (error) {
|
|
5140
|
+
await cot?.finish(error);
|
|
5057
5141
|
if (streamingCard) {
|
|
5058
5142
|
this.#stepStopFlags.delete(key);
|
|
5059
5143
|
await this.#finishStepCard(key, { stopped: true });
|
|
@@ -5063,7 +5147,8 @@ export class FeishuHarnessBridge {
|
|
|
5063
5147
|
// 回合结束(成功/失败/中断):停看门狗并撤回残留思考中心跳。
|
|
5064
5148
|
await watchdog?.stop();
|
|
5065
5149
|
}
|
|
5066
|
-
markAskComplete();
|
|
5150
|
+
markAskComplete(completed.sessionId);
|
|
5151
|
+
await cot?.finish();
|
|
5067
5152
|
const finalStepText = pendingStep ? pendingStep.text : null;
|
|
5068
5153
|
pendingStep = null;
|
|
5069
5154
|
const finalText = (typeof finalStepText === 'string' && finalStepText.trim())
|
|
@@ -5130,7 +5215,7 @@ export class FeishuHarnessBridge {
|
|
|
5130
5215
|
}
|
|
5131
5216
|
textReceipt = createDeliveryReceipt({
|
|
5132
5217
|
deliveryId: messageId,
|
|
5133
|
-
presentation: 'feishu-step-push-post',
|
|
5218
|
+
presentation: liveCot ? 'feishu-live-cot-answer' : 'feishu-step-push-post',
|
|
5134
5219
|
providerMessageIds,
|
|
5135
5220
|
});
|
|
5136
5221
|
this.#status.streamResponses = (this.#status.streamResponses ?? 0) + 1;
|
|
@@ -5201,9 +5286,10 @@ export class FeishuHarnessBridge {
|
|
|
5201
5286
|
const messageId = event.message.message_id;
|
|
5202
5287
|
const text = message.content;
|
|
5203
5288
|
let askCompleted = false;
|
|
5204
|
-
const markAskComplete = () => {
|
|
5289
|
+
const markAskComplete = (sessionId) => {
|
|
5205
5290
|
if (askCompleted) return;
|
|
5206
5291
|
askCompleted = true;
|
|
5292
|
+
prompt.onAskComplete?.(sessionId);
|
|
5207
5293
|
this.#endImTurn(key);
|
|
5208
5294
|
onAskComplete?.();
|
|
5209
5295
|
};
|
|
@@ -5214,23 +5300,10 @@ export class FeishuHarnessBridge {
|
|
|
5214
5300
|
if (this.#stepPush && this.#channel?.stream) {
|
|
5215
5301
|
return this.#answerWithStepPush(event, key, message, { onAskComplete });
|
|
5216
5302
|
}
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
: undefined;
|
|
5220
|
-
const snapshot = this.#acceptedMessageIds.get(messageId);
|
|
5221
|
-
let contextEnhanced = false;
|
|
5222
|
-
if (snapshot) {
|
|
5223
|
-
const originalContent = content ?? text;
|
|
5224
|
-
content = enhanceContextContent(originalContent, snapshot, () => ({
|
|
5225
|
-
channel: 'feishu',
|
|
5226
|
-
senderId: senderOpenId(event),
|
|
5227
|
-
chatId: event.message.chat_id,
|
|
5228
|
-
threadId: event.message.thread_id,
|
|
5229
|
-
}));
|
|
5230
|
-
contextEnhanced = content !== originalContent;
|
|
5231
|
-
}
|
|
5303
|
+
const prompt = await this.#preparePrompt(event, key, message);
|
|
5304
|
+
const { content, contextEnhanced } = prompt;
|
|
5232
5305
|
if (!this.#channel?.stream) {
|
|
5233
|
-
const { answer, artifacts = [] } = await askInWorkspaceSession({
|
|
5306
|
+
const { sessionId, answer, artifacts = [] } = await askInWorkspaceSession({
|
|
5234
5307
|
deferredDelivery: () => ({ coordinator: this.#deferred, chatId, replyToMessageId: messageId }),
|
|
5235
5308
|
harness: this.#harness,
|
|
5236
5309
|
state: this.#state,
|
|
@@ -5238,13 +5311,14 @@ export class FeishuHarnessBridge {
|
|
|
5238
5311
|
text,
|
|
5239
5312
|
content,
|
|
5240
5313
|
titleText: event.batchSubmission?.title,
|
|
5241
|
-
sourceGuidance:
|
|
5314
|
+
sourceGuidance: prompt.sourceGuidance,
|
|
5315
|
+
prepareContent: prompt.prepareContent,
|
|
5242
5316
|
contextEnhanced,
|
|
5243
5317
|
createOptions: { signal: this.#signal },
|
|
5244
5318
|
existsOptions: { signal: this.#signal },
|
|
5245
5319
|
askOptions: this.#interactionAskOptions(event, key, message.files, message.images),
|
|
5246
5320
|
});
|
|
5247
|
-
markAskComplete();
|
|
5321
|
+
markAskComplete(sessionId);
|
|
5248
5322
|
let textReceipt;
|
|
5249
5323
|
let textSendError = null;
|
|
5250
5324
|
try {
|
|
@@ -5319,13 +5393,14 @@ export class FeishuHarnessBridge {
|
|
|
5319
5393
|
text,
|
|
5320
5394
|
content,
|
|
5321
5395
|
titleText: event.batchSubmission?.title,
|
|
5322
|
-
sourceGuidance:
|
|
5396
|
+
sourceGuidance: prompt.sourceGuidance,
|
|
5397
|
+
prepareContent: prompt.prepareContent,
|
|
5323
5398
|
contextEnhanced,
|
|
5324
5399
|
createOptions: { signal: this.#signal },
|
|
5325
5400
|
existsOptions: { signal: this.#signal },
|
|
5326
5401
|
askOptions,
|
|
5327
5402
|
});
|
|
5328
|
-
markAskComplete();
|
|
5403
|
+
markAskComplete(completed.sessionId);
|
|
5329
5404
|
completedAnswer = completed.answer;
|
|
5330
5405
|
completedArtifacts = completed.artifacts ?? [];
|
|
5331
5406
|
await controller.setContent(answerTextForDelivery(completedAnswer, completedArtifacts));
|
|
@@ -5383,7 +5458,7 @@ export class FeishuHarnessBridge {
|
|
|
5383
5458
|
if (promptStarted) throw error;
|
|
5384
5459
|
|
|
5385
5460
|
this.#logger.warn?.('[dsh-feishu] native stream unavailable; using text fallback:', error.message);
|
|
5386
|
-
const { answer, artifacts = [] } = await askInWorkspaceSession({
|
|
5461
|
+
const { sessionId, answer, artifacts = [] } = await askInWorkspaceSession({
|
|
5387
5462
|
deferredDelivery: () => ({ coordinator: this.#deferred, chatId, replyToMessageId: messageId }),
|
|
5388
5463
|
harness: this.#harness,
|
|
5389
5464
|
state: this.#state,
|
|
@@ -5391,13 +5466,14 @@ export class FeishuHarnessBridge {
|
|
|
5391
5466
|
text,
|
|
5392
5467
|
content,
|
|
5393
5468
|
titleText: event.batchSubmission?.title,
|
|
5394
|
-
sourceGuidance:
|
|
5469
|
+
sourceGuidance: prompt.sourceGuidance,
|
|
5470
|
+
prepareContent: prompt.prepareContent,
|
|
5395
5471
|
contextEnhanced,
|
|
5396
5472
|
createOptions: { signal: this.#signal },
|
|
5397
5473
|
existsOptions: { signal: this.#signal },
|
|
5398
5474
|
askOptions: this.#interactionAskOptions(event, key, message.files, message.images),
|
|
5399
5475
|
});
|
|
5400
|
-
markAskComplete();
|
|
5476
|
+
markAskComplete(sessionId);
|
|
5401
5477
|
let textReceipt;
|
|
5402
5478
|
let textSendError = null;
|
|
5403
5479
|
try {
|
|
@@ -8,6 +8,7 @@ const STREAM_ELEMENT_ID = 'stream_md';
|
|
|
8
8
|
const DEFAULT_INITIAL_TEXT = '已连接 DeepSeek Harness,正在思考…';
|
|
9
9
|
const MAX_STREAM_CHARS = 28000;
|
|
10
10
|
const MAX_FILE_OPERATION_TIMEOUT_MS = 120_000;
|
|
11
|
+
const COT_API = '/open-apis/im/v1/message_cot';
|
|
11
12
|
|
|
12
13
|
const FILE_DELIVERY_ERRORS = new Map([
|
|
13
14
|
[99991672, ['artifact-permission-required', 'Feishu file delivery requires the im:resource permission.']],
|
|
@@ -365,6 +366,40 @@ export class VerifiedFeishuChannel {
|
|
|
365
366
|
});
|
|
366
367
|
}
|
|
367
368
|
|
|
369
|
+
/** Open one native Feishu thinking-process message. */
|
|
370
|
+
async createCot(chatId, { replyTo, hidden = false } = {}) {
|
|
371
|
+
const response = assertApiSuccess('Feishu message_cot.create', await this.#client.request({
|
|
372
|
+
method: 'POST',
|
|
373
|
+
url: `${COT_API}?receive_id_type=chat_id`,
|
|
374
|
+
data: {
|
|
375
|
+
receive_id: chatId,
|
|
376
|
+
...(replyTo ? { origin_message_id: replyTo } : {}),
|
|
377
|
+
cot_hidden: hidden === true,
|
|
378
|
+
enable_badge: false,
|
|
379
|
+
update_feed_rank: false,
|
|
380
|
+
},
|
|
381
|
+
}));
|
|
382
|
+
const cotId = response?.data?.cot_id;
|
|
383
|
+
const messageId = response?.data?.message_id;
|
|
384
|
+
if (!cotId || !messageId) {
|
|
385
|
+
throw new Error('Feishu message_cot.create returned no cot_id/message_id');
|
|
386
|
+
}
|
|
387
|
+
return { cotId, messageId };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Append ordered AG-UI events to one native thinking process. */
|
|
391
|
+
async writeCotEvents(handle, events) {
|
|
392
|
+
assertApiSuccess('Feishu message_cot.write', await this.#client.request({
|
|
393
|
+
method: 'PUT',
|
|
394
|
+
url: COT_API,
|
|
395
|
+
data: {
|
|
396
|
+
events,
|
|
397
|
+
message_id: handle.messageId,
|
|
398
|
+
cot_id: handle.cotId,
|
|
399
|
+
},
|
|
400
|
+
}));
|
|
401
|
+
}
|
|
402
|
+
|
|
368
403
|
async #sendArtifact(chatId, file, {
|
|
369
404
|
replyTo,
|
|
370
405
|
signal,
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
const MAX_EVENTS_PER_WRITE = 50;
|
|
2
|
+
const MAX_EVENT_CONTENT_CHARS = 4096;
|
|
3
|
+
const MAX_TOOL_RESULT_CHARS = 1500;
|
|
4
|
+
|
|
5
|
+
const TOOL_ICONS = Object.freeze({
|
|
6
|
+
read: 'read',
|
|
7
|
+
edit: 'write',
|
|
8
|
+
delete: 'write',
|
|
9
|
+
move: 'write',
|
|
10
|
+
search: 'search',
|
|
11
|
+
fetch: 'search',
|
|
12
|
+
execute: 'bash',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
let lastTimestamp = 0;
|
|
16
|
+
|
|
17
|
+
function boundedEventContent(content) {
|
|
18
|
+
const encoded = JSON.stringify(content);
|
|
19
|
+
if (encoded.length <= MAX_EVENT_CONTENT_CHARS) return encoded;
|
|
20
|
+
const field = typeof content?.delta === 'string'
|
|
21
|
+
? 'delta'
|
|
22
|
+
: typeof content?.message === 'string' ? 'message' : null;
|
|
23
|
+
if (!field) return JSON.stringify({ ...content, truncated: true });
|
|
24
|
+
|
|
25
|
+
const truncated = { ...content, truncated: true, [field]: content[field] };
|
|
26
|
+
while (truncated[field].length > 0
|
|
27
|
+
&& JSON.stringify(truncated).length > MAX_EVENT_CONTENT_CHARS) {
|
|
28
|
+
truncated[field] = truncated[field].slice(0, Math.max(0, truncated[field].length - 256));
|
|
29
|
+
}
|
|
30
|
+
if (truncated[field].length === 0
|
|
31
|
+
&& JSON.stringify(truncated).length > MAX_EVENT_CONTENT_CHARS) {
|
|
32
|
+
return JSON.stringify({ truncated: true });
|
|
33
|
+
}
|
|
34
|
+
return JSON.stringify(truncated);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function cotEvent(eventType, content) {
|
|
38
|
+
lastTimestamp = Math.max(Date.now(), lastTimestamp + 1);
|
|
39
|
+
return {
|
|
40
|
+
event_type: eventType,
|
|
41
|
+
content: boundedEventContent(content),
|
|
42
|
+
timestamp: String(lastTimestamp),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function boundedResult(value) {
|
|
47
|
+
const text = String(value ?? '');
|
|
48
|
+
return text.length <= MAX_TOOL_RESULT_CHARS
|
|
49
|
+
? text
|
|
50
|
+
: `${text.slice(0, MAX_TOOL_RESULT_CHARS - 1)}…`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function toolKind(name) {
|
|
54
|
+
const value = String(name ?? '').toLowerCase();
|
|
55
|
+
if (/read|view|list|get/.test(value)) return 'read';
|
|
56
|
+
if (/edit|write|create|patch|update/.test(value)) return 'edit';
|
|
57
|
+
if (/delete|remove/.test(value)) return 'delete';
|
|
58
|
+
if (/move|rename/.test(value)) return 'move';
|
|
59
|
+
if (/search|grep|glob|find/.test(value)) return 'search';
|
|
60
|
+
if (/fetch|web|http|download/.test(value)) return 'fetch';
|
|
61
|
+
if (/shell|bash|command|exec|terminal/.test(value)) return 'execute';
|
|
62
|
+
return '';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function turnFailure(reason) {
|
|
66
|
+
const kind = typeof reason === 'string' ? reason : reason?.kind;
|
|
67
|
+
if (reason === null || reason === undefined || kind === 'completed') return null;
|
|
68
|
+
if (kind !== 'error') {
|
|
69
|
+
return kind ? `Turn ended: ${kind}` : 'Turn failed';
|
|
70
|
+
}
|
|
71
|
+
const error = reason.error ?? reason.failure;
|
|
72
|
+
if (!error) return 'Turn failed';
|
|
73
|
+
return [error.code, error.message].filter(Boolean).join(': ') || 'Turn failed';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Maps Harness progress updates to Feishu's native thinking-process events.
|
|
78
|
+
* Process failures are contained here so they can never suppress the answer.
|
|
79
|
+
*/
|
|
80
|
+
export class FeishuLiveCot {
|
|
81
|
+
#channel;
|
|
82
|
+
#chatId;
|
|
83
|
+
#replyTo;
|
|
84
|
+
#hidden;
|
|
85
|
+
#onFailure;
|
|
86
|
+
#turn = null;
|
|
87
|
+
#opening = null;
|
|
88
|
+
#chain = Promise.resolve();
|
|
89
|
+
#pending = [];
|
|
90
|
+
#reasoningOpen = false;
|
|
91
|
+
#heldAssistant = null;
|
|
92
|
+
#finished = false;
|
|
93
|
+
#broken = false;
|
|
94
|
+
#messageIndex = 0;
|
|
95
|
+
|
|
96
|
+
constructor(channel, chatId, {
|
|
97
|
+
replyTo,
|
|
98
|
+
hidden = false,
|
|
99
|
+
onFailure = () => {},
|
|
100
|
+
} = {}) {
|
|
101
|
+
this.#channel = channel;
|
|
102
|
+
this.#chatId = chatId;
|
|
103
|
+
this.#replyTo = replyTo;
|
|
104
|
+
this.#hidden = hidden === true;
|
|
105
|
+
this.#onFailure = onFailure;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async #start(turn) {
|
|
109
|
+
if (this.#turn !== null || this.#broken) return;
|
|
110
|
+
this.#turn = Number.isSafeInteger(turn) ? turn : 0;
|
|
111
|
+
this.#opening = this.#channel.createCot(this.#chatId, {
|
|
112
|
+
...(this.#replyTo ? { replyTo: this.#replyTo } : {}),
|
|
113
|
+
hidden: this.#hidden,
|
|
114
|
+
}).catch((error) => {
|
|
115
|
+
this.#broken = true;
|
|
116
|
+
this.#onFailure(error);
|
|
117
|
+
return null;
|
|
118
|
+
});
|
|
119
|
+
await this.#write([
|
|
120
|
+
cotEvent('RUN_STARTED', {
|
|
121
|
+
threadId: this.#chatId,
|
|
122
|
+
runId: `turn-${this.#turn}`,
|
|
123
|
+
}),
|
|
124
|
+
]);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#write(events) {
|
|
128
|
+
if (this.#broken || events.length === 0) return;
|
|
129
|
+
this.#pending.push(...events);
|
|
130
|
+
this.#chain = this.#chain.then(async () => {
|
|
131
|
+
const handle = await this.#opening;
|
|
132
|
+
if (!handle || this.#broken) {
|
|
133
|
+
this.#pending.length = 0;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
while (this.#pending.length > 0) {
|
|
137
|
+
await this.#channel.writeCotEvents(
|
|
138
|
+
handle,
|
|
139
|
+
this.#pending.splice(0, MAX_EVENTS_PER_WRITE),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}).catch((error) => {
|
|
143
|
+
this.#broken = true;
|
|
144
|
+
this.#pending.length = 0;
|
|
145
|
+
this.#onFailure(error);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async #closeReasoning() {
|
|
150
|
+
if (!this.#reasoningOpen) return;
|
|
151
|
+
this.#reasoningOpen = false;
|
|
152
|
+
await this.#write([
|
|
153
|
+
cotEvent('REASONING_MESSAGE_END', {
|
|
154
|
+
messageId: `reasoning-${this.#turn}`,
|
|
155
|
+
}),
|
|
156
|
+
]);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async #flushHeldAssistant() {
|
|
160
|
+
const held = this.#heldAssistant;
|
|
161
|
+
this.#heldAssistant = null;
|
|
162
|
+
if (!held?.text) return;
|
|
163
|
+
const messageId = `text-${this.#turn}-${this.#messageIndex++}`;
|
|
164
|
+
await this.#write([
|
|
165
|
+
cotEvent('TEXT_MESSAGE_START', { messageId, role: 'assistant' }),
|
|
166
|
+
cotEvent('TEXT_MESSAGE_CONTENT', { messageId, delta: held.text }),
|
|
167
|
+
cotEvent('TEXT_MESSAGE_END', { messageId }),
|
|
168
|
+
]);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async handle(update) {
|
|
172
|
+
if (!update || this.#finished || this.#broken) return;
|
|
173
|
+
if (update.type === 'turn-start') {
|
|
174
|
+
await this.#start(update.turn);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
await this.#start(update.turn);
|
|
178
|
+
if (update.type === 'reasoning') {
|
|
179
|
+
if (!update.text) return;
|
|
180
|
+
const messageId = `reasoning-${this.#turn}`;
|
|
181
|
+
if (!this.#reasoningOpen) {
|
|
182
|
+
this.#reasoningOpen = true;
|
|
183
|
+
await this.#write([
|
|
184
|
+
cotEvent('REASONING_MESSAGE_START', { messageId, role: 'reasoning' }),
|
|
185
|
+
]);
|
|
186
|
+
}
|
|
187
|
+
await this.#write([
|
|
188
|
+
cotEvent('REASONING_MESSAGE_CONTENT', { messageId, delta: update.text }),
|
|
189
|
+
]);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (update.type === 'assistant-message') {
|
|
193
|
+
if (this.#heldAssistant) await this.#flushHeldAssistant();
|
|
194
|
+
this.#heldAssistant = update;
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (update.type === 'tool') {
|
|
198
|
+
await this.#flushHeldAssistant();
|
|
199
|
+
await this.#closeReasoning();
|
|
200
|
+
const toolCallId = update.callId ?? `tool-${this.#turn}-${this.#messageIndex++}`;
|
|
201
|
+
const kind = toolKind(update.name);
|
|
202
|
+
await this.#write([
|
|
203
|
+
cotEvent('TOOL_CALL_START', {
|
|
204
|
+
toolCallId,
|
|
205
|
+
icon: TOOL_ICONS[kind] ?? 'default',
|
|
206
|
+
title: update.name || 'Tool',
|
|
207
|
+
toolCallName: update.name || 'tool',
|
|
208
|
+
}),
|
|
209
|
+
cotEvent('TOOL_CALL_ARGS', {
|
|
210
|
+
toolCallId,
|
|
211
|
+
delta: update.arguments ?? '',
|
|
212
|
+
}),
|
|
213
|
+
cotEvent('TOOL_CALL_END', { toolCallId }),
|
|
214
|
+
]);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (update.type === 'tool-result' && update.callId) {
|
|
218
|
+
await this.#write([
|
|
219
|
+
cotEvent('TOOL_CALL_RESULT', {
|
|
220
|
+
messageId: `result-${update.callId}`,
|
|
221
|
+
toolCallId: update.callId,
|
|
222
|
+
role: 'tool',
|
|
223
|
+
content: { type: 'code', code: boundedResult(update.text) },
|
|
224
|
+
...(update.errorCode ? { error: update.errorCode } : {}),
|
|
225
|
+
}),
|
|
226
|
+
]);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (update.type === 'turn-end') {
|
|
230
|
+
await this.finish(update.reason);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async finish(reason) {
|
|
235
|
+
if (this.#finished) {
|
|
236
|
+
await this.#chain;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
this.#finished = true;
|
|
240
|
+
this.#heldAssistant = null;
|
|
241
|
+
if (this.#turn === null || this.#broken) {
|
|
242
|
+
await this.#chain;
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
await this.#closeReasoning();
|
|
246
|
+
const failure = reason instanceof Error
|
|
247
|
+
? reason.message
|
|
248
|
+
: turnFailure(reason);
|
|
249
|
+
await this.#write([
|
|
250
|
+
failure
|
|
251
|
+
? cotEvent('RUN_ERROR', { message: failure, code: 'TURN_FAILED' })
|
|
252
|
+
: cotEvent('RUN_FINISHED', {
|
|
253
|
+
threadId: this.#chatId,
|
|
254
|
+
runId: `turn-${this.#turn}`,
|
|
255
|
+
status: 'done',
|
|
256
|
+
}),
|
|
257
|
+
]);
|
|
258
|
+
await this.#chain;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -50,6 +50,23 @@ export const SLASH_COMMAND_MANIFEST = Object.freeze([
|
|
|
50
50
|
{ command: 'unwatch', icon: 'clear_outlined', default: '取消关注会话', en_us: 'Unwatch a session' },
|
|
51
51
|
{ command: 'watchlist', icon: 'flag_outlined', default: '查看关注列表', en_us: 'List watched sessions' },
|
|
52
52
|
{ command: 'archived', icon: 'folder_outlined', default: '设置归档会话显隐(on/off)', en_us: 'Show or hide archived sessions (on/off)' },
|
|
53
|
+
{ command: 'history', icon: 'chat-ai_outlined', default: '查看最近历史消息(仅私聊)', en_us: 'Show recent history (private chats only)' },
|
|
54
|
+
{ command: 'workspace', icon: 'folder_outlined', default: '切换工作区', en_us: 'Switch workspace' },
|
|
55
|
+
{ command: 'conv', icon: 'folder_outlined', default: '设置当前对话专属工作区', en_us: 'Set the workspace for this conversation' },
|
|
56
|
+
{ command: 'session', icon: 'chat-ai_outlined', default: '绑定已有会话', en_us: 'Bind an existing session' },
|
|
57
|
+
{ command: 'models', icon: 'ai-functions_outlined', default: '列出可用模型', en_us: 'List available models' },
|
|
58
|
+
{ command: 'model', icon: 'ai-agent_outlined', default: '查看或切换当前模型', en_us: 'Show or switch the current model' },
|
|
59
|
+
{ command: 'reasoninglist', icon: 'ai-deepthink_outlined', default: '列出可用推理等级', en_us: 'List available reasoning efforts' },
|
|
60
|
+
{ command: 'reasoning', icon: 'ai-deepthink_outlined', default: '查看或切换推理等级', en_us: 'Show or switch the reasoning effort' },
|
|
61
|
+
{ command: 'presetlist', icon: 'skill_outlined', default: '列出可用 Agent 预设', en_us: 'List available Agent Presets' },
|
|
62
|
+
{ command: 'preset', icon: 'skill_outlined', default: '查看或切换 Agent 预设', en_us: 'Show or switch the Agent Preset' },
|
|
63
|
+
{ command: 'stop', icon: 'clear_outlined', default: '停止当前任务', en_us: 'Stop the current task' },
|
|
64
|
+
{ command: 'steer', icon: 'promptword_outlined', default: '给当前任务补充指令', en_us: 'Send additional instructions to the current task' },
|
|
65
|
+
{ command: 'batch', icon: 'chat-ai_outlined', default: '开始批量输入(仅私聊)', en_us: 'Start batch input (private chats only)' },
|
|
66
|
+
{ command: 'send', icon: 'chat-ai_outlined', default: '提交当前批次(仅私聊)', en_us: 'Submit the current batch (private chats only)' },
|
|
67
|
+
{ command: 'cancel', icon: 'clear_outlined', default: '取消当前批次(仅私聊)', en_us: 'Cancel the current batch (private chats only)' },
|
|
68
|
+
{ command: 'version', icon: 'ai-functions_outlined', default: '查看插件版本', en_us: 'Show the plugin version' },
|
|
69
|
+
{ command: 'repair', icon: 'ai-functions_outlined', default: '补全飞书权限与卡片回调(仅私聊)', en_us: 'Complete Feishu permissions and card callbacks (private chats only)' },
|
|
53
70
|
]);
|
|
54
71
|
|
|
55
72
|
// Commands that require a parameter are registered too, so the user can type
|