@xmanrui/dsh-im 4.13.0 → 4.15.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 +8 -1
- package/README.md +8 -1
- package/lib/client.js +327 -3
- package/lib/index.js +249 -245
- package/package.json +1 -1
- package/plugin-src/client/channels/feishu/api.js +2 -0
- package/plugin-src/client/channels/feishu/index.js +58 -1
- package/plugin-src/client/i18n.js +6 -1
- package/plugin-src/client/index.js +3 -0
- package/plugin-src/client/session-channel-logos.js +241 -0
- package/plugin-src/host/channels/feishu/production.mjs +1 -0
- package/plugin-src/host/channels/feishu/rpc.mjs +16 -0
- package/plugin-src/host/index.mjs +14 -0
- package/plugin-src/host/session-title-prefix.mjs +122 -0
- package/scripts/verify-session-channel-logos.mjs +59 -0
- package/scripts/verify-session-title-prefix.mjs +149 -0
- package/src/channels/feishu/bridge.mjs +520 -6
- package/src/channels/feishu/feishu-runtime.mjs +9 -0
- package/src/channels/feishu/multi-bot-controller.mjs +16 -0
- package/src/channels/feishu/plugin-config-store.mjs +1 -0
- package/src/channels/feishu/plugin-controller.mjs +1 -0
- package/src/channels/feishu/repair-manager.mjs +3 -2
- package/src/channels/shared/bot-workspace-store.mjs +14 -1
- package/src/channels/shared/harness-client.mjs +21 -2
- package/src/channels/shared/i18n-en/feishu.mjs +4 -2
- package/src/channels/shared/semantic/artifact.mjs +3 -1
- package/src/channels/shared/session-channel-labels.mjs +26 -0
|
@@ -91,6 +91,7 @@ function configuredBotFingerprint(config) {
|
|
|
91
91
|
activated: config.activated,
|
|
92
92
|
groupResponseMode: normalizeFeishuGroupResponseMode(config.groupResponseMode),
|
|
93
93
|
groupTopicReply: config.groupTopicReply === true,
|
|
94
|
+
stepPush: config.stepPush === true,
|
|
94
95
|
groupMessagePermissionGranted: config.groupMessagePermissionGranted === true,
|
|
95
96
|
deletionPending: config.deletionPending === true,
|
|
96
97
|
connectedAt: config.connectedAt ?? null,
|
|
@@ -599,6 +600,20 @@ export class MultiBotDshFeishuController {
|
|
|
599
600
|
}));
|
|
600
601
|
}
|
|
601
602
|
|
|
603
|
+
async updateStepPush(botId, stepPush) {
|
|
604
|
+
this.#assertOpen();
|
|
605
|
+
if (typeof stepPush !== 'boolean') {
|
|
606
|
+
throw new TypeError('Invalid Feishu step push flag');
|
|
607
|
+
}
|
|
608
|
+
return this.#serializeConfig(() => this.#withBotTransition(botId, async () => {
|
|
609
|
+
const config = this.#requireBot(botId);
|
|
610
|
+
const saved = await this.#configStore.saveBot({ ...config, stepPush });
|
|
611
|
+
this.#runtimes.get(botId)?.setStepPush?.(saved.stepPush);
|
|
612
|
+
this.#touch();
|
|
613
|
+
return this.status(botId);
|
|
614
|
+
}));
|
|
615
|
+
}
|
|
616
|
+
|
|
602
617
|
async deleteBot(botId) {
|
|
603
618
|
this.#assertOpen();
|
|
604
619
|
return this.#serializeConfig(() => this.#withBotTransition(botId, async () => {
|
|
@@ -662,6 +677,7 @@ export class MultiBotDshFeishuController {
|
|
|
662
677
|
configured: true,
|
|
663
678
|
groupResponseMode: normalizeFeishuGroupResponseMode(config.groupResponseMode),
|
|
664
679
|
groupTopicReply: config.groupTopicReply === true,
|
|
680
|
+
stepPush: config.stepPush === true,
|
|
665
681
|
groupMessagePermissionGranted: config.groupMessagePermissionGranted === true,
|
|
666
682
|
bot: publicBot(config),
|
|
667
683
|
connection,
|
|
@@ -46,6 +46,7 @@ function normalizeBot(value, { legacy = false } = {}) {
|
|
|
46
46
|
activated: value.activated ?? null,
|
|
47
47
|
groupResponseMode: normalizeFeishuGroupResponseMode(value.groupResponseMode),
|
|
48
48
|
groupTopicReply: value.groupTopicReply === true,
|
|
49
|
+
stepPush: value.stepPush === true,
|
|
49
50
|
groupMessagePermissionGranted: value.groupMessagePermissionGranted === true,
|
|
50
51
|
deletionPending: value.deletionPending === true,
|
|
51
52
|
connectedAt: cleanString(value.connectedAt),
|
|
@@ -6,6 +6,7 @@ export const FEISHU_SECRET_REF = 'DSH_FEISHU_APP_SECRET';
|
|
|
6
6
|
export const REQUIRED_TENANT_SCOPES = Object.freeze([
|
|
7
7
|
'im:message.p2p_msg:readonly',
|
|
8
8
|
'im:message.group_at_msg:readonly',
|
|
9
|
+
'im:message.group_at_msg.include_bot:readonly',
|
|
9
10
|
'im:message:readonly',
|
|
10
11
|
'im:message:send_as_bot',
|
|
11
12
|
'im:message.reactions:write_only',
|
|
@@ -76,8 +76,8 @@ export function assertCallbackRepairUrl(value, expectedAppId, domain = 'feishu')
|
|
|
76
76
|
* shares RegistrationManager's polling/state implementation while fixing the
|
|
77
77
|
* update manifest in one place so callers can add only the card callback, the
|
|
78
78
|
* message-read scope needed to download user-sent media, the resource scope
|
|
79
|
-
* needed to upload bot-sent images/files,
|
|
80
|
-
* for the native command panel, without adding unrelated scopes, events,
|
|
79
|
+
* needed to upload bot-sent images/files, the group bot-mention scope, and the
|
|
80
|
+
* Slash Command scopes for the native command panel, without adding unrelated scopes, events,
|
|
81
81
|
* presets, or createOnly.
|
|
82
82
|
*/
|
|
83
83
|
export class CallbackRepairManager {
|
|
@@ -116,6 +116,7 @@ export class CallbackRepairManager {
|
|
|
116
116
|
tenant: [
|
|
117
117
|
FEISHU_MESSAGE_READ_SCOPE,
|
|
118
118
|
FEISHU_RESOURCE_SCOPE,
|
|
119
|
+
'im:message.group_at_msg.include_bot:readonly',
|
|
119
120
|
...SLASH_COMMAND_TENANT_SCOPES,
|
|
120
121
|
],
|
|
121
122
|
},
|
|
@@ -161,7 +161,20 @@ function normalizeDeliveryTargets(value, { version } = {}) {
|
|
|
161
161
|
if (!targets || typeof targets !== 'object' || Array.isArray(targets)) return null;
|
|
162
162
|
const normalizedTargets = Object.create(null);
|
|
163
163
|
for (const [targetId, target] of Object.entries(targets)) {
|
|
164
|
-
|
|
164
|
+
// Backward compatibility: some released builds persisted the target id
|
|
165
|
+
// inside the stored object as well. Accept a redundant targetId that
|
|
166
|
+
// matches the map key when loading a stored document; a mismatch stays
|
|
167
|
+
// invalid so a corrupted file still fails closed.
|
|
168
|
+
let candidate = target;
|
|
169
|
+
if (target && typeof target === 'object' && !Array.isArray(target)
|
|
170
|
+
&& target.targetId !== undefined) {
|
|
171
|
+
if (target.targetId !== targetId) {
|
|
172
|
+
throw deliveryTargetError('invalid-target', 'Invalid target id');
|
|
173
|
+
}
|
|
174
|
+
const { targetId: _legacyTargetId, ...withoutLegacyId } = target;
|
|
175
|
+
candidate = withoutLegacyId;
|
|
176
|
+
}
|
|
177
|
+
const normalized = normalizeDeliveryTarget(candidate, {
|
|
165
178
|
targetId,
|
|
166
179
|
allowSessionSync: version >= CURRENT_DOCUMENT_VERSION,
|
|
167
180
|
});
|
|
@@ -590,6 +590,9 @@ export class HarnessReplyTracker {
|
|
|
590
590
|
const text = assistantMessageText(event);
|
|
591
591
|
const step = Number.isSafeInteger(event.data?.step) ? event.data.step : null;
|
|
592
592
|
this.#assistantText.setCanonical(step, text);
|
|
593
|
+
// canonical 定稿且非空时按 step 透出,供分步推送消费方使用;
|
|
594
|
+
// 先于 commitText 透出,保持 text 更新作为批次末尾的既有语义。
|
|
595
|
+
if (text) pushUpdate({ type: 'assistant-message', step, text });
|
|
593
596
|
this.#commitText(this.#assistantText.text, pushUpdate);
|
|
594
597
|
continue;
|
|
595
598
|
}
|
|
@@ -600,7 +603,19 @@ export class HarnessReplyTracker {
|
|
|
600
603
|
?? nonEmptyText(event.data?.subCallId);
|
|
601
604
|
if (callId) this.#toolNames.set(callId, name);
|
|
602
605
|
this.#lastToolName = name;
|
|
603
|
-
|
|
606
|
+
let argsText = null;
|
|
607
|
+
if (event.data?.arguments !== undefined && event.data?.arguments !== null) {
|
|
608
|
+
if (typeof event.data.arguments === 'string') {
|
|
609
|
+
argsText = event.data.arguments;
|
|
610
|
+
} else {
|
|
611
|
+
try {
|
|
612
|
+
argsText = JSON.stringify(event.data.arguments);
|
|
613
|
+
} catch {
|
|
614
|
+
argsText = undefined;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
pushUpdate({ type: 'tool', name, ...(argsText ? { arguments: argsText } : {}), ...(callId ? { callId } : {}) });
|
|
604
619
|
} else if (event.type === 'tool/result') {
|
|
605
620
|
const callId = nonEmptyText(event.data?.message?.source?.callId)
|
|
606
621
|
?? nonEmptyText(event.data?.callId)
|
|
@@ -1562,7 +1577,11 @@ export class HarnessClient {
|
|
|
1562
1577
|
lastPollSeq = tracker.lastSeq;
|
|
1563
1578
|
if (seqAdvanced) lastProgressAt = Date.now();
|
|
1564
1579
|
if (onUpdate) {
|
|
1565
|
-
|
|
1580
|
+
// latest 模式只投递一条最新进展;assistant-message 是分步推送专用更新,
|
|
1581
|
+
// 且 canonical 去重后可能成为批次唯一变化,绝不能冒充进度投给全部渠道。
|
|
1582
|
+
const visibleUpdates = progressMode === 'all'
|
|
1583
|
+
? updates
|
|
1584
|
+
: updates.filter((update) => update.type !== 'assistant-message').slice(-1);
|
|
1566
1585
|
for (const update of visibleUpdates) {
|
|
1567
1586
|
try {
|
|
1568
1587
|
await onUpdate(update);
|
|
@@ -72,8 +72,8 @@ export default {
|
|
|
72
72
|
'旧授权链接已作废,已生成新的修复链接。':
|
|
73
73
|
'The previous authorization link was invalidated and a new repair link was generated.',
|
|
74
74
|
'🔧 准备补全权限与回调。': '🔧 Preparing to complete permissions and the callback.',
|
|
75
|
-
'本次会增量添加当前缺少项:卡片回调 card.action.trigger;飞书显示为“获取单聊、群组消息”的租户权限 im:message:readonly(用于读取用户消息中的图片或文件);im:resource
|
|
76
|
-
'This incrementally adds the currently missing items: the card callback card.action.trigger; the tenant scope im:message:readonly, shown by Feishu as “Read direct and group messages” and used to read images or files in user messages; im:resource, used to upload images or files sent by the bot; and application:app_slash_command:read / write for the native command panel. The confirmation page shows only items the app is currently missing; cancel if anything outside this scope appears.',
|
|
75
|
+
'本次会增量添加当前缺少项:卡片回调 card.action.trigger;飞书显示为“获取单聊、群组消息”的租户权限 im:message:readonly(用于读取用户消息中的图片或文件);im:resource(用于上传机器人发送的图片或文件);im:message.group_at_msg.include_bot:readonly(用于接收群内其他机器人 @ 当前机器人的消息);以及原生命令面板所需的 application:app_slash_command:read / write。确认页只会显示当前缺少的项;若出现上述范围之外的配置,请取消。':
|
|
76
|
+
'This incrementally adds the currently missing items: the card callback card.action.trigger; the tenant scope im:message:readonly, shown by Feishu as “Read direct and group messages” and used to read images or files in user messages; im:resource, used to upload images or files sent by the bot; im:message.group_at_msg.include_bot:readonly, used to receive group messages from other bots that mention this bot; and application:app_slash_command:read / write for the native command panel. The confirmation page shows only items the app is currently missing; cancel if anything outside this scope appears.',
|
|
77
77
|
'当前设备直接打开:': 'Open directly on this device:',
|
|
78
78
|
'若要用另一台设备扫码,发送 /repair qr。{expiry}。':
|
|
79
79
|
'To scan with another device, send /repair qr. {expiry}.',
|
|
@@ -373,4 +373,6 @@ export default {
|
|
|
373
373
|
'已请求停止后台任务。': 'Requested to stop the background task.',
|
|
374
374
|
'无法安全停止后台任务,请在 Harness 中查看并停止对应任务。': 'Unable to safely stop the background task. Please inspect and stop the matching task in Harness.',
|
|
375
375
|
'任务失败': 'Failed',
|
|
376
|
+
// feishu/bridge.mjs — step push context injection
|
|
377
|
+
'📎 已注入会话上下文': '📎 Context injected',
|
|
376
378
|
};
|
|
@@ -46,7 +46,9 @@ function artifactError(code, message) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
function currentTurn(agent) {
|
|
49
|
-
const events = agent?.session?.
|
|
49
|
+
const events = typeof agent?.session?.snapshotEvents === 'function'
|
|
50
|
+
? agent.session.snapshotEvents()
|
|
51
|
+
: agent?.session?.events;
|
|
50
52
|
if (!Array.isArray(events)) return null;
|
|
51
53
|
let turn = null;
|
|
52
54
|
for (const event of events) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const SESSION_CHANNEL_LABELS = Object.freeze(Object.fromEntries(Object.entries({
|
|
2
|
+
weixin: ['微信', 'WeChat'],
|
|
3
|
+
feishu: ['飞书', 'Feishu'],
|
|
4
|
+
dingtalk: ['钉钉', 'DingTalk'],
|
|
5
|
+
wecom: ['企业微信', 'WeCom'],
|
|
6
|
+
qq: ['QQ', 'QQ'],
|
|
7
|
+
slack: ['Slack', 'Slack'],
|
|
8
|
+
telegram: ['Telegram', 'Telegram'],
|
|
9
|
+
discord: ['Discord', 'Discord'],
|
|
10
|
+
whatsapp: ['WhatsApp', 'WhatsApp'],
|
|
11
|
+
office: ['AI Office', 'AI Office'],
|
|
12
|
+
}).map(([channel, labels]) => [channel, Object.freeze(labels)])));
|
|
13
|
+
|
|
14
|
+
/** Read only the exact, leading title prefix emitted by dsh-im. */
|
|
15
|
+
export function parseSessionChannelTitle(title) {
|
|
16
|
+
if (typeof title !== 'string') return null;
|
|
17
|
+
for (const [channel, labels] of Object.entries(SESSION_CHANNEL_LABELS)) {
|
|
18
|
+
for (const label of labels) {
|
|
19
|
+
const prefix = `${label} · `;
|
|
20
|
+
if (title.startsWith(prefix) && title.slice(prefix.length).trim()) {
|
|
21
|
+
return { channel, title: title.slice(prefix.length) };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|