@xmanrui/dsh-im 4.23.0 → 4.24.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 +2 -1
- package/README.md +2 -1
- package/lib/client.js +92 -14
- package/lib/index.js +286 -285
- package/package.json +7 -2
- package/plugin-src/client/channels/feishu/index.js +7 -4
- package/plugin-src/client/channels/shared/token-api.js +3 -0
- 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/i18n.js +8 -1
- package/plugin-src/client/index.js +10 -2
- package/plugin-src/client/styles.js +2 -2
- 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/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 +31 -4
- 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/telegram/config-store.mjs +4 -1
- package/src/channels/telegram/telegram-controller.mjs +13 -1
- package/src/channels/telegram/telegram-runtime.mjs +198 -1
|
@@ -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
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export const FEISHU_STEP_PUSH_MODES = Object.freeze({
|
|
2
2
|
POST: 'post',
|
|
3
3
|
STREAMING_CARD: 'streaming_card',
|
|
4
|
+
LIVE_COT: 'live_cot',
|
|
4
5
|
});
|
|
5
6
|
|
|
6
7
|
/** New connections explicitly opt into the process-card presentation. */
|
|
@@ -8,12 +9,17 @@ export const DEFAULT_FEISHU_STEP_PUSH_MODE = FEISHU_STEP_PUSH_MODES.STREAMING_CA
|
|
|
8
9
|
|
|
9
10
|
export function normalizeFeishuStepPushMode(value) {
|
|
10
11
|
// Bots created before modes existed used posts when step push was enabled.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
if (value === FEISHU_STEP_PUSH_MODES.STREAMING_CARD) {
|
|
13
|
+
return FEISHU_STEP_PUSH_MODES.STREAMING_CARD;
|
|
14
|
+
}
|
|
15
|
+
if (value === FEISHU_STEP_PUSH_MODES.LIVE_COT) {
|
|
16
|
+
return FEISHU_STEP_PUSH_MODES.LIVE_COT;
|
|
17
|
+
}
|
|
18
|
+
return FEISHU_STEP_PUSH_MODES.POST;
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
export function isFeishuStepPushMode(value) {
|
|
17
22
|
return value === FEISHU_STEP_PUSH_MODES.POST
|
|
18
|
-
|| value === FEISHU_STEP_PUSH_MODES.STREAMING_CARD
|
|
23
|
+
|| value === FEISHU_STEP_PUSH_MODES.STREAMING_CARD
|
|
24
|
+
|| value === FEISHU_STEP_PUSH_MODES.LIVE_COT;
|
|
19
25
|
}
|