@xmanrui/dsh-im 2.6.0 → 3.0.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": "2.6.0",
3
+ "version": "3.0.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",
@@ -83,7 +83,7 @@
83
83
  "scripts": {
84
84
  "build": "node plugin-src/client/build.mjs && node plugin-src/host/build.mjs",
85
85
  "test": "node --test test/*.test.mjs test/channels/*/*.test.mjs",
86
- "check": "npm test && npm run build && node scripts/verify-package.mjs"
86
+ "check": "npm run build && npm test && node scripts/verify-package.mjs"
87
87
  },
88
88
  "engines": {
89
89
  "node": ">=22.19"
@@ -16,7 +16,6 @@ import {
16
16
  } from '../../channel-card-meta.js';
17
17
  import {
18
18
  DINGTALK_ENDPOINTS,
19
- DINGTALK_RPC_CHANNEL,
20
19
  connectionTestFeedback,
21
20
  formatRemaining,
22
21
  normalizeProvisioning,
@@ -29,9 +28,6 @@ import { installDingtalkStyles } from './styles.js';
29
28
 
30
29
  const ACTIVE_PROVISION_STATES = new Set(['pending', 'scanned', 'authorizing', 'creating', 'connecting']);
31
30
 
32
- export const name = 'dingtalk-settings';
33
- export const inject = ['slots', 'connection'];
34
-
35
31
  function DingtalkIcon({ size = 28 }) {
36
32
  return h('svg', {
37
33
  width: size,
@@ -864,16 +860,3 @@ export function DingtalkSettingsTab({ rpcCall }) {
864
860
  })
865
861
  : null)));
866
862
  }
867
-
868
- export function apply(ctx) {
869
- ctx.effect(() => installDingtalkStyles(), 'dingtalk-settings: install client styles');
870
- const rpcCall = (endpoint, payload, signal) =>
871
- ctx.connection.rpc.call(DINGTALK_RPC_CHANNEL, endpoint, payload, signal);
872
- ctx.slots.inject('settings.plugins.tab', () => ctx.slots.register({
873
- name: 'settings.plugins.tab',
874
- id: 'dingtalk',
875
- order: 40,
876
- label: '钉钉',
877
- inject: () => ({ rpcCall }),
878
- }, DingtalkSettingsTab));
879
- }
@@ -1,4 +1,4 @@
1
- export const DINGTALK_STYLE_ID = 'xmanrui-dsh-dingtalk-settings';
1
+ export const DINGTALK_STYLE_ID = 'xmanrui-dsh-im-dingtalk-settings';
2
2
 
3
3
  const CSS = String.raw`
4
4
  .ddt-page {
@@ -126,7 +126,7 @@ export function installDingtalkStyles() {
126
126
  const existing = document.querySelector(`style[data-plugin-css="${DINGTALK_STYLE_ID}"]`);
127
127
  if (existing) return () => {};
128
128
  const style = document.createElement('style');
129
- style.dataset.plugin = '@xmanrui/dsh-dingtalk';
129
+ style.dataset.plugin = '@xmanrui/dsh-im';
130
130
  style.dataset.pluginCss = DINGTALK_STYLE_ID;
131
131
  style.textContent = CSS;
132
132
  document.head.appendChild(style);
@@ -6,7 +6,6 @@ import { h } from "../../i18n.js";
6
6
  import {
7
7
  FEISHU_ENDPOINTS,
8
8
  FEISHU_REGISTRATION_OPERATIONS,
9
- FEISHU_RPC_CHANNEL,
10
9
  formatRemaining,
11
10
  normalizeBotsSnapshot,
12
11
  normalizeGroupResponseMode,
@@ -28,11 +27,6 @@ import {
28
27
  ChannelListHeading,
29
28
  LastMessageErrorSummary,
30
29
  } from "../../channel-card-meta.js";
31
- import { installFeishuStyles } from "./styles.js";
32
-
33
- export const name = "feishu-settings";
34
- export const inject = ["slots", "connection"];
35
-
36
30
  const CALLBACK_REPAIR_OPERATION = FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR;
37
31
  const GROUP_MESSAGE_PERMISSION_OPERATION = FEISHU_REGISTRATION_OPERATIONS.GROUP_MESSAGE_PERMISSION;
38
32
 
@@ -1540,26 +1534,3 @@ export function FeishuSettingsTab({ rpcCall }) {
1540
1534
  ),
1541
1535
  ));
1542
1536
  }
1543
-
1544
- export function apply(ctx) {
1545
- ctx.effect(
1546
- () => installFeishuStyles(),
1547
- "feishu-settings: install client styles",
1548
- );
1549
-
1550
- const rpcCall = (endpoint, payload, signal) =>
1551
- ctx.connection.rpc.call(FEISHU_RPC_CHANNEL, endpoint, payload, signal);
1552
-
1553
- ctx.slots.inject("settings.plugins.tab", () =>
1554
- ctx.slots.register(
1555
- {
1556
- name: "settings.plugins.tab",
1557
- id: "feishu",
1558
- order: 20,
1559
- label: "飞书",
1560
- inject: () => ({ rpcCall }),
1561
- },
1562
- FeishuSettingsTab,
1563
- ),
1564
- );
1565
- }
@@ -1,4 +1,4 @@
1
- export const FEISHU_STYLE_ID = "beihuixinghe-dsh-feishu-settings";
1
+ export const FEISHU_STYLE_ID = "xmanrui-dsh-im-feishu-settings";
2
2
 
3
3
  const CSS = String.raw`
4
4
  .bxf-page {
@@ -562,7 +562,7 @@ export function installFeishuStyles() {
562
562
  }
563
563
 
564
564
  const style = document.createElement("style");
565
- style.dataset.plugin = "@xmanrui/dsh-feishu";
565
+ style.dataset.plugin = "@xmanrui/dsh-im";
566
566
  style.dataset.pluginCss = FEISHU_STYLE_ID;
567
567
  style.textContent = CSS;
568
568
  document.head.appendChild(style);
@@ -18,7 +18,6 @@ import {
18
18
  import { installDingtalkStyles } from '../dingtalk/styles.js';
19
19
  import {
20
20
  QQ_ENDPOINTS,
21
- QQ_RPC_CHANNEL,
22
21
  connectionTestFeedback,
23
22
  formatRemaining,
24
23
  normalizeProvisioning,
@@ -514,11 +513,3 @@ export function QqSettingsTab({ rpcCall }) {
514
513
  ? h(EmptyView, { busy, onStart: () => void startProvisioning() }) : null,
515
514
  botList)));
516
515
  }
517
-
518
- export function apply(ctx) {
519
- ctx.effect(() => installQqStyles(), 'qq-settings: install client styles');
520
- const rpcCall = (endpoint, payload, signal) => ctx.connection.rpc.call(QQ_RPC_CHANNEL, endpoint, payload, signal);
521
- ctx.slots.inject('settings.plugins.tab', () => ctx.slots.register({
522
- name: 'settings.plugins.tab', id: 'qq', order: 50, label: 'QQ', inject: () => ({ rpcCall }),
523
- }, QqSettingsTab));
524
- }
@@ -18,7 +18,6 @@ import {
18
18
  import { installDingtalkStyles } from '../dingtalk/styles.js';
19
19
  import {
20
20
  WECOM_ENDPOINTS,
21
- WECOM_RPC_CHANNEL,
22
21
  formatRemaining,
23
22
  normalizeProvisioning,
24
23
  normalizeSnapshot,
@@ -546,11 +545,3 @@ export function WecomSettingsTab({ rpcCall }) {
546
545
  ? h(EmptyView, { busy, onStart: () => void startProvisioning() }) : null,
547
546
  botList)));
548
547
  }
549
-
550
- export function apply(ctx) {
551
- ctx.effect(() => installWecomStyles(), 'wecom-settings: install client styles');
552
- const rpcCall = (endpoint, payload, signal) => ctx.connection.rpc.call(WECOM_RPC_CHANNEL, endpoint, payload, signal);
553
- ctx.slots.inject('settings.plugins.tab', () => ctx.slots.register({
554
- name: 'settings.plugins.tab', id: 'wecom', order: 45, label: '企业微信', inject: () => ({ rpcCall }),
555
- }, WecomSettingsTab));
556
- }
@@ -5,7 +5,6 @@ import { QrActionIcon } from '../../credential-binding.js';
5
5
  import { h } from '../../i18n.js';
6
6
  import {
7
7
  WEIXIN_ENDPOINTS,
8
- WEIXIN_RPC_CHANNEL,
9
8
  formatRemaining,
10
9
  normalizeProvisioning,
11
10
  normalizeSnapshot,
@@ -27,11 +26,6 @@ import {
27
26
  ChannelListHeading,
28
27
  LastMessageErrorSummary,
29
28
  } from '../../channel-card-meta.js';
30
- import { installWeixinStyles } from './styles.js';
31
-
32
- export const name = 'weixin-settings';
33
- export const inject = ['slots', 'connection'];
34
-
35
29
  const Button = React.forwardRef(function Button(
36
30
  { children, kind = 'secondary', className = '', ...props },
37
31
  ref,
@@ -717,16 +711,3 @@ export function WeixinSettingsTab({ rpcCall }) {
717
711
  : null),
718
712
  ));
719
713
  }
720
-
721
- export function apply(ctx) {
722
- ctx.effect(() => installWeixinStyles(), 'weixin-settings: install client styles');
723
- const rpcCall = (endpoint, payload, signal) =>
724
- ctx.connection.rpc.call(WEIXIN_RPC_CHANNEL, endpoint, payload, signal);
725
- ctx.slots.inject('settings.plugins.tab', () => ctx.slots.register({
726
- name: 'settings.plugins.tab',
727
- id: 'weixin',
728
- order: 30,
729
- label: '微信',
730
- inject: () => ({ rpcCall }),
731
- }, WeixinSettingsTab));
732
- }
@@ -1,4 +1,4 @@
1
- export const WEIXIN_STYLE_ID = 'xmanrui-dsh-weixin-settings';
1
+ export const WEIXIN_STYLE_ID = 'xmanrui-dsh-im-weixin-settings';
2
2
 
3
3
  const CSS = String.raw`
4
4
  .dxw-page {
@@ -107,7 +107,7 @@ export function installWeixinStyles() {
107
107
  const existing = document.querySelector(`style[data-plugin-css="${WEIXIN_STYLE_ID}"]`);
108
108
  if (existing) return () => {};
109
109
  const style = document.createElement('style');
110
- style.dataset.plugin = '@xmanrui/dsh-weixin';
110
+ style.dataset.plugin = '@xmanrui/dsh-im';
111
111
  style.dataset.pluginCss = WEIXIN_STYLE_ID;
112
112
  style.textContent = CSS;
113
113
  document.head.appendChild(style);
@@ -333,10 +333,10 @@ export function apply(ctx) {
333
333
  pickDirectory: () => ctx.workspaces.pickDirectory(),
334
334
  });
335
335
 
336
- ctx.slots.inject('settings.plugins.tab', () => ctx.slots.register({
337
- name: 'settings.plugins.tab',
338
- id: 'im',
339
- order: 20,
336
+ ctx.slots.inject('settings.section', () => ctx.slots.register({
337
+ name: 'settings.section',
338
+ id: 'xmanrui-dsh-im',
339
+ order: 21,
340
340
  label: () => t('IM机器人'),
341
341
  locale: IM_LOCALE_NAMESPACE,
342
342
  inject: () => ({
@@ -1,8 +1,23 @@
1
- import { access, readFile, stat } from 'node:fs/promises';
1
+ import { access, readFile, readdir, stat } from 'node:fs/promises';
2
2
  import { resolve } from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
4
 
5
5
  const root = resolve(import.meta.dirname, '..');
6
+
7
+ async function readSourceTree(directory) {
8
+ const entries = await readdir(directory, { withFileTypes: true });
9
+ const chunks = [];
10
+ for (const entry of entries) {
11
+ const path = resolve(directory, entry.name);
12
+ if (entry.isDirectory()) {
13
+ chunks.push(await readSourceTree(path));
14
+ } else if (entry.isFile() && /\.m?js$/u.test(entry.name)) {
15
+ chunks.push(await readFile(path, 'utf8'));
16
+ }
17
+ }
18
+ return chunks.join('\n');
19
+ }
20
+
6
21
  const required = [
7
22
  'lib/index.js',
8
23
  'lib/client.js',
@@ -35,7 +50,17 @@ const required = [
35
50
  ];
36
51
  await Promise.all(required.map((path) => access(resolve(root, path))));
37
52
 
38
- const [client, host, patch, manifestText, lockText, hostSource, clientSource, executable] = await Promise.all([
53
+ const [
54
+ client,
55
+ host,
56
+ patch,
57
+ manifestText,
58
+ lockText,
59
+ hostSource,
60
+ clientEntrySource,
61
+ clientSources,
62
+ executable,
63
+ ] = await Promise.all([
39
64
  readFile(resolve(root, 'lib/client.js'), 'utf8'),
40
65
  readFile(resolve(root, 'lib/index.js'), 'utf8'),
41
66
  readFile(resolve(root, 'cordis.patch.yml'), 'utf8'),
@@ -43,6 +68,7 @@ const [client, host, patch, manifestText, lockText, hostSource, clientSource, ex
43
68
  readFile(resolve(root, 'package-lock.json'), 'utf8'),
44
69
  readFile(resolve(root, 'plugin-src/host/index.mjs'), 'utf8'),
45
70
  readFile(resolve(root, 'plugin-src/client/index.js'), 'utf8'),
71
+ readSourceTree(resolve(root, 'plugin-src/client')),
46
72
  stat(resolve(root, 'bin/dsh-im.mjs')),
47
73
  ]);
48
74
  const manifest = JSON.parse(manifestText);
@@ -82,17 +108,28 @@ if (forbiddenDshLockPaths.length > 0) {
82
108
  );
83
109
  }
84
110
 
85
- if (!client.includes('id: "@xmanrui/dsh-im"')) {
111
+ if (!/\bid\s*:\s*["']@xmanrui\/dsh-im["']/u.test(client)) {
86
112
  throw new Error('client bundle does not register the dsh-im loader id');
87
113
  }
88
- if (!client.includes('id: "im"')
89
- || !client.includes('label: () => t("IM\\u673A\\u5668\\u4EBA")')
90
- || !client.includes('locale: IM_LOCALE_NAMESPACE')
91
- || !client.includes('IM_LOCALE_NAMESPACE = "dsh-im"')) {
92
- throw new Error('client bundle does not register the localized IM settings tab');
114
+ const sourceSectionMarkers = [
115
+ /ctx\.slots\.inject\(\s*["']settings\.section["']/u,
116
+ /name\s*:\s*["']settings\.section["']/u,
117
+ /id\s*:\s*["']xmanrui-dsh-im["']/u,
118
+ /order\s*:\s*21\b/u,
119
+ /label\s*:\s*\(\)\s*=>\s*t\(\s*["']IM机器人["']\s*\)/u,
120
+ /locale\s*:\s*IM_LOCALE_NAMESPACE\b/u,
121
+ ];
122
+ const bundleSectionPattern = /name\s*:\s*["']settings\.section["']\s*,\s*id\s*:\s*["']xmanrui-dsh-im["']\s*,\s*order\s*:\s*21\s*,\s*label\s*:\s*\(\)\s*=>\s*[$A-Z_a-z][$\w]*\(\s*["']IM(?:机器人|\\u673A\\u5668\\u4EBA)["']\s*\)\s*,\s*locale\s*:\s*(?:[$A-Z_a-z][$\w]*|["']dsh-im["'])/u;
123
+ if (sourceSectionMarkers.some((pattern) => !pattern.test(clientEntrySource))
124
+ || !/IM_LOCALE_NAMESPACE\s*=\s*["']dsh-im["']/u.test(clientSources)
125
+ || !bundleSectionPattern.test(client)) {
126
+ throw new Error('client bundle does not register the localized top-level IM settings section');
127
+ }
128
+ if ((client.match(/\.slots\.inject\(\s*["']settings\.section["']/gu) ?? []).length !== 1) {
129
+ throw new Error('client bundle must register exactly one top-level settings section');
93
130
  }
94
- if ((client.match(/ctx\.slots\.inject\("settings\.plugins\.tab"/g) ?? []).length !== 1) {
95
- throw new Error('client bundle must register exactly one settings tab');
131
+ if (client.includes('settings.plugins.tab') || clientSources.includes('settings.plugins.tab')) {
132
+ throw new Error('client source or bundle still contains the legacy Plugins-tab settings entry');
96
133
  }
97
134
  if (/role:\s*["']switch|type:\s*["']checkbox/.test(client)) {
98
135
  throw new Error('client bundle contains a channel enable switch');
@@ -115,7 +152,7 @@ if (/@xmanrui\/dsh-(?:feishu|weixin|dingtalk)/.test(host)) {
115
152
  throw new Error('host bundle still imports an external channel plugin');
116
153
  }
117
154
  if (/@xmanrui\/dsh-(?:feishu|weixin|dingtalk)/.test(
118
- manifestText + lockText + hostSource + clientSource,
155
+ manifestText + lockText + hostSource + clientSources,
119
156
  )) {
120
157
  throw new Error('source or package metadata still depends on an external channel plugin');
121
158
  }
@@ -159,7 +159,7 @@ function artifactFailureText(fileName, error) {
159
159
  const name = String(fileName ?? t('结果文件')).replace(/[\r\n]+/g, ' ').trim() || t('结果文件');
160
160
  switch (error?.code) {
161
161
  case 'artifact-permission-required':
162
- return t('结果文件「{name}」已生成,但机器人缺少飞书文件上传权限 im:resource。请私聊机器人执行 /repair 命令,或者在插件页面点击“补全权限”按钮并扫码。完成飞书要求的发布审批后重试。', { name });
162
+ return t('结果文件「{name}」已生成,但机器人缺少飞书文件上传权限 im:resource。请私聊机器人执行 /repair 命令,或者在「IM机器人」设置页点击“补全权限”按钮并扫码。完成飞书要求的发布审批后重试。', { name });
163
163
  case 'artifact-too-large':
164
164
  return t('结果文件「{name}」超过飞书 30 MB 上限,未发送。', { name });
165
165
  case 'artifact-empty':
@@ -945,7 +945,10 @@ export class FeishuHarnessBridge {
945
945
  const text = message.content;
946
946
  const hasImages = hasInboundImages(message);
947
947
  const hasFiles = hasInboundFiles(message);
948
- const commandText = event.message.message_type === 'text' && !hasImages && !hasFiles ? text : null;
948
+ // 命令识别对 text 与纯文本 post 一视同仁:post 富文本若仅含单个
949
+ // 文本段落(如复制粘贴的 /new),同样按命令处理;带图片/文件不认。
950
+ // accept() 侧已用 nonEmptyString(content) 判定,两侧保持一致。
951
+ const commandText = !hasImages && !hasFiles && text ? text.trim() : null;
949
952
  if (!text && !hasImages && !hasFiles) {
950
953
  await this.#send(event.message.chat_id, t('目前支持文字、图片和文件消息。'));
951
954
  return;
@@ -5,7 +5,7 @@ const FEISHU_MISSING_MESSAGE_SCOPE_CODE = 99991672;
5
5
  const FEISHU_ERROR_BODY_LIMIT = 64 * 1024;
6
6
  const FEISHU_ERROR_BODY_TIMEOUT_MS = 1_000;
7
7
  const FEISHU_IMAGE_PERMISSION_MESSAGE =
8
- '飞书机器人缺少图片读取权限 im:message:readonly(飞书显示为“获取单聊、群组消息”)。请私聊机器人执行 /repair 命令,或者在插件页面点击“补全权限”按钮并扫码。按飞书提示发布新版本、完成必要审批后,再重新发送图片。';
8
+ '飞书机器人缺少图片读取权限 im:message:readonly(飞书显示为“获取单聊、群组消息”)。请私聊机器人执行 /repair 命令,或者在「IM机器人」设置页点击“补全权限”按钮并扫码。按飞书提示发布新版本、完成必要审批后,再重新发送图片。';
9
9
 
10
10
  export function conversationKey(event) {
11
11
  const chatType = event?.message?.chat_type;
@@ -29,7 +29,7 @@ export function connectionTestTarget(state) {
29
29
 
30
30
  export function connectionTestMessage(botName, channelLabel = t('机器人')) {
31
31
  const name = cleanText(botName) ?? channelLabel;
32
- return `${t('✅ DeepSeek Harness 连接测试成功')}\n${t('这条消息由插件页面中的“{name}”机器人卡片发出。', { name })}`;
32
+ return `${t('✅ DeepSeek Harness 连接测试成功')}\n${t('这条消息由「IM机器人」设置页中的“{name}”机器人卡片发出。', { name })}`;
33
33
  }
34
34
 
35
35
  export function connectionTestTargetUnavailable(channelLabel = t('机器人')) {
@@ -18,8 +18,8 @@ export default {
18
18
  '机器人正在移除或已重新接入,无法操作原会话的工作区。':
19
19
  'The bot is being removed or has been reconnected; the original session’s workspace cannot be changed.',
20
20
  '操作失败,请稍后重试。': 'The operation failed. Please try again later.',
21
- '结果文件「{name}」已生成,但机器人缺少飞书文件上传权限 im:resource。请私聊机器人执行 /repair 命令,或者在插件页面点击“补全权限”按钮并扫码。完成飞书要求的发布审批后重试。':
22
- 'The result file "{name}" was generated, but the bot lacks the Feishu file-upload scope im:resource. Run /repair in a direct chat with the bot, or click the “Complete permissions” button on the plugin page and scan the QR code. Complete any publishing approval Feishu requires, then try again.',
21
+ '结果文件「{name}」已生成,但机器人缺少飞书文件上传权限 im:resource。请私聊机器人执行 /repair 命令,或者在「IM机器人」设置页点击“补全权限”按钮并扫码。完成飞书要求的发布审批后重试。':
22
+ 'The result file "{name}" was generated, but the bot lacks the Feishu file-upload scope im:resource. Run /repair in a direct chat with the bot, or click the “Complete permissions” button on the IM Bot settings page and scan the QR code. Complete any publishing approval Feishu requires, then try again.',
23
23
  '结果文件「{name}」超过飞书 30 MB 上限,未发送。':
24
24
  'The result file "{name}" exceeds the Feishu 30 MB limit and was not sent.',
25
25
  '结果文件「{name}」为空,飞书不允许发送空文件。':
@@ -28,8 +28,8 @@ export default {
28
28
  'The result file "{name}" was temporarily rate-limited by Feishu and could not be sent. Please try again later.',
29
29
  '结果文件「{name}」已生成,但暂时未能发送,请稍后重试。':
30
30
  'The result file "{name}" was generated but could not be sent right now. Please try again later.',
31
- '处理失败,请稍后重试。如果问题持续,请在 DeepSeek Harness 的飞书插件页面检查连接状态。':
32
- 'Message processing failed. Please try again later. If the problem persists, check the connection status on the Feishu plugin page in DeepSeek Harness.',
31
+ '处理失败,请稍后重试。如果问题持续,请在 DeepSeek Harness 的「IM机器人」设置页检查飞书连接状态。':
32
+ 'Message processing failed. Please try again later. If the problem persists, check the Feishu connection status on the IM Bot settings page in DeepSeek Harness.',
33
33
  '已开启全新 Harness 会话。': 'A brand-new Harness session has started.',
34
34
  '飞书机器人与 DeepSeek Harness 连接正常。':
35
35
  'The Feishu bot is connected to DeepSeek Harness and working normally.',
@@ -338,8 +338,8 @@ export default {
338
338
  '飞书机器人': 'Feishu bot',
339
339
 
340
340
  // feishu/message-utils.mjs
341
- '飞书机器人缺少图片读取权限 im:message:readonly(飞书显示为“获取单聊、群组消息”)。请私聊机器人执行 /repair 命令,或者在插件页面点击“补全权限”按钮并扫码。按飞书提示发布新版本、完成必要审批后,再重新发送图片。':
342
- 'The Feishu bot is missing the image-read scope im:message:readonly, shown by Feishu as “Read direct and group messages.” Run /repair in a direct chat with the bot, or click the “Complete permissions” button on the plugin page and scan the QR code. Publish a new version and complete any approval requested by Feishu, then resend the image.',
341
+ '飞书机器人缺少图片读取权限 im:message:readonly(飞书显示为“获取单聊、群组消息”)。请私聊机器人执行 /repair 命令,或者在「IM机器人」设置页点击“补全权限”按钮并扫码。按飞书提示发布新版本、完成必要审批后,再重新发送图片。':
342
+ 'The Feishu bot is missing the image-read scope im:message:readonly, shown by Feishu as “Read direct and group messages.” Run /repair in a direct chat with the bot, or click the “Complete permissions” button on the IM Bot settings page and scan the QR code. Publish a new version and complete any approval requested by Feishu, then resend the image.',
343
343
 
344
344
  // feishu/feishu-runtime.mjs — callback probe notices
345
345
  '✅ 修复完成:已实测收到 card.action.trigger,菜单按钮现在可用。':
@@ -73,8 +73,8 @@ export default {
73
73
  // connection-test.mjs
74
74
  '✅ DeepSeek Harness 连接测试成功':
75
75
  '✅ DeepSeek Harness connection test succeeded',
76
- '这条消息由插件页面中的“{name}”机器人卡片发出。':
77
- 'This message was sent from the "{name}" bot card on the plugin page.',
76
+ '这条消息由「IM机器人」设置页中的“{name}”机器人卡片发出。':
77
+ 'This message was sent from the "{name}" bot card on the IM Bot settings page.',
78
78
  '{channelLabel}尚未收到可用于测试的私聊消息。':
79
79
  'The {channelLabel} has not received a direct message that can be used for testing yet.',
80
80
  '机器人': 'bot',