@cubeai/dsh-feishu-channels 0.0.0-stage → 0.2.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/LICENSE +201 -0
- package/NOTICE +15 -0
- package/README.md +56 -2
- package/client.js +4189 -0
- package/cordis.patch.yml +4 -0
- package/docs/coexistence.md +112 -0
- package/docs/dsh-feishu-channels-plan.md +1406 -0
- package/docs/ui-revamp-plan.md +263 -0
- package/docs/verification.md +120 -0
- package/icon.svg +17 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +76 -4
- package/src/channels/connection-state.js +12 -0
- package/src/channels/diagnostics.js +77 -0
- package/src/channels/factory.js +20 -0
- package/src/channels/feishu-app.js +267 -0
- package/src/channels/feishu.js +399 -0
- package/src/channels/media.js +9 -0
- package/src/channels/meta.js +24 -0
- package/src/channels/qr/feishu.js +84 -0
- package/src/channels/qr/hub.js +229 -0
- package/src/channels/qr/render.js +8 -0
- package/src/channels/qr/shared.js +50 -0
- package/src/engine/abort.js +35 -0
- package/src/engine/access.js +23 -0
- package/src/engine/agent-options.js +19 -0
- package/src/engine/approval.js +70 -0
- package/src/engine/assistant-settings.js +38 -0
- package/src/engine/assistant-text.js +11 -0
- package/src/engine/atomic-file.js +45 -0
- package/src/engine/chat-commands.js +752 -0
- package/src/engine/choice-delivery.js +20 -0
- package/src/engine/choices.js +103 -0
- package/src/engine/command-locale.js +70 -0
- package/src/engine/command-messages.js +434 -0
- package/src/engine/command-permissions.js +38 -0
- package/src/engine/command-replies.js +59 -0
- package/src/engine/credentials.js +54 -0
- package/src/engine/deferred-delivery.js +260 -0
- package/src/engine/delivery-history.js +39 -0
- package/src/engine/file-delivery.js +215 -0
- package/src/engine/file-input.js +52 -0
- package/src/engine/file-log.js +25 -0
- package/src/engine/gateway.js +1337 -0
- package/src/engine/image-input.js +31 -0
- package/src/engine/json-state.js +25 -0
- package/src/engine/keyed-queue.js +20 -0
- package/src/engine/merge.js +56 -0
- package/src/engine/message-progress.js +274 -0
- package/src/engine/question.js +201 -0
- package/src/engine/quiet-logger.js +25 -0
- package/src/engine/reply-stream.js +86 -0
- package/src/engine/router.js +713 -0
- package/src/engine/secure-file.js +8 -0
- package/src/engine/seen-store.js +43 -0
- package/src/engine/session-export.js +64 -0
- package/src/engine/session-history.js +30 -0
- package/src/engine/session-id.js +62 -0
- package/src/engine/session-store.js +109 -0
- package/src/engine/session-title.js +29 -0
- package/src/engine/split.js +291 -0
- package/src/engine/workspace-path.js +9 -0
- package/src/index.js +62 -0
- package/src/manager.js +1325 -0
package/client.js
ADDED
|
@@ -0,0 +1,4189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-feishu-channels 浏览器端:飞书频道设置页 + 工作区频道槽。
|
|
3
|
+
*/
|
|
4
|
+
window.__ModuleLoader__.load({
|
|
5
|
+
id: "@cubeai/dsh-feishu-channels",
|
|
6
|
+
factory: (require) => {
|
|
7
|
+
var module = { exports: {} };
|
|
8
|
+
var exports = module.exports;
|
|
9
|
+
const React = require("react");
|
|
10
|
+
const { useState, useEffect, useLayoutEffect, useCallback, useMemo, useRef, useSyncExternalStore } = React;
|
|
11
|
+
let ReactDOM = {};
|
|
12
|
+
try { ReactDOM = require("react-dom"); } catch { /* react-dom is optional; SVG markup degrades gracefully */ }
|
|
13
|
+
const primitives = typeof require === "function" ? (function () { try { return require("@deepseek-ai/dsh-client-ui-primitives"); } catch { return {}; } })() : {};
|
|
14
|
+
const PrimitiveButton = primitives.Button;
|
|
15
|
+
const HostMenu = primitives.Menu || function FallbackMenu(props) {
|
|
16
|
+
const items = Array.isArray(props.items) ? props.items : [];
|
|
17
|
+
const footer = Array.isArray(props.footer) ? props.footer : [];
|
|
18
|
+
const option = (item) => React.createElement("button", {
|
|
19
|
+
key: String(item.value),
|
|
20
|
+
type: "button",
|
|
21
|
+
role: "option",
|
|
22
|
+
disabled: item.disabled,
|
|
23
|
+
"aria-selected": props.selectedId !== undefined && String(item.value) === String(props.selectedId),
|
|
24
|
+
style: {
|
|
25
|
+
display: "block",
|
|
26
|
+
width: "100%",
|
|
27
|
+
padding: "6px 8px",
|
|
28
|
+
border: 0,
|
|
29
|
+
borderRadius: 6,
|
|
30
|
+
background: props.selectedId !== undefined && String(item.value) === String(props.selectedId) ? "var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,.06))" : "transparent",
|
|
31
|
+
color: "var(--dsw-alias-label-primary, inherit)",
|
|
32
|
+
textAlign: "left",
|
|
33
|
+
cursor: item.disabled ? "not-allowed" : "pointer",
|
|
34
|
+
opacity: item.disabled ? 0.5 : 1,
|
|
35
|
+
},
|
|
36
|
+
onClick: () => {
|
|
37
|
+
if (item.disabled) return;
|
|
38
|
+
props.onSelect?.(item.value);
|
|
39
|
+
props.onClose?.();
|
|
40
|
+
},
|
|
41
|
+
}, item.label);
|
|
42
|
+
return React.createElement("div", { className: "dfc-fallback-menu-wrap", style: { position: "relative", display: "inline-flex", width: "100%" } },
|
|
43
|
+
props.anchor,
|
|
44
|
+
props.open && React.createElement("div", {
|
|
45
|
+
className: ["dfc-form-menu", props.listClassName].filter(Boolean).join(" "),
|
|
46
|
+
role: "listbox",
|
|
47
|
+
style: {
|
|
48
|
+
position: "absolute",
|
|
49
|
+
zIndex: 1200,
|
|
50
|
+
top: "calc(100% + 4px)",
|
|
51
|
+
left: 0,
|
|
52
|
+
minWidth: "100%",
|
|
53
|
+
maxHeight: 280,
|
|
54
|
+
overflow: "auto",
|
|
55
|
+
background: "var(--dsw-alias-bg-layer-2, #fff)",
|
|
56
|
+
border: "1px solid var(--dsw-alias-border-l1, rgba(0,0,0,.12))",
|
|
57
|
+
borderRadius: 8,
|
|
58
|
+
padding: 4,
|
|
59
|
+
boxShadow: "0 8px 24px rgba(0,0,0,.16)",
|
|
60
|
+
},
|
|
61
|
+
}, items.map(option), footer.map(option)));
|
|
62
|
+
};
|
|
63
|
+
const HostModal = primitives.Modal;
|
|
64
|
+
const HostSegmentedTabs = primitives.SegmentedTabs || function FallbackSegmentedTabs(props) {
|
|
65
|
+
return React.createElement("div", {
|
|
66
|
+
className: [props.className, "dfc-tabs"].filter(Boolean).join(" "),
|
|
67
|
+
role: "tablist",
|
|
68
|
+
"aria-label": props.label,
|
|
69
|
+
}, (props.items || []).map((item) => React.createElement("button", {
|
|
70
|
+
key: item.value,
|
|
71
|
+
type: "button",
|
|
72
|
+
role: "tab",
|
|
73
|
+
"aria-selected": item.value === props.value,
|
|
74
|
+
className: item.value === props.value ? "dfc-tab on" : "dfc-tab",
|
|
75
|
+
onClick: () => props.onChange(item.value),
|
|
76
|
+
}, item.label)));
|
|
77
|
+
};
|
|
78
|
+
// 宿主 Button 由 forwardRef 包成对象,typeof 是 object,不是 function。
|
|
79
|
+
function isHostComponent(component) {
|
|
80
|
+
return typeof component === "function" || (!!component && typeof component === "object" && typeof component.render === "function");
|
|
81
|
+
}
|
|
82
|
+
// 只关最上层。下层监听先注册,不是顶层就放行,避免一次 Escape 关掉弹窗和宿主设置。
|
|
83
|
+
const modalLayers = [];
|
|
84
|
+
// 0.1.7 把尺寸从图标名里拿掉了(IconEditOutlineRegular)。旧宿主仍导出 *16/*20。
|
|
85
|
+
function pickHostIcon(...names) {
|
|
86
|
+
for (const name of names) {
|
|
87
|
+
const icon = primitives[name];
|
|
88
|
+
if (typeof icon === "function") return icon;
|
|
89
|
+
}
|
|
90
|
+
return function MissingHostIcon() { return null; };
|
|
91
|
+
}
|
|
92
|
+
const IconFolderClose = pickHostIcon("IconFolderCloseRegular", "IconFolderClose16", "IconFolderClose");
|
|
93
|
+
const IconClockOutline = pickHostIcon("IconClockOutlineRegular", "IconClockOutline16");
|
|
94
|
+
const IconQueueOutline = pickHostIcon("IconQueueOutlineRegular");
|
|
95
|
+
const IconCheckOutline = pickHostIcon("IconCheckOutlineRegular");
|
|
96
|
+
const IconCheckCircleOutline = pickHostIcon("IconCheckCircleOutlineRegular");
|
|
97
|
+
const IconCloseCircleFill = pickHostIcon("IconCloseCircleFillRegular");
|
|
98
|
+
const IconInfoOutline = pickHostIcon("IconInfoOutlineRegular");
|
|
99
|
+
const IconWarningOutline = pickHostIcon("IconWarningOutlineRegular");
|
|
100
|
+
const IconCloseFill = pickHostIcon("IconCloseFillRegular");
|
|
101
|
+
const IconChevronDownOutline = pickHostIcon("IconChevronDownOutlineRegular");
|
|
102
|
+
const IconChevronRightOutline = pickHostIcon("IconChevronRightOutlineRegular");
|
|
103
|
+
const IconPlusOutline = pickHostIcon("IconPlusOutlineRegular");
|
|
104
|
+
const IconEditOutline = pickHostIcon("IconEditOutlineRegular");
|
|
105
|
+
const IconRefreshOutline = pickHostIcon("IconRefreshOutlineRegular");
|
|
106
|
+
const IconDownloadOutline = pickHostIcon("IconDownloadOutlineRegular");
|
|
107
|
+
const IconCopyOutline = pickHostIcon("IconCopyOutlineRegular");
|
|
108
|
+
const IconLoadingOutline = pickHostIcon("IconLoadingOutlineRegular");
|
|
109
|
+
const IconStopFill = pickHostIcon("IconStopFillRegular", "IconStopFill16", "IconStopFill");
|
|
110
|
+
function alertIcon(type) {
|
|
111
|
+
if (type === "success") return h(IconCheckCircleOutline, { size: 14 });
|
|
112
|
+
if (type === "warning") return h(IconWarningOutline, { size: 14 });
|
|
113
|
+
if (type === "error") return h(IconCloseCircleFill, { size: 14 });
|
|
114
|
+
return h(IconInfoOutline, { size: 14 });
|
|
115
|
+
}
|
|
116
|
+
function NoticeBar({ type = "info", icon, children, className, role, showIcon }) {
|
|
117
|
+
const iconNode = icon !== undefined ? icon : (showIcon !== false ? alertIcon(type) : null);
|
|
118
|
+
return React.createElement("div", {
|
|
119
|
+
role: role || (type === "error" ? "alert" : undefined),
|
|
120
|
+
className: ["dfc-feedback", "dfc-feedback-" + type, className].filter(Boolean).join(" "),
|
|
121
|
+
},
|
|
122
|
+
iconNode && React.createElement("span", { className: "dfc-feedback-icon" }, iconNode),
|
|
123
|
+
React.createElement("div", { className: "dfc-feedback-body" }, children)
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
function Tag({ color, children, icon, className, bordered, ...props }) {
|
|
127
|
+
const colorCls = color ? "dfc-tag-" + color : "";
|
|
128
|
+
return React.createElement("span", {
|
|
129
|
+
...props,
|
|
130
|
+
className: ["dfc-tag", colorCls, className].filter(Boolean).join(" "),
|
|
131
|
+
}, icon && React.createElement("span", { className: "dfc-tag-icon", style: { display: "inline-flex", alignItems: "center", marginRight: children ? 4 : 0 } }, icon), children);
|
|
132
|
+
}
|
|
133
|
+
function Input(props) {
|
|
134
|
+
const { className, type = "text", ...rest } = props;
|
|
135
|
+
return React.createElement("input", {
|
|
136
|
+
type,
|
|
137
|
+
...rest,
|
|
138
|
+
className: ["dfc-input", className].filter(Boolean).join(" "),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function TextArea(props) {
|
|
142
|
+
const { autoSize, className, rows, ...rest } = props;
|
|
143
|
+
return React.createElement("textarea", {
|
|
144
|
+
...rest,
|
|
145
|
+
className: ["dfc-input dfc-textarea", className].filter(Boolean).join(" "),
|
|
146
|
+
rows: autoSize?.minRows || rows || 4,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
Input.TextArea = TextArea;
|
|
150
|
+
function Checkbox({ checked, onChange, children, className, disabled, ...props }) {
|
|
151
|
+
return React.createElement("label", {
|
|
152
|
+
className: ["dfc-checkbox-label", disabled ? "is-disabled" : "", className].filter(Boolean).join(" "),
|
|
153
|
+
},
|
|
154
|
+
React.createElement("input", {
|
|
155
|
+
type: "checkbox",
|
|
156
|
+
checked: Boolean(checked),
|
|
157
|
+
disabled,
|
|
158
|
+
onChange,
|
|
159
|
+
className: "dfc-checkbox",
|
|
160
|
+
...props,
|
|
161
|
+
}),
|
|
162
|
+
children ? React.createElement("span", { className: "dfc-checkbox-text" }, children) : null
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
function hostModalButton(label, onClick, kind, extra) {
|
|
166
|
+
const options = extra || {};
|
|
167
|
+
const isDanger = kind === "danger" || options.danger;
|
|
168
|
+
const variant = isDanger ? "outline" : (kind === "primary" ? "primary" : "outline");
|
|
169
|
+
const dangerClass = isDanger ? "dfc-btn-danger" : "";
|
|
170
|
+
const className = [options.className, dangerClass].filter(Boolean).join(" ") || undefined;
|
|
171
|
+
if (isHostComponent(PrimitiveButton)) {
|
|
172
|
+
return React.createElement(PrimitiveButton, {
|
|
173
|
+
variant,
|
|
174
|
+
disabled: options.disabled,
|
|
175
|
+
className,
|
|
176
|
+
onClick,
|
|
177
|
+
type: "button",
|
|
178
|
+
}, label);
|
|
179
|
+
}
|
|
180
|
+
return React.createElement(Button, {
|
|
181
|
+
variant,
|
|
182
|
+
disabled: options.disabled,
|
|
183
|
+
className,
|
|
184
|
+
onClick,
|
|
185
|
+
}, label);
|
|
186
|
+
}
|
|
187
|
+
function Modal(props) {
|
|
188
|
+
const { open, title, onCancel, onOk, okText, cancelText, okButtonProps, confirmLoading, width = 640, zIndex = 1100, children, footer, className, contentClassName, keyboard, maskClosable, closeLabel, description } = props;
|
|
189
|
+
const requestClose = onCancel || props.onClose;
|
|
190
|
+
const closeRef = useRef(requestClose);
|
|
191
|
+
closeRef.current = requestClose;
|
|
192
|
+
const layerRef = useRef(null);
|
|
193
|
+
if (!layerRef.current) layerRef.current = { owner: props.layerOwner || "modal" };
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
if (!open) return undefined;
|
|
196
|
+
const layer = layerRef.current;
|
|
197
|
+
modalLayers.push(layer);
|
|
198
|
+
const onKey = (event) => {
|
|
199
|
+
if (event.key !== "Escape") return;
|
|
200
|
+
if (modalLayers[modalLayers.length - 1] !== layer) return;
|
|
201
|
+
event.preventDefault();
|
|
202
|
+
event.stopPropagation();
|
|
203
|
+
if (typeof event.stopImmediatePropagation === "function") event.stopImmediatePropagation();
|
|
204
|
+
if (keyboard === false) return;
|
|
205
|
+
if (closeRef.current) closeRef.current();
|
|
206
|
+
};
|
|
207
|
+
window.addEventListener("keydown", onKey, true);
|
|
208
|
+
return () => {
|
|
209
|
+
window.removeEventListener("keydown", onKey, true);
|
|
210
|
+
const index = modalLayers.lastIndexOf(layer);
|
|
211
|
+
if (index >= 0) modalLayers.splice(index, 1);
|
|
212
|
+
};
|
|
213
|
+
}, [open, keyboard]);
|
|
214
|
+
if (!open) return null;
|
|
215
|
+
const handleClose = () => { if (closeRef.current) closeRef.current(); };
|
|
216
|
+
const handleOk = () => { if (onOk) onOk(); };
|
|
217
|
+
const actionRow = footer === null ? null : (footer || React.createElement(React.Fragment, null,
|
|
218
|
+
hostModalButton(cancelText || "取消", handleClose, "outline"),
|
|
219
|
+
hostModalButton(okText || "确定", handleOk, okButtonProps?.danger ? "danger" : "primary", { disabled: okButtonProps?.disabled || confirmLoading, danger: okButtonProps?.danger })
|
|
220
|
+
));
|
|
221
|
+
const footerNode = Array.isArray(actionRow)
|
|
222
|
+
? React.createElement(React.Fragment, null, actionRow)
|
|
223
|
+
: actionRow;
|
|
224
|
+
const hostDescription = typeof description === "string" && description !== "" ? { description } : {};
|
|
225
|
+
if (typeof HostModal === "function") {
|
|
226
|
+
return h(HostModal, {
|
|
227
|
+
open: true,
|
|
228
|
+
title,
|
|
229
|
+
onClose: handleClose,
|
|
230
|
+
closeLabel: closeLabel || "Close",
|
|
231
|
+
className,
|
|
232
|
+
// 宿主 Modal 把内容区裁在视口内;标记 content 区,让它在超长时滚动。
|
|
233
|
+
contentClassName: ["dfc-modal-content", contentClassName].filter(Boolean).join(" "),
|
|
234
|
+
footer: footerNode,
|
|
235
|
+
...hostDescription,
|
|
236
|
+
}, children);
|
|
237
|
+
}
|
|
238
|
+
const allowMask = maskClosable !== false && modalLayers[modalLayers.length - 1] === layerRef.current;
|
|
239
|
+
return React.createElement("div", { className: "dfc-modal-root", style: { zIndex } },
|
|
240
|
+
React.createElement("div", { className: "dfc-modal-mask", onClick: allowMask ? handleClose : undefined }),
|
|
241
|
+
React.createElement("div", { className: ["dfc-modal-wrap", className].filter(Boolean).join(" ") },
|
|
242
|
+
React.createElement("div", { className: "dfc-modal-panel", style: { width } },
|
|
243
|
+
React.createElement("div", { className: "dfc-modal-header" },
|
|
244
|
+
React.createElement("div", { className: "dfc-modal-title" }, title),
|
|
245
|
+
React.createElement("button", { type: "button", className: "dfc-modal-close", "aria-label": closeLabel || "Close", onClick: handleClose },
|
|
246
|
+
h(IconCloseFill, { size: 14 })
|
|
247
|
+
)
|
|
248
|
+
),
|
|
249
|
+
React.createElement("div", { className: "dfc-modal-body" },
|
|
250
|
+
description ? React.createElement("p", { className: "dfc-confirm-content" }, description) : null,
|
|
251
|
+
children),
|
|
252
|
+
footerNode && React.createElement("div", { className: "dfc-modal-footer-wrap" }, footerNode)
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
function useModal() {
|
|
258
|
+
const [config, setConfig] = React.useState(null);
|
|
259
|
+
const confirm = (opts) => { setConfig(opts); };
|
|
260
|
+
const holder = config ? React.createElement(Modal, {
|
|
261
|
+
open: true,
|
|
262
|
+
title: config.title,
|
|
263
|
+
width: 480,
|
|
264
|
+
description: config.description,
|
|
265
|
+
onCancel: () => { setConfig(null); },
|
|
266
|
+
onOk: () => {
|
|
267
|
+
const p = config.onOk?.();
|
|
268
|
+
if (p && typeof p.then === "function") {
|
|
269
|
+
p.then(() => setConfig(null));
|
|
270
|
+
} else {
|
|
271
|
+
setConfig(null);
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
okText: config.okText,
|
|
275
|
+
cancelText: config.cancelText,
|
|
276
|
+
okButtonProps: config.okButtonProps,
|
|
277
|
+
}, config.content ? React.createElement("p", { className: "dfc-confirm-content" }, config.content) : null) : null;
|
|
278
|
+
return [{ confirm }, holder];
|
|
279
|
+
}
|
|
280
|
+
Modal.useModal = useModal;
|
|
281
|
+
function Switch(props) {
|
|
282
|
+
const on = Boolean(props.checked);
|
|
283
|
+
return React.createElement("button", {
|
|
284
|
+
type: "button",
|
|
285
|
+
role: "switch",
|
|
286
|
+
"aria-checked": on,
|
|
287
|
+
"aria-label": props["aria-label"],
|
|
288
|
+
title: props.title,
|
|
289
|
+
disabled: props.disabled,
|
|
290
|
+
className: "dfc-enable-switch" + (props.className ? " " + props.className : ""),
|
|
291
|
+
onClick: function (event) {
|
|
292
|
+
if (event && typeof event.stopPropagation === "function") event.stopPropagation();
|
|
293
|
+
if (!props.disabled && props.onChange) props.onChange(!on);
|
|
294
|
+
},
|
|
295
|
+
}, React.createElement("span", { className: "dfc-enable-switch-thumb", "aria-hidden": "true" }));
|
|
296
|
+
}
|
|
297
|
+
function Select(props) {
|
|
298
|
+
const [open, setOpen] = useState(false);
|
|
299
|
+
const rootRef = useRef(null);
|
|
300
|
+
|
|
301
|
+
useEffect(() => {
|
|
302
|
+
if (!open) return undefined;
|
|
303
|
+
const updateWidth = () => {
|
|
304
|
+
const width = rootRef.current?.getBoundingClientRect().width ?? 0;
|
|
305
|
+
if (width > 0) document.documentElement.style.setProperty("--dfc-menu-width", `${Math.round(width)}px`);
|
|
306
|
+
};
|
|
307
|
+
updateWidth();
|
|
308
|
+
window.addEventListener("resize", updateWidth);
|
|
309
|
+
return () => {
|
|
310
|
+
window.removeEventListener("resize", updateWidth);
|
|
311
|
+
document.documentElement.style.removeProperty("--dfc-menu-width");
|
|
312
|
+
};
|
|
313
|
+
}, [open]);
|
|
314
|
+
|
|
315
|
+
useEffect(() => {
|
|
316
|
+
if (!open || typeof window === "undefined") return undefined;
|
|
317
|
+
const layer = { owner: "select" };
|
|
318
|
+
modalLayers.push(layer);
|
|
319
|
+
function onKey(event) {
|
|
320
|
+
if (event.key !== "Escape") return;
|
|
321
|
+
if (modalLayers[modalLayers.length - 1] !== layer) return;
|
|
322
|
+
event.preventDefault();
|
|
323
|
+
event.stopPropagation();
|
|
324
|
+
event.stopImmediatePropagation();
|
|
325
|
+
setOpen(false);
|
|
326
|
+
}
|
|
327
|
+
window.addEventListener("keydown", onKey, true);
|
|
328
|
+
return () => {
|
|
329
|
+
window.removeEventListener("keydown", onKey, true);
|
|
330
|
+
const index = modalLayers.lastIndexOf(layer);
|
|
331
|
+
if (index >= 0) modalLayers.splice(index, 1);
|
|
332
|
+
};
|
|
333
|
+
}, [open]);
|
|
334
|
+
|
|
335
|
+
const items = React.useMemo(() => {
|
|
336
|
+
const list = [];
|
|
337
|
+
(props.options || []).forEach((opt, idx) => {
|
|
338
|
+
if (Array.isArray(opt.options)) {
|
|
339
|
+
list.push({ type: "label", id: "group-" + idx, text: opt.label || "" });
|
|
340
|
+
opt.options.forEach((sub) => {
|
|
341
|
+
list.push({ id: String(sub.value), label: sub.label, disabled: sub.disabled, danger: sub.danger });
|
|
342
|
+
});
|
|
343
|
+
} else {
|
|
344
|
+
list.push({ id: String(opt.value), label: opt.label, disabled: opt.disabled, danger: opt.danger });
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
return list;
|
|
348
|
+
}, [props.options]);
|
|
349
|
+
|
|
350
|
+
const footer = React.useMemo(() => {
|
|
351
|
+
if (!Array.isArray(props.footer) || props.footer.length === 0) return undefined;
|
|
352
|
+
return props.footer.map((item) => ({
|
|
353
|
+
id: String(item.value),
|
|
354
|
+
label: item.label,
|
|
355
|
+
disabled: item.disabled,
|
|
356
|
+
danger: item.danger,
|
|
357
|
+
}));
|
|
358
|
+
}, [props.footer]);
|
|
359
|
+
|
|
360
|
+
const selectedOption = React.useMemo(() => {
|
|
361
|
+
for (const opt of props.options || []) {
|
|
362
|
+
if (Array.isArray(opt.options)) {
|
|
363
|
+
const found = opt.options.find((sub) => String(sub.value) === String(props.value));
|
|
364
|
+
if (found) return found;
|
|
365
|
+
} else if (String(opt.value) === String(props.value)) {
|
|
366
|
+
return opt;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return null;
|
|
370
|
+
}, [props.options, props.value]);
|
|
371
|
+
|
|
372
|
+
const currentText = selectedOption ? selectedOption.label : (props.placeholder || props["aria-label"] || "");
|
|
373
|
+
|
|
374
|
+
return React.createElement("div", {
|
|
375
|
+
ref: rootRef,
|
|
376
|
+
className: "dfc-menu-select-wrap " + (props.className || ""),
|
|
377
|
+
style: props.style,
|
|
378
|
+
},
|
|
379
|
+
React.createElement(HostMenu, {
|
|
380
|
+
open: open,
|
|
381
|
+
listClassName: ["dfc-form-menu", props.listClassName].filter(Boolean).join(" "),
|
|
382
|
+
onClose: () => setOpen(false),
|
|
383
|
+
onSelect: (id) => {
|
|
384
|
+
if (props.footer && props.footer.some((item) => String(item.value) === id)) {
|
|
385
|
+
if (typeof props.onFooter === "function") props.onFooter(id);
|
|
386
|
+
setOpen(false);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (props.onChange) props.onChange(id);
|
|
390
|
+
setOpen(false);
|
|
391
|
+
},
|
|
392
|
+
selectedId: props.value != null ? String(props.value) : undefined,
|
|
393
|
+
align: "start",
|
|
394
|
+
portal: true,
|
|
395
|
+
dense: true,
|
|
396
|
+
anchor: React.createElement(Button, {
|
|
397
|
+
variant: "outline",
|
|
398
|
+
size: "sm",
|
|
399
|
+
role: "combobox",
|
|
400
|
+
"aria-label": props["aria-label"] || props.placeholder,
|
|
401
|
+
"aria-expanded": open,
|
|
402
|
+
disabled: props.disabled,
|
|
403
|
+
className: "dfc-menu-select-trigger" + (open ? " is-open" : ""),
|
|
404
|
+
onClick: () => { if (!props.disabled) setOpen((prev) => !prev); },
|
|
405
|
+
},
|
|
406
|
+
React.createElement("span", { className: "dfc-menu-select-label" }, currentText),
|
|
407
|
+
h(IconChevronDownOutline, {
|
|
408
|
+
size: 12,
|
|
409
|
+
className: "dfc-menu-select-arrow",
|
|
410
|
+
style: { marginLeft: 4, transition: "transform 150ms ease", transform: open ? "rotate(180deg)" : "none" },
|
|
411
|
+
})
|
|
412
|
+
),
|
|
413
|
+
items: items,
|
|
414
|
+
...(footer ? { footer } : {}),
|
|
415
|
+
})
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
function createSpinner() {
|
|
419
|
+
return h(IconLoadingOutline, {
|
|
420
|
+
size: 14,
|
|
421
|
+
className: "dfc-btn-spinner",
|
|
422
|
+
style: { marginRight: 6, display: "inline-block", verticalAlign: "-2px", animation: "dfc-spin 0.8s linear infinite" },
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
function Button({ variant, type, danger, loading, block, size, shape, className, children, href, target, rel, icon, ...props }) {
|
|
426
|
+
let mappedVariant = variant ?? "outline";
|
|
427
|
+
if (type === "primary") mappedVariant = "primary";
|
|
428
|
+
else if (type === "text" || type === "link") mappedVariant = "ghost";
|
|
429
|
+
|
|
430
|
+
const isPrimary = mappedVariant === "primary";
|
|
431
|
+
const mappedSize = (size === "small" || size === "sm") ? "sm" : "md";
|
|
432
|
+
|
|
433
|
+
const classes = [
|
|
434
|
+
"dfc-btn",
|
|
435
|
+
href ? "dfc-link-btn" : "",
|
|
436
|
+
className,
|
|
437
|
+
isPrimary ? "is-primary" : "",
|
|
438
|
+
danger ? "is-danger dfc-btn-danger" : "",
|
|
439
|
+
(danger && isPrimary) ? "dfc-btn-danger-primary" : "",
|
|
440
|
+
block ? "is-block" : "",
|
|
441
|
+
loading ? "is-loading" : "",
|
|
442
|
+
].filter(Boolean).join(" ");
|
|
443
|
+
|
|
444
|
+
const iconElement = loading
|
|
445
|
+
? createSpinner()
|
|
446
|
+
: (icon ? React.createElement("span", {
|
|
447
|
+
key: "icon",
|
|
448
|
+
className: "dfc-btn-icon",
|
|
449
|
+
style: { display: "inline-flex", alignItems: "center" },
|
|
450
|
+
}, icon) : null);
|
|
451
|
+
const content = [iconElement, children];
|
|
452
|
+
|
|
453
|
+
if (href) {
|
|
454
|
+
return React.createElement("a", Object.assign({}, props, {
|
|
455
|
+
href,
|
|
456
|
+
target,
|
|
457
|
+
rel: rel || (target === "_blank" ? "noreferrer" : undefined),
|
|
458
|
+
className: classes || undefined,
|
|
459
|
+
"aria-label": props["aria-label"],
|
|
460
|
+
style: props.style,
|
|
461
|
+
}), content);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (isHostComponent(PrimitiveButton)) {
|
|
465
|
+
return React.createElement(PrimitiveButton, Object.assign({}, props, {
|
|
466
|
+
variant: mappedVariant,
|
|
467
|
+
size: mappedSize,
|
|
468
|
+
className: [className, danger ? "dfc-btn-danger" : ""].filter(Boolean).join(" ") || undefined,
|
|
469
|
+
disabled: props.disabled || loading,
|
|
470
|
+
icon: loading ? undefined : icon,
|
|
471
|
+
}), loading ? createSpinner() : children);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return React.createElement("button", Object.assign({}, props, {
|
|
475
|
+
type: "button",
|
|
476
|
+
className: classes || undefined,
|
|
477
|
+
disabled: props.disabled || loading,
|
|
478
|
+
}), content);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const IconSettingsOutline16 = pickHostIcon("IconSettingsOutlineRegular", "IconSettingsOutline16");
|
|
482
|
+
const CHANNEL_SETTINGS_EVENT = "dsh-feishu-channels:open-channel-settings";
|
|
483
|
+
const CHANNEL_SETTINGS_STORAGE_KEY = "dsh-feishu-channels:pending-channel-settings";
|
|
484
|
+
const SETTINGS_CODEX_TRIGGER_SELECTOR = "[data-dcu-settings-trigger]";
|
|
485
|
+
const SETTINGS_CODEX_PAGE_SELECTOR = "[data-dcu-settings-page]";
|
|
486
|
+
const SETTINGS_OPEN_SECTION_EVENT = "dcu-settings-open-section";
|
|
487
|
+
function parseChannelSettingsRequest(value) {
|
|
488
|
+
if (!value || typeof value !== "object") return undefined;
|
|
489
|
+
const channelId = typeof value.channelId === "string" ? value.channelId.trim() : "";
|
|
490
|
+
if (!channelId) return undefined;
|
|
491
|
+
return { channelId, name: typeof value.name === "string" ? value.name : "" };
|
|
492
|
+
}
|
|
493
|
+
function writeChannelSettingsRequest(storage, request) {
|
|
494
|
+
try { storage && storage.setItem(CHANNEL_SETTINGS_STORAGE_KEY, JSON.stringify(request)); } catch { /* ignore */ }
|
|
495
|
+
}
|
|
496
|
+
function requestChannelSettings(request) {
|
|
497
|
+
if (typeof window === "undefined") return;
|
|
498
|
+
writeChannelSettingsRequest(window.sessionStorage, request);
|
|
499
|
+
window.dispatchEvent(new CustomEvent(CHANNEL_SETTINGS_EVENT, { detail: request }));
|
|
500
|
+
}
|
|
501
|
+
function readChannelSettingsRequest(storage) {
|
|
502
|
+
if (!storage) return undefined;
|
|
503
|
+
try {
|
|
504
|
+
const raw = storage.getItem(CHANNEL_SETTINGS_STORAGE_KEY);
|
|
505
|
+
return raw == null ? undefined : parseChannelSettingsRequest(JSON.parse(raw));
|
|
506
|
+
} catch { return undefined; }
|
|
507
|
+
}
|
|
508
|
+
function clearChannelSettingsRequest(storage) {
|
|
509
|
+
try { storage && storage.removeItem(CHANNEL_SETTINGS_STORAGE_KEY); } catch { /* ignore */ }
|
|
510
|
+
}
|
|
511
|
+
function resolveChannelSettingsTarget(request, channels) {
|
|
512
|
+
const parsed = parseChannelSettingsRequest(request);
|
|
513
|
+
if (!parsed || !Array.isArray(channels)) return undefined;
|
|
514
|
+
const channel = channels.find((item) => item && item.id === parsed.channelId);
|
|
515
|
+
if (!channel) return undefined;
|
|
516
|
+
const accounts = channel.accounts || [];
|
|
517
|
+
return { channelId: channel.id, accountId: accounts.length === 1 ? accounts[0].id : undefined };
|
|
518
|
+
}
|
|
519
|
+
function pickSettingsSectionButton(buttons, labels) {
|
|
520
|
+
const normalize = (value) => String(value || "").replace(/\s+/g, "").trim().toLowerCase();
|
|
521
|
+
const labelOf = (button) => normalize(button.textContent) || normalize(button.getAttribute && button.getAttribute("aria-label"));
|
|
522
|
+
const targets = labels.map(normalize).filter(Boolean);
|
|
523
|
+
for (const target of targets) {
|
|
524
|
+
const match = buttons.find((button) => labelOf(button) === target);
|
|
525
|
+
if (match) return match;
|
|
526
|
+
}
|
|
527
|
+
// 宿主可能在标签旁加计数或徽标;退一步做包含匹配。
|
|
528
|
+
for (const target of targets) {
|
|
529
|
+
const match = buttons.find((button) => {
|
|
530
|
+
const text = labelOf(button);
|
|
531
|
+
return text !== "" && (text.includes(target) || target.includes(text));
|
|
532
|
+
});
|
|
533
|
+
if (match) return match;
|
|
534
|
+
}
|
|
535
|
+
return undefined;
|
|
536
|
+
}
|
|
537
|
+
function pickSettingsLauncher(buttons) {
|
|
538
|
+
const named = buttons.find((button) => {
|
|
539
|
+
const label = `${button.textContent || ""} ${typeof button.getAttribute === "function" ? button.getAttribute("aria-label") || "" : ""}`.trim();
|
|
540
|
+
return /(^|\s)(设置|settings)(\s|$)/i.test(label);
|
|
541
|
+
});
|
|
542
|
+
return named || (buttons.length === 1 ? buttons[0] : undefined);
|
|
543
|
+
}
|
|
544
|
+
function findAccountMenuTrigger() {
|
|
545
|
+
const exact = [
|
|
546
|
+
'button[aria-label="账号菜单"]',
|
|
547
|
+
'button[aria-label="Account menu"]',
|
|
548
|
+
];
|
|
549
|
+
for (const selector of exact) {
|
|
550
|
+
const found = document.querySelector(selector);
|
|
551
|
+
if (found) return found;
|
|
552
|
+
}
|
|
553
|
+
return [...document.querySelectorAll('button[aria-haspopup="menu"]')].find((button) => {
|
|
554
|
+
const label = `${typeof button.getAttribute === "function" ? button.getAttribute("aria-label") || "" : ""} ${button.textContent || ""}`.trim();
|
|
555
|
+
return /账号|account/i.test(label);
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
function requestCodexSettingsSection(labels) {
|
|
559
|
+
const trigger = typeof document !== "undefined" && document.querySelector(SETTINGS_CODEX_TRIGGER_SELECTOR);
|
|
560
|
+
if (!trigger) return false;
|
|
561
|
+
const request = new CustomEvent(SETTINGS_OPEN_SECTION_EVENT, { detail: { labels: [...labels] }, cancelable: true });
|
|
562
|
+
return !trigger.dispatchEvent(request);
|
|
563
|
+
}
|
|
564
|
+
function openImSettingsPage(t) {
|
|
565
|
+
openSettingsSection(
|
|
566
|
+
[t("settings.label"), DFC_LOCALES.en["settings.label"], DFC_LOCALES.zh["settings.label"]],
|
|
567
|
+
() => {},
|
|
568
|
+
() => console.warn("[dsh-feishu-channels] 无法打开 IM 助理设置页"),
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
function openSettingsSection(labels, onSelected, onMissing) {
|
|
572
|
+
if (typeof document === "undefined" || typeof window === "undefined") {
|
|
573
|
+
if (onMissing) onMissing();
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
if (requestCodexSettingsSection(labels)) {
|
|
577
|
+
if (onSelected) onSelected();
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
const launchers = [...document.querySelectorAll('button[aria-haspopup="dialog"], button[aria-label*="设置"], button[aria-label*="Settings"], [data-dcu-settings-trigger]')];
|
|
581
|
+
const namedSettingsButton = [...document.querySelectorAll("button")].find((button) => /^(设置|settings)$/i.test((button.getAttribute("aria-label") || "").trim()));
|
|
582
|
+
const launcher = namedSettingsButton || pickSettingsLauncher(launchers);
|
|
583
|
+
const pageTrigger = document.querySelector(SETTINGS_CODEX_TRIGGER_SELECTOR);
|
|
584
|
+
const dialogOpen = document.querySelector('[role="dialog"]') !== null;
|
|
585
|
+
const pageOpen = document.querySelector(SETTINGS_CODEX_PAGE_SELECTOR) !== null;
|
|
586
|
+
let menuTrigger;
|
|
587
|
+
if (!dialogOpen && !pageOpen && !launcher && !pageTrigger) {
|
|
588
|
+
menuTrigger = findAccountMenuTrigger();
|
|
589
|
+
if (!menuTrigger) {
|
|
590
|
+
if (onMissing) onMissing();
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
menuTrigger.click();
|
|
594
|
+
} else if (!dialogOpen && !pageOpen) {
|
|
595
|
+
(pageTrigger || launcher).click();
|
|
596
|
+
}
|
|
597
|
+
let frame;
|
|
598
|
+
let finished = false;
|
|
599
|
+
let menuActivated = false;
|
|
600
|
+
let timeout;
|
|
601
|
+
const observer = new MutationObserver(() => { schedule(); });
|
|
602
|
+
const cleanup = () => {
|
|
603
|
+
if (finished) return;
|
|
604
|
+
finished = true;
|
|
605
|
+
observer.disconnect();
|
|
606
|
+
window.clearTimeout(timeout);
|
|
607
|
+
if (frame !== undefined) window.cancelAnimationFrame(frame);
|
|
608
|
+
};
|
|
609
|
+
const wait = () => {
|
|
610
|
+
timeout = window.setTimeout(() => {
|
|
611
|
+
if (select()) return;
|
|
612
|
+
cleanup();
|
|
613
|
+
if (onMissing) onMissing();
|
|
614
|
+
}, 5000);
|
|
615
|
+
};
|
|
616
|
+
const select = () => {
|
|
617
|
+
if (menuTrigger !== undefined && !menuActivated) {
|
|
618
|
+
const items = [...document.querySelectorAll('[role="menuitem"], [role="menu"] button')];
|
|
619
|
+
const entry = pickSettingsSectionButton(items, ["设置", "Settings"]);
|
|
620
|
+
if (!entry) return false;
|
|
621
|
+
menuActivated = true;
|
|
622
|
+
window.clearTimeout(timeout);
|
|
623
|
+
wait();
|
|
624
|
+
entry.click();
|
|
625
|
+
schedule();
|
|
626
|
+
return true;
|
|
627
|
+
}
|
|
628
|
+
const buttons = [
|
|
629
|
+
...document.querySelectorAll('[role="dialog"] nav button, [role="dialog"] button'),
|
|
630
|
+
...document.querySelectorAll(SETTINGS_CODEX_PAGE_SELECTOR + " nav button"),
|
|
631
|
+
...document.querySelectorAll("nav button, button"),
|
|
632
|
+
];
|
|
633
|
+
const target = pickSettingsSectionButton(buttons, labels);
|
|
634
|
+
if (!target) return false;
|
|
635
|
+
cleanup();
|
|
636
|
+
target.click();
|
|
637
|
+
if (onSelected) onSelected();
|
|
638
|
+
return true;
|
|
639
|
+
};
|
|
640
|
+
const schedule = () => {
|
|
641
|
+
if (finished || frame !== undefined) return;
|
|
642
|
+
frame = window.requestAnimationFrame(() => { frame = undefined; select(); });
|
|
643
|
+
};
|
|
644
|
+
wait();
|
|
645
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
646
|
+
schedule();
|
|
647
|
+
}
|
|
648
|
+
const EMPTY_EXTRA_TABS = [];
|
|
649
|
+
const inject = ["slots", "sessions", "workspaces", "locale"];
|
|
650
|
+
const API_BASE = "/api/dsh-feishu-channels";
|
|
651
|
+
const TAB_KEY = "dsh-feishu-channels.sidebar-tab";
|
|
652
|
+
const ACCOUNT_SELECTION_KEY = "dsh-feishu-channels.settings.selected-account";
|
|
653
|
+
const DFC_LOCALE_NS = "feishu-channel";
|
|
654
|
+
const DFC_LOCALES = {
|
|
655
|
+
zh: {
|
|
656
|
+
"diagnostic.title": "连接诊断", "diagnostic.scope": "以下为本次平台接口或心跳检查结果。未发送测试消息,消息投递权限仍未验证。", "diagnostic.credentials": "应用凭据验证", "diagnostic.bot": "机器人身份查询", "diagnostic.gateway": "网关查询", "diagnostic.passed": "通过", "diagnostic.failed": "失败", "diagnostic.unverified": "未验证",
|
|
657
|
+
"diagnostic.local-state": "无法读取凭据或初始化本地账号状态,请检查本机配置及文件访问权限。", "diagnostic.cancelled": "诊断已取消,未确认连接状态。", "diagnostic.ok": "平台已返回有效响应", "diagnostic.auth": "凭据缺失或失效,请重新配置或扫码绑定。", "diagnostic.permission": "平台拒绝访问,请检查应用权限。", "diagnostic.rate-limit": "平台限流,请稍后再试。", "diagnostic.server": "平台服务异常,请稍后再试。", "diagnostic.network": "网络请求失败,请检查网络或代理。", "diagnostic.timeout": "诊断超时,请检查网络后重试。", "diagnostic.invalid-response": "平台响应不完整或格式不符,无法确认可用。", "diagnostic.rejected": "平台拒绝请求,请核对应用配置和权限。", "diagnostic.missing-context": "缺少原聊天上下文,请用扫码账号先发送一条消息后重试。", "diagnostic.not-connected": "当前没有可验证的连接,请先重新连接。", "diagnostic.unsupported": "当前 SDK 或配置不支持此诊断,未判定为通过。", "diagnostic.changed": "账号状态已变化,请重新诊断。", "connection.restartRequired": "前后端版本不一致。请完整重启 DSH 并刷新页面后重新诊断。", "connection.receiveUnknown": "无法读取接收开关,请重启 DSH 并刷新页面。",
|
|
658
|
+
"connection.checkFailed": "状态检查未完成,请重试;仍失败时查看本机日志。", "connection.diagnostics": "连接诊断", "action.collapse": "收起", "action.expand": "展开", "composer.remoteTitle": "飞书频道会话", "composer.remoteHint": "该会话只能在飞书客户端继续;本地输入已禁用。", "composer.model": "模型", "composer.modelLoading": "模型加载中", "composer.modelUnavailable": "模型不可用", "composer.modelReadOnly": "在账号设置中切换模型", "composer.stopTitle": "停止生成?", "composer.stopDescription": "将中断当前会话正在运行的大模型推理;已排队的消息会保留。", "composer.stopConfirm": "停止生成",
|
|
659
|
+
"connection.connected": "已连接", "connection.connecting": "连接中", "connection.reconnecting": "重连中", "connection.disconnected": "未连接", "connection.stopped": "已停止", "connection.error": "连接异常", "connection.unknown": "状态未知", "connection.paused": "接收已暂停",
|
|
660
|
+
"connection.title": "本次状态检查", "connection.scope": "仅检查本机渠道运行状态,未验证平台凭据或消息收发。", "connection.checkedAt": "检查时间", "connection.receiveOn": "接收开关已开启", "connection.receiveOff": "接收开关已关闭;请在账号列表开启后再发送消息。", "connection.checking": "检查中…",
|
|
661
|
+
"connection.next.connected": "可在原聊天发送消息验证收发;私聊仍需符合准入设置。", "connection.next.connecting": "请稍后再次检查;等待扫码时请完成手机确认。", "connection.next.reconnecting": "渠道正在尝试恢复,请稍后再次检查。", "connection.next.disconnected": "请重新连接;需要扫码的渠道可重新添加绑定。", "connection.next.stopped": "请点击重新连接以启动账号。", "connection.next.error": "请尝试重新连接;仍有异常时查看本机日志。", "connection.next.unknown": "暂时无法确认状态,请稍后再检查或查看本机日志。",
|
|
662
|
+
"settings.label": "飞书频道", "settings.title": "飞书频道", "settings.description": "管理飞书机器人账号。每个账号独立选择工作区、模型和权限,配置仅保存在本机。", "settings.aria": "飞书频道", "settings.publicChatNotice": "未批准用户可以发起聊天,但不能批准工具调用。", "pending.notice": "有访问请求。批准后该用户才能驱动本机助手。", "action.approve": "批准", "action.deny": "拒绝", "loading": "加载中…",
|
|
663
|
+
"account.presetUnavailable": "预设不可用", "account.presetNote": "更改后使用新会话,旧会话保留。", "action.settings": "设置", "command.dm": "私聊命令", "command.group": "群聊命令", "command.enabled": "可以执行命令", "command.disabled": "仅正常对话",
|
|
664
|
+
"account.workspace": "工作区", "account.currentWorkspace": "当前工作区", "account.selectWorkspace": "请选择工作区", "account.selectModel": "请选择模型", "account.selectPermission": "请选择权限", "account.privateAccess": "私聊准入", "account.privateApproved": "仅已批准用户", "account.privateAll": "允许所有私聊用户", "account.sendMode": "发送方式", "account.sendQueue": "排队发送", "account.sendSteer": "插话发送", "account.sendSteerNote": "插话发送会把消息作为补充指令立即交给运行中的任务;任务空闲时与排队发送相同。", "account.autoNameNote": "该名称会作为本地频道名称,并预填到飞书应用创建页。", "account.defaultName": "{channel}账号 {count}", "account.channelName": "频道名称", "account.noSessions": "还没有会话,请通过飞书客户端给机器人发一条消息...", "channel.empty": "还没有飞书账号。点击上方“添加飞书账号”开始。", "session.untitled": "未命名会话", "app.info": "飞书App信息", "app.id": "App ID", "app.secret": "App Secret", "app.copy": "复制", "app.copied": "已复制", "app.reveal": "显示", "app.hide": "隐藏", "app.notSaved": "未保存", "provisioning.redirectMissing": "重定向 URL 未能自动写入", "provisioning.scopesMissing": "部分权限未确认", "provisioning.copyUrls": "复制重定向 URL", "provisioning.manageScopeMissing": "缺少应用配置权限 application:application:patch,无法写入重定向 URL。", "provisioning.applyScope": "去添加权限",
|
|
665
|
+
"account.remark": "名称备注", "account.remarkNote": "只在本机设置页区分账号,留空恢复自动名称。", "account.rename": "修改名称备注",
|
|
666
|
+
"account.count": "{count} 个账号", "account.statusProcessing": "处理中…", "account.statusNotConnected": "未连接", "account.receive": "接收消息", "account.receiveDescription": "关闭后保留账号配置,但不接收新消息", "account.removeConfirm": "确定移除这个账号?本机保存的配置和凭据将一并删除。",
|
|
667
|
+
"action.addAccount": "添加飞书账号", "action.generateQr": "生成二维码", "action.checkConnection": "诊断连接", "action.reconnectAccount": "重新连接", "action.removeAccount": "移除接入", "status.saving": "保存中…", "status.saved": "已保存",
|
|
668
|
+
"channel.feishu": "飞书", "bind.title": "添加飞书账号", "bind.tabsAria": "绑定方式", "bind.quick": "快捷绑定(推荐)", "bind.manual": "手动配置", "bind.saving": "正在保存账号…", "bind.success": "绑定成功,频道已连接", "bind.newIdentity": "检测到新的账号身份,已创建新账号", "bind.qrAlt": "{channel} 绑定二维码", "bind.generating": "正在生成…", "bind.expire": "二维码 {time} 后过期", "bind.scanned": "已扫码,请在手机上确认", "bind.retry": "请重新生成二维码", "bind.refresh": "重新生成二维码", "action.saving": "保存中…", "action.confirm": "确认",
|
|
669
|
+
"qr.feishu": "请使用飞书扫描二维码,将自动创建机器人", "qr.default": "请使用对应 App 扫描二维码",
|
|
670
|
+
"action.more": "{channel} 更多",
|
|
671
|
+
"error.loadAssistant": "无法加载账号配置", "error.noModels": "当前 Host 还没有可用模型,请先在网页里配置提供商", "error.save": "保存失败", "error.chooseWorkspace": "请选择工作区目录", "error.workspaceUnavailable": "当前 Host 无法新增工作区", "error.addWorkspace": "新增工作区失败", "error.load": "加载失败", "error.connection": "无法连接本机 IM 助理接口", "error.request": "请求失败", "error.action": "操作失败", "error.qr": "无法生成二维码", "error.detailsInLog": "操作失败,请查看服务器日志。",
|
|
672
|
+
"composer.noWorkspaces": "暂无工作区", "composer.addWorkspace": "添加工作区…", "composer.workspaceRequired": "请先添加工作区,否则无法保存", "composer.permission": "权限", "composer.noModels": "暂无模型", "action.cancel": "取消",
|
|
673
|
+
"permission.readOnly": "只读", "permission.workspaceWrite": "工作区写入", "permission.fullAccess": "完全访问",
|
|
674
|
+
"rail.workspace": "工作区", "rail.sessions": "会话", "rail.search": "搜索", "rail.searchPlaceholder": "搜索会话...", "rail.clearSearch": "清除搜索", "rail.filter": "筛选", "rail.group": "分组方式", "rail.byWorkspace": "按工作区", "rail.list": "单列表", "rail.byWorkspaceTree": "按工作区树", "rail.byChannel": "按飞书账号", "rail.treeUnavailable": "当前宿主未提供官方任务树。", "rail.groupReadOnly": "飞书分组不能在这里重命名或删除。", "rail.sort": "排序方式", "rail.recent": "最近更新", "rail.showArchived": "显示已归档", "rail.onlyArchived": "仅显示已归档", "rail.running": "运行中", "rail.idle": "空闲", "rail.waitingApproval": "等待审批", "rail.planReview": "计划待审", "rail.waitingAnswer": "等待回答", "rail.subagents": "{n} 个子代理运行中", "rail.completed": "已完成", "rail.archived": "已归档", "rail.compactApproval": "待审批", "rail.compactPlan": "计划待审", "rail.compactAnswer": "待回答", "rail.renameAria": "重命名会话", "rail.rename": "重命名", "rail.fork": "分叉会话", "rail.archive": "归档会话", "rail.unarchive": "恢复会话", "rail.deleteSession": "删除会话", "rail.deleteSessionConfirm": "删除会话", "rail.deleteSessionDescription": "将永久删除会话“{name}”及其子代理(含正在运行的)和全部记录(对话内容、统计、缓存),此操作不可恢复。", "rail.deleteSessionPending": "正在删除会话…", "rail.deleteSessionFailed": "删除会话失败:{message}", "rail.deleteSessionClose": "关闭", "rail.deleteSessionCancel": "取消", "rail.groupActions": "“{name}”的飞书操作", "rail.channelSettings": "飞书频道设置", "rail.addChannel": "添加飞书账号", "rail.empty": "还没有飞书会话。先在设置 → 飞书频道 里连接飞书机器人,并给机器人发一条消息。", "rail.ungrouped": "未分组", "rail.tabsAria": "工作区分类", "rail.tasks": "任务", "rail.channels": "频道", "rail.feishu": "飞书频道",
|
|
675
|
+
"time.now": "刚刚", "time.minutes": "{n}分钟", "time.hours": "{n}小时", "time.days": "{n}天", "time.months": "{n}个月", "time.years": "{n}年", "time.ago": "{t}前",
|
|
676
|
+
"copy": "复制", "hover.copied": "已复制",
|
|
677
|
+
"server.unknownChannel": "未知渠道", "server.channelUnconfigured": "渠道未配置", "server.sessionMissing": "会话不存在", "server.accountMissing": "账号不存在", "server.accountConnectFailed": "账号连接失败,请查看本机日志", "server.accountReconnectFailed": "重新连接失败,请查看本机日志", "server.selectAccountSettings": "请选择提供商、模型、工作区或权限", "server.selectModel": "请选择提供商和模型", "server.selectWorkspace": "请选择工作区", "server.createWorkspace": "请先添加工作区", "server.selectPermission": "请选择权限", "server.missingCredentials": "凭据不足,无法启动渠道", "server.qrUnsupported": "该渠道不支持扫码绑定", "server.qrExpired": "二维码已过期", "server.qrIncomplete": "扫码未完成", "server.accessDenied": "未授权:请管理员在设置 → 飞书频道 中批准你的访问。", "server.remarkTooLong": "账号备注不能超过 40 个字符", "server.remarkControl": "账号备注不能包含换行或控制字符",
|
|
678
|
+
"status.stopped": "已停止", "status.longConnection": "长连接已建立", },
|
|
679
|
+
en: {
|
|
680
|
+
"diagnostic.title": "Connection diagnostics", "diagnostic.scope": "Results from platform queries or a connection heartbeat in this check. No test messages were sent; message delivery permissions remain unverified.", "diagnostic.credentials": "Application credentials", "diagnostic.bot": "Bot identity query", "diagnostic.gateway": "Gateway query", "diagnostic.passed": "Passed", "diagnostic.failed": "Failed", "diagnostic.unverified": "Unverified",
|
|
681
|
+
"diagnostic.local-state": "Could not read credentials or initialize local account state. Check local configuration and file permissions.", "diagnostic.cancelled": "The check was cancelled. Connection status was not verified.", "diagnostic.ok": "The platform returned a valid response", "diagnostic.auth": "Credentials are missing or invalid. Configure or pair the account again.", "diagnostic.permission": "Access was denied. Check application permissions.", "diagnostic.rate-limit": "The platform rate limit was reached. Retry later.", "diagnostic.server": "The platform service failed. Retry later.", "diagnostic.network": "The request failed. Check your network or proxy.", "diagnostic.timeout": "The check timed out. Check your network and retry.", "diagnostic.invalid-response": "The platform response was incomplete or invalid. Availability could not be confirmed.", "diagnostic.rejected": "The platform rejected the request. Check application settings and permissions.", "diagnostic.missing-context": "The original chat context is missing. Send a message from the paired account and retry.", "diagnostic.not-connected": "No active connection is available to verify. Reconnect first.", "diagnostic.unsupported": "The current SDK or configuration does not support this check. It has not passed.", "diagnostic.changed": "The account state changed. Run diagnostics again.", "connection.restartRequired": "The frontend and backend versions do not match. Fully restart DSH, refresh this page, and retry.", "connection.receiveUnknown": "The receiving setting is unavailable. Restart DSH and refresh this page.",
|
|
682
|
+
"connection.checkFailed": "The status check could not be completed. Retry, or check the local logs if it keeps failing.", "connection.diagnostics": "Connection diagnostics", "action.collapse": "Collapse", "action.expand": "Expand", "composer.remoteTitle": "Feishu channel session", "composer.remoteHint": "This session can only continue in Feishu; local input is disabled.", "composer.model": "Model", "composer.modelLoading": "Loading models", "composer.modelUnavailable": "Model unavailable", "composer.modelReadOnly": "Change the model in account settings", "composer.stopTitle": "Stop generating?", "composer.stopDescription": "This interrupts the running model turn in this session; queued messages are preserved.", "composer.stopConfirm": "Stop generating",
|
|
683
|
+
"connection.connected": "Connected", "connection.connecting": "Connecting", "connection.reconnecting": "Reconnecting", "connection.disconnected": "Disconnected", "connection.stopped": "Stopped", "connection.error": "Connection error", "connection.unknown": "Status unknown", "connection.paused": "Receiving paused",
|
|
684
|
+
"connection.title": "Status check result", "connection.scope": "Checks local channel runtime only. Platform credentials and message delivery have not been verified.", "connection.checkedAt": "Checked at", "connection.receiveOn": "Receiving is enabled", "connection.receiveOff": "Receiving is disabled. Enable it in the account list before sending messages.", "connection.checking": "Checking…",
|
|
685
|
+
"connection.next.connected": "Send a message in the original chat to verify delivery. Private chats still follow access settings.", "connection.next.connecting": "Check again shortly. Complete confirmation on your phone if a QR scan is pending.", "connection.next.reconnecting": "The channel is attempting to recover. Check again shortly.", "connection.next.disconnected": "Reconnect the account. QR channels may need to be added again.", "connection.next.stopped": "Select Reconnect to start the account.", "connection.next.error": "Try reconnecting. If the issue persists, check the local logs.", "connection.next.unknown": "The current state could not be confirmed. Check again later or inspect the local logs.",
|
|
686
|
+
"settings.label": "Feishu Channel", "settings.title": "Feishu Channel", "settings.description": "Manage Feishu bot accounts. Each account has its own workspace, model, and permission settings, stored only on this machine.", "settings.aria": "Feishu Channel", "settings.publicChatNotice": "Unapproved users can start chats, but they cannot approve tool calls.", "pending.notice": "There are access requests. Approve a user before they can control the local assistant.", "action.approve": "Approve", "action.deny": "Deny", "loading": "Loading…",
|
|
687
|
+
"account.presetUnavailable": "Preset unavailable", "account.presetNote": "Changes start a new session; previous sessions are kept.", "action.settings": "Settings", "command.dm": "DM commands", "command.group": "Group commands", "command.enabled": "Commands enabled", "command.disabled": "Conversation only",
|
|
688
|
+
"account.workspace": "Workspace", "account.currentWorkspace": "Current workspace", "account.selectWorkspace": "Select a workspace", "account.selectModel": "Select a model", "account.selectPermission": "Select a permission", "account.privateAccess": "Private chat access", "account.privateApproved": "Approved users only", "account.privateAll": "Allow all DM users", "account.sendMode": "Send mode", "account.sendQueue": "Queue", "account.sendSteer": "Steer", "account.sendSteerNote": "Steer hands the message to the running task as additional instructions at its next step; when the task is idle it behaves like Queue.", "account.autoNameNote": "Used as the local channel name and prefilled on the Feishu app creation page.", "account.defaultName": "{channel} account {count}", "account.channelName": "Channel name", "account.noSessions": "No sessions yet. Send the bot a message from the Feishu client...", "channel.empty": "No Feishu accounts yet. Use “Add Feishu account” above.", "session.untitled": "Untitled session", "app.info": "Feishu app info", "app.id": "App ID", "app.secret": "App Secret", "app.copy": "Copy", "app.copied": "Copied", "app.reveal": "Show", "app.hide": "Hide", "app.notSaved": "Not saved", "provisioning.redirectMissing": "Redirect URLs were not written automatically", "provisioning.scopesMissing": "Some scopes were not confirmed", "provisioning.copyUrls": "Copy redirect URLs", "provisioning.manageScopeMissing": "The app is missing application:application:patch, so redirect URLs cannot be written.", "provisioning.applyScope": "Add permission",
|
|
689
|
+
"account.remark": "Name remark", "account.remarkNote": "Shown only in settings to tell accounts apart. Leave blank to restore the automatic name.", "account.rename": "Edit name remark",
|
|
690
|
+
"account.count": "{count} accounts", "account.statusProcessing": "Processing…", "account.statusNotConnected": "Not connected", "account.receive": "Receive messages", "account.receiveDescription": "Turn this off to keep the account settings without receiving new messages", "account.removeConfirm": "Remove this account? Its saved settings and credentials will also be deleted.",
|
|
691
|
+
"action.addAccount": "Add Feishu account", "action.generateQr": "Generate QR code", "action.checkConnection": "Diagnose connection", "action.reconnectAccount": "Reconnect", "action.removeAccount": "Remove", "status.saving": "Saving…", "status.saved": "Saved",
|
|
692
|
+
"channel.feishu": "Feishu", "bind.title": "Add Feishu account", "bind.tabsAria": "Setup method", "bind.quick": "Quick setup (recommended)", "bind.manual": "Manual setup", "bind.saving": "Saving account…", "bind.success": "Connected successfully", "bind.newIdentity": "A new account identity was detected and a new account was created", "bind.qrAlt": "{channel} setup QR code", "bind.generating": "Generating…", "bind.expire": "QR code expires in {time}", "bind.scanned": "Scanned. Confirm on your phone.", "bind.retry": "Generate a new QR code", "bind.refresh": "Generate a new QR code", "action.saving": "Saving…", "action.confirm": "Confirm",
|
|
693
|
+
"qr.feishu": "Scan with Feishu; a bot will be created automatically", "qr.default": "Scan the QR code with the corresponding app",
|
|
694
|
+
"action.more": "More options for {channel}",
|
|
695
|
+
"error.loadAssistant": "Could not load account settings", "error.noModels": "No models are available in the Host. Configure a provider in the web app first.", "error.save": "Could not save", "error.chooseWorkspace": "Choose a workspace directory", "error.workspaceUnavailable": "This Host cannot create workspaces", "error.addWorkspace": "Could not add workspace", "error.load": "Could not load", "error.connection": "Could not connect to the local Feishu Channel API", "error.request": "Request failed", "error.action": "Action failed", "error.qr": "Could not generate a QR code", "error.detailsInLog": "The operation failed. Check the server logs for details.",
|
|
696
|
+
"composer.noWorkspaces": "No workspaces", "composer.addWorkspace": "Add workspace…", "composer.workspaceRequired": "Add a workspace before saving.", "composer.permission": "Permission", "composer.noModels": "No models", "action.cancel": "Cancel",
|
|
697
|
+
"permission.readOnly": "Read Only", "permission.workspaceWrite": "Workspace Write", "permission.fullAccess": "Full access",
|
|
698
|
+
"rail.workspace": "Workspaces", "rail.sessions": "Sessions", "rail.search": "Search", "rail.searchPlaceholder": "Search sessions...", "rail.clearSearch": "Clear search", "rail.filter": "Filter", "rail.group": "Group by", "rail.byWorkspace": "By workspace", "rail.list": "Single list", "rail.byWorkspaceTree": "Workspace Tree", "rail.byChannel": "By channel", "rail.treeUnavailable": "The official tree is unavailable in this Host.", "rail.groupReadOnly": "Feishu groups cannot be renamed or deleted here.", "rail.sort": "Sort by", "rail.recent": "Recently updated", "rail.showArchived": "Show archived", "rail.onlyArchived": "Archived only", "rail.running": "Running", "rail.idle": "Idle", "rail.waitingApproval": "Waiting for approval", "rail.planReview": "Plan to review", "rail.waitingAnswer": "Waiting for an answer", "rail.subagents": "{n} subagents running", "rail.completed": "Completed", "rail.archived": "Archived", "rail.compactApproval": "Approval", "rail.compactPlan": "Plan review", "rail.compactAnswer": "Answer", "rail.renameAria": "Rename session", "rail.rename": "Rename", "rail.fork": "Fork session", "rail.archive": "Archive session", "rail.unarchive": "Restore session", "rail.deleteSession": "Delete session", "rail.deleteSessionConfirm": "Delete session", "rail.deleteSessionDescription": "This permanently deletes session “{name}”, its child agents (including any that are still running), and all of its records (conversation, stats, cache). This cannot be undone.", "rail.deleteSessionPending": "Deleting session…", "rail.deleteSessionFailed": "Could not delete the session: {message}", "rail.deleteSessionClose": "Close", "rail.deleteSessionCancel": "Cancel", "rail.groupActions": "Actions for {name}", "rail.channelSettings": "Channel settings", "rail.addChannel": "Add channel", "rail.empty": "No Feishu sessions yet. Connect a Feishu bot in Settings → Feishu Channel, then send it a message.", "rail.ungrouped": "Ungrouped", "rail.tabsAria": "Workspace categories", "rail.tasks": "Tasks", "rail.channels": "Channels", "rail.feishu": "Feishu",
|
|
699
|
+
"time.now": "now", "time.minutes": "{n}min", "time.hours": "{n}h", "time.days": "{n}d", "time.months": "{n}mo", "time.years": "{n}y", "time.ago": "{t} ago",
|
|
700
|
+
"copy": "Copy", "hover.copied": "Copied",
|
|
701
|
+
"server.unknownChannel": "Unknown channel", "server.channelUnconfigured": "Channel is not configured", "server.sessionMissing": "Session does not exist", "server.accountMissing": "Account does not exist", "server.accountConnectFailed": "Could not connect the account. Check the local logs.", "server.accountReconnectFailed": "Could not reconnect the account. Check the local logs.", "server.selectAccountSettings": "Select a provider, model, workspace, or permission setting", "server.selectModel": "Select a provider and model", "server.selectWorkspace": "Select a workspace", "server.createWorkspace": "Add a workspace first.", "server.selectPermission": "Select a permission", "server.missingCredentials": "The channel cannot start because credentials are missing", "server.qrUnsupported": "This channel does not support QR setup", "server.qrExpired": "QR code has expired", "server.qrIncomplete": "QR setup was not completed", "server.accessDenied": "Access denied: ask an administrator to approve you in Settings → Feishu Channel.", "server.remarkTooLong": "Account remark cannot exceed 40 characters", "server.remarkControl": "Account remark cannot contain line breaks or control characters",
|
|
702
|
+
"status.stopped": "Stopped", "status.longConnection": "Long connection established", },
|
|
703
|
+
};
|
|
704
|
+
const fallbackT = (key) => key;
|
|
705
|
+
const h = React.createElement;
|
|
706
|
+
function HostThemeProvider({ children }) {
|
|
707
|
+
return children;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const CSS = `
|
|
711
|
+
.dfc-page,.dfc-account-modal,.dfc-bind-modal,.dfc-inspector{--dfc-text:var(--dsw-alias-label-primary,var(--dsh-text,#e6edf3));--dfc-muted:var(--dsw-alias-label-tertiary,var(--dsh-text-muted,#8b949e));--dfc-line:var(--dsw-alias-border-l2,var(--dsh-border,rgba(255,255,255,.1)));--dfc-card:var(--dsw-alias-bg-layer-2,rgba(255,255,255,.04));--dfc-card-hover:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.06));--dfc-ok:var(--dsw-alias-state-success-primary,#3fb950);--dfc-warning:var(--dsw-alias-state-warning-primary,#d29922);--dfc-danger:var(--dsw-alias-state-error-primary,#f85149);--dfc-accent:var(--dsw-alias-brand-primary,#4b7cff)}
|
|
712
|
+
.dfc-page{box-sizing:border-box;max-width:none;width:100%;margin:0;padding:0 0 32px;color:var(--dfc-text)}
|
|
713
|
+
.dfc-head{display:flex;flex-direction:column;align-items:stretch;gap:12px;margin-bottom:12px}
|
|
714
|
+
.dfc-heading h2,.dfc-title{margin:0;font-size:24px;line-height:32px;font-weight:600;letter-spacing:-.4px;white-space:nowrap;text-align:left}
|
|
715
|
+
.dfc-title-row{display:flex;align-items:center;gap:8px 12px;min-width:0;flex-wrap:wrap}
|
|
716
|
+
.dfc-title-links{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
717
|
+
.dfc-title-links>span[data-mpi-check]{display:inline-flex;align-items:center}
|
|
718
|
+
[data-dfc-nav-icon="assistant"]>svg{display:none!important}
|
|
719
|
+
.dfc-nav-icon-assistant{display:inline-block;flex:none;width:16px;height:16px;background:currentColor;-webkit-mask:var(--dfc-nav-icon-mask) center/contain no-repeat;mask:var(--dfc-nav-icon-mask) center/contain no-repeat}
|
|
720
|
+
.dfc-btn{display:inline-flex;align-items:center;justify-content:center;gap:4px;height:32px;padding:0 12px;border:0.5px solid var(--dsw-alias-border-l3,#555);border-radius:var(--dsw-radius-sm,8px);background:transparent;color:var(--dsw-alias-label-primary,#eee);font:inherit;font-size:13px;line-height:20px;cursor:pointer;user-select:none;box-sizing:border-box;text-decoration:none;transition:border-color 120ms ease,color 120ms ease,background 120ms ease}
|
|
721
|
+
.dfc-btn:not(.is-primary):not(.dfc-btn-danger){color:var(--dsw-alias-label-primary,#eee)!important}
|
|
722
|
+
.dfc-btn:hover:not(:disabled):not(.dfc-btn-danger){border-color:var(--dsw-alias-border-l3,#666)!important;color:var(--dsw-alias-label-primary,#fff)!important;background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.06))!important}
|
|
723
|
+
.dfc-btn:focus-visible{outline:var(--dsw-focus-ring-width,1px) solid var(--dsw-focus-ring-color,var(--dsw-alias-brand-primary,#4b7cff))!important;outline-offset:0!important}
|
|
724
|
+
.dfc-btn:disabled,.dfc-btn.is-disabled{opacity:.4!important;cursor:not-allowed!important;pointer-events:none!important}
|
|
725
|
+
.dfc-btn.is-primary,.dfc-bind-ready .dfc-btn.is-primary{background:var(--dsw-alias-button-primary-fill,#f9fafb)!important;border-color:transparent!important;color:var(--dsw-alias-label-primary-foreground,#0f1115)!important}
|
|
726
|
+
.dfc-btn.is-primary *,.dfc-bind-ready .dfc-btn.is-primary *{color:inherit!important;fill:currentColor!important}
|
|
727
|
+
.dfc-btn.is-primary:hover:not(:disabled),.dfc-bind-ready .dfc-btn.is-primary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover,#ebeef2)!important;color:var(--dsw-alias-label-primary-foreground,#0f1115)!important}
|
|
728
|
+
.dfc-btn.is-primary:disabled,.dfc-bind-ready .dfc-btn.is-primary:disabled{opacity:.4!important;cursor:not-allowed!important}
|
|
729
|
+
.dfc-btn.is-block{width:100%!important}
|
|
730
|
+
.dfc-link-btn{height:28px!important;padding:0 10px!important;font-size:12px!important}
|
|
731
|
+
.dfc-sub{margin:12px 0 0;max-width:none;color:var(--dfc-muted);font-size:14px;line-height:22px;text-align:left}
|
|
732
|
+
.dfc-error{color:var(--dfc-danger);font-size:12px;margin:0 0 12px}
|
|
733
|
+
.dfc-pending,.dfc-page>.dfc-feedback,.dfc-inspector .dfc-feedback{margin:0 0 12px}
|
|
734
|
+
.dfc-feedback{display:flex;align-items:flex-start;gap:8px;padding:10px 14px;border-radius:var(--dsw-radius-sm,8px);font-size:13px;line-height:20px;border:0.5px solid var(--dsw-alias-border-l2,rgba(255,255,255,.1));background:var(--dsw-alias-bg-layer-2,rgba(128,128,128,.08));color:var(--dsw-alias-label-primary,#eee)}
|
|
735
|
+
.dfc-feedback-error{background:color-mix(in srgb, var(--dsw-alias-state-error-primary,#f85149) 12%, transparent);border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary,#f85149) 30%, transparent);color:var(--dsw-alias-label-primary,#eee)}
|
|
736
|
+
.dfc-feedback-error .dfc-feedback-icon{color:var(--dsw-alias-state-error-primary,#f85149)}
|
|
737
|
+
.dfc-feedback-warning{background:color-mix(in srgb, var(--dsw-alias-state-warning-primary,#d29922) 12%, transparent);border-color:color-mix(in srgb, var(--dsw-alias-state-warning-primary,#d29922) 30%, transparent);color:var(--dsw-alias-label-primary,#eee)}
|
|
738
|
+
.dfc-feedback-warning .dfc-feedback-icon{color:var(--dsw-alias-state-warning-primary,#d29922)}
|
|
739
|
+
.dfc-feedback-success{background:color-mix(in srgb, var(--dsw-alias-state-success-primary,#3fb950) 12%, transparent);border-color:color-mix(in srgb, var(--dsw-alias-state-success-primary,#3fb950) 30%, transparent);color:var(--dsw-alias-label-primary,#eee)}
|
|
740
|
+
.dfc-feedback-success .dfc-feedback-icon{color:var(--dsw-alias-state-success-primary,#3fb950)}
|
|
741
|
+
.dfc-feedback-info{background:var(--dsw-alias-bg-layer-2,rgba(128,128,128,.08));color:var(--dsw-alias-label-primary,#eee)}
|
|
742
|
+
.dfc-feedback-icon{display:inline-flex;align-items:center;flex:none;margin-top:3px}
|
|
743
|
+
.dfc-feedback-body{flex:1;min-width:0;word-break:break-word}
|
|
744
|
+
.dfc-pending{display:flex;flex-direction:column;gap:8px}
|
|
745
|
+
.dfc-feedback-head{display:flex;align-items:center;gap:8px}
|
|
746
|
+
.dfc-feedback-title{font-weight:550;line-height:20px}
|
|
747
|
+
.dfc-pending-list{display:flex;flex-direction:column;gap:6px;width:100%}
|
|
748
|
+
.dfc-pending-row{display:flex;gap:8px;align-items:center;padding:4px 0}
|
|
749
|
+
.dfc-wrap{display:flex;flex-direction:column;min-height:0;flex:1;height:100%;overflow:hidden}.dfc-official-tree{flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden}
|
|
750
|
+
.dfc-tabs-wrap{display:flex;margin:2px var(--dsh-sidebar-inline-padding,12px) 8px 0;padding:0;width:auto}
|
|
751
|
+
.dfc-tabs-wrap .dfc-segmented-tabs{display:grid!important;width:100%!important;box-sizing:border-box!important;height:32px!important;min-height:32px!important;max-height:32px!important;padding:2px!important;border:0!important;border-radius:var(--dsw-radius-sm,8px)!important;box-shadow:inset 0 0 0 .5px var(--dsw-alias-border-l2,rgba(255,255,255,.08))!important;background:var(--dsw-alias-bg-layer-1,rgba(255,255,255,.02))!important}
|
|
752
|
+
.dfc-tabs-wrap .dfc-segmented-tabs>span{inset:2px auto 2px 2px!important;width:calc((100% - 4px)/var(--dfc-tab-count,2))!important;border-radius:6px!important;border:0.5px solid var(--dsw-alias-border-l3,rgba(255,255,255,.12))!important;background:var(--dsw-alias-bg-layer-3,rgba(255,255,255,.08))!important;box-shadow:none!important}
|
|
753
|
+
.dfc-tabs-wrap .dfc-segmented-tabs [role=tab]{display:inline-flex!important;align-items:center!important;justify-content:center!important;gap:6px!important;height:28px!important;padding:0 clamp(6px,calc(100% - 60px),12px)!important;font-size:13px!important;line-height:20px!important;border-radius:6px!important;color:var(--dsw-alias-label-secondary,#8b8f98)!important;text-align:center!important;white-space:nowrap!important;box-sizing:border-box!important;transition:color 150ms ease!important}
|
|
754
|
+
.dfc-tabs-wrap .dfc-segmented-tabs [role=tab]>*{display:inline-flex!important;align-items:center!important;justify-content:center!important}
|
|
755
|
+
.dfc-tabs-wrap .dfc-segmented-tabs [role=tab]:hover{color:var(--dsw-alias-label-primary,inherit)!important}
|
|
756
|
+
.dfc-tabs-wrap .dfc-segmented-tabs [role=tab][aria-selected=true]{color:var(--dsw-alias-label-primary,inherit)!important;font-weight:600!important}
|
|
757
|
+
.dfc-bind-tabs-wrap{width:100%;margin:0 0 16px}
|
|
758
|
+
.dfc-bind-tabs-wrap.is-disabled{opacity:.6;pointer-events:none}
|
|
759
|
+
.dfc-bind-panel[hidden]{display:none!important}
|
|
760
|
+
.dfc-bind-tabs-wrap .dfc-bind-tabs{width:100%;height:32px;box-sizing:border-box;border-bottom:0;background:color-mix(in srgb,var(--dsw-alias-label-primary,currentColor) 3%,transparent)!important;border-radius:var(--dsw-radius-sm,8px)!important;border:0.5px solid var(--dsw-alias-border-l1,rgba(255,255,255,.05))!important;padding:2px!important}
|
|
761
|
+
.dfc-bind-tabs-wrap .dfc-bind-tabs>span{inset:2px auto 2px 2px!important;width:calc((100% - 4px)/2)!important;border-radius:calc(var(--dsw-radius-sm,8px) - 2px)!important;border:0.5px solid color-mix(in srgb,var(--dsw-alias-label-primary,currentColor) 12%,transparent)!important;background:color-mix(in srgb,var(--dsw-alias-label-primary,currentColor) 8%,transparent)!important;box-shadow:none!important}
|
|
762
|
+
.dfc-bind-tabs-wrap .dfc-bind-tabs [role=tab]{display:inline-flex!important;align-items:center!important;justify-content:center!important;gap:6px!important;width:100%!important;height:26px!important;padding:0 10px!important;font-size:13px!important;line-height:18px!important;border-radius:calc(var(--dsw-radius-sm,8px) - 2px)!important;color:var(--dsw-alias-label-secondary,#8b8f98);text-align:center!important}
|
|
763
|
+
.dfc-bind-tabs-wrap .dfc-bind-tabs [role=tab]>*{display:inline-flex!important;align-items:center!important;justify-content:center!important}
|
|
764
|
+
.dfc-bind-tabs-wrap .dfc-bind-tabs [role=tab]:hover{color:var(--dsw-alias-label-primary,inherit)}
|
|
765
|
+
.dfc-bind-tabs-wrap .dfc-bind-tabs [role=tab][aria-selected=true]{color:var(--dsw-alias-label-primary,inherit)!important;font-weight:600}
|
|
766
|
+
.dfc-tabs{display:flex;gap:18px;padding:4px 12px 0;border-bottom:1px solid var(--dfc-line)}
|
|
767
|
+
.dfc-tab{appearance:none;border:0;background:transparent;color:var(--dfc-muted);padding:8px 0;font-size:13px;cursor:pointer}
|
|
768
|
+
.dfc-tab.on{color:var(--dfc-text);box-shadow:inset 0 -2px 0 currentColor}
|
|
769
|
+
.dfc-tabs{flex:none}
|
|
770
|
+
.dfc-empty{color:var(--dfc-muted);font-size:12px;padding:12px 8px}
|
|
771
|
+
.dfc-channel-official{display:flex;flex-direction:column;flex:1;min-width:0;min-height:0;overflow:hidden}
|
|
772
|
+
.dfc-channel-official>div{flex:1;min-width:0;min-height:0}
|
|
773
|
+
.dfc-channel-official>.dfc-channel-error{flex:none}
|
|
774
|
+
.dfc-channel-official>.dfc-channel-unavailable{flex:1}
|
|
775
|
+
.dfc-channel-error{padding:8px 12px;color:var(--dsw-alias-state-error-primary,#f85149);font-size:12px;line-height:18px}
|
|
776
|
+
.dfc-channel-unavailable{padding:16px 12px;color:var(--dsw-alias-label-tertiary,#8b8f98);font-size:13px;line-height:20px}
|
|
777
|
+
.dfc-session-channel-icon{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;flex:none}
|
|
778
|
+
/* 频道页签复用官方工作区浏览器:分组头操作按模式裁剪。 */
|
|
779
|
+
.dfc-channel-official[data-mode="channel"] [data-row-key^="workspace:dsh-im-channel:"]>span:last-child>:last-child{display:none!important}
|
|
780
|
+
.dfc-channel-official[data-mode="workspace"] [data-row-key^="workspace:"]>span:last-child,
|
|
781
|
+
.dfc-channel-official[data-mode="workspace-tree"] [data-row-key^="workspace:"]>span:last-child{display:none!important}
|
|
782
|
+
.dfc-logo{width:28px;height:28px;flex:none;display:block;line-height:0;background:transparent}
|
|
783
|
+
.dfc-logo svg{width:28px;height:28px;display:block}
|
|
784
|
+
.dfc-logo.sm{width:16px;height:16px;overflow:hidden;display:grid;place-items:center}
|
|
785
|
+
.dfc-logo.sm svg{width:16px;height:16px;transform:none}
|
|
786
|
+
.dfc-qrbox{display:flex;flex-direction:column;align-items:center;gap:10px;padding:8px 0 4px}
|
|
787
|
+
.dfc-qrbox img{width:200px;height:200px;background:#fff;border:1px solid var(--dfc-line);border-radius:12px;object-fit:contain}
|
|
788
|
+
.dfc-bind-ready{display:flex;flex-direction:column;align-items:center;gap:14px;padding:12px 0 4px}.dfc-bind-ready .dfc-btn{min-width:136px}.dfc-bind-status{min-height:40px;display:grid;place-items:center;color:var(--dfc-muted);font-size:13px;text-align:center}
|
|
789
|
+
.dfc-hint{margin:0;color:var(--dfc-muted);font-size:13px;text-align:center;line-height:1.6}
|
|
790
|
+
.dfc-field{display:flex;flex-direction:column;gap:6px;margin-bottom:12px;font-size:13px}
|
|
791
|
+
.dfc-field .dfc-input,.dfc-field input{width:100%}
|
|
792
|
+
.dfc-input,.dfc-account-remark-input,.dfc-field input{box-sizing:border-box!important;height:36px!important;padding:0 12px!important;border:0.5px solid var(--dsw-alias-border-l2,#494949)!important;border-radius:var(--dsw-radius-sm,8px)!important;background:var(--dsw-alias-bg-layer-1,transparent)!important;color:var(--dsw-alias-label-primary,#eee)!important;font:inherit!important;font-size:13px!important;line-height:20px!important;transition:border-color 120ms ease,background 120ms ease!important}
|
|
793
|
+
.dfc-textarea{height:auto!important;min-height:80px;padding:8px 12px!important;resize:vertical}
|
|
794
|
+
.dfc-input:hover:not(:disabled),.dfc-account-remark-input:hover:not(:disabled),.dfc-field input:hover:not(:disabled){border-color:var(--dsw-alias-border-l3,#666)!important}
|
|
795
|
+
.dfc-input:focus,.dfc-account-remark-input:focus,.dfc-field input:focus{border-color:var(--dsw-alias-brand-primary,#4b7cff)!important;outline:var(--dsw-focus-ring-width,1px) solid var(--dsw-focus-ring-color,var(--dsw-alias-brand-primary,#4b7cff))!important;outline-offset:0!important}
|
|
796
|
+
.dfc-input::placeholder,.dfc-account-remark-input::placeholder,.dfc-field input::placeholder{color:var(--dsw-alias-label-tertiary,#888)!important}
|
|
797
|
+
.dfc-input:disabled,.dfc-account-remark-input:disabled{opacity:.5!important;cursor:not-allowed!important}
|
|
798
|
+
.dfc-checkbox-label{display:inline-flex;align-items:center;gap:8px;font-size:13px;color:var(--dsw-alias-label-primary,#eee);cursor:pointer;user-select:none}
|
|
799
|
+
.dfc-checkbox-label.is-disabled{opacity:.5;cursor:not-allowed}
|
|
800
|
+
.dfc-checkbox{width:16px;height:16px;margin:0;accent-color:var(--dsw-alias-state-business-primary,#4176e6);cursor:inherit}
|
|
801
|
+
.dfc-tag{display:inline-flex!important;align-items:center!important;gap:4px!important;height:22px!important;padding:0 8px!important;font-size:12px!important;line-height:20px!important;border-radius:var(--dsw-radius-sm,6px)!important;font-weight:500!important;box-sizing:border-box!important;vertical-align:middle!important;background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.12));color:var(--dsw-alias-label-secondary,#b9b9b9);border:none}
|
|
802
|
+
.dfc-tag .dfc-btn-spinner{width:11px!important;height:11px!important;margin-right:2px!important;vertical-align:0!important}
|
|
803
|
+
.dfc-tag-info,.dfc-tag-processing,.dfc-tag.is-busy{background:color-mix(in srgb,var(--dsw-alias-state-business-primary,#4176e6) 14%,transparent)!important;color:var(--dsw-alias-state-business-primary,#4176e6)!important}
|
|
804
|
+
.dfc-tag-success{background:color-mix(in srgb,var(--dsw-alias-state-success-primary,#3fb950) 14%,transparent)!important;color:var(--dsw-alias-state-success-primary,#3fb950)!important}
|
|
805
|
+
.dfc-tag-warning{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#d29922) 14%,transparent)!important;color:var(--dsw-alias-state-warn-primary,#d29922)!important}
|
|
806
|
+
.dfc-tag-error{background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#f85149) 14%,transparent)!important;color:var(--dsw-alias-state-error-primary,#f85149)!important}
|
|
807
|
+
.dfc-modal-root{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;z-index:1100}
|
|
808
|
+
.dfc-modal-mask{position:absolute;inset:0;background:rgba(0,0,0,.45);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px)}
|
|
809
|
+
.dfc-modal-wrap{position:relative;max-width:calc(100vw - 32px);max-height:calc(100vh - 48px);overflow:auto;display:flex}
|
|
810
|
+
.dfc-modal-panel{position:relative;isolation:isolate;box-sizing:border-box;padding:24px 28px 28px;border-radius:var(--dsw-radius-panel,28px);background:var(--dsw-specific-menu,var(--dsw-alias-bg-layer-1));backdrop-filter:var(--dsw-menu-backdrop-filter,blur(20px));-webkit-backdrop-filter:var(--dsw-menu-backdrop-filter,blur(20px));border:0.5px solid var(--dsw-alias-border-l1,rgba(255,255,255,.1));box-shadow:var(--dsw-elevation-panel,0 20px 48px -12px rgba(0,0,0,.35));color:var(--dsw-alias-label-primary,#eee);display:flex;flex-direction:column;gap:16px}
|
|
811
|
+
.dfc-modal-header{display:flex;align-items:center;justify-content:space-between;min-height:24px}
|
|
812
|
+
.dfc-modal-title{font-size:17px;font-weight:600;line-height:24px;color:var(--dsw-alias-label-primary,#eee)}
|
|
813
|
+
.dfc-modal-close{width:28px;height:28px;display:flex;align-items:center;justify-content:center;border:none;border-radius:var(--dsw-radius-sm,8px);background:transparent;color:var(--dsw-alias-label-tertiary,#888);cursor:pointer;transition:all 120ms ease}
|
|
814
|
+
.dfc-modal-close:hover{color:var(--dsw-alias-label-primary,#fff);background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.12))}
|
|
815
|
+
.dfc-modal-body{min-height:0;overflow:visible;display:flex;flex-direction:column;gap:12px}
|
|
816
|
+
.dfc-modal-actions{display:flex;justify-content:flex-end;flex-wrap:wrap;gap:8px}
|
|
817
|
+
.dfc-modal-footer-wrap{display:flex;justify-content:flex-end;margin-top:12px;padding-top:16px}
|
|
818
|
+
.dfc-inspector-actions>button{flex:none;white-space:nowrap}
|
|
819
|
+
.dfc-confirm-content{margin:8px 0 0;font-size:13px;line-height:22px;color:var(--dsw-alias-label-secondary,#b9b9b9)}
|
|
820
|
+
.dfc-page.dfc-account-page{container-type:inline-size;min-width:0;width:100%;padding:0 0 32px}
|
|
821
|
+
.dfc-account-shell,.dfc-account-shell *{box-sizing:border-box}
|
|
822
|
+
.dfc-account-shell{display:block;min-height:0;border:0;border-radius:0;overflow:visible;background:transparent}
|
|
823
|
+
.dfc-platforms{display:flex;flex-direction:column;gap:12px;padding:0;border:0;background:transparent;min-width:0}
|
|
824
|
+
.dfc-platform{border:1px solid color-mix(in srgb,var(--dfc-text) 16%,transparent);border-radius:12px;margin:0;overflow:hidden;background:var(--dsw-alias-bg-layer-1,color-mix(in srgb,var(--dfc-text) 4%,transparent))}.dfc-platform.open{border-color:color-mix(in srgb,var(--dfc-text) 24%,transparent)}
|
|
825
|
+
.dfc-platform-head{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:10px;width:100%;min-height:56px;padding:10px 16px;border:0;border-radius:0;background:transparent;color:inherit;text-align:left;cursor:pointer}
|
|
826
|
+
.dfc-platform:not(.open):not(.empty) .dfc-platform-head:hover{background:var(--dfc-card-hover)}.dfc-platform.empty .dfc-platform-head{cursor:default}
|
|
827
|
+
.dfc-platform-head>.dfc-logo,.dfc-platform-head>.dfc-logo svg{width:24px;height:24px}
|
|
828
|
+
.dfc-platform-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px;font-weight:600}
|
|
829
|
+
.dfc-platform-meta{display:inline-flex;align-items:center;gap:10px}
|
|
830
|
+
.dfc-platform-count{color:var(--dfc-muted);font-size:12px;white-space:nowrap}
|
|
831
|
+
.dfc-platform-caret{display:grid;width:18px;height:18px;place-items:center;color:var(--dfc-muted);transition:transform .15s ease}.dfc-platform-caret.empty{visibility:hidden}.dfc-platform.open .dfc-platform-caret{transform:rotate(90deg)}
|
|
832
|
+
.dfc-account-list{display:flex;flex-direction:column;gap:8px;padding:0 14px 14px}
|
|
833
|
+
.dfc-account-row{display:grid;grid-template-columns:minmax(0,1fr) auto auto auto;align-items:center;gap:12px;width:100%;min-height:48px;padding:10px 12px;border:0;border-radius:8px;background:color-mix(in srgb,var(--dfc-text) 7%,transparent);color:inherit;text-align:left}
|
|
834
|
+
.dfc-account-row:hover{background:var(--dfc-card-hover)}
|
|
835
|
+
.dfc-platform-head:focus-visible,.dfc-account-row:focus-visible{outline:2px solid var(--dfc-accent);outline-offset:2px}
|
|
836
|
+
.dfc-account-copy{min-width:0}.dfc-account-name{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:500}.dfc-account-rename-button{width:100%;padding:0;border:0;background:transparent;color:inherit;text-align:left;cursor:text}.dfc-account-rename-button:hover,.dfc-account-rename-button:focus-visible{color:var(--dfc-accent)}.dfc-account-rename,.dfc-account-remark-input{box-sizing:border-box;width:100%;height:32px;padding:4px 8px;border:1px solid var(--dfc-line);border-radius:6px;background:transparent;color:inherit;font:inherit}.dfc-account-id{display:block;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dfc-muted);font-size:11px}
|
|
837
|
+
.dfc-account-row .dfc-tag{margin-inline-end:0}
|
|
838
|
+
.dfc-account-receive{display:inline-flex;align-items:center;justify-content:flex-end;gap:8px;white-space:nowrap;font-size:12px;color:var(--dfc-muted)}
|
|
839
|
+
.dfc-inspector{padding:22px 24px 26px;min-width:0}.dfc-inspector-head{display:flex;align-items:flex-start;gap:12px;padding-bottom:18px;border-bottom:1px solid var(--dfc-line)}.dfc-inspector-head-copy{min-width:0;flex:1}.dfc-inspector-title{margin:1px 0 3px;font-size:17px;line-height:24px}.dfc-inspector-title-row{display:flex;align-items:center;gap:12px;min-width:0}.dfc-inspector-title-row .dfc-inspector-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin:0}.dfc-inspector-title-row .dfc-inspector-status{flex:none;white-space:nowrap;font-size:11px}.dfc-inspector-head-copy>.dfc-account-id{margin-top:5px}
|
|
840
|
+
.dfc-inspector-status{flex:none}
|
|
841
|
+
.dfc-form{display:flex;flex-direction:column;gap:16px;padding-top:20px}
|
|
842
|
+
.dfc-command-permissions{min-width:0;margin:0;padding:0;border:0}
|
|
843
|
+
.dfc-diagnostic-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.dfc-inspector-title-row .dfc-tag{margin-inline-end:0}.dfc-bind-modal .dfc-feedback{margin-bottom:12px}.dfc-connection-check{display:grid;gap:6px;padding:12px;border:1px solid var(--dfc-line);border-radius:8px;font-size:12px;line-height:1.6;overflow-wrap:anywhere;color:var(--dfc-muted)}.dfc-connection-check strong{color:var(--dfc-text)}.dfc-diagnostic-item{padding:8px 0;border-top:1px solid var(--dfc-line)}
|
|
844
|
+
.dfc-inspector-actions{display:flex!important;flex-wrap:wrap!important;align-items:center!important;justify-content:flex-end!important;gap:8px!important;margin-top:16px!important;padding-top:16px!important;padding-bottom:4px!important}
|
|
845
|
+
.dfc-account-modal .dfc-inspector{padding:0 0 16px 0}
|
|
846
|
+
.dfc-bind-modal,.dfc-account-modal{width:min(720px,100%) !important;max-height:100%;min-height:0}
|
|
847
|
+
/* 宿主 Modal 只把 dialog 限制在视口内,内容区默认不滚动,长内容会被裁掉;
|
|
848
|
+
把宿主的 content 区设为滚动容器,footer 保持在容器外可见。 */
|
|
849
|
+
.dfc-bind-modal>.dfc-modal-content,.dfc-account-modal>.dfc-modal-content{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;scrollbar-gutter:stable}
|
|
850
|
+
.dfc-account-settings{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px 16px;width:100%;box-sizing:border-box}
|
|
851
|
+
.dfc-account-settings.compact{grid-template-columns:1fr}
|
|
852
|
+
.dfc-picker-field{display:flex;min-width:0;width:100%;flex-direction:column;gap:6px;box-sizing:border-box}
|
|
853
|
+
.dfc-picker-field.wide,.dfc-picker-note,.dfc-model-effort{grid-column:1/-1}
|
|
854
|
+
.dfc-model-effort{display:grid;grid-template-columns:1fr 1fr;gap:14px 16px;min-width:0;width:100%;box-sizing:border-box}
|
|
855
|
+
.dfc-account-settings.compact .dfc-model-effort{grid-template-columns:1fr}
|
|
856
|
+
.dfc-picker-label{color:var(--dsw-alias-label-secondary,var(--dfc-muted));font-size:12px;font-weight:550;line-height:16px}
|
|
857
|
+
.dfc-picker-note{color:var(--dsw-alias-label-tertiary,var(--dfc-muted));font-size:11px;line-height:1.6;margin-top:2px}
|
|
858
|
+
.dfc-picker-note.warning{color:var(--dfc-warning)}
|
|
859
|
+
.dfc-save-note{color:var(--dfc-muted);font-size:11px}.dfc-save-note.ok{color:var(--dfc-ok)}
|
|
860
|
+
.dfc-bind-modal .dfc-setup-section{padding:16px!important;margin:0 0 16px!important;border-radius:var(--dsw-radius-md,12px)!important;background:var(--dsw-alias-bg-layer-2,rgba(128,128,128,.04))!important;border:0.5px solid var(--dsw-alias-border-l1,rgba(255,255,255,.06))!important}
|
|
861
|
+
.dfc-bind-ready{display:flex!important;flex-direction:column!important;align-items:center!important;gap:14px!important;padding:20px 16px!important;border-radius:var(--dsw-radius-md,12px)!important;background:var(--dsw-alias-bg-layer-2,rgba(128,128,128,.04))!important;border:0.5px solid var(--dsw-alias-border-l1,rgba(255,255,255,.06))!important}
|
|
862
|
+
.dfc-bind-ready .dfc-btn{min-width:140px!important;height:36px!important}
|
|
863
|
+
.dfc-account-picker{width:100%}
|
|
864
|
+
.dfc-enable-switch{box-sizing:border-box;position:relative;flex:0 0 auto;width:36px;height:20px;padding:2px;border:0;border-radius:999px;corner-shape:round;background:var(--dsw-alias-border-l3,#555);cursor:pointer}
|
|
865
|
+
.dfc-enable-switch[aria-checked=true]{background:var(--dsw-alias-brand-primary)}
|
|
866
|
+
.dfc-enable-switch:disabled{cursor:default;opacity:.5}
|
|
867
|
+
.dfc-enable-switch:focus-visible{outline:var(--dsw-focus-ring-width,1px) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px}
|
|
868
|
+
.dfc-enable-switch-thumb{display:block;width:16px;height:16px;border-radius:50%;corner-shape:round;background:var(--dsw-alias-label-primary-foreground,#fff);transition:transform 120ms ease;pointer-events:none}
|
|
869
|
+
.dfc-enable-switch[aria-checked=false] .dfc-enable-switch-thumb{background:var(--dsw-alias-switch-thumb,#fff)}
|
|
870
|
+
.dfc-enable-switch[aria-checked=true] .dfc-enable-switch-thumb{transform:translateX(16px)}
|
|
871
|
+
.dfc-form-menu{box-sizing:border-box;width:max(var(--dfc-menu-width,144px),144px)!important;min-width:max(var(--dfc-menu-width,144px),144px)!important;max-width:100%!important}
|
|
872
|
+
.dfc-menu-select-wrap,.dfc-menu-select-wrap > *,.dfc-menu-select-wrap span[class*="_root_"],.dfc-menu-select-wrap .dfc-menu-select-trigger,.dfc-account-picker{display:flex!important;width:100%!important;min-width:0!important;box-sizing:border-box!important}
|
|
873
|
+
.dfc-menu-select-trigger{display:flex!important;align-items:center!important;justify-content:space-between!important;gap:6px!important;width:100%!important;height:36px!important;padding:0 12px!important;border:0.5px solid var(--dsw-alias-border-l2,#494949)!important;border-radius:var(--dsw-radius-sm,8px)!important;background:var(--dsw-alias-bg-layer-1,transparent)!important;color:var(--dsw-alias-label-primary,#eee)!important;font:inherit!important;font-size:13px!important;cursor:pointer!important;user-select:none!important;box-sizing:border-box!important;transition:border-color 120ms ease,background 120ms ease!important}
|
|
874
|
+
.dfc-menu-select-trigger:hover:not(:disabled){border-color:var(--dsw-alias-border-l3,#666)!important;background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.06))!important}
|
|
875
|
+
.dfc-menu-select-trigger.is-open{border-color:var(--dsw-alias-brand-primary,#4b7cff)!important;outline:var(--dsw-focus-ring-width,1px) solid var(--dsw-focus-ring-color,var(--dsw-alias-brand-primary,#4b7cff))!important}
|
|
876
|
+
.dfc-menu-select-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;text-align:left}
|
|
877
|
+
.dfc-menu-select-arrow{flex:none;opacity:0.6}
|
|
878
|
+
.dfc-btn-spinner{display:inline-block;vertical-align:-2px;animation:dfc-spin 0.8s linear infinite}
|
|
879
|
+
@keyframes dfc-spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}
|
|
880
|
+
.dfc-btn-danger{color:var(--dsw-alias-state-error-primary,var(--dfc-danger))!important;border-color:var(--dsw-alias-state-error-primary,var(--dfc-danger))!important}
|
|
881
|
+
.dfc-btn-danger:hover:not(:disabled){color:var(--dsw-alias-state-error-primary,var(--dfc-danger))!important;border-color:var(--dsw-alias-state-error-primary,var(--dfc-danger))!important;background:rgba(239,68,68,.12)!important}
|
|
882
|
+
.dfc-btn-danger-primary,.dfc-btn-danger.dfc-btn-danger-primary{background:var(--dsw-alias-state-error-primary,var(--dfc-danger))!important;border-color:var(--dsw-alias-state-error-primary,var(--dfc-danger))!important;color:#fff!important}
|
|
883
|
+
@container (max-width:480px){.dfc-account-row{grid-template-columns:minmax(0,1fr) auto auto;gap:8px;padding:10px}.dfc-account-receive>span{display:none}.dfc-platform-head{padding:10px 12px}.dfc-account-list{padding:0 12px 12px}}
|
|
884
|
+
|
|
885
|
+
.dfc-channel-page{display:flex;flex-direction:column;gap:10px;min-height:0;height:100%;box-sizing:border-box;padding:10px 10px 16px;overflow:auto}
|
|
886
|
+
.dfc-channel-head{display:flex;align-items:center;justify-content:flex-end;gap:8px;min-height:26px;padding:0 2px 4px;flex:none}
|
|
887
|
+
.dfc-channel-add{height:26px!important;min-height:26px!important;padding:0 6px!important;border:0!important;background:transparent!important;color:var(--dsw-alias-label-secondary,#b9b9b9)!important;font-size:11px!important;font-weight:500!important;box-shadow:none!important}
|
|
888
|
+
.dfc-channel-add:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.06))!important;color:var(--dsw-alias-label-primary,#eee)!important}
|
|
889
|
+
.dfc-channel-empty{display:flex;align-items:center;justify-content:center;min-height:120px;padding:18px;border:0.5px dashed var(--dsw-alias-border-l2,#494949);border-radius:var(--dsw-radius-md,12px);color:var(--dsw-alias-label-tertiary,#888);font-size:12px;line-height:1.6;text-align:center}
|
|
890
|
+
.dfc-channel-pending{padding:10px;border:0.5px solid color-mix(in srgb,var(--dsw-alias-state-warn-primary,#d29922) 35%,transparent);border-radius:var(--dsw-radius-md,12px);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#d29922) 8%,transparent)}
|
|
891
|
+
.dfc-channel-pending-title{font-size:12px;font-weight:600;color:var(--dsw-alias-state-warn-primary,#d29922);margin-bottom:6px}
|
|
892
|
+
.dfc-channel-pending-row{display:flex;align-items:center;gap:6px;padding:4px 0;font-size:11px}
|
|
893
|
+
.dfc-channel-pending-user{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-secondary,#b9b9b9)}
|
|
894
|
+
.dfc-channel-accounts{display:flex;flex-direction:column;gap:8px;min-height:0}
|
|
895
|
+
.dfc-channel-account{border:0.5px solid var(--dsw-alias-border-l1,rgba(255,255,255,.08));border-radius:var(--dsw-radius-md,12px);background:var(--dsw-alias-bg-layer-1,transparent);padding:7px 8px;display:flex;flex-direction:column;gap:5px;min-width:0}
|
|
896
|
+
.dfc-channel-account-head{display:flex;align-items:center;gap:6px;min-width:0;flex-wrap:nowrap}
|
|
897
|
+
.dfc-channel-account-copy{flex:1;min-width:90px;display:flex;flex-direction:column;gap:1px}
|
|
898
|
+
.dfc-channel-account-name{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary,#eee);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
899
|
+
.dfc-channel-account-actions{display:flex;align-items:center;gap:4px;flex:none;margin-left:auto}
|
|
900
|
+
.dfc-channel-account-head .dfc-tag{height:18px!important;min-height:18px!important;font-size:10px!important;line-height:16px!important;padding:0 6px!important}
|
|
901
|
+
.dfc-channel-settings{width:24px!important;height:24px!important;min-height:24px!important;padding:0!important;border:0!important;background:transparent!important;color:var(--dsw-alias-label-tertiary,#888)!important;box-shadow:none!important}
|
|
902
|
+
.dfc-channel-settings:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.06))!important;color:var(--dsw-alias-label-primary,#eee)!important}
|
|
903
|
+
.dfc-channel-sessions{display:flex;flex-direction:column;gap:1px;border-top:0.5px solid var(--dsw-alias-border-l1,rgba(255,255,255,.07));padding-top:5px}
|
|
904
|
+
.dfc-channel-session{display:flex;align-items:center;gap:8px;width:100%;min-width:0;border:0;background:transparent;color:inherit;text-align:left;padding:6px 7px;border-radius:var(--dsw-radius-sm,8px);cursor:pointer;font:inherit;transition:background 120ms ease}
|
|
905
|
+
.dfc-channel-session:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.06))}
|
|
906
|
+
.dfc-channel-session.is-current{background:color-mix(in srgb,var(--dsw-alias-state-business-primary,#4b7cff) 14%,transparent);box-shadow:inset 2px 0 0 var(--dsw-alias-state-business-primary,#4b7cff)}
|
|
907
|
+
.dfc-channel-session.is-current .dfc-channel-session-title{color:var(--dsw-alias-label-primary,#eee);font-weight:600}
|
|
908
|
+
.dfc-channel-session-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;color:var(--dsw-alias-label-primary,#eee)}
|
|
909
|
+
.dfc-channel-session-time{flex:none;font-size:10px;color:var(--dsw-alias-label-tertiary,#888);white-space:nowrap}
|
|
910
|
+
.dfc-channel-account-empty{font-size:11px;color:var(--dsw-alias-label-tertiary,#888);line-height:1.5;padding:2px 1px}
|
|
911
|
+
.dfc-channel-name-field{margin-bottom:14px}
|
|
912
|
+
.dfc-channel-name-field .dfc-picker-label{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary,#eee)}
|
|
913
|
+
.dfc-app-info{display:flex;flex-direction:column;gap:7px;padding:0;border:0;border-radius:0;background:transparent;margin-bottom:10px}
|
|
914
|
+
.dfc-app-info-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary,#eee)}
|
|
915
|
+
.dfc-app-fields{display:grid;grid-template-columns:minmax(0,0.66fr) minmax(0,1.34fr);gap:14px;min-width:0}
|
|
916
|
+
.dfc-app-fields .dfc-picker-field{margin:0;min-width:0}
|
|
917
|
+
.dfc-app-fields .dfc-copy-row{min-width:0}
|
|
918
|
+
.dfc-app-fields .dfc-copy-row .dfc-btn{height:26px!important;padding:0 6px!important;font-size:10px!important;white-space:nowrap}
|
|
919
|
+
.dfc-app-info .dfc-picker-field{margin:0}.dfc-app-info .dfc-picker-label{font-size:10px;color:var(--dsw-alias-label-tertiary,#888)}
|
|
920
|
+
.dfc-copy-row{display:flex;align-items:center;gap:6px;min-width:0}
|
|
921
|
+
.dfc-copy-row .dfc-input{flex:1;min-width:0}
|
|
922
|
+
.dfc-copy-row .dfc-btn{flex:none;white-space:nowrap}
|
|
923
|
+
.dfc-app-warning{display:flex;align-items:center;gap:8px;justify-content:space-between;font-size:11px;line-height:1.5;color:var(--dsw-alias-state-warn-primary,#d29922)}
|
|
924
|
+
.dfc-app-warning .dfc-btn{flex:none;height:24px;padding:0 7px;font-size:10px}.dfc-app-warning a{color:var(--dsw-alias-brand-primary,#4b7cff);text-decoration:none;font-weight:600}.dfc-app-warning a:hover{text-decoration:underline}
|
|
925
|
+
|
|
926
|
+
.dfc-tab-inline{display:inline-flex;align-items:center;gap:5px;min-width:0}
|
|
927
|
+
.dfc-tab-brand{display:inline-flex;align-items:center;justify-content:center;width:14px;height:14px;flex:none;color:currentColor}
|
|
928
|
+
.dfc-tab-brand svg{width:14px;height:14px;display:block}
|
|
929
|
+
.dfc-check-section{display:flex;flex-direction:column;gap:8px;padding:10px 0 2px;border-top:0.5px solid var(--dsw-alias-border-l1,rgba(255,255,255,.08))}
|
|
930
|
+
.dfc-check-head{display:flex;align-items:center;gap:8px}
|
|
931
|
+
.dfc-check-title{flex:1;min-width:0;font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary,#b9b9b9)}
|
|
932
|
+
.dfc-check-toggle{height:24px!important;padding:0 7px!important;font-size:11px!important;color:var(--dsw-alias-label-tertiary,#888)!important}
|
|
933
|
+
.dfc-remote-composer{box-sizing:border-box;width:100%;max-width:var(--dsh-composer-card-max-width,952px);margin:0 auto;padding:0 var(--dsh-composer-side-clearance,16px) 18px;display:flex;justify-content:center}
|
|
934
|
+
.dfc-remote-composer-card{box-sizing:border-box;width:100%;display:flex;align-items:center;gap:10px;padding:12px 14px;border-radius:var(--dsw-radius-panel,20px);border:0.5px solid var(--dsw-alias-border-l2,#494949);background:var(--dsw-specific-input-major,var(--dsw-alias-bg-layer-1,#1b1b1b));box-shadow:var(--dsw-elevation-soft,0 4px 16px rgba(0,0,0,.12))}
|
|
935
|
+
.dfc-remote-composer-icon{width:28px;height:28px;flex:none;display:grid;place-items:center;border-radius:calc(var(--dsw-radius-sm,8px));background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.06));color:var(--dsw-alias-label-secondary,#b9b9b9)}
|
|
936
|
+
.dfc-remote-composer-icon svg{width:18px;height:18px;display:block}
|
|
937
|
+
.dfc-remote-composer-copy{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
|
|
938
|
+
.dfc-remote-composer-title{font-size:13px;font-weight:600;line-height:18px;color:var(--dsw-alias-label-primary,#eee)}
|
|
939
|
+
.dfc-remote-composer-hint{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,#888)}
|
|
940
|
+
.dfc-remote-model{display:flex;align-items:center;gap:6px;flex:none;min-width:0}
|
|
941
|
+
.dfc-remote-model-label{font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary,#888);white-space:nowrap}
|
|
942
|
+
.dfc-remote-model-value{box-sizing:border-box;max-width:260px;padding:5px 9px;border:0.5px solid var(--dsw-alias-border-l2,#494949);border-radius:var(--dsw-radius-sm,8px);background:var(--dsw-alias-bg-layer-1,transparent);color:var(--dsw-alias-label-primary,#eee);font-size:13px;line-height:18px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
943
|
+
.dfc-remote-model-missing{font-size:11px}
|
|
944
|
+
.dfc-remote-stop{flex:none;width:30px!important;height:30px!important;min-width:30px!important;padding:0!important;border-radius:50%!important;background:#111!important;border:0.5px solid #333!important;color:#fff!important;margin-left:8px;box-shadow:none!important}
|
|
945
|
+
.dfc-remote-stop:hover:not(:disabled){background:#000!important;border-color:#444!important;color:#fff!important}
|
|
946
|
+
.dfc-remote-stop svg{fill:currentColor!important}
|
|
947
|
+
.dfc-remote-model-missing{flex:none;font-size:10px;line-height:14px;color:var(--dsw-alias-label-tertiary,#888);white-space:nowrap}
|
|
948
|
+
@media (max-width:520px){.dfc-app-fields{grid-template-columns:minmax(0,1fr)}.dfc-remote-composer-card{flex-wrap:wrap}.dfc-remote-model{margin-left:auto}.dfc-remote-model-value{max-width:180px}}
|
|
949
|
+
|
|
950
|
+
`;
|
|
951
|
+
|
|
952
|
+
const TITLE_LINK_CSS = "@media(max-width:720px){.dfc-title-row{flex-wrap:wrap}}";
|
|
953
|
+
|
|
954
|
+
let styleEl = null;
|
|
955
|
+
const ensureStyle = () => {
|
|
956
|
+
if (styleEl && styleEl.isConnected) {
|
|
957
|
+
styleEl.setAttribute("data-plugin", "@cubeai/dsh-feishu-channels");
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
styleEl = document.createElement("style");
|
|
961
|
+
styleEl.setAttribute("data-plugin", "@cubeai/dsh-feishu-channels");
|
|
962
|
+
styleEl.textContent = CSS + TITLE_LINK_CSS;
|
|
963
|
+
document.head.appendChild(styleEl);
|
|
964
|
+
};
|
|
965
|
+
|
|
966
|
+
let requestHostSessionRefresh = () => {};
|
|
967
|
+
let channelSessionIds = new Set();
|
|
968
|
+
let channelSessionChannels = new Map();
|
|
969
|
+
let channelMembershipRevision = 0;
|
|
970
|
+
const channelMembershipListeners = new Set();
|
|
971
|
+
function updateChannelMembership(groups) {
|
|
972
|
+
const next = new Set();
|
|
973
|
+
const channels = new Map();
|
|
974
|
+
for (const group of groups) {
|
|
975
|
+
for (const session of group.sessions || []) {
|
|
976
|
+
next.add(session.sessionId);
|
|
977
|
+
channels.set(String(session.sessionId), group.id);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
let changed = next.size !== channelSessionIds.size || [...next].some(id => !channelSessionIds.has(id));
|
|
981
|
+
if (!changed) {
|
|
982
|
+
for (const [id, channel] of channels) {
|
|
983
|
+
if (channelSessionChannels.get(id) !== channel) { changed = true; break; }
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
if (changed) {
|
|
987
|
+
channelSessionIds = next;
|
|
988
|
+
channelSessionChannels = channels;
|
|
989
|
+
channelMembershipRevision += 1;
|
|
990
|
+
for (const listener of channelMembershipListeners) listener();
|
|
991
|
+
}
|
|
992
|
+
try { requestHostSessionRefresh(changed); } catch { /* 下次频道轮询继续重试 */ }
|
|
993
|
+
}
|
|
994
|
+
/** IM 会话 ID 包含账号实例;接口映射兼容沿用普通 ID 的已接入会话。 */
|
|
995
|
+
function channelIdOfSession(sessionId) {
|
|
996
|
+
const value = String(sessionId || "");
|
|
997
|
+
const mapped = channelSessionChannels.get(value);
|
|
998
|
+
if (mapped) return mapped;
|
|
999
|
+
if (!value.startsWith("dfc:")) return undefined;
|
|
1000
|
+
const rest = value.slice(3);
|
|
1001
|
+
const split = rest.indexOf(":");
|
|
1002
|
+
const instance = split > 0 ? rest.slice(0, split) : rest;
|
|
1003
|
+
const platform = instance.split("_", 1)[0];
|
|
1004
|
+
return platform || undefined;
|
|
1005
|
+
}
|
|
1006
|
+
/** 从 dfc:<account>:<kind>:... 中取出飞书账号 id。 */
|
|
1007
|
+
function accountIdOfSession(sessionId) {
|
|
1008
|
+
const value = String(sessionId || "");
|
|
1009
|
+
if (!value.startsWith("dfc:")) return "";
|
|
1010
|
+
const rest = value.slice(4);
|
|
1011
|
+
const split = rest.indexOf(":");
|
|
1012
|
+
return split > 0 ? rest.slice(0, split) : "";
|
|
1013
|
+
}
|
|
1014
|
+
const isChannelSession = (id) => String(id).startsWith("dfc:") || channelSessionIds.has(String(id));
|
|
1015
|
+
const subscribeChannelMembership = (listener) => {
|
|
1016
|
+
channelMembershipListeners.add(listener);
|
|
1017
|
+
return () => channelMembershipListeners.delete(listener);
|
|
1018
|
+
};
|
|
1019
|
+
const channelMembershipSnapshot = () => channelMembershipRevision;
|
|
1020
|
+
|
|
1021
|
+
/** 飞书会话自动认领“飞书频道”页签;其他插件的页签不抢。 */
|
|
1022
|
+
function sidebarTabForSession(currentId, extraTabs) {
|
|
1023
|
+
if (typeof currentId !== "string" || currentId === "") return null;
|
|
1024
|
+
if (isChannelSession(currentId)) return "feishu";
|
|
1025
|
+
const matched = (extraTabs || []).find((item) => item && item.matchSession && item.matchSession(currentId));
|
|
1026
|
+
if (matched && matched.id !== "schedule" && matched.id !== "channels") return matched.id;
|
|
1027
|
+
if (matched) return null;
|
|
1028
|
+
return "tasks";
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
function followSidebarTab(state, currentId, extraTabs) {
|
|
1032
|
+
const ready = !!(state && state.ready);
|
|
1033
|
+
const next = sidebarTabForSession(currentId, extraTabs);
|
|
1034
|
+
// 首个有效主会话只建立基线,不覆盖用户恢复的页签;空快照继续等待。
|
|
1035
|
+
if (!ready) return { ready: typeof currentId === "string" && currentId !== "", previousId: currentId, tab: null };
|
|
1036
|
+
if (typeof currentId !== "string" || currentId === "") return { ready: true, previousId: currentId, tab: null };
|
|
1037
|
+
if (currentId === state.previousId) return { ready: true, previousId: currentId, tab: null };
|
|
1038
|
+
return { ready: true, previousId: currentId, tab: next };
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function resolveSidebarTab(tab, extraTabs) {
|
|
1042
|
+
return tab === "tasks" || tab === "feishu" || extraTabs.some((item) => item.id === tab) ? tab : "tasks";
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function isHostNewSessionControl(label) {
|
|
1046
|
+
return /^(新会话|新建任务|New chat|New session)(?:\s+(?:Ctrl|Alt|Shift|Meta)(?:\+[^\s+]+)*)?$/i.test(String(label || "").trim());
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
const api = (path, opts) => {
|
|
1050
|
+
// 管理路由复用宿主登录 Cookie,不缓存账号状态或认证错误。
|
|
1051
|
+
const request = Object.assign({}, opts || {}, { credentials: "same-origin", cache: "no-store" });
|
|
1052
|
+
const method = String(request.method || "GET").toUpperCase();
|
|
1053
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
1054
|
+
request.headers = Object.assign({ "content-type": "application/json" }, request.headers || {}, { "x-dsh-feishu-channels-client": "1" });
|
|
1055
|
+
if (request.body === undefined) request.body = "{}";
|
|
1056
|
+
}
|
|
1057
|
+
return fetch(API_BASE + path, request).then((r) => r.json()).then((data) => {
|
|
1058
|
+
if (!request.signal?.aborted && data.ok && Array.isArray(data.groups)) updateChannelMembership(data.groups);
|
|
1059
|
+
return data;
|
|
1060
|
+
});
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
// One request and timer for all channel consumers. Schedule only after settlement.
|
|
1064
|
+
function createChannelRefreshStore(load) {
|
|
1065
|
+
const listeners = new Set();
|
|
1066
|
+
let flight = null, timer = null, latest = null, failures = 0, epoch = 0;
|
|
1067
|
+
const visible = () => typeof document === "undefined" || document.visibilityState !== "hidden";
|
|
1068
|
+
const clearTimer = () => { if (timer !== null) clearTimeout(timer); timer = null; };
|
|
1069
|
+
const notify = (data, error) => { for (const listener of listeners) listener(data, error); };
|
|
1070
|
+
const schedule = () => {
|
|
1071
|
+
clearTimer();
|
|
1072
|
+
if (listeners.size && visible()) timer = setTimeout(() => {
|
|
1073
|
+
timer = null;
|
|
1074
|
+
refresh().catch(() => undefined);
|
|
1075
|
+
}, Math.min(4000 * 2 ** failures, 60000));
|
|
1076
|
+
};
|
|
1077
|
+
function refresh() {
|
|
1078
|
+
if (flight) return flight.promise;
|
|
1079
|
+
clearTimer();
|
|
1080
|
+
const current = { controller: new AbortController(), promise: null };
|
|
1081
|
+
flight = current;
|
|
1082
|
+
current.promise = Promise.resolve().then(() => {
|
|
1083
|
+
current.controller.signal.throwIfAborted();
|
|
1084
|
+
return load(current.controller.signal);
|
|
1085
|
+
}).then(data => {
|
|
1086
|
+
if (flight !== current || current.controller.signal.aborted) return data;
|
|
1087
|
+
failures = data.ok ? 0 : failures + 1;
|
|
1088
|
+
latest = { data, error: null };
|
|
1089
|
+
notify(data, null);
|
|
1090
|
+
return data;
|
|
1091
|
+
}).catch(error => {
|
|
1092
|
+
if (flight === current && !current.controller.signal.aborted) {
|
|
1093
|
+
failures++;
|
|
1094
|
+
latest = { data: null, error };
|
|
1095
|
+
notify(null, error);
|
|
1096
|
+
}
|
|
1097
|
+
throw error;
|
|
1098
|
+
}).finally(() => {
|
|
1099
|
+
if (flight !== current) return;
|
|
1100
|
+
flight = null;
|
|
1101
|
+
schedule();
|
|
1102
|
+
});
|
|
1103
|
+
return current.promise;
|
|
1104
|
+
}
|
|
1105
|
+
// A mutation must fetch again after any pre-mutation request has settled.
|
|
1106
|
+
function invalidate() {
|
|
1107
|
+
const generation = epoch;
|
|
1108
|
+
return (flight ? flight.promise.catch(() => undefined) : Promise.resolve()).then(() => {
|
|
1109
|
+
if (generation === epoch) return refresh();
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
const onVisibility = () => {
|
|
1113
|
+
clearTimer();
|
|
1114
|
+
if (visible() && listeners.size) refresh().catch(() => undefined);
|
|
1115
|
+
};
|
|
1116
|
+
function subscribe(listener) {
|
|
1117
|
+
listeners.add(listener);
|
|
1118
|
+
if (latest) listener(latest.data, latest.error);
|
|
1119
|
+
if (listeners.size === 1) {
|
|
1120
|
+
if (typeof document !== "undefined") document.addEventListener("visibilitychange", onVisibility);
|
|
1121
|
+
if (visible()) refresh().catch(() => undefined);
|
|
1122
|
+
}
|
|
1123
|
+
return () => {
|
|
1124
|
+
listeners.delete(listener);
|
|
1125
|
+
if (listeners.size) return;
|
|
1126
|
+
epoch++;
|
|
1127
|
+
clearTimer();
|
|
1128
|
+
if (typeof document !== "undefined") document.removeEventListener("visibilitychange", onVisibility);
|
|
1129
|
+
if (flight) flight.controller.abort();
|
|
1130
|
+
flight = null;
|
|
1131
|
+
latest = null;
|
|
1132
|
+
failures = 0;
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
return { subscribe, refresh, invalidate };
|
|
1136
|
+
}
|
|
1137
|
+
const channelRefresh = createChannelRefreshStore(signal => api("/channels", { signal }));
|
|
1138
|
+
|
|
1139
|
+
const storedAccountSelection = () => {
|
|
1140
|
+
try { return window.localStorage.getItem(ACCOUNT_SELECTION_KEY) || ""; }
|
|
1141
|
+
catch { return ""; }
|
|
1142
|
+
};
|
|
1143
|
+
const rememberAccountSelection = (id) => {
|
|
1144
|
+
try {
|
|
1145
|
+
if (id) window.localStorage.setItem(ACCOUNT_SELECTION_KEY, id);
|
|
1146
|
+
else window.localStorage.removeItem(ACCOUNT_SELECTION_KEY);
|
|
1147
|
+
} catch { /* Local storage can be unavailable in restricted browser contexts. */ }
|
|
1148
|
+
};
|
|
1149
|
+
|
|
1150
|
+
function BrandMark({ id, compact, mono, size }) {
|
|
1151
|
+
const svg = (viewBox, children) => h("svg", {
|
|
1152
|
+
viewBox,
|
|
1153
|
+
width: size || undefined,
|
|
1154
|
+
height: size || undefined,
|
|
1155
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
1156
|
+
fill: "none",
|
|
1157
|
+
"aria-hidden": "true",
|
|
1158
|
+
}, children);
|
|
1159
|
+
const tone = (color) => mono ? "currentColor" : color;
|
|
1160
|
+
if (id === "feishu") {
|
|
1161
|
+
return svg("0 0 48 48", [
|
|
1162
|
+
h("path", { key: "wing", d: "M10 8c0 1 7 3.5 14.745 16.744 0 0 4.184-4.363 6.255-5.744 1.5-1 2.712-1.332 2.712-1.332C33.712 15.156 29.5 8 28 8z", fill: tone("#00d6b9") }),
|
|
1163
|
+
h("path", { key: "head", d: "M43.5 18.5c-1-.667-3.65-1.771-6.5-1.5a15 15 0 0 0-3.288.668S32.5 18 31 19c-2.07 1.38-6.255 5.744-6.255 5.744-1.428 1.397-3.05 2.732-5.245 3.756 0 0 7 3 11.5 3 5.063 0 7-3.5 7-3.5 1.5-3.305 3.5-7 5.5-9.5", fill: tone("#163c9a") }),
|
|
1164
|
+
h("path", { key: "body", d: "M4 17.5v17c0 1 6 5.5 15 5.5 10 0 17.05-7.705 19-12 0 0-1.937 3.5-7 3.5-4.5 0-11.5-3-11.5-3-5.117-2.239-10.03-6.577-12.906-9.117C4.974 17.953 4 17.093 4 17.5", fill: tone("#3370ff") }),
|
|
1165
|
+
]);
|
|
1166
|
+
}
|
|
1167
|
+
return svg("0 0 24 24", [
|
|
1168
|
+
h("circle", { key: "bg", cx: 12, cy: 12, r: 12, fill: tone("#8b949e") }),
|
|
1169
|
+
]);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
function Logo({ id, small }) {
|
|
1173
|
+
return h("div", { className: small ? "dfc-logo sm" : "dfc-logo", "data-brand": id, "aria-hidden": "true" }, h(BrandMark, { id, compact: small }));
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function feishuTabLabelNode(t) {
|
|
1177
|
+
return h("span", { className: "dfc-tab-inline" },
|
|
1178
|
+
h("span", {
|
|
1179
|
+
className: "dfc-tab-brand",
|
|
1180
|
+
"aria-hidden": "true",
|
|
1181
|
+
style: { display: "inline-flex", alignItems: "center", justifyContent: "center", width: 14, height: 14, flex: "none" },
|
|
1182
|
+
}, h(BrandMark, { id: "feishu", compact: true, mono: true, size: 14 })),
|
|
1183
|
+
h("span", null, t("rail.feishu")),
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function isRasterQr(value) {
|
|
1188
|
+
return typeof value === "string" && (/^data:image\//i.test(value) || /\.(png|jpe?g|gif|webp)(\?|$)/i.test(value));
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function qrSrc(pairing) {
|
|
1192
|
+
if (!pairing) return "";
|
|
1193
|
+
if (isRasterQr(pairing.qrImage)) return pairing.qrImage;
|
|
1194
|
+
return "";
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
const SERVER_TEXT_KEYS = new Map([
|
|
1198
|
+
["未知渠道", "server.unknownChannel"], ["渠道未配置", "server.channelUnconfigured"], ["会话不存在", "server.sessionMissing"],
|
|
1199
|
+
["账号不存在", "server.accountMissing"], ["账号连接失败,请查看本机日志", "server.accountConnectFailed"], ["重新连接失败,请查看本机日志", "server.accountReconnectFailed"],
|
|
1200
|
+
["账号备注不能超过 40 个字符", "server.remarkTooLong"], ["账号备注不能包含换行或控制字符", "server.remarkControl"],
|
|
1201
|
+
["请选择提供商、模型、工作区或权限", "server.selectAccountSettings"], ["请选择提供商和模型", "server.selectModel"], ["请选择工作区", "server.selectWorkspace"], ["请先添加工作区", "server.createWorkspace"], ["请选择权限", "server.selectPermission"], ["凭据不足,无法启动渠道", "server.missingCredentials"],
|
|
1202
|
+
["该渠道不支持扫码绑定", "server.qrUnsupported"], ["二维码已过期", "server.qrExpired"], ["扫码未完成", "server.qrIncomplete"],
|
|
1203
|
+
["未授权:请管理员在设置 → 飞书频道 中批准你的访问。", "server.accessDenied"],
|
|
1204
|
+
["未连接", "account.statusNotConnected"], ["已停止", "status.stopped"], ["长连接已建立", "status.longConnection"],
|
|
1205
|
+
]);
|
|
1206
|
+
function serverText(value, t) {
|
|
1207
|
+
const text = value == null ? "" : String(value);
|
|
1208
|
+
const key = SERVER_TEXT_KEYS.get(text);
|
|
1209
|
+
if (key) return t(key);
|
|
1210
|
+
// Server adapters return diagnostics, not locale keys. Do not leak a
|
|
1211
|
+
// Chinese diagnostic into an English UI; the detailed value remains in
|
|
1212
|
+
// the server log while the browser gets a usable localized fallback.
|
|
1213
|
+
if (/[\u3400-\u9fff]/.test(text) && t("settings.label") === "Feishu Channel") return t("error.detailsInLog");
|
|
1214
|
+
return value;
|
|
1215
|
+
}
|
|
1216
|
+
async function copyText(value) {
|
|
1217
|
+
const text = String(value ?? "");
|
|
1218
|
+
if (!text) return false;
|
|
1219
|
+
try {
|
|
1220
|
+
if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
|
|
1221
|
+
await navigator.clipboard.writeText(text);
|
|
1222
|
+
return true;
|
|
1223
|
+
}
|
|
1224
|
+
} catch { /* fall through to the DOM fallback */ }
|
|
1225
|
+
try {
|
|
1226
|
+
const area = document.createElement("textarea");
|
|
1227
|
+
area.value = text;
|
|
1228
|
+
area.setAttribute("readonly", "");
|
|
1229
|
+
area.style.position = "fixed";
|
|
1230
|
+
area.style.opacity = "0";
|
|
1231
|
+
document.body.appendChild(area);
|
|
1232
|
+
area.select();
|
|
1233
|
+
const ok = typeof document.execCommand === "function" ? document.execCommand("copy") : false;
|
|
1234
|
+
area.remove();
|
|
1235
|
+
return ok;
|
|
1236
|
+
} catch { return false; }
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
function relativeTime(value, t) {
|
|
1240
|
+
const ms = Date.parse(value || "");
|
|
1241
|
+
if (!Number.isFinite(ms)) return "";
|
|
1242
|
+
const diff = Math.max(0, Date.now() - ms);
|
|
1243
|
+
const minutes = Math.floor(diff / 60000);
|
|
1244
|
+
if (minutes < 1) return t("time.now");
|
|
1245
|
+
if (minutes < 60) return t("time.ago", { t: t("time.minutes", { n: minutes }) });
|
|
1246
|
+
const hours = Math.floor(minutes / 60);
|
|
1247
|
+
if (hours < 24) return t("time.ago", { t: t("time.hours", { n: hours }) });
|
|
1248
|
+
const days = Math.floor(hours / 24);
|
|
1249
|
+
if (days < 30) return t("time.ago", { t: t("time.days", { n: days }) });
|
|
1250
|
+
const months = Math.floor(days / 30);
|
|
1251
|
+
if (months < 12) return t("time.ago", { t: t("time.months", { n: months }) });
|
|
1252
|
+
return t("time.ago", { t: t("time.years", { n: Math.floor(months / 12) }) });
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
function defaultChannelName(ch) {
|
|
1256
|
+
const count = Array.isArray(ch && ch.accounts) ? ch.accounts.length : 0;
|
|
1257
|
+
return `飞书账号${Math.max(1, count + 1)}`;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
function channelLabel(ch, t) {
|
|
1261
|
+
const key = "channel." + ch.id;
|
|
1262
|
+
const translated = t(key);
|
|
1263
|
+
return translated === key ? (ch.label || ch.id) : translated;
|
|
1264
|
+
}
|
|
1265
|
+
function accountLabel(account, t) {
|
|
1266
|
+
if (!account || !account.autoName) return account && (account.name || account.id) || "";
|
|
1267
|
+
return t("account.defaultName", {
|
|
1268
|
+
channel: channelLabel({ id: account.platform, label: account.platform }, t),
|
|
1269
|
+
count: account.nameOrdinal || 1,
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
function fieldLabel(ch, field, t) {
|
|
1273
|
+
const key = "field." + ch.id + "." + field.key;
|
|
1274
|
+
const translated = t(key);
|
|
1275
|
+
return translated === key ? field.label : translated;
|
|
1276
|
+
}
|
|
1277
|
+
function hintOf(ch, t) {
|
|
1278
|
+
const key = "qr." + ch.id;
|
|
1279
|
+
const translated = t(key);
|
|
1280
|
+
return translated === key ? t("qr.default") : translated;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// 有 reflect 时只 probe,不能硬读未注入的官方导航服务,否则 Cordis 会挡住插件激活。
|
|
1284
|
+
function probeService(ctx, name) {
|
|
1285
|
+
if (!ctx) return undefined;
|
|
1286
|
+
const reflect = ctx.reflect;
|
|
1287
|
+
if (reflect && typeof reflect.get === "function") {
|
|
1288
|
+
try {
|
|
1289
|
+
const found = reflect.get(name);
|
|
1290
|
+
if (found !== undefined) return found;
|
|
1291
|
+
} catch { /* 未注入时不硬读 ctx[name],改走 get */ }
|
|
1292
|
+
}
|
|
1293
|
+
if (typeof ctx.get === "function") {
|
|
1294
|
+
try {
|
|
1295
|
+
const found = ctx.get(name);
|
|
1296
|
+
if (found !== undefined) return found;
|
|
1297
|
+
} catch { return undefined; }
|
|
1298
|
+
}
|
|
1299
|
+
if (reflect && typeof reflect.get === "function") return undefined;
|
|
1300
|
+
try { return ctx[name]; } catch { return undefined; }
|
|
1301
|
+
}
|
|
1302
|
+
// 旧宿主读 list.current;alpha.2 主视图改由 retainedBy.mainView 标记。
|
|
1303
|
+
function currentSessionId(state) {
|
|
1304
|
+
if (!state) return null;
|
|
1305
|
+
if (typeof state.current === "string" && state.current) return state.current;
|
|
1306
|
+
const byId = state.byId || {};
|
|
1307
|
+
const ids = Array.isArray(state.ids) && state.ids.length ? state.ids : Object.keys(byId);
|
|
1308
|
+
const pick = (id, row) => {
|
|
1309
|
+
if (!row || !row.retainedBy || (row.retainedBy.mainView ?? 0) <= 0) return null;
|
|
1310
|
+
const explicit = row.id && String(row.id).trim();
|
|
1311
|
+
return explicit || id;
|
|
1312
|
+
};
|
|
1313
|
+
for (let i = 0; i < ids.length; i++) {
|
|
1314
|
+
const found = pick(ids[i], byId[ids[i]]);
|
|
1315
|
+
if (found) return found;
|
|
1316
|
+
}
|
|
1317
|
+
for (const id in byId) {
|
|
1318
|
+
if (ids.indexOf(id) !== -1) continue;
|
|
1319
|
+
const found = pick(id, byId[id]);
|
|
1320
|
+
if (found) return found;
|
|
1321
|
+
}
|
|
1322
|
+
return null;
|
|
1323
|
+
}
|
|
1324
|
+
function resolveHostSessionId(sessionId, sessionById) {
|
|
1325
|
+
if (!sessionId) return sessionId;
|
|
1326
|
+
const listed = sessionById && sessionById[sessionId];
|
|
1327
|
+
const fromKey = listed && (listed.id || listed.sessionId);
|
|
1328
|
+
if (fromKey && String(fromKey).trim()) return String(fromKey).trim();
|
|
1329
|
+
return sessionId;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
function mainSessionModel(state) {
|
|
1333
|
+
const id = currentSessionId(state);
|
|
1334
|
+
const row = id && state && state.projectionsBySession && state.projectionsBySession[id];
|
|
1335
|
+
const selection = row && row.values && row.values.modelSelection;
|
|
1336
|
+
const next = selection && (selection.next || selection.lastUsed);
|
|
1337
|
+
if (!next || !next.provider || !next.model) return null;
|
|
1338
|
+
return { provider: next.provider, model: next.model, reasoningEffort: next.reasoningEffort || "" };
|
|
1339
|
+
}
|
|
1340
|
+
function readMainSessionModel(ctx) {
|
|
1341
|
+
const sessions = probeService(ctx, "sessions");
|
|
1342
|
+
const list = sessions && sessions.list;
|
|
1343
|
+
if (!list || typeof list.getSnapshot !== "function") return null;
|
|
1344
|
+
try { return mainSessionModel(list.getSnapshot()); } catch { return null; }
|
|
1345
|
+
}
|
|
1346
|
+
function openHostSession(ctx, id) {
|
|
1347
|
+
if (!id) return false;
|
|
1348
|
+
const uiWorkspace = probeService(ctx, "uiWorkspace");
|
|
1349
|
+
if (uiWorkspace && typeof uiWorkspace.openSession === "function") {
|
|
1350
|
+
uiWorkspace.openSession(id);
|
|
1351
|
+
return true;
|
|
1352
|
+
}
|
|
1353
|
+
// alpha.2 有 retain:旧的 sessions.open 即使还在也不会写入 mainView,点了等于没打开。
|
|
1354
|
+
if (ctx.sessions && typeof ctx.sessions.retain === "function") return false;
|
|
1355
|
+
if (ctx.sessions && typeof ctx.sessions.open === "function") {
|
|
1356
|
+
ctx.sessions.open(id);
|
|
1357
|
+
return true;
|
|
1358
|
+
}
|
|
1359
|
+
return false;
|
|
1360
|
+
}
|
|
1361
|
+
function parsePickedPath(value) {
|
|
1362
|
+
if (typeof value === "string" && value.trim() !== "") return value;
|
|
1363
|
+
if (value === null || typeof value !== "object") return undefined;
|
|
1364
|
+
if (value.canceled === true) return undefined;
|
|
1365
|
+
if (typeof value.path === "string" && value.path.trim() !== "") return value.path;
|
|
1366
|
+
const first = Array.isArray(value.filePaths) ? value.filePaths[0] : undefined;
|
|
1367
|
+
return typeof first === "string" && first.trim() !== "" ? first : undefined;
|
|
1368
|
+
}
|
|
1369
|
+
function pickHostDirectory(ctx) {
|
|
1370
|
+
const host = typeof globalThis !== "undefined" ? globalThis.__DSH_DIRECTORY_PICKER__ : undefined;
|
|
1371
|
+
if (host && typeof host.pick === "function") {
|
|
1372
|
+
return Promise.resolve(host.pick()).then(parsePickedPath);
|
|
1373
|
+
}
|
|
1374
|
+
const uiWorkspace = probeService(ctx, "uiWorkspace");
|
|
1375
|
+
if (uiWorkspace && typeof uiWorkspace.pickDirectory === "function") return Promise.resolve(uiWorkspace.pickDirectory()).then(parsePickedPath);
|
|
1376
|
+
const dialog = probeService(ctx, "dialog");
|
|
1377
|
+
if (dialog && typeof dialog.pickDirectory === "function") return Promise.resolve(dialog.pickDirectory()).then(parsePickedPath);
|
|
1378
|
+
if (ctx && ctx.workspaces && typeof ctx.workspaces.pickDirectory === "function") return Promise.resolve(ctx.workspaces.pickDirectory()).then(parsePickedPath);
|
|
1379
|
+
// 全部来源都不可用时必须显式拒绝:调用方据此提示“当前 Host 无法新增工作区”,
|
|
1380
|
+
// 否则点击“添加工作区…”会静默无响应(取消选择由 resolve(undefined) 表示,二者不能混淆)。
|
|
1381
|
+
return Promise.reject(workspacePickerUnavailable());
|
|
1382
|
+
}
|
|
1383
|
+
function workspacePickerUnavailable() {
|
|
1384
|
+
const error = new Error("workspace picker unavailable");
|
|
1385
|
+
error.code = "workspace/picker-unavailable";
|
|
1386
|
+
return error;
|
|
1387
|
+
}
|
|
1388
|
+
function hostWorkspaceApi(ctx) {
|
|
1389
|
+
return probeService(ctx, "uiWorkspace") || ctx.workspaces || {};
|
|
1390
|
+
}
|
|
1391
|
+
function archiveHostSession(ctx, id, options) {
|
|
1392
|
+
const api = hostWorkspaceApi(ctx);
|
|
1393
|
+
if (typeof api.archiveSession !== "function") return;
|
|
1394
|
+
return options ? api.archiveSession(id, options) : api.archiveSession(id);
|
|
1395
|
+
}
|
|
1396
|
+
function unarchiveHostSession(ctx, id) {
|
|
1397
|
+
const api = hostWorkspaceApi(ctx);
|
|
1398
|
+
if (typeof api.unarchiveSession === "function") return api.unarchiveSession(id);
|
|
1399
|
+
if (typeof api.restoreSession === "function") return api.restoreSession(id);
|
|
1400
|
+
}
|
|
1401
|
+
function forkHostSession(ctx, id) {
|
|
1402
|
+
const uiWorkspace = probeService(ctx, "uiWorkspace");
|
|
1403
|
+
if (uiWorkspace && typeof uiWorkspace.forkSession === "function") return Promise.resolve(uiWorkspace.forkSession(id));
|
|
1404
|
+
if (!ctx.sessions || typeof ctx.sessions.fork !== "function") return Promise.resolve();
|
|
1405
|
+
return ctx.sessions.fork({ sessionId: id, increaseTitle: true })
|
|
1406
|
+
.then((childId) => { openHostSession(ctx, childId); })
|
|
1407
|
+
.catch(() => undefined);
|
|
1408
|
+
}
|
|
1409
|
+
async function renameHostSession(ctx, sessionId, title) {
|
|
1410
|
+
const sessions = ctx.sessions;
|
|
1411
|
+
if (!sessions) throw new Error("unknown session");
|
|
1412
|
+
const renameBound = async (session) => {
|
|
1413
|
+
if (!session || typeof session.rename !== "function") throw new Error("unknown session");
|
|
1414
|
+
const result = await session.rename(title);
|
|
1415
|
+
if (result && result.ok === false) throw new Error((result.error && result.error.message) || "rename failed");
|
|
1416
|
+
};
|
|
1417
|
+
if (typeof sessions.using === "function") {
|
|
1418
|
+
return sessions.using(sessionId, { source: "controllerOperation" }, async (reference) => {
|
|
1419
|
+
if (reference && reference.ready) await reference.ready;
|
|
1420
|
+
return renameBound(reference && reference.binding && reference.binding.session);
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
const existing = sessions.binding && sessions.binding(sessionId);
|
|
1424
|
+
if (existing && existing.session) return renameBound(existing.session);
|
|
1425
|
+
if (typeof sessions.retain !== "function") throw new Error("unknown session");
|
|
1426
|
+
const ref = sessions.retain(sessionId, { source: "controllerOperation" });
|
|
1427
|
+
try {
|
|
1428
|
+
if (ref && ref.ready) await ref.ready;
|
|
1429
|
+
return renameBound(ref && ref.binding && ref.binding.session);
|
|
1430
|
+
} finally {
|
|
1431
|
+
if (ref && typeof ref.release === "function") ref.release();
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
let openImSession = (id) => {
|
|
1436
|
+
try {
|
|
1437
|
+
if (window.__dshSessionsOpen) { window.__dshSessionsOpen(id); return true; }
|
|
1438
|
+
} catch { /* ignore */ }
|
|
1439
|
+
return false;
|
|
1440
|
+
};
|
|
1441
|
+
const tryOpenListedSession = (id, hostOpen) => {
|
|
1442
|
+
if (openImSession(id)) return true;
|
|
1443
|
+
if (typeof hostOpen !== "function") return false;
|
|
1444
|
+
try { hostOpen(id); return true; } catch { return false; }
|
|
1445
|
+
};
|
|
1446
|
+
const openListedSession = (id, hostOpen) => {
|
|
1447
|
+
if (!id) return;
|
|
1448
|
+
if (isChannelSession(id)) {
|
|
1449
|
+
api("/sessions/ensure", {
|
|
1450
|
+
method: "POST",
|
|
1451
|
+
headers: { "content-type": "application/json" },
|
|
1452
|
+
body: JSON.stringify({ sessionId: id }),
|
|
1453
|
+
}).then((data) => {
|
|
1454
|
+
if (!data.ok) { console.warn("[dsh-feishu-channels] 无法恢复会话", data.error || id); return; }
|
|
1455
|
+
const tryOpen = (left) => {
|
|
1456
|
+
if (openImSession(id) || left <= 0) return;
|
|
1457
|
+
setTimeout(() => tryOpen(left - 1), 80);
|
|
1458
|
+
};
|
|
1459
|
+
tryOpen(20);
|
|
1460
|
+
}).catch((error) => console.warn("[dsh-feishu-channels] 无法恢复会话", id, error));
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
tryOpenListedSession(id, hostOpen);
|
|
1464
|
+
};
|
|
1465
|
+
|
|
1466
|
+
function preferredAssistant(defaults, readModel) {
|
|
1467
|
+
const main = typeof readModel === "function" ? readModel() : null;
|
|
1468
|
+
if (main && main.provider && main.model) return main;
|
|
1469
|
+
const assistant = defaults && defaults.assistant;
|
|
1470
|
+
return assistant && assistant.provider && assistant.model ? assistant : null;
|
|
1471
|
+
}
|
|
1472
|
+
function selectableWorkspace(preferred, workspaces) {
|
|
1473
|
+
const items = Array.isArray(workspaces) ? workspaces : [];
|
|
1474
|
+
const key = (path) => {
|
|
1475
|
+
const value = String(path || "");
|
|
1476
|
+
const windows = /^[a-z]:[\\/]|^\\\\|^\/\//i.test(value);
|
|
1477
|
+
const normalized = (windows ? value.replace(/\\/g, "/") : value).replace(/\/+$/, "");
|
|
1478
|
+
return windows ? normalized.toLowerCase() : normalized;
|
|
1479
|
+
};
|
|
1480
|
+
const wanted = key(preferred);
|
|
1481
|
+
const matched = wanted && items.find((item) => item && key(item.path) === wanted);
|
|
1482
|
+
if (matched && matched.path) return matched.path;
|
|
1483
|
+
const first = items.find((item) => item && item.path);
|
|
1484
|
+
return first ? first.path : "";
|
|
1485
|
+
}
|
|
1486
|
+
function workspaceIsListed(cwd, workspaces) {
|
|
1487
|
+
const items = Array.isArray(workspaces) ? workspaces : [];
|
|
1488
|
+
const key = (path) => {
|
|
1489
|
+
const value = String(path || "");
|
|
1490
|
+
const windows = /^[a-z]:[\\/]|^\\\\|^\/\//i.test(value);
|
|
1491
|
+
const normalized = (windows ? value.replace(/\\/g, "/") : value).replace(/\/+$/, "");
|
|
1492
|
+
return windows ? normalized.toLowerCase() : normalized;
|
|
1493
|
+
};
|
|
1494
|
+
const wanted = key(cwd);
|
|
1495
|
+
return Boolean(wanted && items.some((item) => item && key(item.path) === wanted));
|
|
1496
|
+
}
|
|
1497
|
+
function BindModal({ ch, onClose, onConnected, catalog, permissions, agentPresets, workspaces, defaults, readMainSessionModel, createWorkspace, pickDirectory, modelT, permissionT, presetT, t = fallbackT }) {
|
|
1498
|
+
const hasQr = ch.kind === "qr" || ch.kind === "qr-or-credentials";
|
|
1499
|
+
const hasManual = false;
|
|
1500
|
+
const showTabs = false;
|
|
1501
|
+
const [tab, setTab] = useState("qr");
|
|
1502
|
+
const [pairing, setPairing] = useState(null);
|
|
1503
|
+
const [qrStarted, setQrStarted] = useState(false);
|
|
1504
|
+
const [draft, setDraft] = useState({});
|
|
1505
|
+
const [busy, setBusy] = useState(false);
|
|
1506
|
+
const [error, setError] = useState("");
|
|
1507
|
+
const [success, setSuccess] = useState("");
|
|
1508
|
+
const modelTouched = useRef(false);
|
|
1509
|
+
const workspaceTouched = useRef(false);
|
|
1510
|
+
const [settings, setSettings] = useState(() => {
|
|
1511
|
+
const assistant = preferredAssistant(defaults, readMainSessionModel);
|
|
1512
|
+
return {
|
|
1513
|
+
name: defaultChannelName(ch),
|
|
1514
|
+
cwd: selectableWorkspace(defaults && defaults.cwd, workspaces),
|
|
1515
|
+
provider: assistant && assistant.provider || "",
|
|
1516
|
+
model: assistant && assistant.model || "",
|
|
1517
|
+
reasoningEffort: assistant && assistant.reasoningEffort || "",
|
|
1518
|
+
permission: defaults && defaults.permission || "",
|
|
1519
|
+
privateAccess: "approved",
|
|
1520
|
+
sendMode: "queue",
|
|
1521
|
+
agentPreset: defaults && defaults.agentPreset || "standard",
|
|
1522
|
+
};
|
|
1523
|
+
});
|
|
1524
|
+
const alive = useRef(true);
|
|
1525
|
+
useEffect(() => {
|
|
1526
|
+
if (modelTouched.current) return;
|
|
1527
|
+
const assistant = preferredAssistant(defaults, readMainSessionModel);
|
|
1528
|
+
if (!assistant) return;
|
|
1529
|
+
setSettings((current) => current.provider === assistant.provider && current.model === assistant.model
|
|
1530
|
+
? current
|
|
1531
|
+
: { ...current, provider: assistant.provider, model: assistant.model, reasoningEffort: assistant.reasoningEffort || "" });
|
|
1532
|
+
}, [defaults, readMainSessionModel]);
|
|
1533
|
+
useEffect(() => {
|
|
1534
|
+
if (workspaceTouched.current) return;
|
|
1535
|
+
const next = selectableWorkspace(defaults && defaults.cwd, workspaces);
|
|
1536
|
+
setSettings((current) => current.cwd === next ? current : { ...current, cwd: next });
|
|
1537
|
+
}, [defaults, workspaces]);
|
|
1538
|
+
|
|
1539
|
+
const startQr = useCallback((refresh) => {
|
|
1540
|
+
if (!hasQr || !workspaceIsListed(settings.cwd, workspaces)) return;
|
|
1541
|
+
setQrStarted(true);
|
|
1542
|
+
setBusy(true);
|
|
1543
|
+
setError("");
|
|
1544
|
+
setSuccess("");
|
|
1545
|
+
api(`/channels/${ch.id}/qr/${refresh ? "refresh" : "start"}`, {
|
|
1546
|
+
method: "POST",
|
|
1547
|
+
headers: { "content-type": "application/json" },
|
|
1548
|
+
body: JSON.stringify({ settings }),
|
|
1549
|
+
}).then((data) => {
|
|
1550
|
+
if (!alive.current) return;
|
|
1551
|
+
if (!data.ok && !data.pairing) setError(serverText(data.error, t) || t("error.qr"));
|
|
1552
|
+
setPairing(data.pairing || null);
|
|
1553
|
+
}).catch(() => { if (alive.current) setError(t("error.qr")); })
|
|
1554
|
+
.finally(() => { if (alive.current) setBusy(false); });
|
|
1555
|
+
}, [ch.id, hasQr, settings, workspaces]);
|
|
1556
|
+
|
|
1557
|
+
useEffect(() => {
|
|
1558
|
+
alive.current = true;
|
|
1559
|
+
return () => { alive.current = false; };
|
|
1560
|
+
}, []);
|
|
1561
|
+
|
|
1562
|
+
const finished = useRef(false);
|
|
1563
|
+
const onConnectedRef = useRef(onConnected);
|
|
1564
|
+
onConnectedRef.current = onConnected;
|
|
1565
|
+
const finish = (delayMs) => {
|
|
1566
|
+
if (finished.current) return;
|
|
1567
|
+
finished.current = true;
|
|
1568
|
+
alive.current = false;
|
|
1569
|
+
window.setTimeout(() => onConnectedRef.current(), delayMs);
|
|
1570
|
+
};
|
|
1571
|
+
|
|
1572
|
+
useEffect(() => {
|
|
1573
|
+
if (tab !== "qr" || !qrStarted) return undefined;
|
|
1574
|
+
const timer = setInterval(() => {
|
|
1575
|
+
api(`/channels/${ch.id}/qr/status`).then((data) => {
|
|
1576
|
+
if (finished.current || !alive.current || !data.ok) return;
|
|
1577
|
+
setPairing(data.pairing);
|
|
1578
|
+
if (data.pairing && data.pairing.status === "success") finish(800);
|
|
1579
|
+
}).catch(() => undefined);
|
|
1580
|
+
}, 2000);
|
|
1581
|
+
return () => clearInterval(timer);
|
|
1582
|
+
}, [tab, ch.id, qrStarted]);
|
|
1583
|
+
|
|
1584
|
+
const status = qrStarted && pairing && pairing.status;
|
|
1585
|
+
const saving = status === "saving";
|
|
1586
|
+
const close = () => {
|
|
1587
|
+
if (finished.current || saving) return;
|
|
1588
|
+
alive.current = false;
|
|
1589
|
+
if (hasQr) api("/channels/" + ch.id + "/qr/cancel", { method: "POST" }).catch(() => undefined);
|
|
1590
|
+
onClose();
|
|
1591
|
+
};
|
|
1592
|
+
const onCloseRef = useRef(onClose);
|
|
1593
|
+
onCloseRef.current = onClose;
|
|
1594
|
+
useEffect(() => {
|
|
1595
|
+
const onKeyDown = (event) => {
|
|
1596
|
+
if (modalLayers.length && modalLayers[modalLayers.length - 1].owner !== "bind") return;
|
|
1597
|
+
if (event.key !== "Escape") return;
|
|
1598
|
+
event.preventDefault();
|
|
1599
|
+
event.stopPropagation();
|
|
1600
|
+
if (typeof event.stopImmediatePropagation === "function") event.stopImmediatePropagation();
|
|
1601
|
+
if (saving) return;
|
|
1602
|
+
alive.current = false;
|
|
1603
|
+
if (hasQr) api("/channels/" + ch.id + "/qr/cancel", { method: "POST" }).catch(() => undefined);
|
|
1604
|
+
onCloseRef.current();
|
|
1605
|
+
};
|
|
1606
|
+
window.addEventListener("keydown", onKeyDown, true);
|
|
1607
|
+
return () => window.removeEventListener("keydown", onKeyDown, true);
|
|
1608
|
+
}, [hasQr, ch.id, saving]);
|
|
1609
|
+
|
|
1610
|
+
const saveManual = () => {
|
|
1611
|
+
if (!workspaceIsListed(settings.cwd, workspaces)) {
|
|
1612
|
+
setError(t("composer.workspaceRequired"));
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
const config = { ...draft };
|
|
1616
|
+
setBusy(true);
|
|
1617
|
+
setError("");
|
|
1618
|
+
setSuccess("");
|
|
1619
|
+
api(`/channels/${ch.id}/connect`, {
|
|
1620
|
+
method: "POST",
|
|
1621
|
+
headers: { "content-type": "application/json" },
|
|
1622
|
+
body: JSON.stringify({ config, settings }),
|
|
1623
|
+
}).then((data) => {
|
|
1624
|
+
if (!data.ok) setError(serverText(data.error, t) || t("error.save"));
|
|
1625
|
+
else {
|
|
1626
|
+
setSuccess(data.newIdentity ? t("bind.newIdentity") : t("bind.success"));
|
|
1627
|
+
finish(data.newIdentity ? 1400 : 500);
|
|
1628
|
+
}
|
|
1629
|
+
}).catch(() => setError(t("error.save"))).finally(() => setBusy(false));
|
|
1630
|
+
};
|
|
1631
|
+
|
|
1632
|
+
const switchTab = (next) => {
|
|
1633
|
+
if (saving) return;
|
|
1634
|
+
setTab(next);
|
|
1635
|
+
setError("");
|
|
1636
|
+
setSuccess("");
|
|
1637
|
+
if (next !== "qr") {
|
|
1638
|
+
setQrStarted(false);
|
|
1639
|
+
setPairing(null);
|
|
1640
|
+
api(`/channels/${ch.id}/qr/cancel`, { method: "POST" }).catch(() => undefined);
|
|
1641
|
+
}
|
|
1642
|
+
};
|
|
1643
|
+
|
|
1644
|
+
const src = qrStarted ? qrSrc(pairing) : "";
|
|
1645
|
+
const remain = qrStarted && pairing && pairing.remainingSeconds;
|
|
1646
|
+
|
|
1647
|
+
return h(Modal, {
|
|
1648
|
+
open: true,
|
|
1649
|
+
className: "dfc-bind-modal",
|
|
1650
|
+
title: t("bind.title", { channel: channelLabel(ch, t) }),
|
|
1651
|
+
width: 720,
|
|
1652
|
+
zIndex: 1100,
|
|
1653
|
+
destroyOnHidden: true,
|
|
1654
|
+
layerOwner: "bind",
|
|
1655
|
+
maskClosable: !saving,
|
|
1656
|
+
onCancel: close,
|
|
1657
|
+
footer: tab === "manual" && hasManual
|
|
1658
|
+
? h("div", { className: "dfc-modal-actions" },
|
|
1659
|
+
hostModalButton(t("action.cancel"), close, "outline", { disabled: busy }),
|
|
1660
|
+
hostModalButton(busy ? t("action.saving") : t("action.confirm"), saveManual, "primary", { disabled: busy || !workspaceIsListed(settings.cwd, workspaces) }),
|
|
1661
|
+
)
|
|
1662
|
+
: null,
|
|
1663
|
+
},
|
|
1664
|
+
showTabs && h("div", {
|
|
1665
|
+
className: ["dfc-bind-tabs-wrap", saving ? "is-disabled" : ""].filter(Boolean).join(" "),
|
|
1666
|
+
"aria-disabled": saving ? "true" : undefined,
|
|
1667
|
+
}, h(HostSegmentedTabs, {
|
|
1668
|
+
className: "dfc-bind-tabs",
|
|
1669
|
+
label: t("bind.tabsAria"),
|
|
1670
|
+
value: tab,
|
|
1671
|
+
onChange: (next) => switchTab(next),
|
|
1672
|
+
items: [
|
|
1673
|
+
{ value: "qr", id: "dfc-bind-tab-qr", panelId: "dfc-bind-panel-qr", label: t("bind.quick") },
|
|
1674
|
+
{ value: "manual", id: "dfc-bind-tab-manual", panelId: "dfc-bind-panel-manual", label: t("bind.manual") },
|
|
1675
|
+
],
|
|
1676
|
+
})),
|
|
1677
|
+
error && h(NoticeBar, { type: "error" }, error),
|
|
1678
|
+
success && h(NoticeBar, { type: "success" }, success),
|
|
1679
|
+
h("label", { className: "dfc-field dfc-channel-name-field" },
|
|
1680
|
+
h("span", { className: "dfc-picker-label" }, t("account.channelName")),
|
|
1681
|
+
h(Input, {
|
|
1682
|
+
type: "text",
|
|
1683
|
+
value: settings.name || "",
|
|
1684
|
+
placeholder: defaultChannelName(ch),
|
|
1685
|
+
onChange: (event) => setSettings((current) => ({ ...current, name: event.target.value })),
|
|
1686
|
+
}),
|
|
1687
|
+
),
|
|
1688
|
+
h("div", { className: "dfc-setup-section" },
|
|
1689
|
+
h(AccountSettingsPicker, {
|
|
1690
|
+
value: settings,
|
|
1691
|
+
onChange: (patch) => {
|
|
1692
|
+
if (patch && patch.cwd !== undefined) workspaceTouched.current = true;
|
|
1693
|
+
if (patch && (patch.provider !== undefined || patch.model !== undefined)) modelTouched.current = true;
|
|
1694
|
+
setSettings((current) => ({ ...current, ...patch }));
|
|
1695
|
+
},
|
|
1696
|
+
catalog,
|
|
1697
|
+
permissions,
|
|
1698
|
+
agentPresets,
|
|
1699
|
+
workspaces,
|
|
1700
|
+
createWorkspace,
|
|
1701
|
+
pickDirectory,
|
|
1702
|
+
modelT,
|
|
1703
|
+
permissionT,
|
|
1704
|
+
presetT,
|
|
1705
|
+
showAutoNameNote: false,
|
|
1706
|
+
t,
|
|
1707
|
+
}),
|
|
1708
|
+
h(CommandPermissionSettings, { value: settings.commandPermissions, onSave: commandPermissions => setSettings(current => ({ ...current, commandPermissions })), t }),
|
|
1709
|
+
),
|
|
1710
|
+
hasQr && h("div", showTabs ? { className: "dfc-bind-panel", id: "dfc-bind-panel-qr", role: "tabpanel", "aria-labelledby": "dfc-bind-tab-qr", hidden: tab !== "qr" } : null,
|
|
1711
|
+
status === "success"
|
|
1712
|
+
? h(NoticeBar, { type: "success" }, t("bind.success"))
|
|
1713
|
+
: status === "saving"
|
|
1714
|
+
? h("div", { className: "dfc-bind-status" }, t("bind.saving"))
|
|
1715
|
+
: src
|
|
1716
|
+
? h("div", { className: "dfc-qrbox" },
|
|
1717
|
+
h("p", { className: "dfc-hint" }, hintOf(ch, t)),
|
|
1718
|
+
h("img", { src, alt: t("bind.qrAlt", { channel: channelLabel(ch, t) }) }),
|
|
1719
|
+
remain > 0 && h("p", { className: "dfc-hint" }, t("bind.expire", { time: Math.floor(remain / 60) + ":" + String(remain % 60).padStart(2, "0") })),
|
|
1720
|
+
status === "scanned" && h("p", { className: "dfc-hint" }, t("bind.scanned")),
|
|
1721
|
+
(status === "expired" || status === "failed") && h("p", { className: "dfc-error" }, serverText(pairing && pairing.error, t) || t("bind.retry")),
|
|
1722
|
+
hostModalButton(t("bind.refresh"), () => startQr(true), "outline", { disabled: busy }),
|
|
1723
|
+
)
|
|
1724
|
+
: h("div", { className: "dfc-bind-ready" },
|
|
1725
|
+
h("p", { className: "dfc-hint" }, hintOf(ch, t)),
|
|
1726
|
+
(busy || status === "starting") && h("div", { className: "dfc-bind-status" }, t("bind.generating")),
|
|
1727
|
+
(status === "expired" || status === "failed") && h("div", { className: "dfc-error" }, serverText(pairing && pairing.error, t) || t("bind.retry")),
|
|
1728
|
+
hostModalButton(busy ? t("bind.generating") : qrStarted ? t("bind.refresh") : t("action.generateQr"), () => startQr(qrStarted), "primary", { disabled: busy || !workspaceIsListed(settings.cwd, workspaces) || !settings.provider || !settings.model || !settings.permission }),
|
|
1729
|
+
)
|
|
1730
|
+
),
|
|
1731
|
+
hasManual && h("div", showTabs ? { className: "dfc-bind-panel", id: "dfc-bind-panel-manual", role: "tabpanel", "aria-labelledby": "dfc-bind-tab-manual", hidden: tab !== "manual" } : null,
|
|
1732
|
+
...ch.fields.filter((f) => !f.settingsOnly).map((f) => h("label", { key: f.key, className: "dfc-field" },
|
|
1733
|
+
fieldLabel(ch, f, t),
|
|
1734
|
+
h(Input, {
|
|
1735
|
+
type: f.secret ? "password" : "text",
|
|
1736
|
+
value: draft[f.key] || "",
|
|
1737
|
+
placeholder: fieldLabel(ch, f, t),
|
|
1738
|
+
onChange: (e) => setDraft({ ...draft, [f.key]: e.target.value }),
|
|
1739
|
+
}),
|
|
1740
|
+
)),
|
|
1741
|
+
),
|
|
1742
|
+
);
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
const BUILT_IN_PERMISSION_LABELS = new Map([
|
|
1746
|
+
["read-only", ["permission.readOnly", "Read Only"]],
|
|
1747
|
+
["workspace-write", ["permission.workspaceWrite", "Workspace Write"]],
|
|
1748
|
+
["danger-full-access", ["permission.fullAccess", "Full access"]],
|
|
1749
|
+
]);
|
|
1750
|
+
|
|
1751
|
+
function permissionLabel(option, t) {
|
|
1752
|
+
const builtIn = BUILT_IN_PERMISSION_LABELS.get(option.value);
|
|
1753
|
+
if (builtIn && (option.name === option.value || option.name === builtIn[1])) return t(builtIn[0]);
|
|
1754
|
+
return option.name || option.value;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
// 与宿主 settings.agentPreset 一致:内置预设没有自报名称时走官方词条。
|
|
1758
|
+
const BUILT_IN_PRESET_KEYS = {
|
|
1759
|
+
standard: { name: "presetStandardName", description: "presetStandardDescription" },
|
|
1760
|
+
ptc: { name: "presetPtcName", description: "presetPtcDescription" },
|
|
1761
|
+
minimal: { name: "presetMinimalName", description: "presetMinimalDescription" },
|
|
1762
|
+
cordis: { name: "presetCordisName", description: "presetCordisDescription" },
|
|
1763
|
+
};
|
|
1764
|
+
function presetDisplayText(preset, presetT) {
|
|
1765
|
+
const keys = BUILT_IN_PRESET_KEYS[preset.id];
|
|
1766
|
+
if (keys && (preset.name === undefined || preset.name === "" || preset.name === preset.id)) {
|
|
1767
|
+
return { name: presetT(keys.name), description: presetT(keys.description) };
|
|
1768
|
+
}
|
|
1769
|
+
return { name: preset.name || preset.id, description: preset.description };
|
|
1770
|
+
}
|
|
1771
|
+
function PlusIcon() {
|
|
1772
|
+
return h(IconPlusOutline, { size: 14 });
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
function formatProviderLabel(provider, defaultLabel, modelT) {
|
|
1776
|
+
if (provider === "deepseek-account") {
|
|
1777
|
+
const localized = modelT("provider.account");
|
|
1778
|
+
if (localized && localized !== "provider.account") return localized;
|
|
1779
|
+
}
|
|
1780
|
+
return defaultLabel;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
function orderProviders(left, right) {
|
|
1784
|
+
const leftRank = left === "deepseek-account" ? 0 : left === "deepseek-official" ? 1 : 2;
|
|
1785
|
+
const rightRank = right === "deepseek-account" ? 0 : right === "deepseek-official" ? 1 : 2;
|
|
1786
|
+
return leftRank - rightRank;
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
function renderModelOptionLabel(item) {
|
|
1790
|
+
const isOfficialFlash = (item.provider === "deepseek-account" || item.provider === "deepseek-official") && item.model === "deepseek-flash";
|
|
1791
|
+
if (!isOfficialFlash) return item.label;
|
|
1792
|
+
return React.createElement("span", { style: { display: "inline-flex", alignItems: "center", gap: 6 } },
|
|
1793
|
+
React.createElement("span", null, item.label),
|
|
1794
|
+
React.createElement(Tag, { color: "info" }, "1017")
|
|
1795
|
+
);
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
function AccountSettingsPicker(props) {
|
|
1799
|
+
const t = props.t || fallbackT;
|
|
1800
|
+
const modelT = props.modelT || ((key) => key);
|
|
1801
|
+
const permissionT = props.permissionT || ((key) => key);
|
|
1802
|
+
const presetT = props.presetT || ((key) => key);
|
|
1803
|
+
const value = props.value || {};
|
|
1804
|
+
const [addBusy, setAddBusy] = useState(false);
|
|
1805
|
+
const [hint, setHint] = useState("");
|
|
1806
|
+
const [confirmingFullAccess, setConfirmingFullAccess] = useState(false);
|
|
1807
|
+
const [fullAccessAcknowledged, setFullAccessAcknowledged] = useState(false);
|
|
1808
|
+
const items = props.workspaces || [];
|
|
1809
|
+
const providers = props.catalog || [];
|
|
1810
|
+
const permissions = props.permissions || [];
|
|
1811
|
+
const provider = value.provider || (value.assistant && value.assistant.provider) || "";
|
|
1812
|
+
const model = value.model || (value.assistant && value.assistant.model) || "";
|
|
1813
|
+
const effort = value.reasoningEffort || (value.assistant && value.assistant.reasoningEffort) || "";
|
|
1814
|
+
const cwd = value.cwd || "";
|
|
1815
|
+
const permission = value.permission || "";
|
|
1816
|
+
const privateAccess = value.privateAccess === "all" ? "all" : "approved";
|
|
1817
|
+
const sendMode = value.sendMode === "steer" ? "steer" : "queue";
|
|
1818
|
+
const workspace = items.find((item) => item.path === cwd);
|
|
1819
|
+
const modelGroups = providers.map((item) => {
|
|
1820
|
+
const providerName = formatProviderLabel(item.id, item.name || item.id, modelT);
|
|
1821
|
+
return {
|
|
1822
|
+
id: item.id,
|
|
1823
|
+
name: providerName,
|
|
1824
|
+
models: (item.models || []).map((entry) => ({
|
|
1825
|
+
value: item.id + "::" + entry.id,
|
|
1826
|
+
provider: item.id,
|
|
1827
|
+
providerName: providerName,
|
|
1828
|
+
model: entry.id,
|
|
1829
|
+
label: entry.name || entry.id,
|
|
1830
|
+
description: entry.description,
|
|
1831
|
+
reasoning: entry.reasoning,
|
|
1832
|
+
})),
|
|
1833
|
+
};
|
|
1834
|
+
}).filter((item) => item.models.length > 0).sort((left, right) => orderProviders(left.id, right.id));
|
|
1835
|
+
const models = modelGroups.flatMap((item) => item.models);
|
|
1836
|
+
const currentModel = models.find((item) => item.provider === provider && item.model === model);
|
|
1837
|
+
const reasoning = currentModel && currentModel.reasoning;
|
|
1838
|
+
const effectiveEffort = effort || (reasoning && reasoning.defaultEffort) || "";
|
|
1839
|
+
const efforts = reasoning
|
|
1840
|
+
? [
|
|
1841
|
+
...(reasoning.defaultEffort ? [] : [{ id: "", name: modelT("effort.providerDefault") }]),
|
|
1842
|
+
...((reasoning.efforts || []).map((item) => ({ id: item.id, name: item.name || item.id, description: item.description }))),
|
|
1843
|
+
]
|
|
1844
|
+
: [];
|
|
1845
|
+
const effortLabel = reasoning
|
|
1846
|
+
? ((efforts.find((item) => item.id === effectiveEffort) || {}).name || effectiveEffort || modelT("effort.providerDefault"))
|
|
1847
|
+
: "";
|
|
1848
|
+
const permissionOptions = permissions.map((item) => ({ ...item, label: permissionLabel(item, t) }));
|
|
1849
|
+
const currentPermission = permissionOptions.find((item) => item.value === permission);
|
|
1850
|
+
const update = (patch) => {
|
|
1851
|
+
setHint("");
|
|
1852
|
+
if (typeof props.onChange === "function") props.onChange(patch);
|
|
1853
|
+
};
|
|
1854
|
+
|
|
1855
|
+
const addWorkspace = (path) => {
|
|
1856
|
+
const next = (path || "").trim();
|
|
1857
|
+
if (!next) { setHint(t("error.chooseWorkspace")); return Promise.resolve(); }
|
|
1858
|
+
if (typeof props.createWorkspace !== "function") { setHint(t("error.workspaceUnavailable")); return Promise.resolve(); }
|
|
1859
|
+
setAddBusy(true);
|
|
1860
|
+
return Promise.resolve(props.createWorkspace({ path: next })).then((created) => {
|
|
1861
|
+
const cwdPath = (created && (created.path || created.cwd)) || next;
|
|
1862
|
+
update({ cwd: cwdPath });
|
|
1863
|
+
}).catch((error) => setHint((error && error.message) || t("error.addWorkspace")))
|
|
1864
|
+
.finally(() => setAddBusy(false));
|
|
1865
|
+
};
|
|
1866
|
+
const onAddWorkspace = () => {
|
|
1867
|
+
setHint("");
|
|
1868
|
+
if (typeof props.pickDirectory !== "function") {
|
|
1869
|
+
setHint(t("error.workspaceUnavailable"));
|
|
1870
|
+
return Promise.resolve();
|
|
1871
|
+
}
|
|
1872
|
+
setAddBusy(true);
|
|
1873
|
+
return Promise.resolve()
|
|
1874
|
+
.then(() => props.pickDirectory())
|
|
1875
|
+
.then((picked) => {
|
|
1876
|
+
if (typeof picked === "string" && picked.trim()) {
|
|
1877
|
+
return addWorkspace(picked);
|
|
1878
|
+
}
|
|
1879
|
+
})
|
|
1880
|
+
.catch((error) => {
|
|
1881
|
+
setHint(error && error.code === "workspace/picker-unavailable"
|
|
1882
|
+
? t("error.workspaceUnavailable")
|
|
1883
|
+
: (error && error.message) || t("error.addWorkspace"));
|
|
1884
|
+
})
|
|
1885
|
+
.finally(() => {
|
|
1886
|
+
setAddBusy(false);
|
|
1887
|
+
});
|
|
1888
|
+
};
|
|
1889
|
+
const selectPermission = (next) => {
|
|
1890
|
+
if (next === permission) return;
|
|
1891
|
+
if (next === "danger-full-access") {
|
|
1892
|
+
setFullAccessAcknowledged(false);
|
|
1893
|
+
setConfirmingFullAccess(true);
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
update({ permission: next });
|
|
1897
|
+
};
|
|
1898
|
+
const field = (key, label, picker, wide) => h("div", { key, className: "dfc-picker-field" + (wide ? " wide" : "") },
|
|
1899
|
+
h("span", { className: "dfc-picker-label" }, label),
|
|
1900
|
+
picker,
|
|
1901
|
+
);
|
|
1902
|
+
|
|
1903
|
+
return h("div", { className: "dfc-account-settings" + (props.compact ? " compact" : "") },
|
|
1904
|
+
field("workspace", props.workspaceLabel || t("account.workspace"), items.length === 0
|
|
1905
|
+
? h("div", null,
|
|
1906
|
+
h(Button, { variant: "primary", icon: h(PlusIcon), disabled: addBusy, onClick: onAddWorkspace }, t("composer.addWorkspace")),
|
|
1907
|
+
h("div", { className: "dfc-picker-note warning" }, t("composer.workspaceRequired")),
|
|
1908
|
+
)
|
|
1909
|
+
: h(Select, {
|
|
1910
|
+
className: "dfc-account-picker dfc-workspace-picker",
|
|
1911
|
+
value: cwd || undefined,
|
|
1912
|
+
placeholder: t("account.selectWorkspace"),
|
|
1913
|
+
"aria-label": props.workspaceLabel || t("account.workspace"),
|
|
1914
|
+
popupMatchSelectWidth: false,
|
|
1915
|
+
options: items.map((item) => ({ value: item.path, label: item.title || item.path })),
|
|
1916
|
+
footer: [{ value: "add-workspace", label: t("composer.addWorkspace"), disabled: addBusy }],
|
|
1917
|
+
onFooter: () => { onAddWorkspace(); },
|
|
1918
|
+
notFoundContent: t("composer.noWorkspaces"),
|
|
1919
|
+
onChange: (next) => update({ cwd: next }),
|
|
1920
|
+
})),
|
|
1921
|
+
field("agentPreset", presetT("nav"), h(Select, {
|
|
1922
|
+
className: "dfc-account-picker",
|
|
1923
|
+
value: value.agentPreset || undefined,
|
|
1924
|
+
"aria-label": presetT("nav"),
|
|
1925
|
+
options: (props.agentPresets || []).map((item) => {
|
|
1926
|
+
const copy = presetDisplayText(item, presetT);
|
|
1927
|
+
return {
|
|
1928
|
+
value: item.id,
|
|
1929
|
+
label: copy.name,
|
|
1930
|
+
disabled: !!item.broken,
|
|
1931
|
+
title: item.broken ? t("account.presetUnavailable") : copy.description,
|
|
1932
|
+
};
|
|
1933
|
+
}),
|
|
1934
|
+
onChange: (next) => update({ agentPreset: next }),
|
|
1935
|
+
})),
|
|
1936
|
+
h("div", { key: "model-effort", className: "dfc-model-effort" },
|
|
1937
|
+
field("model", modelT("menu.model"), h(Select, {
|
|
1938
|
+
className: "dfc-account-picker dfc-model-select",
|
|
1939
|
+
value: currentModel ? currentModel.value : undefined,
|
|
1940
|
+
placeholder: t("account.selectModel"),
|
|
1941
|
+
title: currentModel && currentModel.label,
|
|
1942
|
+
"aria-label": modelT("trigger.selectAria"),
|
|
1943
|
+
popupMatchSelectWidth: false,
|
|
1944
|
+
options: modelGroups.map((group) => ({
|
|
1945
|
+
label: group.name,
|
|
1946
|
+
options: group.models.map((item) => ({ value: item.value, label: renderModelOptionLabel(item) })),
|
|
1947
|
+
})),
|
|
1948
|
+
notFoundContent: modelT("empty.models"),
|
|
1949
|
+
onChange: (next) => {
|
|
1950
|
+
const item = models.find((entry) => entry.value === next);
|
|
1951
|
+
if (!item) return;
|
|
1952
|
+
update({ provider: item.provider, model: item.model, reasoningEffort: (item.reasoning && item.reasoning.defaultEffort) || "" });
|
|
1953
|
+
},
|
|
1954
|
+
})),
|
|
1955
|
+
reasoning && field("effort", modelT("menu.effort"), h(Select, {
|
|
1956
|
+
className: "dfc-account-picker",
|
|
1957
|
+
value: effectiveEffort,
|
|
1958
|
+
"aria-label": modelT("menu.effort"),
|
|
1959
|
+
options: efforts.map((item) => ({ value: item.id, label: item.name })),
|
|
1960
|
+
onChange: (next) => update({ provider, model, reasoningEffort: next }),
|
|
1961
|
+
})),
|
|
1962
|
+
),
|
|
1963
|
+
field("permission", t("composer.permission"), h(Select, {
|
|
1964
|
+
className: "dfc-account-picker",
|
|
1965
|
+
value: permission || undefined,
|
|
1966
|
+
placeholder: t("account.selectPermission"),
|
|
1967
|
+
"aria-label": t("composer.permission"),
|
|
1968
|
+
options: permissionOptions.map((item) => ({ value: item.value, label: item.label })),
|
|
1969
|
+
onChange: selectPermission,
|
|
1970
|
+
})),
|
|
1971
|
+
field("private", t("account.privateAccess"), h(Select, {
|
|
1972
|
+
className: "dfc-account-picker",
|
|
1973
|
+
value: privateAccess,
|
|
1974
|
+
"aria-label": t("account.privateAccess"),
|
|
1975
|
+
options: [
|
|
1976
|
+
{ value: "approved", label: t("account.privateApproved") },
|
|
1977
|
+
{ value: "all", label: t("account.privateAll") },
|
|
1978
|
+
],
|
|
1979
|
+
onChange: (next) => update({ privateAccess: next }),
|
|
1980
|
+
})),
|
|
1981
|
+
field("sendMode", t("account.sendMode"), h(Select, {
|
|
1982
|
+
className: "dfc-account-picker",
|
|
1983
|
+
value: sendMode,
|
|
1984
|
+
"aria-label": t("account.sendMode"),
|
|
1985
|
+
options: [
|
|
1986
|
+
{ value: "queue", label: t("account.sendQueue") },
|
|
1987
|
+
{ value: "steer", label: t("account.sendSteer") },
|
|
1988
|
+
],
|
|
1989
|
+
onChange: (next) => update({ sendMode: next }),
|
|
1990
|
+
})),
|
|
1991
|
+
h("div", { className: "dfc-picker-note" }, t("account.presetNote")),
|
|
1992
|
+
sendMode === "steer" && h("div", { className: "dfc-picker-note" }, t("account.sendSteerNote")),
|
|
1993
|
+
privateAccess === "all" && h("div", { className: "dfc-picker-note warning" }, t("settings.publicChatNotice")),
|
|
1994
|
+
hint && h("div", { className: "dfc-picker-note" }, hint),
|
|
1995
|
+
props.showAutoNameNote && h("div", { className: "dfc-picker-note" }, t("account.autoNameNote")),
|
|
1996
|
+
h(Modal, {
|
|
1997
|
+
open: confirmingFullAccess,
|
|
1998
|
+
layerOwner: "full-access",
|
|
1999
|
+
title: permissionT("confirm.title"),
|
|
2000
|
+
keyboard: false,
|
|
2001
|
+
maskClosable: false,
|
|
2002
|
+
okText: permissionT("confirm.enable"),
|
|
2003
|
+
cancelText: permissionT("confirm.cancel"),
|
|
2004
|
+
okButtonProps: { disabled: !fullAccessAcknowledged },
|
|
2005
|
+
onCancel: () => { setFullAccessAcknowledged(false); setConfirmingFullAccess(false); },
|
|
2006
|
+
onOk: () => {
|
|
2007
|
+
if (!fullAccessAcknowledged) return;
|
|
2008
|
+
update({ permission: "danger-full-access" });
|
|
2009
|
+
setFullAccessAcknowledged(false);
|
|
2010
|
+
setConfirmingFullAccess(false);
|
|
2011
|
+
},
|
|
2012
|
+
},
|
|
2013
|
+
h("p", null, permissionT("confirm.description")),
|
|
2014
|
+
h(Checkbox, { checked: fullAccessAcknowledged, onChange: (event) => setFullAccessAcknowledged(event.target.checked) }, permissionT("confirm.acknowledge")),
|
|
2015
|
+
),
|
|
2016
|
+
);
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// 与账号配置共用两列网格和 Ant 下拉,私聊与群聊分别保存。
|
|
2020
|
+
function CommandPermissionSettings({ value, onSave, t }) {
|
|
2021
|
+
const policy = value || { dm: { enabled: true, users: [] }, group: { enabled: true, users: [] } };
|
|
2022
|
+
return h("div", { className: "dfc-command-permissions dfc-account-settings" },
|
|
2023
|
+
...["dm", "group"].map(kind => h("div", { key: kind, className: "dfc-picker-field" },
|
|
2024
|
+
h("span", { className: "dfc-picker-label" }, t("command." + kind)),
|
|
2025
|
+
h(Select, {
|
|
2026
|
+
className: "dfc-account-picker",
|
|
2027
|
+
value: policy[kind].enabled ? "on" : "off",
|
|
2028
|
+
"aria-label": t("command." + kind),
|
|
2029
|
+
options: [
|
|
2030
|
+
{ value: "on", label: t("command.enabled") },
|
|
2031
|
+
{ value: "off", label: t("command.disabled") },
|
|
2032
|
+
],
|
|
2033
|
+
onChange: (next) => onSave({ dm: { enabled: policy.dm.enabled, users: [] }, group: { enabled: policy.group.enabled, users: [] }, [kind]: { enabled: next === "on", users: [] } }),
|
|
2034
|
+
}))));
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
function AccountConnectionCheck({ account, t }) {
|
|
2038
|
+
const diagnostics = account.diagnostics;
|
|
2039
|
+
return h("div", { className: "dfc-connection-check", role: "status", "aria-live": "polite" },
|
|
2040
|
+
h("strong", null, t("diagnostic.title")),
|
|
2041
|
+
h("div", null, t("connection.checkedAt") + ": ", h("time", { dateTime: diagnostics.checkedAt }, new Date(diagnostics.checkedAt).toLocaleString(t("settings.label") === "Feishu Channel" ? "en-US" : "zh-CN"))),
|
|
2042
|
+
diagnostics.checks.map(item => h("div", { className: "dfc-diagnostic-item", key: item.id },
|
|
2043
|
+
h("div", { className: "dfc-diagnostic-head" },
|
|
2044
|
+
h("strong", null, t("diagnostic." + item.id)),
|
|
2045
|
+
h(Tag, { color: item.status === "passed" ? "success" : item.status === "failed" ? "error" : "warning" }, t("diagnostic." + item.status)),
|
|
2046
|
+
),
|
|
2047
|
+
h("div", null, t("diagnostic." + item.reason)),
|
|
2048
|
+
h("small", null, [item.durationMs == null ? null : item.durationMs + " ms", item.httpStatus == null ? null : "HTTP " + item.httpStatus, item.platformCode == null ? null : "Code " + item.platformCode].filter(Boolean).join(" · ")),
|
|
2049
|
+
)),
|
|
2050
|
+
h("div", null, t(account.receiveConfigured === true ? "connection.receiveOn" : account.receiveConfigured === false ? "connection.receiveOff" : "connection.receiveUnknown")),
|
|
2051
|
+
h("div", null, t("diagnostic.scope")),
|
|
2052
|
+
);
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
function AccountInspector({ account, catalog, permissions, agentPresets, workspaces, createWorkspace, pickDirectory, modelT, permissionT, presetT, t, onAction, onSave, busy }) {
|
|
2056
|
+
const [modal, modalHolder] = Modal.useModal();
|
|
2057
|
+
const [draft, setDraft] = useState(account);
|
|
2058
|
+
const [note, setNote] = useState("");
|
|
2059
|
+
const [checkResult, setCheckResult] = useState(null);
|
|
2060
|
+
const [checkFailed, setCheckFailed] = useState(false);
|
|
2061
|
+
const [checking, setChecking] = useState(false);
|
|
2062
|
+
const [checkOpen, setCheckOpen] = useState(true);
|
|
2063
|
+
const checkRequest = useRef(0);
|
|
2064
|
+
const checkPending = useRef(false);
|
|
2065
|
+
const [secretVisible, setSecretVisible] = useState(false);
|
|
2066
|
+
const [secretValue, setSecretValue] = useState("");
|
|
2067
|
+
const [secretBusy, setSecretBusy] = useState(false);
|
|
2068
|
+
const [copied, setCopied] = useState("");
|
|
2069
|
+
useEffect(() => {
|
|
2070
|
+
setSecretVisible(false);
|
|
2071
|
+
setSecretValue("");
|
|
2072
|
+
setCopied("");
|
|
2073
|
+
}, [account.id]);
|
|
2074
|
+
const markCopied = (kind) => {
|
|
2075
|
+
setCopied(kind);
|
|
2076
|
+
window.setTimeout(() => setCopied((current) => current === kind ? "" : current), 1500);
|
|
2077
|
+
};
|
|
2078
|
+
const loadSecret = async () => {
|
|
2079
|
+
if (secretValue) return secretValue;
|
|
2080
|
+
setSecretBusy(true);
|
|
2081
|
+
try {
|
|
2082
|
+
const data = await api(`/accounts/${account.id}/secret`, { method: "POST", body: "{}" });
|
|
2083
|
+
if (data.ok && data.appSecret) {
|
|
2084
|
+
setSecretValue(data.appSecret);
|
|
2085
|
+
return data.appSecret;
|
|
2086
|
+
}
|
|
2087
|
+
} catch { /* keep hidden */ }
|
|
2088
|
+
finally { setSecretBusy(false); }
|
|
2089
|
+
return "";
|
|
2090
|
+
};
|
|
2091
|
+
const revealSecret = async () => {
|
|
2092
|
+
if (!account.hasAppSecret) return;
|
|
2093
|
+
if (secretVisible) { setSecretVisible(false); return; }
|
|
2094
|
+
const value = await loadSecret();
|
|
2095
|
+
if (value) setSecretVisible(true);
|
|
2096
|
+
};
|
|
2097
|
+
const copyAppId = async () => {
|
|
2098
|
+
if (await copyText(account.appId)) markCopied("appId");
|
|
2099
|
+
};
|
|
2100
|
+
const copySecret = async () => {
|
|
2101
|
+
const value = secretValue || await loadSecret();
|
|
2102
|
+
if (value && await copyText(value)) markCopied("secret");
|
|
2103
|
+
};
|
|
2104
|
+
useEffect(() => {
|
|
2105
|
+
checkRequest.current += 1;
|
|
2106
|
+
checkPending.current = false;
|
|
2107
|
+
setCheckResult(null);
|
|
2108
|
+
setCheckFailed(false);
|
|
2109
|
+
setChecking(false);
|
|
2110
|
+
setCheckOpen(true);
|
|
2111
|
+
return () => { checkRequest.current += 1; };
|
|
2112
|
+
}, [account.id]);
|
|
2113
|
+
const check = async () => {
|
|
2114
|
+
if (checkPending.current) return;
|
|
2115
|
+
checkPending.current = true;
|
|
2116
|
+
const request = ++checkRequest.current;
|
|
2117
|
+
setChecking(true);
|
|
2118
|
+
setCheckResult(null);
|
|
2119
|
+
setCheckFailed(false);
|
|
2120
|
+
try {
|
|
2121
|
+
const result = await onAction(account.id, "check");
|
|
2122
|
+
if (request === checkRequest.current) {
|
|
2123
|
+
if (result && result.account && result.diagnostics?.version === 1 && Array.isArray(result.diagnostics.checks) && result.diagnostics.checks.length) {
|
|
2124
|
+
setCheckResult({ ...result.account, diagnostics: result.diagnostics });
|
|
2125
|
+
setCheckOpen(true);
|
|
2126
|
+
}
|
|
2127
|
+
else if (result && result.account) setCheckFailed("connection.restartRequired");
|
|
2128
|
+
else setCheckFailed(true);
|
|
2129
|
+
}
|
|
2130
|
+
} catch {
|
|
2131
|
+
if (request === checkRequest.current) setCheckFailed(true);
|
|
2132
|
+
} finally {
|
|
2133
|
+
if (request === checkRequest.current) { checkPending.current = false; setChecking(false); }
|
|
2134
|
+
}
|
|
2135
|
+
};
|
|
2136
|
+
const saveSeq = useRef(0);
|
|
2137
|
+
useEffect(() => {
|
|
2138
|
+
saveSeq.current += 1;
|
|
2139
|
+
setDraft(account);
|
|
2140
|
+
setNote("");
|
|
2141
|
+
}, [account.id, account.cwd, account.permission, account.agentPreset, account.privateAccess, account.sendMode, JSON.stringify(account.commandPermissions), account.receiveEnabled, account.assistant && account.assistant.provider, account.assistant && account.assistant.model, account.assistant && account.assistant.reasoningEffort]);
|
|
2142
|
+
const save = (patch) => {
|
|
2143
|
+
const seq = ++saveSeq.current;
|
|
2144
|
+
const next = { ...draft, ...patch };
|
|
2145
|
+
setDraft(next);
|
|
2146
|
+
setNote("status.saving");
|
|
2147
|
+
const payload = {
|
|
2148
|
+
name: next.name,
|
|
2149
|
+
cwd: next.cwd,
|
|
2150
|
+
provider: next.assistant.provider,
|
|
2151
|
+
model: next.assistant.model,
|
|
2152
|
+
reasoningEffort: next.assistant.reasoningEffort || null,
|
|
2153
|
+
permission: next.permission,
|
|
2154
|
+
privateAccess: next.privateAccess,
|
|
2155
|
+
sendMode: next.sendMode,
|
|
2156
|
+
agentPreset: next.agentPreset,
|
|
2157
|
+
commandPermissions: next.commandPermissions,
|
|
2158
|
+
};
|
|
2159
|
+
return onSave(account.id, payload).then((ok) => {
|
|
2160
|
+
if (seq === saveSeq.current) setNote(ok ? "status.saved" : "error.save");
|
|
2161
|
+
return ok;
|
|
2162
|
+
});
|
|
2163
|
+
};
|
|
2164
|
+
const applySettings = (patch) => {
|
|
2165
|
+
const hasAssistant = Object.prototype.hasOwnProperty.call(patch, "provider") || Object.prototype.hasOwnProperty.call(patch, "model") || Object.prototype.hasOwnProperty.call(patch, "reasoningEffort");
|
|
2166
|
+
if (!hasAssistant) return save(patch);
|
|
2167
|
+
const assistant = {
|
|
2168
|
+
...(draft.assistant || {}),
|
|
2169
|
+
...(Object.prototype.hasOwnProperty.call(patch, "provider") ? { provider: patch.provider } : {}),
|
|
2170
|
+
...(Object.prototype.hasOwnProperty.call(patch, "model") ? { model: patch.model } : {}),
|
|
2171
|
+
...(Object.prototype.hasOwnProperty.call(patch, "reasoningEffort") ? { reasoningEffort: patch.reasoningEffort || undefined } : {}),
|
|
2172
|
+
};
|
|
2173
|
+
return save({ assistant });
|
|
2174
|
+
};
|
|
2175
|
+
return h("div", { className: "dfc-inspector" },
|
|
2176
|
+
modalHolder,
|
|
2177
|
+
h("div", { className: "dfc-inspector-head" },
|
|
2178
|
+
h(Logo, { id: account.platform }),
|
|
2179
|
+
h("div", { className: "dfc-inspector-head-copy" },
|
|
2180
|
+
h("div", { className: "dfc-inspector-title-row" },
|
|
2181
|
+
h("h3", { className: "dfc-inspector-title" }, accountLabel(account, t)),
|
|
2182
|
+
h("div", { className: account.connectionState === "connected" ? "dfc-inspector-status ok" : "dfc-inspector-status" }, h(Tag, {
|
|
2183
|
+
color: account.receiveConfigured === false ? "warning" : account.connectionState === "connected" ? "success" : "warning",
|
|
2184
|
+
bordered: false,
|
|
2185
|
+
}, t(account.receiveConfigured === false ? "connection.paused" : "connection." + (account.connectionState || "unknown")))),
|
|
2186
|
+
),
|
|
2187
|
+
h("div", { className: "dfc-account-id" }, account.id),
|
|
2188
|
+
),
|
|
2189
|
+
),
|
|
2190
|
+
h("div", { className: "dfc-form" },
|
|
2191
|
+
h("div", { className: "dfc-app-info" },
|
|
2192
|
+
h("div", { className: "dfc-app-info-title" }, t("app.info")),
|
|
2193
|
+
h("div", { className: "dfc-app-fields" },
|
|
2194
|
+
h("label", { className: "dfc-picker-field" },
|
|
2195
|
+
h("span", { className: "dfc-picker-label" }, t("app.id")),
|
|
2196
|
+
h("div", { className: "dfc-copy-row" },
|
|
2197
|
+
h(Input, { type: "text", value: account.appId || "", readOnly: true, placeholder: t("app.notSaved") }),
|
|
2198
|
+
h(Button, { size: "sm", variant: "outline", disabled: !account.appId, onClick: copyAppId }, t(copied === "appId" ? "app.copied" : "app.copy")),
|
|
2199
|
+
),
|
|
2200
|
+
),
|
|
2201
|
+
h("label", { className: "dfc-picker-field" },
|
|
2202
|
+
h("span", { className: "dfc-picker-label" }, t("app.secret")),
|
|
2203
|
+
h("div", { className: "dfc-copy-row" },
|
|
2204
|
+
h(Input, {
|
|
2205
|
+
type: secretVisible ? "text" : "password",
|
|
2206
|
+
value: secretVisible ? secretValue : (account.hasAppSecret ? "••••••••" : ""),
|
|
2207
|
+
readOnly: true,
|
|
2208
|
+
placeholder: t("app.notSaved"),
|
|
2209
|
+
}),
|
|
2210
|
+
h(Button, { size: "sm", variant: "outline", disabled: secretBusy || !account.hasAppSecret, onClick: revealSecret }, t(secretVisible ? "app.hide" : "app.reveal")),
|
|
2211
|
+
h(Button, { size: "sm", variant: "outline", disabled: secretBusy || !account.hasAppSecret, onClick: copySecret }, t(copied === "secret" ? "app.copied" : "app.copy")),
|
|
2212
|
+
),
|
|
2213
|
+
),
|
|
2214
|
+
),
|
|
2215
|
+
),
|
|
2216
|
+
h("div", { className: "dfc-picker-field" },
|
|
2217
|
+
h("span", { className: "dfc-picker-label" }, t("account.remark")),
|
|
2218
|
+
h(AccountRemarkEditor, { account, t, always: true, disabled: Boolean(busy), onSave: (name) => save({ name }) }),
|
|
2219
|
+
h("div", { className: "dfc-picker-note" }, t("account.remarkNote")),
|
|
2220
|
+
),
|
|
2221
|
+
h(AccountSettingsPicker, {
|
|
2222
|
+
value: draft,
|
|
2223
|
+
onChange: applySettings,
|
|
2224
|
+
catalog,
|
|
2225
|
+
permissions,
|
|
2226
|
+
agentPresets,
|
|
2227
|
+
workspaces,
|
|
2228
|
+
createWorkspace,
|
|
2229
|
+
pickDirectory,
|
|
2230
|
+
modelT,
|
|
2231
|
+
permissionT,
|
|
2232
|
+
presetT,
|
|
2233
|
+
workspaceLabel: t("account.currentWorkspace"),
|
|
2234
|
+
t,
|
|
2235
|
+
}),
|
|
2236
|
+
h(CommandPermissionSettings, { value: draft.commandPermissions, onSave: commandPermissions => save({ commandPermissions }), t }),
|
|
2237
|
+
note && h("div", { className: note === "status.saved" ? "dfc-save-note ok" : "dfc-save-note" }, t(note)),
|
|
2238
|
+
h("div", { className: "dfc-inspector-actions" },
|
|
2239
|
+
h(Button, { variant: "outline", disabled: Boolean(busy) || checking, onClick: check }, t(checking ? "connection.checking" : "action.checkConnection")),
|
|
2240
|
+
h(Button, { variant: "outline", disabled: Boolean(busy) || checking, onClick: () => { setCheckResult(null); return onAction(account.id, "reconnect"); } }, t("action.reconnectAccount")),
|
|
2241
|
+
h(Button, { variant: "outline", danger: true, disabled: Boolean(busy) || checking, onClick: () => {
|
|
2242
|
+
modal.confirm({
|
|
2243
|
+
title: t("action.removeAccount"),
|
|
2244
|
+
description: t("account.removeConfirm"),
|
|
2245
|
+
okText: t("action.removeAccount"),
|
|
2246
|
+
cancelText: t("action.cancel"),
|
|
2247
|
+
okButtonProps: { danger: true },
|
|
2248
|
+
onOk: () => onAction(account.id, "remove"),
|
|
2249
|
+
});
|
|
2250
|
+
} }, t("action.removeAccount")),
|
|
2251
|
+
),
|
|
2252
|
+
checkResult && h("div", { className: "dfc-check-section" },
|
|
2253
|
+
h("div", { className: "dfc-check-head" },
|
|
2254
|
+
h("span", { className: "dfc-check-title" }, t("connection.diagnostics")),
|
|
2255
|
+
h(Button, {
|
|
2256
|
+
size: "sm",
|
|
2257
|
+
variant: "ghost",
|
|
2258
|
+
className: "dfc-check-toggle",
|
|
2259
|
+
icon: h(checkOpen ? IconChevronDownOutline : IconChevronRightOutline, { size: 13 }),
|
|
2260
|
+
onClick: () => setCheckOpen((open) => !open),
|
|
2261
|
+
}, t(checkOpen ? "action.collapse" : "action.expand")),
|
|
2262
|
+
),
|
|
2263
|
+
checkOpen && h(AccountConnectionCheck, { account: checkResult, t }),
|
|
2264
|
+
),
|
|
2265
|
+
checkFailed && h(NoticeBar, { type: "error", role: "alert" }, t(typeof checkFailed === "string" ? checkFailed : "connection.checkFailed")),
|
|
2266
|
+
),
|
|
2267
|
+
);
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
function AccountRemarkEditor(props) {
|
|
2271
|
+
const account = props.account;
|
|
2272
|
+
const t = props.t;
|
|
2273
|
+
const display = accountLabel(account, t);
|
|
2274
|
+
const stored = account.autoName ? "" : (account.name || "");
|
|
2275
|
+
const [draft, setDraft] = useState(stored);
|
|
2276
|
+
const [editing, setEditing] = useState(Boolean(props.always));
|
|
2277
|
+
const skipCommit = useRef(false);
|
|
2278
|
+
useEffect(() => {
|
|
2279
|
+
setDraft(account.autoName ? "" : (account.name || ""));
|
|
2280
|
+
if (!props.always) setEditing(false);
|
|
2281
|
+
}, [account.id, account.name, account.autoName, props.always]);
|
|
2282
|
+
const commit = () => {
|
|
2283
|
+
if (skipCommit.current) {
|
|
2284
|
+
skipCommit.current = false;
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
const next = draft.trim();
|
|
2288
|
+
const current = account.autoName ? "" : (account.name || "");
|
|
2289
|
+
if (next === current) {
|
|
2290
|
+
if (!props.always) setEditing(false);
|
|
2291
|
+
return;
|
|
2292
|
+
}
|
|
2293
|
+
Promise.resolve(props.onSave(next)).then((ok) => {
|
|
2294
|
+
if (!ok) setDraft(current);
|
|
2295
|
+
if (!props.always) setEditing(false);
|
|
2296
|
+
});
|
|
2297
|
+
};
|
|
2298
|
+
if (!props.always && !editing) {
|
|
2299
|
+
return h("button", {
|
|
2300
|
+
type: "button",
|
|
2301
|
+
className: "dfc-account-name dfc-account-rename-button",
|
|
2302
|
+
title: t("account.rename"),
|
|
2303
|
+
"aria-label": display + " · " + t("account.rename"),
|
|
2304
|
+
disabled: Boolean(props.disabled),
|
|
2305
|
+
onClick: (event) => { event.stopPropagation(); setEditing(true); },
|
|
2306
|
+
}, display);
|
|
2307
|
+
}
|
|
2308
|
+
return h("input", {
|
|
2309
|
+
type: "text",
|
|
2310
|
+
className: props.always ? "dfc-account-remark-input" : "dfc-account-rename",
|
|
2311
|
+
autoFocus: !props.always,
|
|
2312
|
+
maxLength: 40,
|
|
2313
|
+
value: draft,
|
|
2314
|
+
placeholder: display,
|
|
2315
|
+
"aria-label": t("account.remark"),
|
|
2316
|
+
disabled: Boolean(props.disabled),
|
|
2317
|
+
onClick: (event) => event.stopPropagation(),
|
|
2318
|
+
onMouseDown: (event) => event.stopPropagation(),
|
|
2319
|
+
onChange: (event) => setDraft(event.target.value),
|
|
2320
|
+
onBlur: commit,
|
|
2321
|
+
onKeyDown: (event) => {
|
|
2322
|
+
if (event.key === "Enter") { event.preventDefault(); event.currentTarget.blur(); }
|
|
2323
|
+
if (event.key === "Escape" && !props.always) {
|
|
2324
|
+
event.preventDefault();
|
|
2325
|
+
event.stopPropagation();
|
|
2326
|
+
skipCommit.current = true;
|
|
2327
|
+
setDraft(stored);
|
|
2328
|
+
setEditing(false);
|
|
2329
|
+
}
|
|
2330
|
+
},
|
|
2331
|
+
});
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
function FeishuRemoteModelSelect({ sessionId, t }) {
|
|
2335
|
+
const accountId = accountIdOfSession(sessionId);
|
|
2336
|
+
const [accounts, setAccounts] = useState([]);
|
|
2337
|
+
useEffect(() => channelRefresh.subscribe((data) => {
|
|
2338
|
+
if (!data || !data.ok) return;
|
|
2339
|
+
setAccounts((data.channels || []).flatMap((channel) => channel.accounts || []));
|
|
2340
|
+
}), []);
|
|
2341
|
+
if (!accountId) return h("span", { className: "dfc-remote-model-missing" }, t("composer.modelUnavailable"));
|
|
2342
|
+
const account = accounts.find((item) => item.id === accountId);
|
|
2343
|
+
if (!account) return h("span", { className: "dfc-remote-model-missing" }, t("composer.modelLoading"));
|
|
2344
|
+
const assistant = account.assistant || {};
|
|
2345
|
+
const label = assistant.provider && assistant.model
|
|
2346
|
+
? String(assistant.provider) + " / " + String(assistant.model)
|
|
2347
|
+
: "";
|
|
2348
|
+
if (!label) return h("span", { className: "dfc-remote-model-missing" }, t("composer.modelUnavailable"));
|
|
2349
|
+
return h("div", { className: "dfc-remote-model", title: t("composer.modelReadOnly") },
|
|
2350
|
+
h("span", { className: "dfc-remote-model-label" }, t("composer.model")),
|
|
2351
|
+
h("span", { className: "dfc-remote-model-value" }, label),
|
|
2352
|
+
);
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
function FeishuStopButton({ ctx, sessionId, useSession, t }) {
|
|
2356
|
+
const running = useSession((state) => Boolean(state && state.running));
|
|
2357
|
+
const [modal, modalHolder] = Modal.useModal();
|
|
2358
|
+
if (!running) return null;
|
|
2359
|
+
const cancel = async () => {
|
|
2360
|
+
const scoped = ctx && ctx.sessions && typeof ctx.sessions.scope === "function"
|
|
2361
|
+
? ctx.sessions.scope(sessionId)
|
|
2362
|
+
: undefined;
|
|
2363
|
+
const conversation = scoped && typeof scoped.get === "function"
|
|
2364
|
+
? scoped.get("conversation")
|
|
2365
|
+
: undefined;
|
|
2366
|
+
if (conversation && typeof conversation.cancel === "function") {
|
|
2367
|
+
await conversation.cancel();
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2370
|
+
if (ctx && ctx.sessions && typeof ctx.sessions.cancel === "function") {
|
|
2371
|
+
await ctx.sessions.cancel(sessionId);
|
|
2372
|
+
return;
|
|
2373
|
+
}
|
|
2374
|
+
throw new Error("session cancel unavailable");
|
|
2375
|
+
};
|
|
2376
|
+
const requestStop = () => {
|
|
2377
|
+
modal.confirm({
|
|
2378
|
+
title: t("composer.stopTitle"),
|
|
2379
|
+
description: t("composer.stopDescription"),
|
|
2380
|
+
okText: t("composer.stopConfirm"),
|
|
2381
|
+
cancelText: t("action.cancel"),
|
|
2382
|
+
okButtonProps: { danger: true },
|
|
2383
|
+
onOk: async () => {
|
|
2384
|
+
try {
|
|
2385
|
+
await cancel();
|
|
2386
|
+
} catch (error) {
|
|
2387
|
+
console.warn("[dsh-feishu-channels] 无法停止推理", error);
|
|
2388
|
+
}
|
|
2389
|
+
},
|
|
2390
|
+
});
|
|
2391
|
+
};
|
|
2392
|
+
return h(React.Fragment, null,
|
|
2393
|
+
modalHolder,
|
|
2394
|
+
h(Button, {
|
|
2395
|
+
size: "sm",
|
|
2396
|
+
variant: "outline",
|
|
2397
|
+
className: "dfc-remote-stop",
|
|
2398
|
+
icon: h(IconStopFill, { size: 12 }),
|
|
2399
|
+
"aria-label": t("composer.stopConfirm"),
|
|
2400
|
+
title: t("composer.stopConfirm"),
|
|
2401
|
+
onClick: requestStop,
|
|
2402
|
+
}),
|
|
2403
|
+
);
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
function FeishuComposerGuard(props) {
|
|
2407
|
+
const t = props.t || fallbackT;
|
|
2408
|
+
return h("div", { className: "dfc-remote-composer", "data-dfc-composer-guard": "1", role: "group", "aria-label": t("composer.remoteTitle") },
|
|
2409
|
+
h("div", { className: "dfc-remote-composer-card" },
|
|
2410
|
+
h("div", { className: "dfc-remote-composer-icon", "aria-hidden": "true" }, h(BrandMark, { id: "feishu", compact: true, mono: true })),
|
|
2411
|
+
h("div", { className: "dfc-remote-composer-copy" },
|
|
2412
|
+
h("div", { className: "dfc-remote-composer-title" }, t("composer.remoteTitle")),
|
|
2413
|
+
h("div", { className: "dfc-remote-composer-hint" }, t("composer.remoteHint")),
|
|
2414
|
+
),
|
|
2415
|
+
h(FeishuRemoteModelSelect, { sessionId: props.sessionId, t }),
|
|
2416
|
+
typeof props.useSession === "function"
|
|
2417
|
+
? h(FeishuStopButton, { ctx: props.ctx, sessionId: props.sessionId, useSession: props.useSession, t })
|
|
2418
|
+
: null,
|
|
2419
|
+
),
|
|
2420
|
+
);
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
function FeishuChannelPage(props) {
|
|
2424
|
+
const t = props.t || fallbackT;
|
|
2425
|
+
const hostProps = props.hostProps || {};
|
|
2426
|
+
const ctx = props.ctx;
|
|
2427
|
+
const [data, setData] = useState(null);
|
|
2428
|
+
const [catalog, setCatalog] = useState({ providers: [], permissions: [], agentPresets: [], agentPreset: "standard", assistant: null, cwd: "", permission: "" });
|
|
2429
|
+
const [error, setError] = useState("");
|
|
2430
|
+
const [loading, setLoading] = useState(true);
|
|
2431
|
+
const [adding, setAdding] = useState(false);
|
|
2432
|
+
const [selectedId, setSelectedId] = useState("");
|
|
2433
|
+
const [busy, setBusy] = useState({});
|
|
2434
|
+
const refresh = useCallback(() => channelRefresh.invalidate().catch(() => undefined), []);
|
|
2435
|
+
useEffect(() => channelRefresh.subscribe((next, failure) => {
|
|
2436
|
+
if (failure) {
|
|
2437
|
+
setError(t("error.connection"));
|
|
2438
|
+
setLoading(false);
|
|
2439
|
+
return;
|
|
2440
|
+
}
|
|
2441
|
+
if (next && next.ok) {
|
|
2442
|
+
setData(next);
|
|
2443
|
+
setError("");
|
|
2444
|
+
setLoading(false);
|
|
2445
|
+
} else {
|
|
2446
|
+
setError(serverText(next && next.error, t) || t("error.load"));
|
|
2447
|
+
setLoading(false);
|
|
2448
|
+
}
|
|
2449
|
+
}), []);
|
|
2450
|
+
useEffect(() => {
|
|
2451
|
+
api("/assistant").then((next) => {
|
|
2452
|
+
if (!next.ok) return;
|
|
2453
|
+
setCatalog({
|
|
2454
|
+
providers: next.providers || [],
|
|
2455
|
+
permissions: next.permissions || [],
|
|
2456
|
+
agentPresets: next.agentPresets || [],
|
|
2457
|
+
agentPreset: next.agentPreset || "standard",
|
|
2458
|
+
assistant: next.assistant || null,
|
|
2459
|
+
cwd: next.cwd || "",
|
|
2460
|
+
permission: next.permission || "",
|
|
2461
|
+
});
|
|
2462
|
+
}).catch(() => undefined);
|
|
2463
|
+
}, []);
|
|
2464
|
+
const useWorkspacesHook = typeof hostProps.useWorkspaces === "function" ? hostProps.useWorkspaces : null;
|
|
2465
|
+
const workspaces = useWorkspacesHook
|
|
2466
|
+
? (useWorkspacesHook((state) => (state && state.items) || []) || [])
|
|
2467
|
+
: [];
|
|
2468
|
+
const useSessionsHook = typeof hostProps.useSessions === "function" ? hostProps.useSessions : null;
|
|
2469
|
+
const hostCurrentSessionId = useSessionsHook
|
|
2470
|
+
? (useSessionsHook((state) => currentSessionId(state)) || "")
|
|
2471
|
+
: "";
|
|
2472
|
+
const activeSessionId = typeof props.currentSessionId === "string" && props.currentSessionId
|
|
2473
|
+
? props.currentSessionId
|
|
2474
|
+
: hostCurrentSessionId;
|
|
2475
|
+
const createWorkspace = (input) => (ctx && ctx.workspaces && typeof ctx.workspaces.create === "function"
|
|
2476
|
+
? ctx.workspaces.create(input)
|
|
2477
|
+
: Promise.reject(new Error("workspace unavailable")));
|
|
2478
|
+
const pickDirectory = () => pickHostDirectory(ctx);
|
|
2479
|
+
const readMainSessionModelValue = () => readMainSessionModel(ctx);
|
|
2480
|
+
const channels = (data && data.channels) || [];
|
|
2481
|
+
const ch = channels.find((item) => item.id === "feishu") || {
|
|
2482
|
+
id: "feishu",
|
|
2483
|
+
label: "飞书",
|
|
2484
|
+
description: "",
|
|
2485
|
+
kind: "qr-or-credentials",
|
|
2486
|
+
fields: [],
|
|
2487
|
+
accounts: [],
|
|
2488
|
+
};
|
|
2489
|
+
const accounts = Array.isArray(ch.accounts) ? ch.accounts : [];
|
|
2490
|
+
const groups = (data && data.groups) || [];
|
|
2491
|
+
const sessions = groups.flatMap((group) => (group && group.sessions) || []);
|
|
2492
|
+
const pending = (data && data.pending) || [];
|
|
2493
|
+
const selectedAccount = accounts.find((account) => account.id === selectedId) || null;
|
|
2494
|
+
const sessionsFor = (account) => sessions
|
|
2495
|
+
.filter((session) => String(session.channel || "") === account.id)
|
|
2496
|
+
.sort((a, b) => (Date.parse(b.updatedAt || "") || 0) - (Date.parse(a.updatedAt || "") || 0));
|
|
2497
|
+
const openSession = (sessionId) => {
|
|
2498
|
+
if (!sessionId) return;
|
|
2499
|
+
const hostOpen = hostProps.openSession || hostProps.open;
|
|
2500
|
+
if (typeof hostOpen === "function") {
|
|
2501
|
+
hostOpen(sessionId);
|
|
2502
|
+
return;
|
|
2503
|
+
}
|
|
2504
|
+
openHostSession(ctx, sessionId);
|
|
2505
|
+
};
|
|
2506
|
+
const onAction = (id, action, body) => {
|
|
2507
|
+
setBusy((prev) => ({ ...prev, [id]: true }));
|
|
2508
|
+
return api("/accounts/" + id + "/" + action, {
|
|
2509
|
+
method: "POST",
|
|
2510
|
+
headers: { "content-type": "application/json" },
|
|
2511
|
+
body: JSON.stringify(body || {}),
|
|
2512
|
+
}).then((result) => {
|
|
2513
|
+
if (!result.ok) {
|
|
2514
|
+
setError(serverText(result.error, t) || t("error.action"));
|
|
2515
|
+
return false;
|
|
2516
|
+
}
|
|
2517
|
+
setError("");
|
|
2518
|
+
if (action === "remove" && selectedId === id) setSelectedId("");
|
|
2519
|
+
refresh();
|
|
2520
|
+
return action === "check" ? result : true;
|
|
2521
|
+
}).catch(() => {
|
|
2522
|
+
setError(t("error.request"));
|
|
2523
|
+
return false;
|
|
2524
|
+
}).finally(() => setBusy((prev) => ({ ...prev, [id]: false })));
|
|
2525
|
+
};
|
|
2526
|
+
const saveAccount = (id, body) => onAction(id, "settings", body);
|
|
2527
|
+
const toggleReceive = (account) => onAction(account.id, "receive", { receiveEnabled: !account.receiveConfigured });
|
|
2528
|
+
const approve = (item, allow) => onAction(item.channelId, allow ? "approve" : "deny", { userId: item.userId });
|
|
2529
|
+
|
|
2530
|
+
return h("section", { className: "dfc-channel-page", "aria-label": t("settings.label") },
|
|
2531
|
+
h("div", { className: "dfc-channel-head" },
|
|
2532
|
+
h(Button, {
|
|
2533
|
+
variant: "ghost",
|
|
2534
|
+
size: "sm",
|
|
2535
|
+
className: "dfc-channel-add",
|
|
2536
|
+
icon: h(IconPlusOutline, { size: 13 }),
|
|
2537
|
+
disabled: !data,
|
|
2538
|
+
onClick: () => setAdding(true),
|
|
2539
|
+
}, t("action.addAccount")),
|
|
2540
|
+
),
|
|
2541
|
+
error && h(NoticeBar, { type: "error", role: "alert" }, error),
|
|
2542
|
+
pending.length > 0 && h("div", { className: "dfc-channel-pending", role: "region" },
|
|
2543
|
+
h("div", { className: "dfc-channel-pending-title" }, t("pending.notice")),
|
|
2544
|
+
...pending.map((item) => h("div", { key: item.channelId + item.userId, className: "dfc-channel-pending-row" },
|
|
2545
|
+
h("span", { className: "dfc-channel-pending-user" }, (item.username || item.userId) + " · " + (accountLabel(accounts.find((account) => account.id === item.channelId), t) || item.channelId)),
|
|
2546
|
+
h(Button, { size: "sm", variant: "primary", onClick: () => approve(item, true) }, t("action.approve")),
|
|
2547
|
+
h(Button, { size: "sm", variant: "outline", danger: true, onClick: () => approve(item, false) }, t("action.deny")),
|
|
2548
|
+
)),
|
|
2549
|
+
),
|
|
2550
|
+
loading && !data
|
|
2551
|
+
? h("div", { className: "dfc-channel-empty" }, t("loading"))
|
|
2552
|
+
: accounts.length === 0
|
|
2553
|
+
? h("div", { className: "dfc-channel-empty" }, t("channel.empty"))
|
|
2554
|
+
: h("div", { className: "dfc-channel-accounts" },
|
|
2555
|
+
...accounts.map((account) => {
|
|
2556
|
+
const accountSessions = sessionsFor(account);
|
|
2557
|
+
const accountStatus = account.receiveConfigured === false ? "paused" : (account.connectionState || "unknown");
|
|
2558
|
+
return h("div", { key: account.id, className: "dfc-channel-account" },
|
|
2559
|
+
h("div", { className: "dfc-channel-account-head" },
|
|
2560
|
+
h(Logo, { id: "feishu", small: true }),
|
|
2561
|
+
h("div", { className: "dfc-channel-account-copy" },
|
|
2562
|
+
h("div", { className: "dfc-channel-account-name" }, accountLabel(account, t) || account.name || account.id),
|
|
2563
|
+
),
|
|
2564
|
+
h(Tag, {
|
|
2565
|
+
color: account.receiveConfigured === false ? "warning" : account.connectionState === "connected" ? "success" : "warning",
|
|
2566
|
+
bordered: false,
|
|
2567
|
+
}, t("connection." + accountStatus)),
|
|
2568
|
+
h("div", { className: "dfc-channel-account-actions" },
|
|
2569
|
+
h(Button, {
|
|
2570
|
+
size: "sm",
|
|
2571
|
+
variant: "ghost",
|
|
2572
|
+
className: "dfc-channel-settings",
|
|
2573
|
+
icon: h(IconSettingsOutline16, { size: 14 }),
|
|
2574
|
+
"aria-label": t("action.settings"),
|
|
2575
|
+
title: t("action.settings"),
|
|
2576
|
+
disabled: Boolean(busy[account.id]),
|
|
2577
|
+
onClick: () => setSelectedId(account.id),
|
|
2578
|
+
}),
|
|
2579
|
+
),
|
|
2580
|
+
),
|
|
2581
|
+
accountSessions.length > 0
|
|
2582
|
+
? h("div", { className: "dfc-channel-sessions" },
|
|
2583
|
+
...accountSessions.map((session) => {
|
|
2584
|
+
const current = Boolean(activeSessionId) && String(session.sessionId) === String(activeSessionId);
|
|
2585
|
+
return h("button", {
|
|
2586
|
+
key: session.sessionId,
|
|
2587
|
+
type: "button",
|
|
2588
|
+
className: "dfc-channel-session" + (current ? " is-current" : ""),
|
|
2589
|
+
title: session.title || t("session.untitled"),
|
|
2590
|
+
"aria-current": current ? "true" : undefined,
|
|
2591
|
+
onClick: () => openSession(session.sessionId),
|
|
2592
|
+
},
|
|
2593
|
+
h("span", { className: "dfc-channel-session-title" }, session.title || t("session.untitled")),
|
|
2594
|
+
h("span", { className: "dfc-channel-session-time" }, relativeTime(session.updatedAt, t)),
|
|
2595
|
+
);
|
|
2596
|
+
}),
|
|
2597
|
+
)
|
|
2598
|
+
: h("div", { className: "dfc-channel-account-empty" }, t("account.noSessions")),
|
|
2599
|
+
);
|
|
2600
|
+
}),
|
|
2601
|
+
),
|
|
2602
|
+
adding && h(BindModal, {
|
|
2603
|
+
ch,
|
|
2604
|
+
catalog: catalog.providers,
|
|
2605
|
+
permissions: catalog.permissions,
|
|
2606
|
+
agentPresets: catalog.agentPresets,
|
|
2607
|
+
workspaces,
|
|
2608
|
+
createWorkspace,
|
|
2609
|
+
pickDirectory,
|
|
2610
|
+
modelT: props.modelT,
|
|
2611
|
+
permissionT: props.permissionT,
|
|
2612
|
+
presetT: props.presetT,
|
|
2613
|
+
defaults: catalog,
|
|
2614
|
+
readMainSessionModel: readMainSessionModelValue,
|
|
2615
|
+
onClose: () => setAdding(false),
|
|
2616
|
+
onConnected: () => { setAdding(false); refresh(); },
|
|
2617
|
+
t,
|
|
2618
|
+
}),
|
|
2619
|
+
selectedAccount && h(Modal, {
|
|
2620
|
+
open: true,
|
|
2621
|
+
className: "dfc-account-modal",
|
|
2622
|
+
title: t("action.settings"),
|
|
2623
|
+
width: 760,
|
|
2624
|
+
zIndex: 1100,
|
|
2625
|
+
layerOwner: "feishu-account",
|
|
2626
|
+
destroyOnHidden: true,
|
|
2627
|
+
onCancel: () => setSelectedId(""),
|
|
2628
|
+
footer: null,
|
|
2629
|
+
}, h(AccountInspector, {
|
|
2630
|
+
account: selectedAccount,
|
|
2631
|
+
busy: Boolean(busy[selectedAccount.id]),
|
|
2632
|
+
catalog: catalog.providers,
|
|
2633
|
+
permissions: catalog.permissions,
|
|
2634
|
+
agentPresets: catalog.agentPresets,
|
|
2635
|
+
workspaces,
|
|
2636
|
+
createWorkspace,
|
|
2637
|
+
pickDirectory,
|
|
2638
|
+
modelT: props.modelT,
|
|
2639
|
+
permissionT: props.permissionT,
|
|
2640
|
+
presetT: props.presetT,
|
|
2641
|
+
t,
|
|
2642
|
+
onAction,
|
|
2643
|
+
onSave: saveAccount,
|
|
2644
|
+
})),
|
|
2645
|
+
);
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
function SettingsPage(props) {
|
|
2649
|
+
const t = props.t || fallbackT;
|
|
2650
|
+
const [channels, setChannels] = useState(null);
|
|
2651
|
+
const [pending, setPending] = useState([]);
|
|
2652
|
+
const [error, setError] = useState("");
|
|
2653
|
+
const [busy, setBusy] = useState({});
|
|
2654
|
+
const [editing, setEditing] = useState(null);
|
|
2655
|
+
const [selected, setSelected] = useState(storedAccountSelection);
|
|
2656
|
+
const [settingsAccount, setSettingsAccount] = useState(null);
|
|
2657
|
+
const [expanded, setExpanded] = useState({});
|
|
2658
|
+
const [channelSettingsRequest, setChannelSettingsRequest] = useState(() => typeof window !== "undefined" ? readChannelSettingsRequest(window.sessionStorage) : undefined);
|
|
2659
|
+
const [catalog, setCatalog] = useState({ providers: [], permissions: [], assistant: null, cwd: "", permission: "" });
|
|
2660
|
+
const workspaces = props.useWorkspaces ? (props.useWorkspaces((state) => state && state.items || []) || []) : [];
|
|
2661
|
+
const selectAccount = useCallback((id) => {
|
|
2662
|
+
const next = id || "";
|
|
2663
|
+
setSelected(next);
|
|
2664
|
+
rememberAccountSelection(next);
|
|
2665
|
+
}, []);
|
|
2666
|
+
|
|
2667
|
+
const refresh = useCallback(() => {
|
|
2668
|
+
return channelRefresh.invalidate().catch(() => undefined);
|
|
2669
|
+
}, []);
|
|
2670
|
+
|
|
2671
|
+
useEffect(() => channelRefresh.subscribe((data, error) => {
|
|
2672
|
+
if (error) { setError(t("error.connection")); return; }
|
|
2673
|
+
if (data.ok) {
|
|
2674
|
+
setChannels(data.channels);
|
|
2675
|
+
setPending(data.pending || []);
|
|
2676
|
+
setError("");
|
|
2677
|
+
const all = (data.channels || []).flatMap((channel) => channel.accounts || []);
|
|
2678
|
+
setSelected((current) => {
|
|
2679
|
+
const next = all.some((item) => item.id === current) ? current : "";
|
|
2680
|
+
if (next !== current) rememberAccountSelection(next);
|
|
2681
|
+
return next;
|
|
2682
|
+
});
|
|
2683
|
+
} else setError(serverText(data.error, t) || t("error.load"));
|
|
2684
|
+
}), []);
|
|
2685
|
+
|
|
2686
|
+
useEffect(() => {
|
|
2687
|
+
ensureStyle();
|
|
2688
|
+
api("/assistant").then((data) => {
|
|
2689
|
+
if (data.ok) setCatalog({ providers: data.providers || [], permissions: data.permissions || [], agentPresets: data.agentPresets || [], agentPreset: data.agentPreset || "standard", assistant: data.assistant || null, cwd: data.cwd || "", permission: data.permission || "" });
|
|
2690
|
+
}).catch(() => undefined);
|
|
2691
|
+
}, [refresh]);
|
|
2692
|
+
useEffect(() => {
|
|
2693
|
+
if (!settingsAccount) return undefined;
|
|
2694
|
+
const onKey = (event) => {
|
|
2695
|
+
if (modalLayers.length && modalLayers[modalLayers.length - 1].owner !== "settings-account") return;
|
|
2696
|
+
if (event.key !== "Escape") return;
|
|
2697
|
+
if (document.querySelector("[role='listbox'], [role='menu'], .dfc-menu-select-dropdown")) return;
|
|
2698
|
+
event.preventDefault();
|
|
2699
|
+
event.stopPropagation();
|
|
2700
|
+
if (typeof event.stopImmediatePropagation === "function") event.stopImmediatePropagation();
|
|
2701
|
+
setSettingsAccount(null);
|
|
2702
|
+
};
|
|
2703
|
+
window.addEventListener("keydown", onKey, true);
|
|
2704
|
+
return () => window.removeEventListener("keydown", onKey, true);
|
|
2705
|
+
}, [settingsAccount]);
|
|
2706
|
+
useEffect(() => {
|
|
2707
|
+
if (typeof window === "undefined") return undefined;
|
|
2708
|
+
const onRequest = (event) => {
|
|
2709
|
+
const request = parseChannelSettingsRequest(event.detail);
|
|
2710
|
+
if (request) setChannelSettingsRequest(request);
|
|
2711
|
+
};
|
|
2712
|
+
window.addEventListener(CHANNEL_SETTINGS_EVENT, onRequest);
|
|
2713
|
+
return () => window.removeEventListener(CHANNEL_SETTINGS_EVENT, onRequest);
|
|
2714
|
+
}, []);
|
|
2715
|
+
useEffect(() => {
|
|
2716
|
+
if (!channelSettingsRequest || channels == null) return;
|
|
2717
|
+
const target = resolveChannelSettingsTarget(channelSettingsRequest, channels);
|
|
2718
|
+
clearChannelSettingsRequest(typeof window !== "undefined" ? window.sessionStorage : undefined);
|
|
2719
|
+
setChannelSettingsRequest(undefined);
|
|
2720
|
+
if (!target) return;
|
|
2721
|
+
setExpanded((current) => Object.assign({}, current, { [target.channelId]: true }));
|
|
2722
|
+
if (target.accountId) {
|
|
2723
|
+
selectAccount(target.accountId);
|
|
2724
|
+
setSettingsAccount(target.accountId);
|
|
2725
|
+
}
|
|
2726
|
+
if (typeof document !== "undefined") {
|
|
2727
|
+
const node = document.querySelector('[data-dfc-platform="' + target.channelId + '"]');
|
|
2728
|
+
if (node && typeof node.scrollIntoView === "function") node.scrollIntoView({ block: "nearest" });
|
|
2729
|
+
}
|
|
2730
|
+
}, [channelSettingsRequest, channels, selectAccount]);
|
|
2731
|
+
|
|
2732
|
+
const onAction = (id, action, body) => {
|
|
2733
|
+
const removalFallback = action === "remove"
|
|
2734
|
+
? (() => {
|
|
2735
|
+
const owner = (channels || []).find((channel) => (channel.accounts || []).some((account) => account.id === id));
|
|
2736
|
+
const ownerAccounts = owner && owner.accounts || [];
|
|
2737
|
+
const removedIndex = ownerAccounts.findIndex((account) => account.id === id);
|
|
2738
|
+
const adjacent = ownerAccounts[removedIndex + 1] || ownerAccounts[removedIndex - 1];
|
|
2739
|
+
if (adjacent) return adjacent.id;
|
|
2740
|
+
return (channels || []).flatMap((channel) => channel.accounts || []).find((account) => account.id !== id)?.id || "";
|
|
2741
|
+
})()
|
|
2742
|
+
: "";
|
|
2743
|
+
setBusy((prev) => ({ ...prev, [id]: true }));
|
|
2744
|
+
return api("/accounts/" + id + "/" + action, {
|
|
2745
|
+
method: "POST",
|
|
2746
|
+
headers: { "content-type": "application/json" },
|
|
2747
|
+
body: JSON.stringify(body || {}),
|
|
2748
|
+
}).then((data) => {
|
|
2749
|
+
if (!data.ok) { setError(serverText(data.error, t) || t("error.action")); return false; }
|
|
2750
|
+
setError("");
|
|
2751
|
+
if (action === "remove" && selected === id) selectAccount(removalFallback);
|
|
2752
|
+
refresh();
|
|
2753
|
+
return action === "check" ? data : true;
|
|
2754
|
+
}).catch(() => { setError(t("error.request")); return false; }).finally(() => setBusy((prev) => ({ ...prev, [id]: false })));
|
|
2755
|
+
};
|
|
2756
|
+
const saveAccount = (id, body) => onAction(id, "settings", body);
|
|
2757
|
+
const allAccounts = (channels || []).flatMap((channel) => channel.accounts || []);
|
|
2758
|
+
const selectedAccount = allAccounts.find((item) => item.id === settingsAccount);
|
|
2759
|
+
|
|
2760
|
+
return h("section", { className: "dfc-page dfc-account-page", "aria-label": t("settings.aria") },
|
|
2761
|
+
h("header", { className: "dfc-head" },
|
|
2762
|
+
h("div", { className: "dfc-heading" },
|
|
2763
|
+
h("div", { className: "dfc-title-row" },
|
|
2764
|
+
h("h2", { className: "dfc-title" }, t("settings.title")),
|
|
2765
|
+
),
|
|
2766
|
+
h("p", { className: "dfc-sub" }, t("settings.description")),
|
|
2767
|
+
),
|
|
2768
|
+
),
|
|
2769
|
+
error && h(NoticeBar, { type: "error", className: "dfc-error" }, error),
|
|
2770
|
+
pending.length > 0 && h("div", { className: "dfc-feedback dfc-feedback-warning dfc-pending", role: "region" },
|
|
2771
|
+
h("div", { className: "dfc-feedback-head" },
|
|
2772
|
+
h("span", { className: "dfc-feedback-icon" }, alertIcon("warning")),
|
|
2773
|
+
h("span", { className: "dfc-feedback-title" }, t("pending.notice")),
|
|
2774
|
+
),
|
|
2775
|
+
h("div", { className: "dfc-pending-list" },
|
|
2776
|
+
...pending.map((p) => h("div", { key: p.channelId + p.userId, className: "dfc-pending-row" },
|
|
2777
|
+
h("span", { style: { flex: 1 } }, (p.username || p.userId) + " · " + (accountLabel(allAccounts.find((item) => item.id === p.channelId), t) || p.channelId)),
|
|
2778
|
+
h(Button, { variant: "primary", size: "sm", onClick: () => onAction(p.channelId, "approve", { userId: p.userId }) }, t("action.approve")),
|
|
2779
|
+
h(Button, { variant: "outline", size: "sm", danger: true, onClick: () => onAction(p.channelId, "deny", { userId: p.userId }) }, t("action.deny")),
|
|
2780
|
+
)),
|
|
2781
|
+
),
|
|
2782
|
+
),
|
|
2783
|
+
channels == null
|
|
2784
|
+
? h("div", { className: "dfc-empty" }, t("loading"))
|
|
2785
|
+
: h("div", { className: "dfc-account-shell" },
|
|
2786
|
+
h("div", { className: "dfc-platforms" },
|
|
2787
|
+
...channels.map((ch) => {
|
|
2788
|
+
const canExpand = (ch.accounts || []).length > 0;
|
|
2789
|
+
const open = canExpand && Boolean(expanded[ch.id]);
|
|
2790
|
+
const toggleExpanded = canExpand ? () => setExpanded({ ...expanded, [ch.id]: !open }) : undefined;
|
|
2791
|
+
return h("div", { key: ch.id, className: "dfc-platform" + (open ? " open" : "") + (canExpand ? "" : " empty"), "data-dfc-platform": ch.id },
|
|
2792
|
+
h("div", { className: "dfc-platform-head", role: canExpand ? "button" : undefined, tabIndex: canExpand ? 0 : undefined, "aria-expanded": canExpand ? open : undefined, onClick: toggleExpanded, onKeyDown: canExpand ? (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggleExpanded(); } } : undefined },
|
|
2793
|
+
h(Logo, { id: ch.id }),
|
|
2794
|
+
h("span", { className: "dfc-platform-title" }, channelLabel(ch, t)),
|
|
2795
|
+
h("div", { className: "dfc-platform-meta" },
|
|
2796
|
+
h("span", { className: "dfc-platform-count" }, t("account.count", { count: ch.total })),
|
|
2797
|
+
h(Button, { onClick: (e) => { e.stopPropagation(); setEditing(ch.id); } }, t("action.addAccount")),
|
|
2798
|
+
h("span", { className: canExpand ? "dfc-platform-caret" : "dfc-platform-caret empty", "aria-hidden": "true" }, canExpand && h(IconChevron)),
|
|
2799
|
+
),
|
|
2800
|
+
),
|
|
2801
|
+
open && h("div", { className: "dfc-account-list" },
|
|
2802
|
+
...(ch.accounts || []).map((account) => h("div", { key: account.id, className: "dfc-account-row" },
|
|
2803
|
+
h("span", { className: "dfc-account-copy" }, h(AccountRemarkEditor, { account, t, disabled: Boolean(busy[account.id]), onSave: (name) => onAction(account.id, "settings", { name }) })),
|
|
2804
|
+
h(Tag, {
|
|
2805
|
+
color: busy[account.id] ? "processing" : account.receiveConfigured === false ? "warning" : account.connectionState === "connected" ? "success" : "warning",
|
|
2806
|
+
bordered: false,
|
|
2807
|
+
className: "dfc-account-status-tag" + (busy[account.id] ? " is-busy" : ""),
|
|
2808
|
+
icon: busy[account.id] ? createSpinner() : undefined,
|
|
2809
|
+
}, busy[account.id] ? t("account.statusProcessing") : t(account.receiveConfigured === false ? "connection.paused" : "connection." + (account.connectionState || "unknown"))),
|
|
2810
|
+
h("div", { className: "dfc-account-receive", title: t("account.receiveDescription") },
|
|
2811
|
+
h("span", null, t("account.receive")),
|
|
2812
|
+
h(Switch, { role: "switch", checked: account.receiveConfigured === true, "aria-checked": Boolean(account.receiveConfigured === true), "aria-label": accountLabel(account, t) + " · " + t("account.receive"), title: account.receiveConfigured === undefined ? t("connection.receiveUnknown") : t("account.receive"), disabled: Boolean(busy[account.id]) || account.receiveConfigured === undefined, onChange: (checked) => onAction(account.id, "receive", { receiveEnabled: checked }) })),
|
|
2813
|
+
h(Button, { onClick: () => { selectAccount(account.id); setSettingsAccount(account.id); } }, t("action.settings")),
|
|
2814
|
+
)),
|
|
2815
|
+
),
|
|
2816
|
+
);
|
|
2817
|
+
}),
|
|
2818
|
+
),
|
|
2819
|
+
),
|
|
2820
|
+
selectedAccount && h(Modal, {
|
|
2821
|
+
open: true,
|
|
2822
|
+
className: "dfc-account-modal",
|
|
2823
|
+
title: t("action.settings"),
|
|
2824
|
+
width: 720,
|
|
2825
|
+
zIndex: 1100,
|
|
2826
|
+
layerOwner: "settings-account",
|
|
2827
|
+
destroyOnHidden: true,
|
|
2828
|
+
onCancel: () => setSettingsAccount(null),
|
|
2829
|
+
footer: null,
|
|
2830
|
+
},
|
|
2831
|
+
h(AccountInspector, {
|
|
2832
|
+
account: selectedAccount,
|
|
2833
|
+
busy: busy[selectedAccount.id],
|
|
2834
|
+
catalog: catalog.providers,
|
|
2835
|
+
permissions: catalog.permissions,
|
|
2836
|
+
agentPresets: catalog.agentPresets,
|
|
2837
|
+
workspaces,
|
|
2838
|
+
createWorkspace: props.createWorkspace,
|
|
2839
|
+
pickDirectory: props.pickDirectory,
|
|
2840
|
+
modelT: props.modelT,
|
|
2841
|
+
permissionT: props.permissionT,
|
|
2842
|
+
presetT: props.presetT,
|
|
2843
|
+
t,
|
|
2844
|
+
onAction,
|
|
2845
|
+
onSave: saveAccount,
|
|
2846
|
+
}),
|
|
2847
|
+
),
|
|
2848
|
+
editing && h(BindModal, {
|
|
2849
|
+
ch: (channels || []).find((item) => item.id === editing) || { id: editing, label: editing, kind: "qr", fields: [] },
|
|
2850
|
+
catalog: catalog.providers,
|
|
2851
|
+
permissions: catalog.permissions,
|
|
2852
|
+
agentPresets: catalog.agentPresets,
|
|
2853
|
+
workspaces,
|
|
2854
|
+
createWorkspace: props.createWorkspace,
|
|
2855
|
+
pickDirectory: props.pickDirectory,
|
|
2856
|
+
modelT: props.modelT,
|
|
2857
|
+
permissionT: props.permissionT,
|
|
2858
|
+
presetT: props.presetT,
|
|
2859
|
+
defaults: catalog,
|
|
2860
|
+
readMainSessionModel: props.readMainSessionModel,
|
|
2861
|
+
onClose: () => setEditing(null),
|
|
2862
|
+
onConnected: () => { setEditing(null); setExpanded((prev) => ({ ...prev, [editing]: true })); refresh(); },
|
|
2863
|
+
t,
|
|
2864
|
+
}),
|
|
2865
|
+
);
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
function IconChevron() {
|
|
2869
|
+
return h(IconChevronRightOutline, { size: 14 });
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
// ---------------------------------------------------------------------
|
|
2873
|
+
// 频道页签:直接复用官方 WorkspaceBrowser。
|
|
2874
|
+
// 自绘列表已删除;官方树不可用时只显示不可用提示。
|
|
2875
|
+
// ---------------------------------------------------------------------
|
|
2876
|
+
|
|
2877
|
+
const CHANNEL_WORKSPACE_PREFIX = "dsh-feishu-channels:";
|
|
2878
|
+
const CHANNEL_GROUP_IDS = ["feishu"];
|
|
2879
|
+
const EMPTY_SESSION_STATE = { ids: [], byId: {}, current: null };
|
|
2880
|
+
const EMPTY_WORKSPACE_STATE = { items: [], archivedSessionIds: [], pinnedSessionIds: [], state: "idle", phase: "ready", error: null };
|
|
2881
|
+
const CHANNEL_GROUP_OPTION_ATTR = "data-dfc-channel-group-option";
|
|
2882
|
+
const CHANNEL_GROUP_MENU_ATTR = "data-dfc-channel-group-menu";
|
|
2883
|
+
|
|
2884
|
+
function channelWorkspaceId(id) {
|
|
2885
|
+
return CHANNEL_WORKSPACE_PREFIX + String(id || "");
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
function channelIdFromWorkspaceId(workspaceId) {
|
|
2889
|
+
const value = String(workspaceId || "");
|
|
2890
|
+
return value.startsWith(CHANNEL_WORKSPACE_PREFIX) ? value.slice(CHANNEL_WORKSPACE_PREFIX.length) : undefined;
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
/** 频道页签把官方“添加工作区”按钮复用为“添加频道”。 */
|
|
2894
|
+
function channelAddFlow(select) {
|
|
2895
|
+
return select(true);
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
function noopChannelAction() {}
|
|
2899
|
+
|
|
2900
|
+
function copySessionOrders(orders) {
|
|
2901
|
+
return Object.fromEntries(Object.entries(orders || {}).map(([key, order]) => [key, [...order]]));
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
/** 频道页签使用独立视图状态,不污染上方任务树。 */
|
|
2905
|
+
function createChannelViewStore() {
|
|
2906
|
+
let state = {
|
|
2907
|
+
groupBy: "channel",
|
|
2908
|
+
orderBy: "updated",
|
|
2909
|
+
groupExpansion: {},
|
|
2910
|
+
sessionOrderByAccount: {},
|
|
2911
|
+
archivedFilter: "default",
|
|
2912
|
+
};
|
|
2913
|
+
const listeners = new Set();
|
|
2914
|
+
const publish = (next) => {
|
|
2915
|
+
state = next;
|
|
2916
|
+
for (const listener of listeners) listener();
|
|
2917
|
+
};
|
|
2918
|
+
const actions = {
|
|
2919
|
+
setGroupBy(mode) {
|
|
2920
|
+
if (mode !== state.groupBy) publish(Object.assign({}, state, { groupBy: mode }));
|
|
2921
|
+
},
|
|
2922
|
+
setOrderBy(mode, initialOrders) {
|
|
2923
|
+
if (mode === state.orderBy) return;
|
|
2924
|
+
publish(Object.assign({}, state, {
|
|
2925
|
+
orderBy: mode,
|
|
2926
|
+
sessionOrderByAccount: mode === "manual" ? copySessionOrders(initialOrders) : {},
|
|
2927
|
+
}));
|
|
2928
|
+
},
|
|
2929
|
+
setGroupExpanded(key, expanded) {
|
|
2930
|
+
if (state.groupExpansion[key] === expanded) return;
|
|
2931
|
+
publish(Object.assign({}, state, { groupExpansion: Object.assign({}, state.groupExpansion, { [key]: expanded }) }));
|
|
2932
|
+
},
|
|
2933
|
+
retainAccountKeys(workspaceKeys) {
|
|
2934
|
+
const retained = new Set(workspaceKeys || []);
|
|
2935
|
+
const groupExpansion = Object.fromEntries(Object.entries(state.groupExpansion).filter(([key]) => retained.has(key)));
|
|
2936
|
+
const sessionOrderByAccount = Object.fromEntries(Object.entries(state.sessionOrderByAccount).filter(([key]) => retained.has(key)));
|
|
2937
|
+
if (Object.keys(groupExpansion).length === Object.keys(state.groupExpansion).length
|
|
2938
|
+
&& Object.keys(sessionOrderByAccount).length === Object.keys(state.sessionOrderByAccount).length) return;
|
|
2939
|
+
publish(Object.assign({}, state, { groupExpansion, sessionOrderByAccount }));
|
|
2940
|
+
},
|
|
2941
|
+
setSessionOrder(accountKey, order, initialOrders) {
|
|
2942
|
+
const sessionOrderByAccount = state.orderBy === "manual"
|
|
2943
|
+
? Object.assign({}, state.sessionOrderByAccount, { [accountKey]: [...order] })
|
|
2944
|
+
: Object.assign(copySessionOrders(initialOrders), { [accountKey]: [...order] });
|
|
2945
|
+
publish(Object.assign({}, state, { orderBy: "manual", sessionOrderByAccount }));
|
|
2946
|
+
},
|
|
2947
|
+
syncSessionOrders(orders) {
|
|
2948
|
+
if (state.orderBy !== "manual") return;
|
|
2949
|
+
publish(Object.assign({}, state, { sessionOrderByAccount: Object.assign({}, state.sessionOrderByAccount, copySessionOrders(orders)) }));
|
|
2950
|
+
},
|
|
2951
|
+
pinSessionOrder(sessionId, accountKeys, source) {
|
|
2952
|
+
const selected = new Set(accountKeys || []);
|
|
2953
|
+
const sessionOrderByAccount = Object.fromEntries(Object.entries((source && source.members) || {}).map(([key, members]) => {
|
|
2954
|
+
const current = state.sessionOrderByAccount[key] || members;
|
|
2955
|
+
const order = selected.has(key) ? [sessionId, ...current.filter((id) => id !== sessionId)] : [...current];
|
|
2956
|
+
return [key, order];
|
|
2957
|
+
}));
|
|
2958
|
+
publish(Object.assign({}, state, { sessionOrderByAccount }));
|
|
2959
|
+
},
|
|
2960
|
+
setArchivedFilter(filter) {
|
|
2961
|
+
if (filter !== state.archivedFilter) publish(Object.assign({}, state, { archivedFilter: filter }));
|
|
2962
|
+
},
|
|
2963
|
+
ensureExpanded(keys) {
|
|
2964
|
+
let changed = false;
|
|
2965
|
+
const groupExpansion = Object.assign({}, state.groupExpansion);
|
|
2966
|
+
for (const key of keys || []) {
|
|
2967
|
+
if (key === "" || groupExpansion[key] !== undefined) continue;
|
|
2968
|
+
groupExpansion[key] = true;
|
|
2969
|
+
changed = true;
|
|
2970
|
+
}
|
|
2971
|
+
if (changed) publish(Object.assign({}, state, { groupExpansion }));
|
|
2972
|
+
},
|
|
2973
|
+
};
|
|
2974
|
+
return {
|
|
2975
|
+
getSnapshot: () => state,
|
|
2976
|
+
subscribe(listener) {
|
|
2977
|
+
listeners.add(listener);
|
|
2978
|
+
return () => listeners.delete(listener);
|
|
2979
|
+
},
|
|
2980
|
+
actions,
|
|
2981
|
+
};
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
let sharedChannelViewStore;
|
|
2985
|
+
function getChannelViewStore() {
|
|
2986
|
+
sharedChannelViewStore = sharedChannelViewStore || createChannelViewStore();
|
|
2987
|
+
return sharedChannelViewStore;
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
const channelSessionStateCache = new WeakMap();
|
|
2991
|
+
const channelSyntheticWorkspaceCache = new WeakMap();
|
|
2992
|
+
const channelRealWorkspaceCache = new WeakMap();
|
|
2993
|
+
|
|
2994
|
+
/** 只保留 /channels 分组里的会话,避免官方树出现“未分组”桶。 */
|
|
2995
|
+
function channelMemberSessionState(state, memberIds) {
|
|
2996
|
+
const src = state || EMPTY_SESSION_STATE;
|
|
2997
|
+
const hit = channelSessionStateCache.get(src);
|
|
2998
|
+
if (hit && hit.ids === memberIds) return hit.result;
|
|
2999
|
+
const ids = (src.ids || []).filter((id) => memberIds.has(String(id)));
|
|
3000
|
+
const result = cacheFilteredSessions(src, ids);
|
|
3001
|
+
channelSessionStateCache.set(src, { ids: memberIds, result });
|
|
3002
|
+
return result;
|
|
3003
|
+
}
|
|
3004
|
+
|
|
3005
|
+
function channelGroupsFromSessionState(state, t) {
|
|
3006
|
+
const groups = new Map(CHANNEL_GROUP_IDS.map((id) => [id, {
|
|
3007
|
+
id,
|
|
3008
|
+
label: channelLabel({ id }, t),
|
|
3009
|
+
sessions: [],
|
|
3010
|
+
}]));
|
|
3011
|
+
const src = state || EMPTY_SESSION_STATE;
|
|
3012
|
+
for (const id of src.ids || []) {
|
|
3013
|
+
const group = groups.get(channelIdOfSession(id));
|
|
3014
|
+
const session = src.byId && src.byId[id];
|
|
3015
|
+
if (group && session) group.sessions.push({ sessionId: id, updatedAt: session.updatedAt });
|
|
3016
|
+
}
|
|
3017
|
+
return [...groups.values()].filter((group) => group.sessions.length > 0);
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
function channelGroupCreatedAt(group) {
|
|
3021
|
+
let earliest = Infinity;
|
|
3022
|
+
for (const session of group.sessions || []) {
|
|
3023
|
+
const timestamp = Date.parse(session.updatedAt || "");
|
|
3024
|
+
if (Number.isFinite(timestamp) && timestamp < earliest) earliest = timestamp;
|
|
3025
|
+
}
|
|
3026
|
+
return new Date(Number.isFinite(earliest) ? earliest : 0).toISOString();
|
|
3027
|
+
}
|
|
3028
|
+
|
|
3029
|
+
/**
|
|
3030
|
+
* 按频道分组:每个频道合成一个官方 WorkspaceView。
|
|
3031
|
+
* 保留宿主的 phase/state/归档集合,官方浏览器才能正常渲染。
|
|
3032
|
+
*/
|
|
3033
|
+
function syntheticChannelWorkspaceState(state, groups, t) {
|
|
3034
|
+
const src = state || EMPTY_WORKSPACE_STATE;
|
|
3035
|
+
const key = groups.map((group) => [
|
|
3036
|
+
group.id,
|
|
3037
|
+
channelLabel(group, t),
|
|
3038
|
+
channelGroupCreatedAt(group),
|
|
3039
|
+
(group.sessions || []).map((session) => session.sessionId).join(","),
|
|
3040
|
+
].join("\0")).join("\0\0");
|
|
3041
|
+
const hit = channelSyntheticWorkspaceCache.get(src);
|
|
3042
|
+
if (hit && hit.key === key) return hit.result;
|
|
3043
|
+
const items = groups.filter((group) => (group.sessions || []).length > 0).map((group) => {
|
|
3044
|
+
const workspaceId = channelWorkspaceId(group.id);
|
|
3045
|
+
return {
|
|
3046
|
+
workspaceId,
|
|
3047
|
+
id: workspaceId,
|
|
3048
|
+
title: channelLabel(group, t),
|
|
3049
|
+
path: "",
|
|
3050
|
+
sessionIds: (group.sessions || []).map((session) => session.sessionId),
|
|
3051
|
+
createdAt: channelGroupCreatedAt(group),
|
|
3052
|
+
};
|
|
3053
|
+
});
|
|
3054
|
+
const result = Object.assign({}, src, { items, phase: "ready" });
|
|
3055
|
+
channelSyntheticWorkspaceCache.set(src, { key, result });
|
|
3056
|
+
return result;
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
/** 按工作区/工作区树:保留真实工作区,只过滤出频道会话。 */
|
|
3060
|
+
function channelRealWorkspaceState(state, memberIds) {
|
|
3061
|
+
const src = state || EMPTY_WORKSPACE_STATE;
|
|
3062
|
+
const hit = channelRealWorkspaceCache.get(src);
|
|
3063
|
+
if (hit && hit.ids === memberIds) return hit.result;
|
|
3064
|
+
const items = (src.items || []).map((workspace) => {
|
|
3065
|
+
const sessionIds = (workspace.sessionIds || []).filter((id) => memberIds.has(String(id)));
|
|
3066
|
+
return sessionIds.length === (workspace.sessionIds || []).length
|
|
3067
|
+
? workspace
|
|
3068
|
+
: Object.assign({}, workspace, { sessionIds });
|
|
3069
|
+
});
|
|
3070
|
+
const result = Object.assign({}, src, { items });
|
|
3071
|
+
channelRealWorkspaceCache.set(src, { ids: memberIds, result });
|
|
3072
|
+
return result;
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
let channelIconCssValue;
|
|
3076
|
+
|
|
3077
|
+
/** 把 React 元素渲染成可直接注入官方 DOM 的 HTML。 */
|
|
3078
|
+
function channelMarkup(element) {
|
|
3079
|
+
if (typeof document === "undefined") return "";
|
|
3080
|
+
const hasCreateRoot = typeof ReactDOM.createRoot === "function";
|
|
3081
|
+
const createRoot = ReactDOM.createRoot || function createRootFallback(host) {
|
|
3082
|
+
return {
|
|
3083
|
+
render(node) { if (typeof ReactDOM.render === "function") ReactDOM.render(node, host); },
|
|
3084
|
+
unmount() { if (typeof ReactDOM.unmountComponentAtNode === "function") ReactDOM.unmountComponentAtNode(host); },
|
|
3085
|
+
};
|
|
3086
|
+
};
|
|
3087
|
+
try {
|
|
3088
|
+
const host = document.createElement("div");
|
|
3089
|
+
const root = createRoot(host);
|
|
3090
|
+
try {
|
|
3091
|
+
if (hasCreateRoot && typeof ReactDOM.flushSync === "function") ReactDOM.flushSync(() => root.render(element));
|
|
3092
|
+
else root.render(element);
|
|
3093
|
+
return host.innerHTML;
|
|
3094
|
+
} finally {
|
|
3095
|
+
try { root.unmount(); } catch { /* ignore */ }
|
|
3096
|
+
}
|
|
3097
|
+
} catch {
|
|
3098
|
+
return "";
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
const channelLogoUrlCache = new Map();
|
|
3103
|
+
/** 频道品牌标 data URL,供分组头和会话行复用。 */
|
|
3104
|
+
function channelLogoUrl(id) {
|
|
3105
|
+
if (channelLogoUrlCache.has(id)) return channelLogoUrlCache.get(id);
|
|
3106
|
+
const markup = channelMarkup(h(BrandMark, { id, compact: true }));
|
|
3107
|
+
const url = markup ? `url("data:image/svg+xml,${encodeURIComponent(markup)}")` : "";
|
|
3108
|
+
channelLogoUrlCache.set(id, url);
|
|
3109
|
+
return url;
|
|
3110
|
+
}
|
|
3111
|
+
|
|
3112
|
+
/** 频道分组头图标:用品牌标覆盖官方文件夹图标,并保持悬停时可见。 */
|
|
3113
|
+
function channelIconCss() {
|
|
3114
|
+
if (channelIconCssValue !== undefined) return channelIconCssValue;
|
|
3115
|
+
if (typeof document === "undefined") {
|
|
3116
|
+
channelIconCssValue = "";
|
|
3117
|
+
return channelIconCssValue;
|
|
3118
|
+
}
|
|
3119
|
+
channelIconCssValue = CHANNEL_GROUP_IDS.map((id) => {
|
|
3120
|
+
const logo = channelLogoUrl(id);
|
|
3121
|
+
if (!logo) return "";
|
|
3122
|
+
const row = `[data-row-key="workspace:${channelWorkspaceId(id)}"]`;
|
|
3123
|
+
return `.dfc-channel-official ${row}>span:first-child>svg{display:none!important}`
|
|
3124
|
+
+ `.dfc-channel-official ${row}>span:first-child{display:inline-flex!important;background:${logo} center/contain no-repeat!important}`
|
|
3125
|
+
// 宿主在悬停时用 chevron 换掉文件夹;频道分组保持品牌图标可见。
|
|
3126
|
+
+ `.dfc-channel-official ${row}:hover>span:first-child{display:inline-flex!important}`
|
|
3127
|
+
+ `.dfc-channel-official ${row}:hover>span:nth-child(2){display:none!important}`;
|
|
3128
|
+
}).join("");
|
|
3129
|
+
return channelIconCssValue;
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
let channelWorkspaceIconCache = { key: "", css: "" };
|
|
3133
|
+
/**
|
|
3134
|
+
* 按工作区/工作区树分组时,如果某个工作区的频道会话都来自同一个频道,
|
|
3135
|
+
* 用频道品牌标替换文件夹。混合频道的工作区保持官方文件夹图标。
|
|
3136
|
+
*/
|
|
3137
|
+
function channelWorkspaceIconCss(workspaces, memberIds) {
|
|
3138
|
+
const entries = [];
|
|
3139
|
+
for (const workspace of workspaces || []) {
|
|
3140
|
+
const workspaceId = workspace.workspaceId || workspace.id;
|
|
3141
|
+
if (!workspaceId) continue;
|
|
3142
|
+
const channels = new Set();
|
|
3143
|
+
for (const sessionId of workspace.sessionIds || []) {
|
|
3144
|
+
if (!memberIds.has(String(sessionId))) continue;
|
|
3145
|
+
const channelId = channelIdOfSession(sessionId);
|
|
3146
|
+
if (channelId) channels.add(channelId);
|
|
3147
|
+
}
|
|
3148
|
+
if (channels.size !== 1) continue;
|
|
3149
|
+
const channelId = [...channels][0];
|
|
3150
|
+
if (!channelLogoUrl(channelId)) continue;
|
|
3151
|
+
entries.push([workspaceId, channelId]);
|
|
3152
|
+
}
|
|
3153
|
+
const key = entries.map(([workspaceId, channelId]) => `${workspaceId}\0${channelId}`).join("\0\0");
|
|
3154
|
+
if (channelWorkspaceIconCache.key === key) return channelWorkspaceIconCache.css;
|
|
3155
|
+
const css = entries.map(([workspaceId, channelId]) => {
|
|
3156
|
+
const logo = channelLogoUrl(channelId);
|
|
3157
|
+
const row = `[data-row-key=${JSON.stringify("workspace:" + workspaceId)}]`;
|
|
3158
|
+
const scope = `.dfc-channel-official:not([data-mode="channel"])`;
|
|
3159
|
+
return `${scope} ${row}>span:first-child>svg{display:none!important}`
|
|
3160
|
+
+ `${scope} ${row}>span:first-child{display:inline-flex!important;background:${logo} center/contain no-repeat!important}`
|
|
3161
|
+
+ `${scope} ${row}:hover>span:first-child{display:inline-flex!important}`
|
|
3162
|
+
+ `${scope} ${row}:hover>span:nth-child(2){display:none!important}`;
|
|
3163
|
+
}).join("");
|
|
3164
|
+
channelWorkspaceIconCache = { key, css };
|
|
3165
|
+
return css;
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
/**
|
|
3169
|
+
* 官方视图选项菜单没有扩展位,注入第四个分组项「按频道」。
|
|
3170
|
+
* 只在频道页签挂载期间生效。
|
|
3171
|
+
*/
|
|
3172
|
+
function installChannelGroupOption(options) {
|
|
3173
|
+
if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {};
|
|
3174
|
+
let frame;
|
|
3175
|
+
const closeMenu = () => document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true }));
|
|
3176
|
+
const inject = (menu) => {
|
|
3177
|
+
const text = menu.textContent || "";
|
|
3178
|
+
if (!options.groupLabel || !options.workspaceLabel
|
|
3179
|
+
|| !text.includes(options.groupLabel) || !text.includes(options.workspaceLabel)) return;
|
|
3180
|
+
if (menu.querySelector(`[${CHANNEL_GROUP_OPTION_ATTR}="true"]`) !== null) return;
|
|
3181
|
+
const items = [...menu.querySelectorAll('button[role="menuitem"]')];
|
|
3182
|
+
const first = items[0];
|
|
3183
|
+
if (!first) return;
|
|
3184
|
+
const wrapper = first.parentElement;
|
|
3185
|
+
if (!wrapper) return;
|
|
3186
|
+
const clone = wrapper.cloneNode(true);
|
|
3187
|
+
const button = clone.querySelector('button[role="menuitem"]');
|
|
3188
|
+
if (!button) return;
|
|
3189
|
+
button.setAttribute(CHANNEL_GROUP_OPTION_ATTR, "true");
|
|
3190
|
+
button.removeAttribute("aria-disabled");
|
|
3191
|
+
button.removeAttribute("disabled");
|
|
3192
|
+
button.disabled = false;
|
|
3193
|
+
button.classList.remove("danger");
|
|
3194
|
+
for (const svg of button.querySelectorAll(":scope > svg")) svg.remove();
|
|
3195
|
+
const spans = button.querySelectorAll("span");
|
|
3196
|
+
if (spans[0] !== undefined) spans[0].innerHTML = channelMarkup(h(IconQueueOutline, { size: 16 }));
|
|
3197
|
+
if (spans[1] !== undefined) spans[1].textContent = options.label;
|
|
3198
|
+
else button.textContent = options.label;
|
|
3199
|
+
if (options.checked) {
|
|
3200
|
+
const check = document.createElement("span");
|
|
3201
|
+
check.innerHTML = channelMarkup(h(IconCheckOutline, { size: 16 }));
|
|
3202
|
+
const svg = check.firstElementChild;
|
|
3203
|
+
if (svg !== null) {
|
|
3204
|
+
if (svg.style) svg.style.marginLeft = "auto";
|
|
3205
|
+
button.append(svg);
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3208
|
+
button.addEventListener("click", (event) => {
|
|
3209
|
+
event.preventDefault();
|
|
3210
|
+
event.stopPropagation();
|
|
3211
|
+
closeMenu();
|
|
3212
|
+
window.setTimeout(options.onSelect, 0);
|
|
3213
|
+
});
|
|
3214
|
+
wrapper.parentElement?.insertBefore(clone, wrapper);
|
|
3215
|
+
};
|
|
3216
|
+
const scan = () => {
|
|
3217
|
+
frame = undefined;
|
|
3218
|
+
for (const menu of document.querySelectorAll('[role="menu"]')) inject(menu);
|
|
3219
|
+
};
|
|
3220
|
+
const schedule = () => {
|
|
3221
|
+
if (frame !== undefined) return;
|
|
3222
|
+
frame = window.requestAnimationFrame(scan);
|
|
3223
|
+
};
|
|
3224
|
+
const observer = new MutationObserver(schedule);
|
|
3225
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
3226
|
+
schedule();
|
|
3227
|
+
return () => {
|
|
3228
|
+
if (frame !== undefined) window.cancelAnimationFrame(frame);
|
|
3229
|
+
observer.disconnect();
|
|
3230
|
+
for (const item of document.querySelectorAll(`[${CHANNEL_GROUP_OPTION_ATTR}="true"]`)) item.parentElement?.remove();
|
|
3231
|
+
};
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
/**
|
|
3235
|
+
* 官方工作区分组菜单只有重命名/删除;频道组合成工作区需要「频道设置」,因此注入设置入口。
|
|
3236
|
+
*/
|
|
3237
|
+
function installChannelGroupMenu(options) {
|
|
3238
|
+
if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {};
|
|
3239
|
+
let channelId;
|
|
3240
|
+
let rememberedAt = 0;
|
|
3241
|
+
let frame;
|
|
3242
|
+
const closeMenu = () => document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true }));
|
|
3243
|
+
const remember = (event) => {
|
|
3244
|
+
const target = event.target && typeof event.target.closest === "function" ? event.target : undefined;
|
|
3245
|
+
if (target === undefined) return;
|
|
3246
|
+
const row = target.closest(`[data-row-key^="workspace:${CHANNEL_WORKSPACE_PREFIX}"]`);
|
|
3247
|
+
if (row !== null) {
|
|
3248
|
+
const key = row.getAttribute("data-row-key") || "";
|
|
3249
|
+
channelId = channelIdFromWorkspaceId(key.slice("workspace:".length));
|
|
3250
|
+
rememberedAt = performance.now();
|
|
3251
|
+
return;
|
|
3252
|
+
}
|
|
3253
|
+
if (target.closest('[role="menu"]') === null) channelId = undefined;
|
|
3254
|
+
};
|
|
3255
|
+
const hiddenItems = new Map();
|
|
3256
|
+
const hideOthers = (menu, ours) => {
|
|
3257
|
+
for (const item of menu.querySelectorAll('button[role="menuitem"]')) {
|
|
3258
|
+
if (ours.has(item)) continue;
|
|
3259
|
+
if (!hiddenItems.has(item)) {
|
|
3260
|
+
hiddenItems.set(item, {
|
|
3261
|
+
display: item.style.display,
|
|
3262
|
+
ariaHidden: item.getAttribute("aria-hidden"),
|
|
3263
|
+
});
|
|
3264
|
+
}
|
|
3265
|
+
item.style.display = "none";
|
|
3266
|
+
item.setAttribute("aria-hidden", "true");
|
|
3267
|
+
}
|
|
3268
|
+
};
|
|
3269
|
+
const inject = (menu) => {
|
|
3270
|
+
if (channelId === undefined || performance.now() - rememberedAt > 2000) return;
|
|
3271
|
+
if (menu.querySelector(`[${CHANNEL_GROUP_MENU_ATTR}="true"]`) !== null) return;
|
|
3272
|
+
const items = [...menu.querySelectorAll('button[role="menuitem"]')];
|
|
3273
|
+
if (items.length === 0) return;
|
|
3274
|
+
const menuText = items.map((item) => item.textContent || "").join(" ");
|
|
3275
|
+
if (!/(删除工作区|Delete workspace)/i.test(menuText)) return;
|
|
3276
|
+
const group = options.resolve(channelId);
|
|
3277
|
+
if (!group) return;
|
|
3278
|
+
const template = items.find((item) => /^(重命名|Rename)$/i.test((item.textContent || "").trim()))
|
|
3279
|
+
|| items.find((item) => !item.disabled && !/danger|删除|Delete/i.test(item.textContent || ""))
|
|
3280
|
+
|| items[0];
|
|
3281
|
+
const wrapper = template.parentElement;
|
|
3282
|
+
if (!wrapper) return;
|
|
3283
|
+
const build = (label, icon, onClick) => {
|
|
3284
|
+
const clone = wrapper.cloneNode(true);
|
|
3285
|
+
const button = clone.querySelector('button[role="menuitem"]');
|
|
3286
|
+
if (!button) return undefined;
|
|
3287
|
+
clone.removeAttribute(CHANNEL_GROUP_MENU_ATTR);
|
|
3288
|
+
button.removeAttribute(CHANNEL_GROUP_MENU_ATTR);
|
|
3289
|
+
button.removeAttribute("aria-disabled");
|
|
3290
|
+
button.removeAttribute("disabled");
|
|
3291
|
+
button.disabled = false;
|
|
3292
|
+
button.style.opacity = "";
|
|
3293
|
+
button.style.cursor = "";
|
|
3294
|
+
button.classList.remove("danger");
|
|
3295
|
+
button.setAttribute(CHANNEL_GROUP_MENU_ATTR, "true");
|
|
3296
|
+
const spans = button.querySelectorAll("span");
|
|
3297
|
+
if (spans[0] !== undefined) spans[0].innerHTML = icon;
|
|
3298
|
+
if (spans[1] !== undefined) spans[1].textContent = label;
|
|
3299
|
+
else button.textContent = label;
|
|
3300
|
+
let fired = false;
|
|
3301
|
+
const activate = (event) => {
|
|
3302
|
+
if (fired) return;
|
|
3303
|
+
fired = true;
|
|
3304
|
+
event.preventDefault();
|
|
3305
|
+
event.stopPropagation();
|
|
3306
|
+
if (typeof event.stopImmediatePropagation === "function") event.stopImmediatePropagation();
|
|
3307
|
+
closeMenu();
|
|
3308
|
+
// 等宿主菜单完成卸载后再打开设置,避免同一次点击被菜单的关闭逻辑吞掉。
|
|
3309
|
+
window.setTimeout(onClick, 50);
|
|
3310
|
+
};
|
|
3311
|
+
button.addEventListener("click", activate);
|
|
3312
|
+
button.addEventListener("pointerup", (event) => {
|
|
3313
|
+
if (event.button === 0) activate(event);
|
|
3314
|
+
});
|
|
3315
|
+
return clone;
|
|
3316
|
+
};
|
|
3317
|
+
const node = build(options.settingsLabel, channelMarkup(h(ChannelMenuSettingsIcon)), () => options.onSettings(group));
|
|
3318
|
+
if (node) wrapper.parentElement?.insertBefore(node, wrapper);
|
|
3319
|
+
const injected = new Set(menu.querySelectorAll(`[${CHANNEL_GROUP_MENU_ATTR}="true"]`));
|
|
3320
|
+
if (injected.size > 0) hideOthers(menu, injected);
|
|
3321
|
+
};
|
|
3322
|
+
const scan = () => {
|
|
3323
|
+
frame = undefined;
|
|
3324
|
+
for (const menu of document.querySelectorAll('[role="menu"]')) inject(menu);
|
|
3325
|
+
};
|
|
3326
|
+
const schedule = () => {
|
|
3327
|
+
if (frame !== undefined) return;
|
|
3328
|
+
frame = window.requestAnimationFrame(scan);
|
|
3329
|
+
};
|
|
3330
|
+
document.addEventListener("pointerdown", remember, true);
|
|
3331
|
+
document.addEventListener("click", remember, true);
|
|
3332
|
+
const observer = new MutationObserver(schedule);
|
|
3333
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
3334
|
+
schedule();
|
|
3335
|
+
return () => {
|
|
3336
|
+
if (frame !== undefined) window.cancelAnimationFrame(frame);
|
|
3337
|
+
observer.disconnect();
|
|
3338
|
+
document.removeEventListener("pointerdown", remember, true);
|
|
3339
|
+
document.removeEventListener("click", remember, true);
|
|
3340
|
+
for (const [item, previous] of hiddenItems) {
|
|
3341
|
+
item.style.display = previous.display;
|
|
3342
|
+
if (previous.ariaHidden === null) item.removeAttribute("aria-hidden");
|
|
3343
|
+
else item.setAttribute("aria-hidden", previous.ariaHidden);
|
|
3344
|
+
}
|
|
3345
|
+
hiddenItems.clear();
|
|
3346
|
+
for (const item of document.querySelectorAll(`[${CHANNEL_GROUP_MENU_ATTR}="true"]`)) item.parentElement?.remove();
|
|
3347
|
+
};
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
function ChannelMenuSettingsIcon() {
|
|
3351
|
+
return h(IconSettingsOutline16, { size: 16 });
|
|
3352
|
+
}
|
|
3353
|
+
|
|
3354
|
+
function ChannelUnavailable({ t }) {
|
|
3355
|
+
return h("div", { className: "dfc-channel-unavailable" }, t("rail.treeUnavailable"));
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3358
|
+
class ChannelTreeGuard extends React.Component {
|
|
3359
|
+
constructor(props) {
|
|
3360
|
+
super(props);
|
|
3361
|
+
this.state = { failed: false };
|
|
3362
|
+
}
|
|
3363
|
+
static getDerivedStateFromError() {
|
|
3364
|
+
return { failed: true };
|
|
3365
|
+
}
|
|
3366
|
+
componentDidCatch(error) {
|
|
3367
|
+
console.warn("[dsh-feishu-channels] 官方频道树渲染失败", error);
|
|
3368
|
+
}
|
|
3369
|
+
render() {
|
|
3370
|
+
return this.state.failed ? h(ChannelUnavailable, { t: this.props.t }) : this.props.children;
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
function ChannelOfficialView(props) {
|
|
3375
|
+
const t = props.t || fallbackT;
|
|
3376
|
+
const hostProps = props.hostProps || {};
|
|
3377
|
+
const Official = props.officialTree;
|
|
3378
|
+
const hostT = typeof props.hostT === "function"
|
|
3379
|
+
? props.hostT
|
|
3380
|
+
: (typeof hostProps.t === "function" ? hostProps.t : t);
|
|
3381
|
+
const rawUseSessions = hostProps.useSessions;
|
|
3382
|
+
const rawUseWorkspaces = hostProps.useWorkspaces;
|
|
3383
|
+
const store = getChannelViewStore();
|
|
3384
|
+
const membershipRevision = useSyncExternalStore(subscribeChannelMembership, channelMembershipSnapshot, channelMembershipSnapshot);
|
|
3385
|
+
const useChannelStore = useCallback((select) => {
|
|
3386
|
+
const snapshot = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
3387
|
+
return select(snapshot);
|
|
3388
|
+
}, [store]);
|
|
3389
|
+
const groupBy = useChannelStore((state) => state.groupBy);
|
|
3390
|
+
const sessionSource = typeof rawUseSessions === "function"
|
|
3391
|
+
? rawUseSessions((state) => state)
|
|
3392
|
+
: EMPTY_SESSION_STATE;
|
|
3393
|
+
// 宿主 0.2.x 首个权威快照前 phase 为 pending,列表失败时也停在 pending;
|
|
3394
|
+
// 旧宿主快照可能不带 phase,此时按已就绪处理,由 renderable 判定能力缺失。
|
|
3395
|
+
const groupsReady = sessionSource.phase === undefined || sessionSource.phase === "ready";
|
|
3396
|
+
const groupsError = sessionSource.state === "error";
|
|
3397
|
+
const groups = useMemo(() => channelGroupsFromSessionState(sessionSource, t), [membershipRevision, sessionSource, t]);
|
|
3398
|
+
const groupsRef = useRef(groups);
|
|
3399
|
+
groupsRef.current = groups;
|
|
3400
|
+
const [channelIcons, setChannelIcons] = useState("");
|
|
3401
|
+
const [workspaceIcons, setWorkspaceIcons] = useState("");
|
|
3402
|
+
const open = props.openSession || hostProps.open || hostProps.openSession;
|
|
3403
|
+
const openChannelSettings = props.openChannelSettings || hostProps.openChannelSettings;
|
|
3404
|
+
const openImSettings = props.openImSettings || hostProps.openImSettings || (() => openImSettingsPage(t));
|
|
3405
|
+
|
|
3406
|
+
useEffect(() => { ensureStyle(); }, []);
|
|
3407
|
+
|
|
3408
|
+
useEffect(() => {
|
|
3409
|
+
if (groupBy === "channel" && channelIcons === "") setChannelIcons(channelIconCss());
|
|
3410
|
+
}, [channelIcons, groupBy]);
|
|
3411
|
+
|
|
3412
|
+
const memberKey = groups.map((group) => group.id + ":" + (group.sessions || []).map((session) => session.sessionId).join(",")).join("|");
|
|
3413
|
+
const memberIds = useMemo(() => {
|
|
3414
|
+
const ids = new Set();
|
|
3415
|
+
for (const group of groups) {
|
|
3416
|
+
for (const session of group.sessions || []) ids.add(String(session.sessionId));
|
|
3417
|
+
}
|
|
3418
|
+
return ids;
|
|
3419
|
+
}, [memberKey]);
|
|
3420
|
+
|
|
3421
|
+
const useChannelSessions = useCallback((selector, equals) => {
|
|
3422
|
+
if (typeof rawUseSessions !== "function") return selector(EMPTY_SESSION_STATE);
|
|
3423
|
+
return rawUseSessions((state) => selector(channelMemberSessionState(state, memberIds)), equals);
|
|
3424
|
+
}, [memberIds, rawUseSessions]);
|
|
3425
|
+
|
|
3426
|
+
const useChannelWorkspaces = useCallback((selector, equals) => {
|
|
3427
|
+
if (typeof rawUseWorkspaces !== "function") return selector(EMPTY_WORKSPACE_STATE);
|
|
3428
|
+
return rawUseWorkspaces((state) => selector(
|
|
3429
|
+
groupBy === "channel"
|
|
3430
|
+
? syntheticChannelWorkspaceState(state, groups, t)
|
|
3431
|
+
: channelRealWorkspaceState(state, memberIds),
|
|
3432
|
+
), equals);
|
|
3433
|
+
}, [groupBy, groups, memberIds, rawUseWorkspaces, t]);
|
|
3434
|
+
|
|
3435
|
+
const workspaceSource = typeof rawUseWorkspaces === "function"
|
|
3436
|
+
? rawUseWorkspaces((state) => state)
|
|
3437
|
+
: undefined;
|
|
3438
|
+
useEffect(() => {
|
|
3439
|
+
if (groupBy === "channel") {
|
|
3440
|
+
if (workspaceIcons !== "") setWorkspaceIcons("");
|
|
3441
|
+
return;
|
|
3442
|
+
}
|
|
3443
|
+
const items = (workspaceSource && workspaceSource.items) || [];
|
|
3444
|
+
const css = channelWorkspaceIconCss(items, memberIds);
|
|
3445
|
+
if (css !== workspaceIcons) setWorkspaceIcons(css);
|
|
3446
|
+
}, [groupBy, memberIds, membershipRevision, workspaceIcons, workspaceSource]);
|
|
3447
|
+
const groupKeys = useMemo(() => {
|
|
3448
|
+
if (groupBy === "channel") {
|
|
3449
|
+
return groups.filter((group) => (group.sessions || []).length > 0).map((group) => channelWorkspaceId(group.id));
|
|
3450
|
+
}
|
|
3451
|
+
if (groupBy === "flat") return [];
|
|
3452
|
+
return ((workspaceSource && workspaceSource.items) || [])
|
|
3453
|
+
.map((workspace) => String(workspace.workspaceId || workspace.id || ""))
|
|
3454
|
+
.filter((key) => key !== "");
|
|
3455
|
+
}, [groupBy, groups, workspaceSource]);
|
|
3456
|
+
useEffect(() => {
|
|
3457
|
+
store.actions.ensureExpanded(groupKeys);
|
|
3458
|
+
}, [groupKeys, store]);
|
|
3459
|
+
|
|
3460
|
+
const text = useCallback((key, params) => {
|
|
3461
|
+
if (key === "section.workspaces" && groupBy === "channel") return t("rail.feishu");
|
|
3462
|
+
if (key === "workspace.add" || key === "menu.addWorkspace") return t("rail.addChannel");
|
|
3463
|
+
return hostT(key, params);
|
|
3464
|
+
}, [groupBy, hostT, t]);
|
|
3465
|
+
|
|
3466
|
+
useEffect(() => installChannelGroupOption({
|
|
3467
|
+
label: t("rail.byChannel"),
|
|
3468
|
+
groupLabel: text("groupBy.label"),
|
|
3469
|
+
workspaceLabel: text("groupBy.workspace"),
|
|
3470
|
+
checked: groupBy === "channel",
|
|
3471
|
+
onSelect: () => store.actions.setGroupBy("channel"),
|
|
3472
|
+
}), [groupBy, store, t, text]);
|
|
3473
|
+
|
|
3474
|
+
useEffect(() => installChannelGroupMenu({
|
|
3475
|
+
settingsLabel: t("rail.channelSettings"),
|
|
3476
|
+
resolve: (channelId) => groupsRef.current.find((group) => group.id === channelId),
|
|
3477
|
+
onSettings: (group) => {
|
|
3478
|
+
if (typeof openChannelSettings === "function") {
|
|
3479
|
+
openChannelSettings({ channelId: group.id, name: channelLabel(group, t) });
|
|
3480
|
+
return;
|
|
3481
|
+
}
|
|
3482
|
+
openImSettings();
|
|
3483
|
+
},
|
|
3484
|
+
}), [openChannelSettings, openImSettings, t]);
|
|
3485
|
+
|
|
3486
|
+
const rawStartSession = hostProps.startSession;
|
|
3487
|
+
const rawRenameWorkspace = hostProps.renameWorkspace;
|
|
3488
|
+
const rawDeleteWorkspace = hostProps.deleteWorkspace;
|
|
3489
|
+
const rawInsertWorkspaceBefore = hostProps.insertWorkspaceBefore;
|
|
3490
|
+
const rawSearch = hostProps.searchSessions;
|
|
3491
|
+
const startChannelSession = useCallback((workspaceId, options) => {
|
|
3492
|
+
if (channelIdFromWorkspaceId(workspaceId) !== undefined) return;
|
|
3493
|
+
if (typeof rawStartSession === "function") rawStartSession(workspaceId, options);
|
|
3494
|
+
}, [rawStartSession]);
|
|
3495
|
+
const renameChannelWorkspace = useCallback(async (workspaceId, title) => {
|
|
3496
|
+
if (channelIdFromWorkspaceId(workspaceId) !== undefined) throw new Error(t("rail.groupReadOnly"));
|
|
3497
|
+
if (typeof rawRenameWorkspace === "function") await rawRenameWorkspace(workspaceId, title);
|
|
3498
|
+
}, [rawRenameWorkspace, t]);
|
|
3499
|
+
const deleteChannelWorkspace = useCallback(async (workspaceId) => {
|
|
3500
|
+
if (channelIdFromWorkspaceId(workspaceId) !== undefined) throw new Error(t("rail.groupReadOnly"));
|
|
3501
|
+
if (typeof rawDeleteWorkspace === "function") await rawDeleteWorkspace(workspaceId);
|
|
3502
|
+
}, [rawDeleteWorkspace, t]);
|
|
3503
|
+
const insertChannelWorkspaceBefore = useCallback(async (workspaceId, beforeWorkspaceId) => {
|
|
3504
|
+
if (channelIdFromWorkspaceId(workspaceId) !== undefined) return;
|
|
3505
|
+
if (typeof rawInsertWorkspaceBefore === "function") await rawInsertWorkspaceBefore(workspaceId, beforeWorkspaceId);
|
|
3506
|
+
}, [rawInsertWorkspaceBefore]);
|
|
3507
|
+
const searchChannelSessions = useCallback(async (query, signal) => {
|
|
3508
|
+
if (typeof rawSearch !== "function") return { items: [], hasMore: false };
|
|
3509
|
+
const result = await rawSearch(query, signal);
|
|
3510
|
+
return {
|
|
3511
|
+
items: (result.items || []).filter((item) => item && item.id !== undefined && memberIds.has(String(item.id))),
|
|
3512
|
+
hasMore: result.hasMore,
|
|
3513
|
+
};
|
|
3514
|
+
}, [memberIds, rawSearch]);
|
|
3515
|
+
const rawUseShortcuts = hostProps.useShortcuts;
|
|
3516
|
+
const useChannelShortcuts = useCallback((select, equals) => {
|
|
3517
|
+
if (typeof rawUseShortcuts !== "function") return select([]);
|
|
3518
|
+
return rawUseShortcuts((rows) => select((rows || []).filter((row) => row && row.id !== "workspace.add")), equals);
|
|
3519
|
+
}, [rawUseShortcuts]);
|
|
3520
|
+
|
|
3521
|
+
const officialProps = Object.assign({}, hostProps, {
|
|
3522
|
+
t: text,
|
|
3523
|
+
useStore: useChannelStore,
|
|
3524
|
+
actions: store.actions,
|
|
3525
|
+
useSessions: useChannelSessions,
|
|
3526
|
+
useWorkspaces: useChannelWorkspaces,
|
|
3527
|
+
open: typeof open === "function" ? open : noopChannelAction,
|
|
3528
|
+
useDirectoryFlow: channelAddFlow,
|
|
3529
|
+
requestAddWorkspace: typeof openImSettings === "function" ? openImSettings : noopChannelAction,
|
|
3530
|
+
startSession: startChannelSession,
|
|
3531
|
+
renameWorkspace: renameChannelWorkspace,
|
|
3532
|
+
deleteWorkspace: deleteChannelWorkspace,
|
|
3533
|
+
insertWorkspaceBefore: insertChannelWorkspaceBefore,
|
|
3534
|
+
});
|
|
3535
|
+
if (typeof rawSearch === "function") officialProps.searchSessions = searchChannelSessions;
|
|
3536
|
+
if (typeof rawUseShortcuts === "function") officialProps.useShortcuts = useChannelShortcuts;
|
|
3537
|
+
|
|
3538
|
+
const renderable = Official !== undefined && Official !== null
|
|
3539
|
+
&& typeof rawUseSessions === "function" && typeof rawUseWorkspaces === "function";
|
|
3540
|
+
// 官方树会清理不存在的分组,首个权威快照到达前不能传入空列表。
|
|
3541
|
+
const body = !groupsReady ? null : renderable
|
|
3542
|
+
? h(ChannelTreeGuard, { t }, h(Official, officialProps))
|
|
3543
|
+
: h(ChannelUnavailable, { t });
|
|
3544
|
+
|
|
3545
|
+
return h(React.Fragment, null,
|
|
3546
|
+
h("div", { className: "dfc-channel-official", "data-mode": groupBy },
|
|
3547
|
+
groupBy === "channel" && channelIcons !== "" && h("style", null, channelIcons),
|
|
3548
|
+
groupBy !== "channel" && workspaceIcons !== "" && h("style", null, workspaceIcons),
|
|
3549
|
+
// 宿主列表失败时 phase 仍停在 pending,没有横幅就是整页空白。
|
|
3550
|
+
groupsError && h("div", { className: "dfc-channel-error", role: "alert" }, t("error.load")),
|
|
3551
|
+
body,
|
|
3552
|
+
),
|
|
3553
|
+
);
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
const registryFilterCache = new WeakMap();
|
|
3557
|
+
function cacheFilteredSessions(src, ids) {
|
|
3558
|
+
if (ids.length === (src.ids || []).length) return src;
|
|
3559
|
+
const byId = {};
|
|
3560
|
+
for (const id of ids) {
|
|
3561
|
+
if (src.byId && src.byId[id]) byId[id] = src.byId[id];
|
|
3562
|
+
}
|
|
3563
|
+
return Object.assign({}, src, { ids, byId });
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3566
|
+
function openRegisteredSession(sessionId, registry, hostOpen) {
|
|
3567
|
+
const openers = registry && Array.isArray(registry.sessionOpeners) ? registry.sessionOpeners : [];
|
|
3568
|
+
const opener = openers
|
|
3569
|
+
.slice()
|
|
3570
|
+
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
|
3571
|
+
.find((item) => item && typeof item.matchSession === "function" && item.matchSession(sessionId));
|
|
3572
|
+
if (!opener) {
|
|
3573
|
+
if (typeof hostOpen === "function") hostOpen(sessionId);
|
|
3574
|
+
return;
|
|
3575
|
+
}
|
|
3576
|
+
try {
|
|
3577
|
+
opener.open(sessionId, hostOpen);
|
|
3578
|
+
} catch {
|
|
3579
|
+
if (typeof hostOpen === "function") hostOpen(sessionId);
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
|
|
3583
|
+
function createNativeTabRegistry(officialTree) {
|
|
3584
|
+
const tabs = new Map();
|
|
3585
|
+
const sessionFilters = [];
|
|
3586
|
+
const sessionOpeners = [];
|
|
3587
|
+
const listeners = new Set();
|
|
3588
|
+
let cachedTabs = [];
|
|
3589
|
+
let revision = 0;
|
|
3590
|
+
const rebuild = () => { cachedTabs = [...tabs.values()].sort((a, b) => (a.order || 0) - (b.order || 0)); };
|
|
3591
|
+
const emit = () => {
|
|
3592
|
+
revision += 1;
|
|
3593
|
+
for (const listener of listeners) listener();
|
|
3594
|
+
};
|
|
3595
|
+
return {
|
|
3596
|
+
version: 1,
|
|
3597
|
+
officialTree,
|
|
3598
|
+
sessionFilters,
|
|
3599
|
+
sessionOpeners,
|
|
3600
|
+
getTabs() { return cachedTabs; },
|
|
3601
|
+
getRevision() { return revision; },
|
|
3602
|
+
subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); },
|
|
3603
|
+
insert(tab) {
|
|
3604
|
+
if (!tab || !tab.id) return () => {};
|
|
3605
|
+
tabs.set(tab.id, tab);
|
|
3606
|
+
rebuild();
|
|
3607
|
+
emit();
|
|
3608
|
+
return () => {
|
|
3609
|
+
// 同名页签可能已被其他注册替换,旧清理函数只能移除自己持有的项。
|
|
3610
|
+
if (tabs.get(tab.id) !== tab) return;
|
|
3611
|
+
tabs.delete(tab.id); rebuild(); emit();
|
|
3612
|
+
};
|
|
3613
|
+
},
|
|
3614
|
+
addSessionFilter(filter) {
|
|
3615
|
+
sessionFilters.push(filter);
|
|
3616
|
+
emit();
|
|
3617
|
+
return () => {
|
|
3618
|
+
const index = sessionFilters.indexOf(filter);
|
|
3619
|
+
if (index >= 0) sessionFilters.splice(index, 1);
|
|
3620
|
+
emit();
|
|
3621
|
+
};
|
|
3622
|
+
},
|
|
3623
|
+
addSessionOpener(opener) {
|
|
3624
|
+
sessionOpeners.push(opener);
|
|
3625
|
+
emit();
|
|
3626
|
+
return () => {
|
|
3627
|
+
const index = sessionOpeners.indexOf(opener);
|
|
3628
|
+
if (index >= 0) sessionOpeners.splice(index, 1);
|
|
3629
|
+
emit();
|
|
3630
|
+
};
|
|
3631
|
+
},
|
|
3632
|
+
};
|
|
3633
|
+
}
|
|
3634
|
+
|
|
3635
|
+
function attachNativeTabRegistry(target, registry) {
|
|
3636
|
+
try { target.__dshNativeTabs = registry; } catch { /* ignore */ }
|
|
3637
|
+
return registry;
|
|
3638
|
+
}
|
|
3639
|
+
|
|
3640
|
+
function findNativeTabRegistry(entry) {
|
|
3641
|
+
const target = entry && (typeof entry === "object" || typeof entry === "function") && "component" in entry ? entry.component : entry;
|
|
3642
|
+
const registry = target?.__dshNativeTabs;
|
|
3643
|
+
return registry?.version === 1 && typeof registry.getTabs === "function" && typeof registry.subscribe === "function" && typeof registry.insert === "function" ? registry : null;
|
|
3644
|
+
}
|
|
3645
|
+
|
|
3646
|
+
function applySessionFilters(state, registry) {
|
|
3647
|
+
const src = state || { ids: [], byId: {}, current: null };
|
|
3648
|
+
const filters = registry && registry.sessionFilters ? registry.sessionFilters : [];
|
|
3649
|
+
if (!filters.length) return src;
|
|
3650
|
+
const revision = registry && typeof registry.getRevision === "function" ? registry.getRevision() : null;
|
|
3651
|
+
if (revision !== null && src && typeof src === "object") {
|
|
3652
|
+
const hit = registryFilterCache.get(src);
|
|
3653
|
+
if (hit && hit.registry === registry && hit.revision === revision) return hit.result;
|
|
3654
|
+
}
|
|
3655
|
+
const ids = (src.ids || []).filter((id) => filters.every((fn) => fn(String(id))));
|
|
3656
|
+
const result = cacheFilteredSessions(src, ids);
|
|
3657
|
+
if (revision !== null && src && typeof src === "object") registryFilterCache.set(src, { registry, revision, result });
|
|
3658
|
+
return result;
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3661
|
+
function SessionSwitcher(props) {
|
|
3662
|
+
const membershipRevision = useSyncExternalStore(subscribeChannelMembership, channelMembershipSnapshot, channelMembershipSnapshot);
|
|
3663
|
+
const t = props.t || fallbackT;
|
|
3664
|
+
const officialT = props.officialT || t;
|
|
3665
|
+
const Official = props.officialTree;
|
|
3666
|
+
const rawUseSessions = props.useSessions;
|
|
3667
|
+
const nativeTabs = props.nativeTabs;
|
|
3668
|
+
const extraTabs = useSyncExternalStore(
|
|
3669
|
+
(listener) => (nativeTabs && nativeTabs.subscribe ? nativeTabs.subscribe(listener) : () => {}),
|
|
3670
|
+
() => (nativeTabs && nativeTabs.getTabs ? nativeTabs.getTabs() : EMPTY_EXTRA_TABS),
|
|
3671
|
+
() => EMPTY_EXTRA_TABS,
|
|
3672
|
+
);
|
|
3673
|
+
const registryRevision = useSyncExternalStore(
|
|
3674
|
+
(listener) => (nativeTabs && nativeTabs.subscribe ? nativeTabs.subscribe(listener) : () => {}),
|
|
3675
|
+
() => (nativeTabs && nativeTabs.getRevision ? nativeTabs.getRevision() : 0),
|
|
3676
|
+
() => 0,
|
|
3677
|
+
);
|
|
3678
|
+
const [tab, setTab] = useState(() => {
|
|
3679
|
+
try {
|
|
3680
|
+
return localStorage.getItem(TAB_KEY)
|
|
3681
|
+
|| localStorage.getItem("dsh-feishu-channels.sidebar-tab")
|
|
3682
|
+
|| localStorage.getItem("dsh-automation.sidebar-tab") || "tasks";
|
|
3683
|
+
} catch { return "tasks"; }
|
|
3684
|
+
});
|
|
3685
|
+
const sessionSnap = typeof rawUseSessions === "function"
|
|
3686
|
+
? rawUseSessions((state) => state)
|
|
3687
|
+
: undefined;
|
|
3688
|
+
const currentId = typeof rawUseSessions === "function"
|
|
3689
|
+
? rawUseSessions((state) => currentSessionId(state))
|
|
3690
|
+
: (props.selectedId || null);
|
|
3691
|
+
const sessionById = (sessionSnap && sessionSnap.byId) || {};
|
|
3692
|
+
const useTaskSessions = useCallback((selector, eq) => {
|
|
3693
|
+
if (typeof rawUseSessions !== "function") return selector({ ids: [], byId: {}, current: null });
|
|
3694
|
+
// 频道会话也出现在官方任务树;频道页签只是同一批会话的过滤视图。
|
|
3695
|
+
return rawUseSessions((state) => selector(applySessionFilters(state, nativeTabs)), eq);
|
|
3696
|
+
}, [rawUseSessions, nativeTabs, registryRevision]);
|
|
3697
|
+
useEffect(() => { ensureStyle(); }, []);
|
|
3698
|
+
useEffect(() => { try { localStorage.setItem(TAB_KEY, tab); } catch { /* ignore */ } }, [tab]);
|
|
3699
|
+
useEffect(() => {
|
|
3700
|
+
const onClick = (event) => {
|
|
3701
|
+
const target = event.target && event.target.closest ? event.target.closest("button") : null;
|
|
3702
|
+
if (!target) return;
|
|
3703
|
+
if (target.closest("[data-row-key]")) return;
|
|
3704
|
+
const label = target.getAttribute("aria-label") || target.innerText || target.textContent || "";
|
|
3705
|
+
if (isHostNewSessionControl(label)) setTab("tasks");
|
|
3706
|
+
};
|
|
3707
|
+
document.addEventListener("click", onClick, true);
|
|
3708
|
+
return () => document.removeEventListener("click", onClick, true);
|
|
3709
|
+
}, []);
|
|
3710
|
+
const previousCurrentId = useRef(currentId);
|
|
3711
|
+
const tabFollowReady = useRef(false);
|
|
3712
|
+
useEffect(() => {
|
|
3713
|
+
const followed = followSidebarTab(
|
|
3714
|
+
{ ready: tabFollowReady.current, previousId: previousCurrentId.current },
|
|
3715
|
+
currentId,
|
|
3716
|
+
extraTabs,
|
|
3717
|
+
);
|
|
3718
|
+
tabFollowReady.current = followed.ready;
|
|
3719
|
+
previousCurrentId.current = followed.previousId;
|
|
3720
|
+
if (followed.tab) setTab(followed.tab);
|
|
3721
|
+
}, [currentId, extraTabs, membershipRevision]);
|
|
3722
|
+
const hostOpen = props.openSession || props.open;
|
|
3723
|
+
const openListed = (id) => {
|
|
3724
|
+
openListedSession(resolveHostSessionId(id, sessionById), hostOpen);
|
|
3725
|
+
};
|
|
3726
|
+
const openSession = (id) => {
|
|
3727
|
+
if (nativeTabs && Array.isArray(nativeTabs.sessionOpeners)
|
|
3728
|
+
&& nativeTabs.sessionOpeners.some((item) => item && item.id === "feishu-channel")) {
|
|
3729
|
+
openRegisteredSession(id, nativeTabs, hostOpen);
|
|
3730
|
+
return;
|
|
3731
|
+
}
|
|
3732
|
+
openListed(id);
|
|
3733
|
+
};
|
|
3734
|
+
const officialProps = Object.assign({}, props, { useSessions: useTaskSessions, t: officialT, openSession, open: openSession });
|
|
3735
|
+
const channelView = h(FeishuChannelPage, {
|
|
3736
|
+
ctx: props.ctx,
|
|
3737
|
+
t,
|
|
3738
|
+
permissionT: props.permissionT,
|
|
3739
|
+
modelT: props.modelT,
|
|
3740
|
+
presetT: props.presetT,
|
|
3741
|
+
currentSessionId: currentId,
|
|
3742
|
+
hostProps: props,
|
|
3743
|
+
});
|
|
3744
|
+
if (props.wide === false) return Official ? h(Official, officialProps) : null;
|
|
3745
|
+
const officialTree = Official
|
|
3746
|
+
? h("div", { className: "dfc-official-tree" }, h(Official, officialProps))
|
|
3747
|
+
: null;
|
|
3748
|
+
const visibleTab = resolveSidebarTab(tab, extraTabs);
|
|
3749
|
+
const localFeishuTab = {
|
|
3750
|
+
id: "feishu",
|
|
3751
|
+
order: 30,
|
|
3752
|
+
label: t("rail.feishu"),
|
|
3753
|
+
matchSession: isChannelSession,
|
|
3754
|
+
local: true,
|
|
3755
|
+
};
|
|
3756
|
+
const allTabs = [localFeishuTab, ...extraTabs].sort((a, b) => (Number(a.order) || 0) - (Number(b.order) || 0));
|
|
3757
|
+
const activeExtra = extraTabs.find((item) => item.id === visibleTab);
|
|
3758
|
+
const feishuTabLabel = feishuTabLabelNode(t);
|
|
3759
|
+
const imTabItems = [
|
|
3760
|
+
{
|
|
3761
|
+
value: "tasks",
|
|
3762
|
+
id: "dfc-tab-tasks",
|
|
3763
|
+
panelId: "dfc-panel-tasks",
|
|
3764
|
+
label: h("span", { className: "dfc-tab-inline" },
|
|
3765
|
+
h(IconFolderClose, { size: 14 }),
|
|
3766
|
+
h("span", null, t("rail.tasks")),
|
|
3767
|
+
),
|
|
3768
|
+
},
|
|
3769
|
+
];
|
|
3770
|
+
for (const item of allTabs) {
|
|
3771
|
+
const itemLabel = item.id === "feishu"
|
|
3772
|
+
? feishuTabLabel
|
|
3773
|
+
: item.id === "channels"
|
|
3774
|
+
? h("span", { className: "dfc-tab-inline" },
|
|
3775
|
+
h(IconQueueOutline, { size: 14 }),
|
|
3776
|
+
h("span", null, item.label),
|
|
3777
|
+
)
|
|
3778
|
+
: item.id === "schedule"
|
|
3779
|
+
? h("span", { className: "dfc-tab-inline" },
|
|
3780
|
+
h(IconClockOutline, { size: 14 }),
|
|
3781
|
+
h("span", null, item.label),
|
|
3782
|
+
)
|
|
3783
|
+
: item.label;
|
|
3784
|
+
imTabItems.push({
|
|
3785
|
+
value: item.id,
|
|
3786
|
+
id: "dfc-tab-" + item.id,
|
|
3787
|
+
panelId: "dfc-panel-" + item.id,
|
|
3788
|
+
label: itemLabel,
|
|
3789
|
+
});
|
|
3790
|
+
}
|
|
3791
|
+
|
|
3792
|
+
return h("div", { className: "dfc-wrap" },
|
|
3793
|
+
HostSegmentedTabs
|
|
3794
|
+
? h("div", { className: "dfc-tabs-wrap", style: { "--dfc-tab-count": String(imTabItems.length) } },
|
|
3795
|
+
h(HostSegmentedTabs, {
|
|
3796
|
+
className: "dfc-segmented-tabs",
|
|
3797
|
+
label: t("rail.tabsAria"),
|
|
3798
|
+
value: visibleTab,
|
|
3799
|
+
onChange: (next) => setTab(next),
|
|
3800
|
+
items: imTabItems,
|
|
3801
|
+
}),
|
|
3802
|
+
)
|
|
3803
|
+
: h("div", { className: "dfc-tabs", role: "tablist", "aria-label": t("rail.tabsAria") },
|
|
3804
|
+
h("button", { type: "button", role: "tab", "aria-selected": visibleTab === "tasks", className: visibleTab === "tasks" ? "dfc-tab on" : "dfc-tab", onClick: () => setTab("tasks") }, t("rail.tasks")),
|
|
3805
|
+
...allTabs.map((item) => h("button", {
|
|
3806
|
+
key: item.id,
|
|
3807
|
+
type: "button",
|
|
3808
|
+
role: "tab",
|
|
3809
|
+
"aria-selected": visibleTab === item.id,
|
|
3810
|
+
className: visibleTab === item.id ? "dfc-tab on" : "dfc-tab",
|
|
3811
|
+
onClick: () => setTab(item.id),
|
|
3812
|
+
}, item.id === "feishu" ? feishuTabLabel : item.label)),
|
|
3813
|
+
),
|
|
3814
|
+
visibleTab === "tasks"
|
|
3815
|
+
? (officialTree || h(ChannelUnavailable, { t }))
|
|
3816
|
+
: visibleTab === "feishu"
|
|
3817
|
+
? channelView
|
|
3818
|
+
: activeExtra
|
|
3819
|
+
? activeExtra.render(Object.assign({}, props, { t: officialT, openSession, open: openSession }))
|
|
3820
|
+
: channelView,
|
|
3821
|
+
);
|
|
3822
|
+
}
|
|
3823
|
+
|
|
3824
|
+
function sidebarOccupantName(item) {
|
|
3825
|
+
return String(
|
|
3826
|
+
item?.options?.locale ??
|
|
3827
|
+
item?.options?.id ??
|
|
3828
|
+
item?.options?.name ??
|
|
3829
|
+
item?.options?.registrant ??
|
|
3830
|
+
item?.component?.displayName ??
|
|
3831
|
+
item?.component?.name ??
|
|
3832
|
+
item?.id ??
|
|
3833
|
+
item?.name ??
|
|
3834
|
+
"",
|
|
3835
|
+
);
|
|
3836
|
+
}
|
|
3837
|
+
|
|
3838
|
+
/** 只认真正占用 sidebar 槽的主人。包在注册表里但没接管侧栏时,必须走原生页签。 */
|
|
3839
|
+
function hasDshCodexUiSidebar(ctx) {
|
|
3840
|
+
try {
|
|
3841
|
+
const read = ctx.slots && (ctx.slots.entriesOfSlot || ctx.slots.entries);
|
|
3842
|
+
const sidebar = read && read.call(ctx.slots, "sidebar");
|
|
3843
|
+
if (!sidebar) return false;
|
|
3844
|
+
for (const item of sidebar) {
|
|
3845
|
+
if (/dsh-codex-ui|michengai-codex-ui|michengai\.codexUi|codex-ui/i.test(sidebarOccupantName(item))) return true;
|
|
3846
|
+
}
|
|
3847
|
+
} catch { /* ignore */ }
|
|
3848
|
+
return false;
|
|
3849
|
+
}
|
|
3850
|
+
|
|
3851
|
+
/**
|
|
3852
|
+
* 侧栏包装兜底(Issue #13)。插件是用自己的一层替换宿主 sidebar.workspaces 的官方会话列表组件的:
|
|
3853
|
+
* 这层只要渲染失败,宿主的槽位错误边界就给这块画一个空的 crash div ——「左侧会话记录不显示」。
|
|
3854
|
+
* 这里接住错误,直接回落到宿主原本的官方组件(用宿主自己传进来的 props),
|
|
3855
|
+
* 宁可少一个频道页签,也不能让会话列表整个消失。
|
|
3856
|
+
*/
|
|
3857
|
+
class ImSidebarFallbackBoundary extends React.Component {
|
|
3858
|
+
constructor(props) {
|
|
3859
|
+
super(props);
|
|
3860
|
+
this.state = { failed: false };
|
|
3861
|
+
}
|
|
3862
|
+
static getDerivedStateFromError() {
|
|
3863
|
+
return { failed: true };
|
|
3864
|
+
}
|
|
3865
|
+
componentDidCatch(error) {
|
|
3866
|
+
console.warn("[dsh-feishu-channels] 侧栏包装渲染失败,已回落到宿主原生会话列表", error);
|
|
3867
|
+
}
|
|
3868
|
+
render() {
|
|
3869
|
+
if (this.state.failed) {
|
|
3870
|
+
const Official = this.props.officialTree;
|
|
3871
|
+
return Official ? h(Official, this.props.hostProps) : null;
|
|
3872
|
+
}
|
|
3873
|
+
return this.props.children;
|
|
3874
|
+
}
|
|
3875
|
+
}
|
|
3876
|
+
|
|
3877
|
+
class DfcSettingsBoundary extends React.Component {
|
|
3878
|
+
constructor(props) {
|
|
3879
|
+
super(props);
|
|
3880
|
+
this.state = { failed: false };
|
|
3881
|
+
}
|
|
3882
|
+
static getDerivedStateFromError() {
|
|
3883
|
+
return { failed: true };
|
|
3884
|
+
}
|
|
3885
|
+
componentDidCatch(error) {
|
|
3886
|
+
console.warn("[dsh-feishu-channels] 设置页渲染失败", error);
|
|
3887
|
+
}
|
|
3888
|
+
render() {
|
|
3889
|
+
if (this.state.failed) {
|
|
3890
|
+
return h("section", { className: "dfc-page", "aria-label": "飞书频道" },
|
|
3891
|
+
h("h2", { className: "dfc-title" }, "飞书频道"),
|
|
3892
|
+
h("p", { className: "dfc-sub" }, "设置页加载失败。请刷新页面;若仍失败,请在插件管理器中禁用本插件并查看控制台错误。"));
|
|
3893
|
+
}
|
|
3894
|
+
return this.props.children;
|
|
3895
|
+
}
|
|
3896
|
+
}
|
|
3897
|
+
|
|
3898
|
+
function pickOfficialWorkspaces(ctx) {
|
|
3899
|
+
const read = ctx.slots.entriesOfSlot || ctx.slots.entries;
|
|
3900
|
+
const entries = (read && read.call(ctx.slots, "sidebar.workspaces")) || [];
|
|
3901
|
+
for (const item of entries) {
|
|
3902
|
+
if (!item || !item.component) continue;
|
|
3903
|
+
if (isWrappedWorkspaceComponent(item.component)) continue;
|
|
3904
|
+
return item;
|
|
3905
|
+
}
|
|
3906
|
+
return null;
|
|
3907
|
+
}
|
|
3908
|
+
|
|
3909
|
+
function isWrappedWorkspaceComponent(component) {
|
|
3910
|
+
return !!component && (component.__dshNativeTabWrapped === true
|
|
3911
|
+
|| component.__dshNativeTabHost === true
|
|
3912
|
+
|| component.__dfcTabHost === true
|
|
3913
|
+
|| component.__imConnectWrapped === true
|
|
3914
|
+
|| component.__imConnectOriginal !== undefined
|
|
3915
|
+
|| component.__dshAutomationWrapped === true
|
|
3916
|
+
|| component.__dshAutomationOriginal !== undefined);
|
|
3917
|
+
}
|
|
3918
|
+
|
|
3919
|
+
function apply(ctx) {
|
|
3920
|
+
ensureStyle();
|
|
3921
|
+
ctx.effect(() => {
|
|
3922
|
+
let pending = channelSessionIds.size > 0, flight = null, disposed = false;
|
|
3923
|
+
const refresh = (changed) => {
|
|
3924
|
+
pending = pending || changed;
|
|
3925
|
+
if (disposed || !pending || flight) return;
|
|
3926
|
+
const sessions = probeService(ctx, "sessions");
|
|
3927
|
+
if (!sessions || typeof sessions.refresh !== "function") return;
|
|
3928
|
+
const revision = channelMembershipRevision;
|
|
3929
|
+
flight = Promise.resolve().then(() => {
|
|
3930
|
+
if (disposed) return;
|
|
3931
|
+
return sessions.refresh();
|
|
3932
|
+
}).then(() => {
|
|
3933
|
+
if (disposed) return;
|
|
3934
|
+
const snapshot = sessions.list && typeof sessions.list.getSnapshot === "function" ? sessions.list.getSnapshot() : undefined;
|
|
3935
|
+
if (snapshot && snapshot.state === "error") return;
|
|
3936
|
+
if (revision === channelMembershipRevision) pending = false;
|
|
3937
|
+
}).catch(() => { /* 保留待处理状态,由下次渠道刷新重试 */ })
|
|
3938
|
+
.finally(() => { flight = null; });
|
|
3939
|
+
};
|
|
3940
|
+
requestHostSessionRefresh = refresh;
|
|
3941
|
+
return () => {
|
|
3942
|
+
disposed = true;
|
|
3943
|
+
if (requestHostSessionRefresh === refresh) requestHostSessionRefresh = () => {};
|
|
3944
|
+
};
|
|
3945
|
+
}, "feishu-channel: host session refresh");
|
|
3946
|
+
ctx.effect(() => channelRefresh.subscribe((data, error) => {
|
|
3947
|
+
if (error) console.warn("[dsh-feishu-channels] 无法刷新渠道会话归属", error);
|
|
3948
|
+
}), "feishu-channel: channel membership");
|
|
3949
|
+
ctx.effect(() => ctx.locale.register(DFC_LOCALE_NS, DFC_LOCALES), "feishu-channel: dictionaries");
|
|
3950
|
+
const t = ctx.locale.bind(DFC_LOCALE_NS);
|
|
3951
|
+
const permissionT = ctx.locale.bind("permission.access");
|
|
3952
|
+
const modelT = ctx.locale.bind("model");
|
|
3953
|
+
const presetT = ctx.locale.bind("settings.agentPreset");
|
|
3954
|
+
const subscribeLocale = (listener) => ctx.locale.subscribe(listener);
|
|
3955
|
+
const localeSnapshot = () => ctx.locale.getSnapshot();
|
|
3956
|
+
const openChannelSettings = (request) => {
|
|
3957
|
+
const next = parseChannelSettingsRequest(request);
|
|
3958
|
+
if (!next) return;
|
|
3959
|
+
requestChannelSettings(next);
|
|
3960
|
+
openSettingsSection(
|
|
3961
|
+
[t("settings.label"), DFC_LOCALES.en["settings.label"], DFC_LOCALES.zh["settings.label"]],
|
|
3962
|
+
() => { requestChannelSettings(next); },
|
|
3963
|
+
);
|
|
3964
|
+
};
|
|
3965
|
+
const openImSettings = () => openImSettingsPage(t);
|
|
3966
|
+
function LocalizedSessionSwitcher(props) {
|
|
3967
|
+
useSyncExternalStore(subscribeLocale, localeSnapshot, localeSnapshot);
|
|
3968
|
+
const archiveSession = props.archiveSession || ((sessionId, options) => archiveHostSession(ctx, sessionId, options));
|
|
3969
|
+
const unarchiveSession = props.unarchiveSession || ((sessionId) => unarchiveHostSession(ctx, sessionId));
|
|
3970
|
+
return h(SessionSwitcher, Object.assign({}, props, {
|
|
3971
|
+
t,
|
|
3972
|
+
officialT: props.t,
|
|
3973
|
+
ctx,
|
|
3974
|
+
permissionT,
|
|
3975
|
+
modelT,
|
|
3976
|
+
presetT,
|
|
3977
|
+
openChannelSettings,
|
|
3978
|
+
openImSettings,
|
|
3979
|
+
archiveSession,
|
|
3980
|
+
unarchiveSession,
|
|
3981
|
+
}));
|
|
3982
|
+
}
|
|
3983
|
+
function LocalizedFeishuComposerGuard(props) {
|
|
3984
|
+
useSyncExternalStore(subscribeLocale, localeSnapshot, localeSnapshot);
|
|
3985
|
+
return h(FeishuComposerGuard, Object.assign({}, props, { t, ctx }));
|
|
3986
|
+
}
|
|
3987
|
+
function LocalizedSettingsPage(props) {
|
|
3988
|
+
useSyncExternalStore(subscribeLocale, localeSnapshot, localeSnapshot);
|
|
3989
|
+
return h(SettingsPage, Object.assign({}, props, { t, permissionT, modelT, presetT, readMainSessionModel: () => readMainSessionModel(ctx) }));
|
|
3990
|
+
}
|
|
3991
|
+
function SettingsSection(props) {
|
|
3992
|
+
return h(DfcSettingsBoundary, null, h(LocalizedSettingsPage, props));
|
|
3993
|
+
}
|
|
3994
|
+
openImSession = (id) => {
|
|
3995
|
+
try {
|
|
3996
|
+
if (window.__dshSessionsOpen) { window.__dshSessionsOpen(id); return true; }
|
|
3997
|
+
} catch { /* ignore */ }
|
|
3998
|
+
try { return openHostSession(ctx, id); }
|
|
3999
|
+
catch (error) { console.warn("[dsh-feishu-channels] 无法打开会话", id, error); return false; }
|
|
4000
|
+
};
|
|
4001
|
+
|
|
4002
|
+
// 飞书会话只能通过飞书客户端继续;替换宿主 composer,避免本地输入被丢弃。
|
|
4003
|
+
// 有待批准/待回答交互时让位给宿主自己的交互 composer。
|
|
4004
|
+
ctx.slots.inject("conversation.composer", () => ctx.slots.register({
|
|
4005
|
+
name: "conversation.composer",
|
|
4006
|
+
select: (owner) => {
|
|
4007
|
+
if (!owner || !isChannelSession(owner.sessionId)) return null;
|
|
4008
|
+
if (owner.pendingInteraction) return null;
|
|
4009
|
+
return true;
|
|
4010
|
+
},
|
|
4011
|
+
}, LocalizedFeishuComposerGuard));
|
|
4012
|
+
|
|
4013
|
+
// 频道会话行前显示所属频道品牌标,和定时任务的时钟图标同一套 leading 槽位。
|
|
4014
|
+
ctx.slots.inject("sidebar.session.row.leading", () => ctx.slots.register({
|
|
4015
|
+
name: "sidebar.session.row.leading",
|
|
4016
|
+
id: "feishu-channel-session-channel",
|
|
4017
|
+
order: 10,
|
|
4018
|
+
}, function ChannelSessionLeading({ sessionId }) {
|
|
4019
|
+
useSyncExternalStore(subscribeChannelMembership, channelMembershipSnapshot, channelMembershipSnapshot);
|
|
4020
|
+
const channelId = channelIdOfSession(sessionId);
|
|
4021
|
+
if (!channelId) return null;
|
|
4022
|
+
return h("span", { className: "dfc-session-channel-icon", "aria-hidden": "true" }, h(Logo, { id: channelId, small: true }));
|
|
4023
|
+
}));
|
|
4024
|
+
|
|
4025
|
+
// 只包一层官方任务树,绝不在通知回调里再 register,否则会把启动卡在 Loading plugins。
|
|
4026
|
+
ctx.slots.inject("sidebar.workspaces", () => {
|
|
4027
|
+
let wrappedEntry = null;
|
|
4028
|
+
let originalComp = null;
|
|
4029
|
+
let ownedWrapper = null;
|
|
4030
|
+
let ownedRegistry = null;
|
|
4031
|
+
let removeInsertedTab = () => {};
|
|
4032
|
+
let removeInsertedOpener = () => {};
|
|
4033
|
+
let removeInsertedFilter = () => {};
|
|
4034
|
+
let ownedFilter = () => {};
|
|
4035
|
+
let stopInsertedTabLocale = () => {};
|
|
4036
|
+
let insertedTabRegistry = null;
|
|
4037
|
+
let syncing = false;
|
|
4038
|
+
// 与定时插件共享通知,补足直接替换 component 不触发宿主插槽通知的情况。
|
|
4039
|
+
const notifyPeers = () => { queueMicrotask(() => window.dispatchEvent(new Event("dsh-native-sidebar-change"))); };
|
|
4040
|
+
const installSessionOpener = (registry) => {
|
|
4041
|
+
if (!registry || typeof registry.addSessionOpener !== "function") return () => {};
|
|
4042
|
+
if (registry.sessionOpeners && registry.sessionOpeners.some((item) => item.id === "feishu-channel")) return () => {};
|
|
4043
|
+
return registry.addSessionOpener({
|
|
4044
|
+
id: "feishu-channel",
|
|
4045
|
+
order: 20,
|
|
4046
|
+
// 普通任务行也需要把宿主内部 key 解析成官方 session id;定时 opener 优先级更高。
|
|
4047
|
+
matchSession: (sessionId) => typeof sessionId === "string" && sessionId !== "",
|
|
4048
|
+
open: (sessionId, hostOpen) => {
|
|
4049
|
+
let byId;
|
|
4050
|
+
try { byId = ctx.sessions && ctx.sessions.list && ctx.sessions.list.getSnapshot ? ctx.sessions.list.getSnapshot().byId : undefined; } catch { byId = undefined; }
|
|
4051
|
+
openListedSession(resolveHostSessionId(sessionId, byId), hostOpen);
|
|
4052
|
+
},
|
|
4053
|
+
});
|
|
4054
|
+
};
|
|
4055
|
+
const clearInsertedTab = () => {
|
|
4056
|
+
stopInsertedTabLocale();
|
|
4057
|
+
stopInsertedTabLocale = () => {};
|
|
4058
|
+
removeInsertedTab();
|
|
4059
|
+
removeInsertedTab = () => {};
|
|
4060
|
+
removeInsertedOpener();
|
|
4061
|
+
removeInsertedOpener = () => {};
|
|
4062
|
+
removeInsertedFilter();
|
|
4063
|
+
removeInsertedFilter = () => {};
|
|
4064
|
+
insertedTabRegistry = null;
|
|
4065
|
+
};
|
|
4066
|
+
const unwrap = () => {
|
|
4067
|
+
clearInsertedTab();
|
|
4068
|
+
ownedFilter();
|
|
4069
|
+
ownedFilter = () => {};
|
|
4070
|
+
if (wrappedEntry) {
|
|
4071
|
+
if (originalComp && wrappedEntry.component === ownedWrapper) wrappedEntry.component = originalComp;
|
|
4072
|
+
if (wrappedEntry.__dshNativeTabs === ownedRegistry) delete wrappedEntry.__dshNativeTabs;
|
|
4073
|
+
notifyPeers();
|
|
4074
|
+
}
|
|
4075
|
+
wrappedEntry = null;
|
|
4076
|
+
originalComp = null;
|
|
4077
|
+
ownedWrapper = null;
|
|
4078
|
+
ownedRegistry = null;
|
|
4079
|
+
};
|
|
4080
|
+
const insertFeishuTab = (entry) => {
|
|
4081
|
+
const registry = findNativeTabRegistry(entry);
|
|
4082
|
+
if (!registry) return false;
|
|
4083
|
+
// Our own shell already renders the local "feishu" tab; never insert a duplicate
|
|
4084
|
+
// into our own registry (also protects a reloaded instance reusing an old shell).
|
|
4085
|
+
if (registry.__dfcOwner === true) return true;
|
|
4086
|
+
if (insertedTabRegistry !== registry) {
|
|
4087
|
+
clearInsertedTab();
|
|
4088
|
+
insertedTabRegistry = registry;
|
|
4089
|
+
removeInsertedOpener = installSessionOpener(registry);
|
|
4090
|
+
removeInsertedFilter = typeof registry.addSessionFilter === "function"
|
|
4091
|
+
? registry.addSessionFilter((sessionId) => !String(sessionId).startsWith("dfc:"))
|
|
4092
|
+
: () => {};
|
|
4093
|
+
}
|
|
4094
|
+
if (registry.getTabs().some((item) => item.id === "feishu")) return true;
|
|
4095
|
+
const refreshInsertedTab = () => {
|
|
4096
|
+
removeInsertedTab = registry.insert({
|
|
4097
|
+
id: "feishu",
|
|
4098
|
+
label: feishuTabLabelNode(t),
|
|
4099
|
+
order: 30,
|
|
4100
|
+
matchSession: isChannelSession,
|
|
4101
|
+
render: (props) => h(ImSidebarFallbackBoundary, {
|
|
4102
|
+
officialTree: () => h(ChannelUnavailable, { t }),
|
|
4103
|
+
hostProps: props,
|
|
4104
|
+
}, h(FeishuChannelPage, Object.assign({}, props, {
|
|
4105
|
+
ctx,
|
|
4106
|
+
t,
|
|
4107
|
+
permissionT,
|
|
4108
|
+
modelT,
|
|
4109
|
+
presetT,
|
|
4110
|
+
hostProps: props,
|
|
4111
|
+
}))),
|
|
4112
|
+
});
|
|
4113
|
+
};
|
|
4114
|
+
refreshInsertedTab();
|
|
4115
|
+
const stopLocale = subscribeLocale(refreshInsertedTab);
|
|
4116
|
+
const stopMembership = subscribeChannelMembership(refreshInsertedTab);
|
|
4117
|
+
stopInsertedTabLocale = () => { stopLocale(); stopMembership(); };
|
|
4118
|
+
return true;
|
|
4119
|
+
};
|
|
4120
|
+
const sync = () => {
|
|
4121
|
+
if (syncing) return;
|
|
4122
|
+
syncing = true;
|
|
4123
|
+
try {
|
|
4124
|
+
const combo = hasDshCodexUiSidebar(ctx);
|
|
4125
|
+
if (combo) {
|
|
4126
|
+
unwrap();
|
|
4127
|
+
return;
|
|
4128
|
+
}
|
|
4129
|
+
const read = ctx.slots.entriesOfSlot || ctx.slots.entries;
|
|
4130
|
+
const entries = (read && read.call(ctx.slots, "sidebar.workspaces")) || [];
|
|
4131
|
+
const occupant = entries.find((item) => item && item.component);
|
|
4132
|
+
if (wrappedEntry && (occupant !== wrappedEntry || occupant.component !== ownedWrapper)) unwrap();
|
|
4133
|
+
if (!occupant) { clearInsertedTab(); return; }
|
|
4134
|
+
if (insertedTabRegistry && findNativeTabRegistry(occupant) !== insertedTabRegistry) clearInsertedTab();
|
|
4135
|
+
if (ownedWrapper && occupant.component === ownedWrapper) return;
|
|
4136
|
+
if (occupant && (occupant.component.__dshNativeTabHost || occupant.component.__dshNativeTabWrapped || occupant.component.__dshAutomationWrapped || findNativeTabRegistry(occupant))) {
|
|
4137
|
+
insertFeishuTab(occupant);
|
|
4138
|
+
return;
|
|
4139
|
+
}
|
|
4140
|
+
const official = pickOfficialWorkspaces(ctx);
|
|
4141
|
+
if (!official) return;
|
|
4142
|
+
originalComp = official.component;
|
|
4143
|
+
const registry = createNativeTabRegistry(originalComp);
|
|
4144
|
+
registry.__dfcOwner = true;
|
|
4145
|
+
ownedRegistry = registry;
|
|
4146
|
+
ownedFilter = registry.addSessionFilter((sessionId) => !String(sessionId).startsWith("dfc:"));
|
|
4147
|
+
removeInsertedOpener = installSessionOpener(registry);
|
|
4148
|
+
const tree = originalComp;
|
|
4149
|
+
attachNativeTabRegistry(official, registry);
|
|
4150
|
+
function DshNativeTabWorkspaceShell(innerProps) {
|
|
4151
|
+
return h(ImSidebarFallbackBoundary, { officialTree: tree, hostProps: innerProps },
|
|
4152
|
+
h(LocalizedSessionSwitcher, Object.assign({}, innerProps, { officialTree: tree, nativeTabs: registry })));
|
|
4153
|
+
}
|
|
4154
|
+
DshNativeTabWorkspaceShell.displayName = "DshNativeTabWorkspaceShell";
|
|
4155
|
+
DshNativeTabWorkspaceShell.__dshNativeTabHost = true;
|
|
4156
|
+
DshNativeTabWorkspaceShell.__dshNativeTabWrapped = true;
|
|
4157
|
+
DshNativeTabWorkspaceShell.__dshNativeTabOriginal = originalComp;
|
|
4158
|
+
DshNativeTabWorkspaceShell.__dshNativeTabOwner = "feishu-channel";
|
|
4159
|
+
DshNativeTabWorkspaceShell.__dfcTabHost = true;
|
|
4160
|
+
attachNativeTabRegistry(DshNativeTabWorkspaceShell, registry);
|
|
4161
|
+
official.component = DshNativeTabWorkspaceShell;
|
|
4162
|
+
ownedWrapper = DshNativeTabWorkspaceShell;
|
|
4163
|
+
wrappedEntry = official;
|
|
4164
|
+
notifyPeers();
|
|
4165
|
+
} catch (error) {
|
|
4166
|
+
console.warn("[dsh-feishu-channels] 包裹官方任务树失败", error);
|
|
4167
|
+
} finally {
|
|
4168
|
+
syncing = false;
|
|
4169
|
+
}
|
|
4170
|
+
};
|
|
4171
|
+
sync();
|
|
4172
|
+
const unsub = typeof ctx.slots.subscribe === "function" ? ctx.slots.subscribe("sidebar.workspaces", sync) : () => {};
|
|
4173
|
+
const unsubSidebar = typeof ctx.slots.subscribe === "function" ? ctx.slots.subscribe("sidebar", sync) : () => {};
|
|
4174
|
+
window.addEventListener("dsh-native-sidebar-change", sync);
|
|
4175
|
+
// 其他插件直接替换 component/注册表时不一定触发槽位通知,短暂重试兼容加载顺序。
|
|
4176
|
+
let retries = 0;
|
|
4177
|
+
const retryTimer = window.setInterval(() => {
|
|
4178
|
+
sync();
|
|
4179
|
+
if (insertedTabRegistry || ++retries >= 20) window.clearInterval(retryTimer);
|
|
4180
|
+
}, 250);
|
|
4181
|
+
return () => { window.clearInterval(retryTimer); unsub(); unsubSidebar(); window.removeEventListener("dsh-native-sidebar-change", sync); unwrap(); };
|
|
4182
|
+
});
|
|
4183
|
+
}
|
|
4184
|
+
|
|
4185
|
+
exports.apply = apply;
|
|
4186
|
+
exports.inject = inject;
|
|
4187
|
+
return module.exports;
|
|
4188
|
+
},
|
|
4189
|
+
});
|