@xmanrui/dsh-im 4.7.0 → 4.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmanrui/dsh-im",
3
- "version": "4.7.0",
3
+ "version": "4.8.0",
4
4
  "description": "把九种 IM 机器人和公网 AI Office 接入本机 DeepSeek Harness。 Connect nine IM channels and a public AI Office to a local DeepSeek Harness.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -34,6 +34,10 @@
34
34
  {
35
35
  "name": "divingleee",
36
36
  "url": "https://github.com/divingleee"
37
+ },
38
+ {
39
+ "name": "Chan-0312",
40
+ "url": "https://github.com/Chan-0312"
37
41
  }
38
42
  ],
39
43
  "license": "MIT",
@@ -17,6 +17,8 @@ const FIELD_LABELS = Object.freeze({
17
17
  senderId: '发送者标识',
18
18
  senderName: '发送者昵称',
19
19
  conversationTitle: '会话标题',
20
+ chatId: '会话标识',
21
+ threadId: '话题标识',
20
22
  botId: '机器人标识',
21
23
  });
22
24
 
@@ -29,6 +31,14 @@ const FIELD_HELP = Object.freeze({
29
31
  labelKey: 'conversationTitleHelpLabel',
30
32
  text: '该字段不是每个渠道都能提供。钉钉群聊会带上群名。当前消息没有会话标题时,即使已选择该字段,<dsh_im_source> 中也会省略 conversationTitle。',
31
33
  }),
34
+ chatId: Object.freeze({
35
+ labelKey: 'chatIdHelpLabel',
36
+ text: '该字段不是每个渠道都能提供。会话标识用于区分不同的群组或私聊,飞书群聊会带上群 ID。当前消息没有会话标识时,即使已选择该字段,<dsh_im_source> 中也会省略 chatId。',
37
+ }),
38
+ threadId: Object.freeze({
39
+ labelKey: 'threadIdHelpLabel',
40
+ text: '该字段不是每个渠道都能提供。飞书话题群的消息会带上话题 ID,用于区分同一群组内的不同话题;当前消息不在话题中时,即使已选择该字段,<dsh_im_source> 中也会省略 threadId。',
41
+ }),
32
42
  });
33
43
 
34
44
  const SCOPE_COPY = Object.freeze({
@@ -38,6 +48,8 @@ const SCOPE_COPY = Object.freeze({
38
48
  fieldsHelpLabel: '查看群聊来源字段说明',
39
49
  senderNameHelpLabel: '查看群聊发送者昵称字段说明',
40
50
  conversationTitleHelpLabel: '查看群聊会话标题字段说明',
51
+ chatIdHelpLabel: '查看群聊会话标识字段说明',
52
+ threadIdHelpLabel: '查看群聊话题标识字段说明',
41
53
  guidanceLabel: '增强提示词',
42
54
  guidanceHelpLabel: '查看群聊增强提示词使用说明',
43
55
  guidanceUsage: '用于告诉模型如何使用当前群聊消息的 <dsh_im_source> 来源字段。只填写正文,插件会自动添加 <dsh_im_source_guidance> 成对标签。',
@@ -49,6 +61,8 @@ const SCOPE_COPY = Object.freeze({
49
61
  fieldsHelpLabel: '查看私聊来源字段说明',
50
62
  senderNameHelpLabel: '查看私聊发送者昵称字段说明',
51
63
  conversationTitleHelpLabel: '查看私聊会话标题字段说明',
64
+ chatIdHelpLabel: '查看私聊会话标识字段说明',
65
+ threadIdHelpLabel: '查看私聊话题标识字段说明',
52
66
  guidanceLabel: '增强提示词',
53
67
  guidanceHelpLabel: '查看私聊增强提示词使用说明',
54
68
  guidanceUsage: '用于告诉模型如何使用当前私聊消息的 <dsh_im_source> 来源字段。只填写正文,插件会自动添加 <dsh_im_source_guidance> 成对标签。',
@@ -292,6 +292,14 @@ const EN = Object.freeze({
292
292
  '查看群聊会话标题字段说明': 'View group conversation title field details',
293
293
  '查看私聊会话标题字段说明': 'View direct conversation title field details',
294
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.',
295
+ '会话标识': 'Chat ID',
296
+ '查看群聊会话标识字段说明': 'View group chat ID field details',
297
+ '查看私聊会话标识字段说明': 'View direct chat ID field details',
298
+ '该字段不是每个渠道都能提供。会话标识用于区分不同的群组或私聊,飞书群聊会带上群 ID。当前消息没有会话标识时,即使已选择该字段,<dsh_im_source> 中也会省略 chatId。': 'This field is not available on every channel. The chat ID distinguishes different groups or direct chats, and Feishu group chats include the group ID. If the current message has no chat ID, <dsh_im_source> omits chatId even when the field is selected.',
299
+ '话题标识': 'Topic ID',
300
+ '查看群聊话题标识字段说明': 'View group topic ID field details',
301
+ '查看私聊话题标识字段说明': 'View direct topic ID field details',
302
+ '该字段不是每个渠道都能提供。飞书话题群的消息会带上话题 ID,用于区分同一群组内的不同话题;当前消息不在话题中时,即使已选择该字段,<dsh_im_source> 中也会省略 threadId。': 'This field is not available on every channel. Messages in a Feishu topic chat carry the topic ID, which distinguishes different topics inside the same group. If the current message is not inside a topic, <dsh_im_source> omits threadId even when the field is selected.',
295
303
  '机器人标识': 'Bot ID',
296
304
  '增强提示词': 'Guidance',
297
305
  '查看增强提示词使用说明': 'View guidance instructions',
@@ -319,7 +327,7 @@ const EN = Object.freeze({
319
327
  '上下文增强保存失败,请重试。': 'Could not save context enhancement. Try again.',
320
328
  '请提交完整的上下文增强设置。': 'Submit the complete context enhancement settings.',
321
329
  '群聊和私聊开关必须是布尔值。': 'Group and direct switches must be booleans.',
322
- '来源字段只能选择已定义的六个字段。': 'Source fields must be chosen from the six defined fields.',
330
+ '来源字段只能选择已定义的八个字段。': 'Source fields must be chosen from the eight defined fields.',
323
331
  '保存中…': 'Saving…',
324
332
  '未设置': 'Not set',
325
333
  '工作区修改失败,请重试。': 'Could not update the workspace. Try again.',
@@ -7,6 +7,17 @@ function pickerErrorCode(error) {
7
7
  return error?.rpcError?.code ?? error?.code;
8
8
  }
9
9
 
10
+ const PICKER_ERROR_KINDS = new Map([
11
+ ['directory-picker/unavailable', 'unavailable'],
12
+ ['directory-picker-unavailable', 'unavailable'],
13
+ ['directory-picker/unreadable', 'unreadable'],
14
+ ['directory-unreadable', 'unreadable'],
15
+ ]);
16
+
17
+ function pickerErrorKind(error) {
18
+ return PICKER_ERROR_KINDS.get(pickerErrorCode(error));
19
+ }
20
+
10
21
  function pickerErrorDetails(error) {
11
22
  return error?.rpcError?.details ?? error?.details;
12
23
  }
@@ -110,9 +121,9 @@ export function WorkspaceDirectoryPicker({
110
121
  const initialPath = initialPathRef.current;
111
122
  const initial = await loadDirectory(initialPath || undefined, { reportError: false });
112
123
  if (!active || initial.aborted || initial.value) return;
113
- const code = pickerErrorCode(initial.error);
124
+ const kind = pickerErrorKind(initial.error);
114
125
  const details = pickerErrorDetails(initial.error);
115
- if (code === 'directory-picker-unavailable'
126
+ if (kind === 'unavailable'
116
127
  && details?.capability === 'native'
117
128
  && typeof picker.pickDirectory === 'function') {
118
129
  setLoading(true);
@@ -128,7 +139,7 @@ export function WorkspaceDirectoryPicker({
128
139
  }
129
140
  return;
130
141
  }
131
- if (initialPath && code === 'directory-unreadable') {
142
+ if (initialPath && kind === 'unreadable') {
132
143
  const home = await loadDirectory(undefined, { reportError: false });
133
144
  if (!active || home.aborted || home.value) return;
134
145
  setError(pickerErrorMessage(home.error));
@@ -1136,6 +1136,7 @@ export class DingtalkHarnessBridge {
1136
1136
  senderId: sender,
1137
1137
  senderName: message.senderNick,
1138
1138
  conversationTitle: message.conversationTitle,
1139
+ chatId: message.conversationId,
1139
1140
  }));
1140
1141
  contextEnhanced = content !== originalContent;
1141
1142
  }
@@ -3330,6 +3330,8 @@ export class FeishuHarnessBridge {
3330
3330
  content = enhanceContextContent(originalContent, snapshot, () => ({
3331
3331
  channel: 'feishu',
3332
3332
  senderId: senderOpenId(event),
3333
+ chatId: event.message.chat_id,
3334
+ threadId: event.message.thread_id,
3333
3335
  }));
3334
3336
  contextEnhanced = content !== originalContent;
3335
3337
  }
@@ -890,6 +890,7 @@ export class QqHarnessBridge {
890
890
  channel: 'qq',
891
891
  senderId: sender,
892
892
  senderName: message.kind === 'group' ? message.senderName : undefined,
893
+ chatId: message.kind === 'group' ? message.groupOpenid : message.senderId,
893
894
  }));
894
895
  contextEnhanced = content !== originalContent;
895
896
  }
@@ -1,6 +1,7 @@
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', 'conversationTitle', 'botId',
3
+ 'channel', 'conversationType', 'senderId', 'senderName', 'conversationTitle',
4
+ 'chatId', 'threadId', 'botId',
4
5
  ]);
5
6
 
6
7
  export const CONTEXT_ENHANCEMENT_GUIDANCE_MAX_LENGTH = 8_000;
@@ -38,7 +39,7 @@ const CHANNELS = new Set([
38
39
  ]);
39
40
  const SOURCE_LIMITS = {
40
41
  channel: 16, conversationType: 6, senderId: 256, senderName: 256,
41
- conversationTitle: 256, botId: 128,
42
+ conversationTitle: 256, chatId: 256, threadId: 256, botId: 128,
42
43
  };
43
44
  const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g;
44
45
 
@@ -64,7 +65,7 @@ function validateContextEnhancementScope(input) {
64
65
  throw invalidConfig('群聊和私聊开关必须是布尔值。');
65
66
  }
66
67
  if (!Array.isArray(fields) || ![...fields].every((field) => CONTEXT_ENHANCEMENT_FIELDS.includes(field))) {
67
- throw invalidConfig('来源字段只能选择已定义的六个字段。');
68
+ throw invalidConfig('来源字段只能选择已定义的八个字段。');
68
69
  }
69
70
  if (typeof guidance !== 'string' || guidance.length > CONTEXT_ENHANCEMENT_GUIDANCE_MAX_LENGTH) {
70
71
  throw invalidConfig(`增强提示词不得超过 ${CONTEXT_ENHANCEMENT_GUIDANCE_MAX_LENGTH} 个字符。`);
@@ -150,7 +151,7 @@ function sourceString(value, field) {
150
151
  function sourceBlock(snapshot, sourceFactory) {
151
152
  const { fields } = snapshot.config;
152
153
  const needsSource = fields.some((field) => [
153
- 'channel', 'senderId', 'senderName', 'conversationTitle',
154
+ 'channel', 'senderId', 'senderName', 'conversationTitle', 'chatId', 'threadId',
154
155
  ].includes(field));
155
156
  const source = needsSource ? sourceFactory?.() : null;
156
157
  const projected = {};
@@ -683,11 +683,14 @@ export class TextHarnessBridge {
683
683
  let contextEnhanced = false;
684
684
  if (snapshot) {
685
685
  const originalContent = content ?? text;
686
+ const contextSource = message.contextSource?.();
686
687
  content = enhanceContextContent(originalContent, snapshot, () => ({
687
688
  channel: this.#descriptor.key,
688
689
  senderId,
689
- senderName: message.contextSource?.()?.senderName,
690
- conversationTitle: message.contextSource?.()?.conversationTitle,
690
+ senderName: contextSource?.senderName,
691
+ conversationTitle: contextSource?.conversationTitle,
692
+ chatId: contextSource?.chatId ?? message.conversationId,
693
+ threadId: contextSource?.threadId,
691
694
  }));
692
695
  contextEnhanced = content !== originalContent;
693
696
  }
@@ -172,6 +172,10 @@ export function normalizeSlackEvent(payload, botUserId, {
172
172
  senderIsBot: String(event.user) === String(botUserId),
173
173
  kind: direct ? 'direct' : 'group',
174
174
  conversationId: direct ? String(event.channel) : `${event.channel}:${threadTs}`,
175
+ contextSource: () => ({
176
+ chatId: String(event.channel),
177
+ threadId: event.thread_ts ? String(event.thread_ts) : undefined,
178
+ }),
175
179
  content: stripBotMention(event.text ?? '', botUserId),
176
180
  plainText: !Array.isArray(event.files) || event.files.length === 0,
177
181
  images: Array.isArray(event.files)
@@ -266,6 +266,8 @@ export function normalizeTelegramUpdate(update, {
266
266
  .filter((value) => typeof value === 'string' && value.trim())
267
267
  .map((value) => value.trim()).join(' ') || message.from?.username,
268
268
  conversationTitle: direct ? undefined : message.chat?.title,
269
+ chatId: String(chatId),
270
+ threadId: messageThreadId === undefined ? undefined : String(messageThreadId),
269
271
  }),
270
272
  senderIsBot: message.from?.is_bot === true,
271
273
  kind: direct ? 'direct' : 'group',
@@ -1056,6 +1056,7 @@ export class WecomHarnessBridge {
1056
1056
  content = enhanceContextContent(originalContent, snapshot, () => ({
1057
1057
  channel: 'wecom',
1058
1058
  senderId,
1059
+ chatId,
1059
1060
  }));
1060
1061
  contextEnhanced = content !== originalContent;
1061
1062
  }
@@ -786,6 +786,7 @@ export class WeixinHarnessBridge {
786
786
  content = enhanceContextContent(originalContent, snapshot, () => ({
787
787
  channel: 'weixin',
788
788
  senderId: sender,
789
+ chatId: sender,
789
790
  }));
790
791
  contextEnhanced = content !== originalContent;
791
792
  }