@modesto_ma/dsh-channels 0.5.0-beta.5 → 0.5.0-beta.7
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/README.md +31 -13
- package/docs/attachment-pipeline-v2.md +1 -1
- package/docs/feishu-onboarding.md +5 -4
- package/lib/client.js +605 -55
- package/lib/client.js.map +1 -1
- package/lib/index.js +379 -21
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/access/cards.ts +5 -1
- package/src/access/controller.ts +14 -6
- package/src/access/policy.ts +7 -2
- package/src/access/requests.ts +1 -0
- package/src/client/api.ts +45 -2
- package/src/client/avatar.tsx +36 -0
- package/src/client/icon.tsx +25 -0
- package/src/client/index.ts +78 -23
- package/src/client/origin-cache.ts +45 -0
- package/src/client/origin.tsx +201 -0
- package/src/client/panel/access-tab.tsx +41 -13
- package/src/client/panel/index.tsx +23 -9
- package/src/client/panel/profiles.ts +73 -0
- package/src/client/panel/whitelist.tsx +17 -11
- package/src/client/panel/wiki-tab.tsx +10 -2
- package/src/client/styles.ts +26 -3
- package/src/config.ts +9 -2
- package/src/domain.ts +44 -2
- package/src/feishu/scopes.ts +2 -1
- package/src/feishu-channel.ts +87 -0
- package/src/routing/inbound.ts +13 -5
- package/src/service.ts +147 -3
- package/src/session/routes.ts +45 -4
- package/src/settings/contract.ts +70 -1
- package/src/settings/routes.ts +47 -0
- package/src/settings/surface.ts +30 -1
package/lib/client.js
CHANGED
|
@@ -4,8 +4,8 @@ window.__ModuleLoader__.load({
|
|
|
4
4
|
var module = { exports: {} };
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
-
let react = require("react");
|
|
8
7
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
8
|
+
let react = require("react");
|
|
9
9
|
//#region src/settings/contract.ts
|
|
10
10
|
/**
|
|
11
11
|
* Web 设置面板的线上契约:Host 半侧的路由/面板数据与 Browser 半侧的渲染共用。
|
|
@@ -44,6 +44,14 @@ window.__ModuleLoader__.load({
|
|
|
44
44
|
* [`EditableFieldWire.browse`],保留手写输入。
|
|
45
45
|
*/
|
|
46
46
|
const PICK_DIRECTORY_ROUTE = "/dsh-channels/pick-directory";
|
|
47
|
+
/** 会话的飞书来源(GET/HEAD,`?sessionId=`):对话右侧栏「飞书来源」标签与会话头部入口使用。 */
|
|
48
|
+
const SESSION_ORIGIN_ROUTE = "/dsh-channels/session-origin";
|
|
49
|
+
/**
|
|
50
|
+
* `GET PROFILES_ROUTE?users=<user_id,...>&chats=<chat_id,...>`: live Feishu
|
|
51
|
+
* names and avatars for the users and chats the panel shows. At most
|
|
52
|
+
* {@link PROFILES_MAX_IDS} ids per list.
|
|
53
|
+
*/
|
|
54
|
+
const PROFILES_ROUTE = "/dsh-channels/profiles";
|
|
47
55
|
/**
|
|
48
56
|
* Top-level panel tabs, in render order. The panel shows the connection state
|
|
49
57
|
* header above every tab, so switching tabs never hides whether the bridge is
|
|
@@ -242,7 +250,10 @@ window.__ModuleLoader__.load({
|
|
|
242
250
|
cleared: "已恢复为 profile 配置",
|
|
243
251
|
debugModeWarning: "调试模式已打开:应用可用范围内的任何人都能和机器人对话。用完请在「访问授权」关闭。",
|
|
244
252
|
accessAdminMissingWarning: "没有可用的管理员,所有对话都会被拒绝。请在下方填写管理员 user_id(私聊机器人发送 /userid 获取)。",
|
|
245
|
-
|
|
253
|
+
accessAdminCurrent: "当前管理员",
|
|
254
|
+
accessAdminSourceConfig: "已配置",
|
|
255
|
+
accessAdminSourceOwner: "应用所有者",
|
|
256
|
+
profilesWarning: "部分飞书名字和头像未能读取:{reason}",
|
|
246
257
|
accessNotLoaded: "飞书连接建立后加载授权列表",
|
|
247
258
|
accessRequestsTitle: "待处理申请",
|
|
248
259
|
accessGroupsTitle: "已授权群",
|
|
@@ -294,6 +305,69 @@ window.__ModuleLoader__.load({
|
|
|
294
305
|
});
|
|
295
306
|
}
|
|
296
307
|
//#endregion
|
|
308
|
+
//#region src/client/icon.tsx
|
|
309
|
+
/**
|
|
310
|
+
* Paper-plane glyph drawn in `currentColor`, so it follows the row's text color.
|
|
311
|
+
* @param props - the sidebar's icon share.
|
|
312
|
+
* @returns decorative icon.
|
|
313
|
+
*/
|
|
314
|
+
function FeishuChannelsIcon({ size }) {
|
|
315
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
316
|
+
width: size,
|
|
317
|
+
height: size,
|
|
318
|
+
viewBox: "0 0 24 24",
|
|
319
|
+
fill: "none",
|
|
320
|
+
stroke: "currentColor",
|
|
321
|
+
strokeWidth: 1.6,
|
|
322
|
+
strokeLinecap: "round",
|
|
323
|
+
strokeLinejoin: "round",
|
|
324
|
+
"aria-hidden": "true",
|
|
325
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M21 3 3.5 10.2a.6.6 0 0 0 0 1.1l6.6 2.6 2.6 6.6a.6.6 0 0 0 1.1 0L21 3Z" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m10.1 13.9 4.4-4.4" })]
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
//#endregion
|
|
329
|
+
//#region src/client/avatar.tsx
|
|
330
|
+
/**
|
|
331
|
+
* Round Feishu avatar: the user or chat image when Feishu returned one,
|
|
332
|
+
* otherwise (and when the image fails to load) the first character of the
|
|
333
|
+
* display text.
|
|
334
|
+
*
|
|
335
|
+
* @module dsh-channels/client/avatar
|
|
336
|
+
*/
|
|
337
|
+
/**
|
|
338
|
+
* One avatar badge. Decorative: the adjacent label names the user or chat.
|
|
339
|
+
* @param props - `text` supplies the fallback initial, `url` the image, `size` the diameter in px (default 26).
|
|
340
|
+
* @returns the badge.
|
|
341
|
+
*/
|
|
342
|
+
function Avatar(props) {
|
|
343
|
+
const { text, url, size = 26 } = props;
|
|
344
|
+
const [failed, setFailed] = (0, react.useState)(false);
|
|
345
|
+
(0, react.useEffect)(() => {
|
|
346
|
+
setFailed(false);
|
|
347
|
+
}, [url]);
|
|
348
|
+
const style = {
|
|
349
|
+
width: size,
|
|
350
|
+
height: size,
|
|
351
|
+
fontSize: Math.round(size * .46)
|
|
352
|
+
};
|
|
353
|
+
if (url !== void 0 && !failed) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
354
|
+
className: "dsh-channels-avatar",
|
|
355
|
+
style,
|
|
356
|
+
src: url,
|
|
357
|
+
alt: "",
|
|
358
|
+
"aria-hidden": "true",
|
|
359
|
+
referrerPolicy: "no-referrer",
|
|
360
|
+
loading: "lazy",
|
|
361
|
+
onError: () => setFailed(true)
|
|
362
|
+
});
|
|
363
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
364
|
+
className: "dsh-channels-avatar",
|
|
365
|
+
style,
|
|
366
|
+
"aria-hidden": "true",
|
|
367
|
+
children: Array.from(text)[0] ?? "?"
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
//#endregion
|
|
297
371
|
//#region src/client/api.ts
|
|
298
372
|
/**
|
|
299
373
|
* Same-origin HTTP client for the settings panel. No typert contract: the
|
|
@@ -312,9 +386,15 @@ window.__ModuleLoader__.load({
|
|
|
312
386
|
} catch {}
|
|
313
387
|
return `HTTP ${status}`;
|
|
314
388
|
}
|
|
315
|
-
|
|
389
|
+
/**
|
|
390
|
+
* Fetch one Host route. Routes are registered as absolute pathnames, but the
|
|
391
|
+
* browser addresses them relative to the DSH document (`<base href="./">`), so
|
|
392
|
+
* a reverse proxy that mounts DSH under a prefix such as `/dsh/` still routes
|
|
393
|
+
* them to DSH with the login cookie it scoped to that prefix.
|
|
394
|
+
*/
|
|
395
|
+
async function request(route, init) {
|
|
316
396
|
try {
|
|
317
|
-
return await fetch(
|
|
397
|
+
return await fetch(route.slice(1), {
|
|
318
398
|
cache: "no-store",
|
|
319
399
|
credentials: "same-origin",
|
|
320
400
|
...init,
|
|
@@ -340,6 +420,41 @@ window.__ModuleLoader__.load({
|
|
|
340
420
|
async function fetchStatus() {
|
|
341
421
|
return readJson(await request(STATUS_ROUTE));
|
|
342
422
|
}
|
|
423
|
+
/** Feishu origin of one Session (`origin: null` when Feishu never routed it). */
|
|
424
|
+
async function fetchSessionOrigin(sessionId) {
|
|
425
|
+
return readJson(await request(`${SESSION_ORIGIN_ROUTE}?${new URLSearchParams({ sessionId }).toString()}`));
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Live Feishu names and avatars of the given users and chats, split into
|
|
429
|
+
* requests of at most {@link PROFILES_MAX_IDS} ids per list and merged.
|
|
430
|
+
* @param userIds - user `user_id`s.
|
|
431
|
+
* @param chatIds - chat `chat_id`s.
|
|
432
|
+
* @returns the profiles Feishu resolved, by id, with every request's warning.
|
|
433
|
+
*/
|
|
434
|
+
async function fetchFeishuProfiles(userIds, chatIds) {
|
|
435
|
+
const requests = [];
|
|
436
|
+
const rounds = Math.max(1, Math.ceil(userIds.length / 200), Math.ceil(chatIds.length / 200));
|
|
437
|
+
for (let round = 0; round < rounds; round += 1) {
|
|
438
|
+
const window = (ids) => ids.slice(round * 200, (round + 1) * 200).join(",");
|
|
439
|
+
const query = new URLSearchParams({
|
|
440
|
+
users: window(userIds),
|
|
441
|
+
chats: window(chatIds)
|
|
442
|
+
}).toString();
|
|
443
|
+
requests.push(request(`${PROFILES_ROUTE}?${query}`).then((response) => readJson(response)));
|
|
444
|
+
}
|
|
445
|
+
const merged = {
|
|
446
|
+
users: {},
|
|
447
|
+
chats: {}
|
|
448
|
+
};
|
|
449
|
+
const warnings = /* @__PURE__ */ new Set();
|
|
450
|
+
for (const part of await Promise.all(requests)) {
|
|
451
|
+
Object.assign(merged.users, part.users);
|
|
452
|
+
Object.assign(merged.chats, part.chats);
|
|
453
|
+
if (part.warning !== void 0) warnings.add(part.warning);
|
|
454
|
+
}
|
|
455
|
+
if (warnings.size > 0) merged.warning = [...warnings].join(";");
|
|
456
|
+
return merged;
|
|
457
|
+
}
|
|
343
458
|
/**
|
|
344
459
|
* Model-selector catalog for one route: providers, the provider's models, the
|
|
345
460
|
* model's reasoning efforts, and the preset roster. The client re-queries it
|
|
@@ -516,6 +631,257 @@ window.__ModuleLoader__.load({
|
|
|
516
631
|
});
|
|
517
632
|
}
|
|
518
633
|
//#endregion
|
|
634
|
+
//#region src/client/origin-cache.ts
|
|
635
|
+
/**
|
|
636
|
+
* How long a "not from Feishu" answer is reused. A Session shows up in the
|
|
637
|
+
* Web list before its Feishu route is written, so an early null must expire.
|
|
638
|
+
*/
|
|
639
|
+
const NULL_ORIGIN_TTL_MS = 15e3;
|
|
640
|
+
/** One settled or pending origin lookup per Session; failures are not kept. */
|
|
641
|
+
const lookups = /* @__PURE__ */ new Map();
|
|
642
|
+
/**
|
|
643
|
+
* Origin of one Session through the shared cache. An origin is kept for the
|
|
644
|
+
* page's lifetime; a null answer only for {@link NULL_ORIGIN_TTL_MS}.
|
|
645
|
+
* @param sessionId - DSH session id.
|
|
646
|
+
* @param refresh - drop the cached answer and ask the Host again.
|
|
647
|
+
* @returns the origin, or null for a Session Feishu never routed.
|
|
648
|
+
*/
|
|
649
|
+
function loadSessionOrigin(sessionId, refresh = false) {
|
|
650
|
+
const cached = refresh ? void 0 : lookups.get(sessionId);
|
|
651
|
+
if (cached !== void 0 && (cached.nullAt === void 0 || Date.now() - cached.nullAt < NULL_ORIGIN_TTL_MS)) return cached.pending;
|
|
652
|
+
const pending = fetchSessionOrigin(sessionId).then((response) => response.origin);
|
|
653
|
+
const lookup = { pending };
|
|
654
|
+
lookups.set(sessionId, lookup);
|
|
655
|
+
pending.then((origin) => {
|
|
656
|
+
if (origin === null) lookup.nullAt = Date.now();
|
|
657
|
+
}, () => {
|
|
658
|
+
if (lookups.get(sessionId) === lookup) lookups.delete(sessionId);
|
|
659
|
+
});
|
|
660
|
+
return pending;
|
|
661
|
+
}
|
|
662
|
+
//#endregion
|
|
663
|
+
//#region src/client/origin.tsx
|
|
664
|
+
/**
|
|
665
|
+
* 对话里的「飞书来源」:会话头部的入口按钮(只在飞书发起的会话里出现)与
|
|
666
|
+
* 右侧栏的来源标签页。两者共用一份按会话缓存的来源查询。
|
|
667
|
+
*
|
|
668
|
+
* @module dsh-channels/client/origin
|
|
669
|
+
*/
|
|
670
|
+
/** Right-sidebar tab kind opened by the header button. */
|
|
671
|
+
const ORIGIN_TAB_KIND = "feishu-origin";
|
|
672
|
+
/** Tab-type implementation id and keyed body/title slot key. */
|
|
673
|
+
const ORIGIN_TAB_ID = "@modesto_ma/dsh-channels/origin";
|
|
674
|
+
/** Tab chip text. */
|
|
675
|
+
const ORIGIN_TAB_TITLE = "飞书来源";
|
|
676
|
+
/**
|
|
677
|
+
* Load one Session's origin; `reload` asks the Host again. `fresh` skips the
|
|
678
|
+
* shared cache on the first load too, so names and avatars are current.
|
|
679
|
+
*/
|
|
680
|
+
function useSessionOrigin(sessionId, fresh = false) {
|
|
681
|
+
const [state, setState] = (0, react.useState)({ phase: "loading" });
|
|
682
|
+
const [reloadOf, setReloadOf] = (0, react.useState)({
|
|
683
|
+
sessionId,
|
|
684
|
+
ticket: 0
|
|
685
|
+
});
|
|
686
|
+
const ticket = reloadOf.sessionId === sessionId ? reloadOf.ticket : 0;
|
|
687
|
+
(0, react.useEffect)(() => {
|
|
688
|
+
let live = true;
|
|
689
|
+
setState((current) => current.phase === "ready" && ticket > 0 ? current : { phase: "loading" });
|
|
690
|
+
loadSessionOrigin(sessionId, fresh || ticket > 0).then((origin) => {
|
|
691
|
+
if (live) setState({
|
|
692
|
+
phase: "ready",
|
|
693
|
+
origin
|
|
694
|
+
});
|
|
695
|
+
}, (error) => {
|
|
696
|
+
if (live) setState({
|
|
697
|
+
phase: "error",
|
|
698
|
+
message: error instanceof Error ? error.message : String(error)
|
|
699
|
+
});
|
|
700
|
+
});
|
|
701
|
+
return () => {
|
|
702
|
+
live = false;
|
|
703
|
+
};
|
|
704
|
+
}, [
|
|
705
|
+
fresh,
|
|
706
|
+
sessionId,
|
|
707
|
+
ticket
|
|
708
|
+
]);
|
|
709
|
+
return [state, (0, react.useCallback)(() => {
|
|
710
|
+
setReloadOf((current) => ({
|
|
711
|
+
sessionId,
|
|
712
|
+
ticket: current.sessionId === sessionId ? current.ticket + 1 : 1
|
|
713
|
+
}));
|
|
714
|
+
}, [sessionId])];
|
|
715
|
+
}
|
|
716
|
+
/** Header chip text: group name, or 私聊 with the peer's name. */
|
|
717
|
+
function sourceLabel$1(origin) {
|
|
718
|
+
if (origin.chatType === "p2p") return origin.senderName === void 0 ? "飞书私聊" : `私聊 · ${origin.senderName}`;
|
|
719
|
+
return origin.chatName === void 0 ? "飞书群聊" : origin.chatName;
|
|
720
|
+
}
|
|
721
|
+
/** Tab heading: the group, or the private-chat peer, with its avatar. */
|
|
722
|
+
function originHeading(origin) {
|
|
723
|
+
if (origin.chatType === "p2p") return {
|
|
724
|
+
title: origin.senderName ?? "飞书私聊",
|
|
725
|
+
...origin.senderAvatarUrl === void 0 ? {} : { avatarUrl: origin.senderAvatarUrl }
|
|
726
|
+
};
|
|
727
|
+
return {
|
|
728
|
+
title: origin.chatName ?? "飞书群聊",
|
|
729
|
+
...origin.chatAvatarUrl === void 0 ? {} : { avatarUrl: origin.chatAvatarUrl }
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
/** Times a mounted header re-asks after a null answer, and the delay between asks. */
|
|
733
|
+
const HEADER_NULL_RETRIES = 3;
|
|
734
|
+
const HEADER_NULL_RETRY_MS = 5e3;
|
|
735
|
+
/**
|
|
736
|
+
* Header button shown only for Sessions Feishu routed; renders nothing for
|
|
737
|
+
* every other Session and while the lookup is pending or failed.
|
|
738
|
+
* @param props - session id and the tab opener.
|
|
739
|
+
* @returns the button or null.
|
|
740
|
+
*/
|
|
741
|
+
function OriginHeaderAction({ sessionId, openOrigin }) {
|
|
742
|
+
const [state, reload] = useSessionOrigin(sessionId);
|
|
743
|
+
const [retries, setRetries] = (0, react.useState)(0);
|
|
744
|
+
(0, react.useEffect)(() => {
|
|
745
|
+
setRetries(0);
|
|
746
|
+
}, [sessionId]);
|
|
747
|
+
const waiting = state.phase === "ready" && state.origin === null && retries < HEADER_NULL_RETRIES;
|
|
748
|
+
(0, react.useEffect)(() => {
|
|
749
|
+
if (!waiting) return;
|
|
750
|
+
const timer = setTimeout(() => {
|
|
751
|
+
setRetries((value) => value + 1);
|
|
752
|
+
reload();
|
|
753
|
+
}, HEADER_NULL_RETRY_MS);
|
|
754
|
+
return () => {
|
|
755
|
+
clearTimeout(timer);
|
|
756
|
+
};
|
|
757
|
+
}, [waiting, reload]);
|
|
758
|
+
if (state.phase !== "ready" || state.origin === null) return null;
|
|
759
|
+
const label = sourceLabel$1(state.origin);
|
|
760
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
761
|
+
type: "button",
|
|
762
|
+
className: "dsh-channels-origin-chip",
|
|
763
|
+
title: `来自${label},点击查看飞书来源`,
|
|
764
|
+
"aria-label": `飞书来源:${label}`,
|
|
765
|
+
onClick: openOrigin,
|
|
766
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FeishuChannelsIcon, {
|
|
767
|
+
size: 13,
|
|
768
|
+
active: false
|
|
769
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
770
|
+
className: "dsh-channels-origin-chip__text",
|
|
771
|
+
children: label
|
|
772
|
+
})]
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
function formatTime(ms) {
|
|
776
|
+
const date = new Date(ms);
|
|
777
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
778
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Right-sidebar body: where this Session came from in Feishu.
|
|
782
|
+
* @param props - the tab's Session.
|
|
783
|
+
* @returns the origin page.
|
|
784
|
+
*/
|
|
785
|
+
function OriginTab({ sessionId }) {
|
|
786
|
+
const [state, reload] = useSessionOrigin(sessionId, true);
|
|
787
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
788
|
+
className: "dsh-channels-origin",
|
|
789
|
+
children: [
|
|
790
|
+
state.phase === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
791
|
+
className: "dsh-channels-origin__muted",
|
|
792
|
+
children: "正在读取飞书来源…"
|
|
793
|
+
}) : null,
|
|
794
|
+
state.phase === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
795
|
+
className: "dsh-channels-origin__error",
|
|
796
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: ["读取飞书来源失败:", state.message] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
797
|
+
type: "button",
|
|
798
|
+
className: "dsh-channels-origin__link",
|
|
799
|
+
onClick: reload,
|
|
800
|
+
children: "重试"
|
|
801
|
+
})]
|
|
802
|
+
}) : null,
|
|
803
|
+
state.phase === "ready" && state.origin === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
804
|
+
className: "dsh-channels-origin__muted",
|
|
805
|
+
children: ["这个会话不是从飞书发起的。", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
806
|
+
type: "button",
|
|
807
|
+
className: "dsh-channels-origin__link",
|
|
808
|
+
onClick: reload,
|
|
809
|
+
children: "刷新"
|
|
810
|
+
})]
|
|
811
|
+
}) : null,
|
|
812
|
+
state.phase === "ready" && state.origin !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OriginDetails, {
|
|
813
|
+
origin: state.origin,
|
|
814
|
+
onRefresh: reload
|
|
815
|
+
}) : null
|
|
816
|
+
]
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
function OriginDetails({ origin, onRefresh }) {
|
|
820
|
+
const p2p = origin.chatType === "p2p";
|
|
821
|
+
const rows = [["来源", p2p ? "私聊" : origin.kind === "thread" ? "群聊 · 话题" : "群聊"]];
|
|
822
|
+
if (!p2p && origin.chatName === void 0) rows.push(["群 ID", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
823
|
+
className: "dsh-channels-origin__mono",
|
|
824
|
+
children: origin.chatId
|
|
825
|
+
})]);
|
|
826
|
+
rows.push([origin.shared ? "请求人" : "发起人", /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
827
|
+
className: "dsh-channels-origin__who",
|
|
828
|
+
children: [
|
|
829
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, {
|
|
830
|
+
text: origin.senderName ?? origin.senderUserId,
|
|
831
|
+
url: origin.senderAvatarUrl,
|
|
832
|
+
size: 20
|
|
833
|
+
}),
|
|
834
|
+
origin.senderName === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: origin.senderName }),
|
|
835
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
836
|
+
className: "dsh-channels-origin__mono dsh-channels-origin__muted",
|
|
837
|
+
children: origin.senderUserId
|
|
838
|
+
})
|
|
839
|
+
]
|
|
840
|
+
})]);
|
|
841
|
+
const heading = originHeading(origin);
|
|
842
|
+
if (origin.shared) rows.push(["说明", "日志平台分析话题,群内成员都可以继续"]);
|
|
843
|
+
rows.push(["开始于", formatTime(origin.createdAt)]);
|
|
844
|
+
rows.push(origin.bound && origin.lastActivityAt !== void 0 ? ["最近活跃", formatTime(origin.lastActivityAt)] : ["状态", "飞书侧已切换到新会话或已过期"]);
|
|
845
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
846
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
847
|
+
className: "dsh-channels-origin__head",
|
|
848
|
+
children: [heading.avatarUrl === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FeishuChannelsIcon, {
|
|
849
|
+
size: 16,
|
|
850
|
+
active: false
|
|
851
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, {
|
|
852
|
+
text: heading.title,
|
|
853
|
+
url: heading.avatarUrl,
|
|
854
|
+
size: 28
|
|
855
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
856
|
+
className: "dsh-channels-origin__title",
|
|
857
|
+
children: heading.title
|
|
858
|
+
})]
|
|
859
|
+
}),
|
|
860
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dl", {
|
|
861
|
+
className: "dsh-channels-origin__list",
|
|
862
|
+
children: rows.map(([label, value]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
863
|
+
className: "dsh-channels-origin__row",
|
|
864
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: value })]
|
|
865
|
+
}, label))
|
|
866
|
+
}),
|
|
867
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
868
|
+
className: "dsh-channels-origin__actions",
|
|
869
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
870
|
+
className: "dsh-channels-origin__button",
|
|
871
|
+
href: origin.chatLink,
|
|
872
|
+
target: "_blank",
|
|
873
|
+
rel: "noreferrer",
|
|
874
|
+
children: "在飞书中打开"
|
|
875
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
876
|
+
type: "button",
|
|
877
|
+
className: "dsh-channels-origin__link",
|
|
878
|
+
onClick: onRefresh,
|
|
879
|
+
children: "刷新"
|
|
880
|
+
})]
|
|
881
|
+
})
|
|
882
|
+
] });
|
|
883
|
+
}
|
|
884
|
+
//#endregion
|
|
519
885
|
//#region src/client/catalog.ts
|
|
520
886
|
/**
|
|
521
887
|
* 浏览器半区的设置面板选择器目录折叠:把 Host 下发的字段描述与实时目录
|
|
@@ -1452,14 +1818,6 @@ window.__ModuleLoader__.load({
|
|
|
1452
1818
|
default: return PANEL_COPY.accessSourcePanel;
|
|
1453
1819
|
}
|
|
1454
1820
|
}
|
|
1455
|
-
/** Round initial badge in front of a whitelist entry. */
|
|
1456
|
-
function Avatar(props) {
|
|
1457
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1458
|
-
className: "dsh-channels-cfg__avatar",
|
|
1459
|
-
"aria-hidden": "true",
|
|
1460
|
-
children: Array.from(props.text)[0] ?? "?"
|
|
1461
|
-
});
|
|
1462
|
-
}
|
|
1463
1821
|
/**
|
|
1464
1822
|
* One titled whitelist with remove buttons and a collapsed add form.
|
|
1465
1823
|
* `onAdd` resolves true when the entry was added (the form then closes).
|
|
@@ -1484,7 +1842,10 @@ window.__ModuleLoader__.load({
|
|
|
1484
1842
|
!loaded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyRow, { text: PANEL_COPY.accessNotLoaded }) : null,
|
|
1485
1843
|
loaded && entries.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyRow, { text: empty }) : null,
|
|
1486
1844
|
entries.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1487
|
-
leading: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, {
|
|
1845
|
+
leading: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, {
|
|
1846
|
+
text: avatar ?? entry.name ?? entry.id,
|
|
1847
|
+
url: entry.avatarUrl
|
|
1848
|
+
}),
|
|
1488
1849
|
label: entry.name ?? entry.id,
|
|
1489
1850
|
summary: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1490
1851
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -1554,15 +1915,20 @@ window.__ModuleLoader__.load({
|
|
|
1554
1915
|
}
|
|
1555
1916
|
/** Pending authorization requests, settled with the same resolver as the DM card. */
|
|
1556
1917
|
function RequestList(props) {
|
|
1557
|
-
const { requests, disabled, working, onResolve } = props;
|
|
1918
|
+
const { requests, profiles, disabled, working, onResolve } = props;
|
|
1558
1919
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Section, {
|
|
1559
1920
|
title: `${PANEL_COPY.accessRequestsTitle} · ${requests.length}`,
|
|
1560
1921
|
tone: "warn",
|
|
1561
1922
|
children: requests.map((request) => {
|
|
1562
|
-
const
|
|
1563
|
-
const
|
|
1923
|
+
const profile = request.kind === "user" ? profiles.user(request.targetId) : profiles.chat(request.targetId);
|
|
1924
|
+
const target = profile?.name ?? request.name ?? request.targetId;
|
|
1925
|
+
const group = request.chatId === void 0 ? request.groupName : profiles.chat(request.chatId)?.name ?? request.groupName ?? request.chatId;
|
|
1926
|
+
const where = request.kind === "user" && group !== void 0 ? fillCopy(PANEL_COPY.accessRequestIn, { group }) : "";
|
|
1564
1927
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1565
|
-
leading: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, {
|
|
1928
|
+
leading: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, {
|
|
1929
|
+
text: target,
|
|
1930
|
+
url: profile?.avatarUrl
|
|
1931
|
+
}),
|
|
1566
1932
|
label: `${target} ${where}${request.kind === "group" ? PANEL_COPY.accessRequestGroup : PANEL_COPY.accessRequestUser}`,
|
|
1567
1933
|
summary: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1568
1934
|
className: "dsh-channels-cfg__mono",
|
|
@@ -1601,15 +1967,10 @@ window.__ModuleLoader__.load({
|
|
|
1601
1967
|
* @module dsh-channels/client/panel/access-tab
|
|
1602
1968
|
*/
|
|
1603
1969
|
function AccessTab(props) {
|
|
1604
|
-
const { form, accessNotice, accessError, onAddGroup, onRemoveGroup, onAddUser, onRemoveUser, onResolveRequest } = props;
|
|
1970
|
+
const { form, profiles, accessNotice, accessError, onAddGroup, onRemoveGroup, onAddUser, onRemoveUser, onResolveRequest } = props;
|
|
1605
1971
|
const { access } = form.panel;
|
|
1606
1972
|
const working = form.busy === "access";
|
|
1607
1973
|
const disabled = form.busy !== null || form.locked || !access.loaded;
|
|
1608
|
-
const ownerAdmin = access.mode === "guarded" && access.admin.source === "owner" ? access.admin.userId : void 0;
|
|
1609
|
-
const fields = ownerAdmin === void 0 ? form.fields : form.fields.map((field) => field.key === "adminUserId" ? {
|
|
1610
|
-
...field,
|
|
1611
|
-
summary: fillCopy(PANEL_COPY.accessAdminOwnerSummary, { id: ownerAdmin })
|
|
1612
|
-
} : field);
|
|
1613
1974
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1614
1975
|
className: "dsh-channels-cfg__tabBody",
|
|
1615
1976
|
children: [
|
|
@@ -1625,24 +1986,31 @@ window.__ModuleLoader__.load({
|
|
|
1625
1986
|
kind: "error",
|
|
1626
1987
|
text: access.admin.ownerLookupError === void 0 ? PANEL_COPY.accessAdminMissingWarning : `${PANEL_COPY.accessAdminMissingWarning}(${access.admin.ownerLookupError})`
|
|
1627
1988
|
}) : null,
|
|
1989
|
+
profiles.warning === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1990
|
+
className: "dsh-channels-cfg__secFoot",
|
|
1991
|
+
children: fillCopy(PANEL_COPY.profilesWarning, { reason: profiles.warning })
|
|
1992
|
+
}),
|
|
1628
1993
|
access.loaded && access.requests.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RequestList, {
|
|
1629
1994
|
requests: access.requests,
|
|
1995
|
+
profiles,
|
|
1630
1996
|
disabled,
|
|
1631
1997
|
working,
|
|
1632
1998
|
onResolve: onResolveRequest
|
|
1633
1999
|
}) : null,
|
|
1634
2000
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldSections, {
|
|
1635
|
-
form
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
2001
|
+
form,
|
|
2002
|
+
groups: TAB_FIELD_GROUPS.access,
|
|
2003
|
+
extra: { access: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AdminRow, {
|
|
2004
|
+
admin: access.admin,
|
|
2005
|
+
profiles
|
|
2006
|
+
}) }
|
|
1640
2007
|
}),
|
|
1641
2008
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Whitelist, {
|
|
1642
2009
|
title: PANEL_COPY.accessGroupsTitle,
|
|
1643
2010
|
entries: access.groups.map((group) => ({
|
|
1644
2011
|
id: group.chatId,
|
|
1645
|
-
name: group.name,
|
|
2012
|
+
name: profiles.chat(group.chatId)?.name ?? group.name,
|
|
2013
|
+
avatarUrl: profiles.chat(group.chatId)?.avatarUrl,
|
|
1646
2014
|
source: group.source
|
|
1647
2015
|
})),
|
|
1648
2016
|
avatar: "群",
|
|
@@ -1660,7 +2028,8 @@ window.__ModuleLoader__.load({
|
|
|
1660
2028
|
title: PANEL_COPY.accessUsersTitle,
|
|
1661
2029
|
entries: access.users.map((user) => ({
|
|
1662
2030
|
id: user.userId,
|
|
1663
|
-
name: user.name,
|
|
2031
|
+
name: profiles.user(user.userId)?.name ?? user.name,
|
|
2032
|
+
avatarUrl: profiles.user(user.userId)?.avatarUrl,
|
|
1664
2033
|
source: user.source
|
|
1665
2034
|
})),
|
|
1666
2035
|
empty: PANEL_COPY.accessUsersEmpty,
|
|
@@ -1676,6 +2045,31 @@ window.__ModuleLoader__.load({
|
|
|
1676
2045
|
]
|
|
1677
2046
|
});
|
|
1678
2047
|
}
|
|
2048
|
+
/** The effective administrator with their Feishu name and avatar; nothing while there is none. */
|
|
2049
|
+
function AdminRow(props) {
|
|
2050
|
+
const { admin, profiles } = props;
|
|
2051
|
+
if (admin.userId === void 0) return null;
|
|
2052
|
+
const profile = profiles.user(admin.userId);
|
|
2053
|
+
const name = profile?.name ?? admin.userId;
|
|
2054
|
+
const source = admin.source === "owner" ? PANEL_COPY.accessAdminSourceOwner : PANEL_COPY.accessAdminSourceConfig;
|
|
2055
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
2056
|
+
leading: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, {
|
|
2057
|
+
text: name,
|
|
2058
|
+
url: profile?.avatarUrl
|
|
2059
|
+
}),
|
|
2060
|
+
label: name,
|
|
2061
|
+
summary: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
2062
|
+
PANEL_COPY.accessAdminCurrent,
|
|
2063
|
+
" · ",
|
|
2064
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2065
|
+
className: "dsh-channels-cfg__mono",
|
|
2066
|
+
children: admin.userId
|
|
2067
|
+
}),
|
|
2068
|
+
" · ",
|
|
2069
|
+
source
|
|
2070
|
+
] })
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
1679
2073
|
//#endregion
|
|
1680
2074
|
//#region src/client/panel/credentials-card.tsx
|
|
1681
2075
|
/**
|
|
@@ -2159,6 +2553,76 @@ window.__ModuleLoader__.load({
|
|
|
2159
2553
|
});
|
|
2160
2554
|
}
|
|
2161
2555
|
//#endregion
|
|
2556
|
+
//#region src/client/panel/profiles.ts
|
|
2557
|
+
/**
|
|
2558
|
+
* Live Feishu names and avatars for every user and chat the access and wiki
|
|
2559
|
+
* tabs show. The page asks the Host once on entry and again whenever the set
|
|
2560
|
+
* of shown ids changes; a failed request keeps the previous answer.
|
|
2561
|
+
*
|
|
2562
|
+
* @module dsh-channels/client/panel/profiles
|
|
2563
|
+
*/
|
|
2564
|
+
/**
|
|
2565
|
+
* The `user_id`s and `chat_id`s one access snapshot shows.
|
|
2566
|
+
* @param access - the panel's access snapshot.
|
|
2567
|
+
* @returns sorted, distinct ids per kind.
|
|
2568
|
+
*/
|
|
2569
|
+
function profileIds(access) {
|
|
2570
|
+
const users = /* @__PURE__ */ new Set();
|
|
2571
|
+
const chats = /* @__PURE__ */ new Set();
|
|
2572
|
+
if (access.admin.userId !== void 0) users.add(access.admin.userId);
|
|
2573
|
+
for (const user of access.users) users.add(user.userId);
|
|
2574
|
+
for (const user of access.wikiUsers) users.add(user.userId);
|
|
2575
|
+
for (const group of access.groups) chats.add(group.chatId);
|
|
2576
|
+
for (const request of access.requests) {
|
|
2577
|
+
if (request.kind === "user") users.add(request.targetId);
|
|
2578
|
+
else chats.add(request.targetId);
|
|
2579
|
+
if (request.chatId !== void 0) chats.add(request.chatId);
|
|
2580
|
+
}
|
|
2581
|
+
return {
|
|
2582
|
+
users: [...users].sort(),
|
|
2583
|
+
chats: [...chats].sort()
|
|
2584
|
+
};
|
|
2585
|
+
}
|
|
2586
|
+
/**
|
|
2587
|
+
* Profiles for one access snapshot; empty until the panel has loaded.
|
|
2588
|
+
* @param access - the panel's access snapshot, null while loading.
|
|
2589
|
+
* @returns lookups by id and the latest failure reason.
|
|
2590
|
+
*/
|
|
2591
|
+
function useFeishuProfiles(access) {
|
|
2592
|
+
const ids = (0, react.useMemo)(() => access === null ? {
|
|
2593
|
+
users: [],
|
|
2594
|
+
chats: []
|
|
2595
|
+
} : profileIds(access), [access]);
|
|
2596
|
+
const key = `${ids.users.join(",")}|${ids.chats.join(",")}`;
|
|
2597
|
+
const [answer, setAnswer] = (0, react.useState)({
|
|
2598
|
+
users: {},
|
|
2599
|
+
chats: {}
|
|
2600
|
+
});
|
|
2601
|
+
const [requestError, setRequestError] = (0, react.useState)(void 0);
|
|
2602
|
+
(0, react.useEffect)(() => {
|
|
2603
|
+
if (ids.users.length === 0 && ids.chats.length === 0) return;
|
|
2604
|
+
let current = true;
|
|
2605
|
+
fetchFeishuProfiles(ids.users, ids.chats).then((next) => {
|
|
2606
|
+
if (!current) return;
|
|
2607
|
+
setAnswer(next);
|
|
2608
|
+
setRequestError(void 0);
|
|
2609
|
+
}, (error) => {
|
|
2610
|
+
if (current) setRequestError(error instanceof Error ? error.message : String(error));
|
|
2611
|
+
});
|
|
2612
|
+
return () => {
|
|
2613
|
+
current = false;
|
|
2614
|
+
};
|
|
2615
|
+
}, [key]);
|
|
2616
|
+
return (0, react.useMemo)(() => {
|
|
2617
|
+
const warning = requestError ?? answer.warning;
|
|
2618
|
+
return {
|
|
2619
|
+
user: (userId) => answer.users[userId],
|
|
2620
|
+
chat: (chatId) => answer.chats[chatId],
|
|
2621
|
+
...warning === void 0 ? {} : { warning }
|
|
2622
|
+
};
|
|
2623
|
+
}, [answer, requestError]);
|
|
2624
|
+
}
|
|
2625
|
+
//#endregion
|
|
2162
2626
|
//#region src/client/panel/wiki-tab.tsx
|
|
2163
2627
|
/**
|
|
2164
2628
|
* 「知识库」tab:知识库工具字段(读取 / 编辑开关、可访问范围)+ 知识库
|
|
@@ -2168,7 +2632,7 @@ window.__ModuleLoader__.load({
|
|
|
2168
2632
|
* @module dsh-channels/client/panel/wiki-tab
|
|
2169
2633
|
*/
|
|
2170
2634
|
function WikiTab(props) {
|
|
2171
|
-
const { form, accessNotice, accessError, onAddUser, onRemoveUser } = props;
|
|
2635
|
+
const { form, profiles, accessNotice, accessError, onAddUser, onRemoveUser } = props;
|
|
2172
2636
|
const { access } = form.panel;
|
|
2173
2637
|
const disabled = form.busy !== null || form.locked || !access.loaded;
|
|
2174
2638
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -2182,6 +2646,10 @@ window.__ModuleLoader__.load({
|
|
|
2182
2646
|
kind: "error",
|
|
2183
2647
|
text: accessError
|
|
2184
2648
|
}),
|
|
2649
|
+
profiles.warning === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2650
|
+
className: "dsh-channels-cfg__secFoot",
|
|
2651
|
+
children: fillCopy(PANEL_COPY.profilesWarning, { reason: profiles.warning })
|
|
2652
|
+
}),
|
|
2185
2653
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldSections, {
|
|
2186
2654
|
form,
|
|
2187
2655
|
groups: TAB_FIELD_GROUPS.wiki
|
|
@@ -2191,7 +2659,8 @@ window.__ModuleLoader__.load({
|
|
|
2191
2659
|
countLabel: fillCopy(PANEL_COPY.wikiUsersAdminPlus, { n: access.wikiUsers.length }),
|
|
2192
2660
|
entries: access.wikiUsers.map((user) => ({
|
|
2193
2661
|
id: user.userId,
|
|
2194
|
-
name: user.name,
|
|
2662
|
+
name: profiles.user(user.userId)?.name ?? user.name,
|
|
2663
|
+
avatarUrl: profiles.user(user.userId)?.avatarUrl,
|
|
2195
2664
|
source: user.source
|
|
2196
2665
|
})),
|
|
2197
2666
|
empty: PANEL_COPY.wikiUsersEmpty,
|
|
@@ -2211,18 +2680,32 @@ window.__ModuleLoader__.load({
|
|
|
2211
2680
|
//#endregion
|
|
2212
2681
|
//#region src/client/panel/index.tsx
|
|
2213
2682
|
/**
|
|
2214
|
-
*
|
|
2683
|
+
* 左侧栏「飞书渠道」主区页面:数据加载、5 秒状态轮询、全局 draft、顶部状态头 /
|
|
2215
2684
|
* tab 导航与底部保存条的外壳。
|
|
2216
2685
|
*
|
|
2217
2686
|
* 所有 Host 交互(panel / status / catalog / config / credentials / access /
|
|
2218
2687
|
* pick-directory)都集中在这里;tab 组件只接收渲染所需的数据与
|
|
2219
|
-
* 回调。页面不 import 任何 DSH 客户端包,只依赖 react 与 `
|
|
2220
|
-
* 槽位。
|
|
2688
|
+
* 回调。页面不 import 任何 DSH 客户端包,只依赖 react 与 `main` 槽位。
|
|
2221
2689
|
*
|
|
2222
2690
|
* @module dsh-channels/client/panel
|
|
2223
2691
|
*/
|
|
2224
|
-
/**
|
|
2225
|
-
|
|
2692
|
+
/**
|
|
2693
|
+
* Main-column page for the `main` slot: a full-height scroll region with the
|
|
2694
|
+
* centered panel column. The slot passes no owner props.
|
|
2695
|
+
* @returns the page.
|
|
2696
|
+
*/
|
|
2697
|
+
function FeishuChannelsPage() {
|
|
2698
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
2699
|
+
className: "dsh-channels-page",
|
|
2700
|
+
"aria-label": PANEL_COPY.title,
|
|
2701
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2702
|
+
className: "dsh-channels-page__content",
|
|
2703
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FeishuChannelsPanel, {})
|
|
2704
|
+
})
|
|
2705
|
+
});
|
|
2706
|
+
}
|
|
2707
|
+
/** Panel shell: one panel payload, one status poll, one global config draft. */
|
|
2708
|
+
function FeishuChannelsPanel() {
|
|
2226
2709
|
const [load, setLoad] = (0, react.useState)({ phase: "loading" });
|
|
2227
2710
|
const [activeTab, setActiveTab] = (0, react.useState)("connection");
|
|
2228
2711
|
const [draft, setDraft] = (0, react.useState)({});
|
|
@@ -2844,6 +3327,7 @@ window.__ModuleLoader__.load({
|
|
|
2844
3327
|
renderedFields,
|
|
2845
3328
|
updateDraft
|
|
2846
3329
|
]);
|
|
3330
|
+
const profiles = useFeishuProfiles(panel?.access ?? null);
|
|
2847
3331
|
const credentials = (0, react.useMemo)(() => ({
|
|
2848
3332
|
appId: appIdInput,
|
|
2849
3333
|
appSecret: appSecretInput,
|
|
@@ -2969,6 +3453,7 @@ window.__ModuleLoader__.load({
|
|
|
2969
3453
|
}) : null,
|
|
2970
3454
|
activeTab === "access" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccessTab, {
|
|
2971
3455
|
form,
|
|
3456
|
+
profiles,
|
|
2972
3457
|
accessNotice,
|
|
2973
3458
|
accessError,
|
|
2974
3459
|
onAddGroup: onAddAccessGroup,
|
|
@@ -2985,6 +3470,7 @@ window.__ModuleLoader__.load({
|
|
|
2985
3470
|
}) : null,
|
|
2986
3471
|
activeTab === "wiki" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WikiTab, {
|
|
2987
3472
|
form,
|
|
3473
|
+
profiles,
|
|
2988
3474
|
accessNotice,
|
|
2989
3475
|
accessError,
|
|
2990
3476
|
onAddUser: (userId, name) => addUser(addWikiUser, userId, name, PANEL_COPY.wikiUserAdded),
|
|
@@ -3023,7 +3509,7 @@ window.__ModuleLoader__.load({
|
|
|
3023
3509
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3024
3510
|
className: "dsh-channels-cfg__headRow",
|
|
3025
3511
|
children: [
|
|
3026
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
3512
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
|
|
3027
3513
|
className: "dsh-channels-cfg__title",
|
|
3028
3514
|
children: PANEL_COPY.title
|
|
3029
3515
|
}),
|
|
@@ -3131,7 +3617,7 @@ window.__ModuleLoader__.load({
|
|
|
3131
3617
|
//#endregion
|
|
3132
3618
|
//#region src/client/styles.ts
|
|
3133
3619
|
/**
|
|
3134
|
-
*
|
|
3620
|
+
* Feishu channel page styling. DSH semantic tokens (`--dsw-alias-*`) with neutral
|
|
3135
3621
|
* fallbacks, aliased once to local `--dc-*` variables, so the page follows
|
|
3136
3622
|
* the active theme.
|
|
3137
3623
|
*
|
|
@@ -3139,10 +3625,13 @@ window.__ModuleLoader__.load({
|
|
|
3139
3625
|
*/
|
|
3140
3626
|
const STYLE_ATTRIBUTE = "data-dsh-channels-css";
|
|
3141
3627
|
const CSS = `
|
|
3628
|
+
.dsh-channels-page{width:100%;height:100%;min-width:0;min-height:0;overflow:auto;scrollbar-gutter:stable;background:var(--dsw-alias-bg-base,#fff)}
|
|
3629
|
+
.dsh-channels-page__content{box-sizing:border-box;max-width:816px;margin:0 auto;padding:18px clamp(24px,4vw,48px) 48px}
|
|
3630
|
+
[data-platform='darwin'] .dsh-channels-page__content{padding-top:calc(18px + var(--dsh-frame-top-clearance,0px))}
|
|
3142
3631
|
.dsh-channels-cfg{--dc-fg:var(--dsw-alias-label-primary,#1f2328);--dc-fg2:var(--dsw-alias-label-secondary,#5c6169);--dc-fg3:var(--dsw-alias-label-tertiary,#8a8f98);--dc-dim:var(--dsw-alias-label-dimmed,#b0b4ba);--dc-bg:var(--dsw-alias-bg-base,#fff);--dc-line:var(--dsw-alias-border-l2,rgba(0,0,0,.12));--dc-hover:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.05));--dc-accent:var(--dsw-alias-state-business-primary,#0a67d3);--dc-ok:var(--dsw-alias-state-success-primary,#0a7d33);--dc-warn:var(--dsw-alias-state-warn-primary,#a15c00);--dc-bad:var(--dsw-alias-state-error-primary,#c62828);--dc-mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;container-type:inline-size;display:flex;flex-direction:column;gap:14px;width:100%;max-width:720px;color:var(--dc-fg);font-size:13px;line-height:20px}
|
|
3143
3632
|
.dsh-channels-cfg__panelHead{position:sticky;top:0;z-index:2;display:flex;flex-direction:column;gap:4px;padding:10px 0 6px;background:var(--dc-bg)}
|
|
3144
3633
|
.dsh-channels-cfg__headRow{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
3145
|
-
.dsh-channels-cfg__title{margin:0 2px 0 0;font-size:
|
|
3634
|
+
.dsh-channels-cfg__title{margin:0 2px 0 0;font-size:20px;font-weight:500;line-height:28px}
|
|
3146
3635
|
.dsh-channels-cfg__meta{color:var(--dc-fg3);font-size:12px}
|
|
3147
3636
|
.dsh-channels-cfg__spacer{flex:1}
|
|
3148
3637
|
.dsh-channels-cfg__mono{font-family:var(--dc-mono);font-size:12px}
|
|
@@ -3233,7 +3722,7 @@ window.__ModuleLoader__.load({
|
|
|
3233
3722
|
.dsh-channels-cfg__scopeList li{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}
|
|
3234
3723
|
.dsh-channels-cfg__scopeList .dsh-channels-cfg__link{font-size:12px}
|
|
3235
3724
|
.dsh-channels-cfg__list>.dsh-channels-cfg__scopeList{border-top-width:0}
|
|
3236
|
-
.dsh-channels-
|
|
3725
|
+
.dsh-channels-avatar{box-sizing:border-box;display:grid;place-items:center;flex:none;border-radius:50%;object-fit:cover;background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.05));color:var(--dsw-alias-label-secondary,#5c6169);line-height:1}
|
|
3237
3726
|
.dsh-channels-cfg__addRow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:10px 14px}
|
|
3238
3727
|
.dsh-channels-cfg__addRow .dsh-channels-cfg__input{flex:1 1 160px;width:auto}
|
|
3239
3728
|
.dsh-channels-cfg__emptyRow{padding:11px 14px;color:var(--dc-fg3);font-size:12px}
|
|
@@ -3246,6 +3735,26 @@ window.__ModuleLoader__.load({
|
|
|
3246
3735
|
.dsh-channels-cfg__stepTitle{font-weight:500}
|
|
3247
3736
|
.dsh-channels-cfg__error{padding:10px 12px;border:1px solid var(--dc-bad);border-radius:10px;color:var(--dc-bad);font-size:12px;line-height:18px;display:flex;flex-direction:column;gap:8px}
|
|
3248
3737
|
.dsh-channels-cfg__loading{padding:16px 0;color:var(--dc-fg3);font-size:12px}
|
|
3738
|
+
.dsh-channels-origin-chip{display:inline-flex;align-items:center;gap:4px;max-width:180px;height:24px;padding:0 8px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary,#5c6169);font:inherit;font-size:12px;line-height:24px;cursor:pointer;white-space:nowrap}
|
|
3739
|
+
.dsh-channels-origin-chip:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.05));color:var(--dsw-alias-label-primary,#1f2328)}
|
|
3740
|
+
.dsh-channels-origin-chip>svg{flex:none}
|
|
3741
|
+
.dsh-channels-origin__who{display:inline-flex;align-items:center;gap:6px;vertical-align:middle}
|
|
3742
|
+
.dsh-channels-origin-chip__text{overflow:hidden;text-overflow:ellipsis}
|
|
3743
|
+
.dsh-channels-origin{box-sizing:border-box;height:100%;overflow:auto;padding:16px;color:var(--dsw-alias-label-primary,#1f2328);font-size:13px;line-height:20px}
|
|
3744
|
+
.dsh-channels-origin__head{display:flex;align-items:center;gap:8px;margin-bottom:12px}
|
|
3745
|
+
.dsh-channels-origin__title{font-size:15px;font-weight:600;line-height:22px;overflow-wrap:anywhere}
|
|
3746
|
+
.dsh-channels-origin__list{display:flex;flex-direction:column;margin:0;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));border-radius:10px}
|
|
3747
|
+
.dsh-channels-origin__row{display:flex;gap:12px;padding:9px 12px}
|
|
3748
|
+
.dsh-channels-origin__row+.dsh-channels-origin__row{border-top:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12))}
|
|
3749
|
+
.dsh-channels-origin__row dt{flex:none;width:64px;color:var(--dsw-alias-label-tertiary,#8a8f98)}
|
|
3750
|
+
.dsh-channels-origin__row dd{flex:1;min-width:0;margin:0;overflow-wrap:anywhere}
|
|
3751
|
+
.dsh-channels-origin__mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px}
|
|
3752
|
+
.dsh-channels-origin__muted{color:var(--dsw-alias-label-tertiary,#8a8f98)}
|
|
3753
|
+
.dsh-channels-origin__error{display:flex;flex-direction:column;gap:6px;color:var(--dsw-alias-state-error-primary,#c62828);font-size:12px}
|
|
3754
|
+
.dsh-channels-origin__actions{display:flex;align-items:center;gap:14px;margin-top:14px}
|
|
3755
|
+
.dsh-channels-origin__button{display:inline-flex;align-items:center;height:28px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));border-radius:7px;color:inherit;font-size:13px;text-decoration:none}
|
|
3756
|
+
.dsh-channels-origin__button:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.05))}
|
|
3757
|
+
.dsh-channels-origin__link{padding:0;border:0;background:none;color:var(--dsw-alias-state-business-primary,#0a67d3);font:inherit;font-size:13px;cursor:pointer}
|
|
3249
3758
|
@media (prefers-reduced-motion:reduce){.dsh-channels-cfg__switch,.dsh-channels-cfg__switch::after{transition:none}}
|
|
3250
3759
|
@container (max-width:560px){
|
|
3251
3760
|
.dsh-channels-cfg__row:not(.dsh-channels-cfg__row--wide){flex-wrap:wrap}
|
|
@@ -3267,11 +3776,13 @@ window.__ModuleLoader__.load({
|
|
|
3267
3776
|
//#endregion
|
|
3268
3777
|
//#region src/client/index.ts
|
|
3269
3778
|
/**
|
|
3270
|
-
* dsh-channels client half:
|
|
3779
|
+
* dsh-channels client half: 左侧栏全局入口「飞书渠道」+ 主区整页设置面板,
|
|
3780
|
+
* 以及对话内的「飞书来源」(会话头部入口 + 右侧栏标签页)。
|
|
3271
3781
|
*
|
|
3272
3782
|
* 只依赖平台核心的 `slots` 服务与 react(bundle 里 react 走平台模块表,
|
|
3273
|
-
*
|
|
3274
|
-
*
|
|
3783
|
+
* 其余全部内联);右侧栏服务存在时才注册来源标签页。页面数据来自 Host
|
|
3784
|
+
* 半侧的 `/dsh-channels/*` 路由,因此不需要 typert 契约,也不 import 任何
|
|
3785
|
+
* DSH 客户端包。
|
|
3275
3786
|
*
|
|
3276
3787
|
* @module dsh-channels/client
|
|
3277
3788
|
*/
|
|
@@ -3279,28 +3790,67 @@ window.__ModuleLoader__.load({
|
|
|
3279
3790
|
const name = "dsh-channels";
|
|
3280
3791
|
/** Required client services: the Slots registry is part of the web shell core. */
|
|
3281
3792
|
const inject = ["slots"];
|
|
3282
|
-
/** Section id and nav order (built-ins: account -10, general 0, models 10, plugins 15, agent-presets 20). */
|
|
3283
|
-
const SETTINGS_SECTION_ID = "feishu-channels";
|
|
3284
|
-
const SETTINGS_SECTION_ORDER = 40;
|
|
3285
3793
|
/**
|
|
3286
|
-
*
|
|
3794
|
+
* Main panel id: the `sidebar.panellist` row id and the matching `main` slot key.
|
|
3795
|
+
* Must not change once released: DSH keys navigation state by it.
|
|
3796
|
+
*/
|
|
3797
|
+
const PANEL_ID = "feishu-channels";
|
|
3798
|
+
/** Left-sidebar row order (built-ins: tasks 10; Plugins is pinned last by the sidebar). */
|
|
3799
|
+
const PANEL_ORDER = 40;
|
|
3800
|
+
/** Session header action order (after the preset label -10 and subagents 10). */
|
|
3801
|
+
const ORIGIN_ACTION_ORDER = 30;
|
|
3802
|
+
/**
|
|
3803
|
+
* Mount the left-sidebar entry, its main-column page, the stylesheet, and —
|
|
3804
|
+
* while the right sidebar is present — the Feishu origin header entry and tab.
|
|
3287
3805
|
* @param ctx - client plugin context carrying the slots registry.
|
|
3288
3806
|
*/
|
|
3289
3807
|
function apply(ctx) {
|
|
3290
|
-
if (typeof ctx.effect === "function") ctx.effect(() => installStyles(), "dsh-channels:
|
|
3808
|
+
if (typeof ctx.effect === "function") ctx.effect(() => installStyles(), "dsh-channels: panel styles");
|
|
3291
3809
|
else installStyles();
|
|
3292
|
-
ctx.slots.inject("
|
|
3293
|
-
name: "
|
|
3294
|
-
|
|
3810
|
+
ctx.slots.inject("main", () => ctx.slots.register({
|
|
3811
|
+
name: "main",
|
|
3812
|
+
key: PANEL_ID
|
|
3813
|
+
}, FeishuChannelsPage));
|
|
3814
|
+
ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({
|
|
3815
|
+
name: "sidebar.panellist",
|
|
3816
|
+
id: PANEL_ID,
|
|
3295
3817
|
order: 40,
|
|
3296
3818
|
label: () => PANEL_COPY.title
|
|
3297
|
-
},
|
|
3819
|
+
}, FeishuChannelsIcon));
|
|
3820
|
+
ctx.inject?.(["sidebarRightTabs", "sidebarRight"], mountOrigin);
|
|
3821
|
+
}
|
|
3822
|
+
/**
|
|
3823
|
+
* Register the origin tab type, its body and title, and the header entry that
|
|
3824
|
+
* opens it. Everything lives as long as the right-sidebar services do.
|
|
3825
|
+
* @param scope - child context holding the right-sidebar services.
|
|
3826
|
+
*/
|
|
3827
|
+
function mountOrigin(scope) {
|
|
3828
|
+
const definition = {
|
|
3829
|
+
id: ORIGIN_TAB_ID,
|
|
3830
|
+
kind: ORIGIN_TAB_KIND,
|
|
3831
|
+
title: () => ORIGIN_TAB_TITLE
|
|
3832
|
+
};
|
|
3833
|
+
scope.effect?.(() => scope.sidebarRightTabs.register(definition), "dsh-channels: origin tab type");
|
|
3834
|
+
scope.slots.inject("sidebar.right.pane.tab", () => scope.slots.register({
|
|
3835
|
+
name: "sidebar.right.pane.tab",
|
|
3836
|
+
key: ORIGIN_TAB_ID
|
|
3837
|
+
}, OriginTab));
|
|
3838
|
+
scope.slots.inject("conversation.session.header.actions", () => scope.slots.register({
|
|
3839
|
+
name: "conversation.session.header.actions",
|
|
3840
|
+
id: "dsh-channels-origin",
|
|
3841
|
+
order: 30,
|
|
3842
|
+
inject: () => ({ openOrigin: () => {
|
|
3843
|
+
scope.sidebarRight.openTab(ORIGIN_TAB_KIND);
|
|
3844
|
+
} })
|
|
3845
|
+
}, OriginHeaderAction));
|
|
3298
3846
|
}
|
|
3299
3847
|
//#endregion
|
|
3300
|
-
exports.
|
|
3301
|
-
exports.
|
|
3848
|
+
exports.ORIGIN_ACTION_ORDER = ORIGIN_ACTION_ORDER;
|
|
3849
|
+
exports.PANEL_ID = PANEL_ID;
|
|
3850
|
+
exports.PANEL_ORDER = PANEL_ORDER;
|
|
3302
3851
|
exports.apply = apply;
|
|
3303
3852
|
exports.inject = inject;
|
|
3853
|
+
exports.mountOrigin = mountOrigin;
|
|
3304
3854
|
exports.name = name;
|
|
3305
3855
|
return module.exports;
|
|
3306
3856
|
}
|