@sidleo3/dsh-chat 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/bot-list.js +243 -0
- package/client/bot-settings.js +175 -0
- package/client/bot-shared-settings.js +561 -0
- package/client/chat-ui.js +134 -0
- package/client/context-enhancement.js +435 -0
- package/client/delivery-targets.js +334 -0
- package/client/diagnostics.js +160 -0
- package/client/i18n.js +371 -0
- package/client/index.js +77 -0
- package/client/list-order.js +144 -0
- package/client/rpc.js +52 -0
- package/client/scoped-mode-editor.js +111 -0
- package/client/section.js +250 -0
- package/client/session-badges.js +263 -0
- package/client/styles.js +960 -0
- package/client/version-panel.js +97 -0
- package/cordis.patch.yml +5 -0
- package/host/bot-model.mjs +53 -0
- package/host/bot-settings.mjs +247 -0
- package/host/channel-registry.mjs +237 -0
- package/host/commands.mjs +857 -0
- package/host/deferred.mjs +291 -0
- package/host/delivery.mjs +377 -0
- package/host/file-log.mjs +169 -0
- package/host/guidance.mjs +73 -0
- package/host/index.mjs +7 -0
- package/host/interactions.mjs +330 -0
- package/host/json-store.mjs +144 -0
- package/host/log-tail.mjs +63 -0
- package/host/panel.mjs +1012 -0
- package/host/paths.mjs +50 -0
- package/host/plugin.mjs +873 -0
- package/host/prompt-context.mjs +70 -0
- package/host/rpc.mjs +147 -0
- package/host/session-keys.mjs +25 -0
- package/host/session-store.mjs +187 -0
- package/host/sessions.mjs +1348 -0
- package/host/tools.mjs +283 -0
- package/lib/client.js +4431 -0
- package/lib/index.js +5676 -0
- package/package.json +63 -0
- package/shared/access-policy.mjs +263 -0
- package/shared/channel-rail.mjs +156 -0
- package/shared/context-enhancement.mjs +415 -0
- package/shared/contract.mjs +120 -0
- package/shared/panel-sections.mjs +76 -0
- package/shared/reply-reference.mjs +115 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「Chat机器人」页的机器人列表:左栏选聊天软件,右栏列出它的机器人。
|
|
3
|
+
*
|
|
4
|
+
* 每个机器人一行(名称 / 账号 / 状态 / 已处理条数 / 最近处理时间 / 失败原因),
|
|
5
|
+
* 行上带一个「设置」按钮进入该机器人的设置页——和 dsh-im 一样的层次:
|
|
6
|
+
* **渠道 → 机器人 → 机器人设置**。
|
|
7
|
+
*
|
|
8
|
+
* 数据来自渠道自己的 `connection.status`(契约里的规范化 `bots` 名单),
|
|
9
|
+
* hub 只负责渲染,不认识任何平台概念。
|
|
10
|
+
*
|
|
11
|
+
* @module dsh-chat/client/bot-list
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as React from 'react';
|
|
15
|
+
|
|
16
|
+
import { botOrderKey, orderedItems, useListOrder } from './list-order.js';
|
|
17
|
+
|
|
18
|
+
const h = React.createElement;
|
|
19
|
+
|
|
20
|
+
/** 渠道状态 → 文案键。 */
|
|
21
|
+
const STATE_TEXT = Object.freeze({
|
|
22
|
+
running: '运行正常',
|
|
23
|
+
starting: '正在启动',
|
|
24
|
+
reconnecting: '重连中',
|
|
25
|
+
failed: '启动失败',
|
|
26
|
+
stopped: '已停止',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const TONES = Object.freeze({
|
|
30
|
+
running: 'success',
|
|
31
|
+
starting: 'warning',
|
|
32
|
+
reconnecting: 'warning',
|
|
33
|
+
failed: 'error',
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 从渠道的 `connection.status` 里取机器人名单。
|
|
38
|
+
*
|
|
39
|
+
* 契约里的规范字段是 `bots`;早期渠道只返回 `accounts`(微信就是这样),
|
|
40
|
+
* 所以这里按**保守方向**兼容两种键——否则升级期会出现"明明配了机器人却显示没有"。
|
|
41
|
+
*
|
|
42
|
+
* @param value - `connection.status` 的业务值。
|
|
43
|
+
* @returns 机器人数组(永远不是 undefined)。
|
|
44
|
+
*/
|
|
45
|
+
export function normalizeBots(value) {
|
|
46
|
+
if (Array.isArray(value?.bots)) return value.bots;
|
|
47
|
+
if (Array.isArray(value?.accounts)) return value.accounts;
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 机器人身份键。
|
|
53
|
+
*
|
|
54
|
+
* 契约字段是 `botId`;但 **host 与 client 的版本可以错位**(改 host 要重启 dsh,
|
|
55
|
+
* 改 client 刷新页面即可):老 host 只给 `id`。这时如果读 `bot.botId` 会拿到
|
|
56
|
+
* undefined,点「设置」就把 undefined 传进渠道页,渠道页只能退回"显示全部机器人"
|
|
57
|
+
* ——**表面正常、实际进错页**,正是最难查的故障形态。所以这里按 `id` 保守兜底。
|
|
58
|
+
*
|
|
59
|
+
* @param bot - 渠道 `connection.status` 里的一台机器人。
|
|
60
|
+
* @returns 身份字符串,取不到时 null。
|
|
61
|
+
*/
|
|
62
|
+
export function botKeyOf(bot) {
|
|
63
|
+
if (!bot || typeof bot !== 'object') return null;
|
|
64
|
+
const key = bot.botId ?? bot.id ?? null;
|
|
65
|
+
return typeof key === 'string' && key.length > 0 ? key : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function formatTime(value) {
|
|
69
|
+
if (!value) return '—';
|
|
70
|
+
const time = new Date(value);
|
|
71
|
+
if (Number.isNaN(time.getTime())) return '—';
|
|
72
|
+
const pad = (number) => String(number).padStart(2, '0');
|
|
73
|
+
return `${pad(time.getMonth() + 1)}-${pad(time.getDate())} ${pad(time.getHours())}:${pad(time.getMinutes())}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 机器人列表。
|
|
78
|
+
*
|
|
79
|
+
* @param props - {
|
|
80
|
+
* channelId, label, note, connection, chatUi, translate, t, onOpenSettings, setup,
|
|
81
|
+
* }。
|
|
82
|
+
* `setup` 来自渠道注册的 `capabilities.setup`:
|
|
83
|
+
* - 有 `label` → 头部显示一个通往渠道设置页的入口(用渠道给的名称,如「扫码接入」);
|
|
84
|
+
* - 有 `hint` → 空列表时用它说明怎么接入(渠道自己才说得清,如"填凭据"/"扫码");
|
|
85
|
+
* - 两样都没有(或整个 `setup` 缺席)→ 既不放入口也不编接入说明——避免出现一个点进去
|
|
86
|
+
* 跟机器人设置长得一样的空壳入口。
|
|
87
|
+
*
|
|
88
|
+
* @returns React 元素。
|
|
89
|
+
*/
|
|
90
|
+
export function BotList(props) {
|
|
91
|
+
const {
|
|
92
|
+
channelId, label, note, connection, chatUi, translate, t: frameworkT, onOpenSettings,
|
|
93
|
+
setup = null,
|
|
94
|
+
} = props;
|
|
95
|
+
const t = typeof translate === 'function' ? translate
|
|
96
|
+
: (typeof frameworkT === 'function' ? frameworkT : (key) => key);
|
|
97
|
+
/** 渠道设置入口的名称(没有就不显示这个入口)。 */
|
|
98
|
+
const setupLabel = typeof setup?.label === 'string' && setup.label.trim() ? setup.label.trim() : null;
|
|
99
|
+
/** 空列表时的接入说明:优先用渠道给的,没给就退回一句中性的。 */
|
|
100
|
+
const setupHint = typeof setup?.hint === 'string' && setup.hint.trim()
|
|
101
|
+
? setup.hint.trim()
|
|
102
|
+
: t('在渠道自己的配置里完成接入后,机器人会出现在这里。');
|
|
103
|
+
const [state, setState] = React.useState({ phase: 'loading', bots: [], error: null });
|
|
104
|
+
|
|
105
|
+
const load = React.useCallback(() => {
|
|
106
|
+
setState((current) => ({ ...current, phase: 'loading', error: null }));
|
|
107
|
+
chatUi.callChannelRpc(connection, channelId, 'connection.status', {})
|
|
108
|
+
.then((result) => {
|
|
109
|
+
setState({ phase: 'ready', bots: normalizeBots(chatUi.unwrapRpc(result)), error: null });
|
|
110
|
+
})
|
|
111
|
+
.catch((error) => {
|
|
112
|
+
setState({ phase: 'error', bots: [], error: error?.message ?? String(error) });
|
|
113
|
+
});
|
|
114
|
+
}, [channelId, chatUi, connection]);
|
|
115
|
+
|
|
116
|
+
React.useEffect(() => {
|
|
117
|
+
load();
|
|
118
|
+
}, [load]);
|
|
119
|
+
|
|
120
|
+
const { Panel, EmptyState, StatusPill } = chatUi.components;
|
|
121
|
+
/**
|
|
122
|
+
* 机器人顺序:用户拖动过就按用户顺序(每个渠道一份,存浏览器)。
|
|
123
|
+
* 身份取不到的机器人(老 host 没给 botId)用行下标兜底,至少不会被排到看不见的地方。
|
|
124
|
+
*/
|
|
125
|
+
const botOrder = useListOrder(botOrderKey(channelId), (row) => botKeyOf(row.bot));
|
|
126
|
+
const rows = React.useMemo(
|
|
127
|
+
() => orderedItems(
|
|
128
|
+
state.bots.map((bot, index) => ({ bot, fallback: `row-${index}` })),
|
|
129
|
+
botOrder.order,
|
|
130
|
+
(row) => botKeyOf(row.bot) ?? row.fallback,
|
|
131
|
+
),
|
|
132
|
+
[state.bots, botOrder.order],
|
|
133
|
+
);
|
|
134
|
+
/**
|
|
135
|
+
* 拖动中的机器人:**ref 记"拖的是谁"**,state 只管高亮(原因同 section.js:
|
|
136
|
+
* 同一任务里连着派发 dragstart/drop 时,state 的闭包可能还是旧值)。
|
|
137
|
+
*/
|
|
138
|
+
const dragBotRef = React.useRef(null);
|
|
139
|
+
const [dragBot, setDragBot] = React.useState(null);
|
|
140
|
+
const [dropBot, setDropBot] = React.useState(null);
|
|
141
|
+
|
|
142
|
+
return h(Panel, {
|
|
143
|
+
title: `${label()} · ${t('机器人')}`,
|
|
144
|
+
description: note || null,
|
|
145
|
+
actions: h('div', { className: 'dchat-actions' },
|
|
146
|
+
/**
|
|
147
|
+
* 渠道设置入口**只在渠道声明了名称时**显示。
|
|
148
|
+
*
|
|
149
|
+
* 真机反馈:飞书那个入口点进去跟"机器人设置"几乎一样(渠道级只有一行 dataDir,
|
|
150
|
+
* 而 dataDir 在诊断/版本面板里、读取状态就是右边的「重新读取」)——那就不该有它。
|
|
151
|
+
* 微信的入口就是「扫码接入」,名称与说明都由渠道给(hub 不认识这些语义)。
|
|
152
|
+
*/
|
|
153
|
+
setupLabel ? h('button', {
|
|
154
|
+
type: 'button',
|
|
155
|
+
className: 'dchat-button dchat-buttonLink',
|
|
156
|
+
onClick: () => onOpenSettings(null),
|
|
157
|
+
}, t(setupLabel)) : null,
|
|
158
|
+
h('button', {
|
|
159
|
+
type: 'button',
|
|
160
|
+
className: 'dchat-button',
|
|
161
|
+
onClick: load,
|
|
162
|
+
disabled: state.phase === 'loading',
|
|
163
|
+
}, state.phase === 'loading' ? t('读取中…') : t('重新读取'))),
|
|
164
|
+
},
|
|
165
|
+
state.error
|
|
166
|
+
? h('p', { className: 'dchat-error' }, `${t('读取失败')}:${state.error}`)
|
|
167
|
+
: null,
|
|
168
|
+
state.phase !== 'loading' && state.bots.length === 0
|
|
169
|
+
? h(EmptyState, {
|
|
170
|
+
title: t('这个渠道还没有机器人'),
|
|
171
|
+
description: t(setupHint),
|
|
172
|
+
}, setupLabel ? h('button', {
|
|
173
|
+
type: 'button',
|
|
174
|
+
className: 'dchat-button',
|
|
175
|
+
onClick: () => onOpenSettings(null),
|
|
176
|
+
}, t(setupLabel)) : null)
|
|
177
|
+
: null,
|
|
178
|
+
state.bots.length > 0
|
|
179
|
+
? h('ul', { className: 'dchat-botList' }, rows.map((row) => {
|
|
180
|
+
const { bot } = row;
|
|
181
|
+
const identity = botKeyOf(bot);
|
|
182
|
+
const rowKey = identity ?? row.fallback;
|
|
183
|
+
const title = bot.name || identity || t('未命名机器人');
|
|
184
|
+
// 没有名称时标题已经兜底成身份串,账号这一项就不再重复一遍。
|
|
185
|
+
const showIdentity = Boolean(identity) && identity !== title;
|
|
186
|
+
return h('li', {
|
|
187
|
+
key: rowKey,
|
|
188
|
+
className: `dchat-botRow${dropBot === rowKey && dragBot !== rowKey ? ' dchat-dropTarget' : ''}`,
|
|
189
|
+
onDragOver: (event) => {
|
|
190
|
+
const from = dragBotRef.current;
|
|
191
|
+
if (!from || from === rowKey) return;
|
|
192
|
+
event.preventDefault();
|
|
193
|
+
setDropBot(rowKey);
|
|
194
|
+
},
|
|
195
|
+
onDrop: (event) => {
|
|
196
|
+
event.preventDefault();
|
|
197
|
+
const from = dragBotRef.current;
|
|
198
|
+
if (from && from !== rowKey) botOrder.move(rows, from, rowKey);
|
|
199
|
+
dragBotRef.current = null;
|
|
200
|
+
setDropBot(null);
|
|
201
|
+
setDragBot(null);
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
h('span', {
|
|
205
|
+
className: 'dchat-grip',
|
|
206
|
+
draggable: true,
|
|
207
|
+
title: t('拖动可调整顺序'),
|
|
208
|
+
'aria-hidden': 'true',
|
|
209
|
+
onDragStart: (event) => {
|
|
210
|
+
dragBotRef.current = rowKey;
|
|
211
|
+
setDragBot(rowKey);
|
|
212
|
+
// Firefox 不设 dataTransfer 就不会开始拖。
|
|
213
|
+
event.dataTransfer?.setData('text/plain', rowKey);
|
|
214
|
+
if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move';
|
|
215
|
+
},
|
|
216
|
+
onDragEnd: () => {
|
|
217
|
+
dragBotRef.current = null;
|
|
218
|
+
setDragBot(null);
|
|
219
|
+
setDropBot(null);
|
|
220
|
+
},
|
|
221
|
+
}, '⋮⋮'),
|
|
222
|
+
h('div', { className: 'dchat-botMain' },
|
|
223
|
+
h('div', { className: 'dchat-botTitle' },
|
|
224
|
+
h('strong', { title }, title),
|
|
225
|
+
h(StatusPill, { status: bot.state, label: t(STATE_TEXT[bot.state] ?? '已停止') })),
|
|
226
|
+
h('div', { className: 'dchat-botMeta' },
|
|
227
|
+
showIdentity ? h('span', { className: 'dchat-code' }, identity) : null,
|
|
228
|
+
h('span', null, `${t('已处理')} ${bot.handled ?? 0}`),
|
|
229
|
+
h('span', null, `${t('最近')} ${formatTime(bot.lastHandledAt)}`)),
|
|
230
|
+
bot.errorMessage || bot.lastError
|
|
231
|
+
? h('div', { className: 'dchat-botError' }, bot.errorMessage ?? bot.lastError)
|
|
232
|
+
: null),
|
|
233
|
+
h('button', {
|
|
234
|
+
type: 'button',
|
|
235
|
+
className: 'dchat-button',
|
|
236
|
+
// 身份取不到就不能进"这台机器人的设置"——那会静默变成"整个渠道的设置"。
|
|
237
|
+
disabled: identity === null,
|
|
238
|
+
title: identity ?? t('这台机器人没有可用的身份标识,无法单独配置'),
|
|
239
|
+
onClick: () => onOpenSettings(identity),
|
|
240
|
+
}, t('设置')));
|
|
241
|
+
}))
|
|
242
|
+
: null);
|
|
243
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 渠道页共用的每机器人设置读取/保存钩子。
|
|
3
|
+
*
|
|
4
|
+
* 让每个渠道页都能用三行接好"工作区/模型/预设/上下文增强"这类 hub 持有的设置,
|
|
5
|
+
* 而不必各自实现 RPC、加载态与错误处理。
|
|
6
|
+
*
|
|
7
|
+
* @module dsh-chat/client/bot-settings
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as React from 'react';
|
|
11
|
+
|
|
12
|
+
import { callControlRpc, unwrapRpc } from './rpc.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 读取并保存某个机器人的共享设置。
|
|
16
|
+
*
|
|
17
|
+
* @param options - { connection, channelId, botId, enabled }。
|
|
18
|
+
* @returns {
|
|
19
|
+
* record, phase, error, options, loadOptions, reload,
|
|
20
|
+
* saveContextEnhancement, saveWorkspace, saveAgentPreset, saveAccessPolicy,
|
|
21
|
+
* }。
|
|
22
|
+
*/
|
|
23
|
+
export function useBotSettings({ connection, channelId, botId, enabled = true }) {
|
|
24
|
+
const [state, setState] = React.useState({ phase: 'idle', record: null, error: null });
|
|
25
|
+
/** 设置页的可选项(工作区候选、Agent Preset 列表),按需加载。 */
|
|
26
|
+
const [options, setOptions] = React.useState(null);
|
|
27
|
+
const aliveRef = React.useRef(true);
|
|
28
|
+
|
|
29
|
+
React.useEffect(() => {
|
|
30
|
+
aliveRef.current = true;
|
|
31
|
+
return () => {
|
|
32
|
+
aliveRef.current = false;
|
|
33
|
+
};
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
const load = React.useCallback(async () => {
|
|
37
|
+
if (!enabled || !connection || !channelId || !botId) return;
|
|
38
|
+
setState((current) => ({ ...current, phase: 'loading', error: null }));
|
|
39
|
+
try {
|
|
40
|
+
const result = await callControlRpc(connection, 'bot.settings.get', { channelId, botId });
|
|
41
|
+
const value = unwrapRpc(result);
|
|
42
|
+
if (aliveRef.current) setState({ phase: 'ready', record: value.settings, error: null });
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (aliveRef.current) setState({ phase: 'error', record: null, error });
|
|
45
|
+
}
|
|
46
|
+
}, [connection, channelId, botId, enabled]);
|
|
47
|
+
|
|
48
|
+
React.useEffect(() => {
|
|
49
|
+
void load();
|
|
50
|
+
}, [load]);
|
|
51
|
+
|
|
52
|
+
/** 读一次可选项(进设置页时调一次即可)。 */
|
|
53
|
+
const loadOptions = React.useCallback(async () => {
|
|
54
|
+
if (!enabled || !connection || !channelId || !botId) return null;
|
|
55
|
+
try {
|
|
56
|
+
const result = await callControlRpc(connection, 'bot.settings.options', { channelId, botId });
|
|
57
|
+
const value = unwrapRpc(result);
|
|
58
|
+
if (aliveRef.current) setOptions(value);
|
|
59
|
+
return value;
|
|
60
|
+
} catch {
|
|
61
|
+
// 可选项读不到不该让整页报错:对应编辑器退化成"手动输入"。
|
|
62
|
+
if (aliveRef.current) setOptions({ workspacePaths: [], presets: [] });
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}, [connection, channelId, botId, enabled]);
|
|
66
|
+
|
|
67
|
+
const saveContextEnhancement = React.useCallback(async (config) => {
|
|
68
|
+
const result = await callControlRpc(connection, 'bot.context-enhancement.set', {
|
|
69
|
+
channelId,
|
|
70
|
+
botId,
|
|
71
|
+
config,
|
|
72
|
+
});
|
|
73
|
+
const value = unwrapRpc(result);
|
|
74
|
+
if (aliveRef.current) {
|
|
75
|
+
setState((current) => ({
|
|
76
|
+
...current,
|
|
77
|
+
phase: 'ready',
|
|
78
|
+
record: { ...(current.record ?? {}), contextEnhancement: value.contextEnhancement },
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
return value.contextEnhancement;
|
|
82
|
+
}, [connection, channelId, botId]);
|
|
83
|
+
|
|
84
|
+
/** 通用单字段保存:调端点、把返回值并回 record。 */
|
|
85
|
+
const saveField = React.useCallback(async (method, body, key) => {
|
|
86
|
+
const result = await callControlRpc(connection, method, { channelId, botId, ...body });
|
|
87
|
+
const value = unwrapRpc(result);
|
|
88
|
+
if (aliveRef.current) {
|
|
89
|
+
setState((current) => ({
|
|
90
|
+
...current,
|
|
91
|
+
phase: 'ready',
|
|
92
|
+
record: { ...(current.record ?? {}), [key]: value[key] ?? null },
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
return value[key] ?? null;
|
|
96
|
+
}, [connection, channelId, botId]);
|
|
97
|
+
|
|
98
|
+
const saveWorkspace = React.useCallback(
|
|
99
|
+
(workspace) => saveField('bot.workspace.set', { workspace }, 'workspace'),
|
|
100
|
+
[saveField],
|
|
101
|
+
);
|
|
102
|
+
const saveAgentPreset = React.useCallback(
|
|
103
|
+
(agentPreset) => saveField('bot.agent-preset.set', { agentPreset }, 'agentPreset'),
|
|
104
|
+
[saveField],
|
|
105
|
+
);
|
|
106
|
+
const saveAccessPolicy = React.useCallback(
|
|
107
|
+
(policy) => saveField('bot.access-policy.set', { policy }, 'accessPolicy'),
|
|
108
|
+
[saveField],
|
|
109
|
+
);
|
|
110
|
+
const saveModel = React.useCallback(
|
|
111
|
+
(model) => saveField('bot.model.set', { model }, 'model'),
|
|
112
|
+
[saveField],
|
|
113
|
+
);
|
|
114
|
+
const savePanelSections = React.useCallback(
|
|
115
|
+
(sections) => saveField('bot.panel-sections.set', { sections }, 'panelSections'),
|
|
116
|
+
[saveField],
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
record: state.record,
|
|
121
|
+
phase: state.phase,
|
|
122
|
+
error: state.error,
|
|
123
|
+
options,
|
|
124
|
+
loadOptions,
|
|
125
|
+
reload: load,
|
|
126
|
+
saveContextEnhancement,
|
|
127
|
+
saveWorkspace,
|
|
128
|
+
saveAgentPreset,
|
|
129
|
+
saveAccessPolicy,
|
|
130
|
+
saveModel,
|
|
131
|
+
savePanelSections,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 该机器人聊过的会话(带人能认出的名字)。
|
|
137
|
+
*
|
|
138
|
+
* 供"指定用户 / 指定群"这类需要填平台 id 的地方做选择器——用户不该被要求记住 `ou_xxx`。
|
|
139
|
+
* 数据与投递列表同源(hub 的持久绑定 + 渠道发现 + 渠道解析的名字)。
|
|
140
|
+
*
|
|
141
|
+
* @param options - { connection, channelId, botId, enabled }。
|
|
142
|
+
* @returns { conversations, phase, error }。
|
|
143
|
+
*/
|
|
144
|
+
export function useConversations({ connection, channelId, botId, enabled = true }) {
|
|
145
|
+
const [state, setState] = React.useState({ phase: 'idle', conversations: [], error: null });
|
|
146
|
+
const aliveRef = React.useRef(true);
|
|
147
|
+
|
|
148
|
+
React.useEffect(() => {
|
|
149
|
+
aliveRef.current = true;
|
|
150
|
+
return () => {
|
|
151
|
+
aliveRef.current = false;
|
|
152
|
+
};
|
|
153
|
+
}, []);
|
|
154
|
+
|
|
155
|
+
const load = React.useCallback(async () => {
|
|
156
|
+
if (!enabled || !connection || !channelId || !botId) return;
|
|
157
|
+
setState((current) => ({ ...current, phase: 'loading' }));
|
|
158
|
+
try {
|
|
159
|
+
const result = await callControlRpc(connection, 'bot.conversations', { channelId, botId });
|
|
160
|
+
const value = unwrapRpc(result);
|
|
161
|
+
if (aliveRef.current) {
|
|
162
|
+
setState({ phase: 'ready', conversations: value.conversations ?? [], error: null });
|
|
163
|
+
}
|
|
164
|
+
} catch (error) {
|
|
165
|
+
// 选择器只是方便:取不到就退回手填 id,不打扰用户。
|
|
166
|
+
if (aliveRef.current) setState({ phase: 'error', conversations: [], error });
|
|
167
|
+
}
|
|
168
|
+
}, [connection, channelId, botId, enabled]);
|
|
169
|
+
|
|
170
|
+
React.useEffect(() => {
|
|
171
|
+
void load();
|
|
172
|
+
}, [load]);
|
|
173
|
+
|
|
174
|
+
return { ...state, reload: load };
|
|
175
|
+
}
|