mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 00:33:20 +08:00
feat(context-enhancement): 新增 chatId 与 threadId 来源字段 (#128)
Add chatId and threadId source fields across all nine IM channels, update UI/docs/tests, reconcile the latest main branch, and credit @Chan-0312 as a contributor.
This commit is contained in:
parent
3c71712962
commit
3562b9244e
19 changed files with 266 additions and 158 deletions
|
|
@ -6,6 +6,11 @@ This file records the notable changes in each dsh-im release. Its format follows
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added / 新增
|
||||
|
||||
- 上下文增强新增两个来源字段:`chatId`(会话标识,用于区分群组或私聊)与 `threadId`(话题标识,飞书话题群会带上 `thread_id`,用于区分同一群组内的不同话题)。九个渠道均可勾选;飞书群聊直接提供群 ID,Slack/Telegram 话题、Discord 频道等渠道也按各自事件补齐。仍只发送当前消息中已有的值,不查询平台 API。
|
||||
Context enhancement adds two source fields: `chatId` (the chat ID that distinguishes groups or direct chats) and `threadId` (the topic ID; Feishu topic chats carry `thread_id`, telling different topics inside the same group apart). All nine channels can select them; Feishu group chats provide the group ID directly, and Slack/Telegram topics, Discord channels, and other channels are wired per their own events. Only values already present in the current message are sent; no platform APIs are queried.
|
||||
|
||||
### Fixed / 修复
|
||||
|
||||
- 机器人工作区目录选择器现在同时识别新版 DSH 的 `directory-picker/*` 错误码与旧版连字符错误码;native 后端会正确回退到系统目录选择器,已失效的保存路径也会回退到 Host 主目录。
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ See the [Proactive Delivery Guide](PROACTIVE_DELIVERY.en.md) ([简体中文](PRO
|
|||
|
||||
### Context enhancement
|
||||
|
||||
Open **Context enhancement** on a bot card to configure separate enable switches, source fields, and guidance for group and direct chats, then **Save** to apply the complete configuration atomically. The two scopes do not share settings. Each offers `channel`, `conversationType`, `senderId`, `senderName`, and `botId`, with only `senderId` selected by default. Only values selected for the current scope and already available in the incoming message are included; no platform profile API is queried. Weixin currently supports DMs only.
|
||||
Open **Context enhancement** on a bot card to configure separate enable switches, source fields, and guidance for group and direct chats, then **Save** to apply the complete configuration atomically. The two scopes do not share settings. Each offers `channel`, `conversationType`, `senderId`, `senderName`, `conversationTitle`, `chatId`, `threadId`, and `botId`, with only `senderId` selected by default. `chatId` lets the model know which group or direct chat a message came from, and Feishu topic chats additionally carry `threadId` to tell different topics inside the same group apart. Only values selected for the current scope and already available in the incoming message are included; no platform profile API is queried. Weixin currently supports DMs only.
|
||||
|
||||
When enabled, ordinary user messages receive the current scope's `<dsh_im_source>` prefix. Nonempty guidance for that scope is automatically wrapped in `<dsh_im_source_guidance>` tags. Both guidance fields start empty and have their own instructions, example, **Use example**, and **Clear** actions. No fields selected in the current scope means no source block. Commands, approvals and question answers keep their existing control paths.
|
||||
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ dsh web
|
|||
|
||||
### 上下文增强
|
||||
|
||||
点击机器人卡片中的「上下文增强」,分别设置群聊和私聊的启用开关、来源字段与增强提示词,点击「保存」后原子生效。两个场景互不共用配置;五个可选字段均为 `channel`、`conversationType`、`senderId`、`senderName`、`botId`,各自默认只选择 `senderId`。插件只发送当前场景勾选且当前消息已有的值,不查询平台 API 补全。微信当前只支持私聊。
|
||||
点击机器人卡片中的「上下文增强」,分别设置群聊和私聊的启用开关、来源字段与增强提示词,点击「保存」后原子生效。两个场景互不共用配置;八个可选字段均为 `channel`、`conversationType`、`senderId`、`senderName`、`conversationTitle`、`chatId`、`threadId`、`botId`,各自默认只选择 `senderId`。`chatId` 用于让模型知道消息来自哪个群组或私聊,飞书话题群的消息还会带上 `threadId` 以区分同一群组内的不同话题。插件只发送当前场景勾选且当前消息已有的值,不查询平台 API 补全。微信当前只支持私聊。
|
||||
|
||||
开启后,插件在普通用户消息前附加当前场景的 `<dsh_im_source>` 来源块;当前场景非空的增强提示词自动包裹为 `<dsh_im_source_guidance>`。两个场景的提示词默认留空,并分别提供说明、示例、「填入示例」和「清空」;当前场景字段全部取消时不生成来源块。命令、审批和问题回答继续走原有控制链路。
|
||||
|
||||
|
|
|
|||
|
|
@ -605,6 +605,10 @@ var package_default = {
|
|||
{
|
||||
name: "divingleee",
|
||||
url: "https://github.com/divingleee"
|
||||
},
|
||||
{
|
||||
name: "Chan-0312",
|
||||
url: "https://github.com/Chan-0312"
|
||||
}
|
||||
],
|
||||
license: "MIT",
|
||||
|
|
@ -1126,6 +1130,14 @@ var EN = Object.freeze({
|
|||
"\u67E5\u770B\u7FA4\u804A\u4F1A\u8BDD\u6807\u9898\u5B57\u6BB5\u8BF4\u660E": "View group conversation title field details",
|
||||
"\u67E5\u770B\u79C1\u804A\u4F1A\u8BDD\u6807\u9898\u5B57\u6BB5\u8BF4\u660E": "View direct conversation title field details",
|
||||
"\u8BE5\u5B57\u6BB5\u4E0D\u662F\u6BCF\u4E2A\u6E20\u9053\u90FD\u80FD\u63D0\u4F9B\u3002\u9489\u9489\u7FA4\u804A\u4F1A\u5E26\u4E0A\u7FA4\u540D\u3002\u5F53\u524D\u6D88\u606F\u6CA1\u6709\u4F1A\u8BDD\u6807\u9898\u65F6\uFF0C\u5373\u4F7F\u5DF2\u9009\u62E9\u8BE5\u5B57\u6BB5\uFF0C<dsh_im_source> \u4E2D\u4E5F\u4F1A\u7701\u7565 conversationTitle\u3002": "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.",
|
||||
"\u4F1A\u8BDD\u6807\u8BC6": "Chat ID",
|
||||
"\u67E5\u770B\u7FA4\u804A\u4F1A\u8BDD\u6807\u8BC6\u5B57\u6BB5\u8BF4\u660E": "View group chat ID field details",
|
||||
"\u67E5\u770B\u79C1\u804A\u4F1A\u8BDD\u6807\u8BC6\u5B57\u6BB5\u8BF4\u660E": "View direct chat ID field details",
|
||||
"\u8BE5\u5B57\u6BB5\u4E0D\u662F\u6BCF\u4E2A\u6E20\u9053\u90FD\u80FD\u63D0\u4F9B\u3002\u4F1A\u8BDD\u6807\u8BC6\u7528\u4E8E\u533A\u5206\u4E0D\u540C\u7684\u7FA4\u7EC4\u6216\u79C1\u804A\uFF0C\u98DE\u4E66\u7FA4\u804A\u4F1A\u5E26\u4E0A\u7FA4 ID\u3002\u5F53\u524D\u6D88\u606F\u6CA1\u6709\u4F1A\u8BDD\u6807\u8BC6\u65F6\uFF0C\u5373\u4F7F\u5DF2\u9009\u62E9\u8BE5\u5B57\u6BB5\uFF0C<dsh_im_source> \u4E2D\u4E5F\u4F1A\u7701\u7565 chatId\u3002": "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.",
|
||||
"\u8BDD\u9898\u6807\u8BC6": "Topic ID",
|
||||
"\u67E5\u770B\u7FA4\u804A\u8BDD\u9898\u6807\u8BC6\u5B57\u6BB5\u8BF4\u660E": "View group topic ID field details",
|
||||
"\u67E5\u770B\u79C1\u804A\u8BDD\u9898\u6807\u8BC6\u5B57\u6BB5\u8BF4\u660E": "View direct topic ID field details",
|
||||
"\u8BE5\u5B57\u6BB5\u4E0D\u662F\u6BCF\u4E2A\u6E20\u9053\u90FD\u80FD\u63D0\u4F9B\u3002\u98DE\u4E66\u8BDD\u9898\u7FA4\u7684\u6D88\u606F\u4F1A\u5E26\u4E0A\u8BDD\u9898 ID\uFF0C\u7528\u4E8E\u533A\u5206\u540C\u4E00\u7FA4\u7EC4\u5185\u7684\u4E0D\u540C\u8BDD\u9898\uFF1B\u5F53\u524D\u6D88\u606F\u4E0D\u5728\u8BDD\u9898\u4E2D\u65F6\uFF0C\u5373\u4F7F\u5DF2\u9009\u62E9\u8BE5\u5B57\u6BB5\uFF0C<dsh_im_source> \u4E2D\u4E5F\u4F1A\u7701\u7565 threadId\u3002": "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.",
|
||||
"\u673A\u5668\u4EBA\u6807\u8BC6": "Bot ID",
|
||||
"\u589E\u5F3A\u63D0\u793A\u8BCD": "Guidance",
|
||||
"\u67E5\u770B\u589E\u5F3A\u63D0\u793A\u8BCD\u4F7F\u7528\u8BF4\u660E": "View guidance instructions",
|
||||
|
|
@ -1153,7 +1165,7 @@ var EN = Object.freeze({
|
|||
"\u4E0A\u4E0B\u6587\u589E\u5F3A\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\u3002": "Could not save context enhancement. Try again.",
|
||||
"\u8BF7\u63D0\u4EA4\u5B8C\u6574\u7684\u4E0A\u4E0B\u6587\u589E\u5F3A\u8BBE\u7F6E\u3002": "Submit the complete context enhancement settings.",
|
||||
"\u7FA4\u804A\u548C\u79C1\u804A\u5F00\u5173\u5FC5\u987B\u662F\u5E03\u5C14\u503C\u3002": "Group and direct switches must be booleans.",
|
||||
"\u6765\u6E90\u5B57\u6BB5\u53EA\u80FD\u9009\u62E9\u5DF2\u5B9A\u4E49\u7684\u516D\u4E2A\u5B57\u6BB5\u3002": "Source fields must be chosen from the six defined fields.",
|
||||
"\u6765\u6E90\u5B57\u6BB5\u53EA\u80FD\u9009\u62E9\u5DF2\u5B9A\u4E49\u7684\u516B\u4E2A\u5B57\u6BB5\u3002": "Source fields must be chosen from the eight defined fields.",
|
||||
"\u4FDD\u5B58\u4E2D\u2026": "Saving\u2026",
|
||||
"\u672A\u8BBE\u7F6E": "Not set",
|
||||
"\u5DE5\u4F5C\u533A\u4FEE\u6539\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\u3002": "Could not update the workspace. Try again.",
|
||||
|
|
@ -2080,6 +2092,8 @@ var CONTEXT_ENHANCEMENT_FIELDS = Object.freeze([
|
|||
"senderId",
|
||||
"senderName",
|
||||
"conversationTitle",
|
||||
"chatId",
|
||||
"threadId",
|
||||
"botId"
|
||||
]);
|
||||
var CONTEXT_ENHANCEMENT_GUIDANCE_MAX_LENGTH = 8e3;
|
||||
|
|
@ -2119,7 +2133,7 @@ function validateContextEnhancementScope(input) {
|
|||
throw invalidConfig("\u7FA4\u804A\u548C\u79C1\u804A\u5F00\u5173\u5FC5\u987B\u662F\u5E03\u5C14\u503C\u3002");
|
||||
}
|
||||
if (!Array.isArray(fields) || ![...fields].every((field) => CONTEXT_ENHANCEMENT_FIELDS.includes(field))) {
|
||||
throw invalidConfig("\u6765\u6E90\u5B57\u6BB5\u53EA\u80FD\u9009\u62E9\u5DF2\u5B9A\u4E49\u7684\u516D\u4E2A\u5B57\u6BB5\u3002");
|
||||
throw invalidConfig("\u6765\u6E90\u5B57\u6BB5\u53EA\u80FD\u9009\u62E9\u5DF2\u5B9A\u4E49\u7684\u516B\u4E2A\u5B57\u6BB5\u3002");
|
||||
}
|
||||
if (typeof guidance !== "string" || guidance.length > CONTEXT_ENHANCEMENT_GUIDANCE_MAX_LENGTH) {
|
||||
throw invalidConfig(`\u589E\u5F3A\u63D0\u793A\u8BCD\u4E0D\u5F97\u8D85\u8FC7 ${CONTEXT_ENHANCEMENT_GUIDANCE_MAX_LENGTH} \u4E2A\u5B57\u7B26\u3002`);
|
||||
|
|
@ -2910,6 +2924,8 @@ var FIELD_LABELS = Object.freeze({
|
|||
senderId: "\u53D1\u9001\u8005\u6807\u8BC6",
|
||||
senderName: "\u53D1\u9001\u8005\u6635\u79F0",
|
||||
conversationTitle: "\u4F1A\u8BDD\u6807\u9898",
|
||||
chatId: "\u4F1A\u8BDD\u6807\u8BC6",
|
||||
threadId: "\u8BDD\u9898\u6807\u8BC6",
|
||||
botId: "\u673A\u5668\u4EBA\u6807\u8BC6"
|
||||
});
|
||||
var FIELD_HELP = Object.freeze({
|
||||
|
|
@ -2920,6 +2936,14 @@ var FIELD_HELP = Object.freeze({
|
|||
conversationTitle: Object.freeze({
|
||||
labelKey: "conversationTitleHelpLabel",
|
||||
text: "\u8BE5\u5B57\u6BB5\u4E0D\u662F\u6BCF\u4E2A\u6E20\u9053\u90FD\u80FD\u63D0\u4F9B\u3002\u9489\u9489\u7FA4\u804A\u4F1A\u5E26\u4E0A\u7FA4\u540D\u3002\u5F53\u524D\u6D88\u606F\u6CA1\u6709\u4F1A\u8BDD\u6807\u9898\u65F6\uFF0C\u5373\u4F7F\u5DF2\u9009\u62E9\u8BE5\u5B57\u6BB5\uFF0C<dsh_im_source> \u4E2D\u4E5F\u4F1A\u7701\u7565 conversationTitle\u3002"
|
||||
}),
|
||||
chatId: Object.freeze({
|
||||
labelKey: "chatIdHelpLabel",
|
||||
text: "\u8BE5\u5B57\u6BB5\u4E0D\u662F\u6BCF\u4E2A\u6E20\u9053\u90FD\u80FD\u63D0\u4F9B\u3002\u4F1A\u8BDD\u6807\u8BC6\u7528\u4E8E\u533A\u5206\u4E0D\u540C\u7684\u7FA4\u7EC4\u6216\u79C1\u804A\uFF0C\u98DE\u4E66\u7FA4\u804A\u4F1A\u5E26\u4E0A\u7FA4 ID\u3002\u5F53\u524D\u6D88\u606F\u6CA1\u6709\u4F1A\u8BDD\u6807\u8BC6\u65F6\uFF0C\u5373\u4F7F\u5DF2\u9009\u62E9\u8BE5\u5B57\u6BB5\uFF0C<dsh_im_source> \u4E2D\u4E5F\u4F1A\u7701\u7565 chatId\u3002"
|
||||
}),
|
||||
threadId: Object.freeze({
|
||||
labelKey: "threadIdHelpLabel",
|
||||
text: "\u8BE5\u5B57\u6BB5\u4E0D\u662F\u6BCF\u4E2A\u6E20\u9053\u90FD\u80FD\u63D0\u4F9B\u3002\u98DE\u4E66\u8BDD\u9898\u7FA4\u7684\u6D88\u606F\u4F1A\u5E26\u4E0A\u8BDD\u9898 ID\uFF0C\u7528\u4E8E\u533A\u5206\u540C\u4E00\u7FA4\u7EC4\u5185\u7684\u4E0D\u540C\u8BDD\u9898\uFF1B\u5F53\u524D\u6D88\u606F\u4E0D\u5728\u8BDD\u9898\u4E2D\u65F6\uFF0C\u5373\u4F7F\u5DF2\u9009\u62E9\u8BE5\u5B57\u6BB5\uFF0C<dsh_im_source> \u4E2D\u4E5F\u4F1A\u7701\u7565 threadId\u3002"
|
||||
})
|
||||
});
|
||||
var SCOPE_COPY = Object.freeze({
|
||||
|
|
@ -2929,6 +2953,8 @@ var SCOPE_COPY = Object.freeze({
|
|||
fieldsHelpLabel: "\u67E5\u770B\u7FA4\u804A\u6765\u6E90\u5B57\u6BB5\u8BF4\u660E",
|
||||
senderNameHelpLabel: "\u67E5\u770B\u7FA4\u804A\u53D1\u9001\u8005\u6635\u79F0\u5B57\u6BB5\u8BF4\u660E",
|
||||
conversationTitleHelpLabel: "\u67E5\u770B\u7FA4\u804A\u4F1A\u8BDD\u6807\u9898\u5B57\u6BB5\u8BF4\u660E",
|
||||
chatIdHelpLabel: "\u67E5\u770B\u7FA4\u804A\u4F1A\u8BDD\u6807\u8BC6\u5B57\u6BB5\u8BF4\u660E",
|
||||
threadIdHelpLabel: "\u67E5\u770B\u7FA4\u804A\u8BDD\u9898\u6807\u8BC6\u5B57\u6BB5\u8BF4\u660E",
|
||||
guidanceLabel: "\u589E\u5F3A\u63D0\u793A\u8BCD",
|
||||
guidanceHelpLabel: "\u67E5\u770B\u7FA4\u804A\u589E\u5F3A\u63D0\u793A\u8BCD\u4F7F\u7528\u8BF4\u660E",
|
||||
guidanceUsage: "\u7528\u4E8E\u544A\u8BC9\u6A21\u578B\u5982\u4F55\u4F7F\u7528\u5F53\u524D\u7FA4\u804A\u6D88\u606F\u7684 <dsh_im_source> \u6765\u6E90\u5B57\u6BB5\u3002\u53EA\u586B\u5199\u6B63\u6587\uFF0C\u63D2\u4EF6\u4F1A\u81EA\u52A8\u6DFB\u52A0 <dsh_im_source_guidance> \u6210\u5BF9\u6807\u7B7E\u3002",
|
||||
|
|
@ -2940,6 +2966,8 @@ var SCOPE_COPY = Object.freeze({
|
|||
fieldsHelpLabel: "\u67E5\u770B\u79C1\u804A\u6765\u6E90\u5B57\u6BB5\u8BF4\u660E",
|
||||
senderNameHelpLabel: "\u67E5\u770B\u79C1\u804A\u53D1\u9001\u8005\u6635\u79F0\u5B57\u6BB5\u8BF4\u660E",
|
||||
conversationTitleHelpLabel: "\u67E5\u770B\u79C1\u804A\u4F1A\u8BDD\u6807\u9898\u5B57\u6BB5\u8BF4\u660E",
|
||||
chatIdHelpLabel: "\u67E5\u770B\u79C1\u804A\u4F1A\u8BDD\u6807\u8BC6\u5B57\u6BB5\u8BF4\u660E",
|
||||
threadIdHelpLabel: "\u67E5\u770B\u79C1\u804A\u8BDD\u9898\u6807\u8BC6\u5B57\u6BB5\u8BF4\u660E",
|
||||
guidanceLabel: "\u589E\u5F3A\u63D0\u793A\u8BCD",
|
||||
guidanceHelpLabel: "\u67E5\u770B\u79C1\u804A\u589E\u5F3A\u63D0\u793A\u8BCD\u4F7F\u7528\u8BF4\u660E",
|
||||
guidanceUsage: "\u7528\u4E8E\u544A\u8BC9\u6A21\u578B\u5982\u4F55\u4F7F\u7528\u5F53\u524D\u79C1\u804A\u6D88\u606F\u7684 <dsh_im_source> \u6765\u6E90\u5B57\u6BB5\u3002\u53EA\u586B\u5199\u6B63\u6587\uFF0C\u63D2\u4EF6\u4F1A\u81EA\u52A8\u6DFB\u52A0 <dsh_im_source_guidance> \u6210\u5BF9\u6807\u7B7E\u3002",
|
||||
|
|
|
|||
288
lib/index.js
288
lib/index.js
File diff suppressed because one or more lines are too long
|
|
@ -34,6 +34,10 @@
|
|||
{
|
||||
"name": "divingleee",
|
||||
"url": "https://github.com/divingleee"
|
||||
},
|
||||
{
|
||||
"name": "Chan-0312",
|
||||
"url": "https://github.com/Chan-0312"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ const FIELD_LABELS = Object.freeze({
|
|||
senderId: '发送者标识',
|
||||
senderName: '发送者昵称',
|
||||
conversationTitle: '会话标题',
|
||||
chatId: '会话标识',
|
||||
threadId: '话题标识',
|
||||
botId: '机器人标识',
|
||||
});
|
||||
|
||||
|
|
@ -29,6 +31,14 @@ const FIELD_HELP = Object.freeze({
|
|||
labelKey: 'conversationTitleHelpLabel',
|
||||
text: '该字段不是每个渠道都能提供。钉钉群聊会带上群名。当前消息没有会话标题时,即使已选择该字段,<dsh_im_source> 中也会省略 conversationTitle。',
|
||||
}),
|
||||
chatId: Object.freeze({
|
||||
labelKey: 'chatIdHelpLabel',
|
||||
text: '该字段不是每个渠道都能提供。会话标识用于区分不同的群组或私聊,飞书群聊会带上群 ID。当前消息没有会话标识时,即使已选择该字段,<dsh_im_source> 中也会省略 chatId。',
|
||||
}),
|
||||
threadId: Object.freeze({
|
||||
labelKey: 'threadIdHelpLabel',
|
||||
text: '该字段不是每个渠道都能提供。飞书话题群的消息会带上话题 ID,用于区分同一群组内的不同话题;当前消息不在话题中时,即使已选择该字段,<dsh_im_source> 中也会省略 threadId。',
|
||||
}),
|
||||
});
|
||||
|
||||
const SCOPE_COPY = Object.freeze({
|
||||
|
|
@ -38,6 +48,8 @@ const SCOPE_COPY = Object.freeze({
|
|||
fieldsHelpLabel: '查看群聊来源字段说明',
|
||||
senderNameHelpLabel: '查看群聊发送者昵称字段说明',
|
||||
conversationTitleHelpLabel: '查看群聊会话标题字段说明',
|
||||
chatIdHelpLabel: '查看群聊会话标识字段说明',
|
||||
threadIdHelpLabel: '查看群聊话题标识字段说明',
|
||||
guidanceLabel: '增强提示词',
|
||||
guidanceHelpLabel: '查看群聊增强提示词使用说明',
|
||||
guidanceUsage: '用于告诉模型如何使用当前群聊消息的 <dsh_im_source> 来源字段。只填写正文,插件会自动添加 <dsh_im_source_guidance> 成对标签。',
|
||||
|
|
@ -49,6 +61,8 @@ const SCOPE_COPY = Object.freeze({
|
|||
fieldsHelpLabel: '查看私聊来源字段说明',
|
||||
senderNameHelpLabel: '查看私聊发送者昵称字段说明',
|
||||
conversationTitleHelpLabel: '查看私聊会话标题字段说明',
|
||||
chatIdHelpLabel: '查看私聊会话标识字段说明',
|
||||
threadIdHelpLabel: '查看私聊话题标识字段说明',
|
||||
guidanceLabel: '增强提示词',
|
||||
guidanceHelpLabel: '查看私聊增强提示词使用说明',
|
||||
guidanceUsage: '用于告诉模型如何使用当前私聊消息的 <dsh_im_source> 来源字段。只填写正文,插件会自动添加 <dsh_im_source_guidance> 成对标签。',
|
||||
|
|
|
|||
|
|
@ -292,6 +292,14 @@ const EN = Object.freeze({
|
|||
'查看群聊会话标题字段说明': 'View group conversation title field details',
|
||||
'查看私聊会话标题字段说明': 'View direct conversation title field details',
|
||||
'该字段不是每个渠道都能提供。钉钉群聊会带上群名。当前消息没有会话标题时,即使已选择该字段,<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.',
|
||||
'会话标识': 'Chat ID',
|
||||
'查看群聊会话标识字段说明': 'View group chat ID field details',
|
||||
'查看私聊会话标识字段说明': 'View direct chat ID field details',
|
||||
'该字段不是每个渠道都能提供。会话标识用于区分不同的群组或私聊,飞书群聊会带上群 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.',
|
||||
'话题标识': 'Topic ID',
|
||||
'查看群聊话题标识字段说明': 'View group topic ID field details',
|
||||
'查看私聊话题标识字段说明': 'View direct topic ID field details',
|
||||
'该字段不是每个渠道都能提供。飞书话题群的消息会带上话题 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.',
|
||||
'机器人标识': 'Bot ID',
|
||||
'增强提示词': 'Guidance',
|
||||
'查看增强提示词使用说明': 'View guidance instructions',
|
||||
|
|
@ -319,7 +327,7 @@ const EN = Object.freeze({
|
|||
'上下文增强保存失败,请重试。': 'Could not save context enhancement. Try again.',
|
||||
'请提交完整的上下文增强设置。': 'Submit the complete context enhancement settings.',
|
||||
'群聊和私聊开关必须是布尔值。': 'Group and direct switches must be booleans.',
|
||||
'来源字段只能选择已定义的六个字段。': 'Source fields must be chosen from the six defined fields.',
|
||||
'来源字段只能选择已定义的八个字段。': 'Source fields must be chosen from the eight defined fields.',
|
||||
'保存中…': 'Saving…',
|
||||
'未设置': 'Not set',
|
||||
'工作区修改失败,请重试。': 'Could not update the workspace. Try again.',
|
||||
|
|
|
|||
|
|
@ -1136,6 +1136,7 @@ export class DingtalkHarnessBridge {
|
|||
senderId: sender,
|
||||
senderName: message.senderNick,
|
||||
conversationTitle: message.conversationTitle,
|
||||
chatId: message.conversationId,
|
||||
}));
|
||||
contextEnhanced = content !== originalContent;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3330,6 +3330,8 @@ export class FeishuHarnessBridge {
|
|||
content = enhanceContextContent(originalContent, snapshot, () => ({
|
||||
channel: 'feishu',
|
||||
senderId: senderOpenId(event),
|
||||
chatId: event.message.chat_id,
|
||||
threadId: event.message.thread_id,
|
||||
}));
|
||||
contextEnhanced = content !== originalContent;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -890,6 +890,7 @@ export class QqHarnessBridge {
|
|||
channel: 'qq',
|
||||
senderId: sender,
|
||||
senderName: message.kind === 'group' ? message.senderName : undefined,
|
||||
chatId: message.kind === 'group' ? message.groupOpenid : message.senderId,
|
||||
}));
|
||||
contextEnhanced = content !== originalContent;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Shared by the Host and settings UI; keep this module browser-compatible.
|
||||
export const CONTEXT_ENHANCEMENT_FIELDS = Object.freeze([
|
||||
'channel', 'conversationType', 'senderId', 'senderName', 'conversationTitle', 'botId',
|
||||
'channel', 'conversationType', 'senderId', 'senderName', 'conversationTitle',
|
||||
'chatId', 'threadId', 'botId',
|
||||
]);
|
||||
|
||||
export const CONTEXT_ENHANCEMENT_GUIDANCE_MAX_LENGTH = 8_000;
|
||||
|
|
@ -38,7 +39,7 @@ const CHANNELS = new Set([
|
|||
]);
|
||||
const SOURCE_LIMITS = {
|
||||
channel: 16, conversationType: 6, senderId: 256, senderName: 256,
|
||||
conversationTitle: 256, botId: 128,
|
||||
conversationTitle: 256, chatId: 256, threadId: 256, botId: 128,
|
||||
};
|
||||
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g;
|
||||
|
||||
|
|
@ -64,7 +65,7 @@ function validateContextEnhancementScope(input) {
|
|||
throw invalidConfig('群聊和私聊开关必须是布尔值。');
|
||||
}
|
||||
if (!Array.isArray(fields) || ![...fields].every((field) => CONTEXT_ENHANCEMENT_FIELDS.includes(field))) {
|
||||
throw invalidConfig('来源字段只能选择已定义的六个字段。');
|
||||
throw invalidConfig('来源字段只能选择已定义的八个字段。');
|
||||
}
|
||||
if (typeof guidance !== 'string' || guidance.length > CONTEXT_ENHANCEMENT_GUIDANCE_MAX_LENGTH) {
|
||||
throw invalidConfig(`增强提示词不得超过 ${CONTEXT_ENHANCEMENT_GUIDANCE_MAX_LENGTH} 个字符。`);
|
||||
|
|
@ -150,7 +151,7 @@ function sourceString(value, field) {
|
|||
function sourceBlock(snapshot, sourceFactory) {
|
||||
const { fields } = snapshot.config;
|
||||
const needsSource = fields.some((field) => [
|
||||
'channel', 'senderId', 'senderName', 'conversationTitle',
|
||||
'channel', 'senderId', 'senderName', 'conversationTitle', 'chatId', 'threadId',
|
||||
].includes(field));
|
||||
const source = needsSource ? sourceFactory?.() : null;
|
||||
const projected = {};
|
||||
|
|
|
|||
|
|
@ -683,11 +683,14 @@ export class TextHarnessBridge {
|
|||
let contextEnhanced = false;
|
||||
if (snapshot) {
|
||||
const originalContent = content ?? text;
|
||||
const contextSource = message.contextSource?.();
|
||||
content = enhanceContextContent(originalContent, snapshot, () => ({
|
||||
channel: this.#descriptor.key,
|
||||
senderId,
|
||||
senderName: message.contextSource?.()?.senderName,
|
||||
conversationTitle: message.contextSource?.()?.conversationTitle,
|
||||
senderName: contextSource?.senderName,
|
||||
conversationTitle: contextSource?.conversationTitle,
|
||||
chatId: contextSource?.chatId ?? message.conversationId,
|
||||
threadId: contextSource?.threadId,
|
||||
}));
|
||||
contextEnhanced = content !== originalContent;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -172,6 +172,10 @@ export function normalizeSlackEvent(payload, botUserId, {
|
|||
senderIsBot: String(event.user) === String(botUserId),
|
||||
kind: direct ? 'direct' : 'group',
|
||||
conversationId: direct ? String(event.channel) : `${event.channel}:${threadTs}`,
|
||||
contextSource: () => ({
|
||||
chatId: String(event.channel),
|
||||
threadId: event.thread_ts ? String(event.thread_ts) : undefined,
|
||||
}),
|
||||
content: stripBotMention(event.text ?? '', botUserId),
|
||||
plainText: !Array.isArray(event.files) || event.files.length === 0,
|
||||
images: Array.isArray(event.files)
|
||||
|
|
|
|||
|
|
@ -266,6 +266,8 @@ export function normalizeTelegramUpdate(update, {
|
|||
.filter((value) => typeof value === 'string' && value.trim())
|
||||
.map((value) => value.trim()).join(' ') || message.from?.username,
|
||||
conversationTitle: direct ? undefined : message.chat?.title,
|
||||
chatId: String(chatId),
|
||||
threadId: messageThreadId === undefined ? undefined : String(messageThreadId),
|
||||
}),
|
||||
senderIsBot: message.from?.is_bot === true,
|
||||
kind: direct ? 'direct' : 'group',
|
||||
|
|
|
|||
|
|
@ -1056,6 +1056,7 @@ export class WecomHarnessBridge {
|
|||
content = enhanceContextContent(originalContent, snapshot, () => ({
|
||||
channel: 'wecom',
|
||||
senderId,
|
||||
chatId,
|
||||
}));
|
||||
contextEnhanced = content !== originalContent;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -786,6 +786,7 @@ export class WeixinHarnessBridge {
|
|||
content = enhanceContextContent(originalContent, snapshot, () => ({
|
||||
channel: 'weixin',
|
||||
senderId: sender,
|
||||
chatId: sender,
|
||||
}));
|
||||
contextEnhanced = content !== originalContent;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -307,6 +307,10 @@ for (const channel of CHANNELS) {
|
|||
test(`${channel} ${kind}: enabled source uses the actual event, optional name and internal bot ID`, async () => {
|
||||
const current = fixture(channel, { contextEnhancement: provider(channel, settings()) });
|
||||
await current.bridge.accept(current.event(1, 'hello', { kind }));
|
||||
const chatId = channel === 'telegram' ? '100'
|
||||
: channel === 'whatsapp' ? (kind === 'group' ? 'chat@g.us' : 'actor@s.whatsapp.net')
|
||||
: channel === 'wecom' || channel === 'qq' || channel === 'weixin'
|
||||
? (kind === 'group' ? 'chat' : 'actor') : 'chat';
|
||||
const expected = {
|
||||
channel, conversationType: kind,
|
||||
senderId: channel === 'telegram' ? '42' : channel === 'whatsapp' ? 'actor@s.whatsapp.net' : 'actor',
|
||||
|
|
@ -314,6 +318,8 @@ for (const channel of CHANNELS) {
|
|||
? { senderName: 'Ada' } : {}),
|
||||
...(channel === 'dingtalk' && kind === 'group' ? { conversationTitle: '钉钉测试群' } : {}),
|
||||
...(channel === 'telegram' && kind === 'group' ? { conversationTitle: 'Telegram群' } : {}),
|
||||
chatId,
|
||||
...(channel === 'slack' && kind === 'group' ? { threadId: 'thread-existing' } : {}),
|
||||
botId: `${channel}_internal`,
|
||||
};
|
||||
assert.deepEqual(sourceOf(current.prompts[0]), expected);
|
||||
|
|
@ -554,3 +560,25 @@ test('Discord prefers event member nickname, global name, then username without
|
|||
assert.deepEqual(current.prompts.map((content) => sourceOf(content).senderName),
|
||||
['Group Nick', 'Global Name', 'username', undefined]);
|
||||
});
|
||||
|
||||
test('Feishu topics expose chatId always and threadId only when the event carries a thread', async () => {
|
||||
const current = fixture('feishu', { contextEnhancement: provider('feishu', settings()) });
|
||||
await current.bridge.accept(current.event(1, 'main channel', { kind: 'group' }));
|
||||
const main = sourceOf(current.prompts[0]);
|
||||
assert.equal(main.chatId, 'chat');
|
||||
assert.equal(Object.hasOwn(main, 'threadId'), false);
|
||||
assert.deepEqual(main, {
|
||||
channel: 'feishu', conversationType: 'group', senderId: 'actor',
|
||||
chatId: 'chat', botId: 'feishu_internal',
|
||||
});
|
||||
const topic = current.event(2, 'inside a topic', { kind: 'group' });
|
||||
topic.message.thread_id = 'omt_topic';
|
||||
await current.bridge.accept(topic);
|
||||
const topicSource = sourceOf(current.prompts[1]);
|
||||
assert.equal(topicSource.chatId, 'chat');
|
||||
assert.equal(topicSource.threadId, 'omt_topic');
|
||||
assert.equal(current.sessions.size, 2, 'topic messages keep their own thread-scoped Session');
|
||||
await current.bridge.accept(current.event(3, 'direct chat', { kind: 'direct' }));
|
||||
assert.equal(sourceOf(current.prompts[2]).chatId, 'chat');
|
||||
assert.equal(Object.hasOwn(sourceOf(current.prompts[2]), 'threadId'), false);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -185,16 +185,18 @@ test('group and direct messages use only their own fields and guidance', () => {
|
|||
assert.doesNotMatch(direct, /GROUP-ONLY-TOKEN|channel/);
|
||||
});
|
||||
|
||||
test('all 64 source-field subsets are projected in canonical order with no hidden fields', () => {
|
||||
test('all 256 source-field subsets are projected in canonical order with no hidden fields', () => {
|
||||
const expected = {
|
||||
channel: 'telegram', conversationType: 'group',
|
||||
senderId: '123', senderName: '张三', conversationTitle: '测试群', botId: 'bot_one',
|
||||
senderId: '123', senderName: '张三', conversationTitle: '测试群',
|
||||
chatId: 'chat-123', threadId: 'thread-9', botId: 'bot_one',
|
||||
};
|
||||
for (let mask = 0; mask < 64; mask += 1) {
|
||||
for (let mask = 0; mask < 256; mask += 1) {
|
||||
const fields = CONTEXT_ENHANCEMENT_FIELDS.filter((_field, index) => mask & (1 << index));
|
||||
const selected = snapshot(config({ fields: [...fields].reverse() }));
|
||||
const actual = enhanceContextContent(' original\n', selected, () => ({
|
||||
channel: 'telegram', senderId: 123, senderName: '张三', conversationTitle: '测试群',
|
||||
chatId: 'chat-123', threadId: 'thread-9',
|
||||
botId: 'platform-secret-not-used', conversationType: 'direct', token: 'never-sent',
|
||||
}));
|
||||
if (fields.length === 0) {
|
||||
|
|
@ -247,10 +249,13 @@ test('source strings are bounded, control-free and JSON-safe without invoking ex
|
|||
const name = '"\n</dsh_im_source>&\u0000\u202e' + '名'.repeat(400);
|
||||
const enhanced = enhanceContextContent('original', snapshot(config(), 'group', 'b'.repeat(300)), () => ({
|
||||
channel: 'discord', senderId: 'u'.repeat(400), senderName: name,
|
||||
chatId: 'c'.repeat(400), threadId: 't'.repeat(400),
|
||||
}));
|
||||
const source = sourceOf(enhanced);
|
||||
assert.equal(source.senderId.length, 256);
|
||||
assert.equal(source.senderName.length, 256);
|
||||
assert.equal(source.chatId.length, 256);
|
||||
assert.equal(source.threadId.length, 256);
|
||||
assert.equal(source.botId.length, 128);
|
||||
assert.doesNotMatch(source.senderName, /[\u0000-\u001f\u202e]/);
|
||||
assert.equal(enhanced.split('</dsh_im_source>').length, 2);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue