@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.
@@ -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, and the Slash Command scopes needed
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
- const normalized = normalizeDeliveryTarget(target, {
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
- pushUpdate({ type: 'tool', name, ...(callId ? { callId } : {}) });
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
- const visibleUpdates = progressMode === 'all' ? updates : updates.slice(-1);
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(用于上传机器人发送的图片或文件);以及原生命令面板所需的 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; 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?.events;
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
+ }