@sidleo3/dsh-chat-feishu 0.0.4
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/client/index.js +1190 -0
- package/cordis.patch.yml +5 -0
- package/host/bridge.mjs +1605 -0
- package/host/config-store.mjs +240 -0
- package/host/controller.mjs +1514 -0
- package/host/index.mjs +227 -0
- package/host/lark-cli.mjs +543 -0
- package/host/lark-gateway.mjs +1362 -0
- package/host/lark-guard.mjs +200 -0
- package/host/panel-card.mjs +681 -0
- package/host/provision.mjs +247 -0
- package/host/state-store.mjs +160 -0
- package/host/turn-presenter.mjs +778 -0
- package/lib/client.js +1132 -0
- package/lib/index.js +132126 -0
- package/package.json +54 -0
package/client/index.js
ADDED
|
@@ -0,0 +1,1190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-chat-feishu(client 侧):飞书设置页。
|
|
3
|
+
*
|
|
4
|
+
* 页面里只放"飞书特有"的东西(机器人卡片、连接状态、任务过程展示),
|
|
5
|
+
* 渠道无关的面板(上下文增强等)直接用 hub 的共享组件与 hook。
|
|
6
|
+
*
|
|
7
|
+
* @module dsh-chat-feishu/client
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as React from 'react';
|
|
11
|
+
|
|
12
|
+
export const name = 'dsh-chat-feishu-client';
|
|
13
|
+
|
|
14
|
+
export const inject = ['slots', 'locale', 'connection', 'chatChannels', 'chatUi'];
|
|
15
|
+
|
|
16
|
+
const CHANNEL_ID = 'feishu';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 接入提示(渠道级的「渠道设置」入口已经去掉,所以这句话既是空状态说明、
|
|
20
|
+
* 也是渠道元数据里的 `capabilities.setup.hint`——两处必须是同一句)。
|
|
21
|
+
*/
|
|
22
|
+
const FEISHU_SETUP_HINT = '本机还没有飞书机器人配置:把飞书应用的凭据加进渠道的 config.json 后重启 dsh(旧版 dsh-im 的配置会在启动时自动导入)。';
|
|
23
|
+
const PAGE_SLOT = 'chat.channel.page';
|
|
24
|
+
const LOCALE_NAMESPACE = 'dsh-chat-feishu';
|
|
25
|
+
|
|
26
|
+
const zh = {
|
|
27
|
+
'关闭': '关闭',
|
|
28
|
+
'删除': '删除',
|
|
29
|
+
'启用': '启用',
|
|
30
|
+
'填入示例': '填入示例',
|
|
31
|
+
'备注名(可选)': '备注名(可选)',
|
|
32
|
+
'张三': '张三',
|
|
33
|
+
'新增': '新增',
|
|
34
|
+
'来源字段': '来源字段',
|
|
35
|
+
'清空': '清空',
|
|
36
|
+
'还没有指定设置。': '还没有指定设置。',
|
|
37
|
+
'上下文增强': '上下文增强',
|
|
38
|
+
'上下文增强范围': '上下文增强范围',
|
|
39
|
+
'叠加全局提示词(不勾选则只使用上面的专属提示词)': '叠加全局提示词(不勾选则只使用上面的专属提示词)',
|
|
40
|
+
'告诉模型如何使用来源字段。只填正文,插件会自动包成来源增强块。': '告诉模型如何使用来源字段。只填正文,插件会自动包成来源增强块。',
|
|
41
|
+
'增强提示词': '增强提示词',
|
|
42
|
+
'来源字段只在当前消息已提供时才会发送,不会额外查询平台接口。': '来源字段只在当前消息已提供时才会发送,不会额外查询平台接口。',
|
|
43
|
+
'从会话里选…': '从会话里选…',
|
|
44
|
+
'去开通权限': '去开通权限',
|
|
45
|
+
'属主名单是 *(这台机器人没有属主绕过):能用它的人完全由下面的「访问策略」决定。': '属主名单是 *(这台机器人没有属主绕过):能用它的人完全由下面的「访问策略」决定。',
|
|
46
|
+
'Agent 预设': 'Agent 预设',
|
|
47
|
+
'模型': '模型',
|
|
48
|
+
'默认模型': '默认模型',
|
|
49
|
+
'推理等级': '推理等级',
|
|
50
|
+
'这个模型没有可选的推理等级。': '这个模型没有可选的推理等级。',
|
|
51
|
+
'先选一个模型。': '先选一个模型。',
|
|
52
|
+
'模型默认': '模型默认',
|
|
53
|
+
'部分 provider 读取失败:': '部分 provider 读取失败:',
|
|
54
|
+
'当前 Host 读不到模型目录。': '当前 Host 读不到模型目录。',
|
|
55
|
+
'还没有会话时用哪个模型:选完对下一条消息新建的会话生效。会话内还能单独改(面板的模型下拉)。':
|
|
56
|
+
'还没有会话时用哪个模型:选完对下一条消息新建的会话生效。会话内还能单独改(面板的模型下拉)。',
|
|
57
|
+
|
|
58
|
+
'下拉里是这台机器人用过的目录。': '下拉里是这台机器人用过的目录。',
|
|
59
|
+
'仅名单内可用': '仅名单内可用',
|
|
60
|
+
'从会话里选一个人设为属主': '从会话里选一个人设为属主',
|
|
61
|
+
'任何人可用': '任何人可用',
|
|
62
|
+
'允许执行命令': '允许执行命令',
|
|
63
|
+
'可执行命令': '可执行命令',
|
|
64
|
+
'名单为空时只有属主可用。': '名单为空时只有属主可用。',
|
|
65
|
+
'对方的平台 id,回车添加': '对方的平台 id,回车添加',
|
|
66
|
+
'读不到名单里的名字': '读不到名单里的名字',
|
|
67
|
+
'属主': '属主',
|
|
68
|
+
'属主不需要进白名单:消息与命令都直接放行。这里改完会重连一次,立刻生效。': '属主不需要进白名单:消息与命令都直接放行。这里改完会重连一次,立刻生效。',
|
|
69
|
+
'工作区': '工作区',
|
|
70
|
+
'当前 Host 读不到 Agent Preset 列表。': '当前 Host 读不到 Agent Preset 列表。',
|
|
71
|
+
'当前没有属主:没有人绕过访问策略,谁能用完全由下面的「访问策略」决定。': '当前没有属主:没有人绕过访问策略,谁能用完全由下面的「访问策略」决定。',
|
|
72
|
+
'机器人跑在哪个目录:能读写哪些文件、用哪份 AGENTS.md。只对新建会话生效。': '机器人跑在哪个目录:能读写哪些文件、用哪份 AGENTS.md。只对新建会话生效。',
|
|
73
|
+
'没有可选的会话(先和机器人聊一次)': '没有可选的会话(先和机器人聊一次)',
|
|
74
|
+
'添加': '添加',
|
|
75
|
+
'清空后没有人绕过访问策略': '清空后没有人绕过访问策略',
|
|
76
|
+
'清空(无属主)': '清空(无属主)',
|
|
77
|
+
'目录': '目录',
|
|
78
|
+
'显示项': '显示项',
|
|
79
|
+
'控制面板显示项': '控制面板显示项',
|
|
80
|
+
'只影响 /menu 发出来的那张卡片:关掉的项不显示,功能照旧(私聊与群聊分别设置)。': '只影响 /menu 发出来的那张卡片:关掉的项不显示,功能照旧(私聊与群聊分别设置)。',
|
|
81
|
+
'模型与推理等级': '模型与推理等级',
|
|
82
|
+
'会话': '会话',
|
|
83
|
+
'Agent 预设与工作区': 'Agent 预设与工作区',
|
|
84
|
+
'上下文增强(本会话)': '上下文增强(本会话)',
|
|
85
|
+
'访问策略(本会话)': '访问策略(本会话)',
|
|
86
|
+
'渠道设置(任务过程展示等)': '渠道设置(任务过程展示等)',
|
|
87
|
+
'渠道动作按钮(重连等)': '渠道动作按钮(重连等)',
|
|
88
|
+
'命令按钮(新会话/状态/诊断…)': '命令按钮(新会话/状态/诊断…)',
|
|
89
|
+
'私聊': '私聊',
|
|
90
|
+
'移除': '移除',
|
|
91
|
+
'群聊': '群聊',
|
|
92
|
+
'设为属主': '设为属主',
|
|
93
|
+
'访问模式': '访问模式',
|
|
94
|
+
'访问策略': '访问策略',
|
|
95
|
+
'谁能跟机器人说话、谁能执行命令。改动立即生效;属主始终可用。': '谁能跟机器人说话、谁能执行命令。改动立即生效;属主始终可用。',
|
|
96
|
+
'跟随 Host 默认': '跟随 Host 默认',
|
|
97
|
+
'这个机器人用哪套 Agent 预设(人设与工具集)。只对新建会话生效。': '这个机器人用哪套 Agent 预设(人设与工具集)。只对新建会话生效。',
|
|
98
|
+
'飞书': '飞书',
|
|
99
|
+
'找不到这台机器人': '找不到这台机器人',
|
|
100
|
+
'它不在当前渠道的名单里(可能已被移除,或 Host 与页面版本不一致)': '它不在当前渠道的名单里(可能已被移除,或 Host 与页面版本不一致)',
|
|
101
|
+
'飞书渠道': '飞书渠道',
|
|
102
|
+
'已接入的机器人': '已接入的机器人',
|
|
103
|
+
'读取状态': '读取状态',
|
|
104
|
+
'读取中…': '读取中…',
|
|
105
|
+
'重新连接': '重新连接',
|
|
106
|
+
'移除接入': '移除接入',
|
|
107
|
+
'确认移除': '确认移除',
|
|
108
|
+
'取消': '取消',
|
|
109
|
+
'任务过程展示': '任务过程展示',
|
|
110
|
+
'设置执行过程的呈现方式;私聊与群聊分别生效': '设置执行过程的呈现方式;私聊与群聊分别生效',
|
|
111
|
+
'不显示过程(只发送最终答案)': '不显示过程(只发送最终答案)',
|
|
112
|
+
'适合日常问答:不显示工具调用等中间步骤,最终答案仍用一张卡片回复,表格与代码块保留格式':
|
|
113
|
+
'适合日常问答:不显示工具调用等中间步骤,最终答案仍用一张卡片回复,表格与代码块保留格式',
|
|
114
|
+
'实时过程卡(全程一张卡片动态更新)': '实时过程卡(全程一张卡片动态更新)',
|
|
115
|
+
'推荐长任务使用:过程与最终答案都在同一张卡片里实时更新,不刷屏':
|
|
116
|
+
'推荐长任务使用:过程与最终答案都在同一张卡片里实时更新,不刷屏',
|
|
117
|
+
'逐步直播(每一步单独发一条消息)': '逐步直播(每一步单独发一条消息)',
|
|
118
|
+
'每一步都单独发一条消息(含工具调用);长任务会连续发送较多消息':
|
|
119
|
+
'每一步都单独发一条消息(含工具调用);长任务会连续发送较多消息',
|
|
120
|
+
'lark-cli 身份': 'lark-cli 身份',
|
|
121
|
+
'这台机器人调 lark-cli 时用哪个身份;飞书渠道的收发仍走官方 SDK': '这台机器人调 lark-cli 时用哪个身份;飞书渠道的收发仍走官方 SDK',
|
|
122
|
+
'调用身份': '调用身份',
|
|
123
|
+
'只用应用身份(bot)': '只用应用身份(bot)',
|
|
124
|
+
'允许用户身份(--as user)': '允许用户身份(--as user)',
|
|
125
|
+
'以应用自己的身份调 lark-cli,只能访问这台机器人自己的资源': '以应用自己的身份调 lark-cli,只能访问这台机器人自己的资源',
|
|
126
|
+
'以某个人的名义调 lark-cli(能读写他的云文档、日历等个人资源);开启前需要确认,并会钉住当前登录的人': '以某个人的名义调 lark-cli(能读写他的云文档、日历等个人资源);开启前需要确认,并会钉住当前登录的人',
|
|
127
|
+
'lark-cli 里的 profile': 'lark-cli 里的 profile',
|
|
128
|
+
'lark-cli 里还没有这台机器人的 profile(真正调用时会自动创建)': 'lark-cli 里还没有这台机器人的 profile(真正调用时会自动创建)',
|
|
129
|
+
'读取 lark-cli 状态失败:': '读取 lark-cli 状态失败:',
|
|
130
|
+
'应用身份:': '应用身份:',
|
|
131
|
+
'用户身份:': '用户身份:',
|
|
132
|
+
'可用': '可用',
|
|
133
|
+
'不可用': '不可用',
|
|
134
|
+
'登录人:': '登录人:',
|
|
135
|
+
'没有用户登录': '没有用户登录',
|
|
136
|
+
'确认开启': '确认开启',
|
|
137
|
+
'已取消,未做任何改动。': '已取消,未做任何改动。',
|
|
138
|
+
'已保存。': '已保存。',
|
|
139
|
+
'重读 lark-cli 状态': '重读 lark-cli 状态',
|
|
140
|
+
'已处理消息': '已处理消息',
|
|
141
|
+
'没有已接入的飞书机器人': '没有已接入的飞书机器人',
|
|
142
|
+
'本机还没有飞书机器人配置。': '本机还没有飞书机器人配置。',
|
|
143
|
+
'状态': '状态',
|
|
144
|
+
'启动中': '启动中',
|
|
145
|
+
'运行正常': '运行正常',
|
|
146
|
+
'启动失败': '启动失败',
|
|
147
|
+
'已停止': '已停止',
|
|
148
|
+
'正在运行': '正在运行',
|
|
149
|
+
// hub 的共享组件(上下文增强编辑器)用本渠道的 t 取文案,因此这些键必须在渠道字典里。
|
|
150
|
+
'保存': '保存',
|
|
151
|
+
'保存中…': '保存中…',
|
|
152
|
+
'新建机器人接入': '新建机器人接入',
|
|
153
|
+
'两种方式:扫码新建一个飞书机器人(推荐,扫码的人就是属主),或手动填已有机器人的凭据。': '两种方式:扫码新建一个飞书机器人(推荐,扫码的人就是属主),或手动填已有机器人的凭据。',
|
|
154
|
+
'扫码新建机器人': '扫码新建机器人',
|
|
155
|
+
'点下面的按钮会向飞书申请一个一次性授权链接:用飞书扫一扫,或在浏览器里打开它,就会自动创建应用并接入。': '点下面的按钮会向飞书申请一个一次性授权链接:用飞书扫一扫,或在浏览器里打开它,就会自动创建应用并接入。',
|
|
156
|
+
'手动接入已有机器人': '手动接入已有机器人',
|
|
157
|
+
'打开授权页面': '打开授权页面',
|
|
158
|
+
'这台 Host 没能生成二维码,请点上面的链接继续。': '这台 Host 没能生成二维码,请点上面的链接继续。',
|
|
159
|
+
'剩余': '剩余',
|
|
160
|
+
'正在向飞书申请二维码…': '正在向飞书申请二维码…',
|
|
161
|
+
'已授权,正在启动机器人…': '已授权,正在启动机器人…',
|
|
162
|
+
'等待扫码': '等待扫码',
|
|
163
|
+
'等待你在飞书里确认…': '等待你在飞书里确认…',
|
|
164
|
+
'二维码已失效,请重新生成': '二维码已失效,请重新生成',
|
|
165
|
+
'扫码接入失败': '扫码接入失败',
|
|
166
|
+
'已取消': '已取消',
|
|
167
|
+
'重新生成二维码': '重新生成二维码',
|
|
168
|
+
'填自建应用的 App ID 与 App Secret(飞书开放平台 → 凭证与基础信息)。应用需要开启机器人能力。': '填自建应用的 App ID 与 App Secret(飞书开放平台 → 凭证与基础信息)。应用需要开启机器人能力。',
|
|
169
|
+
'应用凭证里的 App Secret': '应用凭证里的 App Secret',
|
|
170
|
+
'域名': '域名',
|
|
171
|
+
'飞书(open.feishu.cn)': '飞书(open.feishu.cn)',
|
|
172
|
+
'Lark(open.larksuite.com)': 'Lark(open.larksuite.com)',
|
|
173
|
+
'属主 open_id(可留空)': '属主 open_id(可留空)',
|
|
174
|
+
'属主留空 = 这台机器人暂时没有属主:接入后私聊会被设为「任何人可用」,属主先跟它说一句话,再回它的设置里把自己选成属主。': '属主留空 = 这台机器人暂时没有属主:接入后私聊会被设为「任何人可用」,属主先跟它说一句话,再回它的设置里把自己选成属主。',
|
|
175
|
+
'接入': '接入',
|
|
176
|
+
'接入中…': '接入中…',
|
|
177
|
+
'私聊访问策略没能自动放宽': '私聊访问策略没能自动放宽',
|
|
178
|
+
'已接入': '已接入',
|
|
179
|
+
'点左上角「返回」就能在列表里看到它。': '点左上角「返回」就能在列表里看到它。',
|
|
180
|
+
'保存失败,请重试。': '保存失败,请重试。',
|
|
181
|
+
'最近一次错误': '最近一次错误',
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const en = {
|
|
185
|
+
'显示项': 'Section',
|
|
186
|
+
'控制面板显示项': 'Control panel sections',
|
|
187
|
+
'只影响 /menu 发出来的那张卡片:关掉的项不显示,功能照旧(私聊与群聊分别设置)。': 'Only affects the card sent by /menu: hidden items are not drawn, everything keeps working (direct and group are configured separately).',
|
|
188
|
+
'模型与推理等级': 'Model & reasoning',
|
|
189
|
+
'会话': 'Session',
|
|
190
|
+
'Agent 预设与工作区': 'Agent preset & workspace',
|
|
191
|
+
'上下文增强(本会话)': 'Context enhancement (this chat)',
|
|
192
|
+
'访问策略(本会话)': 'Access policy (this chat)',
|
|
193
|
+
'渠道设置(任务过程展示等)': 'Channel settings (step display etc.)',
|
|
194
|
+
'渠道动作按钮(重连等)': 'Channel actions (reconnect etc.)',
|
|
195
|
+
'命令按钮(新会话/状态/诊断…)': 'Command buttons (new session/status/diagnostics…)',
|
|
196
|
+
'关闭': 'Close',
|
|
197
|
+
'删除': 'Delete',
|
|
198
|
+
'启用': 'Enabled',
|
|
199
|
+
'填入示例': 'Fill with example',
|
|
200
|
+
'备注名(可选)': 'Display name (optional)',
|
|
201
|
+
'张三': 'Alice',
|
|
202
|
+
'新增': 'Add',
|
|
203
|
+
'来源字段': 'Source fields',
|
|
204
|
+
'清空': 'Clear',
|
|
205
|
+
'还没有指定设置。': 'Nothing configured yet.',
|
|
206
|
+
'上下文增强': 'Context enhancement',
|
|
207
|
+
'上下文增强范围': 'Context enhancement scope',
|
|
208
|
+
'叠加全局提示词(不勾选则只使用上面的专属提示词)': 'Also stack the global prompt (unchecked: use only the prompt above)',
|
|
209
|
+
'告诉模型如何使用来源字段。只填正文,插件会自动包成来源增强块。': 'Tell the model how to use the source fields. Write the body only — the plugin wraps it into a source block.',
|
|
210
|
+
'增强提示词': 'Prepended prompt',
|
|
211
|
+
'来源字段只在当前消息已提供时才会发送,不会额外查询平台接口。': 'Source fields are sent only when the incoming message already carries them; no extra platform calls are made.',
|
|
212
|
+
'从会话里选…': 'Pick a conversation…',
|
|
213
|
+
'去开通权限': 'Grant the permission',
|
|
214
|
+
'属主名单是 *(这台机器人没有属主绕过):能用它的人完全由下面的「访问策略」决定。':
|
|
215
|
+
'The owner list is * (this bot has no owner bypass): who may use it is decided entirely by the access policy below.',
|
|
216
|
+
'Agent 预设': 'Agent preset',
|
|
217
|
+
'模型': 'Model',
|
|
218
|
+
'默认模型': 'Default model',
|
|
219
|
+
'推理等级': 'Reasoning effort',
|
|
220
|
+
'这个模型没有可选的推理等级。': 'This model has no reasoning efforts to choose from.',
|
|
221
|
+
'先选一个模型。': 'Pick a model first.',
|
|
222
|
+
'模型默认': 'Model default',
|
|
223
|
+
'部分 provider 读取失败:': 'Some providers failed to load: ',
|
|
224
|
+
'当前 Host 读不到模型目录。': 'The model catalog is unavailable right now.',
|
|
225
|
+
'还没有会话时用哪个模型:选完对下一条消息新建的会话生效。会话内还能单独改(面板的模型下拉)。':
|
|
226
|
+
'Model used before a conversation exists; it applies to the session created by your next message. You can still change it per conversation from the panel.',
|
|
227
|
+
|
|
228
|
+
'下拉里是这台机器人用过的目录。': 'The suggestions are directories this bot has used before.',
|
|
229
|
+
'仅名单内可用': 'Allowlist only',
|
|
230
|
+
'从会话里选一个人设为属主': 'Pick a person from a conversation to make them the owner',
|
|
231
|
+
'任何人可用': 'Anyone',
|
|
232
|
+
'允许执行命令': 'Allow commands',
|
|
233
|
+
'可执行命令': 'Allow commands',
|
|
234
|
+
'名单为空时只有属主可用。': 'An empty allowlist means only the owner can use it.',
|
|
235
|
+
'对方的平台 id,回车添加': 'Their platform id — press Enter to add',
|
|
236
|
+
'读不到名单里的名字': 'Could not resolve names for the allowlist',
|
|
237
|
+
'属主': 'Owner',
|
|
238
|
+
'属主不需要进白名单:消息与命令都直接放行。这里改完会重连一次,立刻生效。': 'An owner does not need to be on the allowlist: their messages and commands always pass. Saving reconnects this bot once so the change takes effect immediately.',
|
|
239
|
+
'工作区': 'Workspace',
|
|
240
|
+
'当前 Host 读不到 Agent Preset 列表。': 'This Host does not expose an agent preset list.',
|
|
241
|
+
'当前没有属主:没有人绕过访问策略,谁能用完全由下面的「访问策略」决定。': 'No owner right now: nobody bypasses the access policy, so who may use this bot is decided entirely by the access policy below.',
|
|
242
|
+
'机器人跑在哪个目录:能读写哪些文件、用哪份 AGENTS.md。只对新建会话生效。': 'Which directory the bot runs in: which files it may read and write, and which AGENTS.mdapplies. Applies to new conversations only.',
|
|
243
|
+
'没有可选的会话(先和机器人聊一次)': 'No conversation to pick yet (talk to the bot once first)',
|
|
244
|
+
'添加': 'Add',
|
|
245
|
+
'清空后没有人绕过访问策略': 'After clearing, nobody bypasses the access policy',
|
|
246
|
+
'清空(无属主)': 'Clear (no owner)',
|
|
247
|
+
'目录': 'Directory',
|
|
248
|
+
'私聊': 'Direct',
|
|
249
|
+
'移除': 'Remove',
|
|
250
|
+
'群聊': 'Group',
|
|
251
|
+
'设为属主': 'Make owner',
|
|
252
|
+
'访问模式': 'Access mode',
|
|
253
|
+
'访问策略': 'Access policy',
|
|
254
|
+
'谁能跟机器人说话、谁能执行命令。改动立即生效;属主始终可用。': 'Who may talk to the bot and who may run commands. Changes apply immediately;the owner always has access.',
|
|
255
|
+
'跟随 Host 默认': 'Follow the Host default',
|
|
256
|
+
'这个机器人用哪套 Agent 预设(人设与工具集)。只对新建会话生效。': 'Which agent preset this bot uses (persona and tool set). Applies to new conversations only.',
|
|
257
|
+
'飞书': 'Feishu',
|
|
258
|
+
'找不到这台机器人': 'Bot not found',
|
|
259
|
+
'它不在当前渠道的名单里(可能已被移除,或 Host 与页面版本不一致)':
|
|
260
|
+
"It is not in this channel's bot list (it may have been removed, or the Host and the page are on different versions)",
|
|
261
|
+
'飞书渠道': 'Feishu channel',
|
|
262
|
+
'已接入的机器人': 'Connected bots',
|
|
263
|
+
'读取状态': 'Reload',
|
|
264
|
+
'读取中…': 'Loading…',
|
|
265
|
+
'重新连接': 'Reconnect',
|
|
266
|
+
'移除接入': 'Remove',
|
|
267
|
+
'确认移除': 'Confirm removal',
|
|
268
|
+
'取消': 'Cancel',
|
|
269
|
+
'任务过程展示': 'Task progress display',
|
|
270
|
+
'设置执行过程的呈现方式;私聊与群聊分别生效':
|
|
271
|
+
'Choose how the execution is presented; direct and group chats are configured separately',
|
|
272
|
+
'不显示过程(只发送最终答案)': 'Hide the process (final answer only)',
|
|
273
|
+
'适合日常问答:不显示工具调用等中间步骤,最终答案仍用一张卡片回复,表格与代码块保留格式':
|
|
274
|
+
'For everyday Q&A: interim steps stay hidden; the final answer still arrives as a card, '
|
|
275
|
+
+ 'keeping tables and code blocks formatted',
|
|
276
|
+
'实时过程卡(全程一张卡片动态更新)': 'Live process card (one card updated throughout)',
|
|
277
|
+
'推荐长任务使用:过程与最终答案都在同一张卡片里实时更新,不刷屏':
|
|
278
|
+
'Recommended for long tasks: process and answer update in one card without flooding the chat',
|
|
279
|
+
'逐步直播(每一步单独发一条消息)': 'Step-by-step feed (one message per step)',
|
|
280
|
+
'每一步都单独发一条消息(含工具调用);长任务会连续发送较多消息':
|
|
281
|
+
'Every step is its own message; long tasks send many messages',
|
|
282
|
+
'lark-cli 身份': 'lark-cli identity',
|
|
283
|
+
'这台机器人调 lark-cli 时用哪个身份;飞书渠道的收发仍走官方 SDK': 'Which identity this bot uses when calling lark-cli; message send/receive still goes through the official SDK',
|
|
284
|
+
'调用身份': 'Identity',
|
|
285
|
+
'只用应用身份(bot)': 'Application identity only (bot)',
|
|
286
|
+
'允许用户身份(--as user)': 'Allow user identity (--as user)',
|
|
287
|
+
'以应用自己的身份调 lark-cli,只能访问这台机器人自己的资源': 'Calls lark-cli as the app itself; only this bot own resources are reachable',
|
|
288
|
+
'以某个人的名义调 lark-cli(能读写他的云文档、日历等个人资源);开启前需要确认,并会钉住当前登录的人': 'Calls lark-cli on behalf of a person (their docs, calendar and other personal resources); enabling needs confirmation and pins the currently signed-in user',
|
|
289
|
+
'lark-cli 里的 profile': 'Profile in lark-cli',
|
|
290
|
+
'lark-cli 里还没有这台机器人的 profile(真正调用时会自动创建)': 'lark-cli has no profile for this bot yet (created automatically on first real call)',
|
|
291
|
+
'读取 lark-cli 状态失败:': 'Failed to read lark-cli status: ',
|
|
292
|
+
'应用身份:': 'App identity: ',
|
|
293
|
+
'用户身份:': 'User identity: ',
|
|
294
|
+
'可用': 'available',
|
|
295
|
+
'不可用': 'not available',
|
|
296
|
+
'登录人:': 'Signed-in user: ',
|
|
297
|
+
'没有用户登录': 'no user signed in',
|
|
298
|
+
'确认开启': 'Confirm',
|
|
299
|
+
'已取消,未做任何改动。': 'Cancelled; nothing changed.',
|
|
300
|
+
'已保存。': 'Saved.',
|
|
301
|
+
'重读 lark-cli 状态': 'Re-read lark-cli status',
|
|
302
|
+
'已处理消息': 'Messages handled',
|
|
303
|
+
'没有已接入的飞书机器人': 'No Feishu bot connected',
|
|
304
|
+
'本机还没有飞书机器人配置。': 'This Host has no Feishu bot configured yet.',
|
|
305
|
+
'状态': 'State',
|
|
306
|
+
'启动中': 'Starting',
|
|
307
|
+
'运行正常': 'Connected',
|
|
308
|
+
'启动失败': 'Failed',
|
|
309
|
+
'已停止': 'Stopped',
|
|
310
|
+
'正在运行': 'Running',
|
|
311
|
+
'保存': 'Save',
|
|
312
|
+
'保存中…': 'Saving…',
|
|
313
|
+
'新建机器人接入': 'Add a bot',
|
|
314
|
+
'两种方式:扫码新建一个飞书机器人(推荐,扫码的人就是属主),或手动填已有机器人的凭据。': 'Two ways: scan to create a new Feishu bot (recommended — whoever scans becomes the owner), or paste the credentials of an existing bot.',
|
|
315
|
+
'扫码新建机器人': 'Scan to create a bot',
|
|
316
|
+
'点下面的按钮会向飞书申请一个一次性授权链接:用飞书扫一扫,或在浏览器里打开它,就会自动创建应用并接入。': 'This requests a one-time authorization link from Feishu. Scan it with Feishu (or open it in your browser) and a new app is created and connected automatically.',
|
|
317
|
+
'手动接入已有机器人': 'Connect an existing bot manually',
|
|
318
|
+
'打开授权页面': 'Open the authorization page',
|
|
319
|
+
'这台 Host 没能生成二维码,请点上面的链接继续。': 'This Host could not render the QR code — use the link above instead.',
|
|
320
|
+
'剩余': 'expires in',
|
|
321
|
+
'正在向飞书申请二维码…': 'Requesting the QR code from Feishu…',
|
|
322
|
+
'已授权,正在启动机器人…': 'Authorized — starting the bot…',
|
|
323
|
+
'等待扫码': 'Waiting for the scan',
|
|
324
|
+
'等待你在飞书里确认…': 'Waiting for you to confirm in Feishu…',
|
|
325
|
+
'二维码已失效,请重新生成': 'The link expired — generate a new one',
|
|
326
|
+
'扫码接入失败': 'Scan-based setup failed',
|
|
327
|
+
'已取消': 'Cancelled',
|
|
328
|
+
'重新生成二维码': 'Generate a new QR code',
|
|
329
|
+
'填自建应用的 App ID 与 App Secret(飞书开放平台 → 凭证与基础信息)。应用需要开启机器人能力。': 'Paste the App ID and App Secret of your own app (Feishu Open Platform → Credentials & Basic Info). The app must have the bot capability enabled.',
|
|
330
|
+
'应用凭证里的 App Secret': 'The App Secret from the app credentials',
|
|
331
|
+
'域名': 'Domain',
|
|
332
|
+
'飞书(open.feishu.cn)': 'Feishu (open.feishu.cn)',
|
|
333
|
+
'Lark(open.larksuite.com)': 'Lark (open.larksuite.com)',
|
|
334
|
+
'属主 open_id(可留空)': 'Owner open_id (optional)',
|
|
335
|
+
'属主留空 = 这台机器人暂时没有属主:接入后私聊会被设为「任何人可用」,属主先跟它说一句话,再回它的设置里把自己选成属主。': 'Leave the owner empty and this bot starts with no owner. Direct chats are then set to "anyone" so the owner can say hello first, and pick themselves as owner afterwards in this bot’s settings.',
|
|
336
|
+
'接入': 'Add',
|
|
337
|
+
'接入中…': 'Adding…',
|
|
338
|
+
'私聊访问策略没能自动放宽': 'Could not relax the direct-chat access policy',
|
|
339
|
+
'已接入': 'Added',
|
|
340
|
+
'点左上角「返回」就能在列表里看到它。': 'Click Back at the top left to see it in the list.',
|
|
341
|
+
'保存失败,请重试。': 'Could not save. Try again.',
|
|
342
|
+
'最近一次错误': 'Last error',
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const h = React.createElement;
|
|
346
|
+
|
|
347
|
+
const STATE_TEXT = {
|
|
348
|
+
starting: '启动中',
|
|
349
|
+
running: '运行正常',
|
|
350
|
+
failed: '启动失败',
|
|
351
|
+
stopped: '已停止',
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const STEP_PUSH_OPTIONS = [
|
|
355
|
+
{
|
|
356
|
+
value: 'off',
|
|
357
|
+
label: '不显示过程(只发送最终答案)',
|
|
358
|
+
help: '适合日常问答:不显示工具调用等中间步骤,最终答案仍用一张卡片回复,表格与代码块保留格式',
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
value: 'streaming_card',
|
|
362
|
+
label: '实时过程卡(全程一张卡片动态更新)',
|
|
363
|
+
help: '推荐长任务使用:过程与最终答案都在同一张卡片里实时更新,不刷屏',
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
value: 'post',
|
|
367
|
+
label: '逐步直播(每一步单独发一条消息)',
|
|
368
|
+
help: '每一步都单独发一条消息(含工具调用);长任务会连续发送较多消息',
|
|
369
|
+
},
|
|
370
|
+
];
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* lark-cli 身份:这台机器人调 lark-cli 时用哪个身份。
|
|
374
|
+
*
|
|
375
|
+
* 为什么要有这个开关:lark-cli 里可以同时登录**多个应用**,谁不显式指定 profile、
|
|
376
|
+
* 谁省略 `--as`,谁就可能在用别人的授权。策略(能不能用用户身份)由这里设置;
|
|
377
|
+
* **真正调用时的绑定与断言**在 host 的 `lark-cli.mjs`(唯一入口)里做——
|
|
378
|
+
* 它每次都会核对自己是不是那台应用、以及用户是不是这里钉住的那个人。
|
|
379
|
+
*/
|
|
380
|
+
const LARK_IDENTITY_OPTIONS = [
|
|
381
|
+
{
|
|
382
|
+
value: 'bot-only',
|
|
383
|
+
label: '只用应用身份(bot)',
|
|
384
|
+
help: '以应用自己的身份调 lark-cli,只能访问这台机器人自己的资源',
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
value: 'user-allowed',
|
|
388
|
+
label: '允许用户身份(--as user)',
|
|
389
|
+
help: '以某个人的名义调 lark-cli(能读写他的云文档、日历等个人资源);开启前需要确认,并会钉住当前登录的人',
|
|
390
|
+
},
|
|
391
|
+
];
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* 「lark-cli 身份」设置块。
|
|
395
|
+
*
|
|
396
|
+
* 开启用户身份是**放权**(能以某个人的名义读写他的个人资源),所以照访问策略那条口径:
|
|
397
|
+
* 服务端不带 `confirm: true` 就只回 `requiresConfirm`、**一个字节都不写**,确认画在同一张卡上。
|
|
398
|
+
*
|
|
399
|
+
* @param props - { botId, value, chatUi, connection, translate, onChanged }。
|
|
400
|
+
* @returns React 元素。
|
|
401
|
+
*/
|
|
402
|
+
function LarkIdentityEditor({ botId, value, chatUi, connection, translate, onChanged }) {
|
|
403
|
+
const t = typeof translate === 'function' ? translate : (key) => key;
|
|
404
|
+
const { Panel } = chatUi.components;
|
|
405
|
+
const [probe, setProbe] = React.useState({ phase: 'loading', value: null, error: null });
|
|
406
|
+
const [pending, setPending] = React.useState(null);
|
|
407
|
+
const [notice, setNotice] = React.useState(null);
|
|
408
|
+
const [error, setError] = React.useState(null);
|
|
409
|
+
const [busy, setBusy] = React.useState(false);
|
|
410
|
+
|
|
411
|
+
/** 只读体检:lark-cli 里到底有没有这台机器人的 profile、现在会以谁的身份说话。 */
|
|
412
|
+
const load = React.useCallback(async () => {
|
|
413
|
+
try {
|
|
414
|
+
const result = await chatUi.callChannelRpc(connection, CHANNEL_ID, 'bot.lark-identity.get', { botId });
|
|
415
|
+
setProbe({ phase: 'ready', value: chatUi.unwrapRpc(result), error: null });
|
|
416
|
+
} catch (cause) {
|
|
417
|
+
// 读失败与"没开启/没登录"是两回事:读失败必须如实说,不能显示成默认值。
|
|
418
|
+
setProbe({ phase: 'failed', value: null, error: cause?.message ?? String(cause) });
|
|
419
|
+
}
|
|
420
|
+
}, [botId, chatUi, connection]);
|
|
421
|
+
|
|
422
|
+
React.useEffect(() => { void load(); }, [load]);
|
|
423
|
+
|
|
424
|
+
const submit = async (next, { confirm = false } = {}) => {
|
|
425
|
+
setBusy(true);
|
|
426
|
+
setError(null);
|
|
427
|
+
try {
|
|
428
|
+
const result = await chatUi.callChannelRpc(connection, CHANNEL_ID, 'bot.lark-identity.set', {
|
|
429
|
+
botId, value: next, confirm,
|
|
430
|
+
});
|
|
431
|
+
const applied = chatUi.unwrapRpc(result);
|
|
432
|
+
if (applied?.requiresConfirm === true) {
|
|
433
|
+
setPending({ value: next, prompt: applied.confirmPrompt ?? applied.message ?? '' });
|
|
434
|
+
setNotice(null);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
setPending(null);
|
|
438
|
+
setNotice(applied?.message ?? t('已保存。'));
|
|
439
|
+
await load();
|
|
440
|
+
await onChanged?.();
|
|
441
|
+
} catch (cause) {
|
|
442
|
+
setError(cause?.message ?? String(cause));
|
|
443
|
+
} finally {
|
|
444
|
+
setBusy(false);
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const mode = value?.mode === 'user-allowed' ? 'user-allowed' : 'bot-only';
|
|
449
|
+
const selected = LARK_IDENTITY_OPTIONS.find((item) => item.value === mode) ?? LARK_IDENTITY_OPTIONS[0];
|
|
450
|
+
const info = probe.value;
|
|
451
|
+
const line = (label, text) => h('div', { className: 'dchat-scopeRow' },
|
|
452
|
+
h('span', { className: 'dchat-scopeLabel' }, label),
|
|
453
|
+
h('span', { className: 'dchat-cardDescription' }, text));
|
|
454
|
+
const identityText = (entry, empty) => {
|
|
455
|
+
if (!entry) return empty;
|
|
456
|
+
if (entry.error) return `${t('不可用')}(${entry.error.code})`;
|
|
457
|
+
if (entry.available === false) return t('不可用');
|
|
458
|
+
return t('可用');
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
return h(Panel, {
|
|
462
|
+
title: t('lark-cli 身份'),
|
|
463
|
+
description: t('这台机器人调 lark-cli 时用哪个身份;飞书渠道的收发仍走官方 SDK'),
|
|
464
|
+
actions: h('button', {
|
|
465
|
+
type: 'button',
|
|
466
|
+
className: 'dchat-button',
|
|
467
|
+
disabled: busy,
|
|
468
|
+
onClick: () => { void load(); },
|
|
469
|
+
}, t('重读 lark-cli 状态')),
|
|
470
|
+
},
|
|
471
|
+
h('div', { className: 'dchat-scopeGrid' },
|
|
472
|
+
h('div', { className: 'dchat-scopeRow' },
|
|
473
|
+
h('label', {
|
|
474
|
+
className: 'dchat-scopeLabel',
|
|
475
|
+
htmlFor: `lark-identity-${botId}`,
|
|
476
|
+
}, t('调用身份')),
|
|
477
|
+
h('select', {
|
|
478
|
+
id: `lark-identity-${botId}`,
|
|
479
|
+
className: 'dchat-select',
|
|
480
|
+
// 布局守门按这个属性核对"控件状态确实来自假数据"。
|
|
481
|
+
'data-lark-identity': mode,
|
|
482
|
+
value: mode,
|
|
483
|
+
disabled: busy,
|
|
484
|
+
onChange: (event) => {
|
|
485
|
+
setPending(null);
|
|
486
|
+
void submit(event.target.value);
|
|
487
|
+
},
|
|
488
|
+
}, LARK_IDENTITY_OPTIONS.map((option) => h('option', {
|
|
489
|
+
key: option.value,
|
|
490
|
+
value: option.value,
|
|
491
|
+
}, t(option.label)))))),
|
|
492
|
+
h('p', { className: 'dchat-cardDescription' }, t(selected.help)),
|
|
493
|
+
pending ? h('div', { className: 'dchat-warning', role: 'alert' },
|
|
494
|
+
h('p', null, pending.prompt),
|
|
495
|
+
h('div', { className: 'dchat-actions' },
|
|
496
|
+
h('button', {
|
|
497
|
+
type: 'button',
|
|
498
|
+
className: 'dchat-button dchat-buttonPrimary',
|
|
499
|
+
disabled: busy,
|
|
500
|
+
onClick: () => { void submit(pending.value, { confirm: true }); },
|
|
501
|
+
}, t('确认开启')),
|
|
502
|
+
h('button', {
|
|
503
|
+
type: 'button',
|
|
504
|
+
className: 'dchat-button',
|
|
505
|
+
disabled: busy,
|
|
506
|
+
onClick: () => {
|
|
507
|
+
setPending(null);
|
|
508
|
+
setNotice(t('已取消,未做任何改动。'));
|
|
509
|
+
},
|
|
510
|
+
}, t('取消')))) : null,
|
|
511
|
+
notice ? h('p', { className: 'dchat-cardDescription', role: 'status' }, notice) : null,
|
|
512
|
+
error ? h('p', { className: 'dchat-error', role: 'alert' }, error) : null,
|
|
513
|
+
probe.phase === 'failed'
|
|
514
|
+
? h('p', { className: 'dchat-error', role: 'alert' }, `${t('读取 lark-cli 状态失败:')}${probe.error}`)
|
|
515
|
+
: null,
|
|
516
|
+
probe.phase === 'ready' && info
|
|
517
|
+
? line(t('lark-cli 里的 profile'), info.profile?.found
|
|
518
|
+
? `${info.profile.name ?? '?'}(${info.profile.appId ?? '?'})`
|
|
519
|
+
: t('lark-cli 里还没有这台机器人的 profile(真正调用时会自动创建)'))
|
|
520
|
+
: null,
|
|
521
|
+
probe.phase === 'ready' && info?.identity
|
|
522
|
+
? line(t('应用身份:'), identityText(info.identity.bot, t('不可用')))
|
|
523
|
+
: null,
|
|
524
|
+
probe.phase === 'ready' && info?.identity
|
|
525
|
+
? line(t('用户身份:'), info.identity.user?.onBehalfOf?.openId
|
|
526
|
+
? `${t('登录人:')}${info.identity.user.onBehalfOf.userName ?? ''}`
|
|
527
|
+
+ ` (${info.identity.user.onBehalfOf.openId})`
|
|
528
|
+
: (identityText(info.identity.user, t('没有用户登录'))))
|
|
529
|
+
: null,
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// 导出给布局守门用:整张渠道卡在窄栏下的真实渲染(真机炸过「卡片头逐字竖排」)。
|
|
534
|
+
export function BotCard({ bot, status, chatUi, connection, translate, onChanged }) {
|
|
535
|
+
const t = typeof translate === 'function' ? translate : (key) => key;
|
|
536
|
+
const { Panel, StatusPill, ScopedModeEditor, ContextEnhancementEditor,
|
|
537
|
+
DeliveryTargetsEditor, WorkspaceEditor, PresetEditor, ModelEditor,
|
|
538
|
+
AccessPolicyEditor, OwnerEditor, PanelSectionsEditor } = chatUi.components;
|
|
539
|
+
const settings = chatUi.hooks.useBotSettings({
|
|
540
|
+
connection, channelId: CHANNEL_ID, botId: bot.id,
|
|
541
|
+
});
|
|
542
|
+
// 可选项(这台机器人用过的目录、Host 的 Agent Preset 列表)进来读一次。
|
|
543
|
+
React.useEffect(() => {
|
|
544
|
+
void settings.loadOptions();
|
|
545
|
+
}, [settings.loadOptions]);
|
|
546
|
+
/**
|
|
547
|
+
* 会话 → 该应用的**平台 id**(`ou_…` / `oc_…`)。
|
|
548
|
+
*
|
|
549
|
+
* 这一步只有渠道能做:`route` 的字段名是平台概念(飞书是 openId/chatId,微信是 userId)。
|
|
550
|
+
* 属主与「指定用户/指定群」要的都是平台 id,而投递目标的 id(`p2p_ou_…`)**不是**——
|
|
551
|
+
* 混用会被 host 的校验挡下(属主)或永远匹配不上(上下文增强,静默失效)。
|
|
552
|
+
*/
|
|
553
|
+
const sessions = chatUi.hooks.useConversations({
|
|
554
|
+
connection, channelId: CHANNEL_ID, botId: bot.id,
|
|
555
|
+
}).conversations
|
|
556
|
+
.map((item) => ({
|
|
557
|
+
id: item.kind === 'group' ? item.route?.chatId : item.route?.openId,
|
|
558
|
+
name: item.name,
|
|
559
|
+
kind: item.kind,
|
|
560
|
+
}))
|
|
561
|
+
.filter((item) => typeof item.id === 'string' && item.id);
|
|
562
|
+
/**
|
|
563
|
+
* 访问策略白名单里那些 id 是谁。
|
|
564
|
+
*
|
|
565
|
+
* 名单里存的只有 `ou_…` / `oc_…`:设置页上只显示 id 的话,一排 id 认不出是谁、
|
|
566
|
+
* 也看不出加错了人(真机反馈"群了白名单 只显示 id 不显示名称,不方便管理")。
|
|
567
|
+
* 换名字只有渠道能做(要 `contact` / `im:chat:readonly` 权限),所以这一步放在渠道里,
|
|
568
|
+
* 共享的编辑器只负责画"名字 + id"。查不到就退回只显示 id,并把原因写在下面。
|
|
569
|
+
*/
|
|
570
|
+
const policyIds = React.useMemo(() => {
|
|
571
|
+
const policy = settings.record?.accessPolicy;
|
|
572
|
+
const users = [
|
|
573
|
+
...(policy?.direct?.allowlist?.users ?? []),
|
|
574
|
+
...(policy?.group?.allowlist?.users ?? []),
|
|
575
|
+
];
|
|
576
|
+
return [...new Set(users
|
|
577
|
+
.map((user) => user?.id)
|
|
578
|
+
.filter((id) => typeof id === 'string' && id))];
|
|
579
|
+
}, [settings.record?.accessPolicy]);
|
|
580
|
+
const policyIdsKey = policyIds.join(',');
|
|
581
|
+
const [policyNames, setPolicyNames] = React.useState({ names: null, hint: null });
|
|
582
|
+
React.useEffect(() => {
|
|
583
|
+
if (policyIdsKey === '') {
|
|
584
|
+
setPolicyNames({ names: null, hint: null });
|
|
585
|
+
return undefined;
|
|
586
|
+
}
|
|
587
|
+
let cancelled = false;
|
|
588
|
+
void (async () => {
|
|
589
|
+
try {
|
|
590
|
+
const result = await chatUi.callChannelRpc(connection, CHANNEL_ID, 'names.resolve', {
|
|
591
|
+
botId: bot.id, ids: policyIdsKey.split(','),
|
|
592
|
+
});
|
|
593
|
+
const value = chatUi.unwrapRpc(result);
|
|
594
|
+
if (!cancelled) setPolicyNames({ names: value?.names ?? {}, hint: value?.hint ?? null });
|
|
595
|
+
} catch (cause) {
|
|
596
|
+
// 换不到名字不影响策略本身:照样能改名单,只是显示 id,并把原因说清楚。
|
|
597
|
+
if (!cancelled) setPolicyNames({
|
|
598
|
+
names: null,
|
|
599
|
+
hint: { message: `${t('读不到名单里的名字')}:${cause.message}` },
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
})();
|
|
603
|
+
return () => { cancelled = true; };
|
|
604
|
+
}, [chatUi, connection, bot.id, policyIdsKey, t]);
|
|
605
|
+
|
|
606
|
+
/** 设属主:写配置 + 渠道重连一次,然后刷新状态。 */
|
|
607
|
+
const saveOwners = async (owners) => {
|
|
608
|
+
const result = await chatUi.callChannelRpc(connection, CHANNEL_ID, 'bot.owner.set', {
|
|
609
|
+
botId: bot.id, ownerOpenIds: owners,
|
|
610
|
+
});
|
|
611
|
+
chatUi.unwrapRpc(result);
|
|
612
|
+
await onChanged?.();
|
|
613
|
+
};
|
|
614
|
+
const shared = {
|
|
615
|
+
workspace: settings.record?.workspace ?? null,
|
|
616
|
+
agentPreset: settings.record?.agentPreset ?? null,
|
|
617
|
+
// 机器人默认模型(还没有会话时用它):与工作区/预设同一条口径,只对新建会话生效。
|
|
618
|
+
model: settings.record?.model ?? null,
|
|
619
|
+
accessPolicy: settings.record?.accessPolicy ?? null,
|
|
620
|
+
// 控制面板卡片的显示项(null = 全显示)。
|
|
621
|
+
panelSections: settings.record?.panelSections ?? null,
|
|
622
|
+
};
|
|
623
|
+
const [busy, setBusy] = React.useState(false);
|
|
624
|
+
const [error, setError] = React.useState(null);
|
|
625
|
+
const [confirming, setConfirming] = React.useState(false);
|
|
626
|
+
|
|
627
|
+
const run = async (method, payload) => {
|
|
628
|
+
setBusy(true);
|
|
629
|
+
setError(null);
|
|
630
|
+
try {
|
|
631
|
+
const result = await chatUi.callChannelRpc(connection, CHANNEL_ID, method, payload);
|
|
632
|
+
return chatUi.unwrapRpc(result);
|
|
633
|
+
} catch (cause) {
|
|
634
|
+
setError(cause.message);
|
|
635
|
+
throw cause;
|
|
636
|
+
} finally {
|
|
637
|
+
setBusy(false);
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
const tone = status.state === 'running' ? 'success'
|
|
642
|
+
: status.state === 'failed' ? 'error' : 'warning';
|
|
643
|
+
|
|
644
|
+
return h(Panel, {
|
|
645
|
+
title: bot.name ?? bot.id,
|
|
646
|
+
description: bot.appIdMasked,
|
|
647
|
+
actions: h('div', { className: 'dchat-actions' },
|
|
648
|
+
h(StatusPill, { status: status.state, label: t(STATE_TEXT[status.state] ?? '状态') }),
|
|
649
|
+
h('button', {
|
|
650
|
+
type: 'button',
|
|
651
|
+
className: 'dchat-button',
|
|
652
|
+
disabled: busy,
|
|
653
|
+
onClick: async () => {
|
|
654
|
+
try {
|
|
655
|
+
await run('bot.reconnect', { botId: bot.id });
|
|
656
|
+
await onChanged?.();
|
|
657
|
+
} catch { /* 错误已展示 */ }
|
|
658
|
+
},
|
|
659
|
+
}, t('重新连接')),
|
|
660
|
+
confirming
|
|
661
|
+
? h(React.Fragment, null,
|
|
662
|
+
h('button', {
|
|
663
|
+
type: 'button',
|
|
664
|
+
className: 'dchat-button dchat-buttonDangerSolid',
|
|
665
|
+
disabled: busy,
|
|
666
|
+
onClick: async () => {
|
|
667
|
+
try {
|
|
668
|
+
await run('bot.delete', { botId: bot.id, confirm: true });
|
|
669
|
+
setConfirming(false);
|
|
670
|
+
await onChanged?.();
|
|
671
|
+
} catch { /* 错误已展示 */ }
|
|
672
|
+
},
|
|
673
|
+
}, t('确认移除')),
|
|
674
|
+
h('button', {
|
|
675
|
+
type: 'button',
|
|
676
|
+
className: 'dchat-button',
|
|
677
|
+
disabled: busy,
|
|
678
|
+
onClick: () => setConfirming(false),
|
|
679
|
+
}, t('取消')))
|
|
680
|
+
: h('button', {
|
|
681
|
+
type: 'button',
|
|
682
|
+
className: 'dchat-button dchat-buttonDanger',
|
|
683
|
+
disabled: busy,
|
|
684
|
+
onClick: () => setConfirming(true),
|
|
685
|
+
}, t('移除接入'))),
|
|
686
|
+
},
|
|
687
|
+
status.errorMessage
|
|
688
|
+
? h('p', { className: 'dchat-error', role: 'alert' }, status.errorMessage)
|
|
689
|
+
: null,
|
|
690
|
+
// 属主名单是 `*`(绑定时没记录属主)时必须说清楚:这台机器人**没有属主绕过**,
|
|
691
|
+
// 一切按访问策略判定,所以 /allow 会回"只有属主",名单要在下面的「访问策略」里改。
|
|
692
|
+
status.ownersWildcard
|
|
693
|
+
? h('p', { className: 'dchat-cardDescription' }, t('属主名单是 *(这台机器人没有属主绕过):能用它的人完全由下面的「访问策略」决定。'))
|
|
694
|
+
: null,
|
|
695
|
+
// 名字拿不到(多为缺权限)时说明原因并给出开通入口:否则用户只能看到一串 id 猜原因。
|
|
696
|
+
status.nameHint
|
|
697
|
+
? h('p', { className: 'dchat-cardDescription', role: 'status' },
|
|
698
|
+
`${status.nameHint.message} `,
|
|
699
|
+
status.nameHint.url
|
|
700
|
+
? h('a', { href: status.nameHint.url, target: '_blank', rel: 'noreferrer' }, t('去开通权限'))
|
|
701
|
+
: null)
|
|
702
|
+
: null,
|
|
703
|
+
// 处理最近一条消息失败时留下的现场(与终端日志对应)。
|
|
704
|
+
status.lastError
|
|
705
|
+
? h('p', { className: 'dchat-error', role: 'alert' },
|
|
706
|
+
`${t('最近一次错误')}:${status.lastError}`)
|
|
707
|
+
: null,
|
|
708
|
+
error ? h('p', { className: 'dchat-error', role: 'alert' }, error) : null,
|
|
709
|
+
h('div', { className: 'dchat-list' },
|
|
710
|
+
h('div', { className: 'dchat-listItem' },
|
|
711
|
+
h('span', null, t('已处理消息')),
|
|
712
|
+
h('span', null, String(status.handled ?? 0)))),
|
|
713
|
+
|
|
714
|
+
h(WorkspaceEditor, {
|
|
715
|
+
value: shared.workspace,
|
|
716
|
+
options: settings.options?.workspacePaths ?? [],
|
|
717
|
+
translate: t,
|
|
718
|
+
onSave: settings.saveWorkspace,
|
|
719
|
+
}),
|
|
720
|
+
|
|
721
|
+
h(PresetEditor, {
|
|
722
|
+
value: shared.agentPreset,
|
|
723
|
+
options: settings.options?.presets ?? [],
|
|
724
|
+
translate: t,
|
|
725
|
+
onSave: settings.saveAgentPreset,
|
|
726
|
+
}),
|
|
727
|
+
|
|
728
|
+
h(ModelEditor, {
|
|
729
|
+
value: shared.model ?? null,
|
|
730
|
+
options: settings.options?.models ?? [],
|
|
731
|
+
hostDefault: settings.options?.hostDefault ?? null,
|
|
732
|
+
failures: settings.options?.modelFailures ?? [],
|
|
733
|
+
translate: t,
|
|
734
|
+
onSave: settings.saveModel,
|
|
735
|
+
}),
|
|
736
|
+
|
|
737
|
+
h(OwnerEditor, {
|
|
738
|
+
owners: status.ownerOpenIds ?? [],
|
|
739
|
+
wildcard: status.ownersWildcard === true,
|
|
740
|
+
// 属主只能是人(私聊会话),群不参与。
|
|
741
|
+
candidates: sessions.filter((item) => item.kind === 'direct'),
|
|
742
|
+
translate: t,
|
|
743
|
+
onSave: saveOwners,
|
|
744
|
+
}),
|
|
745
|
+
|
|
746
|
+
h(AccessPolicyEditor, {
|
|
747
|
+
value: shared.accessPolicy,
|
|
748
|
+
// 名单里的 id 换成名字(渠道查的);查不到就只显示 id + 原因。
|
|
749
|
+
names: policyNames.names,
|
|
750
|
+
namesHint: policyNames.hint,
|
|
751
|
+
translate: t,
|
|
752
|
+
onSave: settings.saveAccessPolicy,
|
|
753
|
+
}),
|
|
754
|
+
|
|
755
|
+
h(PanelSectionsEditor, {
|
|
756
|
+
value: shared.panelSections ?? null,
|
|
757
|
+
disabled: settings.phase !== 'ready',
|
|
758
|
+
translate: t,
|
|
759
|
+
onSave: settings.savePanelSections,
|
|
760
|
+
}),
|
|
761
|
+
|
|
762
|
+
h(ScopedModeEditor, {
|
|
763
|
+
title: t('任务过程展示'),
|
|
764
|
+
description: t('设置执行过程的呈现方式;私聊与群聊分别生效'),
|
|
765
|
+
scopes: [{ key: 'direct', label: '私聊' }, { key: 'group', label: '群聊' }],
|
|
766
|
+
options: STEP_PUSH_OPTIONS,
|
|
767
|
+
value: status.stepPush,
|
|
768
|
+
translate: t,
|
|
769
|
+
onSave: async (next) => {
|
|
770
|
+
// 不走 run():那样失败会同时落到卡片级 error 和编辑器内部,
|
|
771
|
+
// 同一个错误在这张卡上显示两遍。编辑器自己会回滚并就地提示。
|
|
772
|
+
const result = await chatUi.callChannelRpc(connection, CHANNEL_ID, 'bot.step-push.set', {
|
|
773
|
+
botId: bot.id, stepPush: next,
|
|
774
|
+
});
|
|
775
|
+
chatUi.unwrapRpc(result);
|
|
776
|
+
await onChanged?.();
|
|
777
|
+
},
|
|
778
|
+
}),
|
|
779
|
+
|
|
780
|
+
h(LarkIdentityEditor, {
|
|
781
|
+
botId: bot.id,
|
|
782
|
+
value: status.larkIdentity ?? { mode: 'bot-only', userOpenId: null },
|
|
783
|
+
chatUi,
|
|
784
|
+
connection,
|
|
785
|
+
translate: t,
|
|
786
|
+
onChanged,
|
|
787
|
+
}),
|
|
788
|
+
|
|
789
|
+
h(ContextEnhancementEditor, {
|
|
790
|
+
config: settings.record?.contextEnhancement ?? null,
|
|
791
|
+
disabled: settings.phase !== 'ready',
|
|
792
|
+
translate: t,
|
|
793
|
+
onSave: settings.saveContextEnhancement,
|
|
794
|
+
// 「指定用户/指定群」用它做"从会话里选",而不是让人填 id。
|
|
795
|
+
conversations: sessions,
|
|
796
|
+
}),
|
|
797
|
+
|
|
798
|
+
// 渠道无关面板:目标清单与测试发送都由 hub 的共享组件负责。
|
|
799
|
+
h(DeliveryTargetsEditor, {
|
|
800
|
+
chatUi,
|
|
801
|
+
connection,
|
|
802
|
+
channelId: CHANNEL_ID,
|
|
803
|
+
botId: bot.id,
|
|
804
|
+
}));
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* 机器人接入:**两条路**(与 dsh-im 一致)。
|
|
809
|
+
*
|
|
810
|
+
* ① 扫码新建:向飞书申请一次性授权链接,用飞书扫一下(或在浏览器里打开)就自动创建应用并
|
|
811
|
+
* 返回凭据;**扫码的人就是属主**,不用再去设置里选人。
|
|
812
|
+
* ② 手动接入已有机器人:填 App ID + App Secret(属主可留空,留空时接入后把私聊放宽到
|
|
813
|
+
* 「任何人可用」,让属主先跟它说上第一句话)。
|
|
814
|
+
*
|
|
815
|
+
* 之前只能手工往渠道的 `config.json` 里写凭据(旧版 dsh-im 的配置启动时自动导入),
|
|
816
|
+
* 真机反馈「飞书缺少接入机器人的入口」——所以这个入口做在渠道页上,
|
|
817
|
+
* 机器人列表头部的「新建机器人接入」打开的就是这一页。
|
|
818
|
+
*
|
|
819
|
+
* 导出给布局守门用:二维码、状态行与四个控件(三个输入框 + 一个下拉)在 320px 下都要排得下。
|
|
820
|
+
*
|
|
821
|
+
* @param props - { chatUi, connection, translate, onAdded }。
|
|
822
|
+
* @returns React 元素。
|
|
823
|
+
*/
|
|
824
|
+
export function FeishuOnboard({ chatUi, connection, translate, onAdded }) {
|
|
825
|
+
const t = typeof translate === 'function' ? translate : (key) => key;
|
|
826
|
+
const { Panel } = chatUi.components;
|
|
827
|
+
const [form, setForm] = React.useState({ appId: '', appSecret: '', domain: 'feishu', owner: '' });
|
|
828
|
+
const [phase, setPhase] = React.useState('idle');
|
|
829
|
+
const [error, setError] = React.useState(null);
|
|
830
|
+
/** 刚接进来的机器人(`{ name, id }`)与"策略没放宽成"的告警。 */
|
|
831
|
+
const [added, setAdded] = React.useState(null);
|
|
832
|
+
const [warning, setWarning] = React.useState(null);
|
|
833
|
+
/** 扫码那条路的状态(`bot.register.*` 的返回原样存着)。 */
|
|
834
|
+
const [scan, setScan] = React.useState({ state: 'idle' });
|
|
835
|
+
const aliveRef = React.useRef(true);
|
|
836
|
+
React.useEffect(() => () => { aliveRef.current = false; }, []);
|
|
837
|
+
const busy = phase === 'busy';
|
|
838
|
+
const ready = form.appId.trim().length > 0 && form.appSecret.trim().length > 0;
|
|
839
|
+
|
|
840
|
+
const patch = (next) => setForm((current) => ({ ...current, ...next }));
|
|
841
|
+
/** 扫码进行中的状态(这些状态下才轮询、才显示二维码与取消)。 */
|
|
842
|
+
const SCAN_ACTIVE = ['starting', 'qr_ready', 'polling', 'slow_down', 'domain_switched', 'saving'];
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* 接进来的机器人如果**没有属主**,把私聊访问策略放宽到「任何人可用」。
|
|
846
|
+
*
|
|
847
|
+
* 为什么:属主只能从"它聊过的会话"里选,而新机器人一个会话都没有,默认
|
|
848
|
+
* 「仅名单内可用」+ 空名单 = 谁都进不来,属主自己也没法说上第一句话。
|
|
849
|
+
* 扫码那条路通常是知道属主的(扫码的人),这条路只在真的没有属主时才走。
|
|
850
|
+
* 失败只告警:机器人已经加上了,用户能自己去「访问策略」改。
|
|
851
|
+
*/
|
|
852
|
+
const relaxIfOwnerless = async (bot) => {
|
|
853
|
+
if (bot?.ownersWildcard !== true) return;
|
|
854
|
+
const botId = bot?.botId ?? bot?.id ?? null;
|
|
855
|
+
if (!botId) return;
|
|
856
|
+
try {
|
|
857
|
+
await chatUi.callControlRpc(connection, 'bot.access-policy.open-scope', {
|
|
858
|
+
channelId: CHANNEL_ID, botId, conversationType: 'direct',
|
|
859
|
+
});
|
|
860
|
+
} catch (cause) {
|
|
861
|
+
setWarning(`${t('私聊访问策略没能自动放宽')}:${cause?.message ?? String(cause)}`);
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
/** 手动接入:填已有机器人的 App ID + App Secret。 */
|
|
866
|
+
const submit = async () => {
|
|
867
|
+
if (!ready || busy) return;
|
|
868
|
+
setPhase('busy');
|
|
869
|
+
setError(null);
|
|
870
|
+
setWarning(null);
|
|
871
|
+
setAdded(null);
|
|
872
|
+
let bot = null;
|
|
873
|
+
try {
|
|
874
|
+
const result = await chatUi.callChannelRpc(connection, CHANNEL_ID, 'bot.add', {
|
|
875
|
+
appId: form.appId.trim(),
|
|
876
|
+
appSecret: form.appSecret.trim(),
|
|
877
|
+
domain: form.domain,
|
|
878
|
+
// 属主可以留空(也可以写多个,用空格/逗号分开):留空 = 这台机器人暂时没有属主。
|
|
879
|
+
...(form.owner.trim()
|
|
880
|
+
? { ownerOpenIds: form.owner.trim().split(/[\s,,]+/).filter(Boolean) }
|
|
881
|
+
: {}),
|
|
882
|
+
});
|
|
883
|
+
bot = chatUi.unwrapRpc(result)?.bot ?? null;
|
|
884
|
+
} catch (cause) {
|
|
885
|
+
setError(cause?.message ?? String(cause));
|
|
886
|
+
setPhase('error');
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
await relaxIfOwnerless(bot);
|
|
890
|
+
setAdded({ id: bot?.botId ?? bot?.id ?? null, name: bot?.name ?? null });
|
|
891
|
+
patch({ appId: '', appSecret: '', owner: '' });
|
|
892
|
+
setPhase('done');
|
|
893
|
+
onAdded?.();
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
/** 扫码新建:发起一次尝试(之后靠轮询推进)。 */
|
|
897
|
+
const beginScan = async () => {
|
|
898
|
+
setError(null);
|
|
899
|
+
setWarning(null);
|
|
900
|
+
setAdded(null);
|
|
901
|
+
try {
|
|
902
|
+
const result = await chatUi.callChannelRpc(connection, CHANNEL_ID, 'bot.register.start', {});
|
|
903
|
+
const value = chatUi.unwrapRpc(result);
|
|
904
|
+
if (aliveRef.current) setScan(value);
|
|
905
|
+
} catch (cause) {
|
|
906
|
+
if (aliveRef.current) setError(cause?.message ?? String(cause));
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
|
|
910
|
+
const cancelScan = async () => {
|
|
911
|
+
try {
|
|
912
|
+
const result = await chatUi.callChannelRpc(connection, CHANNEL_ID, 'bot.register.cancel', {});
|
|
913
|
+
const value = chatUi.unwrapRpc(result);
|
|
914
|
+
if (aliveRef.current) setScan(value);
|
|
915
|
+
} catch (cause) {
|
|
916
|
+
if (aliveRef.current) setError(cause?.message ?? String(cause));
|
|
917
|
+
}
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
// 轮询扫码状态:服务端不做长轮询,这里每 2 秒问一次(与微信那条路一致)。
|
|
921
|
+
React.useEffect(() => {
|
|
922
|
+
if (!SCAN_ACTIVE.includes(scan.state)) return undefined;
|
|
923
|
+
let stopped = false;
|
|
924
|
+
const tick = async () => {
|
|
925
|
+
try {
|
|
926
|
+
const result = await chatUi.callChannelRpc(connection, CHANNEL_ID, 'bot.register.status', {});
|
|
927
|
+
const value = chatUi.unwrapRpc(result);
|
|
928
|
+
if (stopped || !aliveRef.current) return;
|
|
929
|
+
setScan(value);
|
|
930
|
+
if (!SCAN_ACTIVE.includes(value.state)) return;
|
|
931
|
+
} catch (cause) {
|
|
932
|
+
if (!stopped && aliveRef.current) setError(cause?.message ?? String(cause));
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
if (!stopped) timer = setTimeout(tick, 2_000);
|
|
936
|
+
};
|
|
937
|
+
let timer = setTimeout(tick, 1_000);
|
|
938
|
+
return () => {
|
|
939
|
+
stopped = true;
|
|
940
|
+
clearTimeout(timer);
|
|
941
|
+
};
|
|
942
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
943
|
+
}, [scan.state, chatUi, connection]);
|
|
944
|
+
|
|
945
|
+
// 扫码成功:刷新列表、顺手把"没有属主"的私聊策略放宽,并回头看一眼渲染。
|
|
946
|
+
React.useEffect(() => {
|
|
947
|
+
if (scan.state !== 'succeeded' || !scan.bot) return;
|
|
948
|
+
setAdded({ id: scan.bot.botId ?? scan.bot.id ?? null, name: scan.bot.name ?? null });
|
|
949
|
+
void relaxIfOwnerless(scan.bot);
|
|
950
|
+
onAdded?.();
|
|
951
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
952
|
+
}, [scan.state, scan.bot]);
|
|
953
|
+
|
|
954
|
+
const field = (key, options) => h('div', { className: 'dchat-scopeRow' },
|
|
955
|
+
// `label` 由调用方给:界面文案传 t(…) 的结果,飞书自己的术语(App ID / App Secret)原样传。
|
|
956
|
+
h('label', { className: 'dchat-scopeLabel', htmlFor: options.id }, options.label),
|
|
957
|
+
options.select
|
|
958
|
+
? h('select', {
|
|
959
|
+
id: options.id,
|
|
960
|
+
className: 'dchat-select',
|
|
961
|
+
value: form[key],
|
|
962
|
+
disabled: busy,
|
|
963
|
+
onChange: (event) => patch({ [key]: event.target.value }),
|
|
964
|
+
}, options.select.map((item) => h('option', { key: item.value, value: item.value }, t(item.label))))
|
|
965
|
+
: h('input', {
|
|
966
|
+
id: options.id,
|
|
967
|
+
className: 'dchat-input',
|
|
968
|
+
type: options.type ?? 'text',
|
|
969
|
+
value: form[key],
|
|
970
|
+
disabled: busy,
|
|
971
|
+
placeholder: options.placeholder ?? '',
|
|
972
|
+
autoComplete: 'off',
|
|
973
|
+
spellCheck: false,
|
|
974
|
+
onChange: (event) => patch({ [key]: event.target.value }),
|
|
975
|
+
onKeyDown: (event) => {
|
|
976
|
+
if (event.key !== 'Enter') return;
|
|
977
|
+
event.preventDefault();
|
|
978
|
+
void submit();
|
|
979
|
+
},
|
|
980
|
+
}));
|
|
981
|
+
|
|
982
|
+
const scanActive = SCAN_ACTIVE.includes(scan.state);
|
|
983
|
+
const scanLabel = scan.state === 'starting' ? t('正在向飞书申请二维码…')
|
|
984
|
+
: scan.state === 'saving' ? t('已授权,正在启动机器人…')
|
|
985
|
+
: scan.state === 'succeeded' ? t('已接入')
|
|
986
|
+
: scan.state === 'expired' ? t('二维码已失效,请重新生成')
|
|
987
|
+
: scan.state === 'cancelled' ? t('已取消')
|
|
988
|
+
: scan.state === 'error' ? t('扫码接入失败')
|
|
989
|
+
: scan.state === 'polling' || scan.state === 'slow_down' || scan.state === 'domain_switched'
|
|
990
|
+
? t('等待你在飞书里确认…')
|
|
991
|
+
: t('等待扫码');
|
|
992
|
+
|
|
993
|
+
return h(Panel, {
|
|
994
|
+
title: t('新建机器人接入'),
|
|
995
|
+
description: t('两种方式:扫码新建一个飞书机器人(推荐,扫码的人就是属主),或手动填已有机器人的凭据。'),
|
|
996
|
+
actions: null,
|
|
997
|
+
},
|
|
998
|
+
// ① 扫码新建
|
|
999
|
+
h('div', { className: 'dchat-onboardSection' },
|
|
1000
|
+
h('h4', { className: 'dchat-onboardTitle' }, t('扫码新建机器人')),
|
|
1001
|
+
h('p', { className: 'dchat-cardDescription' },
|
|
1002
|
+
t('点下面的按钮会向飞书申请一个一次性授权链接:用飞书扫一扫,或在浏览器里打开它,就会自动创建应用并接入。')),
|
|
1003
|
+
h('div', { className: 'dchat-actions' },
|
|
1004
|
+
h('button', {
|
|
1005
|
+
type: 'button',
|
|
1006
|
+
className: 'dchat-button dchat-buttonPrimary',
|
|
1007
|
+
disabled: scanActive,
|
|
1008
|
+
onClick: () => { void beginScan(); },
|
|
1009
|
+
}, scan.state === 'idle' || scan.state === 'cancelled' || scan.state === 'expired' || scan.state === 'error'
|
|
1010
|
+
? t('扫码新建机器人')
|
|
1011
|
+
: t('重新生成二维码')),
|
|
1012
|
+
scanActive
|
|
1013
|
+
? h('button', {
|
|
1014
|
+
type: 'button', className: 'dchat-button', onClick: () => { void cancelScan(); },
|
|
1015
|
+
}, t('取消'))
|
|
1016
|
+
: null),
|
|
1017
|
+
scan.qrCodeDataUrl
|
|
1018
|
+
? h('img', {
|
|
1019
|
+
className: 'dchat-onboardQr',
|
|
1020
|
+
src: scan.qrCodeDataUrl,
|
|
1021
|
+
alt: t('扫码新建机器人'),
|
|
1022
|
+
width: 200,
|
|
1023
|
+
height: 200,
|
|
1024
|
+
})
|
|
1025
|
+
: null,
|
|
1026
|
+
// Host 没能把链接编码成二维码(缺 qrcode 模块)时**必须说清**,并给一条还能走的路。
|
|
1027
|
+
scan.verificationUrl && !scan.qrCodeDataUrl
|
|
1028
|
+
? h('div', null,
|
|
1029
|
+
h('a', {
|
|
1030
|
+
className: 'dchat-onboardLink', href: scan.verificationUrl, target: '_blank', rel: 'noreferrer',
|
|
1031
|
+
}, t('打开授权页面')),
|
|
1032
|
+
h('p', { className: 'dchat-cardDescription' },
|
|
1033
|
+
`${t('这台 Host 没能生成二维码,请点上面的链接继续。')}`))
|
|
1034
|
+
: null,
|
|
1035
|
+
h('p', { className: 'dchat-cardDescription', role: 'status' },
|
|
1036
|
+
scan.remainingSeconds !== null && scan.remainingSeconds !== undefined && scanActive
|
|
1037
|
+
? `${scanLabel}(${t('剩余')} ${scan.remainingSeconds}s)`
|
|
1038
|
+
: scanLabel),
|
|
1039
|
+
scan.state === 'error' && scan.error
|
|
1040
|
+
? h('p', { className: 'dchat-error', role: 'alert' }, `${scan.error.message}(${scan.error.code})`)
|
|
1041
|
+
: null),
|
|
1042
|
+
|
|
1043
|
+
h('hr', { className: 'dchat-onboardDivider' }),
|
|
1044
|
+
|
|
1045
|
+
// ② 手动接入已有机器人
|
|
1046
|
+
h('div', { className: 'dchat-onboardSection' },
|
|
1047
|
+
h('h4', { className: 'dchat-onboardTitle' }, t('手动接入已有机器人')),
|
|
1048
|
+
h('p', { className: 'dchat-cardDescription' },
|
|
1049
|
+
t('填自建应用的 App ID 与 App Secret(飞书开放平台 → 凭证与基础信息)。应用需要开启机器人能力。')),
|
|
1050
|
+
h('div', { className: 'dchat-scopeGrid' },
|
|
1051
|
+
field('appId', { id: 'dchat-onboard-appId', label: 'App ID', placeholder: 'cli_xxxxxxxxxxxx' }),
|
|
1052
|
+
field('appSecret', {
|
|
1053
|
+
id: 'dchat-onboard-appSecret', label: 'App Secret', type: 'password', placeholder: t('应用凭证里的 App Secret'),
|
|
1054
|
+
}),
|
|
1055
|
+
field('domain', {
|
|
1056
|
+
id: 'dchat-onboard-domain',
|
|
1057
|
+
label: t('域名'),
|
|
1058
|
+
select: [{ value: 'feishu', label: '飞书(open.feishu.cn)' }, { value: 'lark', label: 'Lark(open.larksuite.com)' }],
|
|
1059
|
+
}),
|
|
1060
|
+
field('owner', {
|
|
1061
|
+
id: 'dchat-onboard-owner',
|
|
1062
|
+
label: t('属主 open_id(可留空)'),
|
|
1063
|
+
placeholder: 'ou_xxxxxxxxxxxx',
|
|
1064
|
+
}),
|
|
1065
|
+
h('p', { className: 'dchat-cardDescription' },
|
|
1066
|
+
t('属主留空 = 这台机器人暂时没有属主:接入后私聊会被设为「任何人可用」,属主先跟它说一句话,再回它的设置里把自己选成属主。'))),
|
|
1067
|
+
h('div', { className: 'dchat-actions' },
|
|
1068
|
+
h('button', {
|
|
1069
|
+
type: 'button',
|
|
1070
|
+
className: 'dchat-button dchat-buttonPrimary',
|
|
1071
|
+
disabled: busy || !ready,
|
|
1072
|
+
onClick: () => { void submit(); },
|
|
1073
|
+
}, busy ? t('接入中…') : t('接入')))),
|
|
1074
|
+
|
|
1075
|
+
error ? h('p', { className: 'dchat-error', role: 'alert' }, error) : null,
|
|
1076
|
+
warning ? h('p', { className: 'dchat-warning', role: 'alert' }, warning) : null,
|
|
1077
|
+
added
|
|
1078
|
+
? h('p', { className: 'dchat-cardDescription', role: 'status' },
|
|
1079
|
+
`${t('已接入')}${added.name ? ` 「${added.name}」` : ''}。`
|
|
1080
|
+
+ t('点左上角「返回」就能在列表里看到它。'))
|
|
1081
|
+
: null);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/**
|
|
1085
|
+
* 渠道设置页(`chat.channel.page`)。
|
|
1086
|
+
*
|
|
1087
|
+
* **两个视图严格分开,页面上不混**:
|
|
1088
|
+
* - `botId` 为空 = 「新建机器人接入」:**只画接入相关的东西**。已接入的机器人属于左栏那份列表,
|
|
1089
|
+
* 点它们的「设置」才是各自的配置页——真机反馈:这个页面里混着已接入机器人的配置,
|
|
1090
|
+
* 分不清哪些是"新建"、哪些是"已经在用的"。
|
|
1091
|
+
* - `botId` 给了 = 这一台机器人的设置页。
|
|
1092
|
+
*
|
|
1093
|
+
* 导出给布局守门用:守门断言"接入页里不出现已接入机器人的配置"(真机就是这么翻的车)。
|
|
1094
|
+
*/
|
|
1095
|
+
export function FeishuPage(props) {
|
|
1096
|
+
// hub 从机器人列表点「设置」进来时会带上 botId:只展示这一台机器人的设置。
|
|
1097
|
+
const { chatUi, connection, translate, botId = null } = props;
|
|
1098
|
+
const t = typeof translate === 'function' ? translate : (key) => key;
|
|
1099
|
+
const [state, setState] = React.useState({ phase: 'idle', value: null, error: null });
|
|
1100
|
+
|
|
1101
|
+
const load = React.useCallback(async () => {
|
|
1102
|
+
setState((current) => ({ ...current, phase: 'loading' }));
|
|
1103
|
+
try {
|
|
1104
|
+
const result = await chatUi.callChannelRpc(connection, CHANNEL_ID, 'connection.status', {});
|
|
1105
|
+
setState({ phase: 'ready', value: chatUi.unwrapRpc(result), error: null });
|
|
1106
|
+
} catch (error) {
|
|
1107
|
+
setState({ phase: 'error', value: null, error });
|
|
1108
|
+
}
|
|
1109
|
+
}, [chatUi, connection]);
|
|
1110
|
+
|
|
1111
|
+
React.useEffect(() => {
|
|
1112
|
+
void load();
|
|
1113
|
+
}, [load]);
|
|
1114
|
+
|
|
1115
|
+
const { EmptyState } = chatUi.components;
|
|
1116
|
+
const allBots = state.value?.bots ?? [];
|
|
1117
|
+
const scoped = Boolean(botId);
|
|
1118
|
+
const bots = scoped
|
|
1119
|
+
? allBots.filter((bot) => (bot?.botId ?? bot?.id ?? null) === botId)
|
|
1120
|
+
: [];
|
|
1121
|
+
// 指定了 botId 却一台都没匹配到:明确报出来,**绝不退回"显示全部"**——
|
|
1122
|
+
// 静默显示全部会让人以为自己点错了机器人,而真正的问题(版本错位/已被移除)被藏起来。
|
|
1123
|
+
const missing = scoped && state.phase === 'ready' && bots.length === 0;
|
|
1124
|
+
|
|
1125
|
+
return h(React.Fragment, null,
|
|
1126
|
+
// 读状态失败照旧要显示:这个页面上不能只留一行日志。
|
|
1127
|
+
state.error ? h('p', { className: 'dchat-error', role: 'alert' }, state.error.message) : null,
|
|
1128
|
+
scoped ? null : h(FeishuOnboard, { chatUi, connection, translate: t, onAdded: load }),
|
|
1129
|
+
missing
|
|
1130
|
+
? h(EmptyState, {
|
|
1131
|
+
title: t('找不到这台机器人'),
|
|
1132
|
+
description: `${t('它不在当前渠道的名单里(可能已被移除,或 Host 与页面版本不一致)')}:${botId}`,
|
|
1133
|
+
})
|
|
1134
|
+
: null,
|
|
1135
|
+
scoped
|
|
1136
|
+
? bots.map((bot) => h(BotCard, {
|
|
1137
|
+
key: bot.id,
|
|
1138
|
+
bot,
|
|
1139
|
+
status: bot,
|
|
1140
|
+
chatUi,
|
|
1141
|
+
connection,
|
|
1142
|
+
translate: t,
|
|
1143
|
+
onChanged: load,
|
|
1144
|
+
}))
|
|
1145
|
+
: null);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
/**
|
|
1149
|
+
* Cordis client 插件入口。
|
|
1150
|
+
*
|
|
1151
|
+
* @param ctx - client 上下文。
|
|
1152
|
+
*/
|
|
1153
|
+
export function apply(ctx) {
|
|
1154
|
+
ctx.effect(() => ctx.locale.register(LOCALE_NAMESPACE, { zh, en }),
|
|
1155
|
+
'dsh-chat-feishu: 双语文案');
|
|
1156
|
+
const t = typeof ctx.locale?.bind === 'function'
|
|
1157
|
+
? ctx.locale.bind(LOCALE_NAMESPACE)
|
|
1158
|
+
: (key) => zh[key] ?? key;
|
|
1159
|
+
|
|
1160
|
+
ctx.effect(() => ctx.chatChannels.register({
|
|
1161
|
+
id: CHANNEL_ID,
|
|
1162
|
+
order: 20,
|
|
1163
|
+
label: () => t('飞书'),
|
|
1164
|
+
// 侧边栏会话行的渠道徽标(飞书品牌蓝)。
|
|
1165
|
+
// 图标:**飞书开放平台官网的矢量标志**(open.feishu.cn 的 favicon-logo.svg,三条 path
|
|
1166
|
+
// 逐字节未改)。相对原件只做两件事:去掉官方文件里那层白色圆角底 `rect`(否则是白底方图),
|
|
1167
|
+
// 并把 viewBox 按官方 48px favicon 的留白比例收到标志外框;出处见 THIRD_PARTY_NOTICES.md。
|
|
1168
|
+
icon: { svg: '<svg viewBox="0.977 0.673 14.655 14.655" width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M8.5611 8.26287L8.59322 8.23075C8.6141 8.20988 8.63658 8.18739 8.65906 8.16652L8.70402 8.12316L8.8373 7.99148L9.02037 7.81324L9.17613 7.65908L9.32226 7.51456L9.47481 7.36361L9.61452 7.22551L9.81043 7.03281C9.84737 6.99588 9.8859 6.96055 9.92444 6.92522C9.9951 6.86099 10.069 6.79836 10.1428 6.73734C10.2119 6.68274 10.2825 6.62975 10.3548 6.57836C10.456 6.5061 10.5603 6.44026 10.6663 6.37603C10.7707 6.31501 10.8783 6.2572 10.9875 6.2026C11.0903 6.15282 11.1963 6.10625 11.3038 6.0645C11.3633 6.04041 11.4243 6.01954 11.4853 5.99866C11.5158 5.98903 11.5463 5.97779 11.5784 5.96815C11.3071 4.90028 10.8109 3.90468 10.122 3.04556C9.98868 2.88016 9.78634 2.78381 9.57438 2.78381H3.94598C3.88817 2.78381 3.84 2.83038 3.84 2.8898C3.84 2.92352 3.85605 2.95403 3.88335 2.97491C5.80391 4.38321 7.39689 6.19297 8.54826 8.27732L8.5611 8.26287Z" fill="#00D6B9"/> <path d="M6.32424 13.2168C9.23077 13.2168 11.7631 11.6126 13.0831 9.24238C13.1297 9.15887 13.1747 9.07537 13.218 8.99026C13.1522 9.11712 13.0783 9.23917 12.9964 9.35478C12.9675 9.39493 12.9386 9.43508 12.9081 9.47522C12.8696 9.525 12.831 9.57157 12.7909 9.61814C12.7588 9.65507 12.7266 9.6904 12.6929 9.72573C12.6255 9.79638 12.5548 9.86383 12.4809 9.92646C12.4392 9.96178 12.3991 9.99551 12.3557 10.0276C12.3059 10.0662 12.2545 10.1031 12.2031 10.1368C12.171 10.1593 12.1373 10.1802 12.1036 10.2011C12.0699 10.2219 12.0345 10.2428 11.9976 10.2637C11.9253 10.3038 11.8499 10.3424 11.7744 10.3761C11.7085 10.405 11.6411 10.4323 11.5737 10.458C11.4998 10.4853 11.4259 10.5094 11.3488 10.5302C11.2348 10.5624 11.1208 10.5864 11.0036 10.6041C10.9201 10.617 10.8334 10.6266 10.7483 10.633C10.6583 10.6394 10.5668 10.641 10.4753 10.641C10.3741 10.6394 10.2729 10.633 10.1702 10.6218C10.0947 10.6137 10.0192 10.6025 9.94375 10.5897C9.87791 10.5784 9.81208 10.564 9.74624 10.5479C9.71091 10.5399 9.67719 10.5302 9.64186 10.5206C9.54551 10.4949 9.44916 10.4676 9.35281 10.4403C9.30464 10.4259 9.25646 10.413 9.20989 10.3986C9.13763 10.3777 9.06698 10.3552 8.99632 10.3327C8.93851 10.3151 8.8807 10.2958 8.82289 10.2765C8.76829 10.2589 8.71209 10.2412 8.65749 10.2219L8.54508 10.1834C8.50012 10.1673 8.45355 10.1513 8.40859 10.1352L8.31224 10.0999C8.24801 10.0774 8.18378 10.0533 8.12115 10.0292C8.08421 10.0148 8.04728 10.0019 8.01035 9.98748C7.96057 9.96821 7.91239 9.94894 7.86261 9.92967C7.81123 9.90879 7.75823 9.88792 7.70685 9.86704L7.60568 9.82529L7.48043 9.7739L7.38408 9.73376L7.28452 9.6904L7.1978 9.65186L7.11912 9.61653L7.03883 9.5796L6.95693 9.54106L6.85255 9.49288L6.74336 9.4415C6.70482 9.42223 6.66628 9.40456 6.62774 9.38529L6.52978 9.33712C4.80192 8.4748 3.24267 7.31218 1.92269 5.90227C1.88254 5.86052 1.8167 5.85731 1.77335 5.89746C1.75247 5.91673 1.73962 5.94563 1.73962 5.97454L1.74284 10.9413V11.3444C1.74284 11.5788 1.85845 11.7972 2.05276 11.9273C3.31654 12.772 4.80353 13.22 6.32424 13.2168Z" fill="#3370FF"/> <path d="M14.8656 6.21539C13.8844 5.73525 12.7619 5.63248 11.7101 5.92795C11.6652 5.94079 11.6218 5.95364 11.5784 5.96649C11.5479 5.97612 11.5174 5.98576 11.4853 5.997C11.4243 6.01787 11.3633 6.04036 11.3039 6.06284C11.1963 6.10459 11.0919 6.15116 10.9875 6.20094C10.8783 6.25393 10.7707 6.31174 10.6663 6.37276C10.5588 6.43539 10.456 6.50283 10.3548 6.57509C10.2825 6.62648 10.2119 6.67947 10.1428 6.73407C10.0674 6.79509 9.99511 6.85611 9.92445 6.92195C9.88591 6.95728 9.84898 6.99261 9.81044 7.02954L9.61453 7.22224L9.47482 7.36034L9.32227 7.51129L9.17614 7.65581L9.02038 7.80997L8.83892 7.98982L8.70564 8.1215L8.66067 8.16485C8.6398 8.18573 8.61732 8.20821 8.59483 8.22909L8.56272 8.2612L8.51294 8.30777C8.49367 8.32544 8.476 8.34149 8.45673 8.35916C7.97338 8.80397 7.43383 9.18455 6.85413 9.49447L6.9585 9.54265L7.0404 9.58119L7.12069 9.61812L7.19938 9.65345L7.28609 9.69199L7.38565 9.73534L7.482 9.77549L7.60725 9.82688L7.70842 9.86863C7.75981 9.8895 7.8128 9.91038 7.86419 9.93125C7.91236 9.95052 7.96214 9.9698 8.01192 9.98907C8.04886 10.0035 8.08579 10.0164 8.12272 10.0308C8.18696 10.0549 8.25119 10.0774 8.31382 10.1015L8.41016 10.1368C8.45513 10.1529 8.50009 10.1689 8.54666 10.185L8.65907 10.2235C8.71366 10.2412 8.76826 10.2604 8.82447 10.2781C8.88228 10.2974 8.94008 10.315 8.99789 10.3343C9.06855 10.3568 9.14081 10.3777 9.21147 10.4002C9.25964 10.4146 9.30782 10.4291 9.35439 10.4419C9.45073 10.4692 9.54708 10.4965 9.64343 10.5222C9.67876 10.5318 9.71248 10.5399 9.74781 10.5495C9.81365 10.5656 9.87949 10.5784 9.94533 10.5912C10.0208 10.6041 10.0963 10.6153 10.1717 10.6234C10.2745 10.6346 10.3757 10.641 10.4769 10.6426C10.5684 10.6442 10.6599 10.641 10.7498 10.6346C10.8366 10.6282 10.9217 10.6185 11.0052 10.6057C11.1208 10.588 11.2364 10.5623 11.3504 10.5318C11.4259 10.511 11.5014 10.4869 11.5752 10.4596C11.6427 10.4355 11.7101 10.4082 11.776 10.3777C11.8514 10.344 11.9269 10.3054 11.9992 10.2653C12.0345 10.246 12.0698 10.2251 12.1052 10.2026C12.1405 10.1818 12.1726 10.1593 12.2047 10.1384C12.2561 10.1031 12.3075 10.0677 12.3573 10.0292C12.4006 9.99709 12.4424 9.96337 12.4825 9.92804C12.5548 9.86542 12.6254 9.79797 12.6929 9.72732C12.7266 9.69199 12.7587 9.65666 12.7908 9.61973C12.831 9.57316 12.8711 9.52498 12.9081 9.47681C12.9386 9.43827 12.9675 9.39812 12.9964 9.35637C13.0767 9.24075 13.1505 9.12032 13.2164 8.99506L13.2919 8.84572L13.9631 7.50807L13.9711 7.49202C14.1927 7.01348 14.4946 6.58312 14.8656 6.21539Z" fill="#133C9A"/> </svg>' },
|
|
1169
|
+
sessionBadge: { text: '飞', color: '#3370ff' },
|
|
1170
|
+
/**
|
|
1171
|
+
* `capabilities.setup.label` 决定机器人列表头部显不显示这个入口:飞书现在有了——
|
|
1172
|
+
* 打开的是「新建机器人接入」(填自建应用的 App ID + App Secret 就能加机器人),
|
|
1173
|
+
* 不再是当初那个点进去什么都改不了的「渠道设置」空壳。
|
|
1174
|
+
*/
|
|
1175
|
+
capabilities: {
|
|
1176
|
+
groups: true,
|
|
1177
|
+
setup: {
|
|
1178
|
+
label: t('新建机器人接入'),
|
|
1179
|
+
hint: t(FEISHU_SETUP_HINT),
|
|
1180
|
+
},
|
|
1181
|
+
},
|
|
1182
|
+
}), 'dsh-chat-feishu: 渠道元数据');
|
|
1183
|
+
|
|
1184
|
+
ctx.effect(() => ctx.slots.inject(PAGE_SLOT, () => ctx.slots.register({
|
|
1185
|
+
name: PAGE_SLOT,
|
|
1186
|
+
key: CHANNEL_ID,
|
|
1187
|
+
locale: LOCALE_NAMESPACE,
|
|
1188
|
+
inject: () => ({ chatUi: ctx.chatUi, connection: ctx.connection, translate: t }),
|
|
1189
|
+
}, FeishuPage)), 'dsh-chat-feishu: 渠道设置页');
|
|
1190
|
+
}
|