@xmanrui/dsh-im 4.4.0 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/lib/client.js +26 -9
- package/lib/index.js +229 -226
- package/package.json +1 -1
- package/plugin-src/client/context-enhancement.js +19 -5
- package/plugin-src/client/i18n.js +5 -1
- package/plugin-src/host/modern-harness-api.mjs +3 -0
- package/src/channels/dingtalk/dingtalk-bridge.mjs +9 -3
- package/src/channels/feishu/bridge.mjs +19 -2
- package/src/channels/feishu/feishu-cards.mjs +2 -2
- package/src/channels/feishu/feishu-channel.mjs +36 -6
- package/src/channels/qq/qq-bridge.mjs +8 -3
- package/src/channels/shared/bot-workspace-store.mjs +5 -0
- package/src/channels/shared/context-enhancement.mjs +9 -4
- package/src/channels/shared/harness-client.mjs +6 -0
- package/src/channels/shared/i18n-en/feishu.mjs +3 -3
- package/src/channels/shared/i18n-en/shared-a.mjs +2 -1
- package/src/channels/shared/i18n-en/shared-b.mjs +6 -3
- package/src/channels/shared/session-title.mjs +74 -0
- package/src/channels/shared/text-harness-bridge.mjs +7 -2
- package/src/channels/shared/workspace-command.mjs +16 -4
- package/src/channels/shared/workspace-session.mjs +28 -2
- package/src/channels/telegram/telegram-runtime.mjs +1 -0
- package/src/channels/wecom/wecom-bridge.mjs +6 -2
- package/src/channels/weixin/weixin-bridge.mjs +8 -3
package/package.json
CHANGED
|
@@ -16,15 +16,28 @@ const FIELD_LABELS = Object.freeze({
|
|
|
16
16
|
conversationType: '会话类型',
|
|
17
17
|
senderId: '发送者标识',
|
|
18
18
|
senderName: '发送者昵称',
|
|
19
|
+
conversationTitle: '会话标题',
|
|
19
20
|
botId: '机器人标识',
|
|
20
21
|
});
|
|
21
22
|
|
|
23
|
+
const FIELD_HELP = Object.freeze({
|
|
24
|
+
senderName: Object.freeze({
|
|
25
|
+
labelKey: 'senderNameHelpLabel',
|
|
26
|
+
text: '该字段不是每个渠道都能提供。当前消息没有发送者昵称时,即使已选择该字段,<dsh_im_source> 中也会省略 senderName。',
|
|
27
|
+
}),
|
|
28
|
+
conversationTitle: Object.freeze({
|
|
29
|
+
labelKey: 'conversationTitleHelpLabel',
|
|
30
|
+
text: '该字段不是每个渠道都能提供。钉钉群聊会带上群名。当前消息没有会话标题时,即使已选择该字段,<dsh_im_source> 中也会省略 conversationTitle。',
|
|
31
|
+
}),
|
|
32
|
+
});
|
|
33
|
+
|
|
22
34
|
const SCOPE_COPY = Object.freeze({
|
|
23
35
|
group: Object.freeze({
|
|
24
36
|
title: '群聊',
|
|
25
37
|
enable: '启用',
|
|
26
38
|
fieldsHelpLabel: '查看群聊来源字段说明',
|
|
27
39
|
senderNameHelpLabel: '查看群聊发送者昵称字段说明',
|
|
40
|
+
conversationTitleHelpLabel: '查看群聊会话标题字段说明',
|
|
28
41
|
guidanceLabel: '增强提示词',
|
|
29
42
|
guidanceHelpLabel: '查看群聊增强提示词使用说明',
|
|
30
43
|
guidanceUsage: '用于告诉模型如何使用当前群聊消息的 <dsh_im_source> 来源字段。只填写正文,插件会自动添加 <dsh_im_source_guidance> 成对标签。',
|
|
@@ -35,6 +48,7 @@ const SCOPE_COPY = Object.freeze({
|
|
|
35
48
|
enable: '启用',
|
|
36
49
|
fieldsHelpLabel: '查看私聊来源字段说明',
|
|
37
50
|
senderNameHelpLabel: '查看私聊发送者昵称字段说明',
|
|
51
|
+
conversationTitleHelpLabel: '查看私聊会话标题字段说明',
|
|
38
52
|
guidanceLabel: '增强提示词',
|
|
39
53
|
guidanceHelpLabel: '查看私聊增强提示词使用说明',
|
|
40
54
|
guidanceUsage: '用于告诉模型如何使用当前私聊消息的 <dsh_im_source> 来源字段。只填写正文,插件会自动添加 <dsh_im_source_guidance> 成对标签。',
|
|
@@ -73,7 +87,6 @@ function ContextEnhancementScopeEditor({
|
|
|
73
87
|
const copy = SCOPE_COPY[kind];
|
|
74
88
|
const unavailableId = `${idPrefix}-${kind}-unavailable`;
|
|
75
89
|
const fieldsHelpId = `${idPrefix}-${kind}-fields-help`;
|
|
76
|
-
const senderNameHelpId = `${idPrefix}-${kind}-sender-name-help`;
|
|
77
90
|
const guidanceId = `${idPrefix}-${kind}-guidance`;
|
|
78
91
|
const guidanceHelpId = `${idPrefix}-${kind}-guidance-help`;
|
|
79
92
|
const disabled = busy || !supported;
|
|
@@ -118,16 +131,17 @@ function ContextEnhancementScopeEditor({
|
|
|
118
131
|
}),
|
|
119
132
|
h('span', { className: 'dim-contextFieldText' },
|
|
120
133
|
h('label', { className: 'dim-contextFieldName', htmlFor: fieldId }, FIELD_LABELS[field]),
|
|
121
|
-
field
|
|
134
|
+
FIELD_HELP[field] ? h('span', { className: 'dim-contextHelp dim-contextFieldHelp' },
|
|
122
135
|
h('button', {
|
|
123
136
|
type: 'button', className: 'dim-contextHelpButton dim-contextFieldHelpButton', disabled,
|
|
124
|
-
'aria-label': copy.
|
|
137
|
+
'aria-label': copy[FIELD_HELP[field].labelKey],
|
|
138
|
+
'aria-describedby': `${idPrefix}-${kind}-${field}-help`,
|
|
125
139
|
}, h('span', { 'aria-hidden': 'true' }, '?')),
|
|
126
140
|
h('span', {
|
|
127
|
-
id:
|
|
141
|
+
id: `${idPrefix}-${kind}-${field}-help`,
|
|
128
142
|
className: 'dim-contextTooltip dim-contextFieldTooltip',
|
|
129
143
|
role: 'tooltip',
|
|
130
|
-
},
|
|
144
|
+
}, FIELD_HELP[field].text)) : null,
|
|
131
145
|
h('label', { className: 'dim-contextFieldKey', htmlFor: fieldId }, field)));
|
|
132
146
|
}))),
|
|
133
147
|
h('div', { className: 'dim-contextGuidance dim-contextScopeBlock' },
|
|
@@ -288,6 +288,10 @@ const EN = Object.freeze({
|
|
|
288
288
|
'查看群聊发送者昵称字段说明': 'View group sender name field details',
|
|
289
289
|
'查看私聊发送者昵称字段说明': 'View direct sender name field details',
|
|
290
290
|
'该字段不是每个渠道都能提供。当前消息没有发送者昵称时,即使已选择该字段,<dsh_im_source> 中也会省略 senderName。': 'This field is not available on every channel. If the current message has no sender name, <dsh_im_source> omits senderName even when the field is selected.',
|
|
291
|
+
'会话标题': 'Conversation title',
|
|
292
|
+
'查看群聊会话标题字段说明': 'View group conversation title field details',
|
|
293
|
+
'查看私聊会话标题字段说明': 'View direct conversation title field details',
|
|
294
|
+
'该字段不是每个渠道都能提供。钉钉群聊会带上群名。当前消息没有会话标题时,即使已选择该字段,<dsh_im_source> 中也会省略 conversationTitle。': 'This field is not available on every channel. DingTalk group chats include the group name. If the current message has no conversation title, <dsh_im_source> omits conversationTitle even when the field is selected.',
|
|
291
295
|
'机器人标识': 'Bot ID',
|
|
292
296
|
'增强提示词': 'Guidance',
|
|
293
297
|
'查看增强提示词使用说明': 'View guidance instructions',
|
|
@@ -315,7 +319,7 @@ const EN = Object.freeze({
|
|
|
315
319
|
'上下文增强保存失败,请重试。': 'Could not save context enhancement. Try again.',
|
|
316
320
|
'请提交完整的上下文增强设置。': 'Submit the complete context enhancement settings.',
|
|
317
321
|
'群聊和私聊开关必须是布尔值。': 'Group and direct switches must be booleans.',
|
|
318
|
-
'
|
|
322
|
+
'来源字段只能选择已定义的六个字段。': 'Source fields must be chosen from the six defined fields.',
|
|
319
323
|
'保存中…': 'Saving…',
|
|
320
324
|
'未设置': 'Not set',
|
|
321
325
|
'工作区修改失败,请重试。': 'Could not update the workspace. Try again.',
|
|
@@ -221,6 +221,9 @@ class ModernHarnessApi {
|
|
|
221
221
|
{ request: { requestId: request.rpcId, ...request.payload } },
|
|
222
222
|
signal,
|
|
223
223
|
)),
|
|
224
|
+
rename: (request, signal) => rpcResult(request, () => this.#invoke(
|
|
225
|
+
'session', 'rename', { request: request.payload }, signal,
|
|
226
|
+
)),
|
|
224
227
|
cancel: (request, signal) => rpcResult(request, () => this.#invoke(
|
|
225
228
|
'session', 'cancel', { request: { sessionId: request.payload.sessionId } }, signal,
|
|
226
229
|
)),
|
|
@@ -74,7 +74,7 @@ const HELP_TEXT_LINES = [
|
|
|
74
74
|
'/new 开启一个全新会话',
|
|
75
75
|
'/compact 压缩当前会话的较早上下文',
|
|
76
76
|
'/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)',
|
|
77
|
-
'/workspace
|
|
77
|
+
'/workspace 工作区序号或绝对路径 切换工作区',
|
|
78
78
|
'/workspacelist 列出工作区绝对路径',
|
|
79
79
|
'/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题',
|
|
80
80
|
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
|
@@ -996,12 +996,16 @@ export class DingtalkHarnessBridge {
|
|
|
996
996
|
? await promptContentForMessage(promptMessage, { signal: this.#signal })
|
|
997
997
|
: undefined;
|
|
998
998
|
const snapshot = this.#acceptedMessageIds.get(messageId);
|
|
999
|
+
let contextEnhanced = false;
|
|
999
1000
|
if (snapshot) {
|
|
1000
|
-
|
|
1001
|
+
const originalContent = content ?? text;
|
|
1002
|
+
content = enhanceContextContent(originalContent, snapshot, () => ({
|
|
1001
1003
|
channel: 'dingtalk',
|
|
1002
1004
|
senderId: sender,
|
|
1003
1005
|
senderName: message.senderNick,
|
|
1006
|
+
conversationTitle: message.conversationTitle,
|
|
1004
1007
|
}));
|
|
1008
|
+
contextEnhanced = content !== originalContent;
|
|
1005
1009
|
}
|
|
1006
1010
|
if (typeof this.#api.createAiCard === 'function'
|
|
1007
1011
|
&& typeof this.#api.updateAiCard === 'function'
|
|
@@ -1020,7 +1024,9 @@ export class DingtalkHarnessBridge {
|
|
|
1020
1024
|
harness: this.#harness,
|
|
1021
1025
|
state: this.#state,
|
|
1022
1026
|
key,
|
|
1023
|
-
|
|
1027
|
+
text,
|
|
1028
|
+
content,
|
|
1029
|
+
contextEnhanced,
|
|
1024
1030
|
createOptions: { signal: this.#signal },
|
|
1025
1031
|
existsOptions: { signal: this.#signal },
|
|
1026
1032
|
askOptions: {
|
|
@@ -153,6 +153,7 @@ function isFeishuLocalCommand(text, { hasImages = false, hasFiles = false } = {}
|
|
|
153
153
|
|
|
154
154
|
/** Canonical workspace/session help advertised by every bridge family. */
|
|
155
155
|
const WORKSPACE_HELP_LINES = [
|
|
156
|
+
'/workspace 工作区序号或绝对路径 切换工作区',
|
|
156
157
|
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
|
157
158
|
'/workspacelist 列出工作区绝对路径',
|
|
158
159
|
'/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题',
|
|
@@ -3292,11 +3293,14 @@ export class FeishuHarnessBridge {
|
|
|
3292
3293
|
? await promptContentForMessage(message, { signal: this.#signal })
|
|
3293
3294
|
: undefined;
|
|
3294
3295
|
const snapshot = this.#acceptedMessageIds.get(messageId);
|
|
3296
|
+
let contextEnhanced = false;
|
|
3295
3297
|
if (snapshot) {
|
|
3296
|
-
|
|
3298
|
+
const originalContent = content ?? text;
|
|
3299
|
+
content = enhanceContextContent(originalContent, snapshot, () => ({
|
|
3297
3300
|
channel: 'feishu',
|
|
3298
3301
|
senderId: senderOpenId(event),
|
|
3299
3302
|
}));
|
|
3303
|
+
contextEnhanced = content !== originalContent;
|
|
3300
3304
|
}
|
|
3301
3305
|
if (!this.#channel?.stream) {
|
|
3302
3306
|
const { answer, artifacts = [] } = await askInWorkspaceSession({
|
|
@@ -3305,6 +3309,7 @@ export class FeishuHarnessBridge {
|
|
|
3305
3309
|
key,
|
|
3306
3310
|
text,
|
|
3307
3311
|
content,
|
|
3312
|
+
contextEnhanced,
|
|
3308
3313
|
createOptions: { signal: this.#signal },
|
|
3309
3314
|
existsOptions: { signal: this.#signal },
|
|
3310
3315
|
askOptions: this.#interactionAskOptions(event, key, message.files),
|
|
@@ -3351,8 +3356,18 @@ export class FeishuHarnessBridge {
|
|
|
3351
3356
|
stream = await this.#channel.stream(chatId, {
|
|
3352
3357
|
markdown: async (controller) => {
|
|
3353
3358
|
promptStarted = true;
|
|
3359
|
+
const baseAskOptions = this.#interactionAskOptions(event, key, message.files);
|
|
3354
3360
|
const askOptions = {
|
|
3355
|
-
...
|
|
3361
|
+
...baseAskOptions,
|
|
3362
|
+
// issue #86:独立交互消息(提问/审批)会落在占位卡下方,呈现前
|
|
3363
|
+
// 先换卡,让最终答案落在交互消息之后的新流式卡上。
|
|
3364
|
+
onInteraction: async (interaction) => {
|
|
3365
|
+
if ((interaction?.kind === 'question' || interaction?.kind === 'approval')
|
|
3366
|
+
&& typeof controller?.rotate === 'function') {
|
|
3367
|
+
await controller.rotate();
|
|
3368
|
+
}
|
|
3369
|
+
await baseAskOptions.onInteraction(interaction);
|
|
3370
|
+
},
|
|
3356
3371
|
onUpdate: async (update) => {
|
|
3357
3372
|
await controller.setContent(this.#progressText(update));
|
|
3358
3373
|
this.#status.streamUpdates = (this.#status.streamUpdates ?? 0) + 1;
|
|
@@ -3364,6 +3379,7 @@ export class FeishuHarnessBridge {
|
|
|
3364
3379
|
key,
|
|
3365
3380
|
text,
|
|
3366
3381
|
content,
|
|
3382
|
+
contextEnhanced,
|
|
3367
3383
|
createOptions: { signal: this.#signal },
|
|
3368
3384
|
existsOptions: { signal: this.#signal },
|
|
3369
3385
|
askOptions,
|
|
@@ -3427,6 +3443,7 @@ export class FeishuHarnessBridge {
|
|
|
3427
3443
|
key,
|
|
3428
3444
|
text,
|
|
3429
3445
|
content,
|
|
3446
|
+
contextEnhanced,
|
|
3430
3447
|
createOptions: { signal: this.#signal },
|
|
3431
3448
|
existsOptions: { signal: this.#signal },
|
|
3432
3449
|
askOptions: this.#interactionAskOptions(event, key, message.files),
|
|
@@ -490,7 +490,7 @@ export function menuHelpText() {
|
|
|
490
490
|
'/sessionlist 或 /sessions 列出工作区会话',
|
|
491
491
|
'/session ID 绑定已有会话',
|
|
492
492
|
'/workspacelist 列出工作区',
|
|
493
|
-
'/workspace
|
|
493
|
+
'/workspace 工作区序号或绝对路径 切换工作区',
|
|
494
494
|
'/new 开启全新会话',
|
|
495
495
|
'',
|
|
496
496
|
'📊 状态 / 压缩',
|
|
@@ -554,7 +554,7 @@ const HELP_TEXT_COMMANDS = [
|
|
|
554
554
|
'`/new` — 开启全新会话',
|
|
555
555
|
'`/session ID` — 绑定已有会话',
|
|
556
556
|
'`/sessionlist [工作区]` 或 `/sessions [工作区]` — 列出会话',
|
|
557
|
-
'`/workspace
|
|
557
|
+
'`/workspace 工作区序号或绝对路径` — 切换工作区',
|
|
558
558
|
'`/workspacelist` — 列出工作区',
|
|
559
559
|
'`/status` — 查看连接状态',
|
|
560
560
|
'`/compact` — 压缩上下文',
|
|
@@ -205,15 +205,45 @@ export class VerifiedFeishuChannel {
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
const cards = [];
|
|
208
|
+
let activeCard = null;
|
|
209
|
+
let rotating = false;
|
|
208
210
|
try {
|
|
209
|
-
|
|
210
|
-
cards.push(
|
|
211
|
+
activeCard = await this.#createStreamCard(chatId, options.replyTo);
|
|
212
|
+
cards.push(activeCard);
|
|
211
213
|
let lastContent = this.#initialText;
|
|
214
|
+
// issue #86:独立交互消息(提问/审批)落在占位卡下方后,最终答案不得
|
|
215
|
+
// 回写旧卡。rotate() 把旧卡定格为「过程记录 + 指引行」并标记换卡态;
|
|
216
|
+
// 下一次 setContent(过程更新或最终答案)才创建新卡——新卡必然创建于
|
|
217
|
+
// 交互消息之后。旧卡纳入 cards,参与 recall 与 providerMessageIds。
|
|
218
|
+
const ensureActiveCard = async () => {
|
|
219
|
+
if (!rotating) return activeCard;
|
|
220
|
+
activeCard = await this.#createStreamCard(chatId, options.replyTo);
|
|
221
|
+
cards.push(activeCard);
|
|
222
|
+
rotating = false;
|
|
223
|
+
return activeCard;
|
|
224
|
+
};
|
|
212
225
|
const controller = {
|
|
213
|
-
messageId
|
|
226
|
+
get messageId() {
|
|
227
|
+
return activeCard.messageId;
|
|
228
|
+
},
|
|
229
|
+
rotate: async () => {
|
|
230
|
+
if (rotating) return;
|
|
231
|
+
rotating = true;
|
|
232
|
+
try {
|
|
233
|
+
await this.#updateStreamCard(
|
|
234
|
+
activeCard,
|
|
235
|
+
`${streamPreview(lastContent)}\n\n${t('⤵️ 最终结果见下方')}`,
|
|
236
|
+
);
|
|
237
|
+
await this.#finishStreamCard(activeCard);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
// 明确降级:定格失败不阻塞交互呈现,旧卡保留原内容。
|
|
240
|
+
console.warn('[dsh-feishu] unable to finalize the superseded stream card:', error.message);
|
|
241
|
+
}
|
|
242
|
+
},
|
|
214
243
|
setContent: async (content) => {
|
|
215
244
|
const next = String(content ?? '') || '…';
|
|
216
|
-
await
|
|
245
|
+
const card = await ensureActiveCard();
|
|
246
|
+
await this.#updateStreamCard(card, streamPreview(next));
|
|
217
247
|
// Updates are replaceable snapshots, including progress/tool text.
|
|
218
248
|
// Retain the full latest snapshot even when its preview is unchanged.
|
|
219
249
|
lastContent = next;
|
|
@@ -224,14 +254,14 @@ export class VerifiedFeishuChannel {
|
|
|
224
254
|
const chunks = splitStreamContent(lastContent);
|
|
225
255
|
for (const [index, chunk] of chunks.entries()) {
|
|
226
256
|
const card = index === 0
|
|
227
|
-
?
|
|
257
|
+
? await ensureActiveCard()
|
|
228
258
|
: await this.#createStreamCard(chatId, options.replyTo);
|
|
229
259
|
if (index > 0) cards.push(card);
|
|
230
260
|
await this.#updateStreamCard(card, chunk);
|
|
231
261
|
await this.#finishStreamCard(card);
|
|
232
262
|
}
|
|
233
263
|
return {
|
|
234
|
-
messageId:
|
|
264
|
+
messageId: cards[0].messageId,
|
|
235
265
|
providerMessageIds: cards.map((card) => card.messageId),
|
|
236
266
|
};
|
|
237
267
|
} catch (error) {
|
|
@@ -85,7 +85,7 @@ function helpText() {
|
|
|
85
85
|
t('/new 开启一个全新会话'),
|
|
86
86
|
t('/compact 压缩当前会话的较早上下文'),
|
|
87
87
|
t('/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)'),
|
|
88
|
-
t('/workspace
|
|
88
|
+
t('/workspace 工作区序号或绝对路径 切换工作区'),
|
|
89
89
|
t('/workspacelist 列出工作区绝对路径'),
|
|
90
90
|
t('/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题'),
|
|
91
91
|
t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
|
|
@@ -840,12 +840,15 @@ export class QqHarnessBridge {
|
|
|
840
840
|
? await promptContentForMessage(promptMessage, { signal: this.#signal })
|
|
841
841
|
: undefined;
|
|
842
842
|
const snapshot = this.#acceptedMessageIds.get(messageId);
|
|
843
|
+
let contextEnhanced = false;
|
|
843
844
|
if (snapshot) {
|
|
844
|
-
|
|
845
|
+
const originalContent = content ?? text;
|
|
846
|
+
content = enhanceContextContent(originalContent, snapshot, () => ({
|
|
845
847
|
channel: 'qq',
|
|
846
848
|
senderId: sender,
|
|
847
849
|
senderName: message.kind === 'group' ? message.senderName : undefined,
|
|
848
850
|
}));
|
|
851
|
+
contextEnhanced = content !== originalContent;
|
|
849
852
|
}
|
|
850
853
|
// QQ stream_messages can acknowledge a final frame without rendering it in
|
|
851
854
|
// some C2C clients. Standard Markdown delivery is the reliable reply path.
|
|
@@ -860,7 +863,9 @@ export class QqHarnessBridge {
|
|
|
860
863
|
harness: this.#harness,
|
|
861
864
|
state: this.#state,
|
|
862
865
|
key,
|
|
863
|
-
|
|
866
|
+
text,
|
|
867
|
+
content,
|
|
868
|
+
contextEnhanced,
|
|
864
869
|
createOptions: { signal: this.#signal },
|
|
865
870
|
existsOptions: { signal: this.#signal },
|
|
866
871
|
askOptions: {
|
|
@@ -1211,6 +1211,11 @@ export function createBotWorkspaceScope(
|
|
|
1211
1211
|
readHistory(...args) {
|
|
1212
1212
|
return invokeCurrentSession('readSessionHistory', args, 'history read');
|
|
1213
1213
|
},
|
|
1214
|
+
...(typeof target.renameSession === 'function' ? {
|
|
1215
|
+
renameTitle(...args) {
|
|
1216
|
+
return invokeStartedSessionMutation('renameSession', args, 'title rename');
|
|
1217
|
+
},
|
|
1218
|
+
} : {}),
|
|
1214
1219
|
selectModel(...args) {
|
|
1215
1220
|
return invokeCurrentSession('selectSessionModel', args, 'model selection');
|
|
1216
1221
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Shared by the Host and settings UI; keep this module browser-compatible.
|
|
2
2
|
export const CONTEXT_ENHANCEMENT_FIELDS = Object.freeze([
|
|
3
|
-
'channel', 'conversationType', 'senderId', 'senderName', 'botId',
|
|
3
|
+
'channel', 'conversationType', 'senderId', 'senderName', 'conversationTitle', 'botId',
|
|
4
4
|
]);
|
|
5
5
|
|
|
6
6
|
export const CONTEXT_ENHANCEMENT_GUIDANCE_MAX_LENGTH = 8_000;
|
|
@@ -36,7 +36,10 @@ const CHANNELS = new Set([
|
|
|
36
36
|
'wecom', 'weixin', 'feishu', 'dingtalk', 'qq',
|
|
37
37
|
'slack', 'telegram', 'discord', 'whatsapp',
|
|
38
38
|
]);
|
|
39
|
-
const SOURCE_LIMITS = {
|
|
39
|
+
const SOURCE_LIMITS = {
|
|
40
|
+
channel: 16, conversationType: 6, senderId: 256, senderName: 256,
|
|
41
|
+
conversationTitle: 256, botId: 128,
|
|
42
|
+
};
|
|
40
43
|
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g;
|
|
41
44
|
|
|
42
45
|
function invalidConfig(message) {
|
|
@@ -61,7 +64,7 @@ function validateContextEnhancementScope(input) {
|
|
|
61
64
|
throw invalidConfig('群聊和私聊开关必须是布尔值。');
|
|
62
65
|
}
|
|
63
66
|
if (!Array.isArray(fields) || ![...fields].every((field) => CONTEXT_ENHANCEMENT_FIELDS.includes(field))) {
|
|
64
|
-
throw invalidConfig('
|
|
67
|
+
throw invalidConfig('来源字段只能选择已定义的六个字段。');
|
|
65
68
|
}
|
|
66
69
|
if (typeof guidance !== 'string' || guidance.length > CONTEXT_ENHANCEMENT_GUIDANCE_MAX_LENGTH) {
|
|
67
70
|
throw invalidConfig(`增强提示词不得超过 ${CONTEXT_ENHANCEMENT_GUIDANCE_MAX_LENGTH} 个字符。`);
|
|
@@ -146,7 +149,9 @@ function sourceString(value, field) {
|
|
|
146
149
|
|
|
147
150
|
function sourceBlock(snapshot, sourceFactory) {
|
|
148
151
|
const { fields } = snapshot.config;
|
|
149
|
-
const needsSource = fields.some((field) => [
|
|
152
|
+
const needsSource = fields.some((field) => [
|
|
153
|
+
'channel', 'senderId', 'senderName', 'conversationTitle',
|
|
154
|
+
].includes(field));
|
|
150
155
|
const source = needsSource ? sourceFactory?.() : null;
|
|
151
156
|
const projected = {};
|
|
152
157
|
for (const field of fields) {
|
|
@@ -911,6 +911,12 @@ export class HarnessClient {
|
|
|
911
911
|
return created.sessionId;
|
|
912
912
|
}
|
|
913
913
|
|
|
914
|
+
async renameSession(sessionId, title, options = {}) {
|
|
915
|
+
if (typeof sessionId !== 'string' || !sessionId) throw new TypeError('sessionId is required');
|
|
916
|
+
if (typeof title !== 'string' || !title.trim()) throw new TypeError('session title is required');
|
|
917
|
+
return this.rpc('session.rename', { sessionId, title }, 30_000, options);
|
|
918
|
+
}
|
|
919
|
+
|
|
914
920
|
async executeCommand(sessionId, line, options = {}) {
|
|
915
921
|
if (typeof sessionId !== 'string' || !sessionId) throw new TypeError('sessionId is required');
|
|
916
922
|
if (typeof line !== 'string' || !line) throw new TypeError('command line is required');
|
|
@@ -227,7 +227,6 @@ export default {
|
|
|
227
227
|
'/sessionlist or /sessions List workspace sessions',
|
|
228
228
|
'/session ID 绑定已有会话': '/session ID Bind an existing session',
|
|
229
229
|
'/workspacelist 列出工作区': '/workspacelist List workspaces',
|
|
230
|
-
'/workspace 路径 切换工作区': '/workspace PATH Switch workspace',
|
|
231
230
|
'/new 开启全新会话': '/new Start a new session',
|
|
232
231
|
'📊 状态 / 压缩': '📊 Status / compact',
|
|
233
232
|
'/status 连接状态': '/status Connection status',
|
|
@@ -247,8 +246,8 @@ export default {
|
|
|
247
246
|
'/steer 指令 给 Agent 补充指令': '/steer INSTRUCTION Steer the Agent',
|
|
248
247
|
'**📋 卡片功能**\n\n1. 会话下拉 — 切换当前绑定会话\n2. 工作区下拉 — 切换工作区\n3. 🤖 预设下拉 — 切换 Agent 预设\n4. 🧠 模型下拉 — 切换模型\n5. 🆕 新会话 — 开启全新会话\n6. 📋 会话/关注 — 查看/绑定会话,管理关注\n7. ⏹ 停止 — 停止当前任务\n8. 📐 压缩 — 压缩当前会话上下文\n9. 补充指令 — 给 Agent 发送指令\n10. 🗄 归档切换 — 显示/隐藏归档会话\n11. 📊 状态 — 查看系统连接状态\n12. 📖 帮助 — 查看本帮助':
|
|
249
248
|
'**📋 Card features**\n\n1. Session dropdown — switch the bound session\n2. Workspace dropdown — switch workspace\n3. 🤖 Preset dropdown — switch Agent Preset\n4. 🧠 Model dropdown — switch model\n5. 🆕 New session — start fresh\n6. 📋 Sessions/watches — view or bind sessions and manage watches\n7. ⏹ Stop — stop the current task\n8. 📐 Compact — compact the current session context\n9. Steer task — send an instruction to the Agent\n10. 🗄 Archived toggle — show or hide archived sessions\n11. 📊 Status — view connection status\n12. 📖 Help — view this help',
|
|
250
|
-
'**⌨️ 文本命令**\n\n`/m` — 打开菜单卡片\n`/new` — 开启全新会话\n`/session ID` — 绑定已有会话\n`/sessionlist [工作区]` 或 `/sessions [工作区]` — 列出会话\n`/workspace
|
|
251
|
-
'**⌨️ Text commands**\n\n`/m` — open the menu card\n`/new` — start a new session\n`/session ID` — bind an existing session\n`/sessionlist [workspace]` or `/sessions [workspace]` — list sessions\n`/workspace
|
|
249
|
+
'**⌨️ 文本命令**\n\n`/m` — 打开菜单卡片\n`/new` — 开启全新会话\n`/session ID` — 绑定已有会话\n`/sessionlist [工作区]` 或 `/sessions [工作区]` — 列出会话\n`/workspace 工作区序号或绝对路径` — 切换工作区\n`/workspacelist` — 列出工作区\n`/status` — 查看连接状态\n`/compact` — 压缩上下文\n`/stop` — 停止当前任务\n`/steer 指令` — 补充指令\n`/watch ID` — 关注会话\n`/watchlist` — 关注列表\n`/unwatch ID` — 取消关注\n`/archived on/off` — 归档显隐\n`/presetlist` 或 `/presets` — 列出预设\n`/preset [序号/ID]` — 切换预设\n`/preset --default` — 跟随默认\n`/models` — 列出模型\n`/reasoninglist` 或 `/reasonings` — 按序号列出当前模型可用推理等级\n`/reasoning [序号、等级ID或 --default]` — 查看或切换当前推理等级\n`/model [序号或完整模型ID] [推理等级ID]` — 查看或切换当前会话模型\n`/batch` — 开启批量输入(仅私聊,最多 10 条文字)\n`/send` — 提交当前批次\n`/cancel` — 取消当前批次\n`/repair` — 补全飞书权限与卡片回调':
|
|
250
|
+
'**⌨️ Text commands**\n\n`/m` — open the menu card\n`/new` — start a new session\n`/session ID` — bind an existing session\n`/sessionlist [workspace]` or `/sessions [workspace]` — list sessions\n`/workspace <workspace index or absolute path>` — switch workspace\n`/workspacelist` — list workspaces\n`/status` — view connection status\n`/compact` — compact context\n`/stop` — stop the current task\n`/steer INSTRUCTION` — steer the task\n`/watch ID` — watch a session\n`/watchlist` — list watched sessions\n`/unwatch ID` — stop watching\n`/archived on/off` — show or hide archived sessions\n`/presetlist` or `/presets` — list presets\n`/preset [index/ID]` — switch preset\n`/preset --default` — follow default\n`/models` — list models\n`/reasoninglist` or `/reasonings` — list reasoning efforts for the current model\n`/reasoning [index, effort ID, or --default]` — show or switch reasoning effort\n`/model [index or full model ID] [reasoning effort ID]` — show or switch the current Session model\n`/batch` — start batch input (direct messages only, up to 10 text messages)\n`/send` — submit the current batch\n`/cancel` — cancel the current batch\n`/repair` — complete Feishu permissions and the card callback',
|
|
252
251
|
'**💡 数字兜底**\n回复数字快速操作:\n**1**工作区列表 · **2**新会话 · **3**会话/关注\n**4**状态 · **5**补全权限 · **6**帮助':
|
|
253
252
|
'**💡 Number fallback**\nReply with a number for a quick action:\n**1** Workspace list · **2** New session · **3** Sessions/watches\n**4** Status · **5** Complete permissions · **6** Help',
|
|
254
253
|
'从下方下拉选择补充指令;最后一项可自定义输入。':
|
|
@@ -338,6 +337,7 @@ export default {
|
|
|
338
337
|
|
|
339
338
|
// feishu/feishu-channel.mjs
|
|
340
339
|
'正在生成…': 'Generating…',
|
|
340
|
+
'⤵️ 最终结果见下方': '⤵️ Final result below',
|
|
341
341
|
'回答完成': 'Answer complete',
|
|
342
342
|
'内容较长,生成完成后将分段发送完整回答。':
|
|
343
343
|
'This response is long. The complete answer will be sent in parts when generation finishes.',
|
|
@@ -120,7 +120,8 @@ export default {
|
|
|
120
120
|
'{label}机器人已连接 DeepSeek Harness。': 'The {label} bot is connected to DeepSeek Harness.',
|
|
121
121
|
'/new 开启一个全新会话': '/new Start a brand-new session',
|
|
122
122
|
'/compact 压缩当前会话的较早上下文': '/compact Compact the earlier context of the current session',
|
|
123
|
-
'/workspace
|
|
123
|
+
'/workspace 工作区序号或绝对路径 切换工作区':
|
|
124
|
+
'/workspace <workspace index or absolute path> Switch workspace',
|
|
124
125
|
'/workspacelist 列出工作区绝对路径': '/workspacelist List absolute workspace paths',
|
|
125
126
|
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题':
|
|
126
127
|
'/sessionlist [workspace index or absolute path] List session IDs and titles',
|
|
@@ -17,7 +17,8 @@ export default {
|
|
|
17
17
|
'当前 Harness Host 上存在的工作区({count}):':
|
|
18
18
|
'Workspaces on the current Harness Host ({count}):',
|
|
19
19
|
'(当前)': '(current)',
|
|
20
|
-
'切换用法:/workspace
|
|
20
|
+
'切换用法:/workspace 工作区序号或绝对路径':
|
|
21
|
+
'To switch: /workspace Workspace index or absolute path',
|
|
21
22
|
'查看会话:/sessionlist 工作区序号或绝对路径':
|
|
22
23
|
'To view Sessions: /sessionlist Workspace index or absolute path',
|
|
23
24
|
'机器人正在移除或已重新接入,无法列出原会话的工作区。':
|
|
@@ -80,10 +81,12 @@ export default {
|
|
|
80
81
|
'归档:{archived}': 'Archived: {archived}',
|
|
81
82
|
'是': 'Yes',
|
|
82
83
|
'否': 'No',
|
|
83
|
-
'用法:/workspace
|
|
84
|
+
'用法:/workspace 工作区序号或绝对路径':
|
|
85
|
+
'Usage: /workspace Workspace index or absolute path',
|
|
84
86
|
'当前机器人暂不支持切换工作区。': 'This bot does not support switching Workspaces yet.',
|
|
85
87
|
'工作区已切换为:{workspace}': 'Workspace switched to: {workspace}',
|
|
86
|
-
'{message}\n用法:/workspace
|
|
88
|
+
'{message}\n用法:/workspace 工作区序号或绝对路径':
|
|
89
|
+
'{message}\nUsage: /workspace Workspace index or absolute path',
|
|
87
90
|
'机器人正在移除或已重新接入,无法切换原会话的工作区。':
|
|
88
91
|
'The bot is being removed or has been reconnected; cannot switch the Workspace of the original Session.',
|
|
89
92
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const DEFAULT_MAX_TITLE_BYTES = 60;
|
|
2
|
+
|
|
3
|
+
const OSC_SEQUENCE = /(?:\u001b\]|\u009d)(?:(?!\u0007|\u001b\\)[\s\S])*(?:\u0007|\u001b\\|$)/gu;
|
|
4
|
+
const CSI_SEQUENCE = /(?:\u001b\[|\u009b)[0-?]*[ -/]*[@-~]/gu;
|
|
5
|
+
const ESC_SEQUENCE = /\u001b[@-_]/gu;
|
|
6
|
+
const CONTROL_CHARACTER = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/gu;
|
|
7
|
+
const DIRECTIONAL_CONTROL = /[\u200b\u200e\u200f\u202a-\u202e\u2060-\u2064\u2066-\u206f\ufeff]/gu;
|
|
8
|
+
const INJECTED_CONTEXT_PREFIX = /^(?:<dsh_im_source>[\s\S]*?<\/dsh_im_source>\s*)?(?:<dsh_im_source_guidance>[\s\S]*?<\/dsh_im_source_guidance>\s*)?/u;
|
|
9
|
+
const SEGMENTER = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
10
|
+
|
|
11
|
+
function cleanTitleText(input) {
|
|
12
|
+
return input
|
|
13
|
+
.replace(OSC_SEQUENCE, '')
|
|
14
|
+
.replace(CSI_SEQUENCE, '')
|
|
15
|
+
.replace(ESC_SEQUENCE, '')
|
|
16
|
+
.replace(CONTROL_CHARACTER, '')
|
|
17
|
+
.replace(DIRECTIONAL_CONTROL, '')
|
|
18
|
+
.replace(/\s+/gu, ' ')
|
|
19
|
+
.trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function truncateTitle(input, maxBytes = DEFAULT_MAX_TITLE_BYTES) {
|
|
23
|
+
if (Buffer.byteLength(input, 'utf8') <= maxBytes) return input;
|
|
24
|
+
const suffix = '…';
|
|
25
|
+
const budget = maxBytes - Buffer.byteLength(suffix, 'utf8');
|
|
26
|
+
let output = '';
|
|
27
|
+
for (const { segment } of SEGMENTER.segment(input)) {
|
|
28
|
+
if (Buffer.byteLength(output + segment, 'utf8') > budget) break;
|
|
29
|
+
output += segment;
|
|
30
|
+
}
|
|
31
|
+
return `${output.trimEnd()}${suffix}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function contentText(content) {
|
|
35
|
+
if (typeof content === 'string') return content;
|
|
36
|
+
if (!Array.isArray(content)) return '';
|
|
37
|
+
return content
|
|
38
|
+
.filter((block) => block?.type === 'text' && typeof block.text === 'string')
|
|
39
|
+
.map((block) => block.text)
|
|
40
|
+
.join('\n');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function fileDisplayName(file) {
|
|
44
|
+
const value = file?.name ?? file?.filename ?? file?.fileName;
|
|
45
|
+
if (typeof value !== 'string') return '';
|
|
46
|
+
return value
|
|
47
|
+
.replaceAll('\\', '/')
|
|
48
|
+
.split('/')
|
|
49
|
+
.at(-1)
|
|
50
|
+
?.replace(/[\u0000-\u001f\u007f]/gu, '')
|
|
51
|
+
.trim() ?? '';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Build a deterministic title from the unenhanced first user message. */
|
|
55
|
+
export function initialSessionTitle({ text, content, files } = {}) {
|
|
56
|
+
const original = typeof text === 'string' ? cleanTitleText(text) : '';
|
|
57
|
+
if (original) return truncateTitle(original);
|
|
58
|
+
|
|
59
|
+
// Structured image prompts may only expose their default text through content.
|
|
60
|
+
// Strip only the leading blocks inserted by dsh-im; matching tags later in user
|
|
61
|
+
// content remain ordinary user text.
|
|
62
|
+
const visibleContent = cleanTitleText(
|
|
63
|
+
contentText(content).replace(INJECTED_CONTEXT_PREFIX, ''),
|
|
64
|
+
);
|
|
65
|
+
if (visibleContent) return truncateTitle(visibleContent);
|
|
66
|
+
|
|
67
|
+
const fileName = Array.isArray(files)
|
|
68
|
+
? files.map(fileDisplayName).find(Boolean)
|
|
69
|
+
: '';
|
|
70
|
+
const cleanFileName = fileName ? cleanTitleText(fileName) : '';
|
|
71
|
+
return cleanFileName ? truncateTitle(cleanFileName) : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const INITIAL_SESSION_TITLE_MAX_BYTES = DEFAULT_MAX_TITLE_BYTES;
|
|
@@ -588,7 +588,7 @@ export class TextHarnessBridge {
|
|
|
588
588
|
t('/new 开启一个全新会话'),
|
|
589
589
|
t('/compact 压缩当前会话的较早上下文'),
|
|
590
590
|
t('/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)'),
|
|
591
|
-
t('/workspace
|
|
591
|
+
t('/workspace 工作区序号或绝对路径 切换工作区'),
|
|
592
592
|
t('/workspacelist 列出工作区绝对路径'),
|
|
593
593
|
t('/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题'),
|
|
594
594
|
t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
|
|
@@ -673,12 +673,16 @@ export class TextHarnessBridge {
|
|
|
673
673
|
? await promptContentForMessage(message, { signal: this.#signal })
|
|
674
674
|
: undefined;
|
|
675
675
|
const snapshot = this.#acceptedMessageIds.get(messageId);
|
|
676
|
+
let contextEnhanced = false;
|
|
676
677
|
if (snapshot) {
|
|
677
|
-
|
|
678
|
+
const originalContent = content ?? text;
|
|
679
|
+
content = enhanceContextContent(originalContent, snapshot, () => ({
|
|
678
680
|
channel: this.#descriptor.key,
|
|
679
681
|
senderId,
|
|
680
682
|
senderName: message.contextSource?.()?.senderName,
|
|
683
|
+
conversationTitle: message.contextSource?.()?.conversationTitle,
|
|
681
684
|
}));
|
|
685
|
+
contextEnhanced = content !== originalContent;
|
|
682
686
|
}
|
|
683
687
|
const { answer, artifacts = [] } = await askInWorkspaceSession({
|
|
684
688
|
harness: this.#harness,
|
|
@@ -686,6 +690,7 @@ export class TextHarnessBridge {
|
|
|
686
690
|
key: conversationKey,
|
|
687
691
|
text,
|
|
688
692
|
content,
|
|
693
|
+
contextEnhanced,
|
|
689
694
|
createOptions: this.#signal ? { signal: this.#signal } : undefined,
|
|
690
695
|
existsOptions: this.#signal ? { signal: this.#signal } : undefined,
|
|
691
696
|
askOptions: {
|
|
@@ -142,7 +142,7 @@ async function runWorkspaceListCommand(match, harness) {
|
|
|
142
142
|
`${index + 1}. ${workspace}${workspace === current ? t('(当前)') : ''}`
|
|
143
143
|
)),
|
|
144
144
|
'',
|
|
145
|
-
t('切换用法:/workspace
|
|
145
|
+
t('切换用法:/workspace 工作区序号或绝对路径'),
|
|
146
146
|
t('查看会话:/sessionlist 工作区序号或绝对路径'),
|
|
147
147
|
];
|
|
148
148
|
const message = lines.join('\n');
|
|
@@ -366,18 +366,30 @@ export async function runWorkspaceCommand(text, harness, conversationKey) {
|
|
|
366
366
|
if (!match) return null;
|
|
367
367
|
const workspace = match[1]?.trim();
|
|
368
368
|
if (!workspace) {
|
|
369
|
-
return commandResult(t('用法:/workspace
|
|
369
|
+
return commandResult(t('用法:/workspace 工作区序号或绝对路径'));
|
|
370
370
|
}
|
|
371
371
|
if (typeof harness?.switchWorkspace !== 'function') {
|
|
372
372
|
return commandResult(t('当前机器人暂不支持切换工作区。'));
|
|
373
373
|
}
|
|
374
374
|
try {
|
|
375
|
-
|
|
375
|
+
let selected = workspace;
|
|
376
|
+
if (/^\d+$/u.test(workspace)) {
|
|
377
|
+
if (typeof harness?.listWorkspaces !== 'function') {
|
|
378
|
+
return commandResult(t('当前机器人暂不支持按序号选择工作区。'));
|
|
379
|
+
}
|
|
380
|
+
const { paths } = await workspacePathSnapshot(harness);
|
|
381
|
+
const position = Number(workspace);
|
|
382
|
+
if (!Number.isSafeInteger(position) || position < 1 || position > paths.length) {
|
|
383
|
+
return commandResult(t('工作区序号不存在,请先执行 /workspacelist。'));
|
|
384
|
+
}
|
|
385
|
+
selected = paths[position - 1];
|
|
386
|
+
}
|
|
387
|
+
const current = await harness.switchWorkspace(selected);
|
|
376
388
|
return commandResult(t('工作区已切换为:{workspace}', { workspace: current }));
|
|
377
389
|
} catch (error) {
|
|
378
390
|
if (['workspace-not-absolute', 'workspace-not-found', 'workspace-not-directory'].includes(error?.code)) {
|
|
379
391
|
return commandResult(t(`{message}
|
|
380
|
-
用法:/workspace
|
|
392
|
+
用法:/workspace 工作区序号或绝对路径`, { message: error.message }));
|
|
381
393
|
}
|
|
382
394
|
if (error?.code === 'workspace-bot-not-found') {
|
|
383
395
|
return commandResult(t('机器人正在移除或已重新接入,无法切换原会话的工作区。'));
|